Cómo iterar sobre argumentos en un script Bash

901

Tengo un comando complejo del que me gustaría hacer un script de shell / bash. Puedo escribirlo en términos de $1fá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.

Thelema
fuente
67
Para su información, es una buena idea poner siempre los parámetros entre comillas. Nunca se sabe cuándo un parm puede contener un espacio incrustado.
David R Tribble

Respuestas:

1483

Use "$@"para representar todos los argumentos:

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

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

sh test.sh 1 2 '3 4'
1
2
3 4
Robert Gamble
fuente
38
Por cierto, uno de los otros méritos "$@"es que se expande a nada cuando no hay parámetros posicionales, mientras que se "$*"expande a la cadena vacía, y sí, hay una diferencia entre ningún argumento y un argumento vacío. Ver ${1:+"$@"} in / bin / sh` .
Jonathan Leffler
99
Tenga en cuenta que esta notación también debe usarse en funciones de shell para acceder a todos los argumentos de la función.
Jonathan Leffler
Al descartar las comillas dobles: $varpude ejecutar los argumentos.
eonist
¿Cómo empiezo desde el segundo argumento? Quiero decir, necesito esto, pero siempre empiezo desde el segundo argumento, es decir, omita $ 1.
m4l490n
44
@ m4l490n: shifttira $1y baja todos los elementos posteriores.
MSalters
239

Vuelva a escribir una respuesta ahora eliminada por VonC .

Robert GambleLa respuesta sucinta de trata directamente con la pregunta. Este amplifica algunos problemas con nombres de archivos que contienen espacios.

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

Tesis básica: "$@" es correcta 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, también "$*"está bien, pero "$@"generalmente (pero no siempre) funciona en los mismos lugares. Sin comillas, $@y $*son equivalentes (y casi siempre incorrectos).

Así que, ¿cuál es la diferencia entre $*, $@, "$*"y "$@"? Todos están relacionados con 'todos los argumentos del shell', pero hacen cosas diferentes. Cuando no se cita, $*y $@hacer lo mismo. Tratan cada 'palabra' (secuencia de espacios no en 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 eran cuando se especificaban en la línea de comando. "$@"se expande a nada cuando no hay argumentos posicionales; "$*"se expande a una cadena vacía, y sí, hay una diferencia, aunque puede ser difícil de percibir. Consulte 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 ayudarlo. (O debe usar matrices, con cuidado: se "${array[@]}"comporta de manera análoga "$@").

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 cotizaciones antes de expandir las variables. Entonces, para que el shell preste atención a las citas incrustadas $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 tiene nombres de archivo como " He said, "Don't do this!"" (con comillas y 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 shells (todos ellos) no hacen que sea particularmente fácil manejar tales cosas, por lo que (curiosamente) muchos programas de Unix no hacen un buen trabajo al manejarlos. En Unix, un nombre de archivo (componente único) puede contener cualquier carácter excepto barra diagonal y NUL'\0' . Sin embargo, los shells no recomiendan espacios, líneas nuevas o pestañas en ningún lugar de los nombres de las rutas. También es la razón por la cual 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 hace mucho tiempo descubrí que necesitaba un programa que no sea estándar en Unix. Lo llamoescape (la versión 1.1 tenía fecha de 1989-08-23T16: 01: 45Z).

Aquí hay un ejemplo de escapeuso: con el sistema de control SCCS. Es un guión de portada que hace tanto un delta(pensar registro ) como un get(pensar registro ). Varios argumentos, especialmente -y(la razón por la que realizó el cambio) contendrían espacios en blanco y nuevas líneas. Tenga en cuenta que el script data de 1992, por lo que utiliza ticks de retroceso en lugar de $(cmd ...)notación y no se utiliza #!/bin/shen 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 escape tan a fondo en estos días -e, por ejemplo, no es necesario con el argumento, pero en general, este es uno de mis scripts más simples que uso escape).

El escapeprograma simplemente genera sus argumentos, al igual que lo echo hace, pero asegura que los argumentos estén protegidos para su uso con eval(un nivel de eval; Tengo un programa que realizó la ejecución remota de shell, y que necesitaba escapar de la salida de escape).

    $ escape $var
    '"./my' 'dir"' '"./anotherdir"'
    $ escape "$var"
    '"./my dir" "./anotherdir"'
    $ escape x y z
    x y z
    $ 

Tengo otro programa llamado alque enumera sus argumentos uno por línea (y es aún más antiguo: la versión 1.1 con fecha 27-01-27T14: 35: 49). Es más útil al depurar 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 "$@"anotaciones, 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
$

Tenga en cuenta 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 la línea de comandos' 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 , tal vez exégesis es el mejor término. El código fuente está escapedisponible a pedido (correo electrónico a firstname dot lastname en gmail dot com). El código fuente para ales increíblemente simple:

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

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

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

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

El condicional es necesario para que no produzca salida cuando no se pasan argumentos. El printfcomando producirá una línea en blanco con solo el argumento de cadena de formato, pero el programa C no produce nada.

Jonathan Leffler
fuente
132

Tenga en cuenta que la respuesta de Robert es correcta, y también funciona sh. Puede (de forma portátil) 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 a b "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

Leí por primera vez sobre esto en Unix Programming Environment por Kernighan y Pike.

En bash, help fordocumenta esto:

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

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

Alok Singhal
fuente
44
Estoy en desacuerdo. Uno tiene que saber lo que "$@"significa el críptico , y una vez que sabe lo que for isignifica, no es menos legible que for i in "$@".
Alok Singhal
10
No afirmé que for ies mejor porque ahorra pulsaciones de teclas. Comparé la legibilidad de for iy for i in "$@".
Alok Singhal
esto es lo que estaba buscando: ¿cómo llaman a esta suposición de $ @ en bucles donde no tiene que hacer referencia explícita a ella? ¿Hay una desventaja de no usar la referencia $ @?
qodeninja
58

Para casos simples también puedes usar shift. Trata la lista de argumentos como una cola. Cada uno shiftarroja el primer argumento y se disminuye el índice de cada uno de los argumentos restantes.

#this prints all arguments
while test $# -gt 0
do
    echo "$1"
    shift
done
nuoritoveri
fuente
Creo que esta respuesta es mejor porque si lo haces shiften un bucle for, ese elemento sigue siendo parte de la matriz, mientras que con esto shiftfunciona como se esperaba.
DavidG
2
Tenga en cuenta que debe usar echo "$1"para preservar el espacio en el valor de la cadena de argumento. Además, (un problema menor) esto es destructivo: no puede reutilizar los argumentos si los ha cambiado todos. A menudo, eso no es un problema: de todos modos, solo procesas la lista de argumentos una vez.
Jonathan Leffler
1
Acepte que el cambio es mejor, porque entonces tiene una manera fácil de tomar dos argumentos en un caso de cambio para procesar argumentos de marca como "-o archivo de salida".
Baxissimo
Por otro lado, si usted está comenzando a analizar los argumentos de la bandera, es posible que desee considerar una más potente lenguaje de script de fiesta :)
nuoritoveri
44
Esta solución es mejor si necesita un par de valores de parámetros, por ejemplo --file myfile.txt , $ 1 es el parámetro, $ 2 es el valor y llama a shift dos veces cuando necesita omitir otro argumento
Daniele Licitra
16

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

argc=$#
argv=("$@")

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done
baz
fuente
55
Creo que la línea argv = ($ @) debería ser argv = ("$ @") otros argumentos sabios con espacios no se manejan correctamente
kdubs
Confirmo que la sintaxis correcta es argv = ("$ @"). a menos que no obtenga los últimos valores si tiene parámetros citados. Por ejemplo, si usa algo como ./my-script.sh param1 "param2 is a quoted param": - usando argv = ($ @) obtendrá [param1, param2], - usando argv = ("$ @"), obtendrá [param1, param2 es un parámetro citado]
jseguillon
2
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"
g24l
fuente
1
Como se indica a continuación, [ $# != 0 ]es mejor. En lo anterior, #$debe ser $#como enwhile [[ $# > 0 ]] ...
hute37
1

Amplificando la respuesta de baz, si necesita enumerar la lista de argumentos con un índice (como buscar una palabra específica), puede hacerlo sin copiar la lista o mutarla.

Supongamos que desea dividir una lista de argumentos en un guión doble ("-") y pasar los argumentos antes de los guiones a un comando, y los argumentos después de los guiones a otro:

 toolwrapper() {
   for i in $(seq 1 $#); do
     [[ "${!i}" == "--" ]] && break
   done || return $? # returns error status if we don't "break"

   echo "dashes at $i"
   echo "Before dashes: ${@:1:i-1}"
   echo "After dashes: ${@:i+1:$#}"
 }

Los resultados deberían verse así:

 $ toolwrapper args for first tool -- and these are for the second
 dashes at 5
 Before dashes: args for first tool
 After dashes: and these are for the second
Rich Kadel
fuente
1

getopt Use el comando en sus scripts para formatear cualquier opción o parámetro de la línea de comando.

#!/bin/bash
# Extract command line options & values with getopt
#
set -- $(getopt -q ab:cd "$@")
#
echo
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option" ;;
-b) param="$2"
echo "Found the -b option, with parameter value $param"
shift ;;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
JimmyLandStudios
fuente
Difícil, investigaría esto a fondo. Ejemplo: archivo: ///usr/share/doc/util-linux/examples/getopt-parse.bash, Manual: man.jimmylandstudios.xyz/?man=getopt
JimmyLandStudios