在 PHP 中解析命令参数

2022-08-31 00:17:58

有没有一种原生的“PHP方式”来解析来自一个的命令参数?例如,给定以下内容:stringstring

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 /命令行解析参数。


编辑#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

答案 1

正则表达式非常强大:.那么这个表达式是什么意思呢?(?s)(?<!\\)("|')(?:[^\\]|\\.)*?\1|\S+

  • (?s):设置修饰符以将换行符与点匹配s.
  • (?<!\\):负向查看,检查下一个令牌之前是否没有反斜杠
  • ("|'):匹配单引号或双引号,并将其放在组 1 中
  • (?:[^\\]|\\.)*?:匹配所有未包含 \ 的内容,或将 \ 与紧随其后(转义)字符匹配
  • \1:匹配第一组中匹配的内容
  • |:或
  • \S+:匹配除空格以外的任何内容一次或多次。

这个想法是捕获报价并将其分组,以记住它是单引号还是双引号。负面的后缀是为了确保我们不匹配转义引号。 用于匹配第二对引号。最后,我们使用交替来匹配任何不是空格的内容。此解决方案非常方便,几乎适用于支持查找隐藏和反向引用的任何语言/风格。当然,此解决方案期望报价是闭合的。结果位于组 0 中。\1

让我们用PHP实现它:

$string = <<<INPUT
foo "bar \"baz\"" '\'quux\''
'foo"bar' "baz'boz"
hello "regex

world\""
"escaped escape\\\\"
INPUT;

preg_match_all('#(?<!\\\\)("|\')(?:[^\\\\]|\\\\.)*?\1|\S+#s', $string, $matches);
print_r($matches[0]);

如果你想知道为什么我使用4个反斜杠。然后看看我之前的答案

输出

Array
(
    [0] => foo
    [1] => "bar \"baz\""
    [2] => '\'quux\''
    [3] => 'foo"bar'
    [4] => "baz'boz"
    [5] => hello
    [6] => "regex

world\""
    [7] => "escaped escape\\"
)

                                       Online regex demo                                 Online php demo


删除引号

使用命名组和简单循环非常简单:

preg_match_all('#(?<!\\\\)("|\')(?<escaped>(?:[^\\\\]|\\\\.)*?)\1|(?<unescaped>\S+)#s', $string, $matches, PREG_SET_ORDER);

$results = array();
foreach($matches as $array){
   if(!empty($array['escaped'])){
      $results[] = $array['escaped'];
   }else{
      $results[] = $array['unescaped'];
   }
}
print_r($results);

Online php demo


答案 2

我制定了以下表达式来匹配各种外壳和擒纵机构:

$pattern = <<<REGEX
/
(?:
  " ((?:(?<=\\\\)"|[^"])*) "
|
  ' ((?:(?<=\\\\)'|[^'])*) '
|
  (\S+)
)
/x
REGEX;

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

它匹配:

  1. 两个双引号,其中一个双引号可以转义
  2. 与 #1 相同,但适用于单引号
  3. 不带引号的字符串

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

$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;
    }
}

推荐