-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(run): improve escaping for script arguments #4135
Conversation
72bb441
to
156bb01
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for doing this!
src/cli/commands/run.js
Outdated
function quoteForCmd(arg: string): string { | ||
// See the below blog post for an explanation of what's going on here: | ||
// eslint-disable-next-line max-len | ||
// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hah! I'm glad there is another one who read the same article 🗡
I think you should release this as a library so yarn
and others can use it from there because I've yet to see a proper Windows-compatible shell escaping library on NPM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I completely agree; I started writing that library this morning. I was a little shy about writing it first and then proposing that yarn take a dependency on a 0.0.1 library that nobody's used yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well then, add lots of tests and have a high coverage and mark it as 1.0 ;)
src/cli/commands/run.js
Outdated
return arg; | ||
} | ||
|
||
const quoteForShell = process.platform === 'win32' ? quoteForCmd : arg => `'${arg.replace(/'/g, "'\\''")}'`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be we should have the following in constants.js
:
export const IS_WINDOWS = process.platform === 'win32';
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like there's a bit more to bash
escaping too: https://github.com/xxorax/node-shell-escape/blob/master/shell-escape.js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds reasonable to me. There are a few instances of that check throughout the code; I looked to make sure there wasn't some other pattern already in place that I should follow.
If I understand correctly, I think those extra replaces in node-shell-escape are just cosmetic; they remove some redundant quotes but don't affect the final interpreted string. I'll probably add something like that though if I do go and release this as a library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds reasonable to me. There are a few instances of that check throughout the code; I looked to make sure there wasn't some other pattern already in place that I should follow.
On second thought, I think that change deserves its own diff so I'll work on it. For now, your diff is fine. :)
If I understand correctly, I think those extra replaces in node-shell-escape are just cosmetic; they remove some redundant quotes but don't affect the final interpreted string. I'll probably add something like that though if I do go and release this as a library.
Fine by me if you say they are safe. It's frightening for me to realize that I know more about Windows escaping than Bash escaping at this point :D
src/cli/commands/run.js
Outdated
function joinArgs(args: Array<string>): string { | ||
return args.reduce((joinedArgs, arg) => joinedArgs + ' "' + arg.replace(/"/g, '\\"') + '"', ''); | ||
return args.length ? ' ' + args.map(quoteForShell).join(' ') : ''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need the args.length ?
check. [].map(whatever).join(' ')
still returns ''
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but the entire function would return the ' '
that gets prepended to the joined string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sorry I missed that part! That said, then the name of this function should be stringifyArgs
or getArgsString
to convey the meaning better.
Optionally, you can pass in the first part and name the function serializeShellCommand
and do something like:
function serializeShellCommand(cmd: string, args: Array<string>): string {
return [cmd].concat(args.map(quoteForShell)).join(' ');
}
That would take care all of it. Anyways, I'm just rambling. Just changing the name of the function would be enough for me but I'm still a bit concerned that people may miss that extra space at the beginning or don't understand the reason at first glance.
@@ -0,0 +1,5 @@ | |||
{ | |||
"scripts": { | |||
"write-args": "node write-args.js" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can do node -p -e "process.argv[2], process.argv.slice(3).join(' ')"
instead and then read the output from stdout. I'd prefer not to write to file system if/when possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion. I'll give it a try.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out it should just be node -p "..."
without the -e
part.
__tests__/commands/run.js
Outdated
@@ -13,6 +13,7 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000; | |||
const execCommand: $FlowFixMe = require('../../src/util/execute-lifecycle-script').execCommand; | |||
|
|||
const path = require('path'); | |||
const q = process.platform === 'win32' ? '"' : "'"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename this to shellQuotes
or something. q
is a very ambiguous variable name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm with you in general, but in this case, this guy gets interpolated several times into several strings below, and having it be a short name really helps readability in my opinion. Compare:
const args = ['cat-names', config, `${q}${script}${q} ${q}--filter${q} ${q}cat names${q}`, config.cwd];
const args = [
'cat-names',
config,
`${shellQuote}${script}${shellQuote} ${shellQuote}--filter${shellQuote} ${shellQuote}cat names${shellQuote}`,
config.cwd,
];
Easy enough to change if you still disagree though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, let's agree to add a comment above it stating this then and call it a day? :)
} | ||
} | ||
} | ||
cases.sort(() => Math.random() - 0.5); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very bad way to randomize an array. Also, why do we need randomization anyways?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's bad if true randomness is required, but it's good enough for a quick scramble (would you prefer I implement/require
a Knuth shuffle just for this test?). I'm randomizing because I'm not actually testing every combination; I didn't want to take too long on this test but I also wanted to get a decent sampling of the space. Basically it's a poor man's property-based test. If you think it's a good idea, I'd be happy to pull in jsverify or jssmartcheck and write a real property-based test. Alternatively, if this is a library, the heavy property checking can go there and yarn can test a much smaller sampling of cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any non-determinism in tests would create doubt when running things on the CI: did it fail for reals or was that a random blip? Did I catch a real edge case thanks to randomness, or the CI had a bad network connection? So let's have this test all possible combinations instead of random sampling for each test run or agree to ahve the same random sample for each run.
return run(config, reporter, flags, args); | ||
}); | ||
|
||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh. Something is happening here that I don't understand.
Originally I wrote this test to invoke the script serially, and that was consistently timing out on AppVeyor, even with a much higher DEFAULT_TIMEOUT_INTERVAL
(2.5 minutes!), and even with only three iterations of the script invocation. Once I parallelized, the tests stopped timing out and in fact finished much faster than timeout/degree of parallelism, so there was probably something I was doing wrong with the serial implementation (I was await
ing on all the right things, I thought!), but I was nervous that there was a chance that Windows just wanted to take its time spawning child processes on that build machine sometimes. So I took this down a little but it is still higher than the current test times seem to justify. What's your general sense here; should I try to keep the value conservatively high to reduce the chance of random build failure, or lower to try to crack down on build time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, now I see. Actually, I think you can benefit from using test.concurrent
and splitting your tests into individual test cases, generated programmatically, wrap them in a describe
block so you can use beforeEach
hooks and then jest
should take care of it. If even after that and not writing to the file system (remember, we agreed to try using stdout instead above?) I'm okay leaving the timeout higher.
cases.sort(() => Math.random() - 0.5); | ||
const tempDir = await makeTemp(); | ||
const prefix = path.resolve(__dirname, '../..'); | ||
const origPrefix = process.env.PREFIX; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't modify process.env
directly, please. There should be a way to pass this to runRun
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you give me more of a hint? I looked through the code for such a way and couldn't find a straightforward one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Search for getGlobalPrefix
in the code. Spoiler: you can use flags
or config
to override the prefix.
Hey, @rhendric! Long time no see. Any updates? |
Hi! Sorry for the delay; I still expect to finish the swing on this but I think it'll be another week or so before I have the time. |
No worries, just wanted to check if you're still working on it :) |
Hi, I'm back on the case! Quick question: I see that there is now a comment in __tests__/integration.js linking back to this PR, claiming that this is blocking those tests from being enabled on Windows, but I don't see the connection—those tests look like they don't use any characters that are likely to cause problems. Looks like it was added in #4152. @arcanis, or anyone, what's the story with that? Should I try to remove that gating |
I figured out what the deal with those tests is. It's a combination of the below:
I see three ways to get these tests passing, the first of which I'm going to go with unless I hear objections:
|
**Summary** Extra command-line arguments to scripts were not being escaped correctly. This patch uses puka to add robust shell quoting for both Windows and Linux/macOS. **Test plan** On *nix, create a `package.json` containing `"scripts":{"echo":"echo"}`. Run `yarn run -s echo -- '$X \"blah\"'`. Expect to observe ` \blah\` prior to this patch, and `$X \"blah\"` after it. Testing on Windows should be similar, but may require fancier escaping to get the arguments into yarn in the first place.
This change will increase the build size from 9.83 MB to 9.9 MB, an increase of 73.16 KB (1%)
|
1 similar comment
This change will increase the build size from 9.83 MB to 9.9 MB, an increase of 73.16 KB (1%)
|
@BYK, re-review please? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! @arcanis can you also give it a look before merging?
@rhendric thank you so much for this! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm !
**Summary** Extra command-line arguments to scripts were not being escaped correctly. This patch adds robust shell quoting logic for both Windows and Linux/macOS. **Test plan** On *nix, create a `package.json` containing `"scripts":{"echo":"echo"}`. Run `yarn run -s echo -- '$X \"blah\"'`. Expect to observe ` \blah\` prior to this patch, and `$X \"blah\"` after it. Testing on Windows should be similar, but may require fancier escaping to get the arguments into yarn in the first place. (I don't have access to a Windows box to verify the exact procedure to follow, sorry—but I did confirm that my automated tests succeed in AppVeyor.)
Summary
Extra command-line arguments to scripts were not being escaped correctly. This patch adds robust shell quoting logic for both Windows and Linux/macOS.
Test plan
On *nix, create a
package.json
containing"scripts":{"echo":"echo"}
. Runyarn run -s echo -- '$X \"blah\"'
. Expect to observe\blah\
prior to this patch, and$X \"blah\"
after it.Testing on Windows should be similar, but may require fancier escaping to get the arguments into yarn in the first place. (I don't have access to a Windows box to verify the exact procedure to follow, sorry—but I did confirm that my automated tests succeed in AppVeyor.)