Cómo iterar sobre argumentos en un script Bash

Tengo un comando complejo del que me gustaría crear un script de shell / bash. Puedo escribirlo en términos de $1 fácilmente:

 foo $1 args -o $1.ext 

Quiero poder pasar múltiples nombres de entrada al script. ¿Cuál es la forma correcta de hacerlo?

Y, por supuesto, quiero manejar nombres de archivos con espacios en ellos.

Use "$@" para representar todos los argumentos:

 for var in "$@" do echo "$var" done 

Esto iterará sobre cada argumento e imprimirá en una línea separada. $ @ se comporta como $ *, excepto que cuando se cotizan los argumentos se dividen correctamente si hay espacios en ellos:

 sh test.sh 1 2 '3 4' 1 2 3 4 

Reescribir una respuesta ahora eliminada por VonC .

La respuesta sucinta de Robert Gamble trata directamente con la pregunta. Éste amplifica algunos problemas con nombres de archivos que contienen espacios.

Ver también: $ {1: + “$ @”} en / bin / sh

Tesis básica: "$@" es correcto, y $* (sin comillas) casi siempre está mal. Esto se debe a que "$@" funciona bien cuando los argumentos contienen espacios, y funciona igual que $* cuando no lo hacen. En algunas circunstancias, "$*" está bien, pero "$@" generalmente (pero no siempre) funciona en los mismos lugares. Sin comillas, $@ y $* son equivalentes (y casi siempre incorrectos).

Entonces, ¿cuál es la diferencia entre $* , $@ , "$*" y "$@" ? Todos están relacionados con “todos los argumentos del caparazón”, pero hacen cosas diferentes. Cuando no se incluyen las comillas, $* y $@ hacen lo mismo. Tratan cada ‘palabra’ (secuencia de espacio no blanco) como un argumento separado. Sin embargo, las formas citadas son bastante diferentes: "$*" trata la lista de argumentos como una sola cadena separada por espacios, mientras que "$@" trata los argumentos casi exactamente como lo estaban cuando se especificaban en la línea de comandos. "$@" expande a nada cuando no hay argumentos posicionales; "$*" expande a una cadena vacía, y sí, hay una diferencia, aunque puede ser difícil de percibir. Ver más información a continuación, después de la introducción del comando (no estándar) al .

Tesis secundaria: si necesita procesar argumentos con espacios y luego pasarlos a otros comandos, a veces necesita herramientas no estándar para ayudar. (O debería usar matrices, cuidadosamente: "${array[@]}" comporta de manera análoga a "$@" ).

Ejemplo:

  $ mkdir "my dir" anotherdir $ ls anotherdir my dir $ cp /dev/null "my dir/my file" $ cp /dev/null "anotherdir/myfile" $ ls -Fltr total 0 drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 my dir/ drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 anotherdir/ $ ls -Fltr * my dir: total 0 -rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 my file anotherdir: total 0 -rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 myfile $ ls -Fltr "./my dir" "./anotherdir" ./my dir: total 0 -rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 my file ./anotherdir: total 0 -rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 myfile $ var='"./my dir" "./anotherdir"' && echo $var "./my dir" "./anotherdir" $ ls -Fltr $var ls: "./anotherdir": No such file or directory ls: "./my: No such file or directory ls: dir": No such file or directory $ 

¿Por qué eso no funciona? No funciona porque el shell procesa las comillas antes de expandir las variables. Entonces, para que el intérprete de comandos preste atención a las comillas incrustadas en $var , debe usar eval :

  $ eval ls -Fltr $var ./my dir: total 0 -rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 my file ./anotherdir: total 0 -rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 myfile $ 

Esto se vuelve realmente complicado cuando tienes nombres de archivos como ” He said, "Don't do this!" ” (Con comillas, comillas dobles y espacios).

  $ cp /dev/null "He said, \"Don't do this!\"" $ ls He said, "Don't do this!" anotherdir my dir $ ls -l total 0 -rw-r--r-- 1 jleffler staff 0 Nov 1 15:54 He said, "Don't do this!" drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 anotherdir drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 my dir $ 

Los proyectiles (todos ellos) no hacen que sea particularmente fácil manejar este tipo de cosas, por lo que (curiosamente) muchos progtwigs Unix no hacen un buen trabajo al manejarlos. En Unix, un nombre de archivo (componente único) puede contener cualquier carácter, excepto barra inclinada y NUL '\0' . Sin embargo, los shells recomiendan que no haya espacios ni nuevas líneas o tabs en ningún lugar de los nombres de las rutas. También es la razón por la que los nombres de archivo estándar de Unix no contienen espacios, etc.

Cuando se trata de nombres de archivos que pueden contener espacios y otros caracteres problemáticos, debe ser extremadamente cuidadoso, y descubrí hace mucho tiempo que necesitaba un progtwig que no es estándar en Unix. Lo llamo escape (la versión 1.1 estaba fechada el 1989-08-23T16: 01: 45Z).

Aquí hay un ejemplo de escape en uso: con el sistema de control SCCS. Es un script de portada que hace un delta (pensar en el check-in ) y un get (pensar en check-out ). Varios argumentos, especialmente -y (la razón por la que realizó el cambio) contendrían espacios en blanco y líneas nuevas. Tenga en cuenta que el script data de 1992, por lo que utiliza la anotación de retroceso en lugar de $(cmd ...) y no utiliza #!/bin/sh en la primera línea.

 : "@(#)$Id: delget.sh,v 1.8 1992/12/29 10:46:21 jl Exp $" # # Delta and get files # Uses escape to allow for all weird combinations of quotes in arguments case `basename $0 .sh` in deledit) eflag="-e";; esac sflag="-s" for arg in "$@" do case "$arg" in -r*) gargs="$gargs `escape \"$arg\"`" dargs="$dargs `escape \"$arg\"`" ;; -e) gargs="$gargs `escape \"$arg\"`" sflag="" eflag="" ;; -*) dargs="$dargs `escape \"$arg\"`" ;; *) gargs="$gargs `escape \"$arg\"`" dargs="$dargs `escape \"$arg\"`" ;; esac done eval delta "$dargs" && eval get $eflag $sflag "$gargs" 

(Probablemente no usaría el escape tan a fondo en estos días, no es necesario con el argumento -e , por ejemplo, pero en general, este es uno de mis scripts más simples que usan escape .)

El progtwig de escape simplemente muestra sus argumentos, al igual que echo , pero asegura que los argumentos están protegidos para su uso con eval (un nivel de eval ; yo sí tengo un progtwig que hizo la ejecución remota del shell, y que necesitaba escapar de la salida de escape ).

  $ escape $var '"./my' 'dir"' '"./anotherdir"' $ escape "$var" '"./my dir" "./anotherdir"' $ escape xyz xyz $ 

Tengo otro progtwig llamado al que enumera sus argumentos uno por línea (y es aún más antiguo: versión 1.1 con fecha de 1987-01-27T14: 35: 49). Es más útil cuando se depuran los scripts, ya que se puede conectar a una línea de comando para ver qué argumentos se pasan realmente al comando.

  $ echo "$var" "./my dir" "./anotherdir" $ al $var "./my dir" "./anotherdir" $ al "$var" "./my dir" "./anotherdir" $ 

[ Agregado: Y ahora para mostrar la diferencia entre las diversas notaciones "$@" , aquí hay un ejemplo más:

 $ cat xx.sh set -x al $@ al $* al "$*" al "$@" $ sh xx.sh * */* + al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file He said, "Don't do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file + al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file He said, "Don't do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file + al 'He said, "Don'\''t do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file' He said, "Don't do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file + al 'He said, "Don'\''t do this!"' anotherdir 'my dir' xx.sh anotherdir/myfile 'my dir/my file' He said, "Don't do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file $ 

Observe que nada conserva los espacios en blanco originales entre * y */* en la línea de comando. Además, tenga en cuenta que puede cambiar los ‘argumentos de línea de comando’ en el shell utilizando:

 set -- -new -opt and "arg with space" 

Esto establece 4 opciones, ‘ -new ‘, ‘ -opt ‘, ‘ and ‘, y ‘ arg with space ‘.
]

Hmm, esa es una respuesta bastante larga: quizás la exégesis es el mejor término. El código fuente para el escape disponible a petición (dirección de correo electrónico al apellido apellido del punto en gmail punto com). El código fuente de al es increíblemente simple:

 #include  int main(int argc, char **argv) { while (*++argv != 0) puts(*argv); return(0); } 

Eso es todo. Es equivalente al script test.sh que Robert Gamble mostró, y podría escribirse como una función de shell (pero las funciones de shell no existían en la versión local del shell de Bourne cuando escribí por primera vez).

También tenga en cuenta que puede escribir al como un simple script de shell:

 [ $# != 0 ] && printf "%s\n" "$@" 

El condicional es necesario para que no produzca ningún resultado cuando no pasa ningún argumento. El comando printf producirá una línea en blanco con solo el argumento de cadena de formato, pero el progtwig C no produce nada.

Tenga en cuenta que la respuesta de Robert es correcta, y también funciona en sh . Usted puede (portably) simplificarlo aún más:

 for i in "$@" 

es equivalente a:

 for i 

Es decir, ¡no necesitas nada!

Prueba ( $ es símbolo del sistema):

 $ set ab "spaces here" d $ for i; do echo "$i"; done a b spaces here d $ for i in "$@"; do echo "$i"; done a b spaces here d 

La primera vez que leí esto fue en Unix Programming Environment por Kernighan y Pike.

En bash , help for documentos esto:

for NAME [in WORDS ... ;] do COMMANDS; done

Si 'in WORDS ...;' no está presente, entonces se asume 'in "$@"' .

Para casos simples también puedes usar shift . Trata la lista de argumentos como una cola, cada shift arroja el primer argumento, el número de cada argumento que queda se reduce.

 #this prints all arguments while test $# -gt 0 do echo $1 shift done 

También puede acceder a ellos como un conjunto de elementos, por ejemplo, si no desea iterar a través de todos ellos

 argc=$# argv=($@) for (( j=0; j 
 aparse() { while [[ $# > 0 ]] ; do case "$1" in --arg1) varg1=${2} shift ;; --arg2) varg2=true ;; esac shift done } aparse "$@"