有没有一种原生的"PHP方法“来解析string
中的命令参数?例如,给定以下string
foo "bar \"baz\"" '\'quux\''
我想创建以下array
array(3) {
[0] =>
string(3) "foo"
[1] =>
string(7) "bar "baz""
[2] =>
string(6) "'quux'"
}
我已经尝试过利用token_get_all()
,但是PHP的可变插值语法(例如"foo ${bar} baz"
)在我的尝试中遇到了很大困难。
我非常清楚,我可以编写自己的解析器。命令参数语法非常简单,但如果有一种现有的本机方法可以做到这一点,我更喜欢它而不是滚动我自己的方法。
shell编辑:请注意,我希望解析来自string
**,的参数,而不是来自/命令行。**
EDIT #2:下面是参数的预期输入->输出的更全面的示例:
foo -> foo
"foo" -> foo
'foo' -> foo
"foo'foo" -> foo'foo
'foo"foo' -> foo"foo
"foo\"foo" -> foo"foo
'foo\'foo' -> foo'foo
"foo\foo" -> foo\foo
"foo\\foo" -> foo\foo
"foo foo" -> foo foo
'foo foo' -> foo foo
发布于 2013-08-14 18:37:00
我已经计算出了下面的表达式,以匹配不同的封闭和转义:
$pattern = <<<REGEX
/
(?:
" ((?:(?<=\\\\)"|[^"])*) "
|
' ((?:(?<=\\\\)'|[^'])*) '
|
(\S+)
)
/x
REGEX;
preg_match_all($pattern, $input, $matches, PREG_SET_ORDER);
它匹配:
然后,您需要(小心地)删除转义字符:
$args = array();
foreach ($matches as $match) {
if (isset($match[3])) {
$args[] = $match[3];
} elseif (isset($match[2])) {
$args[] = str_replace(['\\\'', '\\\\'], ["'", '\\'], $match[2]);
} else {
$args[] = str_replace(['\\"', '\\\\'], ['"', '\\'], $match[1]);
}
}
print_r($args);
更新
为了好玩,我编写了一个更正式的解析器,如下所示。它不会给你带来更好的性能,它大约比正则表达式慢三倍,这主要是因为它的面向对象的性质。我认为它的优势更多的是理论上的而不是实际的:
class ArgvParser2 extends StringIterator
{
const TOKEN_DOUBLE_QUOTE = '"';
const TOKEN_SINGLE_QUOTE = "'";
const TOKEN_SPACE = ' ';
const TOKEN_ESCAPE = '\\';
public function parse()
{
$this->rewind();
$args = [];
while ($this->valid()) {
switch ($this->current()) {
case self::TOKEN_DOUBLE_QUOTE:
case self::TOKEN_SINGLE_QUOTE:
$args[] = $this->QUOTED($this->current());
break;
case self::TOKEN_SPACE:
$this->next();
break;
default:
$args[] = $this->UNQUOTED();
}
}
return $args;
}
private function QUOTED($enclosure)
{
$this->next();
$result = '';
while ($this->valid()) {
if ($this->current() == self::TOKEN_ESCAPE) {
$this->next();
if ($this->valid() && $this->current() == $enclosure) {
$result .= $enclosure;
} elseif ($this->valid()) {
$result .= self::TOKEN_ESCAPE;
if ($this->current() != self::TOKEN_ESCAPE) {
$result .= $this->current();
}
}
} elseif ($this->current() == $enclosure) {
$this->next();
break;
} else {
$result .= $this->current();
}
$this->next();
}
return $result;
}
private function UNQUOTED()
{
$result = '';
while ($this->valid()) {
if ($this->current() == self::TOKEN_SPACE) {
$this->next();
break;
} else {
$result .= $this->current();
}
$this->next();
}
return $result;
}
public static function parseString($input)
{
$parser = new self($input);
return $parser->parse();
}
}
它基于StringIterator
一次遍历字符串中的一个字符:
class StringIterator implements Iterator
{
private $string;
private $current;
public function __construct($string)
{
$this->string = $string;
}
public function current()
{
return $this->string[$this->current];
}
public function next()
{
++$this->current;
}
public function key()
{
return $this->current;
}
public function valid()
{
return $this->current < strlen($this->string);
}
public function rewind()
{
$this->current = 0;
}
}
发布于 2013-08-14 04:58:17
那么,您也可以使用递归正则表达式构建此解析器:
$regex = "([a-zA-Z0-9.-]+|\"([^\"\\\\]+(?1)|\\\\.(?1)|)\"|'([^'\\\\]+(?2)|\\\\.(?2)|)')s";
现在这有点长了,所以让我们把它分开:
$identifier = '[a-zA-Z0-9.-]+';
$doubleQuotedString = "\"([^\"\\\\]+(?1)|\\\\.(?1)|)\"";
$singleQuotedString = "'([^'\\\\]+(?2)|\\\\.(?2)|)'";
$regex = "($identifier|$doubleQuotedString|$singleQuotedString)s";
那么这是如何工作的呢?那么,标识符应该是显而易见的.
这两个带引号的子模式基本上是相同的,所以让我们看一下单引号字符串:
'([^'\\\\]+(?2)|\\\\.(?2)|)'
实际上,这是一个引号字符,后跟一个递归子模式,后跟一个结束引号。
魔术发生在子模式中。
[^'\\\\]+(?2)
该部分基本上使用任何非引号和非转义字符。我们不关心它们,所以把它们吃掉吧。然后,如果我们遇到引号或反斜杠,则触发再次匹配整个子模式的尝试。
\\\\.(?2)
如果我们可以使用反斜杠,那么使用下一个字符(而不关心它是什么),然后再次递归。
最后,我们有一个空组件(如果转义字符是最后一个,或者如果没有转义字符)。
在提供的测试输入@HamZa上运行此命令将返回相同的结果:
array(8) {
[0]=>
string(3) "foo"
[1]=>
string(13) ""bar \"baz\"""
[2]=>
string(10) "'\'quux\''"
[3]=>
string(9) "'foo"bar'"
[4]=>
string(9) ""baz'boz""
[5]=>
string(5) "hello"
[6]=>
string(16) ""regex
world\"""
[7]=>
string(18) ""escaped escape\\""
}
发生的主要区别是在效率方面。这个模式应该回溯较少(因为它是一个递归模式,对于格式良好的字符串应该几乎没有回溯),而另一个正则表达式是一个非递归的正则表达式,将回溯每个字符(这是*
之后的?
强制执行的,非贪婪模式消耗)。
对于简短的输入,这无关紧要。提供的测试用例,它们运行在彼此的几个%内(误差幅度大于差值)。但是只有一个没有转义序列的长字符串:
"with a really long escape sequence match that will force a large backtrack loop"
差异是显著的(100次运行):
递归的
float(0.00030398368835449)
float(0.00055909156799316)
当然,我们可以通过大量的转义序列部分地失去这一优势:
"This is \" A long string \" With a\lot \of \"escape \sequences"
递归的
float(0.00040411949157715)
float(0.00045490264892578)
但请注意,长度仍然占主导地位。这是因为backtracker在O(n^2)
上扩展,而递归解决方案在O(n)
上扩展。然而,由于递归模式总是需要至少递归一次,因此它比短字符串上的回溯解决方案要慢:
"1"
递归的
float(0.0002598762512207)
float(0.00017595291137695)
权衡似乎发生在15个字符左右...但两者都足够快,除非您解析几KB或MB的数据,否则不会有什么不同……但这是值得讨论的。
在正常的输入上,它不会有什么显著的不同。但是,如果匹配的字节数超过几百个,则可能会开始显著增加...
编辑
如果需要处理任意的“裸词”(未加引号的字符串),则可以将原始正则表达式更改为:
$regex = "([^\s'\"]\S*|\"([^\"\\\\]+(?1)|\\\\.(?1)|)\"|'([^'\\\\]+(?2)|\\\\.(?2)|)')s";
然而,这实际上取决于你的语法和你认为是不是一个命令。我建议将你所期望的语法正式化。
发布于 2013-08-14 05:26:23
如果你想遵循这样的解析规则,就像在shell中一样,我认为有一些边缘情况是不容易用正则表达式覆盖的,因此你可能想要写一个方法来做到这一点:
$string = 'foo "bar \"baz\"" \'\\\'quux\\\'\'';
echo $string, "\n";
print_r(StringUtil::separate_quoted($string));
输出:
foo "bar \"baz\"" '\'quux\''
Array
(
[0] => foo
[1] => bar "baz"
[2] => 'quux'
)
我猜这和你要找的差不多。示例中使用的函数可以针对转义字符和引号进行配置,如果愿意,您甚至可以使用[
]
之类的括号来构成“引号”。
要允许本机bytesafe字符串不是每个字节一个字符,您可以传递一个数组而不是一个字符串。该数组需要为每个值包含一个字符作为二进制安全字符串。例如,以UTF-8的形式传递NFC形式的unicode,每个数组值一个代码点,这应该可以完成unicode的工作。
https://stackoverflow.com/questions/17848618
复制相似问题