construir comando concatenando cadena en bash

13

Tengo un script bash que construye una línea de comandos en una cadena basada en algunos parámetros antes de ejecutarlo de una vez. Se supone que las partes que se concatenan en la cadena de comando están separadas por tuberías para facilitar una "transmisión" de datos a través de cada componente.

Un ejemplo muy simplificado:

#!/bin/bash
part1=gzip -c
part2=some_other_command
cmd="cat infile"

if [ ! "$part1" = "" ]
then
    cmd+=" | $part1"
fi


if [ ! "$part2" = "" ]
then
    cmd+=" | $part2"
fi


cmd+="> outfile"
#show command. It looks ok
echo $cmd
#run the command. fails with pipes
$cmd

Por alguna razón, las tuberías no parecen funcionar. Cuando ejecuto este script, recibo diferentes mensajes de error relacionados generalmente con la primera parte del comando (antes de la primera canalización).

Entonces, mi pregunta es si es posible o no construir un comando de esta manera, y ¿cuál es la mejor manera de hacerlo?

Lennart Rolland
fuente
¿Cuáles son los mensajes de error?
CameronNemo
En mi script (que es algo más complejo que esta simplificación) me sale "archivo no encontrado"
Lennart Rolland
¿Es seguro asumir que infileexiste en el directorio actual?
saiarcot895
si. en mi código es wget -O, en lugar de un archivo. En realidad, si solo copio la cadena concatenada y la paso en la terminal, funciona bien
Lennart Rolland

Respuestas:

17

Todo depende de cuándo se evalúan las cosas. Cuando escribe $cmd, todo el resto de la línea se pasa como argumentos a la primera palabra $cmd.

walt@spong:~(0)$ a="cat /etc/passwd"
walt@spong:~(0)$ b="| wc -l"
walt@spong:~(0)$ c="$a $b"
walt@spong:~(0)$ echo $c
cat /etc/passwd | wc -l
walt@spong:~(0)$ $c
cat: invalid option -- 'l'
Try 'cat --help' for more information.
walt@spong:~(1)$ eval $c
62
walt@spong:~(0)$ a="echo /etc/passwd"
walt@spong:~(0)$ c="$a $b"
walt@spong:~(0)$ echo $c
echo /etc/passwd | wc -l
walt@spong:~(0)$ $c
/etc/passwd | wc -l
walt@spong:~(0)$ $c |od -bc
0000000 057 145 164 143 057 160 141 163 163 167 144 040 174 040 167 143  
          /   e   t   c   /   p   a   s   s   w   d       |       w   c  
0000020 040 055 154 012  
              -   l  \n  
0000024
walt@spong:~(0)$ eval $c
1  

Esto muestra que los argumentos pasados ​​al echocomando son: " /etc/passwd", " |" (el carácter de la barra vertical), " wc" y " -l".

De man bash:

eval [arg ...]  
    The  args  are read and concatenated together into   
    a single command.  This command is then read and  
    executed by the shell, and its exit status is returned  
    as the value of eval.  If there are no args, or only null  
    arguments, eval returns 0.
Waltinator
fuente
8

Una solución a esto, para referencia futura, es usar "eval". Esto garantiza que cualquier manera en que bash interprete la cadena se olvide y que todo se lea como si estuviera escrito directamente en un shell (que es exactamente lo que queremos).

Entonces, en el ejemplo anterior, reemplazando

$cmd

con

eval $cmd

resuelto.

Lennart Rolland
fuente
Sin embargo, tenga cuidado con los parámetros citados. eval foo "a b"sería lo mismo que eval foo "a" "b".
udondan
2

@waltinator ya explicó por qué esto no funciona como esperaba. Otra forma de evitarlo es usar bash -cpara ejecutar su comando:

$ comm="cat /etc/passwd"
$ comm+="| wc -l"
$ $comm
cat: invalid option -- 'l'
Try 'cat --help' for more information.
$ bash -c "$comm"
51
terdon
fuente
1
Parsimony me dice que no comience otro proceso bash -c, sino que use evalpara hacer el comando en el proceso actual.
waltinator
@waltinator seguro, probablemente usaría eval para esto también (es por eso que te voté a ti y a Lennart). Solo estoy proporcionando una alternativa.
terdon
0

Posiblemente, una mejor manera de hacer esto es evitar usar evaly simplemente usar una matriz Bash y es una expansión en línea para construir todos los argumentos y luego ejecutarlos contra el comando.

runcmd=() # This is slightly messier than declare -a but works
for cmd in $part1 $part2 $part3; do runcmd+="| $cmd "; done
cat infile ${runcmd[@]} # You might be able to do $basecmd ${runcmd[@]}
# but that sometimes requires an `eval` which isn't great
dragon788
fuente