Skip to content

Commit d61116c

Browse files
committed
suport bash's ANSI-C quoted strings
1 parent 6c60415 commit d61116c

File tree

2 files changed

+88
-5
lines changed

2 files changed

+88
-5
lines changed

lib/yargs-parser.ts

+61-5
Original file line numberDiff line numberDiff line change
@@ -600,11 +600,14 @@ export class YargsParser {
600600

601601
function processValue (key: string, val: any) {
602602
// strings may be quoted, clean this up as we assign values.
603-
if (typeof val === 'string' &&
604-
(val[0] === "'" || val[0] === '"') &&
605-
val[val.length - 1] === val[0]
606-
) {
607-
val = val.substring(1, val.length - 1)
603+
if (typeof val === 'string') {
604+
if ((val[0] === "'" || val[0] === '"') &&
605+
val[val.length - 1] === val[0]
606+
) {
607+
val = val.substring(1, val.length - 1)
608+
} else if (val.slice(0, 2) === "$'" && val[val.length - 1] === "'") {
609+
val = parseAnsiCQuote(val)
610+
}
608611
}
609612

610613
// handle parsing boolean arguments --foo=true --bar false.
@@ -629,6 +632,59 @@ export class YargsParser {
629632
return value
630633
}
631634

635+
// ANSI-C quoted string are a bash-only feature and have the form $'some text'
636+
// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
637+
function parseAnsiCQuote (str: string): string {
638+
function unescapeChar (x: string): string {
639+
switch (x.slice(0, 2)) {
640+
case '\\\\':
641+
return '\\'
642+
case '\\a':
643+
return '\a' // eslint-disable-line
644+
case '\\b':
645+
return '\b'
646+
case '\\e':
647+
return '\u001b'
648+
case '\\E':
649+
return '\u001b'
650+
case '\\f':
651+
return '\f'
652+
case '\\n':
653+
return '\n'
654+
case '\\r':
655+
return '\r'
656+
case '\\t':
657+
return '\t'
658+
case '\\v':
659+
return '\v'
660+
case "\\'":
661+
return "'"
662+
case '\\"':
663+
return '"'
664+
case '\\?':
665+
return '?'
666+
case '\\c':
667+
// Control codes
668+
// "\c1" -> 11, "\c2" -> 12 and so on
669+
if (x.match(/\\c[0-9]/)) {
670+
return String.fromCharCode(parseInt(x.slice(2), 10) + 16)
671+
}
672+
// "\ca" -> 01, "\cb" -> 02 and so on
673+
return String.fromCharCode(x.toLowerCase().charCodeAt(2) - 'a'.charCodeAt(0) + 1)
674+
case '\\x':
675+
case '\\u':
676+
case '\\U':
677+
// Hexadecimal character literal
678+
return String.fromCharCode(parseInt(x.slice(2), 16))
679+
}
680+
// Octal character literal
681+
return String.fromCharCode(parseInt(x.slice(1), 8))
682+
}
683+
684+
const ANSI_BACKSLASHES = /\\(\\|a|b|e|E|f|n|r|t|v|'|"|\?|[0-7]{1,3}|x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|c[0-9a-zA-Z])/g
685+
return str.slice(2, -1).replace(ANSI_BACKSLASHES, unescapeChar)
686+
}
687+
632688
function maybeCoerceNumber (key: string, value: string | number | null | undefined) {
633689
if (!configuration['parse-positional-numbers'] && key === '_') return value
634690
if (!checkAllAliases(key, flags.strings) && !checkAllAliases(key, flags.bools) && !Array.isArray(value)) {

test/yargs-parser.cjs

+27
Original file line numberDiff line numberDiff line change
@@ -3587,6 +3587,33 @@ describe('yargs-parser', function () {
35873587
args.foo.should.equal('-hello world')
35883588
args.bar.should.equal('--goodnight moon')
35893589
})
3590+
3591+
it('handles bash ANSI-C quoted strings', () => {
3592+
const args = parser("--foo $'text with \\n newline'")
3593+
args.foo.should.equal('text with \n newline')
3594+
3595+
// Double quotes shouldn't work
3596+
const args2 = parser('--foo $"text without \\n newline"')
3597+
args2.foo.should.equal('$"text without \\n newline"')
3598+
3599+
const characters = '\\\\' + '\\a' + '\\b' + '\\e' + '\\E' + '\\f' + '\\n' + '\\r' + '\\t' + '\\v' + "\\'" + '\\"' + '\\?'
3600+
const args3 = parser("--foo $'" + characters + "'")
3601+
args3.foo.should.equal('\\\a\b\u001b\u001b\f\n\r\t\v\'"?') // eslint-disable-line
3602+
3603+
const args4 = parser("--foo $'text \\xFFFF with \\xFF hex'")
3604+
args4.foo.should.equal('text \u00FFFF with \u00FF hex')
3605+
const args5 = parser("--foo $'text \\uFFFFFF\\uFFFF with \\uFF hex'")
3606+
args5.foo.should.equal('text \uFFFFFF\uFFFF with \u00FF hex')
3607+
const args6 = parser("--foo $'text \\UFFFFFF\\UFFFF with \\U00FF hex'")
3608+
const longCodePoint = String.fromCharCode(parseInt('FFFFFF', 16))
3609+
args6.foo.should.equal(`text ${longCodePoint}\uFFFF with \u00FF hex`)
3610+
3611+
const args7 = parser("--foo $'text \\cAB \\cz with \\c12 control \\c011 chars'")
3612+
args7.foo.should.equal('text \u0001B \u001A with \u00112 control \u001011 chars')
3613+
3614+
const args8 = parser("--foo $'text \\0 \\001 with \\12 \\123 \\129 octal'")
3615+
args8.foo.should.equal('text \u0000 \u0001 with \u000A \u0053 \u000A9 octal')
3616+
})
35903617
})
35913618

35923619
// see: https://github.com/yargs/yargs-parser/issues/144

0 commit comments

Comments
 (0)