Skip to content

Shell avancé

Programmes et redirections

Les fichiers spéciaux

Il existe 3 fichiers spéciaux utilisés par de nombreux programmes et gérés par votre shell:

  • stdin: entrée d'un programme, par exemple la saisie clavier dans le shell
  • stdout: sortie d'un programme, par exemple la zone d'affichage dans le shell
  • stderr: sortie d'erreur d'un programme, par exemple la zone d'affichage dans le shell

Un ou plusieurs programmes peuvent lire séparément stderr et stdout d'un autre programme. Il est donc important de ne pas mélanger les messages envoyés (on parlera de données écrites) sur stdout (données de résultat) et stderr (messages de déroulement)

    //OK: les messages de déroulements sont bine séparés des données produites 
    fprintf(stdout, "Résultat ligne 1\n");
    fprintf(stderr, "En cours");
    fprintf(stdout, "Résultat ligne 2\n");

    //MAUVAISE PRATIQUE: les messages de déroulements et les données produites sont mélangés 
    fprintf(stdout, "Résultat ligne 1\n");
    fprintf(stdout, "En cours");
    fprintf(stdout, "Résultat ligne 2\n");

Redirection des fichiers spéciaux

Le shell associe par défaut stdout ou stderr à la zone d'affichage de la fenêtre où il s'exécute, en d'autres termes toute donnée écrite par le programme dans ces deux fichiers s'affichera à l'écran sous forme de texte, pas forcément lisible si les données écrites ne sont pas du texte.

Mais il est possible de dire au shell que ces fichiers spéciaux sont à remplacer par d'autres fichiers. On parle alors de redirection des sorties d'un programme.

Le shell peut envoyer les données émises sur stdout ou stderr vers des fichiers avec le symbole >:

  • redirection de sortie:

    echo "toto" > montest.txt
    

  • redirection de sortie et d'erreur:

    ./mon_programme_avec_erreur > montest.txt 2> erreurs.txt
    

Note

> est équivalent à 1> et signifie redirection de la sortie stdout. Si le programme génère des erreurs, elles seront affichées dans le terminal si stderr n'a pas été redirigé.

Bien évidement cela ne marche que si le programme est conçu pour écrire dans ces fichiers spéciaux.

Une redirection très utile consiste à envoyer les données "nulle part" donc à ne pas les transmettre du tout. Sous la plupart des systèmes, ceci se fait via le fichier spécial /dev/null.

Par exemple pour tester uniquement ce qu'un programme écrit sur stderr sans s'occuper des données qu'il produit:

./mon_programme_avec_erreur > /dev/null 2> erreurs.txt

La redirection > écrase le fichier de destination. Si vous voulez ajouter le contenu à la fin du fichier de destination, utilisez >> (ou 2>>pour stderr):

./mon_programme >> montest.txt

Note

Prenez le temps de réfléchir à la gestion de ces fichiers spéciaux lorsque vous codez un programme, en particulier si votre programme produit des fichiers (autres que des logs) qui pourraient être utilisés par d'autres programmes.

Enchaîner des programmes

Si un programme sait lire depuis un fichier, on peut lui indiquer que le fichier à prendre en entrée est en fait le fichier stdout d'un autre programme. De cette manière, les fichiers spéciaux peuvent être utilisés comme canal unidirectionnel de communication entre programmes.

On utilise pour cela le symbole | pour data pipe ou tube de données, qui redirige stdout du premier programme vers l'entrée du suivant.

cat logs | grep text

Si le programme de destination ne prend pas ses données d'entrée depuis stdin mais depuis un nom de fichier donnée en argument, la convention est d'utiliser - comme nom pour désigner stdin:

cat logs | mon_programme -i -

On peut enchaîner ainsi plusieurs programmes

cat logs | mon_programme -i - | other_program

Cette technique est très utile pour échanger simplement des informations entre programmes. Il faut toutefois noter que c'est une technique dite bloquante: dans prog1 | prog2, prog2 est mis en attente par le système tant que prog1 fonctionne et qu'aucune nouvelle donnée n'est écrite par prog1 sur son fichier stdout.

Cas avancés de chaînage

Ces techniques ne marchent que si les programmes respectent les conventions des fichiers spéciaux. De même, si un programme produit plusieurs fichiers en une exécution, ces techniques ne marcheront pas.

Il est possible de s'en sortir via des noms explicites pour ces "tubes", on parlera de tubes nommés ou named pipe. La technique est nettement plus complexe à mettre en oeuvre dans un programme (en particulier sous windows), et ne sera pas couverte dans cette UE.

Pour aller plus loin

Ligne de commande avancée

Un shell ne se contente pas de traiter une ligne de commande simple comme nous l'avons vu jusqu'à présent. Il dispose aussi d'une grammaire permettant de construire des lignes de commandes avancés.

Il est possible de faire deux commandes en une, en séparant les commandes par ;:

ls file > /dev/null ; echo "présent"
ls: file: No such file or directory
présent

Il est possible de faire deux commandes, la seconde ne s'exécutant que si la première est valide, en séparant les commandes par &&:

ls file > /dev/null && echo "présent"
ls: file: No such file or directory

Il est possible de faire deux commandes, la seconde ne s'exécutant que si la première n'est pas valide, en séparant les commandes par ||:

echo "présent" || ls file > /dev/null
présent

Attention

Ne confondez pas le symbole de tubes | et le symbole OU ||

La logique de ces opérations repose sur le fait qu'un programme, lorsqu'il finit son exécution, retourne un code d'erreur (un entier) qui par convention est 0 en cas de succès et non-0 en cas d'erreur.

Note

Prenez le temps de réfléchir à la gestion de votre code de retour lorsque vous codez un programme, cela simplifiera l'utilisation de votre programme via le shell.

Vous pouvez par exemple tester le résultat de la dernière commande exécutée par le shell en utilisant le mot-clé $?:

ls file 2> /dev/null                   
echo $?
1

Chaque type de shell (bash, zsh, sh) a sa propre grammaire et ses outils pour faire des opérations très classiques comme des boucles for, while, des additions, etc...

for i in * ; do echo "fichier $i"; done

Il n'est pas dans les objectifs de cette UE de couvrir tout cela, mais vous trouverez d'excellents tutoriels en ligne comme, par exemple, sur wikibooks.

Ligne de commande trop longue

Il est assez fréquent d'avoir des lignes de commandes complexes dont la longueur dépasse la zone d'affichage. Cela rend l'édition de la ligne de commande assez difficile.

Pour éviter ceci, le shell interprète le caractère \ comme une indication de continuation sur plusieurs lignes:

./mon_super_programe --option1 --option2=bla  --option3=foo fichier_in1 fichier_in2 fichier_in3 -o fichier_out

peut donc s'écrire

./mon_super_programe --option1 --option2=bla  --option3=foo \
    fichier_in1 fichier_in2 fichier_in3 \
    -o fichier_out

Ceci est souvent le cas dans les exemples que vous trouverez sur internet, et peut s'avérer très pratique pour la lisibilité de vos scripts (voir ci-après).

Un shell pour plusieurs programmes

Nous avons jusqu'à présent invoqué commandes/programmes en tapant directement le nom désiré puis avons attendus le résultat.

Dans un système d'exploitation, chaque programme est en fait exécuté par une logique spéciale du système appelée processus (process en anglais), de manière à isoler le programme du reste du monde (mémoire, accès aux ressources, etc...).

Le processus du shell est différent du processus exécutant la commande entrée, afin que si une erreur fatale advienne dans le programme démarré et qu'il doive être arrêté par le système (on parlera de tuer un processus ou kill a process), le shell ne soit pas tué lui aussi.

Lorsqu'un programme est lancé dans le shell comme vu précédemment, le shell attendra que le programme finisse avant de vous rendre la main. On parle de lancement en premier plan ou foreground.

Il est possible de lancer un programme sans bloquer le shell, on parlera de lancement en arrière-plan ou background. On utilise la plupart du temps le symbole &en fin de ligne pour cela.

Voici un exemple utilisant la commande sleep qui fait dormir le shell pour le nombre de secondes indiqué:

for i in {1..10} ; do  echo "Alive $i" && sleep 1 ; done &
ps

Avec cette méthode, vous pouvez lancer plusieurs programmes en même temps, et attendre le résultat de l'ensemble via la commande wait:

for i in {1..10} ; do  echo "CompteurA $i" && sleep 1 ; done &
for i in {1..5} ; do  echo "CompteurB $i" && sleep 2 ; done &
echo "Compteurs démarrés"
wait

Ceci est particulièrement utile dans les scripts abordés ci-dessous.

Warning

Indépendamment du mode de lancement du programme, le processus créé sera automatiquement tué si le processus qui l'a lancé (ici le shell) est tué. Nous reviendrons sur cet aspect dans la partie réseau.

Scripts shell

Vous aurez assez rapidement un ensemble de commande à appliquer pour configurer un projet, lancer un programme et regarder les résultats, etc...

Au lieu de taper chaque commande à chaque fois, il est recommandé de passer par un script shell, qui est dans le plus simple des cas la liste de vos commandes ligne par ligne.

Pour ceci il faut:

  • Créer un fichier vide (touch monscript.sh) et donner les droits d'exécution (chmod +x monscript.sh) au fichier.
  • Ajouter au tout début #!/bin/sh
    • ou #!/bin/bash si vous voulez forcer l'utilisation du shell bash
  • Coller vos commandes
  • Mettre des commentaires en commençant une ligne par #
  • Sauver le fichier

Exemples de commandes:

cd mon_dir
prog1
prog2
cd ..

La version script run.sh sera:

#!/bin/sh
#aller dans le répertoire
cd mon_dir
#on invoque ici blabla
prog1
#puis on fait  blabla
prog2
cd ..
./run.sh

L'avantage est de mieux documenter vos commandes, de simplifier l'utilisation du shell et d'éviter les fautes de frappes en retapant la même commande à longueur de temps.

Info

Il est possible de lancer un script n'ayant pas les droits d’exécution en invoquant le shell désiré, par exemple bash script.sh

Vous pouvez aussi utiliser la notion de lancement en arrière-plan vu précédemment dans vos scripts. Par exemple, au lieu de lancer prog1 dans un terminal et prog2 dans un deuxième terminal, faites simplement un script:

#!/bin/sh
#lancer prog1 sans attendre qu'il soit terminé
./prog1 &
#lancer prog2 
./prog2
#puis on attend le tout
wait