在PHP中解析命令参数

FtD*_*Xw6 16 php parsing command-line-arguments

是否存在从PHP解析命令参数的本地"PHP方式" string?例如,给出以下内容string:

foo "bar \"baz\"" '\'quux\''
Run Code Online (Sandbox Code Playgroud)

我想创建以下内容array:

array(3) {
  [0] =>
  string(3) "foo"
  [1] =>
  string(7) "bar "baz""
  [2] =>
  string(6) "'quux'"
}
Run Code Online (Sandbox Code Playgroud)

我已经尝试过利用token_get_all(),但PHP的变量插值语法(例如"foo ${bar} baz")在我的游行中几乎下雨了.

我完全知道我可以编写自己的解析器.命令参数语法是超级简单的,但如果有一个现有的本地方法来做,我更喜欢滚动我自己.

编辑:请注意,我正在寻找解析来自a string,而不是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
Run Code Online (Sandbox Code Playgroud)

Ham*_*mZa 11

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

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

我们的想法是捕获一个引用并将其分组以记住它是单引号还是双引号.消极的外观是为了确保我们不匹配转义报价.\1用于匹配第二对引号.最后,我们使用替换来匹配任何不是空格的东西.此解决方案非常方便,几乎适用于支持lookbehinds和backreferences的任何语言/风格.当然,此解决方案希望报价已关闭.结果见于第0组.

让我们用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]);
Run Code Online (Sandbox Code Playgroud)

如果你想知道为什么我用了4个反斜杠.然后看看我以前的答案.

产量

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

world\""
    [7] => "escaped escape\\"
)
Run Code Online (Sandbox Code Playgroud)

                                       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);
Run Code Online (Sandbox Code Playgroud)

Online php demo

  • `preg_match_all('#(?<!\\\\)("| \')(?:[^ \\\\] | \\\\.)*?\ 1 |\S +#s',$ string ,$ match);` (2认同)

Ja͢*_*͢ck 10

我已经计算出以下表达式来匹配各种外壳和擒纵机构:

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

preg_match_all($pattern, $input, $matches, PREG_SET_ORDER);
Run Code Online (Sandbox Code Playgroud)

它匹配:

  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);
Run Code Online (Sandbox Code Playgroud)

更新

为了它的乐趣,我写了一个更正式的解析器,概述如下.它不会给你更好的性能,它比正则表达式慢大约三倍,主要是因为它的面向对象性质.我认为优势更具学术性而非实际性:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

它基于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;
    }
}
Run Code Online (Sandbox Code Playgroud)


irc*_*ell 8

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

$regex = "([a-zA-Z0-9.-]+|\"([^\"\\\\]+(?1)|\\\\.(?1)|)\"|'([^'\\\\]+(?2)|\\\\.(?2)|)')s";
Run Code Online (Sandbox Code Playgroud)

现在有点长,让我们分解一下:

$identifier = '[a-zA-Z0-9.-]+';
$doubleQuotedString = "\"([^\"\\\\]+(?1)|\\\\.(?1)|)\"";
$singleQuotedString = "'([^'\\\\]+(?2)|\\\\.(?2)|)'";
$regex = "($identifier|$doubleQuotedString|$singleQuotedString)s";
Run Code Online (Sandbox Code Playgroud)

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

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

'([^'\\\\]+(?2)|\\\\.(?2)|)'
Run Code Online (Sandbox Code Playgroud)

真的,这是一个引号字符,后跟一个递归子模式,后跟一个结束引号.

魔术发生在子模式中.

[^'\\\\]+(?2)
Run Code Online (Sandbox Code Playgroud)

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

\\\\.(?2)
Run Code Online (Sandbox Code Playgroud)

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

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

在提供的测试输入@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\\""
}
Run Code Online (Sandbox Code Playgroud)

发生的主要差异在于效率.这个模式应该回溯较少(因为它是一个递归模式,对于格式良好的字符串,旁边应该没有回溯),其中另一个正则表达式是非递归正则表达式并且将回溯每个单个字符(这就是?后面的*力量) ,非贪婪模式消费).

对于短输入,这无关紧要.提供的测试用例,它们在彼此的几个百分点内运行(误差范围大于差异).但是有一个没有转义序列的长字符串:

"with a really long escape sequence match that will force a large backtrack loop"
Run Code Online (Sandbox Code Playgroud)

差异很大(100次运行):

  • 递归: float(0.00030398368835449)
  • 回溯: float(0.00055909156799316)

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

"This is \" A long string \" With a\lot \of \"escape \sequences"
Run Code Online (Sandbox Code Playgroud)
  • 递归: float(0.00040411949157715)
  • 回溯: float(0.00045490264892578)

但请注意,长度仍占主导地位.那是因为回溯缩放O(n^2),递归解决方案在那里缩放O(n).但是,由于递归模式总是需要至少递归一次,所以它比短字符串上的回溯解决方案慢:

"1"
Run Code Online (Sandbox Code Playgroud)
  • 递归: float(0.0002598762512207)
  • 回溯: float(0.00017595291137695)

权衡似乎发生在大约15个字符......但两者都足够快,除非你正在解析几个KB或MB的数据,否则它不会有所作为......但值得讨论......

在合理的投入上,它不会产生重大影响.但如果你匹配超过几百个字节,它可能会开始显着加起来......

编辑

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

$regex = "([^\s'\"]\S*|\"([^\"\\\\]+(?1)|\\\\.(?1)|)\"|'([^'\\\\]+(?2)|\\\\.(?2)|)')s";
Run Code Online (Sandbox Code Playgroud)

但是,它实际上取决于你的语法以及你认为的命令与否.我建议你正式化你期望的语法......


hak*_*kre 5

如果你想遵循那些解析和shell中的解析规则,我认为有些边缘情况不容易用正则表达式覆盖,因此你可能想编写一个执行此操作的方法(例子):

$string = 'foo "bar \"baz\"" \'\\\'quux\\\'\'';
echo $string, "\n";
print_r(StringUtil::separate_quoted($string));
Run Code Online (Sandbox Code Playgroud)

输出:

foo "bar \"baz\"" '\'quux\''
Array
(
    [0] => foo
    [1] => bar "baz"
    [2] => 'quux'
)
Run Code Online (Sandbox Code Playgroud)

我想这几乎与你想要的相匹配.示例中使用的函数可以为转义字符和引号配置,[ ]如果您愿意,甚至可以使用括号表示"引号".

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


Bab*_*aba 5

您可以只使用str_getcsv,而很少进行带斜线修剪的整容手术

范例:

$str =<<<DATA
"bar \"baz\"" '\'quux\''
"foo"
'foo'
"foo'foo"
'foo"foo'
"foo\"foo"
'foo\'foo'
"foo\foo"
"foo\\foo"
"foo foo"
'foo foo' "foo\\foo" \'quux\' \"baz\" "foo'foo"
DATA;


$str = explode("\n", $str);

foreach($str as $line) {
    $line = array_map("stripslashes",str_getcsv($line," "));
    print_r($line);
}
Run Code Online (Sandbox Code Playgroud)

输出量

Array
(
    [0] => bar "baz"
    [1] => ''quux''
)
Array
(
    [0] => foo
)
Array
(
    [0] => 'foo'
)
Array
(
    [0] => foo'foo
)
Array
(
    [0] => 'foo"foo'
)
Array
(
    [0] => foo"foo
)
Array
(
    [0] => 'foo'foo'
)
Array
(
    [0] => foooo
)
Array
(
    [0] => foofoo
)
Array
(
    [0] => foo foo
)
Array
(
    [0] => 'foo
    [1] => foo'
    [2] => foofoo
    [3] => 'quux'
    [4] => "baz"
    [5] => foo'foo
)
Run Code Online (Sandbox Code Playgroud)

警告

没有什么比不通用的参数格式更好地具体化特定格式了,最简单的就是CSV

 app.php arg1 "arg 2" "'arg 3'" > 4 
Run Code Online (Sandbox Code Playgroud)

使用CSV,您可以简单地得到以下输出

Array
(
    [0] => app.php
    [1] => arg1
    [2] => arg 2
    [3] => 'arg 3'
    [4] => >
    [5] => 4
)
Run Code Online (Sandbox Code Playgroud)


小智 -1

我建议类似:

$str = <<<EOD
foo "bar \"baz\"" '\'quux\''
EOD;

$match = preg_split("/('(?:.*)(?<!\\\\)(?>\\\\\\\\)*'|\"(?:.*)(?<!\\\\)(?>\\\\\\\\)*\")/U", $str, null, PREG_SPLIT_DELIM_CAPTURE);

var_dump(array_filter(array_map('trim', $match)));
Run Code Online (Sandbox Code Playgroud)

在以下方面的帮助下:字符串到数组,用正则表达式的单引号和双引号分隔

之后您仍然需要对数组中的字符串进行转义。

array(3) {
  [0]=>
  string(3) "foo"
  [1]=>
  string(13) ""bar \"baz\"""
  [3]=>
  string(10) "'\'quux\''"
}
Run Code Online (Sandbox Code Playgroud)

但你明白了。