Conséquences pour la sécurité de l'oubli de citer une variable dans les shells POSH / bash

206

Si vous suivez unix.stackexchange.com depuis un moment, vous devriez savoir maintenant que laisser une variable non citée dans le contexte de liste (comme dans echo $var) dans les shells Bourne / POSIX (zsh étant l'exception) a une signification toute particulière et ne devrait pas être fait à moins d’avoir une très bonne raison de le faire.

Il est discuté longuement dans un certain nombre de questions-réponses ici (Exemples: ? Pourquoi mon starter script shell sur les espaces ou autres caractères spéciaux , Quand des guillemets doubles nécessaire? , Extension d'une variable shell et l' effet de glob et divisé sur elle , Quoted vs extension de chaîne sans guillemets)

C’est le cas depuis la publication initiale du shell Bourne à la fin des années 70 et le shell Korn n’a pas été modifié par celui-ci (l’un des plus grands regrets de David Korn (question n ° 7) ) ou bashqui a essentiellement copié le shell Korn, comment cela a été spécifié par POSIX / Unix.

Maintenant, nous voyons encore un certain nombre de réponses ici et même parfois de code shell publié publiquement où les variables ne sont pas citées. Vous auriez pensé que les gens auraient appris maintenant.

D'après mon expérience, il y a principalement 3 types de personnes qui omettent de citer leurs variables:

  • débutants. Celles-ci peuvent être excusées car il s'agit d'une syntaxe totalement non intuitive. Et c'est notre rôle sur ce site de les éduquer.

  • les gens oublieux.

  • des gens qui ne sont pas convaincus, même après des coups répétés, qui pensent que l'auteur du shell Bourne ne voulait sûrement pas citer toutes nos variables .

Peut-être pouvons-nous les convaincre si nous exposons le risque associé à ce type de comportement.

Quelle est la pire chose qui puisse éventuellement arriver si vous oubliez de citer vos variables. Est-ce vraiment si mauvais?

De quel type de vulnérabilité parle-t-on ici?

Dans quels contextes cela peut-il poser problème?

Stéphane Chazelas
la source
8
Je pense que vous aimerez BashPitfalls .
pawel7318
backlink de cet article, j'ai écrit , merci pour la rédaction
mirabilos
5
Je voudrais suggérer l' ajout d' un quatrième groupe: les gens qui ont été frappés sur la tête pour excès citant trop de fois, peut - être par les membres du troisième groupe prenant leur frustration sur les autres (la victime devient l'agresseur). Ce qui est bien sûr triste, c’est que ceux du quatrième groupe finissent parfois par ne pas citer les choses au moment le plus important.
Ack

Réponses:

201

Préambule

Premièrement, je dirais que ce n'est pas la bonne façon de résoudre le problème. C'est un peu comme dire " tu ne devrais pas assassiner des gens, sinon tu iras en prison ".

De même, vous ne citez pas votre variable car sinon, vous introduisez des vulnérabilités de sécurité. Vous citez vos variables car il est faux de ne pas le faire (mais si la peur de la prison peut aider, pourquoi pas).

Un petit résumé pour ceux qui viennent de sauter dans le train.

Dans la plupart des shells, laisser un développement variable sans guillemets (bien que cela (et le reste de cette réponse) s'applique également à la substitution de commande ( `...`ou $(...)) et au développement arithmétique ( $((...))ou $[...])) a une signification toute particulière. La meilleure façon de le décrire est que cela revient à invoquer une sorte d’ opérateur implicite scindé + glob¹ .

cmd $var

dans une autre langue serait écrit quelque chose comme:

cmd(glob(split($var)))

$varest d'abord divisé en une liste de mots selon des règles complexes impliquant le $IFSparamètre spécial (la partie divisée ), puis chaque mot résultant de cette division est considéré comme un motif qui est développé en une liste de fichiers qui lui correspond (la partie globale ) .

Par exemple, si $varcontient *.txt,/var/*.xmlet $IFS contient ,, cmdserait appelé avec un certain nombre d'arguments, le premier cmdet les suivants étant les txt fichiers du répertoire en cours et les xmlfichiers du répertoire /var.

Si vous vouliez appeler cmdavec seulement deux arguments littéraux cmd et *.txt,/var/*.xmlécrire, vous écririez:

cmd "$var"

qui serait dans votre autre langue plus familière:

cmd($var)

Qu'entendons-nous par vulnérabilité dans un shell ?

Après tout, on sait depuis la nuit des temps que les scripts shell ne doivent pas être utilisés dans des contextes sensibles à la sécurité. OK, laisser une variable sans guillemets est un bogue, mais cela ne fait pas beaucoup de mal, n'est-ce pas?

Bien que quiconque vous dise que les scripts shell ne doivent jamais être utilisés pour les CGI Web, ou que, heureusement, la plupart des systèmes n’autorisent plus les scripts shell setuid / setgid de nos jours, le shellshock (le bogue bash exploitable à distance titre en septembre 2014) a révélé que les shells sont encore largement utilisés là où ils ne devraient probablement pas: dans les CGI, dans les scripts de hook client DHCP, dans les commandes sudoers, invoqués par (sinon comme ) les commandes setuid ...

Parfois sans le savoir. Par exemple, system('cmd $PATH_INFO') dans un script php/ perl/ pythonCGI, un shell est interprété pour interpréter cette ligne de commande (sans parler du fait qu’il cmdpeut s’agir d’un script shell et que son auteur ne s’est jamais attendu à ce qu’il soit appelé depuis un CGI).

Vous avez une vulnérabilité quand il y a un chemin pour l'ascension des privilèges, c'est-à-dire quand quelqu'un (appelons-le l'attaquant ) est capable de faire quelque chose qu'il n'est pas censé faire.

Invariablement, cela signifie que l'attaquant fournit des données, ces données étant traitées par un utilisateur / processus privilégié qui fait par inadvertance quelque chose qu'il ne devrait pas faire, dans la plupart des cas à cause d'un bogue.

En gros, vous rencontrez un problème lorsque votre code de bogue traite des données sous le contrôle de l'attaquant .

À présent, la provenance de ces données n’est pas toujours évidente , et il est souvent difficile de dire si votre code parviendra à traiter des données non fiables.

En ce qui concerne les variables, dans le cas d'un script CGI, il est assez évident que les données sont les paramètres CGI GET / POST et des éléments tels que les cookies, le chemin d'accès, les paramètres de l'hôte ....

Pour un script setuid (exécuté en tant qu'utilisateur lorsqu'il est appelé par un autre), il s'agit des arguments ou des variables d'environnement.

Les noms de fichiers sont un autre vecteur très courant. Si vous obtenez une liste de fichiers à partir d’un répertoire, il est possible que l’attaquant y ait planté des fichiers .

À cet égard, même à l'invite d'un shell interactif, vous pourriez être vulnérable (lors du traitement de fichiers dans /tmpou ~/tmp par exemple).

Même un ~/.bashrcpeut être vulnérable (par exemple, bashl'interprète lorsqu'il est appelé sshpour exécuter un processus ForcedCommand similaire à celui d' gitun déploiement de serveur avec certaines variables sous le contrôle du client).

Désormais, un script ne peut pas être appelé directement pour traiter des données non fiables, mais il peut être appelé par une autre commande. Ou votre code incorrect peut être copié-collé dans des scripts (par vous 3 ans en arrière ou l'un de vos collègues). Les réponses dans les sites de questions-réponses sont particulièrement critiques , car vous ne saurez jamais où les copies de votre code pourraient se retrouver.

Aux affaires; c'est comment?

Laisser une variable (ou une substitution de commande) sans guillemets est de loin la source de vulnérabilités en matière de sécurité associée au code shell. En partie parce que ces bogues se traduisent souvent par des vulnérabilités, mais aussi parce qu'il est si courant de voir des variables non citées.

En fait, lorsque vous recherchez des vulnérabilités dans le code shell, la première chose à faire est de rechercher des variables non citées. Il est facile de repérer, souvent un bon candidat, généralement facile de retrouver des données contrôlées par des attaquants.

Il existe un nombre infini de façons dont une variable non citée peut se transformer en vulnérabilité. Je vais juste donner quelques tendances communes ici.

Divulgation d'information

La plupart des gens se heurteront à des bogues associés à des variables sans guillemets à cause de la partie scindée (par exemple, il est courant que les fichiers comportent des espaces dans leurs noms et que la valeur par défaut est IFS). Beaucoup de gens vont négliger la partie glob . La partie glob est au moins aussi dangereuse que la partie scindée .

Globbing sur une entrée externe non authentifiée signifie que l'attaquant peut vous faire lire le contenu de n'importe quel répertoire.

Dans:

echo You entered: $unsanitised_external_input

si $unsanitised_external_inputcontient /*, cela signifie que l'attaquant peut voir le contenu de /. Pas grand chose. Cela devient cependant plus intéressant, avec la /home/*liste des noms d’utilisateur de la machine /tmp/*, /home/*/.forwarddes astuces sur d’autres pratiques dangereuses, /etc/rc*/*des services activés ... Inutile de les nommer individuellement. Une valeur de /* /*/* /*/*/*...va simplement lister tout le système de fichiers.

Vulnérabilités de déni de service.

Prenant le cas précédent un peu trop loin et nous avons un DoS.

En fait, toute variable non citée dans le contexte de liste avec une entrée non authentifiée est au moins une vulnérabilité de type DoS.

Même les scripteurs experts oublient souvent de citer des choses telles que:

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

:est la commande no-op. Qu'est ce qui pourrait aller mal?

Cela est destiné à affecter $1à $QUERYSTRINGsi $QUERYSTRING était non défini. C'est un moyen rapide de rendre un script CGI appelable à partir de la ligne de commande.

Cela $QUERYSTRINGreste cependant étendu et comme il n’est pas cité, l’ opérateur split + glob est appelé.

Maintenant, il y a des globs qui sont particulièrement coûteux à développer. Celui- /*/*/*/*ci est déjà assez mauvais car cela signifie qu’il faut lister les répertoires jusqu’à 4 niveaux. En plus de l'activité du disque et du processeur, cela signifie stocker des dizaines de milliers de chemins de fichiers (40k ici sur une VM serveur minimale, dont 10k répertoires).

Désormais, cela /*/*/*/*/../../../../*/*/*/*signifie 40k x 10k et /*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*est suffisant pour amener même la machine la plus puissante à ses genoux.

Essayez-le vous-même (préparez-vous à ce que votre machine tombe en panne ou se bloque):

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

Bien sûr, si le code est:

echo $QUERYSTRING > /some/file

Ensuite, vous pouvez remplir le disque.

Il suffit de faire une recherche google sur shell cgi ou bash cgi ou ksh cgi et vous trouverez quelques pages qui vous montreront comment écrire des CGI dans des shells. Notez que la moitié de ceux qui traitent les paramètres sont vulnérables.

Même celui de David Korn est vulnérable (regardez la gestion des cookies).

jusqu'à vulnérabilités d'exécution de code arbitraire

L'exécution de code arbitraire est le pire type de vulnérabilité, car si l'attaquant peut exécuter n'importe quelle commande, il n'y a aucune limite à ce qu'il peut faire.

C'est généralement la partie divisée qui mène à ceux-ci. Cette division entraîne la transmission de plusieurs arguments aux commandes lorsqu'un seul est attendu. Tandis que le premier de ceux-ci sera utilisé dans le contexte attendu, les autres seront dans un contexte différent, donc potentiellement interprété différemment. Mieux avec un exemple:

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

Ici, l’intention était d’affecter le contenu de la $external_inputvariable shell à la foo awkvariable.

Maintenant:

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

Le second mot résultant du fractionnement de $external_input n'est pas affecté à, foomais considéré comme un awkcode (ici, il exécute une commande arbitraire:) uname.

Cela est particulièrement un problème pour les commandes qui peuvent exécuter d' autres commandes ( awk, env, sed(GNU un), perl, find...) en particulier avec les variantes GNU (qui acceptent les options après arguments). Parfois, vous ne soupçonneriez pas que des commandes soient capables d'exécuter des tâches telles que ksh, bashou zshest [ou printf...

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

Si nous créons un répertoire appelé x -o yes, alors le test devient positif, car nous évaluons une expression conditionnelle complètement différente.

Pire, si nous créons un fichier appelé x -a a[0$(uname>&2)] -gt 1, avec au moins toutes les implémentations de ksh (qui inclut les sh Unices les plus commerciaux et certains BSD), s’exécute uname parce que ces shells effectuent une évaluation arithmétique sur les opérateurs de comparaison numériques de la [commande.

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

Même chose bashpour un nom de fichier comme x -a -v a[0$(uname>&2)].

Bien sûr, s’ils ne peuvent pas exécuter une exécution arbitraire, l’attaquant peut s’attendre à des dommages moindres (ce qui peut aider à une exécution arbitraire). Toute commande pouvant écrire des fichiers ou modifier les autorisations, la propriété ou avoir un effet principal ou secondaire pourrait être exploitée.

Toutes sortes de choses peuvent être faites avec les noms de fichiers.

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

Et vous ..finissez par rendre l’ écriture (récursive avec GNU chmod).

Les scripts effectuant un traitement automatique des fichiers dans des zones accessibles en écriture publique /tmpdoivent être écrits avec beaucoup de soin.

Qu'en est-il de [ $# -gt 1 ]

C'est quelque chose que je trouve exaspérant. Certaines personnes ont la peine de se demander si une expansion donnée peut être problématique pour décider si elles peuvent ou non omettre les guillemets.

C'est comme dire. Hé, il semble que $#ne peut pas être soumis à l'opérateur split + glob, demandons au shell de le scinder + glob . Ou alors , écrivons un code incorrect simplement parce qu'il est peu probable que le bogue soit touché .

Maintenant, à quel point est-ce improbable? OK, $#(ou $!, $?ou toute substitution arithmétique) ne peut contenir que des chiffres (ou -pour certains), de sorte que la partie glob est sortie. Pour que la partie divisée fasse quelque chose, tout ce dont nous avons besoin est $IFSde contenir des chiffres (ou -).

Avec certains coquillages, $IFSpeut être hérité de l'environnement, mais si l'environnement n'est pas sûr, la partie est terminée de toute façon.

Maintenant, si vous écrivez une fonction comme:

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

Cela signifie que le comportement de votre fonction dépend du contexte dans lequel elle est appelée. Ou en d'autres termes, $IFS devient l'un des intrants. Strictement parlant, lorsque vous écrivez la documentation de l'API pour votre fonction, cela devrait ressembler à ceci:

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

Et le code appelant votre fonction doit s’assurer que $IFSne contient pas de chiffres. Tout cela parce que vous n'aviez pas envie de taper ces 2 caractères à double guillemet.

Maintenant, pour que ce [ $# -eq 2 ]bogue devienne une vulnérabilité, il vous faudrait en quelque sorte que la valeur de $IFSdevienne sous le contrôle de l'attaquant . En théorie, cela ne se produirait normalement que si l'attaquant réussissait à exploiter un autre bogue.

Ce n'est pas du jamais vu. Il arrive souvent que des personnes oublient de nettoyer leurs données avant de les utiliser dans une expression arithmétique. Nous avons déjà vu plus haut que cela peut permettre l'exécution de code arbitraire dans certains shells, mais permet à l'attaquant de donner à toute variable une valeur entière.

Par exemple:

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

Et avec une $1valeur with (IFS=-1234567890), cette évaluation arithmétique a pour effet secondaire les paramètres IFS et la [ commande suivante échoue, ce qui signifie que la vérification de trop d'arguments est ignorée.

Qu'en est-il lorsque l' opérateur split + glob n'est pas appelé?

Il existe un autre cas où des guillemets sont nécessaires autour des variables et autres développements: quand il est utilisé comme motif.

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

ne testez pas si $aet $bsont les mêmes (sauf avec zsh) mais si $acorrespond au modèle dans $b. Et vous devez citer $bsi vous voulez comparer en tant que chaînes (la même chose dans "${a#$b}"ou "${a%$b}"ou "${a##*$b*}"$bdevrait être citée si elle ne doit pas être considérée comme un motif).

Cela signifie que [[ $a = $b ]]peut retourner vrai dans les cas où $aest différent de $b(par exemple quand $aest anythinget $best *) ou peut retourner faux quand ils sont identiques (par exemple quand les deux $aet $bsont [a]).

Cela peut-il créer une vulnérabilité de sécurité? Oui, comme n'importe quel bug. Ici, l'attaquant peut modifier le flux de code logique de votre script et / ou briser les hypothèses émises par votre script. Par exemple, avec un code comme:

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

L'attaquant peut contourner le contrôle en passant '[a]' '[a]'.

Maintenant, si ni cette correspondance de modèle ni l' opérateur split + glob ne s'appliquent, quel est le danger de laisser une variable sans guillemets?

Je dois admettre que j'écris:

a=$b
case $a in...

Là, citer ne nuit pas, mais n'est pas strictement nécessaire.

Toutefois, l’absence de guillemets dans ces cas (par exemple, dans les réponses aux questions et réponses) a pour effet secondaire d’envoyer un message erroné aux débutants: il peut être correct de ne pas citer de variables .

Par exemple, ils peuvent commencer à penser que si a=$bOK est correct, alors le export a=$bserait aussi (ce qui n'est pas dans beaucoup de shells , mais dans les arguments de la exportcommande, donc dans le contexte d'une liste) ou env a=$b.

Qu'en est- il zsh?

zshn'a pas résolu la plupart de ces maladresses de conception. En zsh(au moins lorsqu'ils ne sont pas en mode d'émulation sh / ksh), si vous voulez diviser , ou englobement ou correspondance de motif , vous devez demander explicitement: $=vardiviser, et $~varà glob ou pour le contenu de la variable à traiter comme un motif.

Cependant, le fractionnement (mais pas la suppression) est toujours effectué implicitement lors de la substitution de commande non entre guillemets (comme dans echo $(cmd)).

En outre, un effet secondaire parfois indésirable de ne pas citer de variable est la suppression des éléments vides . Le zshcomportement est similaire à ce que vous pouvez obtenir dans d’autres coques en désactivant complètement l’effacement (avec set -f) et le dédoublement (avec IFS=''). Toujours dedans:

cmd $var

Il n'y aura pas de split + glob , mais si $varest vide, au lieu de recevoir un argument vide, il cmdne recevra aucun argument.

Cela peut causer des bugs (comme l'évidence [ -n $var ]). Cela peut éventuellement casser les attentes et les hypothèses d’un script et causer des vulnérabilités, mais je ne peux pas vous donner d’exemple pas trop farfelu pour le moment).

Qu'en est- il lorsque vous avez besoin de la scission + glob opérateur?

Oui, c’est généralement le cas lorsque vous souhaitez laisser votre variable non citée. Mais ensuite, vous devez vous assurer que vous réglez correctement vos opérateurs split et glob avant de l’utiliser. Si vous ne voulez que la partie divisée et non la partie globale (ce qui est le cas la plupart du temps), vous devez désactiver globbing ( set -o noglob/ set -f) et corriger $IFS. Sinon, vous causerez également des vulnérabilités (comme l'exemple CGI de David Korn mentionné ci-dessus).

Conclusion

En bref, laisser une variable (ou une substitution de commande ou une expansion arithmétique) sans mention de shells dans les shells peut être très dangereux, surtout dans les mauvais contextes, et il est très difficile de savoir quels sont ces mauvais contextes.

C'est l'une des raisons pour lesquelles c'est considéré comme une mauvaise pratique .

Merci d'avoir lu jusqu'ici. Si cela vous dépasse, ne vous inquiétez pas. On ne peut pas s'attendre à ce que tout le monde comprenne toutes les implications de l'écriture de son code de la façon dont il l'écrit. C'est pourquoi nous avons des recommandations de bonnes pratiques , afin qu'elles puissent être suivies sans nécessairement comprendre pourquoi.

(et si cela n’est pas encore évident, évitez d’écrire du code sensible à la sécurité dans des shells).

Et citez vos variables sur vos réponses sur ce site!


¹Dans ksh93et pdkshet dérivés, le développement d'accolades est également effectué, sauf si la suppression est désactivée (dans le cas de ksh93versions jusqu'à ksh93u +, même lorsque l' braceexpandoption est désactivée).

Stéphane Chazelas
la source
Notez qu'avec [[seulement l'ERS des comparaisons doit être cité:if [[ $1 = "$2" ]]; then
mirabilos
2
@mirabilos, oui, mais le LHS n'a pas besoin de ne pas être cité, il n'y a donc aucune raison impérieuse de ne pas le citer ici (si nous devons prendre la décision consciente de citer par défaut, car cela semble être la chose la plus sensée à faire. ) Notez également que ce [[ $* = "$var" ]]n'est pas la même chose que [[ "$*" = "$var" ]]si le premier caractère de $IFSn'est pas un espace bash(et aussi mkshsi $IFSest vide mais dans ce cas, je ne suis pas sûr de ce qui $*est égal à, devrais-je générer un bogue?)).
Stéphane Chazelas
1
Oui, vous pouvez citer par défaut ici. S'il vous plait, plus de bogues au sujet de la division des champs pour le moment, je dois tout d'abord réparer ceux que je connais (de vous et des autres) avant de pouvoir réévaluer cela.
mirabilos
2
@Barmar, en supposant que vous vouliez dire foo='bar; rm *', non, ce ne sera pas le cas, mais le contenu du répertoire actuel sera répertorié, ce qui peut être considéré comme une divulgation d'informations. print $fooin ksh93(où printest le remplacement echoqui résout certaines de ses lacunes) a une vulnérabilité d'injection de code bien que (par exemple avec foo='-f%.0d z[0$(uname>&2)]') (vous avez réellement besoin print -r -- "$foo". echo "$foo"est toujours faux et non réparable (bien que généralement moins dommageable)).
Stéphane Chazelas
3
Je ne suis pas un expert en bash, mais je le code depuis plus de dix ans. J'ai beaucoup utilisé les guillemets, mais principalement pour gérer les blancs incorporés. Maintenant, je les utiliserai beaucoup plus! Ce serait bien si quelqu'un développait cette réponse pour faciliter l'absorption de tous les détails. J'en ai eu beaucoup, mais j'en ai aussi manqué. C'est déjà un long post, mais je sais qu'il y a encore beaucoup à apprendre ici. Merci!
Joe
34

[Inspiré par cette réponse par cas .]

Mais si …?

Mais que se passe-t-il si mon script attribue à une variable une valeur connue avant de l’utiliser? En particulier, que se passe-t-il si une variable est définie sur une ou plusieurs valeurs possibles (mais elle est toujours définie sur une valeur connue) et qu'aucune de ces valeurs ne contient de caractère espace ou global? N'est-il pas prudent de l'utiliser sans guillemets dans ce cas ?

Et que se passe-t-il si l'une des valeurs possibles est la chaîne vide et que je compte sur le «retrait des vidages»? C'est-à-dire que si la variable contient la chaîne vide, je ne veux pas obtenir la chaîne vide dans ma commande; Je veux rien obtenir. Par exemple,

si certaines conditions
ensuite
    ignorecase = "- i"
autre
    ignorecase = ""
Fi
                                        # Notez que les guillemets dans les commandes ci-dessus ne sont pas strictement nécessaires. 
grep $ ignorecase   autre_ grep _args

Je ne peux pas dire ; cela échouera si est la chaîne vide.grep "$ignorecase" other_grep_args$ignorecase

Réponse:

Comme indiqué dans l'autre réponse, cela échouera encore si IFScontient un -ou un i. Si vous vous êtes assuré qu'il IFSne contenait aucun caractère dans votre variable (et que vous êtes certain que votre variable ne contenait aucun caractère glob), cela est probablement sans danger.

Mais il existe un moyen plus sûr (bien que ce soit un peu moche et peu intuitif): utiliser ${ignorecase:+"$ignorecase"}. A partir de la spécification POSIX Shell Command Language , sous  2.6.2 Extension des paramètres ,

${parameter:+[word]}

    Utilisez la valeur alternative.   Si parameternon défini ou nul, null doit être substitué; sinon, le développement de word (ou une chaîne vide si elle wordest omise) doit être remplacé.

Le truc ici, tel qu’il est, c’est que nous utilisons ignorecasecomme le parameter et "$ignorecase"comme le word. Donc ${ignorecase:+"$ignorecase"}moyen

Si $ignorecasenon défini ou nul (c'est-à-dire vide), null (c'est-à-dire rien entre guillemets ) doit être substitué; sinon, l'expansion de "$ignorecase"doit être substituée.

Cela nous mène là où nous voulons aller: si la variable est définie sur la chaîne vide, elle sera «supprimée» (cette expression complète et convoluée ne sera évaluée à rien - pas même une chaîne vide), et si la variable a une valeur non -une valeur, nous obtenons cette valeur, citée.


Mais si …?

Mais que se passe-t-il si j'ai une variable que je veux / dois séparer en mots? (Cela ressemble autrement au premier cas; mon script a défini la variable, et je suis sûr qu'il ne contient pas de caractères glob. Mais il peut contenir des espaces, et je veux le scinder en deux arguments séparés. frontières.
PS Je veux encore vide le retrait.)

Par exemple,

si certaines conditions
ensuite
    critères = "- type f"
autre
    critères = ""
Fi
si une autre condition
ensuite
    criteres = "$ criteres -mtime +42"
Fi
find "$ start_directory" $ criteres   other_ find _args

Réponse:

Vous pourriez penser que c'est un cas d'utilisation eval.  Non!   Résistez à la tentation de penser même à utiliser evalici.

Encore une fois, si vous vous êtes assuré qu'il IFSne contenait aucun caractère dans votre variable (à l'exception des espaces que vous souhaitez respecter), et que vous êtes sûr que votre variable ne contient aucun caractère glob, la réponse ci-dessus est probablement sûr.

Mais si vous utilisez bash (ou ksh, zsh ou yash), il existe un moyen plus sûr: utilisez un tableau:

si certaines conditions
ensuite
    critères = (- type f) # Vous pouvez dire `critères = (" - type "" f ")`, mais c'est vraiment inutile.
autre
    criteres = () # N'utilisez pas de guillemets sur cette commande!
Fi
si une autre condition
ensuite
    criteres + = (- mtime +42) # Remarque: pas `=`, mais ` + =`, pour ajouter (ajouter) à un tableau.
Fi
find "$ start_directory" "$ {critères [@]}"   other_ find _args

De bash (1) ,

Tout élément d'un tableau peut être référencé à l'aide de . … Si est ou  , le mot s'étend à tous les membres de . Ces indices ne diffèrent que lorsque le mot apparaît entre guillemets. Si le mot est cité en double,… développe chaque élément de en un mot séparé.${name[subscript]}subscript@*name${name[@]}name

Donc, "${criteria[@]}"développe (dans l'exemple ci-dessus) le zéro, deux ou quatre éléments du criteriatableau, chacun cité. En particulier, si aucune des conditions  s n'est vraie, le criteriatableau n'a pas de contenu (comme défini par l' criteria=()instruction) et est "${criteria[@]}"évalué à rien (pas même une chaîne vide gênante).


Cela devient particulièrement intéressant et compliqué lorsque vous utilisez plusieurs mots, dont certains sont des entrées dynamiques (utilisateur), que vous ne connaissez pas à l'avance, et peuvent contenir des espaces ou d'autres caractères spéciaux. Considérer:

printf "Entrez le nom du fichier à rechercher:"
lire fname
si ["$ fname"! = ""]
ensuite
    critères + = (- nom "$ fname")
Fi

Notez que $fnamecite chaque fois qu'il est utilisé. Cela fonctionne même si l'utilisateur entre quelque chose comme foo barou foo*"${criteria[@]}"évalue à -name "foo bar"ou -name "foo*". (N'oubliez pas que chaque élément du tableau est cité.)

Les tableaux ne fonctionnent pas dans tous les shells POSIX; les tableaux sont un ksh / bash / zsh / yash-ism. Sauf que… il y a un tableau que tous les shells supportent: la liste d'arguments, aka "$@". Si vous en avez terminé avec la liste d'arguments avec laquelle vous avez été appelé (par exemple, vous avez copié tous les «paramètres de position» (arguments) dans des variables, ou les avez autrement traités), vous pouvez utiliser la liste d'arguments comme un tableau:

si certaines conditions
ensuite
    set - -type f # Vous pourriez dire `set -" -type "" f "`, mais c'est vraiment inutile.
autre
    ensemble --
Fi
si une autre condition
ensuite
    set - "$ @" -mtime +42
Fi
# De même: set - "$ @" -name "$ fname"
find "$ start_directory" "$ @"   other_ find _args

La "$@"construction (qui, historiquement, est arrivée en premier) a la même sémantique que - elle développe chaque argument (c'est-à-dire chaque élément de la liste d'arguments) en un mot séparé, comme si vous l'aviez saisi ."${name[@]}""$1" "$2" "$3" …

Extrait de la spécification POSIX Shell Command Language , sous 2.5.2 Paramètres spéciaux ,

@

    Se développe jusqu'aux paramètres de position, en commençant par un, en produisant initialement un champ pour chaque paramètre de position défini. …, Les champs initiaux doivent être conservés en tant que champs séparés,…. S'il n'y a pas de paramètre de position, le développement de @doit générer des champs nuls, même si ceux-ci @sont entre guillemets doubles; …

Le texte intégral est quelque peu cryptique; le point clé est qu'il spécifie que "$@"doit générer des champs zéro lorsqu'il n'y a pas de paramètre de position. Note historique: lorsqu’il a "$@"été introduit pour la première fois dans le shell Bourne (prédécesseur de bash) en 1979, il contenait un bogue qui "$@"avait été remplacé par une seule chaîne vide en l’absence de paramètres de position; voir Que ${1+"$@"}signifie un script shell et en quoi diffère-t-il "$@"? Shell traditionnel Bourne familleQu'est-ce que ${1+"$@"}signifie ... et où est - il nécessaire? et "$@"contre${1+"$@"} .


Les tableaux aident également à la première situation:

si certaines conditions
ensuite
    ignorecase = (- i) # Vous pouvez dire `ignorecase = (" - i ")`, mais c'est vraiment inutile.
autre
    ignorecase = () # N'utilisez pas de guillemets sur cette commande!
Fi
grep "$ {ignoreCase [@]}"   other_ grep _args

____________________

PS (csh)

Cela devrait aller de soi, mais pour le bénéfice des nouveaux venus: csh, tcsh, etc., ne sont pas des shells Bourne / POSIX. Ils sont une famille complètement différente. Un cheval de couleur différente. Un tout autre jeu de balle. Une race de chat différente. Oiseaux d'une autre plume. Et, plus particulièrement, une autre boîte de Pandore.

Une partie de ce qui a été dit sur cette page s’applique à csh; tels que: c’est une bonne idée de citer toutes vos variables à moins que vous n’ayez une bonne raison de ne pas le faire, et vous êtes sûr de savoir ce que vous faites. Mais, dans csh, chaque variable est un tableau - il se trouve que presque chaque variable est un tableau composé d’un seul élément et qu’elle se comporte comme une variable shell ordinaire dans les shells Bourne / POSIX. Et la syntaxe est terriblement différente (et je dis terriblement ). Donc, nous n'en dirons pas plus sur les obus de la famille csh ici.

G-Man
la source
1
Notez que dans csh, vous préférez utiliser plutôt $var:qque "$var"ce dernier ne fonctionne pas pour les variables contenant des caractères de nouvelle ligne (ou si vous souhaitez citer les éléments du tableau individuellement, plutôt que de les joindre avec des espaces dans un seul argument).
Stéphane Chazelas
Dans bash, bitrate="--bitrate 320"fonctionne avec ${bitrate:+$bitrate}, et bitrate=--bitrate 128ne fonctionnera pas ${bitrate:+"$bitrate"}car il interrompt la commande. Est-ce sécuritaire d'utiliser ${variable:+$variable}sans "?
Freedo
@ Freedo: Je trouve votre commentaire peu clair. Je ne vois surtout pas ce que vous voulez savoir, mais je ne l'ai pas déjà dit. Voir le deuxième en-tête «Mais si…» - «Mais si j'ai une variable que je veux / dois diviser en mots?», Telle est votre situation et vous devriez suivre les conseils donnés. Mais si vous ne pouvez pas (par exemple, parce que vous utilisez un shell autre que bash, ksh, zsh ou yash et que vous utilisez  $@pour autre chose), ou si vous refusez simplement d'utiliser un tableau pour d'autres raisons, je vous renvoie vous à «comme indiqué dans l’autre réponse». … (Suite)
G-Man le
(Suite) ... Votre suggestion ( en utilisant ${variable:+$variable}sans ") échouera si IFS contient  -,  0, 2, 3, a, b, e, i, rou  t.
G-Man le
Eh bien, ma variable a les caractères a, e, b, i 2 et 3 et fonctionne toujours bien avec${bitrate:+$bitrate}
Freedo
11

J'étais sceptique quant à la réponse de Stéphane, cependant il est possible d'abuser $#:

$ set `seq 101`

$ IFS=0

$ echo $#
1 1

ou $ ?:

$ IFS=0

$ awk 'BEGIN {exit 101}'

$ echo $?
1 1

Ce sont des exemples artificiels, mais le potentiel existe.

Steven Penny
la source