Esto se ha discutido en una serie de preguntas sobre unix.SE, intentaré recopilar todos los problemas que pueda encontrar aquí. Referencias al final.
Por qué falla
La razón por la que enfrenta esos problemas es la división de palabras y el hecho de que las comillas expandidas a partir de variables no actúan como comillas, sino que son solo caracteres ordinarios.
Los casos presentados en la pregunta:
$ abc='ls -l "/tmp/test/my dir"'
Aquí, $abc
se divide, y ls
obtiene los dos argumentos "/tmp/test/my
y dir"
(con las comillas al frente del primero y al reverso del segundo):
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
Aquí, se cita la expansión, por lo que se mantiene como una sola palabra. El shell intenta encontrar un programa llamado ls -l "/tmp/test/my dir"
, espacios y comillas incluidos.
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
Y aquí, solo $abc
se toma la primera palabra o como argumento -c
, por lo que Bash solo se ejecuta ls
en el directorio actual. Las otras palabras son argumentos para golpear, y se utilizan para rellenar $0
, $1
etc.
$ bash -c $abc
'my dir'
Con bash -c "$abc"
, y eval "$abc"
, hay un paso adicional de procesamiento de shell, que hace que las cotizaciones funcionen, pero también hace que todas las expansiones de shell se procesen nuevamente , por lo que existe el riesgo de ejecutar accidentalmente una expansión de comando a partir de datos proporcionados por el usuario, a menos que sea muy cuidado con las citas.
Mejores formas de hacerlo
Las dos mejores formas de almacenar un comando son a) usar una función en su lugar, b) usar una variable de matriz (o los parámetros posicionales).
Usando una función:
Simplemente declare una función con el comando dentro y ejecute la función como si fuera un comando. Las expansiones en los comandos dentro de la función solo se procesan cuando se ejecuta el comando, no cuando está definido, y no necesita citar los comandos individuales.
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
Usando una matriz:
Las matrices permiten crear variables de varias palabras donde las palabras individuales contienen espacios en blanco. Aquí, las palabras individuales se almacenan como elementos de matriz distintos, y la "${array[@]}"
expansión expande cada elemento como palabras de shell separadas:
# define the array
mycmd=(ls -l "/tmp/test/my dir")
# run the command
"${mycmd[@]}"
La sintaxis es un poco horrible, pero las matrices también le permiten construir la línea de comando pieza por pieza. Por ejemplo:
mycmd=(ls) # initial command
if [ "$want_detail" = 1 ]; then
mycmd+=(-l) # optional flag
fi
mycmd+=("$targetdir") # the filename
"${mycmd[@]}"
o mantenga partes de la línea de comando constantes y use la matriz para llenar solo una parte, opciones o nombres de archivo:
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
La desventaja de los arreglos es que no son una característica estándar, por lo que los shells POSIX simples (como dash
el predeterminado /bin/sh
en Debian / Ubuntu) no los admiten (pero vea a continuación). Sin embargo, Bash, ksh y zsh sí, por lo que es probable que su sistema tenga algún shell que admita matrices.
Utilizando "$@"
En shells sin soporte para matrices con nombre, aún se pueden usar los parámetros posicionales (la pseudo-matriz "$@"
) para contener los argumentos de un comando.
Los siguientes deben ser bits de script portátiles que hacen el equivalente de los bits de código en la sección anterior. La matriz se reemplaza con "$@"
la lista de parámetros posicionales. La configuración "$@"
se realiza con set
, y las comillas dobles "$@"
son importantes (esto hace que los elementos de la lista se citen individualmente).
Primero, simplemente almacenando un comando con argumentos "$@"
y ejecutándolo:
set -- ls -l "/tmp/test/my dir"
"$@"
Configuración condicional de partes de las opciones de línea de comando para un comando:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "$@" -l
fi
set -- "$@" "$targetdir"
"$@"
Solo se usa "$@"
para opciones y operandos:
set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir
transmutate "$@"
(Por supuesto, "$@"
generalmente se rellena con los argumentos del script en sí, por lo que deberá guardarlos en algún lugar antes de volver a utilizarlos "$@"
).
¡Cuidado con eval
!
A medida que eval
introduce un nivel adicional de procesamiento de cotizaciones y expansión, debe tener cuidado con las aportaciones del usuario. Por ejemplo, esto funciona siempre que el usuario no escriba comillas simples:
read -r filename
cmd="ls -l '$filename'"
eval "$cmd";
Pero si dan la entrada '$(uname)'.txt
, su script ejecuta felizmente la sustitución del comando.
Una versión con matrices es inmune a eso, ya que las palabras se mantienen separadas durante todo el tiempo, no hay presupuesto u otro procesamiento para el contenido de filename
.
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
Referencias