http://www.perlmonks.org?node_id=921513


in reply to system() implementation on Windows (again)

There are two problems here, and other nodes have covered parts of them.

First is that argument lists are always passed as a single string in Windows, as opposed to arrays on other systems. This is less of a problem than it appears, because 95% of programs use the same rules for parsing that string into an array. Roughly speaking, the rules are that arguments can be quoted with double quotes, and backslashes can escape any character.

The second issue is that cmd.exe uses different quoting rules than the normal parsing routine. It uses caret as the escape character instead of backslash.

The result of this is that you can't create a string that will be treated the same for both of these cases. This means you have to quote strings differently depending on if they have shell meta-characters or not. And there isn't any good way to check that without reimplementing the code to detect them that exists inside perl. So here is a routine that will quote arguments correctly to use with system:

sub quote_list { my (@args) = @_; my $args = join ' ', map { quote_literal($_) } @args; if (_has_shell_metachars($args)) { # cmd.exe treats quotes differently from normal argument parsi +ng. # just escape everything using ^. $args =~ s/([()%!^"<>&|])/^$1/g; } return $args; } sub quote_literal { my ($text) = @_; # basic argument quoting. uses backslashes and quotes to escape # everything. if ($text ne '' && $text !~ /[ \t\n\v"]/) { # no quoting needed } else { my @text = split '', $text; $text = q{"}; for (my $i = 0; ; $i++) { my $bs_count = 0; while ( $i < @text && $text[$i] eq "\\" ) { $i++; $bs_count++; } if ($i > $#text) { $text .= "\\" x ($bs_count * 2); last; } elsif ($text[$i] eq q{"}) { $text .= "\\" x ($bs_count * 2 + 1); } else { $text .= "\\" x $bs_count; } $text .= $text[$i]; } $text .= q{"}; } return $text; } # direct port of code from win32.c sub _has_shell_metachars { my $string = shift; my $inquote = 0; my $quote = ''; my @string = split '', $string; for my $char (@string) { if ($char eq q{%}) { return 1; } elsif ($char eq q{'} || $char eq q{"}) { if ($inquote) { if ($char eq $quote) { $inquote = 0; $quote = ''; } } else { $quote = $char; $inquote++; } } elsif ($char eq q{<} || $char eq q{>} || $char eq q{|}) { if ( ! $inquote) { return 1; } } } return; }
Most of this is taken from the article Everyone quotes command line arguments the wrong way. There are probably some things Perl could do better for this.

Replies are listed 'Best First'.
Re^2: system() implementation on Windows (again)
by Anonymous Monk on Aug 23, 2011 at 10:19 UTC
    Well... Thanks for providing detailed workarounds. But that's exactly my point: the same system() call cannot apparently be used on both platforms, win and unix.

    And that's my problem. I have a piece of perl code which needs to execute an external program, and I want/need to make it portable across both platforms. It is not really satisfactory for me, when I have to introduce additional 30 lines of code for the windows part, just to catch all the possible quirks that can happen to me because of quotes or other special characters that might be contained in the arguments to my external program.

    I had the hope, that the system() call in the argument array form, would do what the documentation says: pass the arguments _diretcly_ to the executable. But thanks to the discussion in this thread, I understand now that it's simply not possible on Windows. And I have to live with that (and obviously implement one or another workaround for this). Thanks again.