Skip to content

Commit c409192

Browse files
deiwindanielbayley
authored andcommitted
Fix and improve quote support in Run Options (#1424)
* Fix using quotes in Run Options Currently, whenever Command Arguments or Program Arguments contain quotes in the Run Options dialog, then the script blows up with a "Uncaught TypeError: Cannot read property 'concat' of undefined". Commit 668057c ("Make linter happy and fix convert errors", 2016-12-05) changed `(matches != null) ? matches : []` to `!matches ? matches : []`, which reverses the original meaning. This only causes errors when the string contains quotes, because there's an early return in this function. Fixes #1339. * Test argument splitting PR #742, that changed the logic from the simple "split on space" logic to the current version, mentions these cases as the ones that were tested with this implementation. Add tests to guarantee behavior remains the same for these cases. * Allow using nested quotes and parentheses in Run Options These changes allow using the expression shown in the added test, which allows running racket scripts. This usage differs from the default racket behavior, because this also outputs the return value of the script. The change to `matches` creation uses a regex backreference and the lazy (non-greedy) quantifier to only match outer quotes. So whereas the current version would've matched both `"{FILE_ACTIVE}"` and `'(load "{FILE_ACTIVE}")'`, then the new version only matches the outer quotes. The following code then broke with the previous version, trying to match and replace both of these overlapping instances. The `args.replace` change was necessary, because otherwise the argument was used as regex, and any parentheses and other regex operators were not being escaped. Using simple substring replacing instead avoids the need to escape special characters. The last change avoids removing all quotes from the arguments and instead only replaces matching quotes that are at the beginning and end of the argument. I'm not sure if the "strips quotes from argument values" cases were actually useful in any situation or the behavior was simply incidental. Currently removed support for that behavior. If this behavior turns out to be necessary, then separate handling should be added for it. * Simplify and improve argument splitting logic Instead of multiple back and forth replacements, this uses a single regex to find the arguments and simply adds them together while looping over the provided text. As far as I can tell, this now behaves exactly like most shells do. Except for escaped quotes. This does not support escaping quotes. This re-adds support for the "strips quotes from argument values" tests that was removed in the previous commit c4c48cb ("Allow using nested quotes and parentheses in Run Options", 2017-09-17).
1 parent fc84ded commit c409192

File tree

2 files changed

+91
-40
lines changed

2 files changed

+91
-40
lines changed

lib/script-options-view.js

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -87,55 +87,39 @@ export default class ScriptOptionsView extends View {
8787
this.panel.hide();
8888
}
8989

90-
splitArgs(element) {
91-
let args = element.get(0).getModel().getText().trim();
92-
93-
if (args.indexOf('"') === -1 && args.indexOf("'") === -1) {
94-
// no escaping, just split
95-
return (args.split(' ').filter(item => item !== '').map(item => item));
96-
}
97-
98-
const replaces = {};
99-
100-
const regexps = [/"[^"]*"/ig, /'[^']*'/ig];
101-
102-
let matches;
103-
// find strings in arguments
104-
regexps.forEach((regex) => {
105-
matches = (!matches ? matches : []).concat((args.match(regex)) || []);
106-
});
107-
108-
// format replacement as bash comment to avoid replacing valid input
109-
matches.forEach((match) => {
110-
replaces[`\`#match${Object.keys(replaces).length + 1}\``] = match;
111-
});
112-
113-
// replace strings
114-
for (const match in replaces) {
115-
const part = replaces[match];
116-
args = args.replace(new RegExp(part, 'g'), match);
117-
}
118-
const split = (args.split(' ').filter(item => item !== '').map(item => item));
119-
120-
const replacer = (argument) => {
121-
for (const match in replaces) {
122-
const replacement = replaces[match];
123-
argument = argument.replace(match, replacement);
90+
static splitArgs(argText) {
91+
const text = argText.trim();
92+
const argSubstringRegex = /([^'"\s]+)|((["'])(.*?)\3)/g;
93+
const args = [];
94+
let lastMatchEndPosition = -1;
95+
let match = argSubstringRegex.exec(text);
96+
while (match !== null) {
97+
const matchWithoutQuotes = match[1] || match[4];
98+
// Combine current result with last match, if last match ended where this
99+
// one begins.
100+
if (lastMatchEndPosition === match.index) {
101+
args[args.length - 1] += matchWithoutQuotes;
102+
} else {
103+
args.push(matchWithoutQuotes);
124104
}
125-
return argument;
126-
};
127105

128-
// restore strings, strip quotes
129-
return split.map(argument => replacer(argument).replace(/"|'/g, ''));
106+
lastMatchEndPosition = argSubstringRegex.lastIndex;
107+
match = argSubstringRegex.exec(text);
108+
}
109+
return args;
130110
}
131111

132112
getOptions() {
133113
return {
134114
workingDirectory: this.inputCwd.get(0).getModel().getText(),
135115
cmd: this.inputCommand.get(0).getModel().getText(),
136-
cmdArgs: this.splitArgs(this.inputCommandArgs),
116+
cmdArgs: this.constructor.splitArgs(
117+
this.inputCommandArgs.get(0).getModel().getText(),
118+
),
137119
env: this.inputEnv.get(0).getModel().getText(),
138-
scriptArgs: this.splitArgs(this.inputScriptArgs),
120+
scriptArgs: this.constructor.splitArgs(
121+
this.inputScriptArgs.get(0).getModel().getText(),
122+
),
139123
};
140124
}
141125

spec/script-options-view-spec.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use babel';
2+
3+
/* eslint-disable no-underscore-dangle */
4+
import ScriptOptionsView from '../lib/script-options-view';
5+
6+
describe('ScriptOptionsView', () => {
7+
describe('splitArgs', () => {
8+
[{
9+
text: '',
10+
expectedArgs: [],
11+
description: 'returns an empty array for empty string',
12+
}, {
13+
text: ' \t\n',
14+
expectedArgs: [],
15+
description: 'returns an empty array for just whitespace',
16+
}, {
17+
text: 'arg1 arg2',
18+
expectedArgs: ['arg1', 'arg2'],
19+
description: 'splits arguments on whitespace',
20+
}, {
21+
text: 'arg1=val1 arg2',
22+
expectedArgs: ['arg1=val1', 'arg2'],
23+
description: 'keeps argument values',
24+
}, {
25+
text: '"foo bar" arg2',
26+
expectedArgs: ['foo bar', 'arg2'],
27+
description: 'does not split quoted arguments on whitespace',
28+
}, {
29+
text: '\'foo bar\' arg2',
30+
expectedArgs: ['foo bar', 'arg2'],
31+
description: 'recognizes single quotes',
32+
}, {
33+
text: '"foo bar" "another string"',
34+
expectedArgs: ['foo bar', 'another string'],
35+
description: 'handles multiple quoted arguments',
36+
}, {
37+
text: '\'foo bar\' \'another string\'',
38+
expectedArgs: ['foo bar', 'another string'],
39+
description: 'handles multiple single quoted arguments',
40+
}, {
41+
text: '"foo bar" \'another string\'',
42+
expectedArgs: ['foo bar', 'another string'],
43+
description: 'handles multiple quoted arguments, with mixed single and double quotes',
44+
}, {
45+
text: 'arg1="foo bar"',
46+
expectedArgs: ['arg1=foo bar'],
47+
description: 'strips quotes from argument values',
48+
}, {
49+
text: 'arg1=\'foo bar\'',
50+
expectedArgs: ['arg1=foo bar'],
51+
description: 'strips single quotes from argument values',
52+
}, {
53+
text: '-e \'(load "{FILE_ACTIVE}")\'',
54+
expectedArgs: ['-e', '(load "{FILE_ACTIVE}")'],
55+
description: 'keeps nested quotes intact',
56+
}, {
57+
text: 'we"ird way to inc"l"ude spaces in arg"s',
58+
expectedArgs: ['weird way to include spaces in args'],
59+
description: 'supports multiple top level quotes',
60+
}].forEach(({ text, expectedArgs, description }) => {
61+
it(description, () => {
62+
const args = ScriptOptionsView.splitArgs(text);
63+
expect(args).toEqual(expectedArgs);
64+
});
65+
});
66+
});
67+
});

0 commit comments

Comments
 (0)