Comment puis-je renforcer les scripts bash pour ne pas nuire lors de modifications ultérieures?

46

J'ai donc supprimé mon dossier personnel (ou plus précisément tous les fichiers auxquels j'avais accès en écriture). Qu'est-ce qui s'est passé, c'est que j'avais

build="build"
...
rm -rf "${build}/"*
...
<do other things with $build>

dans un script bash et, après ne plus en avoir besoin $build, supprimer la déclaration et tous ses usages - sauf le rm. Bash s'étend joyeusement à rm -rf /*. Oui.

Je me suis senti stupide, installé la sauvegarde, refait le travail que j'ai perdu. Essayer de dépasser la honte.

Maintenant, je me demande: quelles sont les techniques pour écrire des scripts bash afin que de telles erreurs ne puissent pas se produire, ou du moins soient moins probables? Par exemple, avais-je écrit

FileUtils.rm_rf("#{build}/*")

dans un script en ruby, l'interprète se serait plaint de buildne pas avoir été déclaré, donc le langage me protège.

Ce que j’ai envisagé lors de bash, outre le corraling rm(qui, comme le soulignent de nombreuses réponses à des questions connexes, n’est pas sans poser de problèmes):

  1. rm -rf "./${build}/"*
    Cela aurait tué mon travail actuel (un dépôt Git) mais rien d’autre.
  2. Une variante / paramétrage de rmcette opération nécessite une interaction lorsque vous agissez en dehors du répertoire en cours. (Impossible d'en trouver.) Effet similaire.

Est-ce le cas ou existe-t-il d'autres moyens d'écrire des scripts bash "robustes" en ce sens?

Raphaël
la source
3
Il n'y a aucune raison de rm -rf "${build}/*ne pas savoir où vont les guillemets. rm -rf "${build} fera la même chose à cause de la f.
Monty Harder
1
La vérification statique avec shellcheck.net est un endroit très solide pour commencer. Une intégration de l'éditeur est disponible, vous pouvez donc recevoir un avertissement de vos outils dès que vous supprimez la définition de quelque chose qui est encore utilisé.
Charles Duffy
@CharlesDuffy Pour ajouter à ma honte, j'ai écrit ce script dans un IDE de type IDEA avec BashSupport installé, ce qui avertit dans ce cas. Alors oui, point valable, mais il me fallait vraiment une annulation ferme.
Raphaël
2
Je t'ai eu. Assurez-vous de noter les mises en garde dans BashFAQ # 112 . set -un'est pas aussi mal vu qu'il l' set -eest, mais il a toujours ses pièges.
Charles Duffy
2
Réponse de blague: il suffit de mettre #! /usr/bin/env rubyen haut de chaque script shell et oublier bash;)
Pod

Réponses:

75
set -u

ou

set -o nounset

Cela ferait en sorte que le shell actuel traite les développements de variables non définies comme une erreur:

$ unset build
$ set -u
$ rm -rf "$build"/*
bash: build: unbound variable

set -uet set -o nounsetsont des options de shell POSIX .

Une valeur vide ne déclencherait cependant pas d'erreur.

Pour cela, utilisez

$ rm -rf "${build:?Error, variable is empty or unset}"/*
bash: build: Error, variable is empty or unset

L'expansion de ${variable:?word}serait étendue à la valeur de variablesauf si elle est vide ou non définie. S'il est vide ou non défini, l'icône wordserait affichée en cas d'erreur standard et le shell traiterait l'extension comme une erreur (la commande ne serait pas exécutée et, si elle était exécutée dans un shell non interactif, elle se terminerait). Si vous laissez la :sortie, l'erreur ne serait déclenchée que pour une valeur non définie, tout comme sous set -u.

${variable:?word}est une extension de paramètre POSIX .

Ni l'un ni l'autre ne provoquerait la fermeture d'un shell interactif à moins que set -e(ou set -o errexit) ne soit également en vigueur. ${variable:?word}provoque la fermeture des scripts si la variable est vide ou non définie. set -uprovoquerait la fermeture d'un script s'il était utilisé avec set -e.


Quant à votre deuxième question. Il n'y a aucun moyen de limiter rmpour ne pas travailler en dehors du répertoire en cours.

L’implémentation GNU de rma une --one-file-systemoption qui l’empêche de supprimer de manière récursive les systèmes de fichiers montés, mais je pense que c’est aussi proche que nous pouvons obtenir sans insérer l’ rmappel dans une fonction qui vérifie réellement les arguments.


Comme note latérale: ${build}est exactement équivalent à $buildmoins que le développement ne se produise en tant que partie d’une chaîne où le caractère immédiatement suivant est un caractère valide dans un nom de variable, tel que dans "${build}x".

Kusalananda
la source
Très bien, merci! 1) Est-ce ${build:?msg}ou ${build?msg}? 2) Dans le contexte de quelque chose comme les scripts de compilation, je pense qu’utiliser un outil différent de rm pour une suppression plus sûre serait bien: nous savons que nous ne voulons pas travailler en dehors du répertoire actuel, nous le rendons donc explicite en utilisant un accès restreint. commander. Il n'est pas nécessaire de rendre les entreprises plus sûres en général.
Raphaël
1
@Raphael Désolé, devrait être avec:. Je vais corriger cette faute de frappe maintenant et je mentionnerai sa signification plus tard lorsque je serai de retour à mon ordinateur.
Kusalananda
2
@ Raphaël Ok, j'ai ajouté une courte phrase sur ce qui se passe sans le :(cela ne déclencherait l'erreur que pour les variables non définies ). Je n'ose pas essayer d'écrire un outil permettant de supprimer des fichiers par un chemin spécifique exclusivement, en toutes circonstances. Analyser des chemins et se soucier / ne pas se soucier de liens symboliques, etc. est un peu trop délicat pour moi pour le moment.
Kusalananda
1
Merci, l'explication aide! En ce qui concerne la "société locale": je réfléchissais surtout, peut-être à la pêche d'un "bien sûr, c'est <toolname>" - mais je n'essayais certainement pas de vous aider-vampire à l'écrire! OO Tout va bien, vous avez assez aidé! :)
Raphael
2
@MateuszKonieczny Je ne pense pas que je le ferai, désolé. Je crois que des mesures de sécurité comme celles-ci devraient être utilisées en cas de besoin . Comme pour tout ce qui rend un environnement sûr, cela finira par rendre les gens de plus en plus imprudents et dépendants des mesures de sécurité. Il est préférable de savoir ce que chaque mesure de sécurité fait et ensuite de les appliquer sélectivement selon les besoins.
Kusalananda
12

Je vais proposer des contrôles de validation normales en utilisant test/[ ]

Vous auriez été en sécurité si vous aviez écrit votre script en tant que tel:

build="build"
...
[ -n "${build}" ] || exit 1
rm -rf "${build}/"*
...

Les [ -n "${build}" ]vérifications qui "${build}"sont une chaîne de longueur non nulle .

Le ||est l'opérateur OU logique en bash. Une autre commande sera exécutée si la première a échoué.

De cette façon, avait ${build}été vide / indéfini / etc. le script aurait quitté (avec un code de retour de 1, qui est une erreur générique).

Cela vous aurait également protégé au cas où vous auriez supprimé toutes les utilisations, ${build}car ce [ -n "" ]sera toujours faux.


L'avantage d'utiliser test/ [ ]est qu'il y a beaucoup d'autres contrôles plus significatifs qu'il peut également utiliser.

Par exemple:

[ -f FILE ] True if FILE exists and is a regular file.
[ -d FILE ] True if FILE exists and is a directory.
[ -O FILE ] True if FILE exists and is owned by the effective user ID.
Centimane
la source
Juste, mais un peu difficile à manier. Est-ce que quelque chose me manque ou est-ce que cela correspond à peu près à ce que ${variable:?word}propose @Kusalananda?
Raphaël
@Raphael cela fonctionne de la même manière dans le cas de "la chaîne a-t-elle une valeur" mais test(c'est-à-dire [) a de nombreux autres contrôles pertinents, tels que -d(l'argument est un répertoire), -f(l'argument est un fichier), -O(le fichier appartient à l'utilisateur actuel) et ainsi de suite.
Centimane
1
@ Raphaël également, la validation devrait faire partie intégrante de tout code / script. Notez également que si vous supprimiez toutes les instances de génération, n’auriez-vous pas également supprimé ${build:?word}avec elle? Le ${variable:?word}format ne vous protège pas si vous supprimez toutes les occurrences de la variable.
Centimane
1
1) Je pense qu'il y a une différence entre s'assurer que les hypothèses sont bien tenues (les fichiers existent, etc.) et vérifier si les variables sont définies (travail imho d'un compilateur / interprète). Si je dois le faire à la main, il vaut mieux utiliser une syntaxe ultra élégante - ce qui ifn’est pas le cas pour tous. 2) "N'auriez-vous pas également supprimé $ {build:? Word} avec elle" - le scénario est que j'ai manqué une utilisation de la variable. L'utilisation ${v:?w}m'aurait protégé des dommages. Si j'avais supprimé tous les usages, même un simple accès n'aurait évidemment pas été préjudiciable.
Raphaël
Cela étant dit, votre réponse est une réponse juste et utile à la question générale du titulaire: il est important de s’assurer que les hypothèses sont valides pour les scripts qui restent. Le problème spécifique dans le corps de la question est, à mon humble avis, mieux traité par Kusalananda. Merci!
Raphaël
4

Dans votre cas particulier, j'ai déjà retravaillé le terme "suppression" pour déplacer des fichiers / répertoires (en supposant que / tmp se trouve sur la même partition que votre répertoire):

# mktemp -d is also a good, reliable choice
trashdir=/tmp/.trash-$USER/trash-`date`
mkdir -p "$trashdir"
...
mv "${build}"/* "$trashdir"
...

En coulisse, cela déplace les références de fichier / $trashdirrépertoire de niveau supérieur des structures de répertoires source vers la destination, sur la même partition, sans perdre de temps à parcourir la structure de répertoires et à libérer les blocs de disque par fichier immédiatement. Cela produit un nettoyage beaucoup plus rapide lorsque le système est en cours d'utilisation active, en contrepartie d'un redémarrage légèrement plus lent (/ tmp est nettoyé au redémarrage).

Sinon, une entrée cron pour nettoyer périodiquement /tmp/.trash-$USER empêchera / tmp de se remplir, pour les processus (par exemple, les constructions) qui utilisent beaucoup d’espace disque. Si votre répertoire se trouve sur une autre partition que / tmp, vous pouvez créer un répertoire similaire à / tmp sur votre partition et laisser cron le nettoyer.

Mais surtout, si vous bousillez les variables de quelque manière que ce soit, vous pouvez récupérer le contenu avant le nettoyage.

utilisateur117529
la source
2
"Cela permet un nettoyage beaucoup plus rapide lorsque le système est en cours d'utilisation" - le fait-il? J'aurais pensé que les deux concernent uniquement la modification des inodes affectés. Ce n'est certainement pas plus rapide si vous /tmpvous trouvez sur une autre partition (je pense que cela l'est toujours pour moi, du moins pour les scripts exécutés dans l'espace utilisateur); ensuite, la corbeille doit être changée (et ne profitera pas de la gestion du système d'exploitation /tmp).
Raphaël
1
Vous pouvez utiliser mktemp -dpour obtenir ce répertoire temporaire sans marcher sur les pieds d'un autre processus (et pour le respecter correctement $TMPDIR).
Toby Speight
Ajout de vos notes précises et utiles de vous deux, merci. Je pense que je l’avais initialement posté trop rapidement sans prendre en compte les points que vous avez évoqués.
user117529
2

Utilisez la substitution bash paramater pour attribuer des valeurs par défaut lorsque les variables ne sont pas initialisées, par exemple:

rm -rf $ {variable: - "/ nonexistent"}

rackandboneman
la source
1

J'essaie toujours de démarrer mes scripts Bash avec une ligne #!/bin/bash -ue.

-edes moyens « échouent sur la première uncathed e rror »;

-udes moyens « échouent sur la première utilisation de u ndeclared variable ».

Trouvez plus de détails dans un article de qualité Utilisez le mode strict non officiel de Bash (sauf si vous aimez le débogage) . Aussi l'auteur recommande d'utiliser, set -o pipefail; IFS=$'\n\t'mais pour mes fins, c'est exagéré.

niya3
la source
1

Le conseil général pour vérifier si votre variable est définie est un outil utile pour éviter ce genre de problème. Mais dans ce cas, il y avait une solution plus simple.

Il n’est probablement pas nécessaire de déplacer le contenu du $buildrépertoire pour les supprimer, mais pas le $buildrépertoire lui-même. Donc, si vous ignorez ce qui est superflu, *alors une valeur non définie se transformera en rm -rf /laquelle, par défaut, la plupart des implémentations de rm de la dernière décennie refuseront d’être exécutées (sauf si vous désactivez cette protection avec l’ --no-preserve-rootoption de GNU rm ).

Sauter la fin /également rm ''entraînerait le message d'erreur suivant:

rm: can't remove '': No such file or directory

Cela fonctionne même si votre commande rm n'implémente pas de protection pour /.

Eschwartz
la source
-2

Vous pensez en termes de langage de programmation, mais bash est un langage de script :-) Utilisez donc un paradigme d’ instruction totalement différent pour un paradigme de langage totalement différent .

Dans ce cas:

rmdir ${build}

Étant donné rmdirque refusera de supprimer un répertoire non vide, vous devez d'abord supprimer les membres. Vous savez ce que sont ces membres, non? Ce sont probablement des paramètres de votre script, ou dérivés d'un paramètre, donc:

rm -rf ${build}/${parameter}
rmdir ${build}

Maintenant, si vous mettez d’autres fichiers ou répertoires dedans comme un fichier temporaire ou quelque chose que vous n’auriez pas dû, le rmdirjettera une erreur. Manipulez-le correctement, puis:

rmdir ${build} || build_dir_not_empty "${build}"

Cette façon de penser m'a bien servi parce que ... oui, j'ai été là.

Riches
la source
6
Merci pour vos efforts, mais c'est complètement faux. L'hypothèse "vous savez ce que sont ces membres" est incorrecte; penser à la sortie du compilateur. make cleanutilisera des caractères génériques (sauf si un compilateur très diligent crée une liste de tous les fichiers qu'il crée). De plus, il me semble qu'avoir rm -rf ${build}/${parameter}seulement fait un peu baisser le problème.
Raphaël
Hm. Regardez comment ça se lit. "Merci, mais c'est pour quelque chose que je n'ai pas divulgué dans la question initiale, donc je vais non seulement rejeter votre réponse, mais aussi la réduire, même si cela s'applique davantage au cas général." Sensationnel.
Rich
"Permettez-moi de formuler des hypothèses supplémentaires allant au-delà de ce que vous avez écrit dans la question, puis rédigez une réponse spécialisée." * haussement d'épaules * (Pour votre information, cela ne vous regarde pas : je n'ai pas voté. J'aurais probablement dû, car la réponse ne m'a pas été utile (du moins pour moi).)
Raphael
Hm. Je n'ai fait aucune hypothèse et écrit une réponse générale . Il n'y a rien du tout makedans la question. Ecrire une réponse qui makeserait uniquement applicable serait une hypothèse très spécifique et surprenante. Ma réponse ne fonctionne que dans des cas autres que makele développement logiciel, autres que votre problème novice spécifique que vous avez rencontré en supprimant vos données.
Rich
1
Kusalananda m'a appris à pêcher. Vous êtes venus et avez dit: "Puisque vous vivez dans les plaines, pourquoi ne mangez-vous pas du bœuf à la place?"
Raphaël