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:
- Dos comillas dobles, dentro de las cuales se puede escapar una comilla doble
- Igual que el # 1 pero para comillas simples
- 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 modificadors
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);
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("/'","'",$string);
// Do the same with double quotes
$string = str_replace("///"",""",$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.