From a16e57fedc1a2bcc09cdb5bbf86ea37b4c0cba1d Mon Sep 17 00:00:00 2001 From: Boris Verkhovskiy Date: Mon, 15 Mar 2021 08:32:59 -0400 Subject: [PATCH] suport bash's ANSI-C quoted strings --- lib/yargs-parser.ts | 67 +++++++++++++++++++++++++++++++++++++++---- test/yargs-parser.cjs | 28 ++++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/lib/yargs-parser.ts b/lib/yargs-parser.ts index b8bd8775..1d3ee9d1 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,60 @@ 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'; + 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 + const code = x.slice(2); + // "\c1" -> 11, "\c2" -> 12 and so on + if (code.match(/[0-9]/)) { + return String.fromCharCode(parseInt(code, 10) + 16); + } + // "\ca" -> 01, "\cb" -> 02 and so on + return String.fromCharCode(code.toLowerCase().charCodeAt(0) - '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)); + } + + 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..5cda9229 100644 --- a/test/yargs-parser.cjs +++ b/test/yargs-parser.cjs @@ -3587,6 +3587,34 @@ 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\'"?') + + 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