argv-split
Version:
Split argv(argument vector) and handle special cases, such as quoted values.
249 lines (194 loc) • 4.43 kB
JavaScript
// Flags Characters
// 0 1 2 3 4 5
// ------------------------------------------------------------------------
// \ ' " normal space \n
// e,sq n/a n/a n/a n/a n/a n/a
// 0 ue,sq a \ suq a " a + a _ EOF
// 1 e,dq a \,ue a \',ue a ",ue a \+,ue a \_,ue ue
// 2 ue,dq e a ' duq a + a _ EOF
// 3 e,uq a \,ue a \',ue a \",ue a \+,ue a _,ue ue
// 4 ue,uq e sq dq a + tp EOF
const MATRIX = {
// object is more readable than multi-dim array.
0: [a, suq, a, a, a, EOF],
1: [eaue, aue, eaue, aue, aue, ue],
2: [e, a, duq, a, a, EOF],
3: [eaue, aue, aue, aue, eaue, ue],
4: [e, sq, dq, a, tp, EOF]
}
// - a: add
// - e: turn on escape mode
// - ue: turn off escape mode
// - q: turn on quote mode
// - sq: single quoted
// - dq: double quoted
// - uq: turn off quote mode
// - tp: try to push if there is something in the stash
// - EOF: end of file(input)
let escaped = false // 1
let single_quoted = false // 2
let double_quoted = false // 4
let ended = false
const FLAGS = {
2: 0,
5: 1,
4: 2,
1: 3,
0: 4
}
function y () {
let sum = 0
if (escaped) {
sum ++
}
if (single_quoted) {
sum += 2
}
if (double_quoted) {
sum += 4
}
return FLAGS[sum]
}
const BACK_SLASH = '\\'
const SINGLE_QUOTE = "'"
const DOUBLE_QUOTE = '"'
const WHITE_SPACE = ' '
const LINE_FEED = '\n'
function x () {
return c in CHARS
? CHARS[c]
: CHARS.NORMAL
}
const CHARS = {
[BACK_SLASH]: 0,
[SINGLE_QUOTE]: 1,
[DOUBLE_QUOTE]: 2,
NORMAL: 3,
[WHITE_SPACE]: 4,
[LINE_FEED]: 5
}
let c = ''
let stash = ''
let ret = []
function reset () {
escaped = false
single_quoted = false
double_quoted = false
ended = false
c = ''
stash = ''
ret.length = 0
}
function a () {
stash += c
}
function sq () {
single_quoted = true
}
function suq () {
single_quoted = false
}
function dq () {
double_quoted = true
}
function duq () {
double_quoted = false
}
function e () {
escaped = true
}
function ue () {
escaped = false
}
// add a backslash and a normal char, and turn off escaping
function aue () {
stash += BACK_SLASH + c
escaped = false
}
// add a escaped char and turn off escaping
function eaue () {
stash += c
escaped = false
}
// try to push
function tp () {
if (stash) {
ret.push(stash)
stash = ''
}
}
function EOF () {
ended = true
}
function split (str) {
if (typeof str !== 'string') {
type_error(`\`str\` must be a string. Received ${str}`, 'NON_STRING')
}
reset()
const length = str.length
let i = -1
while (++ i < length) {
c = str[i]
MATRIX[y()][x()]()
if (ended) {
break
}
}
if (single_quoted) {
error('unmatched single quote', 'UNMATCHED_SINGLE')
}
if (double_quoted) {
error('unmatched double quote', 'UNMATCHED_DOUBLE')
}
if (escaped) {
error('unexpected end with \\', 'ESCAPED_EOF')
}
tp()
return ret
}
function error (message, code) {
const err = new Error(message)
err.code = code
throw err
}
function type_error (message, code) {
const err = new TypeError(message)
err.code = code
throw err
}
const REGEX_NEED_QUOTE = /\s|"|'/
const CLI_LINE_FEED = '\\\n'
const LF = Symbol.for('argv-split:LF')
function join(args, {
quote = DOUBLE_QUOTE
} = {}) {
if (![SINGLE_QUOTE, DOUBLE_QUOTE].includes(quote)) {
type_error(
`\`options.quote\` must be either ${SINGLE_QUOTE} or ${DOUBLE_QUOTE}. Received ${quote}` , 'INVALID_QUOTE'
)
}
const process_arg = arg => {
if (!REGEX_NEED_QUOTE.test(arg)) {
return arg
}
if (!arg.includes(quote)) {
return quote + arg + quote
}
// escape quote
const escaped = arg.replace(new RegExp(quote, 'g'), `\\${quote}`)
return quote + escaped + quote
}
let joined = ''
for (const arg of args) {
if (arg === LF) {
joined += CLI_LINE_FEED
continue
}
joined += process_arg(arg) + WHITE_SPACE
}
return joined.trimRight()
}
module.exports = split
split.join = join
split.LF = LF