Разбор аргументов команды в PHP

Есть ли собственный способ 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") довольно много дождя на моем параде.

Я прекрасно знаю, что могу написать свой собственный парсер. Синтаксис синтаксиса команды супер упрощен, но если есть существующий собственный способ сделать это, я бы предпочел, чтобы это перевернуло мою собственную.

EDIT: Обратите внимание, что я ищу, чтобы проанализировать аргументы из 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

Ответы

Ответ 1

Regexes достаточно мощные: (?s)(?<!\\)("|')(?:[^\\]|\\.)*?\1|\S+. Итак, что означает это выражение?

  • (?s): установите модификатор s в соответствие с символами новой строки с точкой .
  • (?<!\\): отрицательный lookbehind, проверьте, нет ли обратной косой черты перед следующим токеном.
  • ("|'): сопоставить одиночную или двойную кавычку и поместить ее в группу 1
  • (?:[^\\]|\\.)*?: сопоставить все, что не \, или совместить\с символом, следующим за (экранированным)
  • \1: сопоставить то, что сопоставляется в первой группе
  • |: или
  • \S+: сопоставить что-либо, кроме пробелов, один или несколько раз.

Идея состоит в том, чтобы зафиксировать цитату и сгруппировать ее, чтобы помнить, является ли она одиночной или двойной. Отрицательные lookbehinds там, чтобы убедиться, что мы не отвечаем экранированные кавычки. \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]);

Если вам интересно, почему я использовал 4 обратной косой черты. Затем просмотрите предыдущий ответ .

Выход

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

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

                                        Демо-версия онлайн-регге                                 Демо-версия онлайн php


Удаление цитат

Достаточно просто использовать именованные группы и простой цикл:

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);

Демо-версия онлайн php

Ответ 2

Я разработал следующее выражение для соответствия различным приложениям и спускам:

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

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

Он соответствует:

  • Две двойные кавычки, внутри которых может быть скрыта двойная кавычка
  • То же, что и # 1, но для одинарных кавычек
  • Некотированная строка

Затем вам необходимо (осторожно) удалить экранированные символы:

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

Ответ 3

Ну, вы также можете собрать этот парсер с рекурсивным регулярным выражением:

$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)

Если мы сможем использовать обратную косую черту, тогда используйте следующий символ (не заботясь о том, что это такое) и снова перезапустите.

Наконец, у нас есть пустой компонент (если последний символ - последний, или нет escape-символа).

Запуск этого на тестовом входе @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\\""
}

Основное различие, которое происходит, - это эффективность. Этот шаблон должен отступать меньше (поскольку он является рекурсивным шаблоном, он должен быть рядом с отсутствием обратного отсчета для хорошо сформированной строки), где другое регулярное выражение является нерекурсивным регулярным выражением и будет отбрасывать каждый отдельный символ (что то, что ? после сил *, потребления неживого шаблона).

Для коротких входов это не имеет значения. Приведенный тестовый пример, они работают в пределах нескольких% друг от друга (погрешность больше разницы). Но с одной длинной строкой без 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)
  • Backtracking: float(0.00017595291137695)

Компромисс, по-видимому, происходит примерно по 15 символов... Но оба они достаточно быстры, что это не будет иметь никакого значения, если вы не разберете несколько КБ или МБ данных... Но стоит обсудить...

На нормальных входах это не будет иметь существенных отличий. Но если вы сопоставляете более нескольких сотен байт, это может значительно увеличиться...

Изменить

Если вам нужно обрабатывать произвольные "голые слова" (некорректные строки), вы можете изменить исходное регулярное выражение на:

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

Однако, это действительно зависит от вашей грамматики и того, что вы считаете командой или нет. Я бы предложил формализовать грамматику, которую вы ожидаете...

Ответ 4

Если вы хотите следовать правилам такого синтаксического анализа, которые есть, а также в оболочке, есть некоторые краевые случаи, которые, я думаю, нелегко покрыть регулярными выражениями, и поэтому вы можете написать метод, который делает это (пример):

$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'
)

Я думаю, это в значительной степени соответствует тому, что вы ищете. Функция, используемая в примере, может быть сконфигурирована как для escape-символа, так и для кавычек, вы можете даже использовать скобки, например [ ], чтобы сформировать "цитату", если хотите.

Чтобы разрешить помимо собственных байтов-байтов с одним символом на каждый байт, вы можете передать массив вместо строки. массив должен содержать один символ на значение в виде двоичной безопасной строки. например передайте unicode в форме NFC как UTF-8 с одной кодовой точкой на значение массива, и это должно выполнить задание для unicode.

Ответ 5

Вы можете просто использовать str_getcsv и сделать несколько косметических операций с помощью stripslashes и trim

Пример:

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

Выход

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
)

Предостережение

Нет ничего похожего на неформатный формат аргумента, лучше всего использовать специальный формат, и наиболее простым из них является CSV

Пример

 app.php arg1 "arg 2" "'arg 3'" > 4 

Используя CSV, вы можете просто иметь этот выход

Array
(
    [0] => app.php
    [1] => arg1
    [2] => arg 2
    [3] => 'arg 3'
    [4] => >
    [5] => 4
)

Ответ 6

Поскольку вы запрашиваете собственный способ сделать это, и PHP не предоставляет никакой функции, которая отображала бы создание $argv, вы могли бы обмануть это отсутствие следующим образом:

Создать исполняемый файл PHP script foo.php:

<?php

// Skip this file name
array_shift( $argv );

// output an valid PHP code
echo 'return '. var_export( $argv, 1 ).';';

?>

И используйте его для извлечения аргументов, как это делает PHP, если вы выполните команду $command:

function parseCommand( $command )
{
    return eval(
        shell_exec( "php foo.php ".$command )
    );
}


$command = <<<CMD
foo "bar \"baz\"" '\'quux\''
CMD;


$args = parseCommand( $command );

var_dump( $args );

Преимущества:

  • Очень простой код
  • Должно быть быстрее, чем любое регулярное выражение
  • 100% близко к поведению PHP

Недостатки:

  • Требуется выполнение привилегий на хосте
  • Shell exec + eval на том же $var, пусть участник! Вы должны доверять вводам или делать так много фильтрации, что простое регулярное выражение может быть быстрее (я не углубляюсь в это).

Ответ 7

Я бы порекомендовал пойти другим путем. Уже существует "стандартный" способ использования аргументов командной строки. он называется get_opts:

http://php.net/manual/en/function.getopt.php

Я бы предположил, что вы меняете свой script на использование get_opts, тогда любой, кто использует ваш script, будет передавать параметры таким образом, который им знаком, и вроде "отраслевого стандарта" вместо того, чтобы учиться на своем пути делать вещи.

Ответ 8

Основываясь на ответе HamZa:

function parse_cli_args($cmd) {
    preg_match_all('#(?<!\\\\)("|\')(?<escaped>(?:[^\\\\]|\\\\.)*?)\1|(?<unescaped>\S+)#s', $cmd, $matches, PREG_SET_ORDER);
    $results = [];
    foreach($matches as $array){
        $results[] = !empty($array['escaped']) ? $array['escaped'] : $array['unescaped'];
    }
    return $results;
}

Ответ 9

Я написал несколько пакетов для консольных взаимодействий:

Разбор аргументов

Существует пакет, который обрабатывает весь аргумент weew/php-console-arguments

Пример:

$parser = new ArgumentsParser();
$args = $parser->parse('command:name arg1 arg2 --flag="custom \"value" -f="1+1=2" -vvv');

$args будет массивом:

['command:name', 'arg1', 'arg2', '--flag', 'custom "value', '-f', '1+1=2', '-v', '-v', '-v']

Аргументы могут быть сгруппированы:

$args = $parser->group($args);

$args станет:

['arguments' => ['command:name', 'arg1', 'arg2'], 'options' => ['--flag' => 1, '-f' => 1, '-v' => 1], '--flag' => ['custom "value'], '-f' => ['1+1=2'], '-v' => []]

Он может сделать гораздо больше, просто проверьте readme.

Моделирование вывода

Вам может понадобиться пакет для моделирования выпуска weew/php-console-formatter

Консольное приложение

Пакеты выше могут использоваться автономно или в сочетании с причудливым консольным приложением приложения weew/php-console

Примечание. Эти решения не являются родными, но могут быть полезны для некоторых людей.

Ответ 10

Я предлагаю что-то вроде:

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

С помощью: строки в массив, разделенной одиночными и двойными кавычками для regexp

Вам по-прежнему нужно unescape строки в массиве после.

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

Но вы получите картину.

Ответ 11

На самом деле не существует встроенной функции для синтаксического анализа команд. Тем не менее, я создал функцию, которая делает трюк изначально на PHP. Используя str_replace несколько раз, вы можете преобразовать строку в какой-то массив конвертируемых. Я не знаю, как быстро вы считаете, но при запуске запроса 400 раз самый медленный запрос находился под 34 микросекундами.

function get_array_from_commands($string) {
    /*
    **  Turns a command string into a field
    **  of arrays through multiple lines of 
    **  str_replace, until we have a single
    **  string to split using explode().
    **  Returns an array.
    */

    // replace single quotes with their related
    // ASCII escape character
    $string = str_replace("\'","&#x27;",$string);
    // Do the same with double quotes
    $string = str_replace("\\\"","&quot;",$string);
    // Now turn all remaining single quotes into double quotes
    $string = str_replace("'","\"",$string);
    // Turn " " into " so we don't replace it too many times
    $string = str_replace("\" \"","\"",$string);
    // Turn the remaining double quotes into @@@ or some other value
    $string = str_replace("\"","@@@",$string);
    // Explode by @@@ or value listed above
    $string = explode("@@@",$string);
    return $string;
}