Разбор аргументов команды в 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 , а не из командной строки / shell.


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 

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\\" ) 

Онлайн-демо-версия regex Онлайн-демо-версия


Удаление котировок

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

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

Демо-версия

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

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

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

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

Эта часть в основном потребляет любой символ без кавычек и не-escape. Мы не заботимся о них, поэтому съедаем их. Затем, если мы столкнемся либо с цитатой, либо с обратной косой чертой, попробуйте повторить попытку согласования всей подматрицы.

 \\\\.(?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)
  • Backtracking: 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 символов … Но оба они достаточно быстры, что это не изменит ситуацию, если вы не разберете несколько КБ или МБ данных … Но стоит обсудить …

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

редактировать

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

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

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

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

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

Вы можете просто использовать 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); } 

Вывод

 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 ) 

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

Создайте исполняемый PHP-скрипт 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, давайте! Вы должны доверять вводам или делать так много фильтрации, что простое регулярное выражение может быть быстрее (я не углубляюсь в это).

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

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

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

Основываясь на ответе Хамза :

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

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

Анализ аргументов

Существует пакет, в котором все аргументы разбираются в вещи 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

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

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

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

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

Вам все равно придется unescape строки в массиве после.

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

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

На самом деле нет никакой нативной функции для синтаксического анализа команд. Тем не менее, я создал функцию, которая делает трюк изначально на 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; }