Skip to content

Commit a0c3943

Browse files
committed
Windows: fix escaping of external commands
The `escapeshellcmd()` function apparently does not escape spaces within a path on Windows which can result in broken functionality. While the sniffs and report affected by this are apparently not used that much based on the lack of bug reports, fixing it still seemed like the _right thing to do_. Noticed while running the unit tests on a fresh install on Windows 10. At some point over the past years, Node has apparently changed their default install directory on Windows and the order in which they register their paths to the Windows system `PATH`. This means that `where csslint` may result in a `$cmd` path like `C:\Program Files\nodejs\csslint.cmd`, which would be escaped to `C:^\Program Files^\nodejs^\csslint.cmd` on Windows, which in turn results in a `'C:\Program' is not recognized as an internal or external command, operable program or batch file.` error. I could have changed the install path for NVM on my machine, but that would just have hidden the underlying issue. It does appear to be a known issue with the function based on the last two comments in this upstream bug report: https://bugs.php.net/bug.php?id=43261, however as that issue is closed, I don't expect this to be fixed in PHP itself, though it might be worth it to open a new issue upstream about it (as those two comments were left on a closed issue years after the close). Fixed now by checking an escaped path for unescaped spaces when on Windows and if necessary, escaping them. The escaping is done in such a way that, even if PHP itself would start escaping these spaces, the `Common::escapeshellcmd()` function will still handle this correctly.
1 parent 2da6904 commit a0c3943

File tree

10 files changed

+42
-11
lines changed

10 files changed

+42
-11
lines changed

src/Reports/Notifysend.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
use PHP_CodeSniffer\Config;
2020
use PHP_CodeSniffer\Files\File;
21+
use PHP_CodeSniffer\Util\Common;
2122

2223
class Notifysend implements Report
2324
{
@@ -58,7 +59,7 @@ public function __construct()
5859
{
5960
$path = Config::getExecutablePath('notifysend');
6061
if ($path !== null) {
61-
$this->path = escapeshellcmd($path);
62+
$this->path = Common::escapeshellcmd($path);
6263
}
6364

6465
$timeout = Config::getConfigData('notifysend_timeout');

src/Standards/Generic/Sniffs/Debug/CSSLintSniff.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHP_CodeSniffer\Config;
1313
use PHP_CodeSniffer\Files\File;
1414
use PHP_CodeSniffer\Sniffs\Sniff;
15+
use PHP_CodeSniffer\Util\Common;
1516

1617
class CSSLintSniff implements Sniff
1718
{
@@ -54,7 +55,7 @@ public function process(File $phpcsFile, $stackPtr)
5455

5556
$fileName = $phpcsFile->getFilename();
5657

57-
$cmd = escapeshellcmd($csslintPath).' '.escapeshellarg($fileName).' 2>&1';
58+
$cmd = Common::escapeshellcmd($csslintPath).' '.escapeshellarg($fileName).' 2>&1';
5859
exec($cmd, $output, $retval);
5960

6061
if (is_array($output) === false) {

src/Standards/Generic/Sniffs/Debug/ClosureLinterSniff.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHP_CodeSniffer\Config;
1313
use PHP_CodeSniffer\Files\File;
1414
use PHP_CodeSniffer\Sniffs\Sniff;
15+
use PHP_CodeSniffer\Util\Common;
1516

1617
class ClosureLinterSniff implements Sniff
1718
{
@@ -71,7 +72,7 @@ public function process(File $phpcsFile, $stackPtr)
7172

7273
$fileName = $phpcsFile->getFilename();
7374

74-
$lintPath = escapeshellcmd($lintPath);
75+
$lintPath = Common::escapeshellcmd($lintPath);
7576
$cmd = $lintPath.' --nosummary --notime --unix_mode '.escapeshellarg($fileName);
7677
exec($cmd, $output, $retval);
7778

src/Standards/Generic/Sniffs/Debug/ESLintSniff.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHP_CodeSniffer\Config;
1313
use PHP_CodeSniffer\Files\File;
1414
use PHP_CodeSniffer\Sniffs\Sniff;
15+
use PHP_CodeSniffer\Util\Common;
1516

1617
class ESLintSniff implements Sniff
1718
{
@@ -76,7 +77,7 @@ public function process(File $phpcsFile, $stackPtr)
7677
$eslintOptions[] = '--config '.escapeshellarg($configFile);
7778
}
7879

79-
$cmd = escapeshellcmd(escapeshellarg($eslintPath).' '.implode(' ', $eslintOptions).' '.escapeshellarg($filename));
80+
$cmd = Common::escapeshellcmd(escapeshellarg($eslintPath).' '.implode(' ', $eslintOptions).' '.escapeshellarg($filename));
8081

8182
// Execute!
8283
exec($cmd, $stdout, $code);

src/Standards/Generic/Sniffs/Debug/JSHintSniff.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHP_CodeSniffer\Config;
1414
use PHP_CodeSniffer\Files\File;
1515
use PHP_CodeSniffer\Sniffs\Sniff;
16+
use PHP_CodeSniffer\Util\Common;
1617

1718
class JSHintSniff implements Sniff
1819
{
@@ -56,10 +57,10 @@ public function process(File $phpcsFile, $stackPtr)
5657
}
5758

5859
$fileName = $phpcsFile->getFilename();
59-
$jshintPath = escapeshellcmd($jshintPath);
60+
$jshintPath = Common::escapeshellcmd($jshintPath);
6061

6162
if ($rhinoPath !== null) {
62-
$rhinoPath = escapeshellcmd($rhinoPath);
63+
$rhinoPath = Common::escapeshellcmd($rhinoPath);
6364
$cmd = "$rhinoPath \"$jshintPath\" ".escapeshellarg($fileName);
6465
exec($cmd, $output, $retval);
6566

src/Standards/Generic/Sniffs/PHP/SyntaxSniff.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHP_CodeSniffer\Config;
1414
use PHP_CodeSniffer\Files\File;
1515
use PHP_CodeSniffer\Sniffs\Sniff;
16+
use PHP_CodeSniffer\Util\Common;
1617

1718
class SyntaxSniff implements Sniff
1819
{
@@ -53,7 +54,7 @@ public function process(File $phpcsFile, $stackPtr)
5354
}
5455

5556
$fileName = escapeshellarg($phpcsFile->getFilename());
56-
$cmd = escapeshellcmd($this->phpPath)." -l -d display_errors=1 -d error_prepend_string='' $fileName 2>&1";
57+
$cmd = Common::escapeshellcmd($this->phpPath)." -l -d display_errors=1 -d error_prepend_string='' $fileName 2>&1";
5758
$output = shell_exec($cmd);
5859
$matches = [];
5960
if (preg_match('/^.*error:(.*) in .* on line ([0-9]+)/m', trim($output), $matches) === 1) {

src/Standards/Squiz/Sniffs/Debug/JSLintSniff.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHP_CodeSniffer\Config;
1313
use PHP_CodeSniffer\Files\File;
1414
use PHP_CodeSniffer\Sniffs\Sniff;
15+
use PHP_CodeSniffer\Util\Common;
1516

1617
class JSLintSniff implements Sniff
1718
{
@@ -56,8 +57,8 @@ public function process(File $phpcsFile, $stackPtr)
5657

5758
$fileName = $phpcsFile->getFilename();
5859

59-
$rhinoPath = escapeshellcmd($rhinoPath);
60-
$jslintPath = escapeshellcmd($jslintPath);
60+
$rhinoPath = Common::escapeshellcmd($rhinoPath);
61+
$jslintPath = Common::escapeshellcmd($jslintPath);
6162

6263
$cmd = "$rhinoPath \"$jslintPath\" ".escapeshellarg($fileName);
6364
exec($cmd, $output, $retval);

src/Standards/Squiz/Sniffs/Debug/JavaScriptLintSniff.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHP_CodeSniffer\Exceptions\RuntimeException;
1414
use PHP_CodeSniffer\Files\File;
1515
use PHP_CodeSniffer\Sniffs\Sniff;
16+
use PHP_CodeSniffer\Util\Common;
1617

1718
class JavaScriptLintSniff implements Sniff
1819
{
@@ -56,7 +57,7 @@ public function process(File $phpcsFile, $stackPtr)
5657

5758
$fileName = $phpcsFile->getFilename();
5859

59-
$cmd = '"'.escapeshellcmd($jslPath).'" -nologo -nofilelisting -nocontext -nosummary -output-format __LINE__:__ERROR__ -process '.escapeshellarg($fileName);
60+
$cmd = '"'.Common::escapeshellcmd($jslPath).'" -nologo -nofilelisting -nocontext -nosummary -output-format __LINE__:__ERROR__ -process '.escapeshellarg($fileName);
6061
$msg = exec($cmd, $output, $retval);
6162

6263
// Variable $exitCode is the last line of $output if no error occurs, on

src/Standards/Zend/Sniffs/Debug/CodeAnalyzerSniff.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHP_CodeSniffer\Files\File;
1515
use PHP_CodeSniffer\Config;
1616
use PHP_CodeSniffer\Exceptions\RuntimeException;
17+
use PHP_CodeSniffer\Util\Common;
1718

1819
class CodeAnalyzerSniff implements Sniff
1920
{
@@ -53,7 +54,7 @@ public function process(File $phpcsFile, $stackPtr)
5354
// In the command, 2>&1 is important because the code analyzer sends its
5455
// findings to stderr. $output normally contains only stdout, so using 2>&1
5556
// will pipe even stderr to stdout.
56-
$cmd = escapeshellcmd($analyzerPath).' '.escapeshellarg($fileName).' 2>&1';
57+
$cmd = Common::escapeshellcmd($analyzerPath).' '.escapeshellarg($fileName).' 2>&1';
5758

5859
// There is the possibility to pass "--ide" as an option to the analyzer.
5960
// This would result in an output format which would be easier to parse.

src/Util/Common.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,28 @@ public static function isStdinATTY()
239239
}//end isStdinATTY()
240240

241241

242+
/**
243+
* Escape a path to a system command.
244+
*
245+
* @param string $cmd The path to the system command.
246+
*
247+
* @return string
248+
*/
249+
public static function escapeshellcmd($cmd)
250+
{
251+
$cmd = escapeshellcmd($cmd);
252+
253+
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
254+
// Spaces are not escaped by escapeshellcmd on Windows, but need to be
255+
// for the command to be able to execute.
256+
$cmd = preg_replace('`(?<!^) `', '^ ', $cmd);
257+
}
258+
259+
return $cmd;
260+
261+
}//end escapeshellcmd()
262+
263+
242264
/**
243265
* Prepares token content for output to screen.
244266
*

0 commit comments

Comments
 (0)