bash sinon conditions multiples sans sous-shell?


23

Je veux combiner plusieurs conditions dans une instruction shell if et annuler la combinaison. J'ai le code de travail suivant pour une combinaison simple de conditions:

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

Cela fonctionne bien. Si je veux l'annuler, je peux utiliser le code de travail suivant:

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

Cela fonctionne également comme prévu. Cependant, il me vient à l'esprit que je ne sais pas ce que les parenthèses font réellement là-bas. Je veux les utiliser uniquement pour le regroupement, mais est-ce réellement un sous-shell? (Comment savoir?) Dans l'affirmative, existe-t-il un moyen de regrouper les conditions sans engendrer un sous-shell?


Je suppose que je pourrais également utiliser la logique booléenne pour déterminer l'expression équivalente: if ! [ -f file1 ] || ! [ -f file 2 ] || ! [ -f file3 ] ; thenmais je voudrais une réponse plus générale.
Wildcard du

Vous pouvez également annuler à l'intérieur des accolades, par exempleif [[ ! -f file1 ]] && [[ ! -f file2 ]]; then
DopeGhoti

1
Oui, je viens de tester if [ ! 1 -eq 2 ] && [ ! 2 -eq 3 ]; then echo yep; fiet ça marche. J'écris juste toujours des tests à double accolade par habitude. De plus, pour m'assurer que ce n'est pas le cas bash, j'ai encore testé if /bin/test ! 1 -eq 2 && /bin/test ! 2 -eq 3 ; then echo yep; fiet cela fonctionne aussi de cette façon.
DopeGhoti

1
@Wildcard - [ ! -e file/. ] && [ -r file ]supprimera les répertoires. niez-le comme vous le souhaitez. bien sûr, c'est ce qui -dfait.
mikeserv

1
Question connexe (similaire mais pas autant de discussion et les réponses ne sont pas si utiles): unix.stackexchange.com/q/156885/135943
Wildcard

Réponses:


32

Vous devez utiliser { list;}au lieu de (list):

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

Les deux sont des commandes de regroupement , mais { list;}exécutent des commandes dans l'environnement shell actuel.

Notez que, l' ;en { list;}est nécessaire pour délimiter la liste de }mots inverse, vous pouvez utiliser d' autres delimiter ainsi. L'espace (ou autre délimiteur) après {est également requis.


Je vous remercie! Quelque chose qui m'a fait trébucher au début (puisque j'utilise des accolades bouclés awkplus souvent qu'en bash) est le besoin d'espaces après l'accolade ouverte. Vous mentionnez "vous pouvez également utiliser un autre délimiteur"; des exemples?
Wildcard

@Wildcard: Oui, l'espace blanc après l'accolade ouverte est nécessaire. Un exemple pour un autre délimiteur est une nouvelle ligne, car vous voulez souvent définir une fonction.
cuonglm

@don_crissti: Dans zsh, vous pouvez utiliser des espaces blancs
cuonglm

@don_crissti: &c'est aussi un séparateur, vous pouvez l'utiliser { echo 1;:&}, plusieurs sauts de ligne peuvent également être utilisés.
cuonglm

2
Vous pouvez utiliser une citation métacaractère du manuel they must be separated from list by whitespace or another shell metacharacter.En bash ils sont: metacharacter: | & ; ( ) < > space tab . Pour cette tâche spécifique, je crois que tout & ; ) space tabfonctionnera. .... .... De zsh (comme commentaire) each sublist is terminated by &', &!', or a newline.

8

Vous pouvez utiliser entièrement la testfonctionnalité pour réaliser ce que vous voulez. À partir de la page de manuel 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.

Votre état pourrait donc ressembler à:

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

Pour annuler l'utilisation de parenthèses d'échappement:

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

6

Pour annuler de manière portative un conditionnel complexe en shell, vous devez soit appliquer la loi de De Morgan et pousser la négation jusqu'en bas dans les [appels ...

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

... ou vous devez utiliser then :; else...

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

if ! commandn'est pas disponible sur le marché et ni l'un ni l'autre [[.

Si vous n'avez pas besoin d'une portabilité totale, n'écrivez pas de script shell . Vous êtes en fait plus susceptible de trouver /usr/bin/perlsur un Unix sélectionné au hasard que vous bash.


1
!est POSIX qui est de nos jours suffisamment portable. Même les systèmes livrés avec le shell Bourne ont également un sh POSIX ailleurs sur le système de fichiers que vous pouvez utiliser pour interpréter votre syntaxe standard.
Stéphane Chazelas

@ StéphaneChazelas C'est techniquement vrai, mais d'après mon expérience, il est plus facile de réécrire un script en Perl que de gérer les tracas liés à la localisation et à l'activation de l'environnement shell compatible POSIX à partir de /bin/sh. Les scripts Autoconf font comme vous le suggérez, mais je pense que nous savons tous à quel point cette approbation est faible.
zwol

5

d'autres ont noté le regroupement des {commandes composées ;}, mais si vous effectuez des tests identiques sur un ensemble, vous pouvez utiliser un type différent:

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

... comme cela a été démontré ailleurs { :;}, l'imbrication des commandes composées ne pose aucun problème ...

Notez que les tests ci-dessus (généralement) pour les fichiers réguliers . Si vous cherchez seulement existants , lisibles les fichiers qui ne sont pas des répertoires:

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

Si vous ne vous souciez pas qu'il s'agisse de répertoires ou non:

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

... fonctionne pour tout fichier lisible et accessible , mais se bloquera probablement pour fifos sans écrivains.


2

Ce n'est pas une réponse complète à votre question principale, mais j'ai remarqué que vous mentionnez les tests composés (fichier lisible) dans un commentaire; par exemple,

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

Vous pouvez consolider cela un peu en définissant une fonction shell; par exemple,

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

Ajoutez la gestion des erreurs à (par exemple, [ $# = 1 ]) au goût. La première ifdéclaration ci-dessus peut maintenant être condensée en

if readable_file file1  &&  readable_file file2  &&  readable_file file3

et vous pouvez encore raccourcir cela en raccourcissant le nom de la fonction. De même, vous pouvez définir not_readable_file()(ou nrfpour faire court) et inclure la négation dans la fonction.


Bien, mais pourquoi ne pas simplement ajouter une boucle? readable_files() { [ $# -eq 0 ] && return 1 ; for file in "$@" ; do [ -f "$file" ] && [ -r "$file" ] || return 1 ; done ; } (Avertissement: j'ai testé ce code et il fonctionne mais ce n'était pas un trait. J'ai ajouté des points-virgules pour les poster ici mais je n'ai pas testé leur placement.)
Wildcard

1
Bon point. Je voudrais soulever la préoccupation mineure qu'il cache plus de la logique de contrôle (conjonction) dans la fonction; quelqu'un qui lit le script et voit if readable_files file1 file2 file3ne sait pas si la fonction faisait AND ou OR - bien que (a) je suppose que c'est assez intuitif, (b) si vous ne distribuez pas le script, c'est assez bien si vous le comprenez, et (c) puisque j'ai suggéré d'abréger le nom de la fonction en obscurité, je ne suis pas en mesure de parler de lisibilité. … (Suite)
G-Man dit «Réintègre Monica»

1
(Suite)… BTW, la fonction est très bien comme vous l'avez écrit, mais vous pouvez la remplacer for file in "$@" ; dopar for file do- elle est par défaut in "$@", et (un peu contre-intuitivement) ;non seulement inutile mais découragée.
G-Man dit «Réinstalle Monica»

0

Vous pouvez également annuler les tests d'accolade, donc pour réutiliser votre code d'origine:

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

2
Vous avez besoin ||au lieu de&&
cuonglm

Le test n'est pas « aucun des fichiers n'est là», le test est « aucun fichier n'est là». L'utilisation ||exécuterait le prédicat si aucun des fichiers n'est présent, pas si et seulement si tous les fichiers ne sont pas présents. NON (A ET B ET C) est identique à (NON A) ET (NON B) ET (NON C); voir la question d'origine.
DopeGhoti

@DopeGhoti, cuonglm a raison. Si ce n'est pas le cas (tous les fichiers sont là), donc lorsque vous le divisez de cette façon, vous avez besoin ||de &&. Voir mon premier commentaire ci-dessous ma question elle-même.
Wildcard

2
@DopeGhoti: not (a and b)est le même que (not a) or (not b). Voir en.wikipedia.org/wiki/De_Morgan%27s_laws
cuonglm

1
Ce doit être jeudi. Je n'ai jamais pu comprendre le jeudi. Corrigé, et je laisserai mon pied-à-bouche pour la postérité.
DopeGhoti

-1

Je veux les utiliser uniquement pour le regroupement, mais est-ce réellement un sous-shell? (Comment savoir?)

Oui, les commandes dans le (...)sont exécutées dans un sous-shell.

Pour tester si vous êtes dans un sous-shell, vous pouvez comparer le PID de votre shell actuel avec le PID du shell interactif.

echo $BASHPID; (echo $BASHPID); 

Exemple de sortie, démontrant que les parenthèses génèrent un sous-shell et les accolades ne font pas:

$ echo $BASHPID; (echo $BASHPID); { echo $BASHPID;}
50827
50843
50827
$ 

6
()ne spawn subshell..peut-être que vous vouliez dire {}....
heemayl

@heemayl, c'est l'autre partie de ma question - existe-t-il un moyen de voir ou de démontrer sur mon écran le fait qu'un sous-shell est généré? (Cela pourrait vraiment être une question distincte mais ce serait bien de savoir ici.)
Wildcard

1
@heemayl Vous avez raison! Je l'avais fait echo $$ && ( echo $$ )pour comparer les PID et ils étaient les mêmes, mais juste avant de soumettre le commentaire vous disant que vous aviez tort, j'ai essayé une autre chose. echo $BASHPID && ( echo $BASHPID )donne différents PID
David King

1
@heemayl echo $BASHPID && { echo $BASHPID; }donne le même PID que vous avez suggéré serait le cas. Merci pour l'éducation
David King

@DavidKing, merci pour les commandes de test à l'aide $BASHPID, c'est l'autre chose qui me manquait.
Wildcard
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.