Skip to content

Instantly share code, notes, and snippets.

@x-yuri
Last active July 16, 2024 05:34
Show Gist options
  • Save x-yuri/9e3fb8d41d805bf01e4a222880c74eb9 to your computer and use it in GitHub Desktop.
Save x-yuri/9e3fb8d41d805bf01e4a222880c74eb9 to your computer and use it in GitHub Desktop.
Splitting a command line into arguments

Splitting a command line into arguments

a.mjs:

// ", ', \ and (space) can be escaped with \
// \ before other symbols is removed
// inside double quotes " and \ can be escaped with \
// \ before other symbols remains
export default function splitCommandLine(l) {
    const r = [];
    loop: while (l.length) {
        l = l.replace(/^ +/, '');
        let i = 0;
        loop2: while (true) {
            i = i + l.slice(i).search(/"|'| |\\|$/);
            const c = l.charAt(i);
            switch (c) {
                case '"':
                case "'":
                    let i2 = 0;
                    if (c == "'") {
                        i2 = l.slice(i + 1).indexOf(c);
                        if (i2 == -1) throw new Error("unterminated '");
                    } else {
                        loop3: while (true) {
                            const r = l.slice(i + 1 + i2).search(/"|\\/);
                            if (r == -1) throw new Error('unterminated "');
                            i2 = i2 + r;
                            const c2 = l.charAt(i + 1 + i2);
                            switch (c2) {
                                case '"': break loop3;
                                case '\\':
                                    if (['"', '\\'].includes(l.charAt(i + 1 + i2 + 1)))
                                        l = l.slice(0, i + 1 + i2)
                                          + l.slice(i + 1 + i2 + 1);
                                    i2++;
                                    break;
                            }
                        }
                    }
                    l = l.slice(0, i)
                      + l.slice(i + 1, i + 1 + i2)
                      + l.slice(i + 1 + i2 + 1);
                    i = i + i2;
                    break;
                case '\\':
                    l = l.slice(0, i) + l.slice(i + 1);
                    i++;
                    break;
                default:
                    break loop2;
            }
        }
        if (i > 0)
            r.push(l.slice(0, i));
        l = l.slice(i);
    }
    return r;
}

a.test.mjs:

import t from 'tap'
import splitCommandLine from './a.mjs'

t.matchOnly(splitCommandLine(''), []);
t.matchOnly(splitCommandLine('a'), ['a']);
t.matchOnly(splitCommandLine('a b'), ['a', 'b']);
t.matchOnly(splitCommandLine('\\ '), [' ']);
t.matchOnly(splitCommandLine("\\'"), ["'"]);
t.matchOnly(splitCommandLine('\\"'), ['"']);
t.matchOnly(splitCommandLine('\\.'), ['.']);
t.matchOnly(splitCommandLine('"a"'), ['a']);
t.matchOnly(splitCommandLine("'a'"), ['a']);
t.matchOnly(splitCommandLine("'\\ '"), ['\\ ']);
t.matchOnly(splitCommandLine('"\\ "'), ['\\ ']);
t.matchOnly(splitCommandLine('"\\\\"'), ['\\']);
t.matchOnly(splitCommandLine('"\\""'), ['"']);
t.throws(() => { splitCommandLine('"') });
t.throws(() => { splitCommandLine("'") });
$ docker run --rm -itv "$PWD:/app" -w /app alpine:3.20
/app # apk add nodejs npm
/app # npm i tap
/app # npx tap
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment