Tengo un comando complejo del que me gustaría hacer un script de shell/bash. Puedo escribirlo en términos de $1
fácilmente:
foo $1 args -o $1.ext
Quiero ser capaz de pasar múltiples nombres de entrada a la secuencia de comandos. ¿Cuál es la forma correcta de hacerlo?
Y, por supuesto, quiero manejar los nombres de archivo con espacios en ellos.
Utilice "$@"
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 adecuadamente si hay espacios en ellos:
sh test.sh 1 2 '3 4'
1
2
3 4
Redacción de una respuesta ahora borrada por VonC.
La sucinta respuesta de Robert Gamble aborda directamente la cuestión.
Ésta amplía algunas cuestiones relativas a los nombres de archivos que contienen espacios.
Véase también: ${1:+"$@"} en /bin/sh
Tesis básica: "$@"
es correcto, y $*
(sin comillas) es casi siempre incorrecto.
Esto es porque "$@"
funciona bien cuando los argumentos contienen espacios, y
funciona igual que $*
cuando no los contienen.
En algunas circunstancias, "$*"
también está bien, pero "$@"
normalmente (pero no
siempre) funciona en los mismos lugares.
Sin citar, $@
y $*
son equivalentes (y casi siempre erróneos).
Entonces, ¿cuál es la diferencia entre $*
, $@
, "$*"
y "$@"
? Todos están relacionados con "todos los argumentos del shell", pero hacen cosas diferentes. Cuando no se citan, $*
y $@
hacen lo mismo. Tratan cada 'palabra' (secuencia de espacios no blancos) como un argumento separado. Sin embargo, las formas entrecomilladas son bastante diferentes: "$*"
trata la lista de argumentos como una única cadena separada por espacios, mientras que "$@"
trata los argumentos casi exactamente como se especificaron en la línea de órdenes.
"$@" 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.
Véase más información a continuación, tras 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 necesita herramientas
para ayudar. (O deberías usar arrays, con cuidado: "${array[@]}"
se comporta de forma 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é no funciona?
No funciona porque el shell procesa las comillas antes de expandir las
las variables.
Así que, para que el shell preste atención a las comillas 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 vuelve realmente complicado cuando tienes nombres de archivos como "He said, "¡No hagas esto!"
" (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 shells (todos ellos) no hacen que sea particularmente fácil de manejar tales
cosas, así que (curiosamente) muchos programas Unix no hacen un buen trabajo de
manejarlos.
En Unix, un nombre de archivo (componente único) puede contener cualquier carácter excepto
barra y NUL '\0'
.
Sin embargo, los shells recomiendan encarecidamente que no haya espacios ni nuevas líneas ni tabulaciones
en ninguna parte 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
y otros caracteres problemáticos, hay que tener mucho cuidado, y hace tiempo descubrí
hace tiempo que necesitaba un programa que no es estándar en Unix.
Lo llamo escape
(la versión 1.1 data de 1989-08-23T16:01:45Z).
Aquí hay un ejemplo de escape
en uso - con el sistema de control SCCS.
Es un script de cobertura que hace tanto un delta
(piensa en check-in) como un
get
(piensa en check-out).
Varios argumentos, especialmente y
(la razón por la que hizo el cambio)
contienen espacios en blanco y nuevas líneas.
Tenga en cuenta que el script data de 1992, por lo que utiliza tildes 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"
(Yo probablemente no usaría el escape tan a fondo hoy en día - no es
no es necesario con el argumento -e
, por ejemplo - pero en general, este es
uno de mis scripts más simples usando escape
).
El programa escape
simplemente da salida a sus argumentos, como hace echo pero se asegura de que los argumentos estén protegidos para su uso con (un nivel de
eval; tengo un programa que hace una ejecución remota de la 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: la versión 1.1 data de 1987-01-27T14:35:49).
Es muy útil cuando se depuran scripts, ya que se puede conectar a una
línea de comandos 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 distintas 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 el *
y el */*
en la línea de comandos. Además, observe 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, nuevo
, opt
, y
, y arg con espacio
.
]
Hmm, esa es una respuesta bastante larga - quizás exégesis sea el mejor término.
El código fuente de escape
está disponible si se solicita (correo electrónico a firstname dot
lastname at gmail dot com).
El código fuente de al
es increíblemente sencillo:
#include <stdio.h>
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 ser escrito como una función de shell (pero las funciones de shell no existían en la versión local del shell Bourne cuando escribí al
por primera vez).
También hay que tener en cuenta que se puede escribir al
como un simple script de shell:
[ $# != 0 ] && printf "%s\n" "$@"
El condicional es necesario para que no produzca ninguna salida cuando no se le pasen argumentos. El comando printf
producirá una línea en blanco con sólo el argumento de la cadena de formato, pero el programa C no produce nada.
Tenga en cuenta que la respuesta de Robert es correcta, y también funciona en sh
. Se puede simplificar (de forma portátil) aún más:
for i in "$@"
es equivalente a:
for i
Es decir, ¡no necesitas nada!
Probando ($
es el 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 for
documenta esto:
for NAME [in WORDS ... ;] do COMMANDS; done
Si
'in PALABRAS ...;'
no está presente, entonces se asume'in "$@"'
.