Skip to content

Commit

Permalink
suport bash's ANSI-C quoted strings
Browse files Browse the repository at this point in the history
  • Loading branch information
verhovsky committed Mar 15, 2021
1 parent 6c60415 commit c1420f7
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 5 deletions.
66 changes: 61 additions & 5 deletions lib/yargs-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)) {
Expand Down
27 changes: 27 additions & 0 deletions test/yargs-parser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit c1420f7

Please sign in to comment.