Implicaciones de seguridad de olvidarse de citar una variable en bash / POSIX shells

206

Si ha estado siguiendo unix.stackexchange.com durante un tiempo, debería saber que dejar una variable sin comillas en el contexto de la lista (como en echo $var) en los shells Bourne / POSIX (zsh es la excepción) tiene un significado muy especial y no debe hacerse a menos que tenga una muy buena razón para hacerlo.

Se analiza en profundidad en una serie de preguntas y respuestas aquí (ejemplos: ? ¿Por qué la cáscara estrangulador secuencia de comandos en el espacio en blanco u otros caracteres especiales , cuando es de doble citando necesario? , Expansión de una variable de shell y el efecto de pegote y se dividió en él , Citado vs expansión de cadena sin comillas)

Ese ha sido el caso desde el lanzamiento inicial del shell Bourne a finales de los 70 y no ha sido modificado por el shell Korn (uno de los mayores remordimientos de David Korn (pregunta # 7) ) o bashque mayormente copió el shell Korn, y eso es cómo ha sido especificado por POSIX / Unix.

Ahora, todavía estamos viendo una serie de respuestas aquí e incluso ocasionalmente lanzamos código de shell público donde las variables no se citan. Hubieras pensado que la gente ya habría aprendido.

En mi experiencia, hay principalmente 3 tipos de personas que omiten citar sus variables:

  • principiantes Esos pueden ser excusados ​​ya que es una sintaxis completamente intuitiva. Y es nuestro papel en este sitio educarlos.

  • gente olvidadiza

  • personas que no están convencidas incluso después de martilleos repetidos, que piensan que seguramente el autor de Bourne shell no tenía la intención de citar todas nuestras variables .

Tal vez podamos convencerlos si exponemos el riesgo asociado con este tipo de comportamientos.

¿Qué es lo peor que puede suceder si olvida citar sus variables? ¿Es realmente tan malo?

¿De qué tipo de vulnerabilidad estamos hablando aquí?

¿En qué contextos puede ser un problema?

Stéphane Chazelas
fuente
8
BashPitfalls es algo que te gustará, creo.
pawel7318
vínculo de retroceso de este artículo que escribí , gracias por la redacción
mirabilos
55
Me gustaría sugerir agregar un cuarto grupo: personas que han sido golpeadas en la cabeza por citar en exceso demasiadas veces, tal vez por miembros del tercer grupo que expresan su frustración con los demás (la víctima se convierte en el acosador). Lo triste, por supuesto, es que aquellos del cuarto grupo pueden terminar fallando en citar las cosas cuando más importa.
ack

Respuestas:

201

Preámbulo

Primero, diría que no es la forma correcta de abordar el problema. Es un poco como decir " no deberías asesinar personas porque de lo contrario irás a la cárcel ".

Del mismo modo, no cita su variable porque de lo contrario está introduciendo vulnerabilidades de seguridad. Usted cita sus variables porque está mal no hacerlo (pero si el temor a la cárcel puede ayudar, ¿por qué no?).

Un pequeño resumen para aquellos que acaban de subirse al tren.

En la mayoría de los shells, dejar una expansión variable sin comillas (aunque eso (y el resto de esta respuesta) también se aplica a la sustitución de comandos ( `...`o $(...)) y la expansión aritmética ( $((...))o $[...])) tiene un significado muy especial. La mejor manera de describirlo es que es como invocar algún tipo de operador implícito split + glob¹ .

cmd $var

en otro idioma se escribiría algo como:

cmd(glob(split($var)))

$varprimero se divide en una lista de palabras de acuerdo con reglas complejas que involucran el $IFSparámetro especial (la parte dividida ) y luego cada palabra resultante de esa división se considera como un patrón que se expande a una lista de archivos que coinciden (la parte global ) .

Como ejemplo, si $varcontiene *.txt,/var/*.xmly $IFS contiene ,, cmdse llamaría con varios argumentos, siendo el primero cmdy los siguientes los txt archivos en el directorio actual y los xmlarchivos en /var.

Si quisieras llamar cmdsolo con los dos argumentos literales cmd y *.txt,/var/*.xmlescribirías:

cmd "$var"

que estaría en tu otro idioma más familiar:

cmd($var)

¿Qué queremos decir con vulnerabilidad en un shell ?

Después de todo, se sabe desde los albores del tiempo que los scripts de shell no deben usarse en contextos sensibles a la seguridad. Seguramente, OK, dejar una variable sin comillas es un error, pero eso no puede hacer tanto daño, ¿verdad?

Bueno, a pesar del hecho de que alguien le diría que los scripts de shell nunca deberían usarse para CGIs web, o que afortunadamente la mayoría de los sistemas no permiten scripts de shell setuid / setgid hoy en día, una cosa que es shellshock (el error bash explotable remotamente que hizo que los titulares en septiembre de 2014) revelados es que los shells todavía se usan ampliamente donde probablemente no deberían: en CGI, en scripts de enlace de cliente DHCP, en comandos sudoers, invocados por (si no como ) comandos setuid ...

A veces sin saberlo. Por ejemplo, system('cmd $PATH_INFO') en un script php/ perl/ pythonCGI invoca un shell para interpretar esa línea de comando (sin mencionar el hecho de que en cmdsí mismo puede ser un script de shell y su autor puede nunca haber esperado que se llame desde un CGI).

Tienes una vulnerabilidad cuando hay un camino para la escalada de privilegios, es decir, cuando alguien (llamémosle el atacante ) puede hacer algo que no debe hacer.

Invariablemente, eso significa que el atacante proporciona datos, que los datos están siendo procesados ​​por un usuario / proceso privilegiado que inadvertidamente hace algo que no debería estar haciendo, en la mayoría de los casos debido a un error.

Básicamente, tienes un problema cuando tu código con errores procesa datos bajo el control del atacante .

Ahora, no siempre es obvio de dónde provienen esos datos , y a menudo es difícil saber si su código alguna vez podrá procesar datos no confiables.

En lo que respecta a las variables, en el caso de un script CGI, es bastante obvio, los datos son los parámetros CGI GET / POST y cosas como cookies, ruta, host ... parámetros.

Para un script setuid (que se ejecuta como un usuario cuando es invocado por otro), son los argumentos o las variables de entorno.

Otro vector muy común son los nombres de archivo. Si obtiene una lista de archivos de un directorio, es posible que el atacante haya plantado archivos allí .

En ese sentido, incluso cuando se le solicite un shell interactivo, podría ser vulnerable (al procesar archivos en, /tmpo ~/tmp por ejemplo).

Incluso a ~/.bashrcpuede ser vulnerable (por ejemplo, bashlo interpretará cuando se invoque sshpara ejecutar un ForcedCommand like en gitimplementaciones de servidor con algunas variables bajo el control del cliente).

Ahora, un script no puede ser llamado directamente para procesar datos no confiables, pero puede ser llamado por otro comando que lo haga. O su código incorrecto puede ser copiado en scripts que lo hagan (por usted 3 años más adelante o uno de sus colegas). Un lugar donde es particularmente crítico es en las respuestas en los sitios de preguntas y respuestas, ya que nunca sabrá dónde pueden terminar las copias de su código.

Abajo a los negocios; ¿qué tan malo es?

Dejar una variable (o sustitución de comando) sin comillas es, con mucho, la fuente número uno de vulnerabilidades de seguridad asociadas con el código de shell. En parte porque esos errores a menudo se traducen en vulnerabilidades, pero también porque es muy común ver variables sin comillas.

En realidad, al buscar vulnerabilidades en el código de shell, lo primero que debe hacer es buscar variables sin comillas. Es fácil de detectar, a menudo un buen candidato, generalmente fácil de rastrear a los datos controlados por el atacante.

Hay un número infinito de formas en que una variable sin comillas puede convertirse en una vulnerabilidad. Solo daré algunas tendencias comunes aquí.

Divulgación de información

La mayoría de las personas se toparán con errores asociados con variables no citadas debido a la parte dividida (por ejemplo, es común que los archivos tengan espacios en sus nombres hoy en día y el espacio está en el valor predeterminado de IFS). Mucha gente pasará por alto la parte glob . La parte glob es al menos tan peligrosa como la parte dividida .

La aplicación de globo realizada sobre una entrada externa no desinfectada significa que el atacante puede hacerle leer el contenido de cualquier directorio.

En:

echo You entered: $unsanitised_external_input

si $unsanitised_external_inputcontiene /*, eso significa que el atacante puede ver el contenido de /. No es gran cosa. Sin embargo, se vuelve más interesante con lo /home/*que le brinda una lista de nombres de usuario en la máquina /tmp/*, /home/*/.forwardpara obtener sugerencias sobre otras prácticas peligrosas, /etc/rc*/*para servicios habilitados ... No es necesario nombrarlos individualmente. Un valor de /* /*/* /*/*/*...solo enumerará todo el sistema de archivos.

Denegación de vulnerabilidades de servicio.

Llevando el caso anterior demasiado lejos y tenemos un DoS.

En realidad, cualquier variable sin comillas en el contexto de la lista con entrada no higiénica es al menos una vulnerabilidad DoS.

Incluso los scripters de shell expertos suelen olvidarse de citar cosas como:

#! /bin/sh -
: ${QUERYSTRING=$1}

:es el comando no-op. ¿Qué podría salir mal?

Eso está destinado a asignar $1a $QUERYSTRINGif no $QUERYSTRING estaba configurado. Esa es una manera rápida de hacer que un script CGI sea invocable desde la línea de comandos también.

Sin $QUERYSTRINGembargo, todavía se expande y, como no se cita, se invoca el operador split + glob .

Ahora, hay algunos globos que son particularmente caros de expandir. El /*/*/*/*primero es lo suficientemente malo, ya que significa enumerar directorios de hasta 4 niveles inferiores. Además de la actividad del disco y la CPU, eso significa almacenar decenas de miles de rutas de archivos (40k aquí en una máquina virtual de servidor mínima, 10k de los cuales directorios).

Ahora /*/*/*/*/../../../../*/*/*/*significa 40k x 10k y /*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*es suficiente para poner de rodillas incluso a la máquina más poderosa.

Pruébelo usted mismo (aunque esté preparado para que su máquina se bloquee o cuelgue):

a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'

Por supuesto, si el código es:

echo $QUERYSTRING > /some/file

Entonces puedes llenar el disco.

Simplemente haga una búsqueda en google en shell cgi o bash cgi o ksh cgi , y encontrará algunas páginas que le muestran cómo escribir CGI en shells. Observe cómo la mitad de los que procesan los parámetros son vulnerables.

Incluso el propio David Korn es vulnerable (mira el manejo de las cookies).

hasta vulnerabilidades de ejecución de código arbitrario

La ejecución de código arbitrario es el peor tipo de vulnerabilidad, ya que si el atacante puede ejecutar cualquier comando, no hay límite en lo que puede hacer.

Esa es generalmente la parte dividida que conduce a esos. Esa división da como resultado que se pasen varios argumentos a los comandos cuando solo se espera uno. Mientras que el primero de ellos se usará en el contexto esperado, los otros estarán en un contexto diferente, por lo que potencialmente se interpretarán de manera diferente. Mejor con un ejemplo:

awk -v foo=$external_input '$2 == foo'

Aquí, la intención era asignar el contenido de la $external_inputvariable de shell a la foo awkvariable.

Ahora:

$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux

La segunda palabra resultante de la división de $external_input no se asigna foosino que se considera como awkcódigo (aquí, que ejecuta un comando arbitrario:) uname.

Esto es especialmente un problema para los comandos que se pueden ejecutar otros comandos ( awk, env, sed(GNU uno), perl, find...) sobre todo con las variantes de GNU (que aceptan opciones después de argumentos). A veces, no sospecharías que los comandos puedan ejecutar otros como ksh, basho zsh's [o printf...

for file in *; do
  [ -f $file ] || continue
  something-that-would-be-dangerous-if-$file-were-a-directory
done

Si creamos un directorio llamado x -o yes, entonces la prueba se vuelve positiva, porque estamos evaluando una expresión condicional completamente diferente.

Peor aún, si creamos un archivo llamado x -a a[0$(uname>&2)] -gt 1, con todas las implementaciones de ksh al menos (que incluye la sh mayoría de los Unices comerciales y algunos BSD), que se ejecuta uname porque esos shells realizan una evaluación aritmética en los operadores de comparación numéricos del [comando.

$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux

Lo mismo bashpara un nombre de archivo como x -a -v a[0$(uname>&2)].

Por supuesto, si no pueden obtener una ejecución arbitraria, el atacante puede conformarse con un daño menor (lo que puede ayudar a obtener una ejecución arbitraria). Cualquier comando que pueda escribir archivos o cambiar permisos, propiedad o tener algún efecto principal o secundario podría ser explotado.

Se pueden hacer todo tipo de cosas con nombres de archivos.

$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done

Y terminas haciendo ..grabable (recursivamente con GNU chmod).

Las secuencias de comandos que realizan el procesamiento automático de archivos en áreas públicamente grabables como /tmpdeben escribirse con mucho cuidado.

Qué pasa [ $# -gt 1 ]

Eso es algo que encuentro exasperante. Algunas personas se molestan en preguntarse si una expansión en particular puede ser problemática para decidir si pueden omitir las citas.

Es como decir Oye, parece que $#no puede estar sujeto al operador split + glob, pidamos al shell que lo divida + glob . O bien , vamos a escribir un código incorrecto solo porque es poco probable que se solucione el error .

Ahora, ¿qué tan improbable es? OK, $#(o $!, $?o cualquier sustitución aritmética) solo puede contener dígitos (o -para algunos) por lo que la parte glob está fuera. Sin embargo, para que la parte dividida haga algo, todo lo que necesitamos es $IFScontener dígitos (o -).

Con algunos proyectiles, $IFSpuede heredarse del entorno, pero si el entorno no es seguro, se acabó el juego de todos modos.

Ahora si escribes una función como:

my_function() {
  [ $# -eq 2 ] || return
  ...
}

Lo que eso significa es que el comportamiento de su función depende del contexto en el que se llama. O, en otras palabras, se $IFS convierte en una de las entradas. Estrictamente hablando, cuando escribe la documentación API para su función, debería ser algo como:

# my_function
#   inputs:
#     $1: source directory
#     $2: destination directory
#   $IFS: used to split $#, expected not to contain digits...

Y el código que llama a su función debe asegurarse de $IFSque no contenga dígitos. Todo eso porque no tenía ganas de escribir esos 2 caracteres de comillas dobles.

Ahora, para que ese [ $# -eq 2 ]error se convierta en una vulnerabilidad, necesitaría de alguna manera que el valor de $IFSestar bajo el control del atacante . Posiblemente, eso normalmente no sucedería a menos que el atacante lograra explotar otro error.

Sin embargo, eso no es desconocido. Un caso común es cuando las personas olvidan desinfectar los datos antes de usarlos en expresiones aritméticas. Ya hemos visto anteriormente que puede permitir la ejecución de código arbitrario en algunos shells, pero en todos ellos, permite al atacante dar a cualquier variable un valor entero.

Por ejemplo:

n=$(($1 + 1))
if [ $# -gt 2 ]; then
  echo >&2 "Too many arguments"
  exit 1
fi

Y con un $1valor con (IFS=-1234567890), esa evaluación aritmética tiene el efecto secundario de la configuración IFS y el siguiente [ comando falla, lo que significa que se omite la comprobación de demasiados argumentos .

¿Qué pasa cuando no se invoca el operador split + glob ?

Hay otro caso en el que se necesitan comillas alrededor de variables y otras expansiones: cuando se usa como patrón.

[[ $a = $b ]]   # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac

no probar si $ay $bson los mismos (excepto con zsh) pero si $acoincide con el patrón en $b. Y hay que citar $bsi usted desea comparar como cadenas (lo mismo en "${a#$b}"o "${a%$b}"o "${a##*$b*}"donde $bdebe ser citado si no es para ser tomado como un patrón).

Lo que esto significa es que [[ $a = $b ]]puede devolver cierto en los casos en que $aes diferente de $b(por ejemplo, cuando $aes anythingy $bes *) o puede devolver false cuando son idénticos (por ejemplo, cuando ambos $ay $bson [a]).

¿Puede eso crear una vulnerabilidad de seguridad? Sí, como cualquier error. Aquí, el atacante puede alterar el flujo de código lógico de su script y / o romper las suposiciones que está haciendo su script. Por ejemplo, con un código como:

if [[ $1 = $2 ]]; then
   echo >&2 '$1 and $2 cannot be the same or damage will incur'
   exit 1
fi

El atacante puede pasar por alto el cheque al pasar '[a]' '[a]'.

Ahora, si no se aplica esa coincidencia de patrón ni el operador de división + glob , ¿cuál es el peligro de dejar una variable sin comillas?

Tengo que admitir que sí escribo:

a=$b
case $a in...

Allí, las citas no perjudican pero no son estrictamente necesarias.

Sin embargo, un efecto secundario de omitir citas en esos casos (por ejemplo, en las respuestas de preguntas y respuestas) es que puede enviar un mensaje incorrecto a los principiantes: que puede estar bien no citar variables .

Por ejemplo, pueden comenzar a pensar que si a=$bestá bien, entonces también export a=$blo estaría (lo cual no está en muchos shells, ya que está en argumentos para el exportcomando en el contexto de la lista) o env a=$b.

¿Qué hay de zsh?

zshsolucionó la mayoría de esas torpezas de diseño. En zsh(al menos cuando no está en el modo de emulación sh / ksh), si desea dividir , globular o emparejar patrones , debe solicitarlo explícitamente: $=vardividir y $~varglob o para que el contenido de la variable sea tratado como un patrón.

Sin embargo, la división (pero no globalización) todavía se realiza implícitamente en la sustitución de comandos sin comillas (como en echo $(cmd)).

Además, un efecto secundario a veces no deseado de no citar variables es la eliminación de vacíos . El zshcomportamiento es similar a lo que puede lograr en otros shells al deshabilitar el globbing por completo (con set -f) y dividir (con IFS=''). Todavía en:

cmd $var

No habrá división + glob , pero si $varestá vacío, en lugar de recibir un argumento vacío, cmdno recibirá ningún argumento.

Eso puede causar errores (como lo obvio [ -n $var ]). Posiblemente eso pueda romper las expectativas y suposiciones de un script y causar vulnerabilidades, pero no puedo encontrar un ejemplo no muy descabellado en este momento).

¿Y cuando se hace necesario la división + glob operador?

Sí, generalmente es cuando quieres dejar tu variable sin comillas. Pero luego debe asegurarse de sintonizar sus operadores de división y glob correctamente antes de usarlo. Si solo desea la parte dividida y no la parte glob (que es el caso la mayor parte del tiempo), entonces necesita deshabilitar globbing ( set -o noglob/ set -f) y corregir $IFS. De lo contrario, también causará vulnerabilidades (como el ejemplo CGI de David Korn mencionado anteriormente).

Conclusión

En resumen, dejar una variable (o sustitución de comando o expansión aritmética) sin comillas en shells puede ser muy peligroso, especialmente cuando se realiza en contextos incorrectos, y es muy difícil saber cuáles son esos contextos incorrectos.

Esa es una de las razones por las que se considera una mala práctica .

Gracias por leer hasta ahora. Si se te pasa por la cabeza, no te preocupes. No se puede esperar que todos entiendan todas las implicaciones de escribir su código de la manera en que lo escriben. Es por eso que tenemos recomendaciones de buenas prácticas , para que se puedan seguir sin necesariamente entender por qué.

(y en caso de que aún no sea obvio, evite escribir código confidencial de seguridad en shells).

¡Y por favor cite sus variables en sus respuestas en este sitio!


¹En ksh93y pdkshy derivados, la expansión de llaves también se realiza a menos que el globbing esté deshabilitado (en el caso de ksh93versiones hasta ksh93u +, incluso cuando la braceexpandopción está deshabilitada).

Stéphane Chazelas
fuente
Tenga en cuenta que, con [[, solo es necesario citar el RHS de las comparaciones:if [[ $1 = "$2" ]]; then
mirabilos
2
@mirabilos, sí, pero el LHS necesidades no no a ser citado, por lo que no hay ninguna razón de peso para no citar que hay (si hemos de tomar la decisión consciente de citar de forma predeterminada , ya que parece ser lo más sensato ) También tenga en cuenta que [[ $* = "$var" ]]no es lo mismo que [[ "$*" = "$var" ]]si el primer carácter de $IFSno es espacio con bash(y también mkshsi $IFSestá vacío, aunque en ese caso no estoy seguro de qué $*es igual, ¿debería generar un error?)).
Stéphane Chazelas
1
Sí, puedes citar por defecto allí. Por favor, no más errores sobre la división de campos en este momento, todavía tengo que arreglar aquellos que conozco (de usted y otros) primero, antes de que podamos volver a evaluar esto.
mirabilos
2
@Barmar, suponiendo que quisieras decir foo='bar; rm *', no, no lo hará, sin embargo, enumerará el contenido del directorio actual que puede contar como una divulgación de información. print $fooSin embargo, en ksh93(donde printes el reemplazo para echoeso se abordan algunas de sus deficiencias) tiene una vulnerabilidad de inyección de código (por ejemplo, con foo='-f%.0d z[0$(uname>&2)]') (realmente necesita print -r -- "$foo". echo "$foo"Todavía está mal y no es reparable (aunque generalmente menos dañino)).
Stéphane Chazelas
3
No soy un experto en bash, pero lo he codificado durante más de una década. Usé muchas comillas, pero principalmente para manejar espacios en blanco incrustados. ¡Ahora los usaré mucho más! Sería bueno si alguien ampliara esta respuesta para que sea un poco más fácil absorber todos los puntos finos. Obtuve mucho, pero también extrañé mucho. Ya es una publicación larga, pero sé que hay mucho más aquí para que yo aprenda. ¡Gracias!
Joe
34

[Inspirado por esta respuesta de cas .]

Pero que si …?

Pero, ¿qué pasa si mi script establece una variable en un valor conocido antes de usarlo? En particular, ¿qué sucede si establece una variable en uno de dos o más valores posibles (pero siempre lo establece en algo conocido) y ninguno de los valores contiene caracteres de espacio o glob? ¿No es seguro usarlo sin comillas en ese caso ?

¿Y qué pasa si uno de los valores posibles es la cadena vacía, y estoy dependiendo de la "eliminación de vacíos"? Es decir, si la variable contiene la cadena vacía, no quiero obtener la cadena vacía en mi comando; No quiero obtener nada Por ejemplo,

si alguna_condición
entonces
    ignorecase = "- i"
más
    ignorecase = ""
fi
                                        # Tenga en cuenta que las comillas en los comandos anteriores no son estrictamente necesarias. 
grep $ ignorecase   other_ grep _args

No puedo decir ; eso fallará si es la cadena vacía.grep "$ignorecase" other_grep_args$ignorecase

Respuesta:

Como se discutió en la otra respuesta, esto todavía fallará si IFScontiene un -o un i. Si se ha asegurado de que IFSno contiene ningún carácter en su variable (y está seguro de que su variable no contiene ningún carácter global), entonces esto probablemente sea seguro.

Pero hay una forma más segura (aunque es algo fea y poco intuitiva): usar ${ignorecase:+"$ignorecase"}. De la especificación del lenguaje de comandos de shell POSIX , en  2.6.2 Expansión de parámetros ,

${parameter:+[word]}

    Usar valor alternativo.   Si no parameterestá establecido o es nulo, se sustituirá nulo; de lo contrario, se sustituirá la expansión de word (o una cadena vacía si wordse omite).

El truco aquí, tal como es, es que estamos usando ignorecasecomo parameter y "$ignorecase"como el word. Entonces ${ignorecase:+"$ignorecase"}significa

Si no $ignorecaseestá establecido o es nulo (es decir, está vacío), se sustituirá nulo (es decir, nada entre comillas ); de lo contrario, la expansión de "$ignorecase"será sustituida.

Esto nos lleva a donde queremos ir: si la variable se establece en la cadena vacía, se "eliminará" (toda esta expresión enrevesada no se evaluará a nada , ni siquiera una cadena vacía), y si la variable tiene un valor no -valor vacío, obtenemos ese valor, citado.


Pero que si …?

Pero, ¿qué pasa si tengo una variable que quiero / necesito dividir en palabras? (De lo contrario, es como el primer caso; mi secuencia de comandos ha establecido la variable, y estoy seguro de que no contiene caracteres globales. Pero puede contener espacios, y quiero que se divida en argumentos separados en el espacio límites.
PD: Todavía quiero la eliminación de vacíos.)

Por ejemplo,

si alguna_condición
entonces
    criterio = "- tipo f"
más
    criterio = ""
fi
si alguna_ otra_condición
entonces
    criterios = "$ criterios -mtime +42"
fi
find "$ start_directory" $ criterios   other_ find _args

Respuesta:

Puede pensar que este es un caso para usar eval.  ¡No!   Resista la tentación de siquiera pensar en usar evalaquí.

Una vez más, si se ha asegurado de que IFSno contiene ningún carácter en su variable (excepto los espacios, que desea honrar), y está seguro de que su variable no contiene ningún carácter global, entonces lo anterior es probablemente seguro.

Pero, si está usando bash (o ksh, zsh o yash), hay una forma más segura: use una matriz:

si alguna_condición
entonces
    criterios = (- tipo f) # Podrías decir `criterios = (" - tipo "" f ")`, pero es realmente innecesario.
más
    criterios = () # ¡No use comillas en este comando!
fi
si alguna_ otra_condición
entonces
    criterios + = (- mtime +42) # Nota: no `=`, sino ` + =`, para agregar (agregar) a una matriz.
fi
find "$ start_directory" "$ {criterios [@]}"   other_ find _args

De bash (1) ,

Se puede hacer referencia a cualquier elemento de una matriz usando . ... Si es o  , la palabra se expande a todos los miembros de . Estos subíndices difieren solo cuando la palabra aparece entre comillas dobles. Si la palabra está entre comillas dobles, ... expande cada elemento de a una palabra separada.${name[subscript]}subscript@*name${name[@]}name

Entonces se "${criteria[@]}"expande a (en el ejemplo anterior) los elementos cero, dos o cuatro de la criteriamatriz, cada uno citado. En particular, si ninguna de las condiciones  s es verdadera, la criteriamatriz no tiene contenido (como lo establece la criteria=()instrucción) y no se "${criteria[@]}"evalúa como nada (ni siquiera una cadena vacía inconveniente).


Esto se vuelve especialmente interesante y complicado cuando se trata de varias palabras, algunas de las cuales son entradas dinámicas (del usuario), que no conoce de antemano, y pueden contener espacios u otros caracteres especiales. Considerar:

printf "Ingrese el nombre del archivo a buscar:"
leer fname
if ["$ fname"! = ""]
entonces
    criterio + = (- nombre "$ fname")
fi

Tenga en cuenta que $fnamese cita cada vez que se utiliza. Esto funciona incluso si el usuario ingresa algo como foo baro foo*"${criteria[@]}"evalúa a -name "foo bar"o -name "foo*". (Recuerde que se cita cada elemento de la matriz).

Las matrices no funcionan en todos los shells POSIX; Las matrices son un ksh / bash / zsh / yash-ism. Excepto ... hay una matriz que admite todos los shells: la lista de argumentos, también conocida como "$@". Si ha terminado con la lista de argumentos con la que fue invocado (por ejemplo, ha copiado todos los "parámetros posicionales" (argumentos) en variables, o los ha procesado de otra manera), puede usar la lista arg como una matriz:

si alguna_condición
entonces
    set - -type f # Podrías decir `set -" -type "" f "`, pero es realmente innecesario.
más
    conjunto -
fi
si alguna_ otra_condición
entonces
    conjunto - "$ @" -mtime +42
fi
# Del mismo modo: set - "$ @" -name "$ fname"
find "$ start_directory" "$ @"   other_ find _args

La "$@"construcción (que, históricamente, vino primero) tiene la misma semántica que : expande cada argumento (es decir, cada elemento de la lista de argumentos) a una palabra separada, como si hubiera escrito ."${name[@]}""$1" "$2" "$3" …

Extracto de la especificación del lenguaje de comandos de shell POSIX , en 2.5.2 Parámetros especiales ,

@

    Se expande a los parámetros posicionales, comenzando desde uno, inicialmente produciendo un campo para cada parámetro posicional que se establece. ..., los campos iniciales se conservarán como campos separados, ... Si no hay parámetros posicionales, la expansión de @generará campos cero, incluso cuando @esté entre comillas dobles; ...

El texto completo es algo críptico; El punto clave es que especifica que "$@"generará campos cero cuando no hay parámetros posicionales. Nota histórica: cuando "$@"se introdujo por primera vez en el shell Bourne (predecesor de bash) en 1979, tenía un error que "$@"fue reemplazado por una sola cadena vacía cuando no había parámetros posicionales; ver ¿Qué ${1+"$@"}significa en un script de shell y en qué se diferencia "$@"? La familia tradicional Bourne Shell¿Qué ${1+"$@"}significa ... y dónde es necesario? y "$@"contra${1+"$@"} .


Las matrices también ayudan con la primera situación:

si alguna_condición
entonces
    ignorecase = (- i) # Se podría decir `ignorecase = (" - i ")`, pero es realmente innecesario.
más
    ignorecase = () # ¡No use comillas en este comando!
fi
grep "$ {ignorecase [@]}"   other_ grep _args

____________________

PS (csh)

Esto debería ser evidente, pero, en beneficio de las personas que son nuevas aquí: csh, tcsh, etc., no son shells Bourne / POSIX. Son una familia completamente diferente. Un caballo de un color diferente. Un juego completamente diferente. Una raza diferente de gato. Aves de otra pluma. Y, más particularmente, una lata diferente de gusanos.

Parte de lo que se ha dicho en esta página se aplica a csh; tales como: es una buena idea citar todas sus variables a menos que tenga una buena razón para no hacerlo, y esté seguro de saber lo que está haciendo. Pero, en csh, cada variable es una matriz: sucede que casi todas las variables son una matriz de un solo elemento y actúan de manera bastante similar a una variable de shell ordinaria en shells Bourne / POSIX. Y la sintaxis es terriblemente diferente (y quiero decir terriblemente ). Entonces no diremos nada más sobre los shells de la familia csh aquí.

G-Man
fuente
1
Cabe destacar que en csh, se prefiere utilizar $var:qde "$var"que este último no funciona para las variables que contienen caracteres de nueva línea (o si desea citar los elementos de la matriz de forma individual, en lugar de unirse a ellos con espacios en un solo argumento).
Stéphane Chazelas
En bash, bitrate="--bitrate 320"funciona con ${bitrate:+$bitrate}y bitrate=--bitrate 128no funcionará ${bitrate:+"$bitrate"}porque rompe el comando. ¿Es seguro usar ${variable:+$variable}con no "?
Freedo
@Freedo: Encuentro tu comentario poco claro. No tengo muy claro qué es lo que quieres saber que aún no he dicho. Vea el segundo encabezado “Pero qué pasa si”: “¿Pero qué pasa si tengo una variable que quiero / necesito dividir en palabras?” Esa es su situación, y debería seguir los consejos allí. Pero si no puede (por ejemplo, porque está usando un shell que no sea bash, ksh, zsh o yash, y lo está usando  $@para otra cosa), o simplemente se niega a usar una matriz por otras razones, me remito a "Como se discutió en la otra respuesta". ... (Continúa)
G-Man
(Continuación) ... Su sugerencia (usando ${variable:+$variable}sin ") fallará si IFS contiene  -,  0, 2, 3, a, b, e, i, ro  t.
G-Man
Bueno mi variable tiene dos A, E, B, I 2 y 3 caracteres y todavía funciona bien con${bitrate:+$bitrate}
Freedo
11

Era escéptico de la respuesta de Stéphane, sin embargo, es posible abusar de $#:

$ set `seq 101`

$ IFS=0

$ echo $#
1 1

o $ ?:

$ IFS=0

$ awk 'BEGIN {exit 101}'

$ echo $?
1 1

Estos son ejemplos artificiales, pero el potencial existe.

Steven Penny
fuente