diff --git a/lib/yargs-parser.ts b/lib/yargs-parser.ts index b8bd8775..727ddcfa 100644 --- a/lib/yargs-parser.ts +++ b/lib/yargs-parser.ts @@ -600,11 +600,14 @@ export class YargsParser { function processValue (key: string, val: any) { // strings may be quoted, clean this up as we assign values. - if (typeof val === 'string' && - (val[0] === "'" || val[0] === '"') && - val[val.length - 1] === val[0] - ) { - val = val.substring(1, val.length - 1) + if (typeof val === 'string') { + if ((val[0] === "'" || val[0] === '"') && + val[val.length - 1] === val[0] + ) { + val = val.substring(1, val.length - 1) + } else if (val.slice(0, 2) === "$'" && val[val.length - 1] === "'") { + val = parseAnsiCQuote(val) + } } // handle parsing boolean arguments --foo=true --bar false. @@ -629,6 +632,59 @@ export class YargsParser { return value } + // ANSI-C quoted string are a bash-only feature and have the form $'some text' + // https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html + function parseAnsiCQuote (str: string): string { + function unescapeChar (x: string): string { + switch (x.slice(0, 2)) { + case '\\\\': + return '\\' + case '\\a': + return '\a' // eslint-disable-line + case '\\b': + return '\b' + case '\\e': + return '\u001b' + case '\\E': + return '\u001b' + case '\\f': + return '\f' + case '\\n': + return '\n' + case '\\r': + return '\r' + case '\\t': + return '\t' + case '\\v': + return '\v' + case "\\'": + return "'" + case '\\"': + return '"' + case '\\?': + return '?' + case '\\c': + // Control codes + // "\c1" -> 11, "\c2" -> 12 and so on + if (x.match(/\\c[0-9]/)) { + return String.fromCharCode(parseInt(x.slice(2), 10) + 16) + } + // "\ca" -> 01, "\cb" -> 02 and so on + return String.fromCharCode(x.toLowerCase().charCodeAt(2) - 'a'.charCodeAt(0) + 1) + case '\\x': + case '\\u': + case '\\U': + // Hexadecimal character literal + return String.fromCharCode(parseInt(x.slice(2), 16)) + } + // Octal character literal + return String.fromCharCode(parseInt(x.slice(1), 8) % 256) + } + + 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 + return str.slice(2, -1).replace(ANSI_BACKSLASHES, unescapeChar) + } + function maybeCoerceNumber (key: string, value: string | number | null | undefined) { if (!configuration['parse-positional-numbers'] && key === '_') return value if (!checkAllAliases(key, flags.strings) && !checkAllAliases(key, flags.bools) && !Array.isArray(value)) { diff --git a/test/yargs-parser.cjs b/test/yargs-parser.cjs index f45256ed..a10dd6bb 100644 --- a/test/yargs-parser.cjs +++ b/test/yargs-parser.cjs @@ -3587,6 +3587,33 @@ describe('yargs-parser', function () { args.foo.should.equal('-hello world') args.bar.should.equal('--goodnight moon') }) + + it('handles bash ANSI-C quoted strings', () => { + const args = parser("--foo $'text with \\n newline'") + args.foo.should.equal('text with \n newline') + + // Double quotes shouldn't work + const args2 = parser('--foo $"text without \\n newline"') + args2.foo.should.equal('$"text without \\n newline"') + + const characters = '\\\\' + '\\a' + '\\b' + '\\e' + '\\E' + '\\f' + '\\n' + '\\r' + '\\t' + '\\v' + "\\'" + '\\"' + '\\?' + const args3 = parser("--foo $'" + characters + "'") + args3.foo.should.equal('\\\a\b\u001b\u001b\f\n\r\t\v\'"?') // eslint-disable-line + + const args4 = parser("--foo $'text \\xFFFF with \\xFF hex'") + args4.foo.should.equal('text \u00FFFF with \u00FF hex') + const args5 = parser("--foo $'text \\uFFFFFF\\uFFFF with \\uFF hex'") + args5.foo.should.equal('text \uFFFFFF\uFFFF with \u00FF hex') + const args6 = parser("--foo $'text \\UFFFFFF\\UFFFF with \\U00FF hex'") + const longCodePoint = String.fromCharCode(parseInt('FFFFFF', 16)) + args6.foo.should.equal(`text ${longCodePoint}\uFFFF with \u00FF hex`) + + const args7 = parser("--foo $'text \\cAB \\cz with \\c12 control \\c011 chars'") + args7.foo.should.equal('text \u0001B \u001A with \u00112 control \u001011 chars') + + const args8 = parser("--foo $'text \\0 \\001 with \\12 \\123 \\129 octal'") + args8.foo.should.equal('text \u0000 \u0001 with \u000A \u0053 \u000A9 octal') + }) }) // see: https://github.com/yargs/yargs-parser/issues/144