Skip to content

Commit 7040f0b

Browse files
committed
Perl: Support spaces in command arguments on MSWin32
On a native Windows build of Perl (where $^O equals 'MSWin32'), like Strawberry or ActiveState Perl, the Git.pm module follows a unique code path, one that alters the behavior of piped Git commands. This special path failed to handle spaces contained in an argument to Git. Remove that limitation by improving Windows command line support. On Windows, the command line is part of the operating system; it is not provided by a shell. This system requires certain characters (\, ") to be escaped (in certain cases). The cmd.exe command prompt rests on top of the command line and it has its own, additional escaping mechanism. When creating pipes, an MSWin32 build of Perl will either use the system call, CreateProcess (which takes the command line), or, if interpreted characters are detected, the command prompt, cmd.exe. Add a function to unescape command line arguments, one that will work with CreateProcess or with cmd.exe, as appropriate. This ensures any Git command will have its arguments parsed correctly, regardless of content. Additionally, include tests to verify that Perl's system() behavior remains unchanged -- notably, that the list of interpreted characters (for selecting cmd.exe) does not change. These tests need an MSWin32 build of Perl in the PATH to work; otherwise, they are skipped. This change will improve commands (or batch scripts) run in CMD, where native (MSWin32) builds of Perl are all that is available in the PATH (unless the user opts to install Unix utils in the PATH, which is not recommended in the Git for Windows installer). Commands run in MSYS Perl, which is included with Git for Windows, will not change (as it never had a problem). Closes: #1602 Signed-off-by: Chris Lindee <[email protected]>
1 parent 53f9a3e commit 7040f0b

File tree

3 files changed

+115
-6
lines changed

3 files changed

+115
-6
lines changed

perl/Git.pm

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,12 +1692,8 @@ package Git::activestate_pipe;
16921692

16931693
sub TIEHANDLE {
16941694
my ($class, @params) = @_;
1695-
# FIXME: This is probably horrible idea and the thing will explode
1696-
# at the moment you give it arguments that require some quoting,
1697-
# but I have no ActiveState clue... --pasky
1698-
# Let's just hope ActiveState Perl does at least the quoting
1699-
# correctly.
1700-
my @data = qx{git @params};
1695+
my $cmdline = make_windows_commandline('git', @params);
1696+
my @data = qx{$cmdline};
17011697
bless { i => 0, data => \@data }, $class;
17021698
}
17031699

@@ -1726,5 +1722,35 @@ sub EOF {
17261722
return ($self->{i} >= scalar @{$self->{data}});
17271723
}
17281724

1725+
sub make_windows_commandline {
1726+
my $cmdline = join ' ', unescape_windows_commandline_args(@_);
1727+
1728+
# The set of meta-characters, as determined by Perl, was found in
1729+
# has_shell_metachars() in perl5/win32/win32.c
1730+
return unescape_cmd_commandline($cmdline) if $cmdline =~ m/[<>|%]/;
1731+
return $cmdline;
1732+
}
1733+
1734+
# See https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
1735+
sub unescape_windows_commandline_args {
1736+
my @out;
1737+
foreach (@_) {
1738+
my $value = $_;
1739+
$value =~ s{(\\*)"}{$1$1\\"}g;
1740+
if ($value =~ m/\W/) {
1741+
$value =~ s{(\\+)$}{$1$1};
1742+
$value = "\"$value\"";
1743+
}
1744+
push @out, $value;
1745+
}
1746+
return @out;
1747+
}
1748+
1749+
sub unescape_cmd_commandline {
1750+
my ($cmdline) = @_;
1751+
$cmdline =~ s{([()%!^"<>&|])}{^$1}g;
1752+
return $cmdline;
1753+
}
1754+
17291755

17301756
1; # Famous last words

t/t9701-perl-git-MSWin32.sh

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/bin/sh
2+
#
3+
# Copyright (c) 2018 Chris Lindee
4+
#
5+
6+
test_description='perl interface (Git.pm) on MSWin32'
7+
. ./test-lib.sh
8+
9+
find_MSWin32_perl() {
10+
local perl
11+
for perl in $(which -a perl); do
12+
if "$perl" -e 'exit 1 if $^O ne q{MSWin32}'; then
13+
echo "$perl"
14+
return 0
15+
fi
16+
done
17+
return 1
18+
}
19+
20+
MSWin32_Perl="$(find_MSWin32_perl)"
21+
if [ $? -ne 0 ]; then
22+
skip_all='skipping perl on MSWin32 interface tests, MSWin32 perl not available'
23+
test_done
24+
fi
25+
26+
"$MSWin32_Perl" -MTest::More -e 0 2>/dev/null || {
27+
skip_all="MSWin32 Perl Test::More unavailable, skipping test"
28+
test_done
29+
}
30+
31+
# The external test will outputs its own plan
32+
test_external_has_tap=1
33+
34+
# Ensure paths are recognized by Windows executables.
35+
perl_test_path="$TEST_DIRECTORY"/t9701/test.pl
36+
if test_have_prereq CYGWIN || test_have_prereq MINGW; then
37+
perl_test_path="$(cygpath -w "$perl_test_path")"
38+
GITPERLLIB="$(cygpath -w -p "$GITPERLLIB")"
39+
fi
40+
41+
test_external_without_stderr \
42+
'Windows command line (Perl API)' \
43+
"$MSWin32_Perl" "$perl_test_path"
44+
45+
test_done

t/t9701/test.pl

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/perl
2+
use lib (split(/:/, $ENV{GITPERLLIB}));
3+
4+
use 5.008;
5+
use strict;
6+
use warnings;
7+
8+
use Test::More qw(no_plan);
9+
10+
BEGIN {
11+
# t9701-perl-git-MSWin32.sh kicks off our testing, so we have to go from
12+
# there.
13+
Test::More->builder->current_test(1);
14+
Test::More->builder->no_ending(1);
15+
}
16+
17+
my @tests = (
18+
'\\\\',
19+
'()%!^"<>&|',
20+
'C:\\Windows\\system32\\',
21+
map { chr } 32 .. 126, 160 .. 255 # printable ISO-8859-1, the base for Windows-1252
22+
);
23+
24+
# Just in case
25+
plan skip_all => 'Test will not run on MSYS Perl (distributed with Git for Windows)' if $^O eq 'msys';
26+
plan skip_all => 'Test requires Microsoft Windows' if $^O ne 'MSWin32';
27+
28+
require_ok('Git');
29+
30+
foreach (@tests) {
31+
my $cmdline = Git::activestate_pipe::make_windows_commandline($^X, '-e', 'print $ARGV[0]', $_);
32+
is qx{$cmdline}, $_, $cmdline;
33+
}
34+
35+
printf "1..%d\n", Test::More->builder->current_test;
36+
37+
my $is_passing = eval { Test::More->is_passing };
38+
exit($is_passing ? 0 : 1) unless $@ =~ /Can't locate object method/;

0 commit comments

Comments
 (0)