Skip to content

Commit

Permalink
Add logic for extracting options from string input
Browse files Browse the repository at this point in the history
Passing a string to getOpts() had the effect of splitting each character
and returning them as separate arguments. Now the function parses string
input using simplistic shell-like parsing.
  • Loading branch information
Alhadis committed Oct 22, 2018
1 parent d7f005f commit bf7a188
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 1 deletion.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ be found by digging through the commit logs.
This project honours [Semantic Versioning](http://semver.org/).


[Staged]
------------------------------------------------------------------------
* __Added:__ Ability to extract option-lists from strings
* __Fixed:__ Options array being modified by reference


[v1.1.3]
------------------------------------------------------------------------
**October 21st, 2018**
Expand Down Expand Up @@ -67,6 +73,7 @@ Initial release.


[Referenced links]:_____________________________________________________
[Staged]: https://github.com/Alhadis/GetOptions/compare/v1.1.3...HEAD
[v1.1.3]: https://github.com/Alhadis/GetOptions/releases/tag/v1.1.3
[v1.1.2]: https://github.com/Alhadis/GetOptions/releases/tag/v1.1.2
[v1.1.1]: https://github.com/Alhadis/GetOptions/releases/tag/v1.1.1
Expand Down
72 changes: 71 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,67 @@ function uniqueStrings(input){
}


/**
* Parse a string as a whitespace-delimited list of options,
* preserving quoted and escaped characters.
*
* @example unstringify("--foo --bar") => ["--foo", "--bar"];
* @example unstringify('--foo "bar baz"') => ["--foo", '"bar baz"'];
* @param {String} input
* @return {Object}
* @internal
*/
function unstringify(input){
input = String(input || "");
const tokens = [];
const {length} = input;

let quoteChar = ""; // Quote-type enclosing current region
let tokenData = ""; // Characters currently being collected
let isEscaped = false; // Flag identifying an escape sequence

for(let i = 0; i < length; ++i){
const char = input[i];

// Previous character was a backslash
if(isEscaped){
tokenData += char;
isEscaped = false;
continue;
}

// Whitespace: terminate token unless quoted
if(!quoteChar && /[ \t\n]/.test(char)){
tokenData && tokens.push(tokenData);
tokenData = "";
continue;
}

// Backslash: escape next character
if("\\" === char){
isEscaped = true;

// Swallow backslash if it escapes a metacharacter
const next = input[i + 1];
if(quoteChar && (quoteChar === next || "\\" === next)
|| !quoteChar && /[- \t\n\\'"`]/.test(next))
continue;
}

// Quote marks
else if((!quoteChar || char === quoteChar) && /['"`]/.test(char)){
quoteChar = quoteChar === char ? "" : char;
continue;
}

tokenData += char;
}
if(tokenData)
tokens.push(tokenData);
return tokens;
}


/**
* Parse input using "best guess" logic. Called when no optdef is passed.
*
Expand Down Expand Up @@ -264,7 +325,7 @@ function autoOpts(input, config = {}){
/**
* Extract command-line options from a list of strings.
*
* @param {Array} input
* @param {String|Array} input
* @param {String|Object} [optdef=null]
* @param {Object} [config={}]
*/
Expand All @@ -274,6 +335,15 @@ function getOpts(input, optdef = null, config = {}){
if(!input || 0 === input.length)
return {options: {}, argv: []};

// Avoid modifying original array
if(Array.isArray(input))
input = [...input].map(String);

// If called with a string, break it apart into an array
else if("string" === typeof input)
input = unstringify(input);


// Take a different approach if optdefs aren't specified
if(null === optdef || "" === optdef || false === optdef)
return autoOpts(input, config);
Expand Down
162 changes: 162 additions & 0 deletions test/string-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"use strict";

const getOpts = require("../index.js");
const {assert} = require("chai");


suite("String input", () => {
const expect = (input, optdef, output) => {
const msg = `Parsing ${input}`;
assert.deepEqual(getOpts(input, optdef), output, msg);
};

test("Basic value: Single", () =>
expect("--foo", {"--foo": ""}, {
argv: [],
options: {foo: true},
}));

test("Basic value: Multiple", () => {
expect("--foo bar", {"--foo": ""}, {
argv: ["bar"],
options: {foo: true},
});
expect("--foo bar", {"--foo": "string"}, {
argv: [],
options: {foo: "bar"},
});
expect("--foo --bar baz", {
"--foo": "string",
"--bar": "string",
}, {
argv: [],
options: {
foo: undefined,
bar: "baz",
},
});
expect("--foo baz --bar qux", {
"--foo": "string",
"--bar": "string",
}, {
argv: [],
options: {
foo: "baz",
bar: "qux",
},
});
});


test("Quotes: Basic usage", () => {
expect('"--foo" "--bar"', {"--foo": "", "--bar": ""}, {
argv: [],
options: {
foo: true,
bar: true,
},
});
expect('"--foo --bar"', {"--foo": "", "--bar": ""}, {
argv: ["--foo --bar"],
options: {},
});
expect('--foo "bar baz"', {"--foo": "string"}, {
argv: [],
options: {foo: "bar baz"},
});
});


test("Quotes: Nested", () => {
expect("--foo \"--bar='qux'\"", {"--foo": ""}, {
argv: ["--bar='qux'"],
options: {foo: true},
});
expect("--foo baz --bar='qux \" \" qul' 123", {
"--foo": "string",
"--bar": "string",
}, {
argv: ["123"],
options: {
foo: "baz",
bar: 'qux " " qul',
},
});
});


test("Escapes: Delimiters", () => {
expect("--foo bar\\ baz", {"--foo": "string"}, {
argv: [],
options: {foo: "bar baz"},
});
expect("--foo bar\\ baz", {"--foo": ""}, {
argv: ["bar baz"],
options: {foo: true},
});
});


test("Escapes: Consecutive", () => {
expect("--foo bar\\ \\ baz", {"--foo": "string"}, {
argv: [],
options: {foo: "bar baz"},
});
expect("--foo bar\\ \\ baz", {"--foo": ""}, {
argv: ["bar baz"],
options: {foo: true},
});
});


test("Escapes: Bare quotes", () => {
expect("--foo bar\\'s baz", {"--foo": "string"}, {
argv: ["baz"],
options: {foo: "bar's"},
});
expect('--foo bar\\"s baz', {"--foo": "string"}, {
argv: ["baz"],
options: {foo: 'bar"s'},
});
expect('--title="Foo \\"Bar\\" Baz"', {"--title": "string"}, {
argv: [],
options: {title: 'Foo "Bar" Baz'},
});
});


test("Escapes: Between quotes", () => {
expect("--title='Foo\\'s Bar' bar", {"--title": "string"}, {
argv: ["bar"],
options: {title: "Foo's Bar"},
});
expect("--title='Foo \\\"Bar\\\" Baz'", {"--title": "string"}, {
argv: [],
options: {title: 'Foo \\"Bar\\" Baz'},
});
expect("--title '\\\"bar\\\"'", {"--title": "string"}, {
argv: [],
options: {title: '\\"bar\\"'},
});
expect('--title "\\\'bar\\\'"', {"--title": "string"}, {
argv: [],
options: {title: "\\'bar\\'"},
});
});


test("Escapes: Dashes", () => {
expect("bar\\-baz", {"-b, --baz": ""}, {argv: ["bar-baz"], options: {}});
expect('"bar\\-baz"', {"-b, --baz": ""}, {argv: ["bar\\-baz"], options: {}});
expect("\\--foo", {"--foo": ""}, {argv: [], options: {foo: true}});
expect("\\ --foo", {"--foo": ""}, {argv: [" --foo"], options: {}});
expect('"\\--foo"', {"--foo": ""}, {argv: ["\\--foo"], options: {}});
expect('"\\\\--foo"', {"--foo": ""}, {argv: ["\\--foo"], options: {}});

// Assert that parsing esaped option-strings won't have affect arrays
expect(["\\--foo"], {"--foo": ""}, {argv: ["\\--foo"], options: {}});
expect(["\\ --foo"], {"--foo": ""}, {argv: ["\\ --foo"], options: {}});
expect(['"\\--foo"'], {"--foo": ""}, {argv: ['"\\--foo"'], options: {}});
expect(['"\\\\--foo"'], {"--foo": ""}, {argv: ['"\\\\--foo"'], options: {}});
});
});

0 comments on commit bf7a188

Please sign in to comment.