web-dev-qa-db-fra.com

Comment puis-je renforcer les scripts bash contre les dommages lorsqu'ils seront modifiés à l'avenir?

J'ai donc supprimé mon dossier personnel (ou, plus précisément, tous les fichiers auxquels j'avais accès en écriture). 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 n'avoir plus besoin de $build, en supprimant la déclaration et toutes ses utilisations - mais le rm. Bash se développe avec bonheur à rm -rf /*. Oui.

Je me sentais stupide, j'ai 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 soient au moins moins probables? Par exemple, avais-je écrit

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

dans un script Ruby, l'interpréteur se serait plaint que build ne soit pas déclaré, donc la langue me protège.

Ce que j'ai considéré dans bash, en plus de corriger rm (qui, comme le mentionnent de nombreuses réponses dans des questions connexes, n'est pas sans problème):

  1. rm -rf "./${build}/"*
    Cela aurait tué mon travail actuel (un dépôt Git) mais rien d'autre.
  2. Variante/paramétrage de rm qui nécessite une interaction lors de l'action en dehors du répertoire courant. (Impossible d'en trouver.) Effet similaire.

Est-ce bien cela, ou existe-t-il d'autres façons d'écrire des scripts bash qui sont "robustes" dans ce sens?

46
Raphael
set -u

ou

set -o nounset

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

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

set -u et set -o nounset sont options du shell POSIX .

Une valeur vide ne provoquerait pas une 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} s'étendrait à la valeur de variable sauf s'il est vide ou non défini. S'il est vide ou non défini, le Word s'afficherait sur l'erreur standard et le shell traiterait l'expansion comme une erreur (la commande ne serait pas exécutée, et si elle s'exécutait dans un shell non interactif, cela se terminerait ). Quittant le : out déclencherait l'erreur uniquement pour une valeur non définie, comme sous set -u.

${variable:?word} est un expansion des paramètres POSIX .

Aucun de ces éléments n'entraînerait la fin d'un shell interactif à moins que set -e (ou set -o errexit) était également en vigueur. ${variable:?word} provoque la fermeture des scripts si la variable est vide ou non définie. set -u entraînerait 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 rm pour qu'il ne fonctionne pas en dehors du répertoire courant.

L'implémentation GNU de rm a un --one-file-system option qui l'empêche de supprimer récursivement les systèmes de fichiers montés, mais c'est aussi proche que je pense que nous pouvons obtenir sans encapsuler l'appel rm dans une fonction qui vérifie réellement les arguments.


En note: ${build} est exactement équivalent à $build sauf si l'expansion se produit dans le cadre d'une chaîne où le caractère immédiatement suivant est un caractère valide dans un nom de variable, comme dans "${build}x".

75
Kusalananda

Je vais suggérer des vérifications de validation normales en utilisant test/[ ]

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

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

Le [ -n "${build}" ] vérifie que "${build}" est une chaîne de longueur non nulle .

Le || est l'opérateur logique OR en bash. Il provoque l'exécution d'une autre commande si la première échoue.

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

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


L'avantage d'utiliser test/[ ] est qu'il existe de nombreuses autres vérifications plus significatives 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.
12
Centimane

Dans votre cas spécifique, j'ai retravaillé la "suppression" dans le passé pour déplacer les fichiers/répertoires à la place (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 arrière-plan, cela déplace les références de fichier/dir de niveau supérieur de la source vers le $trashdir les structures de répertoires de destination se trouvent toutes sur la même partition, et ne passent pas de temps à parcourir la structure de répertoires et à libérer les blocs de disques par fichier sur-le-champ. Cela produit un nettoyage beaucoup plus rapide lorsque le système est en cours d'utilisation, en échange d'un redémarrage légèrement plus lent (/ tmp est nettoyé lors des redémarrages).

Alternativement, une entrée cron pour nettoyer périodiquement /tmp/.trash-$USER empêchera/tmp de se remplir, pour les processus (par exemple, les builds) qui consomment beaucoup d'espace disque. Si votre répertoire se trouve sur une partition différente en tant que/tmp, vous pouvez créer un répertoire similaire à/tmp sur votre partition et demander à cron de le nettoyer à la place.

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

4
user117529

Utilisez la substitution de paramètre bash pour attribuer des valeurs par défaut lorsque la variable n'est pas initialisée, par exemple:

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

2
rackandboneman

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

-e signifie "échec au premier non traité e rror";

-u signifie "échec lors de la première utilisation de u variable non déclarée".

Trouvez plus de détails dans un excellent article tilisez le mode strict non officiel Bash (à moins que vous n'aimiez le débogage) . L'auteur recommande également d'utiliser set -o pipefail; IFS=$'\n\t' mais pour mes besoins, c'est exagéré.

1
niya3

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 globaliser le contenu du $build répertoire afin de les supprimer mais pas le _ $build répertoire lui-même. Donc, si vous avez ignoré le * alors une valeur non définie deviendrait rm -rf / qui, par défaut, la plupart des implémentations rm de la dernière décennie refuseront de fonctionner (sauf si vous désactivez cette protection avec GNU rm's --no-preserve-root option).

Ignorer la fin / entraînerait également rm '' ce qui entraînerait le message d'erreur:

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

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

1
eschwartz