Pregunta 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.


665
2017-11-01 18:14


origen


Respuestas:


Utilizar "$@" 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

1116
2017-11-01 18:25



Reescribir un ahora borrado responder 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 es porque "$@" funciona bien cuando los argumentos contienen espacios, y funciona igual que $* cuando no lo hacen En algunas circunstancias, "$*" está bien también, pero "$@" usualmente (pero no siempre) trabaja en los mismos lugares. No cotizado, $@ 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 cita, $* y $@ hacer la misma cosa. Tratan cada 'palabra' (secuencia de espacio no blanco) como un argumento separado. Las formas citadas son bastante diferentes, sin embargo: "$*" trata la lista de argumentos como una sola cadena separada por espacios, mientras que "$@" trata los argumentos casi exactamente como estaban cuando se especifica 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 percibirla. 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, entonces a veces se necesita un estándar no estándar herramientas para ayudar. (O debería usar matrices, con cuidado: "${array[@]}" se 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 citas antes de que se expanda variables. Entonces, para que el caparazón preste atención a las citas incrustadas en $var, tienes que 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 pone realmente complicado cuando tienes nombres de archivos 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 proyectiles (todos ellos) no hacen que sea particularmente fácil manejarlos cosas, así que (por extraño que parezca) muchos programas de Unix no hacen un buen trabajo de manejándolos. En Unix, un nombre de archivo (componente único) puede contener cualquier carácter excepto barra oblicua y NUL '\0'. Sin embargo, los proyectiles no recomiendan espacios ni líneas nuevas ni pestañas. en cualquier lugar en los nombres de ruta. 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 personajes problemáticos, tienes que ser extremadamente cuidadoso, y encontré Hace mucho tiempo que necesitaba un programa que no es estándar en Unix. Yo 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 una delta (pensar registrarse) y un get (pensar revisa) Varios argumentos, especialmente -y (la razón por la que hiciste el cambio) contendría espacios en blanco y saltos de línea. Tenga en cuenta que el script data de 1992, por lo que utiliza los ticks de retroceso en lugar de $(cmd ...) notación y no utiliza #!/bin/sh en la primera linea

:   "@(#)$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 hoy en día - es no es necesario con el -e argumento, por ejemplo, pero en general, esto es uno de mis scripts más simples utilizando escape.)

los escape programa simplemente emite sus argumentos, más bien como echo lo hace, pero asegura que los argumentos están protegidos para su uso con eval (un nivel de eval; Tengo un programa que hizo shell remoto ejecución, 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 al que enumera sus argumentos uno por línea (y es aún más antiguo: versión 1.1 de 1987-01-27T14: 35: 49). Es más útil al depurar scripts, ya que se puede conectar a un 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"
    $

[Adicional: Y ahora para mostrar la diferencia entre los diversos "$@" 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
$

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 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, eso es bastante largo responder - quizás exégesis es el mejor término. Código fuente para escape disponible bajo petición (correo electrónico a firstname dot apellido en gmail punto com). El código fuente para al es 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 test.sh secuencia de comandos 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) al)

También ten en cuenta que puedes 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. los printf comando producirá una línea en blanco con solo el argumento de cadena de formato, pero el programa C no produce nada.


203
2017-11-01 23:04



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

for i in "$@"

es equivalente a:

for i

Es decir, ¡no necesitas nada!

Prueba ($ es un 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

Primero leí acerca de esto en Entorno de programación de Unix 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.


103
2017-12-31 22:25



Para casos simples también puedes usar shift. Trata la lista de argumentos como una cola, cada shift lanza 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

50
2017-10-29 08:36



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<argc; j++ )); do
    echo ${argv[j]}
done

6
2017-10-30 00:58



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

aparse "$@"

3
2018-01-14 14:20