Skip to content

Commit 9c3692f

Browse files
committed
Follow Windows argument quoting rules
Fixes #142
1 parent 211e794 commit 9c3692f

File tree

2 files changed

+42
-6
lines changed

2 files changed

+42
-6
lines changed

lib/IPC/Run/Win32Helper.pm

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@ trying to be a little cross-platform here. The only difference is
323323
that "\" is *not* treated as an escape except when it precedes
324324
punctuation, since it's used all over the place in DOS path specs.
325325
326+
TODO: strip caret escapes?
327+
328+
TODO: use
329+
https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args#parsing-c-command-line-arguments
330+
326331
TODO: globbing? probably not (it's unDOSish).
327332
328333
TODO: shebang emulation? Probably, but perhaps that should be part
@@ -396,6 +401,26 @@ sub _dup2_gently {
396401
IPC::Run::_dup2_rudely( $fd1, $fd2 );
397402
}
398403

404+
# Quote a string for use in the lpCommandLine argument of CreateProcessA(), such
405+
# that the string yields a single argv element in the callee.
406+
# https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args#parsing-c-command-line-arguments
407+
# gives the algorithm for translating a command line to an argv.
408+
sub _quote_cmd_line_arg {
409+
my $arg = shift;
410+
411+
# Skip optional quoting.
412+
return $arg unless $arg =~ /[\"\s]|^$/;
413+
414+
# Change N >= 0 backslashes before a double quote to 2N+1 backslashes.
415+
$arg =~ s/(\\*)"/${\($1 . $1)}\\"/gs;
416+
417+
# Change N >= 1 backslashes at end of argument to 2N backslashes.
418+
$arg =~ s/(\\+)\z/${\($1 . $1)}/gs;
419+
420+
# Wrap the whole thing in unescaped double quotes.
421+
return "\"$arg\"";
422+
}
423+
399424
sub win32_spawn {
400425
my ( $cmd, $ops ) = @_;
401426

@@ -442,11 +467,7 @@ sub win32_spawn {
442467
}
443468

444469
my $process;
445-
my $cmd_line = join " ", map {
446-
( my $s = $_ ) =~ s/"/"""/g;
447-
$s = qq{"$s"} if /[\"\s]|^$/;
448-
$s;
449-
} @$cmd;
470+
my $cmd_line = join " ", map { _quote_cmd_line_arg $_ } @$cmd;
450471

451472
_debug "cmd line: ", $cmd_line
452473
if _debugging;

t/run.t

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ sub get_warnings {
3838
select STDERR;
3939
select STDOUT;
4040

41-
use Test::More tests => 268;
41+
use Test::More tests => 271;
4242
use IPC::Run::Debug qw( _map_fds );
4343
use IPC::Run qw( :filters :filter_imp start );
4444

@@ -210,6 +210,21 @@ is $? >> 8, 42;
210210
is( _map_fds, $fd_map );
211211
$fd_map = _map_fds;
212212

213+
##
214+
## Arguments bearing all permitted bytes and special sequences of bytes
215+
##
216+
my $ascii;
217+
for my $i ( 1 .. 127 ) {
218+
219+
# skip BEL, since it's noisy and not otherwise special
220+
$ascii .= sprintf( '%c', $i ) unless $i == 7;
221+
}
222+
my $bs_dquote_bs = qq{\\"\\az\\\\"\\\\\\};
223+
foreach my $payload ( $ascii, $bs_dquote_bs, "$bs_dquote_bs\n" ) {
224+
$r = run [ $perl, '-e', 'print @ARGV', $payload ], '>', \$out;
225+
eok( $out, $payload );
226+
}
227+
213228
##
214229
## A function
215230
##

0 commit comments

Comments
 (0)