bash si no múltiples condiciones sin subshell?

23

Quiero combinar varias condiciones en una instrucción if de shell y negar la combinación. Tengo el siguiente código de trabajo para una combinación simple de condiciones:

if [ -f file1 ] && [ -f file2 ] && [ -f file3 ] ; then
  # do stuff with the files
fi

Esto funciona bien Si quiero negarlo, puedo usar el siguiente código de trabajo:

if ! ( [ -f file1 ] && [ -f file2 ] && [ -f file3 ] ) ; then
  echo "Error: You done goofed."
  exit 1
fi
# do stuff with the files

Esto también funciona como se esperaba. Sin embargo, se me ocurre que no sé qué hacen los paréntesis en realidad allí. Yo quiero a utilizarlos sólo para la agrupación, pero ¿es realmente el desove un subnivel? (¿Cómo puedo saberlo?) Si es así, ¿hay alguna forma de agrupar las condiciones sin generar una subshell?

Comodín
fuente
Supongo que también podría usar la lógica booleana para determinar la expresión equivalente: if ! [ -f file1 ] || ! [ -f file 2 ] || ! [ -f file3 ] ; thenpero me gustaría una respuesta más general.
Comodín el
También puede negar dentro de las llaves, por ejemploif [[ ! -f file1 ]] && [[ ! -f file2 ]]; then
DopeGhoti
1
Sí, acabo de probar if [ ! 1 -eq 2 ] && [ ! 2 -eq 3 ]; then echo yep; fiy funciona. Siempre escribo pruebas con llaves dobles como una costumbre. Además, para garantizar que no lo sea bash, lo probé más if /bin/test ! 1 -eq 2 && /bin/test ! 2 -eq 3 ; then echo yep; fiy también funciona de esa manera.
DopeGhoti
1
@Wildcard: soltará [ ! -e file/. ] && [ -r file ]directorios. negarlo como quieras. por supuesto, eso es lo que -dhace.
mikeserv
1
Pregunta relacionada (similar pero no tanta discusión y respuestas no tan útiles): unix.stackexchange.com/q/156885/135943
Comodín el

Respuestas:

32

Debe usar en { list;}lugar de (list):

if ! { [ -f file1 ] && [ -f file2 ] && [ -f file3 ]; }; then
  : do something
fi

Ambos son Comandos de Agrupación , pero { list;}ejecutan comandos en el entorno actual del shell.

Tenga en cuenta que, el ;en { list;}que se necesita para delimitar la lista de }palabra inversa, se pueden utilizar otros delimitador también. {También se requiere el espacio (u otro delimitador) después .

Cuonglm
fuente
¡Gracias! Algo que me hizo tropezar al principio (ya que uso llaves entre awkmás veces bash) es la necesidad de espacios en blanco después de la llave abierta. Usted menciona "también puede usar otro delimitador"; algun ejemplo?
Comodín el
@Wildcard: Sí, se necesita el espacio en blanco después de la llave abierta. Un ejemplo para otro delimitador es una nueva línea, ya que a menudo desea definir una función.
Cuonglm
@don_crissti: En zsh, puedes usar espacios en blanco
cuonglm
@don_crissti: &también es un separador, puede usar { echo 1;:&}, también se puede usar varias líneas nuevas .
Cuonglm
2
Se puede utilizar cualquier cita metacarácter del Manual they must be separated from list by whitespace or another shell metacharacter.En bash son: metacharacter: | & ; ( ) < > space tab . Para esta tarea específica, creo que cualquiera de & ; ) space tabellos funcionará. .... .... De zsh (como comentario) each sublist is terminated by &', &!', or a newline.
8

Puede utilizar completamente la testfuncionalidad para lograr lo que desea. Desde la página del manual de test:

 ! expression  True if expression is false.
 expression1 -a expression2
               True if both expression1 and expression2 are true.
 expression1 -o expression2
               True if either expression1 or expression2 are true.
 (expression)  True if expression is true.

Entonces su condición podría verse así:

if [ -f file1 -a -f file2 -a -f file3 ] ; then
    # do stuff with the files
fi

Para negar el uso de paréntesis escapados:

if [ ! \( -f file1 -a -f file2 -a -f file3 \) ] ; then
    echo "Error: You done goofed."
    exit 1
fi
# do stuff with the files
Dios de madera
fuente
6

A negar de forma portátil un condicional complejo en shell, debe aplicar la ley de De Morgan y empujar la negación hasta el fondo dentro de las [llamadas ...

if [ ! -f file1 ] || [ ! -f file2 ] || [ ! -f file3 ]
then
    # do stuff
fi

... o debes usar then :; else...

if [ -f file1 ] && [ -f file2 ] && [ -f file3 ]
then :
else
  # do stuff
fi

if ! commandno está disponible de forma portátil y tampoco lo está [[.

Si no necesita portabilidad total, no escriba un script de shell . En realidad, es más probable que encuentres /usr/bin/perlen un Unix seleccionado al azar de lo que eresbash .

zwol
fuente
1
!es POSIX que hoy en día es lo suficientemente portátil. Incluso los sistemas que todavía se envían con el shell Bourne también tienen un POSIX sh en otro lugar del sistema de archivos que puede usar para interpretar su sintaxis estándar.
Stéphane Chazelas
@ StéphaneChazelas Esto es técnicamente cierto, pero en mi experiencia es más fácil reescribir un script en Perl que lidiar con la molestia de localizar y activar el entorno de shell compatible con POSIX a partir de /bin/sh. Los scripts de Autoconf hacen lo que usted sugiere, pero creo que todos sabemos lo poco que es un respaldo.
zwol
5

otros han notado la agrupación de {comandos compuestos ;}, pero si está realizando pruebas idénticas en un conjunto, es posible que desee utilizar un tipo diferente:

if  ! for f in file1 file2 file3
      do  [ -f "$f" ] || ! break
      done
then  : do stuff
fi

... como se demuestra en otra parte { :;}, no hay dificultad involucrada con los comandos compuestos de anidación ...

Tenga en cuenta que lo anterior (normalmente) prueba los archivos normales . Si solo está buscando archivos existentes y legibles que no sean directorios:

if  ! for f in file1 file2 file3
      do  [ ! -d "$f" ] && 
          [   -r "$f" ] || ! break
      done
then  : do stuff
fi

Si no te importa si son directorios o no:

if  ! command <file1 <file2 <file3
then  : do stuff
fi

... funciona para cualquier archivo legible y accesible , pero es probable que se bloquee por quince sin escritores.

mikeserv
fuente
2

Esta no es una respuesta completa a su pregunta principal, pero noté que menciona las pruebas compuestas (archivo legible) en un comentario; p.ej,

if [ -f file1 ] && [ -r file1 ] && [ -f file2 ] && [ -r file2 ] && [ -f file3 ] && [ -r file3 ]

Puede consolidar esto un poco definiendo una función de shell; p.ej,

readable_file()
{
    [ -f "$1" ]  &&  [ -r "$1" ]
}

Agregue manejo de errores a (p. Ej. [ $# = 1 ]) Al gusto. La primera ifdeclaración, arriba, ahora se puede condensar a

if readable_file file1  &&  readable_file file2  &&  readable_file file3

y puede acortar esto aún más acortando el nombre de la función. Del mismo modo, podría definir not_readable_file()(o nrfpara abreviar) e incluir la negación en la función.

G-Man dice 'restablecer a Monica'
fuente
Bien, pero ¿por qué no solo agregar un bucle? readable_files() { [ $# -eq 0 ] && return 1 ; for file in "$@" ; do [ -f "$file" ] && [ -r "$file" ] || return 1 ; done ; } (Descargo de responsabilidad: probé este código y funciona, pero no fue de una sola línea. Agregué puntos y comas para publicar aquí pero no probé su ubicación.)
Comodín el
1
Buen punto. Plantearía la menor preocupación de que oculta más de la lógica de control (conjunción) en la función; alguien que lea el script y vea if readable_files file1 file2 file3que no sabría si la función estaba haciendo AND u OR , aunque (a) supongo que es bastante intuitivo, (b) si no está distribuyendo el script, es lo suficientemente bueno si lo comprende, y (c) como sugerí abreviar el nombre de la función en la oscuridad, no estoy en posición de hablar sobre legibilidad. … (Continúa)
G-Man dice 'Reincorporar a Monica' el
1
(Continúa) ... Por cierto, la función está bien de la forma en que la escribiste, pero puedes reemplazarla for file in "$@" ; docon for file do- por defecto in "$@", y (algo contra-intuitivamente) ;no solo es innecesaria, sino que realmente se desaconseja.
G-Man dice 'Restablecer a Monica' el
0

También puede negar dentro de las pruebas de llaves, para reutilizar su código original:

if [[ ! -f file1 ]] || [[ ! -f file2 ]] || [[ ! -f file3 ]] ; then
  # do stuff with the files
fi
DopeGhoti
fuente
2
Necesitas en ||lugar de&&
cuonglm
La prueba no es "si alguno de los archivos no está allí", la prueba es " no hay ninguno de los archivos allí". El uso ||ejecutaría el predicado si alguno de los archivos no está presente, no si y solo si todos los archivos no están presentes. NO (A Y B Y C) es lo mismo que (NO A) Y (NO B) Y (NO C); Ver la pregunta original.
DopeGhoti
@DopeGhoti, cuonglm tiene razón. Si no es así (todos los archivos están allí), cuando lo divide de esta manera, necesita en ||lugar de hacerlo &&. Vea mi primer comentario debajo de mi pregunta.
Comodín el
2
@DopeGhoti: not (a and b)es lo mismo que (not a) or (not b). Ver en.wikipedia.org/wiki/De_Morgan%27s_laws
cuonglm
1
Debe ser jueves. Nunca pude acostumbrarme a los jueves. Corregido, y dejaré mi pie en la boca para la posteridad.
DopeGhoti
-1

Quiero usarlos solo para agrupar, pero ¿realmente está generando una subshell? (¿Cómo puedo decir?)

Sí, los comandos dentro de (...)se ejecutan en una subshell.

Para probar si está en un subshell, puede comparar el PID de su shell actual con el PID del shell interactivo.

echo $BASHPID; (echo $BASHPID); 

Ejemplo de salida, que demuestra que los paréntesis generan una subshell y las llaves no:

$ echo $BASHPID; (echo $BASHPID); { echo $BASHPID;}
50827
50843
50827
$ 
David King
fuente
66
()hace subshell..perhaps de regeneración que quería decir {}....
heemayl
@heemayl, esa es la otra parte de mi pregunta: ¿hay alguna forma en que pueda ver o demostrar en mi pantalla el hecho de que se genera una subshell? (Esa podría ser una pregunta por separado, pero sería bueno saberlo aquí.)
Comodín el
1
@heemayl Tienes razón! Había hecho echo $$ && ( echo $$ )una comparación de los PID y eran los mismos, pero justo antes de enviar el comentario diciéndole que estaba equivocado, intenté otra cosa. echo $BASHPID && ( echo $BASHPID )da diferentes PID's
David King
1
@heemayl echo $BASHPID && { echo $BASHPID; }da el mismo PID que sugeriste que sería el caso. Gracias por la educación
David King
@DavidKing, gracias por usar los comandos de prueba $BASHPID, esa es la otra cosa que me faltaba.
Comodín el