O, una guía introductoria para el manejo robusto de nombres de archivo y otras cadenas que pasan en scripts de shell.
Escribí un script de shell que funciona bien la mayor parte del tiempo. Pero se ahoga en algunas entradas (por ejemplo, en algunos nombres de archivo).
Encontré un problema como el siguiente:
- Tengo un nombre de archivo que contiene un espacio
hello world
, y fue tratado como dos archivos separadoshello
yworld
. - Tengo una línea de entrada con dos espacios consecutivos y se redujeron a uno en la entrada.
- Los espacios en blanco iniciales y finales desaparecen de las líneas de entrada.
- A veces, cuando la entrada contiene uno de los caracteres
\[*?
, estos se reemplazan por algún texto que en realidad es el nombre de los archivos. - Hay un apóstrofe
'
(o una comilla doble"
) en la entrada y las cosas se pusieron raras después de ese punto. - Hay una barra invertida en la entrada (o: estoy usando Cygwin y algunos de mis nombres de archivo tienen
\
separadores de estilo Windows ).
¿Qué está pasando y cómo lo soluciono?
bash
shell
shell-script
quoting
whitespace
Gilles
fuente
fuente
shellcheck
ayudarlo a mejorar la calidad de sus programas.Respuestas:
Siempre use comillas dobles sustituciones de variables y sustituciones de comando:
"$foo"
,"$(foo)"
Si usa sin
$foo
comillas, su secuencia de comandos se ahogará en la entrada o los parámetros (o la salida del comando, con$(foo)
) que contienen espacios en blanco o\[*?
.Ahí puedes dejar de leer. Bueno, ok, aquí hay algunos más:
read
- Para leer la línea de entrada de acuerdo con laread
orden interna, utilizarwhile IFS= read -r line; do …
Plain
read
barras invertidas trata y espacios en blanco en especial.xargs
- Evitarxargs
. Si debe usarxargs
, haga esoxargs -0
. En lugar defind … | xargs
, prefierafind … -exec …
.xargs
trata los espacios en blanco y los caracteres\"'
especialmente.Esta respuesta se aplica a los depósitos / estilo POSIX (Bourne
sh
,ash
,dash
,bash
,ksh
,mksh
,yash
...). Los usuarios de Zsh deben omitirlo y leer el final de ¿ Cuándo es necesaria una doble cita? en lugar. Si desea todo lo esencial, lea el estándar o el manual de su shell.Tenga en cuenta que las explicaciones a continuación contienen algunas aproximaciones (afirmaciones que son ciertas en la mayoría de las condiciones pero que pueden verse afectadas por el contexto o la configuración).
¿Por qué necesito escribir
"$foo"
? ¿Qué pasa sin las comillas?$foo
no significa "tomar el valor de la variablefoo
". Significa algo mucho más complejo:foo * bar
entonces el resultado de este paso es la lista 3-elementofoo
,*
,bar
.foo
, seguida de la lista de archivos en el directorio actual, y finalmentebar
. Si el directorio actual está vacía, el resultado esfoo
,*
,bar
.Tenga en cuenta que el resultado es una lista de cadenas. Hay dos contextos en la sintaxis de shell: contexto de lista y contexto de cadena. La división de campos y la generación de nombre de archivo solo ocurren en el contexto de la lista, pero eso es la mayor parte del tiempo. Las comillas dobles delimitan un contexto de cadena: la cadena completa entre comillas dobles es una sola cadena, que no se debe dividir. (Excepción:
"$@"
expandirse a la lista de parámetros posicionales, por ejemplo,"$@"
es equivalente a"$1" "$2" "$3"
si hay tres parámetros posicionales. Consulte ¿Cuál es la diferencia entre $ * y $ @? )Lo mismo sucede con la sustitución de comandos con
$(foo)
o con`foo`
. En una nota al margen, no use`foo`
: sus reglas de cotización son extrañas y no portátiles, y todos los shells modernos$(foo)
son absolutamente equivalentes, excepto por tener reglas de cotización intuitivas.La salida de la sustitución aritmética también sufre las mismas expansiones, pero eso normalmente no es una preocupación, ya que solo contiene caracteres no expandibles (suponiendo
IFS
que no contenga dígitos o-
).Ver ¿ Cuándo es necesaria la doble cita? para obtener más detalles sobre los casos en que puede omitir las citas.
A menos que quiera que ocurra todo este rigmarole, recuerde siempre usar comillas dobles alrededor de las sustituciones de variables y comandos. Tenga cuidado: omitir las comillas puede conducir no solo a errores sino a agujeros de seguridad .
¿Cómo proceso una lista de nombres de archivo?
Si escribe
myfiles="file1 file2"
, con espacios para separar los archivos, esto no puede funcionar con nombres de archivos que contienen espacios. Los nombres de archivos Unix pueden contener cualquier carácter que no sea/
(que siempre es un separador de directorio) y bytes nulos (que no puede usar en los scripts de shell con la mayoría de los shells).Mismo problema con
myfiles=*.txt; … process $myfiles
. Cuando hace esto, la variablemyfiles
contiene la cadena de 5 caracteres*.txt
, y es cuando escribe$myfiles
que se expande el comodín. Este ejemplo realmente funcionará, hasta que cambie su script para que seamyfiles="$someprefix*.txt"; … process $myfiles
. Sisomeprefix
se establece enfinal report
, esto no funcionará.Para procesar una lista de cualquier tipo (como nombres de archivos), colóquela en una matriz. Esto requiere mksh, ksh93, yash o bash (o zsh, que no tiene todos estos problemas de citas); un shell POSIX simple (como ash o dash) no tiene variables de matriz.
Ksh88 tiene variables de matriz con una sintaxis de asignación diferente
set -A myfiles "someprefix"*.txt
(consulte la variable de asignación en un entorno ksh diferente si necesita portabilidad ksh88 / bash). Los shells de estilo Bourne / POSIX tienen una única matriz, la matriz de parámetros posicionales con los"$@"
que estableceset
y que es local para una función:¿Qué pasa con los nombres de archivo que comienzan con
-
?En una nota relacionada, tenga en cuenta que los nombres de los archivos pueden comenzar con un
-
(guión / menos), que la mayoría de los comandos interpretan como una opción. Si tiene un nombre de archivo que comienza con una parte variable, asegúrese de pasarlo--
antes, como en el fragmento anterior. Esto le indica al comando que ha llegado al final de las opciones, por lo que cualquier cosa después de eso es un nombre de archivo, incluso si comienza con-
.Alternativamente, puede asegurarse de que los nombres de sus archivos comiencen con un carácter distinto de
-
. Los nombres de archivos absolutos comienzan con/
, y puede agregarlos./
al comienzo de los nombres relativos. El siguiente fragmento convierte el contenido de la variablef
en una forma "segura" de referirse al mismo archivo con el que se garantiza que no comenzará-
.En una nota final sobre este tema, tenga en cuenta que algunos comandos interpretan
-
como entrada estándar o salida estándar, incluso después--
. Si necesita hacer referencia a un archivo real llamado-
, o si está llamando a dicho programa y no desea que lea desde stdin o escriba en stdout, asegúrese de volver a escribir-
como se indica arriba. Consulte ¿Cuál es la diferencia entre "du -sh *" y "du -sh ./*"? para mayor discusión.¿Cómo almaceno un comando en una variable?
"Comando" puede significar tres cosas: un nombre de comando (el nombre como un ejecutable, con o sin ruta completa, o el nombre de una función, incorporado o alias), un nombre de comando con argumentos o un código de shell. En consecuencia, hay diferentes formas de almacenarlos en una variable.
Si tiene un nombre de comando, simplemente guárdelo y use la variable con comillas dobles como de costumbre.
Si tiene un comando con argumentos, el problema es el mismo que con una lista de nombres de archivo anteriores: esta es una lista de cadenas, no una cadena. No puede simplemente rellenar los argumentos en una sola cadena con espacios en el medio, porque si lo hace, no puede distinguir la diferencia entre los espacios que forman parte de los argumentos y los espacios que separan los argumentos. Si su shell tiene matrices, puede usarlas.
¿Qué pasa si está utilizando un shell sin matrices? Aún puede usar los parámetros posicionales, si no le importa modificarlos.
¿Qué sucede si necesita almacenar un comando de shell complejo, por ejemplo, con redirecciones, tuberías, etc.? ¿O si no desea modificar los parámetros posicionales? Luego puede construir una cadena que contenga el comando y usar el
eval
incorporado.Tenga en cuenta las comillas anidadas en la definición de
code
: las comillas simples'…'
delimitan un literal de cadena, de modo que el valor de la variablecode
es la cadena/path/to/executable --option --message="hello world" -- /path/to/file1
. Eleval
builtin le dice al shell que analice la cadena pasada como argumento como si apareciera en el script, por lo que en ese punto se analizan las comillas y la tubería, etc.Usar
eval
es complicado. Piensa cuidadosamente sobre lo que se analiza cuando. En particular, no puede simplemente introducir un nombre de archivo en el código: debe citarlo, como lo haría si estuviera en un archivo de código fuente. No hay forma directa de hacer eso. Algo así comocode="$code $filename"
roturas si el nombre de archivo contiene ningún carácter especial shell (espacios,$
,;
,|
,<
,>
, etc.).code="$code \"$filename\""
Todavía se rompe"$\`
. Incluso secode="$code '$filename'"
rompe si el nombre del archivo contiene un'
. Hay dos solucionesAgregue una capa de comillas alrededor del nombre del archivo. La forma más fácil de hacerlo es agregar comillas simples a su alrededor y reemplazar las comillas simples por
'\''
.Mantenga la expansión variable dentro del código, de modo que se busque cuando se evalúa el código, no cuando se construye el fragmento de código. Esto es más simple pero solo funciona si la variable todavía está alrededor con el mismo valor en el momento en que se ejecuta el código, no por ejemplo, si el código se construye en un bucle.
Finalmente, ¿realmente necesitas una variable que contenga código? La forma más natural de dar un nombre a un bloque de código es definir una función.
¿Qué pasa con
read
?Sin
-r
,read
permite líneas de continuación: esta es una sola línea lógica de entrada:read
divide la línea de entrada en campos delimitados por caracteres en$IFS
(sin-r
, la barra diagonal inversa también escapa a esos). Por ejemplo, si la entrada es una línea que contiene tres palabras, seread first second third
establecefirst
en la primera palabra de entrada,second
en la segunda palabra ythird
en la tercera palabra. Si hay más palabras, la última variable contiene todo lo que queda después de configurar las anteriores. Los espacios en blanco iniciales y finales se recortan.Establecer
IFS
la cadena vacía evita cualquier recorte. Vea Por qué se usa `while IFS = read` con tanta frecuencia, en lugar de` IFS =; mientras lee..`? Para una explicación más larga.¿Qué tiene de malo
xargs
?El formato de entrada de
xargs
es cadenas separadas por espacios en blanco que opcionalmente pueden ser entre comillas simples o dobles. Ninguna herramienta estándar genera este formato.La entrada
xargs -L1
aoxargs -l
es casi una lista de líneas, pero no del todo: si hay un espacio al final de una línea, la siguiente línea es una línea de continuación.Puede usar
xargs -0
donde corresponda (y donde esté disponible: GNU (Linux, Cygwin), BusyBox, BSD, OSX, pero no está en POSIX). Eso es seguro, porque los bytes nulos no pueden aparecer en la mayoría de los datos, en particular en los nombres de archivo. Para producir una lista de nombres de archivo separados por nulos, usefind … -print0
(o puede usarfind … -exec …
como se explica a continuación).¿Cómo proceso los archivos encontrados por
find
?some_command
debe ser un comando externo, no puede ser una función de shell o un alias. Si necesita invocar un shell para procesar los archivos, llamesh
explícitamente.Tengo otra pregunta
Explore la etiqueta de comillas en este sitio, o shell o shell-script . (Haga clic en "aprender más ..." para ver algunos consejos generales y una lista seleccionada a mano de preguntas comunes). Si ha buscado y no puede encontrar una respuesta, pregunte .
fuente
$(( ... ))
(también$[...]
en algunos shells) excepto enzsh
(incluso en emulación sh) ymksh
.xargs -0
no es POSIX. Excepto con FreeBSDxargs
, generalmente desea enxargs -r0
lugar dexargs -0
.ls --quoting-style=shell-always
no es compatible conxargs
. Pruebatouch $'a\nb'; ls --quoting-style=shell-always | xargs
xargs -d "\n"
para que pueda ejecutar, por ejemplo,locate PATTERN1 |xargs -d "\n" grep PATTERN2
para buscar nombres de archivo que coincidan con PATTERN1 con contenido que coincida con PATTERN2 . Sin GNU, puede hacerlo, por ejemplo, comolocate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1
Si bien la respuesta de Gilles es excelente, yo cuestiono su punto principal
Cuando está comenzando con un shell tipo Bash que divide las palabras, sí, por supuesto, el consejo seguro es siempre usar comillas. Sin embargo, la división de palabras no siempre se realiza
§ División de palabras
Estos comandos pueden ejecutarse sin error
No estoy alentando a los usuarios a adoptar este comportamiento, pero si alguien comprende con firmeza cuándo se produce la división de palabras, entonces deberían poder decidir por sí mismos cuándo usar comillas.
fuente
foo=$bar
está bien, peroexport foo=$bar
oenv foo=$var
no lo está (al menos en algunos shells). Un consejo para principiantes: siempre cite sus variables a menos que sepa lo que está haciendo y tenga una buena razón para no hacerlo .criteria="-type f"
, entoncesfind . $criteria
funciona perofind . "$criteria"
no funciona.Hasta donde yo sé, solo hay dos casos en los que es necesario hacer comillas dobles de expansiones, y esos casos involucran los dos parámetros especiales de shell
"$@"
y"$*"
, que se especifican para expandirse de manera diferente cuando están encerrados entre comillas dobles. En todos los demás casos (excluyendo, quizás, implementaciones de matriz específicas de shell), el comportamiento de una expansión es algo configurable: hay opciones para eso.Esto no quiere decir, por supuesto, que se deben evitar las comillas dobles; por el contrario, es probablemente el método más conveniente y robusto para delimitar una expansión que el shell tiene para ofrecer. Pero creo que, como las alternativas ya han sido expuestas por expertos, este es un excelente lugar para discutir lo que sucede cuando el shell expande un valor.
El caparazón, en su corazón y alma (para aquellos que lo tienen) , es un intérprete de comandos, es un analizador, como un gran, interactivo
sed
. Si su declaración de shell se ahoga en espacios en blanco o similar, es muy probable porque no ha entendido completamente el proceso de interpretación del shell, especialmente cómo y por qué traduce una declaración de entrada en un comando procesable. El trabajo del shell es:aceptar entrada
interpretarlo y dividirlo correctamente en palabras de entrada con token
las palabras de entrada son los elementos de sintaxis de shell como
$word
oecho $words 3 4* 5
las palabras siempre se dividen en espacios en blanco, eso es solo sintaxis, pero solo los caracteres de espacios en blanco literales se sirven al shell en su archivo de entrada
expandirlos si es necesario en múltiples campos
los campos resultan de expansiones de palabras : constituyen el comando ejecutable final
exceptuando
"$@"
,$IFS
campo de división , y la expansión ruta una entrada palabra debe evaluar siempre a un solo campo .y luego ejecutar el comando resultante
La gente a menudo dice que el caparazón es un pegamento y, si esto es cierto, entonces lo que está pegando son listas de argumentos, o campos , de un proceso u otro cuando se trata de
exec
ellos. La mayoría de los shells no manejan bien elNUL
byte, si es que lo hacen, y esto se debe a que ya se están dividiendo. El shell tieneexec
mucho que hacer y debe hacerlo con unaNUL
matriz delimitada de argumentos que entrega al núcleo del sistema en eseexec
momento. Si tuviera que mezclar el delimitador del shell con sus datos delimitados, entonces el shell probablemente lo arruinaría. Sus estructuras de datos internos, como la mayoría de los programas, se basan en ese delimitador.zsh
, notablemente, no arruina esto.Y ahí es donde
$IFS
entra.$IFS
Es un parámetro de shell siempre presente, y también configurable, que define cómo el shell debe dividir las expansiones de shell de palabra a campo , específicamente en qué valores deben delimitar esos campos .$IFS
divide las expansiones de shell en delimitadores que no seanNUL
, o, en otras palabras, el shell sustituye bytes que resultan de una expansión que coincide con aquellos en el valor de$IFS
withNUL
en sus matrices de datos internas. Cuando lo mira así, puede comenzar a ver que cada expansión de shell dividida en campo es una$IFS
matriz de datos delimitada.Es importante comprender que
$IFS
solo delimita las expansiones que no están delimitadas de otra manera, lo que puede hacer con"
comillas dobles. Cuando cita una expansión, la delimita en la cabeza y al menos en la cola de su valor. En esos casos$IFS
no se aplica ya que no hay campos para separar. De hecho, una expansión con comillas dobles exhibe un comportamiento de división de campo idéntico a una expansión sin comillas cuandoIFS=
se establece en un valor vacío.A menos que se cite,
$IFS
es una$IFS
expansión de shell delimitada. El valor predeterminado es un valor específico de<space><tab><newline>
- los tres exhiben propiedades especiales cuando están contenidos dentro$IFS
. Mientras que$IFS
se especifica cualquier otro valor para evaluar a un solo campo por cada ocurrencia de expansión , el$IFS
espacio en blanco , cualquiera de esos tres, se especifica para eludir a un solo campo por secuencia de expansión y las secuencias iniciales / finales se eluyen por completo. Esto es probablemente más fácil de entender a través del ejemplo.Pero eso es solo
$IFS
: solo la división de palabras o el espacio en blanco como se le preguntó, entonces, ¿qué pasa con los caracteres especiales ?El shell, de forma predeterminada, también expandirá ciertos tokens sin comillas (
?*[
como se indica en otro lugar aquí) en múltiples campos cuando aparecen en una lista. Esto se llama expansión de nombre de ruta o globbing . Es una herramienta increíblemente útil y, como ocurre después de la división de campos en el orden de análisis del shell, no se ve afectada por $ IFS : los campos generados por la expansión de un nombre de ruta se delimitan en la cabecera / cola de los nombres de archivo, independientemente de si sus contenidos contienen caracteres actualmente en$IFS
. Este comportamiento está activado de forma predeterminada, pero de lo contrario se configura muy fácilmente.Que indica al shell no a glob . La expansión del nombre de ruta no ocurrirá al menos hasta que esa configuración se deshaga de alguna manera, como si el shell actual se reemplaza por otro nuevo proceso de shell o ...
... se emite a la cáscara. Las comillas dobles, como también lo hacen para
$IFS
la división de campos , hacen que esta configuración global sea innecesaria por expansión. Entonces:... si la expansión de nombre de ruta está habilitada actualmente, probablemente producirá resultados muy diferentes por argumento, ya que el primero se expandirá solo a su valor literal (el carácter de asterisco único, es decir, en absoluto) y el segundo solo al mismo si el directorio de trabajo actual no contiene nombres de archivo que puedan coincidir (y coincide con casi todos) . Sin embargo, si lo haces:
... los resultados para ambos argumentos son idénticos;
*
en ese caso, no se expande.fuente
IFS
funciona realmente. Lo que no entiendo es por qué sería jamás ser una buena idea establecerIFS
a algo que no sea por defecto.$IFS
.cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; done
impresiones\n
entoncesusr\n
entoncesbin\n
. El primeroecho
está vacío porque/
es un campo nulo. Los componentes_ruta pueden tener nuevas líneas o espacios o lo que sea, no importaría porque los componentes estaban divididos/
y no el valor predeterminado. la gente lo haceawk
todo el tiempo, de todos modos. tu caparazón lo hace tambiénTuve un gran proyecto de video con espacios en los nombres de archivo y espacios en los nombres de directorio. Si bien
find -type f -print0 | xargs -0
funciona para varios propósitos y en diferentes shells, encuentro que usar un IFS (separador de campo de entrada) personalizado le brinda más flexibilidad si está usando bash. El fragmento a continuación usa bash y establece IFS en solo una nueva línea; siempre que no haya nuevas líneas en sus nombres de archivo:Tenga en cuenta el uso de parens para aislar la redefinición de IFS. He leído otras publicaciones sobre cómo recuperar IFS, pero esto es más fácil.
Además, configurar IFS en nueva línea le permite establecer variables de shell de antemano e imprimirlas fácilmente. Por ejemplo, puedo hacer crecer una variable V incrementalmente usando nuevas líneas como separadores:
y correspondientemente:
Ahora puedo "enumerar" la configuración de V
echo "$V"
usando comillas dobles para generar las nuevas líneas. (Agradezca a este hilo por la$'\n'
explicación).fuente
zsh
, puede usarIFS=$'\0'
y usar-print0
(zsh
no hace glob en las expansiones, por lo que los caracteres glob no son un problema allí).set -f
. Por otro lado, su enfoque falla fundamentalmente con los nombres de archivo que contienen nuevas líneas. Cuando se trata de datos que no sean nombres de archivo, también falla con elementos vacíos.Teniendo en cuenta todas las implicaciones de seguridad mencionadas anteriormente y suponiendo que confía y tiene control sobre las variables que está expandiendo, es posible tener múltiples rutas con espacios en blanco
eval
. ¡Pero ten cuidado!fuente