Scripts Shell

Oct 29, 2021

Vue d’ensemble

Enseignement : 30 min
Exercices : 15 min
Questions

  • Comment puis-je enregistrer et réutiliser des commandes ?

Objectifs

  • Écrire un script shell qui exécute une commande ou une série de commandes pour un ensemble fixe de fichiers.

  • Exécuter un script shell à partir de la ligne de commande.

  • Écrire un script shell qui opère sur un ensemble de fichiers définis par l’utilisateur sur la ligne de commande.

  • Créer des pipelines qui incluent des scripts shell que vous, et d’autres, avez écrits.

Nous sommes enfin prêts à voir ce qui fait du shell un environnement de programmation si puissant.Nous allons prendre les commandes que nous répétons fréquemment et les enregistrer dans des fichiersafin de pouvoir réexécuter toutes ces opérations plus tard en tapant une seule commande.Pour des raisons historiques,un ensemble de commandes enregistrées dans un fichier est généralement appelé un script shell,mais ne vous y trompez pas:ce sont en fait de petits programmes.

Débutons en retournant à molecules/ et en créant un nouveau fichier, middle.sh qui deviendra notre script shell:

$ cd molecules$ nano middle.sh

La commande nano middle.sh ouvre le fichier middle.sh dans l’éditeur de texte ‘nano'(qui s’exécute dans le shell).Si le fichier n’existe pas, il sera créé.Nous pouvons utiliser l’éditeur de texte pour modifier directement le fichier – nous allons simplement insérer la ligne suivante:

head -n 15 octane.pdb | tail -n 5

C’est une variation du pipe que nous avons construit plus tôt:il sélectionne les lignes 11-15 du fichier octane.pdb.Rappelez-vous, nous ne l’exécutons pas encore comme une commande : nous mettons les commandes dans un fichier.

Puis nous enregistrons le fichier (Ctrl-O dans nano), et quittons l’éditeur de texte (Ctrl-X dans nano).Vérifiez que le répertoire molecules contient maintenant un fichier appelé middle.sh.

Une fois que nous avons sauvegardé le fichier, nous pouvons demander au shell d’exécuter les commandes qu’il contient.Notre shell s’appelle bash, nous exécutons donc la commande suivante :

$ bash middle.sh
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00

Surement, la sortie de notre script est exactement ce que nous obtiendrions si nous exécutions ce pipeline directement.

Texte contre quoi que ce soit

Nous appelons généralement des programmes comme Microsoft Word ou LibreOffice Writer des « éditeurs de texte », mais nous devons être un peu plus prudents quand il s’agit de programmation. Par défaut, Microsoft Word utilise des fichiers .docx pour stocker non seulement du texte, mais aussi des informations de formatage sur les polices, les titres et bientôt. Ces informations supplémentaires ne sont pas stockées sous forme de caractères et ne signifient rien pour des outils comme head : ils s’attendent à ce que les fichiers d’entrée ne contiennent rien d’autre que les lettres, les chiffres et la ponctuation d’un clavier d’ordinateur standard. Lorsque vous éditez des programmes, vous devez donc soit utiliser un éditeur de texte en clair, soit faire attention à enregistrer les fichiers en texte en clair.

Que faire si nous voulons sélectionner des lignes d’un fichier arbitraire ? Nous pourrions éditer middle.sh à chaque fois pour changer le nom du fichier, mais cela prendrait probablement plus de temps que de retaper la commande dans le shell et de l’exécuter avec un nouveau nom de fichier.Au lieu de cela, éditons middle.sh et rendons-le plus polyvalent:

$ nano middle.sh

Maintenant, dans « nano », remplacez le texte octane.pdb par la variable spéciale appelée :

head -n 15 "" | tail -n 5

Dans un script shell, signifie « le premier nom de fichier (ou autre argument) sur la ligne de commande ».Nous pouvons maintenant exécuter notre script comme ceci:

$ bash middle.sh octane.pdb
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00

ou sur un fichier différent comme ceci :

$ bash middle.sh pentane.pdb
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00

Doubles guillemets autour des arguments

Pour la même raison que nous mettons la variable de boucle à l’intérieur de doubles guillemets, au cas où le nom de fichier se trouve contenir des espaces, nous entourons de doubles guillemets.

Actuellement, nous devons éditer middle.sh chaque fois que nous voulons ajuster la plage de lignes qui est retournée. Corrigeons cela en configurant notre script pour qu’il utilise plutôt trois arguments de ligne de commande. Après le premier argument de ligne de commande (), chaque argument supplémentaire que nous fournissons sera accessible via les variables spéciales , , , qui font référence aux premier, deuxième et troisième arguments de ligne de commande, respectivement.

Sachant cela, nous pouvons utiliser des arguments supplémentaires pour définir la plage de lignes à passer à head et tail respectivement:

$ nano middle.sh
head -n "" "" | tail -n ""

Nous pouvons maintenant exécuter :

$ bash middle.sh pentane.pdb 15 5
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00

En modifiant les arguments de notre commande, nous pouvons changer le comportement de notre script :

$ bash middle.sh pentane.pdb 20 5
ATOM 14 H 1 -1.259 1.420 0.112 1.00 0.00ATOM 15 H 1 -2.608 -0.407 1.130 1.00 0.00ATOM 16 H 1 -2.540 -1.303 -0.404 1.00 0.00ATOM 17 H 1 -3.393 0.254 -0.321 1.00 0.00TER 18 1

Cela fonctionne, mais cela peut prendre un moment à la prochaine personne qui lit middle.sh pour comprendre ce que cela fait.Nous pouvons améliorer notre script en ajoutant quelques commentaires en haut :

$ nano middle.sh
# Select lines from the middle of a file.# Usage: bash middle.sh filename end_line num_lineshead -n "" "" | tail -n ""

Un commentaire commence par un caractère # et va jusqu’à la fin de la ligne.L’ordinateur ignore les commentaires, mais ils sont précieux pour aider les gens (y compris votre futur moi) à comprendre et à utiliser les scripts.La seule mise en garde est qu’à chaque fois que vous modifiez le script, vous devez vérifier que le commentaire est toujours exact:une explication qui envoie le lecteur dans la mauvaise direction est pire que rien du tout.

Que faire si nous voulons traiter de nombreux fichiers dans un seul pipeline ?Par exemple, si nous voulons trier nos fichiers .pdb par longueur, nous taperions:

$ wc -l *.pdb | sort -n

parce que wc -l liste le nombre de lignes dans les fichiers(rappelez-vous que wc signifie ‘nombre de mots’, ajouter l’option -l signifie ‘compter les lignes’ à la place)et sort -n trie les choses numériquement.Nous pourrions mettre cela dans un fichier, mais alors il ne trierait jamais qu’une liste de .pdb fichiers dans le répertoire actuel.Si nous voulons être en mesure d’obtenir une liste triée d’autres types de fichiers, nous avons besoin d’un moyen d’obtenir tous ces noms dans le script.Nous ne pouvons pas utiliser , , et ainsi de suiteparce que nous ne savons pas combien de fichiers il y a.A la place, nous utilisons la variable spéciale $@,qui signifie, ‘Tous les arguments de la ligne de commande du script shell’.Nous devons également mettre $@ à l’intérieur de doubles-quotaspour gérer le cas des arguments contenant des espaces("$@" est une syntaxe spéciale et est équivalente à "" "" …).

Voici un exemple :

$ nano sorted.sh
# Sort files by their length.# Usage: bash sorted.sh one_or_more_filenameswc -l "$@" | sort -n
$ bash sorted.sh *.pdb ../creatures/*.dat
9 methane.pdb12 ethane.pdb15 propane.pdb20 cubane.pdb21 pentane.pdb30 octane.pdb163 ../creatures/basilisk.dat163 ../creatures/minotaur.dat163 ../creatures/unicorn.dat596 total

Liste des espèces uniques

Leah possède plusieurs centaines de fichiers de données, chacun étant formaté de la manière suivante :

2013-11-05,deer,52013-11-05,rabbit,222013-11-05,raccoon,72013-11-06,rabbit,192013-11-06,deer,22013-11-06,fox,12013-11-07,rabbit,182013-11-07,bear,1

Un exemple de ce type de fichier est donné dans data-shell/data/animal-counts/animals.txt.

Nous pouvons utiliser la commande cut -d , -f 2 animals.txt | sort | uniq pour produire l’espèce unique dans animals.txt. Afin d’éviter d’avoir à taper cette série de commandes à chaque fois, un scientifique peut choisir d’écrire un script shell à la place.

Écrivez un script shell appelé species.sh qui prend un nombre quelconque de noms de fichiers comme arguments de ligne de commande, et utilise une variation de la commande ci-dessus pour imprimer une liste des espèces uniques apparaissant dans chacun de ces fichiers séparément.

Solution

# Script to find unique species in csv files where species is the second data field# This script accepts any number of file names as command line arguments# Loop over all filesfor file in $@doecho "Unique species in $file:"# Extract species namescut -d , -f 2 $file | sort | uniqdone

Supposons que nous venons d’exécuter une série de commandes qui ont fait quelque chose d’utile – par exemple, qui ont créé un graphique que nous aimerions utiliser dans un article.Nous aimerions pouvoir recréer le graphique plus tard si nous en avons besoin, donc nous voulons enregistrer les commandes dans un fichier.Au lieu de les taper à nouveau (et potentiellement de se tromper), nous pouvons faire ceci :

$ history | tail -n 5 > redo-figure-3.sh

Le fichier redo-figure-3.sh contient maintenant :

297 bash goostats NENE01729B.txt stats-NENE01729B.txt298 bash goodiff stats-NENE01729B.txt /data/validated/01729.txt > 01729-differences.txt299 cut -d ',' -f 2-3 01729-differences.txt > 01729-time-series.txt300 ygraph --format scatter --color bw --borders none 01729-time-series.txt figure-3.png301 history | tail -n 5 > redo-figure-3.sh

Après un moment de travail dans un éditeur pour enlever les numéros de série sur les commandes, et pour enlever la dernière ligne où nous avons appelé la commande history, nous avons un enregistrement complètement précis de la façon dont nous avons créé cette figure.

Pourquoi enregistrer les commandes dans l’historique avant de les exécuter ?

Si vous exécutez la commande :

$ history | tail -n 5 > recent.sh

la dernière commande du fichier est la commande history elle-même, c’est à dire ,le shell a ajouté history au journal des commandes avant de l’exécuter réellement. En fait, le shell ajoute toujours les commandes au journal avant de les exécuter. Pourquoi pensez-vous qu’il fait cela ?

Solution

Si une commande provoque un crash ou un blocage, il pourrait être utile de savoir quelle était cette commande, afin d’enquêter sur le problème.Si la commande n’était enregistrée qu’après son exécution, nous n’aurions pas d’enregistrement de la dernière commande exécutée en cas de crash.

En pratique, la plupart des gens développent des scripts shell en exécutant des commandes à l’invite du shell quelques fois pour s’assurer qu’ils font la bonne chose,puis en les enregistrant dans un fichier pour les réutiliser.Ce style de travail permet aux gens de recycler ce qu’ils découvrent sur leurs données et leur flux de travail avec un appel à historyet un peu d’édition pour nettoyer la sortie et l’enregistrer comme un script shell.

Nelle’s Pipeline : Création d’un script

Le superviseur de Nelle a insisté pour que toutes ses analyses soient reproductibles. La façon la plus simple de capturer toutes les étapes est dans un script.

Premièrement, nous retournons dans le répertoire de données de Nelle:

$ cd ../north-pacific-gyre/2012-07-03/

Elle lance l’éditeur et écrit ce qui suit:

# Calculate stats for data files.for datafile in "$@"do echo $datafile bash goostats $datafile stats-$datafiledone

Elle enregistre cela dans un fichier appelé do-stats.shde sorte qu’elle peut maintenant refaire la première étape de son analyse en tapant :

$ bash do-stats.sh NENE*.txt

Elle peut aussi faire ceci:

$ bash do-stats.sh NENE*.txt | wc -l

pour que la sortie soit juste le nombre de fichiers traités plutôt que les noms des fichiers qui ont été traités.

Une chose à noter à propos du script de Nelle est qu’il laisse la personne qui l’exécute décider des fichiers à traiter.Elle aurait pu l’écrire comme suit :

# Calculate stats for Site A and Site B data files.for datafile in NENE*.txtdo echo $datafile bash goostats $datafile stats-$datafiledone

L’avantage est que cela sélectionne toujours les bons fichiers : elle n’a pas besoin de se rappeler d’exclure les fichiers ‘Z’. L’inconvénient est que cela ne sélectionne toujours que ces fichiers – elle ne peut pas l’exécuter sur tous les fichiers (y compris les fichiers ‘Z’), ou sur les fichiers ‘G’ ou ‘H’ que ses collègues en Antarctique produisent, sans modifier le script.Si elle voulait être plus aventureuse, elle pourrait modifier son script pour vérifier les arguments de la ligne de commande, et utiliser NENE*.txt si aucun n’a été fourni.Bien sûr, cela introduit un autre compromis entre flexibilité et complexité.

Variables dans les scripts shell

Dans le répertoire molecules, imaginez que vous avez un script shell appelé script.sh contenant les commandes suivantes :

head -n  tail -n  

Alors que vous êtes dans le répertoire molecules, vous tapez la commande suivante :

bash script.sh '*.pdb' 1 1

Quelle sortie suivante vous attendriez-vous à voir ?

  1. Toutes les lignes entre la première et la dernière ligne de chaque fichier se terminant par .pdbdans le répertoire molecules
  2. La première et la dernière ligne de chaque fichier se terminant par .pdb dans le molecules répertoire
  3. La première et la dernière ligne de chaque fichier dans le répertoire molecules
  4. Une erreur à cause des guillemets autour de *.pdb

Solution

La bonne réponse est 2.

Les variables spéciales $1, $2 et $3 représentent les arguments de ligne de commande donnés au script, de sorte que les commandes exécutées sont :

$ head -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb$ tail -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb

Le shell ne développe pas '*.pdb' parce qu’il est entouré de guillemets.Ainsi, le premier argument du script est '*.pdb' qui est développé dans le script par head et tail.

Trouver le fichier le plus long avec une extension donnée

Écrivez un script shell appelé longest.sh qui prend le nom d’un répertoire et une extension de nom de fichier comme arguments, et imprime le nom du fichier avec le plus de lignes dans ce répertoire avec cette extension. Par exemple:

$ bash longest.sh /tmp/data pdb

imprime le nom du fichier .pdb dans /tmp/data qui a le plus de lignes.

Solution

# Shell script which takes two arguments:# 1. a directory name# 2. a file extension# and prints the name of the file in that directory# with the most lines which matches the file extension.wc -l /*. | sort -n | tail -n 2 | head -n 1

La première partie du pipeline, wc -l /*. | sort -n, compte les lignes dans chaque fichier et les trie numériquement (les plus grandes en dernier). Lorsqu’il y a plus d’un fichier, wc produit également une ligne de résumé final, donnant le nombre total de lignes dans tous les fichiers. Nous utilisons tail-n 2 | head -n 1 pour jeter cette dernière ligne.

Avec wc -l /*. | sort -n | tail -n 1, nous verrons la ligne de résumé final : nous pouvons construire notre pipeline par morceaux pour être sûrs de comprendre la sortie.

Compréhension de lecture de scripts

Pour cette question, considérez à nouveau le répertoire data-shell/molecules.Celui-ci contient un certain nombre de fichiers .pdb en plus de tout autre fichier que vous avez pu créer.Expliquez ce que chacun des trois scripts suivants ferait lorsqu’il est exécuté en tant quebash script1.sh *.pdb, bash script2.sh *.pdb et bash script3.sh *.pdb respectivement.

# Script 1echo *.*
# Script 2for filename in   do cat $filenamedone
# Script 3echo [email protected]

Solutions

Dans chaque cas, l’interpréteur de commandes développe le caractère générique dans *.pdb avant de transmettre la liste résultante de noms de fichiers comme arguments au script.

Le script 1 imprimerait une liste de tous les fichiers contenant un point dans leur nom.Les arguments passés au script ne sont en fait utilisés nulle part dans le script.

Le script 2 imprimerait le contenu des 3 premiers fichiers ayant une extension .pdb., et font référence au premier, deuxième et troisième argument respectivement.

Le script 3 imprimerait tous les arguments du script (c’est-à-dire tous les fichiers .pdb),suivis de .pdb.$@ fait référence à tous les arguments donnés à un script shell.

cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb

Débogage de scripts

Supposons que vous ayez enregistré le script suivant dans un fichier appelé do-errors.shdans le répertoire north-pacific-gyre/2012-07-03 de Nelle :

# Calculate stats for data files.for datafile in "$@"do echo $datfile bash goostats $datafile stats-$datafiledone

Lorsque vous l’exécutez :

$ bash do-errors.sh NENE*.txt

la sortie est vide.Pour savoir pourquoi, ré-exécutez le script en utilisant l’option -x:

bash -x do-errors.sh NENE*.txt

Que vous montre la sortie ? Quelle ligne est responsable de l’erreur ?

Solution

L’option -x fait tourner bash en mode débogage.Cela imprime chaque commande au fur et à mesure de son exécution, ce qui vous aidera à localiser les erreurs.Dans cet exemple, nous pouvons voir que echo n’imprime rien. Nous avons fait une faute de frappe dans le nom de la variable de boucle, et la variable datfile n’existe pas, renvoyant donc une chaîne vide.

Points clés

  • Enregistrer les commandes dans des fichiers (généralement appelés scripts shell) pour les réutiliser.

  • bash exécute les commandes enregistrées dans un fichier.

  • $@ fait référence à tous les arguments de ligne de commande d’un script shell.

  • , , etc, font référence au premier argument de ligne de commande, au deuxième argument de ligne de commande, etc.

  • Placez les variables entre guillemets si les valeurs peuvent comporter des espaces.

  • Laisser les utilisateurs décider des fichiers à traiter est plus souple et plus cohérent avec les commandes Unix intégrées.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.