`test`, `[`, et `[[` (2020)
(jmmv.dev)- Dans les systèmes de type Unix, il peut exister un exécutable
/bin/[dont le nom est un symbole d’un seul caractère, et une syntaxe qui ressemble à une expression conditionnelle shell repose en réalité sur l’exécution d’une commande et son code de sortie testévalue une expression et renvoie 0 si elle est vraie, 1 si elle est fausse ; lorsqu’il est invoqué comme[il vérifie aussi que le dernier argument est bien]- Comme beaucoup de shells fournissent aussi
testet[comme commandes intégrées, les messages d’erreur ou le comportement peuvent différer entre/bin/testexterne et l’implémentation intégrée du shell - L’extension Bash
[[n’est pas une commande externe mais une syntaxe intégrée ; elle applique donc des règles différentes de[— par exemple, comme dans l’exemple,long*n’est pas développé comme un glob et est comparé comme une chaîne littérale - Pour les scripts portables,
[est le bon choix ; si le script est réservé à Bash, mieux vaut utiliser[[de manière cohérente, à condition de bien connaître les différences de règles d’expansion entre les deux
Ce que sont vraiment /bin/[ et /bin/test
- Sur un système Unix, il peut exister un exécutable nommé
/bin/[- La commande d’exemple
ls /bin/?affiche/bin/[
- La commande d’exemple
/bin/[et/bin/testpeuvent désigner le même binaire- Dans l’exemple, les deux chemins correspondent à des fichiers ayant le même inode et la même taille
- Cela ne signifie pas pour autant qu’il s’agit forcément de liens physiques sur tous les systèmes
testest un programme shell qui évalue des expressions- comparaison de chaînes
- comparaison numérique
- vérification de conditions sur des fichiers
- Si l’évaluation est vraie, il renvoie le code de sortie 0, sinon 1
Pourquoi [ se comporte comme une commande
test a = bressemble peu à une condition, mais la même logique écrite sous la forme[ a = b ]paraît plus familière[donne l’impression d’une syntaxe spéciale, mais il s’agit en réalité d’un appel de commandeif [ a = b ]; then ... fiexécute la commande[puis vérifie son code de sortie- Lorsqu’il est invoqué comme
[, le programme vérifie que le dernier argument est bien le crochet fermant]
- La structure
ifn’interprète pas directement une condition ; elle décide du branchement en fonction du code de sortie de la commande reçuetest a = a; echo $?donne0test a = b; echo $?donne1[ a = a ]; echo $?donne0[ a = b ]; echo $?donne1
- Dans le même esprit,
trueetfalsepeuvent aussi être vus comme des binaires auxiliaires qui renvoient un code de sortie
Différences entre binaire externe et commande intégrée du shell
testet[sont si fréquents dans les scripts shell que la plupart des shells les implémentent aussi comme commandes intégrées- À entrée identique, le binaire externe et la commande intégrée du shell peuvent produire des sorties différentes
/bin/test a bproduittest: a: unexpected operatortest a bproduitdash: 2: test: a: unexpected operator
- Ce type de différence n’existe pas seulement pour
testet[; on peut aussi l’observer avec des commandes apparemment simples commeecho - Comme chaque shell peut avoir sa propre implémentation intégrée, le comportement d’un script peut varier selon le shell qui l’exécute
Les règles particulières de l’extension Bash [[
[[est une extension de Bash qui peut remplacer l’usage de[- La plus grande différence est que
[[est toujours une syntaxe intégrée- Contrairement à
[, qui peut être exécuté comme binaire externe,[[permet à Bash de modifier les règles du langage à l’intérieur de l’expression
- Contrairement à
- Dans l’exemple avec les globs,
[et[[ne se comportent pas de la même manière- Après
touch long-name,[ long* = long-name ] && echo matchaffichematch - Les arguments de la commande
[suivent les règles normales d’expansion du shell, donclong*est développé enlong-namedans le répertoire [[ long* = long-name ]] && echo matchn’affiche rien[[traitelong*comme une chaîne littérale et le compare tel quel àlong-name, donc la comparaison échoue
- Après
- Dans un script réservé à Bash,
[[permet aussi d’utiliser des fonctions comme la correspondance par expression régulière=~
Que choisir dans un script
- Pour les scripts shell portables, il est préférable d’utiliser
[ testpeut aussi être utilisé, mais ce n’est pas le choix le plus courant- Si le script est spécifique à Bash, mieux vaut utiliser
[[de manière cohérente - Le shell lui-même possède aussi des opérateurs d’expression comme
!,&&et||- Ces opérateurs fonctionnent à partir de l’état de sortie des commandes
grep ^hello$ ... && grep ^bye$ ...donne un code de sortie global de0si les deux commandes réussissent- Si le premier
grepéchoue, la commande après&&ne peut pas valider l’ensemble, et le code de sortie global devient1
- On peut donc combiner les expressions
test/[avec les opérateurs logiques du shell dans une même condition- Exemple :
[ a = b ] || grep -q ^hello$ /usr/share/dict/words
- Exemple :
- POSIX n’exige pas que
/bin/[et/bin/testsoient des liens physiques- Sur NetBSD, ils l’étaient
- macOS Catalina fournit des copies distinctes du même binaire
- Debian testing fournit deux binaires différents
- La spécification POSIX n’impose pas que ces deux fichiers soient liés
1 commentaires
Commentaires sur Hacker News
Je suis l’auteur du billet original. Merci de l’avoir partagé, et je suis ravi qu’il soit même monté en page d’accueil. Le titre devrait probablement inclure (2020), et il vaudrait mieux ne pas mettre de majuscule à "test", puisque cela désigne la commande elle-même
J’ai aussi écrit un billet connexe en 2021, qui traite même de l’opérateur
[[de bash, donc il pourrait être intéressant dans ce contexte : https://jmmv.dev/2021/08/useless-use-of-gnu.html[[n’est pas, à strictement parler, une commande interne, mais plutôt fondamentalement un élément de syntaxe. Il utilise probablement en interne une commande interne presque inaccessible, mais ce qui est amusant, c’est que]]est aussi un mot réservé alors qu’il ne peut pas apparaître à une position où il aurait du sensDans certains shells non bash, le mot-clé
functionest nécessaire pour déclarer certains types de fonctions. Le$(shell)demakepeut produire une différence de performance mesurable lorsqu’on construit beaucoup de cibles. Malgré cela, quand il ne fait rien, c’est une perte, donc en général il vaut mieux utiliserincludepour déclencher une régénération. Le fait que GNU ignore POSIX est tout à fait justifié, car POSIX n’est pas très utile pour résoudre la plupart des vrais problèmes.explicite est utile, et pouvoir ajouter des options à la fin de la commande qu’on vient de taper est vraiment pratique. Ça m’agace toujours quand une commande ne permet pas ce genre de chosePour les scripts, viser la compatibilité POSIX
shest généralement raisonnable. Au minimum, il faut savoir quand on utilise une syntaxe spécifique à Bash--ignore-caseouset -o pipefailréduit la portabilité des scripts. C’est vrai en soiMais il n’explique pas pourquoi les utilisateurs de Linux devraient autant se soucier de la portabilité. OpenBSD et FreeBSD existent bel et bien, mais leur base d’utilisateurs est trop faible pour que cela semble particulièrement préoccupant. On peut dire, par souci d’équité, qu’il faut aussi tenir compte de ces systèmes, mais où s’arrête ce critère ? Faut-il aussi considérer des systèmes obscurs comme
vxWorks? BusyBox et Alpine sont plus intéressants, mais les changements sont si importants qu’un portage séparé est de toute façon presque toujours nécessaire. Y a-t-il une autre raison convaincante de se soucier des écosystèmes non GNU ?if [ a = b ] || grep -q ^hello$ /usr/share/dict/words; then echo "test failed and grep succeeded"; fi, ce n’est pas tout simplement un usage du shell banal et quotidien ?make $(shell …) expansion, alors que cela devrait êtremake $(shell ...) expansionComme c’est bien écrit dans le corps du texte, ce sont trois points et non une ellipse typographique, donc
mldrnon plus n’est pas correct. Deux bugs sans rapport semblent probablement s’être combinés iciEn poussant le dernier point un cran plus loin, on peut même supprimer le bloc
iflui-même.if [ a = b ]; then echo "Oops!"; else echo "Expected; phew!"; fidevient[ a = b ] && echo "Oops!" || echo "Expected; phew!"Je ne sais pas à quelle fréquence il faut vraiment faire ça, mais c’est parfois utile pour envoyer conditionnellement une sortie de debug vers la sortie d’erreur standard, par exemple
[ "$debug" ] && echo "what's going on" >&2. Le fait que les blocsiftestent des commandes ordinaires permet aussi des choses commeif grep -q 'debug' /var/log/nginx/access.log; then echo "Debug request found!"; fi. Ce que je n’ai pas encore examiné, c’est s’il vaut mieux écrire[ $(expr 1 + 1) -eq 2 ] && [ $(expr 2 + 2) -eq 3 ], ou utiliser le ET logique intégré àtest:[ $(expr 1 + 1) -eq 2 -a $(expr 2 + 2) -eq 4 ]. Si la performance n’est pas un problème, les deux semblent défendables pour des raisons similaires[ a = b ] && echo "Oops!" || echo "Expected; phew!"comme règle générale. Bash interprétera probablement cette ligne comme([ a = b ] && echo "Oops!") || echo "Expected; phew!"Donc si la suite de commandes après
&&échoue, le code après||s’exécutera quoi qu’il arrive. Par exemple, si>/dev/full echo "strings match"échoue à cause d’une erreur d’écriture, alors"strings don't match"sera affiché alors que les chaînes sont égales. Ce n’est pas la même sémantique qu’un blocifset -e, comme il faudrait le faire, alorsif [ a = b ]; then echo "Oops!"; fise comportera comme prévu, mais[ a = b ] && echo "Oops!"provoquera une sortie sur erreur quand l’expressionan’est pas égale àb-a,-oet les opérateurs(,)sont marqués comme obsolescents. Voir la section "Application Usage" de https://pubs.opengroup.org/onlinepubs/9699919799/utilities/t... pour plus de détails-aou deux tests avec&&, si l’on peut utiliser l’évaluation arithmétique de bash, il n’est pas nécessaire d’invoquerexpr:[ $((1+1)) -eq 2 ]Depuis quelques années, j’ai arrêté d’utiliser
[.testrenforce l’idée qu’il ne s’agit pas de syntaxe, mais bien d’une commande comme une autre. Et consulterman testest bien plus agréable que fouiller dansman bashman test, mais aussi la pageman [Bash propose aussi
help testcomme antisèche rapide. La commande[est très ancienne : elle était déjà présente dans Version 7 Unix en 1979D’accord.
[et[[, spécifique à bash, créent en pratique beaucoup de confusion sur ce qui se passe réellement, au point qu’il était difficile d’avoir confiance en leur comportementCela dit, le fait que
[[soit garanti comme commande intégrée avait clairement un intérêt à l’époque où les performances des scripts shell comptaient vraiment, et ce n’était pas il y a si longtempstest. Je n’écris pas souvent des scripts bash, donc je me fais toujours piéger par les instructionsif, surtout par les règles sur les espaces. Une fois la raison comprise, ça devient tout à fait évident, ettestmontre aussi plus clairement qu’on ne fait que passer des argumentsLe plus gros piège de
[et detest, c’est le comportement avec un seul argument. Par exemple, on peut écrire[ -n $FOO ]pour vérifier qu’une variable n’est pas videMais si
FOOn’est pas définie, elle ne s’étend pas en chaîne vide mais en rien du tout, ce qui revient à[ -n ]. POSIX exige que, dans la forme à un seul argument de[, cet argument — ici"-n"— provoque un succès s’il n’est pas vide. Le test indique donc à tort que$FOOn’est pas vide. Il faut toujours mettre les variables entre guillemetstest, mais dans le shell lui-mêmeLe comportement mentionné est logique.
[ "$FOO" ]est en effet une forme qui vérifie toujours si le contenu n’est pas vide, quel qu’il soit, même si c’est"-n"$FOOcontient des espaces, il sera étendu en plusieurs arguments. Donc, mettez toujours les variables entre guillemets[ x"$FOO" != x"" ][ -n "${FOO?}" ]pour que le script s’arrête immédiatement si$FOOest nul ou non définichubot a écrit un document intéressant qui explore les aspects plus subtils de
test/[/[[. Les autres articles de ce blog expliquent aussi de façon assez intéressante les bizarreries du shell¹ https://www.oilshell.org/blog/2017/08/31.html
² https://www.oilshell.org/blog/2016/11/18.html
Je ne savais pas du tout que
[était un programme, et je trouve assez drôle qu’il vérifie si le dernier argument est bien un crochet fermantEn tout cas, ça explique pourquoi il faut des espaces des deux côtés des crochets
[[est spécifique à bash. Si vous savez que vous n’utiliserez que bash, vous pouvez l’utiliser. L’article couvre bien les détailshttps://zsh.sourceforge.io/Doc/Release/Conditional-Expressio...
[[C’est aussi présent dans zsh et ksh, et je suis presque certain que ça vient de ksh en 1988, voire avant
testet[sont spécifiés par POSIX et existent généralement comme vrais binaires, même s’ils peuvent être masqués par une commande intégrée du shellEn revanche,
[[n’est pas spécifié par POSIX et n’existe généralement que comme commande intégrée au shellSe limiter au plus petit dénominateur commun des shells me paraît être une préoccupation totalement archaïque
Je n’ai pas bien compris pourquoi la dernière instruction
ifétait censée être déroutante. Si c’est parce qu’en apprenant les scripts shell on suppose généralement que[fait partie du langage bash plutôt que d’être simplement un autre programme, alors je comprends. Sinon, j’aimerais bien qu’on m’explique ce qu’il y a de surprenant[est un binaire, je ne vois pas bien pourquoi ce serait déroutant. Ça ressemble à du bash tout ce qu’il y a de plus normalJ’ai des préférences très arrêtées sur le shell, qui collent mal avec celles du reste du monde
Pour moi, il ne faut jamais utiliser
[, seulementtest.[donne l’illusion que son mécanisme fait partie de la grammaire du langage, alors qu’en réalité ce n’est qu’un autre « programme ». Par « programme », j’inclus aussi les commandes intégrées et les fonctions.if/||/&®ardent l’état de sortie, et un programme ne peut pas voir l’état de sortie d’autre chose, sauf à consulter la variable magique$?, qui n’est plus qu’une chaîne après expansion.caseregarde des chaînes, mais ne fonctionne pas sur les états de sortie et ne définit pas non plus d’état de sortie dans le cadre du fonctionnement decase ... esac. C’est le « programme » qui définit l’état de sortie. De plus,[/testne devraient servir qu’à évaluer la structure du système de fichiers, comme danstest -f /dev/null. Pour l’évaluation de chaînes, il faudrait utilisercase. Évidemment, la plupart des scripts me démangent, et les scripts que j’écris paraissent bizarres aux autresQuand j’utilise le shell, je préfère mettre le programme après
ifsur une ligne séparée avant de passer àthen, afin de bien souligner qu’on regarde « l’état de sortie de la dernière commande avant then ». Par exemple, dans une structure commeif; ls /tmp/goober; test -d /tmp/goober; echo "I'll always execute the then clause because echo will always return a 0 return code"; then ... fi,ifne regarde finalement que l’état de sortie deecho, donc la clause then s’exécute toujourstestprouvent que je suis d’accord. C’est une route solitaire... j’accuse le guide de style shell de GoogleEn écrivant des scripts qui doivent fonctionner à la fois en
shet enbash, j’ai pris l’habitude de préférertest, mais je continue surtout parce que ça me paraît plus cohérent sémantiquement que de traiter le caractère[comme une commande. Le fait que]ne soit pas un binaire séparé mais un argument de[est aussi étrange. Je comprends la raison technique, mais ça donne l’impression d’un hacktest. Des dizaines !On m’a appris à n’utiliser
[[que lorsqu’on veut faire du filtrage par expression régulière. Par exemple :if [[ "${foo}" =~ ^bar$ ]]; then echo Yes; fiPour le reste, j’utilise simplement
"test"ou"[". J’en suis à 85 000 lignes de bash. Je ne dis pas que bash est formidable, mais il répond encore à mes besoins pour beaucoup de chosesexprpeut faire du matching avec des expressions régulières de base et peut aussi renvoyer des groupes capturés1 : https://pubs.opengroup.org/onlinepubs/9699919799/utilities/e...
[ "$foo" = bar ] && echo Yes.Pour faire du matching de sous-chaîne,
[et le glob*suffisent généralement. Par exemple :[ "$bar" = extra* ] && echo '$bar began with extra'. Le dialecte regex de Bash est primitif, donc ça vaut rarement la peine de se fatiguer à l’utiliser. Pour les choses complexes, mieux vaut utiliser d’autres outils commegrep,awkouperl. Si on s’obstine à tout faire en bash, y compris des tâches complexes qui demandent plus de réutilisabilité, de modularité et des types intégrés, le rendement diminue très vite.