Digamos que tengo los siguientes dos scripts de shell:
#!/bin/sh
#This script is named: args.sh
echo 1 "\"Two words\"" 3
, y:
#!/bin/sh
#This script is named: test.sh
echo "Argument 1: "$1
echo "Argument 2: "$2
echo "Argument 3: "$3
Cuando llamo a los scripts como:
sh test.sh $(sh args.sh)
, Yo recibo:
Argument 1: 1
Argument 2: "Two
Argument 3: words"
Cuando esperaba obtener:
Argument 1: 1
Argument 2: Two words
Argument 3: 3
Copiar la salida sh args.sh
y pegarla como entrada sh test.sh
funciona bien; así que supongo que esto no es realmente lo que está haciendo el shell. Puedo lograr el resultado deseado/esperado llamando sh args.sh | xargs sh test.sh
en su lugar.
Sin embargo, me pregunto si existe una forma equivalente de hacer esto sin canalizar la salida del primer script (args.sh) a xargs. Quiero que los scripts se llamen en el orden original; el script de argumento para enviar los parámetros al segundo. También estoy buscando una explicación de por qué esta llamada no funciona como se esperaba.
Respuesta1
Parte del problema es que la cadena devuelta por args.sh no se analiza de la misma manera que un comando directo, sino solo por el valor de $IFS ( $' \t\n'
). Intente activar el seguimiento de comandos con set -x
:
$ sh /tmp/test.sh $(sh /tmp/args.sh)
++ sh /tmp/args.sh
+ sh /tmp/test.sh 1 '"Two' 'words"' 3
Argument 1: 1
Argument 2: "Two
Argument 3: words"
$
Observe la línea que comienza con un solo +
: hay cuatro argumentos, los '"Two'
y 'words"'
son dos analizados como argumentos separados. Lo que querrías hacer es cambiar $IFS.
$ set -x
$ IFS='"'
+ IFS='"'
$ sh /tmp/test.sh $(sh /tmp/args.sh)
++ sh /tmp/args.sh
+ sh /tmp/test.sh '1 ' 'Two words' ' 3'
Argument 1: 1
Argument 2: Two words
Argument 3: 3
$
Esto no funcionará para todas las salidas. Lo mejor que puedes hacer es cambiar la salida de args.sh para separar la salida con algo diferente a un espacio, por ejemplo una coma o dos puntos:
$ cat /tmp/args.sh
#!/bin/sh
#This script is named: args.sh
echo "1,Two words,3"
$ IFS=,
$ sh /tmp/test.sh $(sh /tmp/args.sh)
+ sh /tmp/args.sh
+ sh /tmp/test.sh 1 Two words 3
Argument 1: 1
Argument 2: Two words
Argument 3: 3
$
Respuesta2
Cuando dejas una sustitución de variable $var
o una sustitución de comando $(cmd)
sin comillas, el resultado sufre las siguientes transformaciones:
- Divide la cadena resultante en palabras. La división ocurre en espacios en blanco (secuencias de espacios, tabulaciones y nuevas líneas); esto se puede configurar configurando
IFS
(ver1,2). - Cada una de las palabras resultantes se trata como un patrón global y, si coincide con algunos archivos, la palabra se reemplaza por la lista de nombres de archivos.
Tenga en cuenta que el resultado no es una cadena sino una lista de cadenas. Además, tenga en cuenta que "
aquí no intervienen caracteres como comillas; son parte de la sintaxis fuente del shell, no de la expansión de cadenas.
Una regla general de la programación de shell es poner siempre comillas dobles alrededor de las sustituciones de variables y comandos, a menos que sepa por qué debe omitirlas. Entonces en test.sh
, escribe echo "Argument 1: $1"
. Para pasar argumentos a test.sh
, estás en problemas: necesitas pasar una lista de palabras de args.sh
a test.sh
, pero el método elegido implica una sustitución de comando y eso solo proporciona el camino para una cadena simple.
Si puede garantizar que los argumentos que se pasarán no contengan nuevas líneas y es aceptable cambiar ligeramente el proceso de invocación, puede configurarlo IFS
para que contenga solo una nueva línea. Asegúrese de que args.sh
genere exactamente un nombre de archivo por línea sin comillas perdidas.
IFS='
'
test.sh $(args.sh)
unset IFS
Si los argumentos pueden contener caracteres arbitrarios (presumiblemente excepto bytes nulos, que no puede pasar como argumentos), deberá realizar alguna codificación. Cualquier codificación servirá; Por supuesto, no será lo mismo que pasar los argumentos directamente: eso no es posible. Por ejemplo, en args.sh
(reemplace \t
por un carácter de tabulación real si su shell no lo admite):
for x; do
printf '%s_\n' "$x" |
sed -e 's/q/qq/g' -e 's/ /qs/g' -e 's/\t/qt/g' -e 's/$/qn/'
done
y en test.sh
:
for arg; do
decoded=$(printf '%s\n' "$arg" |
sed -e 's/qs/ /g' -e 's/qt/\t/g' -e 's/qn/\n/g' -e 's/qq/q/g')
decoded=${decoded%_qn}
# process "$decoded"
done
Es posible que prefiera cambiar test.sh
para aceptar una lista de cadenas como entrada. Si las cadenas no contienen nuevas líneas, invoque args.sh | test.sh
y use esto en args.sh
(explicación):
while IFS= read -r line; do
# process "$line"
done
Otro método que evita por completo la necesidad de citar es invocar el segundo script del primero.
…
args.sh "$@"