with script passing pass from example cli argvs argumentos php parsing command-line-arguments

script - php command pass arguments



AnĂ¡lisis de los argumentos del comando en PHP (11)

Basado en la respuesta de 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; }

¿Existe una "forma PHP" nativa para analizar los argumentos de comando de una string ? Por ejemplo, dada la siguiente string :

foo "bar /"baz/"" ''/'quux/'''

Me gustaría crear la siguiente array :

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

Ya he intentado aprovechar token_get_all() , pero la sintaxis de interpolación variable de PHP (por ejemplo, "foo ${bar} baz" ) prácticamente llovió en mi desfile.

Sé muy bien que podría escribir mi propio analizador. La sintaxis de los argumentos de comando es súper simplista, pero si hay una forma nativa de hacerlo, preferiría eso a la mía.

EDITAR: Tenga en cuenta que estoy buscando analizar los argumentos de una string , NO de la línea de comandos / shell.

EDITAR # 2: A continuación se muestra un ejemplo más completo de la entrada esperada -> salida para argumentos:

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


Bueno, también podrías construir este analizador con una expresión regular recursiva:

$regex = "([a-zA-Z0-9.-]+|/"([^/"////]+(?1)|////.(?1)|)/"|''([^''////]+(?2)|////.(?2)|)'')s";

Ahora que es un poco largo, así que vamos a desglosarlo:

$identifier = ''[a-zA-Z0-9.-]+''; $doubleQuotedString = "/"([^/"////]+(?1)|////.(?1)|)/""; $singleQuotedString = "''([^''////]+(?2)|////.(?2)|)''"; $regex = "($identifier|$doubleQuotedString|$singleQuotedString)s";

Entonces, ¿cómo funciona esto? Bueno, el identificador debería ser obvio ...

Los dos sub-patrones citados son básicamente los mismos, así que echemos un vistazo a la cadena entre comillas simples:

''([^''////]+(?2)|////.(?2)|)''

Realmente, ese es un carácter de cita seguido de un sub-patrón recursivo, seguido de una cita final.

La magia sucede en el sub-patrón.

[^''////]+(?2)

Esa parte básicamente consume cualquier carácter que no sea de comillas ni de escape. No nos preocupamos por ellos, así que cómelos. Luego, si encontramos una comilla o una barra invertida, desencadena un intento de igualar todo el sub-patrón nuevamente.

////.(?2)

Si podemos consumir una barra diagonal inversa, consuma el siguiente carácter (sin importar qué es), y repita el ejercicio nuevamente.

Finalmente, tenemos un componente vacío (si el carácter de escape es el último, o si no hay ningún carácter de escape).

Ejecutando esto en la entrada de prueba @HamZa proporcionada devuelve el mismo resultado:

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//"" }

La principal diferencia que ocurre es en términos de eficiencia. Este patrón debería retroceder menos (ya que es un patrón recursivo, no debería haber casi retroceso para una cadena bien formada), donde el otro regex es un regex no recursivo y retrocederá cada carácter individual (eso es lo que ? Después de la * Fuerzas, consumo de patrones no codiciosos).

Para entradas cortas esto no importa. El caso de prueba proporcionado, se ejecutan dentro de unos pocos% entre sí (el margen de error es mayor que la diferencia). Pero con una sola cuerda larga sin secuencias de escape:

"with a really long escape sequence match that will force a large backtrack loop"

La diferencia es significativa (100 carreras):

  • Recursivo: float(0.00030398368835449)
  • float(0.00055909156799316) : float(0.00055909156799316)

Por supuesto, podemos perder parcialmente esta ventaja con muchas secuencias de escape:

"This is /" A long string /" With a/lot /of /"escape /sequences"

  • Recursivo: float(0.00040411949157715)
  • float(0.00045490264892578) : float(0.00045490264892578)

Pero tenga en cuenta que la longitud sigue dominando. Esto se debe a que el backtracker se escala en O(n^2) , donde la solución recursiva se escala en O(n) . Sin embargo, dado que el patrón recursivo siempre necesita repetirse al menos una vez, es más lento que la solución de retroceso en cadenas cortas:

"1"

  • Recursivo: float(0.0002598762512207)
  • float(0.00017595291137695) : float(0.00017595291137695)

La compensación parece ocurrir alrededor de 15 caracteres ... Pero ambos son lo suficientemente rápidos como para que no hagan una diferencia a menos que esté analizando varios KB o MB de datos ... Pero vale la pena discutir ...

En insumos sanos, no habrá una diferencia significativa. Pero si está combinando más de unos pocos cientos de bytes, puede comenzar a sumarse significativamente ...

Editar

Si necesita manejar "palabras simples" arbitrarias (cadenas sin comillas), entonces puede cambiar la expresión regular original a:

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

Sin embargo, realmente depende de tu gramática y de lo que consideres un comando o no. Sugiero formalizar la gramática que esperas ...


Escribí algunos paquetes para interacciones de consola:

Análisis de argumentos

Hay un paquete que hace todo el análisis de los argumentos weew/php-console-arguments

Ejemplo:

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

$args será una matriz:

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

Los argumentos se pueden agrupar:

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

$args se convertirá en:

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

Puede hacer mucho más, solo revisa el weew/php-console-arguments .

Estilo de salida

Es posible que necesite un paquete para el estilo de salida weew/php-console-formatter

Aplicación de consola

Los paquetes anteriores se pueden utilizar de forma independiente o en combinación con una aplicación de consola sofisticada, weew/php-console

Nota: estas soluciones no son nativas pero pueden ser útiles para algunas personas.


He elaborado la siguiente expresión para que coincida con los diversos recintos y escapes:

$pattern = <<<REGEX / (?: " ((?:(?<=////)"|[^"])*) " | '' ((?:(?<=////)''|[^''])*) '' | (/S+) ) /x REGEX; preg_match_all($pattern, $input, $matches, PREG_SET_ORDER);

Concuerda:

  1. Dos comillas dobles, dentro de las cuales se puede escapar una comilla doble
  2. Igual que el # 1 pero para comillas simples
  3. Cadena sin comillas

Después, debe (con cuidado) eliminar los caracteres escapados:

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

Actualizar

Por el gusto de hacerlo, he escrito un analizador más formal, que se describe a continuación. No le dará un mejor rendimiento, es aproximadamente tres veces más lento que la expresión regular, principalmente debido a su naturaleza orientada a objetos. Supongo que la ventaja es más académica que práctica:

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

Se basa en StringIterator para recorrer la cadena un carácter a la vez:

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


Las expresiones regulares son bastante poderosas: (?s)(?<!//)("|'')(?:[^//]|//.)*?/1|/S+ . Entonces, ¿qué significa esta expresión?

  • (?s) : configura el modificador s para que coincida con las nuevas líneas con un punto .
  • (?<!//) : aspecto negativo detrás de, compruebe si no hay barra diagonal inversa antes del siguiente token
  • ("|'') : coincide con una comilla simple o doble y póngala en el grupo 1
  • (?:[^//]|//.)*? : haga coincidir todo lo que no sea / o coincida con / con el siguiente carácter (escapado)
  • /1 : coincide con lo que coincide en el primer grupo
  • | : o
  • /S+ : coincide con cualquier cosa excepto el espacio en blanco una o más veces.

La idea es capturar una cita y agruparla para recordar si es simple o doble. Las miradas negativas están ahí para asegurarnos de que no coincidamos con las citas escapadas. /1 se utiliza para hacer coincidir el segundo par de citas. Finalmente usamos una alternancia para hacer coincidir cualquier cosa que no sea un espacio en blanco. Esta solución es práctica y es casi aplicable a cualquier idioma / sabor que admita la apariencia y las referencias. Por supuesto, esta solución espera que las citas estén cerradas. Los resultados se encuentran en el grupo 0.

Vamos a implementarlo en 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]);

Si te preguntas por qué usé 4 barras invertidas. Entonces mira mi respuesta anterior .

Salida

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

Quitando las comillas

Bastante simple usando grupos nombrados y un simple bucle:

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

Demo online php


Realmente no hay una función nativa para analizar comandos que yo sepa. Sin embargo, he creado una función que hace el truco de forma nativa en PHP. Al usar str_replace varias veces, puedes convertir la cadena en algo convertible en matriz. No sé qué tan rápido consideras rápido, pero al ejecutar la consulta 400 veces, la consulta más lenta se realizó en 34 microsegundos.

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


Si desea seguir las reglas de dicho análisis que están allí, así como en shell, hay algunos casos de borde que creo que no son fáciles de cubrir con expresiones regulares y, por lo tanto, es posible que desee escribir un método que haga esto ( example ):

$string = ''foo "bar /"baz/"" /'///'quux///'/'''; echo $string, "/n"; print_r(StringUtil::separate_quoted($string));

Salida:

foo "bar /"baz/"" ''/'quux/''' Array ( [0] => foo [1] => bar "baz" [2] => ''quux'' )

Supongo que esto coincide bastante con lo que estás buscando. La función utilizada en el ejemplo se puede configurar para el carácter de escape, así como para las comillas, incluso puede usar paréntesis como [ ] para formar una "cotización" si lo desea.

Para permitir que no sean cadenas nativas de bytes seguros con un carácter por byte, puede pasar una matriz en lugar de una cadena. la matriz debe contener un carácter por valor como una cadena segura binaria. por ejemplo, pase unicode en forma NFC como UTF-8 con un punto de código por valor de matriz y esto debería hacer el trabajo para Unicode.


Simplemente puede usar str_getcsv y realizar pocas cirugías estéticas con stripslashes y trim

Ejemplo:

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

Salida

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 )

Precaución

No hay nada como un formato universal para la discusión. Lo mejor es especificar un formato específico y lo más fácil de ver es CSV.

Ejemplo

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

Usando CSV puedes simplemente tener esta salida

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


Sugiero algo como:

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

Con algo de ayuda de: cadena a matriz, dividida por comillas simples y dobles para la expresión regular

Aún tienes que liberar las cadenas en la matriz después.

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

Pero te haces una idea.


Ya que solicita una forma nativa de hacer esto, y PHP no proporciona ninguna función que pueda asignar la creación de $ argv, podría solucionar este problema de la siguiente manera:

Crea un script PHP ejecutable foo.php :

<?php // Skip this file name array_shift( $argv ); // output an valid PHP code echo ''return ''. var_export( $argv, 1 ).'';''; ?>

Y úselo para recuperar argumentos, la forma en que PHP lo hará realmente si ejecuta el comando $ :

function parseCommand( $command ) { return eval( shell_exec( "php foo.php ".$command ) ); } $command = <<<CMD foo "bar /"baz/"" ''/'quux/''' CMD; $args = parseCommand( $command ); var_dump( $args );

Ventajas:

  • Código muy simple
  • Debería ser más rápido que cualquier expresión regular
  • 100% cerca del comportamiento de PHP

Inconvenientes:

  • Requiere privilegio de ejecución en el host.
  • Shell exec + eval en el mismo $ var, ¡vamos de fiesta! Tienes que confiar en la entrada o hacer mucho filtrado para que las expresiones regulares sean más rápidas (no me gustaría profundizar en eso).

Yo recomendaría ir de otra manera. Ya existe una forma "estándar" de hacer argumentos de línea de comandos. se llama get_opts:

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

Le sugiero que cambie su secuencia de comandos para usar get_opts, entonces cualquiera que use su secuencia de comandos pasará parámetros de una forma que les sea familiar y un tipo de "estándar de la industria" en lugar de tener que aprender su forma de hacer las cosas.