2

Given the following script:

const yargs = require('yargs');

const argv =
    yargs
        .usage('Usage: $0 [--whatIf]')
        .alias('d', 'directory')
        .alias('wi', 'whatIf')
        .nargs('d', 1)
        .describe('d', 'alphabetize this directory')
        .describe('whatIf', 'show what would happen if run')
        .demandOption(['d'])
        .argv;

console.log(argv.directory);

If I invoke the script from Windows PowerShell like so: node .\alphabetizer.js -d 'l:\my folder\Files - Some Files In Here\' --whatIf I get the output l:\my folder\Files - Some Files In Here\" --whatIf where I would expect just l:\my folder\Files - Some Files In Here\. It works OK with folder names that require no escaping, but it seems to get confused by the escaping.

If I examine process.argv, I can see the same escaping issue.

I have noticed that if I remove the trailing slash it will work. However, this still points to the node script not handling the input properly, because this should not be necessary with string set off by single quotes.

Is there a way to make this work?

Casey
  • 3,307
  • 1
  • 26
  • 41

1 Answers1

4

Both Windows PowerShell (powershell.exe) and PowerShell (Core) (pwsh) up to v7.2.x are fundamentally broken with respect to quoting arguments for external programs properly - see this answer for background info.

Generally, PowerShell on Windows has to perform re-quoting behind the scenes in order to ensure that just "..."-quoting is used, given that external programs can't be assumed to understand '...'-quoting too when parsing their command line (which on Windows every program has to do itself).

Windows PowerShell is more broken with respect to arguments that end in \ and have embedded spaces, re-quoting them improperly; e.g.:

PS> foo.exe 'c:\foo \' bar

is translated into the following command line behind the scenes:

foo.exe "c:\ foo \" bar

This is broken, in that most applications - including PowerShell's own CLI - sensibly assume that the \" is an escaped " char. to be taken verbatim, thinking that the argument continues with  bar and then implicitly ends, despite the formal lack of a closing ".

PowerShell (Core) v6+ more sensibly translates the above to foo.exe "c:\foo \\" bar, where the \\ is interpreted as an escaped \, and the following " again has syntactic function.


If you're stuck with Windows PowerShell, your only choices are:

  • either: if possible, leave off the trailing \
  • otherwise: manually double it (\\), but only do so if the argument also contains spaces (otherwise, the \\ will be retained as-is, though in the case of filesystem paths that is usually benign).
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Interesting. Well, isn't `"` an illegal character in Windows filenames? Perhaps it would be reasonable to just split on `\"`? Not sure if this would break my script on other operating systems. Since the tab completion adds a trailing slash by default it's a little irritating to have to remember this. – Casey Jan 29 '20 at 04:03
  • @Casey, yes, `"` is not a legal character in _file names_, but not every argument is and can be assumed to be a file name; a given runtime's command-line parser cannot make such an assumption, as it has no knowledge of what the arguments ultimately represent. – mklement0 Jan 29 '20 at 04:06
  • Of course not, but I am thinking of a workaround for my own script, where I know it is a filename that I want. Of course this doesn't solve the problem that it also swallows the other arguments. – Casey Jan 29 '20 at 04:08
  • @Casey, I hear you re tab completion, but what's really irritating is that Windows PowerShell is and has always been broken in this respect. Trying to compensate for the broken way in which the parameters are passed from _inside your script_ sounds like a painful and nontrivial undertaking, possibly requiring you to obtain the raw command line and perform custom-parsing - not sure that is worth it. – mklement0 Jan 29 '20 at 04:11
  • @Casey: Looking at your specific case: with such few options, perhaps fixing the problem in your script is feasible, after all: split `argv.directory` by `"`, append `\ ` to the part before (if needed) and use it as the dir. path; trim leading whitespace from the part after, and manually parse it as an option name. – mklement0 Jan 29 '20 at 04:19