首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >解析PHP中的命令参数

解析PHP中的命令参数
EN

Stack Overflow用户
提问于 2013-07-25 11:40:46
回答 10查看 5.2K关注 0票数 16

有没有一种原生的"PHP方法“来解析string中的命令参数?例如,给定以下string

代码语言:javascript
复制
foo "bar \"baz\"" '\'quux\''

我想创建以下array

代码语言:javascript
复制
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:下面是参数的预期输入->输出的更全面的示例:

代码语言:javascript
复制
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
EN

回答 10

Stack Overflow用户

发布于 2013-08-14 18:37:00

我已经计算出了下面的表达式,以匹配不同的封闭和转义:

代码语言:javascript
复制
$pattern = <<<REGEX
/
(?:
  " ((?:(?<=\\\\)"|[^"])*) "
|
  ' ((?:(?<=\\\\)'|[^'])*) '
|
  (\S+)
)
/x
REGEX;

preg_match_all($pattern, $input, $matches, PREG_SET_ORDER);

它匹配:

  1. 两个双引号,其中的双引号可以转义为与#1相同的双引号,但用于

然后,您需要(小心地)删除转义字符:

代码语言:javascript
复制
$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);

更新

为了好玩,我编写了一个更正式的解析器,如下所示。它不会给你带来更好的性能,它大约比正则表达式慢三倍,这主要是因为它的面向对象的性质。我认为它的优势更多的是理论上的而不是实际的:

代码语言:javascript
复制
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一次遍历字符串中的一个字符:

代码语言:javascript
复制
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;
    }
}
票数 11
EN

Stack Overflow用户

发布于 2013-08-14 04:58:17

那么,您也可以使用递归正则表达式构建此解析器:

代码语言:javascript
复制
$regex = "([a-zA-Z0-9.-]+|\"([^\"\\\\]+(?1)|\\\\.(?1)|)\"|'([^'\\\\]+(?2)|\\\\.(?2)|)')s";

现在这有点长了,所以让我们把它分开:

代码语言:javascript
复制
$identifier = '[a-zA-Z0-9.-]+';
$doubleQuotedString = "\"([^\"\\\\]+(?1)|\\\\.(?1)|)\"";
$singleQuotedString = "'([^'\\\\]+(?2)|\\\\.(?2)|)'";
$regex = "($identifier|$doubleQuotedString|$singleQuotedString)s";

那么这是如何工作的呢?那么,标识符应该是显而易见的.

这两个带引号的子模式基本上是相同的,所以让我们看一下单引号字符串:

代码语言:javascript
复制
'([^'\\\\]+(?2)|\\\\.(?2)|)'

实际上,这是一个引号字符,后跟一个递归子模式,后跟一个结束引号。

魔术发生在子模式中。

代码语言:javascript
复制
[^'\\\\]+(?2)

该部分基本上使用任何非引号和非转义字符。我们不关心它们,所以把它们吃掉吧。然后,如果我们遇到引号或反斜杠,则触发再次匹配整个子模式的尝试。

代码语言:javascript
复制
\\\\.(?2)

如果我们可以使用反斜杠,那么使用下一个字符(而不关心它是什么),然后再次递归。

最后,我们有一个空组件(如果转义字符是最后一个,或者如果没有转义字符)。

在提供的测试输入@HamZa上运行此命令将返回相同的结果:

代码语言:javascript
复制
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\\""
}

发生的主要区别是在效率方面。这个模式应该回溯较少(因为它是一个递归模式,对于格式良好的字符串应该几乎没有回溯),而另一个正则表达式是一个非递归的正则表达式,将回溯每个字符(这是*之后的?强制执行的,非贪婪模式消耗)。

对于简短的输入,这无关紧要。提供的测试用例,它们运行在彼此的几个%内(误差幅度大于差值)。但是只有一个没有转义序列的长字符串:

代码语言:javascript
复制
"with a really long escape sequence match that will force a large backtrack loop"

差异是显著的(100次运行):

递归的

  • float(0.00030398368835449)
  • Backtracking:float(0.00055909156799316)

当然,我们可以通过大量的转义序列部分地失去这一优势:

代码语言:javascript
复制
"This is \" A long string \" With a\lot \of \"escape \sequences"

递归的

  • float(0.00040411949157715)
  • Backtracking:float(0.00045490264892578)

但请注意,长度仍然占主导地位。这是因为backtracker在O(n^2)上扩展,而递归解决方案在O(n)上扩展。然而,由于递归模式总是需要至少递归一次,因此它比短字符串上的回溯解决方案要慢:

代码语言:javascript
复制
"1"

递归的

  • float(0.0002598762512207)
  • Backtracking:float(0.00017595291137695)

权衡似乎发生在15个字符左右...但两者都足够快,除非您解析几KB或MB的数据,否则不会有什么不同……但这是值得讨论的。

在正常的输入上,它不会有什么显著的不同。但是,如果匹配的字节数超过几百个,则可能会开始显著增加...

编辑

如果需要处理任意的“裸词”(未加引号的字符串),则可以将原始正则表达式更改为:

代码语言:javascript
复制
$regex = "([^\s'\"]\S*|\"([^\"\\\\]+(?1)|\\\\.(?1)|)\"|'([^'\\\\]+(?2)|\\\\.(?2)|)')s";

然而,这实际上取决于你的语法和你认为是不是一个命令。我建议将你所期望的语法正式化。

票数 8
EN

Stack Overflow用户

发布于 2013-08-14 05:26:23

如果你想遵循这样的解析规则,就像在shell中一样,我认为有一些边缘情况是不容易用正则表达式覆盖的,因此你可能想要写一个方法来做到这一点:

代码语言:javascript
复制
$string = 'foo "bar \"baz\"" \'\\\'quux\\\'\'';
echo $string, "\n";
print_r(StringUtil::separate_quoted($string));

输出:

代码语言:javascript
复制
foo "bar \"baz\"" '\'quux\''
Array
(
    [0] => foo
    [1] => bar "baz"
    [2] => 'quux'
)

我猜这和你要找的差不多。示例中使用的函数可以针对转义字符和引号进行配置,如果愿意,您甚至可以使用[ ]之类的括号来构成“引号”。

要允许本机bytesafe字符串不是每个字节一个字符,您可以传递一个数组而不是一个字符串。该数组需要为每个值包含一个字符作为二进制安全字符串。例如,以UTF-8的形式传递NFC形式的unicode,每个数组值一个代码点,这应该可以完成unicode的工作。

票数 5
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/17848618

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档