如何在 Windows 上使用 escapeshellarg() 但“针对 Linux”(反之亦然)?
Posted
技术标签:
【中文标题】如何在 Windows 上使用 escapeshellarg() 但“针对 Linux”(反之亦然)?【英文标题】:How do I use escapeshellarg() on Windows but "aimed for Linux" (and vice versa)? 【发布时间】:2019-10-23 01:32:22 【问题描述】:如果 php 在 Windows 上运行,escapeshellarg() 会以某种方式转义文件名(例如),然后在其周围添加 " (DOUBLE) 引号。
如果 PHP 在 Linux 上运行,escapeshellarg() 使用基于 Linux 的转义,然后在其周围添加 ' (SINGLE) 引号。
在我的情况下,我在 Windows 上生成一个 SHA256SUMS 文件,但针对的是 Linux。因为我使用 escapeshellarg() 来转义文件名,所以我最终得到了一个类似的文件:
cabcdccas12exdqdqadanacvdkjsc123ccfcfq3rdwcndwf2qefcf "cool filename with spaces.zip"
但是,Linux 工具可能期望:
cabcdccas12exdqdqadanacvdkjsc123ccfcfq3rdwcndwf2qefcf 'cool filename with spaces.zip'
查看手册,似乎没有办法执行以下操作:escapeshellarg($blabla, TARGET_OS_LINUX);以便它使用 Linux 的规则而不是运行脚本的操作系统 (Windows)。
我不能只是 str_replace 引号,因为它不会考虑所有特定于平台的规则。
另外,是的,我需要在文件名中包含空格(以及任何其他跨平台有效字符)。
遗憾的是,在我拥有的唯一信息来源中,我没有提及首选的引用样式:https://help.ubuntu.com/community/HowToSHA256SUM
也许读取该 SHA256SUMS 文件的 SHA256 安全验证工具可以理解并解析这两种类型?
【问题讨论】:
【参考方案1】:escapeshellarg()
的行为是硬编码的,具体取决于 PHP 是在 Windows 上运行还是在任何其他操作系统上运行。 您应该重新实现 escapeshellarg()
以获得一致的行为。
这是我在 PHP 中使用 Windows/其他操作系统切换重新实现 escapeshellarg()
的尝试:
<?php namespace polyfill;
const TARGET_OS_WINDOWS = 1;
const TARGET_OS_UNIX = 2;
function escapeshellarg(string $input, int $os_mode = 0): string
if (false !== strpos($input, "\x00"))
throw new \UnexpectedValueException(__FUNCTION__ . '(): Argument #1 ($input) must not contain any null bytes');
if ($os_mode == 0)
$os_mode = TARGET_OS_UNIX;
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN')
$os_mode = TARGET_OS_WINDOWS;
$maxlen = 4096;
if ($os_mode === TARGET_OS_WINDOWS) $maxlen = 8192;
if (strlen($input) > $maxlen - 2) return "";
if ($os_mode === TARGET_OS_WINDOWS)
$output =
str_replace(['"', '%', '!'],
[' ', ' ', ' '],
$input);
# https://bugs.php.net/bug.php?id=69646
if (substr($output, -1) === "\\")
$k = 0; $n = strlen($output) - 1;
for (; $n >= 0 && substr($output, $n, 1) === "\\"; $n--, $k++);
if ($k % 2) $output .= "\\";
$output = "\"$output\"";
else
$output = str_replace("'", "'\''", $input);
$output = "'$output'";
if (strlen($output) > $maxlen) return "";
return $output;
它应该在功能上几乎等同于原生 PHP escapeshellarg()
,除了:
\UnexpectedValueException
而不是某种 PHP 错误,
它不会因为输入太长而发出错误,并且
在类 Unix 平台上硬编码为 4096 作为最大参数长度。
要使用这个替换功能:
# In Unix/Linux/macOS mode
\polyfill\escapeshellarg($blabla, \polyfill\TARGET_OS_UNIX);
# In Windows mode
\polyfill\escapeshellarg($blabla, \polyfill\TARGET_OS_WINDOWS);
# In auto-detect (running OS) mode
\polyfill\escapeshellarg($blabla);
参考
这是来自PHP 7.3.10 (./ext/standard/exec.c
) 的完整 C 实现:
PHPAPI zend_string *php_escape_shell_arg(char *str)
size_t x, y = 0;
size_t l = strlen(str);
zend_string *cmd;
uint64_t estimate = (4 * (uint64_t)l) + 3;
/* max command line length - two single quotes - \0 byte length */
if (l > cmd_max_len - 2 - 1)
php_error_docref(NULL, E_ERROR, "Argument exceeds the allowed length of %zu bytes", cmd_max_len);
return ZSTR_EMPTY_ALLOC();
cmd = zend_string_safe_alloc(4, l, 2, 0); /* worst case */
#ifdef PHP_WIN32
ZSTR_VAL(cmd)[y++] = '"';
#else
ZSTR_VAL(cmd)[y++] = '\'';
#endif
for (x = 0; x < l; x++)
int mb_len = php_mblen(str + x, (l - x));
/* skip non-valid multibyte characters */
if (mb_len < 0)
continue;
else if (mb_len > 1)
memcpy(ZSTR_VAL(cmd) + y, str + x, mb_len);
y += mb_len;
x += mb_len - 1;
continue;
switch (str[x])
#ifdef PHP_WIN32
case '"':
case '%':
case '!':
ZSTR_VAL(cmd)[y++] = ' ';
break;
#else
case '\'':
ZSTR_VAL(cmd)[y++] = '\'';
ZSTR_VAL(cmd)[y++] = '\\';
ZSTR_VAL(cmd)[y++] = '\'';
#endif
/* fall-through */
default:
ZSTR_VAL(cmd)[y++] = str[x];
#ifdef PHP_WIN32
if (y > 0 && '\\' == ZSTR_VAL(cmd)[y - 1])
int k = 0, n = y - 1;
for (; n >= 0 && '\\' == ZSTR_VAL(cmd)[n]; n--, k++);
if (k % 2)
ZSTR_VAL(cmd)[y++] = '\\';
ZSTR_VAL(cmd)[y++] = '"';
#else
ZSTR_VAL(cmd)[y++] = '\'';
#endif
ZSTR_VAL(cmd)[y] = '\0';
if (y > cmd_max_len + 1)
php_error_docref(NULL, E_ERROR, "Escaped argument exceeds the allowed length of %zu bytes", cmd_max_len);
zend_string_release_ex(cmd, 0);
return ZSTR_EMPTY_ALLOC();
if ((estimate - y) > 4096)
/* realloc if the estimate was way overill
* Arbitrary cutoff point of 4096 */
cmd = zend_string_truncate(cmd, y, 0);
ZSTR_LEN(cmd) = y;
return cmd;
// … [truncated] …
/* proto string escapeshellarg(string arg)
Quote and escape an argument for use in a shell command */
PHP_FUNCTION(escapeshellarg)
char *argument;
size_t argument_len;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(argument, argument_len)
ZEND_PARSE_PARAMETERS_END();
if (argument)
if (argument_len != strlen(argument))
php_error_docref(NULL, E_ERROR, "Input string contains NULL bytes");
return;
RETVAL_STR(php_escape_shell_arg(argument));
/* */
逻辑相当简单。以下是散文中的一些等效功能测试用例:
输入的字符串不能包含 NUL 字符。 应用于输入字符串, 在 Windows 模式下, 添加"
字符。
将所有"
、%
和!
字符替换为
。
如果末尾包含奇数个\
字符,则在末尾添加一个\
字符。 (Bug #69646)
附加一个"
字符。
在其他平台模式下,
添加 '
字符。
将所有'
字符替换为'\''
附加一个'
字符。
在 Windows 上,如果输出长度超过 8192 个字符,则发出 E_ERROR
并返回空字符串。
在其他平台上,如果输出长度超过 4096 个字符(或编译时覆盖的最大值),则发出 E_ERROR
并返回空字符串。
【讨论】:
$arg
未定义。应该是$input
吗?在这一行:if (strlen($arg) > $maxlen - 2) return "";
其次,这个 polyfill 功能是否在某些 packagegist 或 github repo 中可用?
@ttk:是的,错误的变量使用现在已经修复。同样不,此功能不在任何包或存储库中;我所做的只是把它写在这个答案中。
增加了对空字节的检测(至少在 Linux 上,不可能为参数转义空字节。关于 Windows 的 idk 但我怀疑它有什么不同)以上是关于如何在 Windows 上使用 escapeshellarg() 但“针对 Linux”(反之亦然)?的主要内容,如果未能解决你的问题,请参考以下文章
我们如何通过 git 在 windows 上使用 linux 命令?
如何使用 Visual Studio 2017 在 Windows 上构建 OpenSSL?