Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Mon livre « Cyberstructure »

Ève

Tirer davantage du shell Unix avec les fonctions

Première rédaction de cet article le 23 janvier 2006


Je suis d'accord que le shell Unix est plus dur à apprendre, pour le débutant complet ou pour l'utilisateur occasionnel, que l'interface graphique, dite GUI. Mais, pour l'utilisateur quotidien, c'est un formidable outil de productivité. Comme le décrit bien Neal Stephenson dans son essai In the beginning was the command line, le shell est un langage. Apprendre un langage est plus lent que de simplement faire des gestes et prononcer quelques onomatopées. Mais cela permet aussi bien plus de choses, et de manière bien plus efficace.

Un exemple qui est très difficile à faire avec les GUI : le shell permet de définir ses propres fonctions (une forme simplifiée des fonctions, les alias, est décrite dans un autre article). Comme tout langage, il peut servir à définir de nouveaux mots ou concepts, qui permettront d'aller plus vite la prochaine fois.

Voyons quelques exemples concrets. Ils sont tirés de mon .zshrc, le fichier d'initialisation du shell que j'utilise, zsh (deux notes pour les gens peu familiarisés avec Unix : chaque utilisateur peut choisir son shell, et le fichier d'initialisation est lu au démarrage du shell, ce qui évite de retaper des définitions à chaque fois). On peut trouver bien d'autres fichiers d'initialisation sur l'excellent site http://www.dotfiles.com/.

Voici une première fonction qui simplifie l'envoi d'un formulaire signé au RIPE-NCC. Comme beaucoup de registres, le RIPE-NCC accepte des demandes de modification par des formulaires texte signés avec un outil comme GnuPG. Pour éviter d'oublier de signer et pour éviter d'envoyer le formulaire à la mauvaise adresse, une petite fonction est bien pratique :

ripe-sign-and-send() {
    if [ "$1" = "" ]; then
	echo "Usage: $0 form-file"
	return
    fi
    if [ ! -e $1 ]; then
	echo "$1 does not exist"
	return
    fi
    gpg --clearsign --armor --output - --default-key 'NIC France' $1 | \
	command mutt -s MODIFY \
	    -e 'set from=bortzmeyer@nic.fr' \
		auto-dbm@ripe.net 
}

Comment se lit-elle ? On définit une fonction nommée ripe-sign-and-send qui commence par tester si elle a bien un argument (noté $1), si le fichier de ce nom existe (le shell est un langage de programmation complet, avec tests, boucles, etc) et elle appelle ensuite GnuPG puis passe le résultat à mutt pour envoi au RIPE-NCC. On note que je préfère, dans une fonction, employer les noms longs des options (--armor au lieu de -a) car on en tape le corps de la fonction qu'une fois et cela la rend plus lisible. En général, dans un script, il faut toujours utiliser les noms longs des options.

Une fois cette fonction définie, typiquement dans le fichier d'initialisation (~/.zshrc pour mon shell), on peut l'utiliser facilement :

ripe-sign-and-send mnter-frnic.txt

et tout est fait automatiquement. Je dois dire que je me suis toujours demandé comment les gens qui ne font pas de fonctions shell pouvaient supporter des tâches répétitives qu'il faut documenter dans une procédure. Cela me semble un incroyable gaspillage de compétences.

Voyons une deuxième fonction. Elle me sert sur les machines NetBSD à trouver un paquetage déjà compilé en donnant une partie de son nom :

    export NETBSD_SERVER=ftp.fr.netbsd.org
    export PKG_PATH=ftp://$NETBSD_SERVER/pub/NetBSD/packages/`uname -r|cut -d _ -f 1`/`uname -p`/All
    findpkg() {
      index=/tmp/NetBSD-`uname -m`-INDEX
      if [ ! -e $index ]; then
	 echo "Retrieving to $index..."
         wget --quiet -O $index ftp://$NETBSD_SERVER/pub/NetBSD/packages/pkgsrc/INDEX  
      fi
       grep -i "^[^\|]*$1" $index | \
         awk -F\| '{print $1 " (" $4 ")"}'
    }

On commence par déclarer, en dehors de la fonction, deux variables d'environnement, NETBSD_SERVER et PKG_PATH. Cette dernière est calculée à partir des résultats de la commande uname, qui affiche de l'information sur le système (NetBSD a des paquetages différents par architecture, et il tourne sur beaucoup d'architectures). Ensuite, on définit la fonction findpkg qui teste si un fichier d'index est déjà présent sur le disque (le shell a aussi des variables, ici, $index) et le télécharge sinon. Elle appelle ensuite grep pour fouiller dans le fichier index puis awk pour formater le résultat.

Les fonctions shell permettent d'enrichir les capacités d'un programme existant. Par exemple, l'excellent logiciel de gestion de versions darcs, que j'utilise pour ce blog, permet d'envoyer par courrier électronique les changements réalisés dans un dépôt. Le courrier électronique, qui marche même lorsque la connexion Internet n'est pas permanente, ne devrait pas être affecté si on est à un endroit perdu et déconnecté. Par défaut, darcs nécessite d'avoir une connexion Internet avec l'autre dépôt avec lequel on le compare. darcs n'a pas de commande pour envoyer les changements locaux, sans comparer avec un autre dépôt. Mais il est possible d'écrire cette commande :

# Send (by email) all the changes since a given synch point
darcsdiff() {
    if [ "$1" = "" ]; then
	echo "Usage: $0 patch-ID"
	return
    fi
    file=`mktemp`
    darcs changes --to-patch "$1" --context > ${file}
    darcs send --all --context ${file} . 
    rm ${file}
}

Elle crée un fichier temporaire (la commande située entre les apostrophes inverses est exécutée et son résultat va dans la variable $file), récupère le contexte des patches depuis un certain patch (donné en argument) avec darcs changes et les met dans le fichier temporaire. On envoie alors ces patches par courrier (avec darcs send). Et on détruit le fichier temporaire. On peut utiliser cette fonction avec darcsdiff "ID of the patch BEFORE the first patch I want", par exemple :

% darcsdiff 'BIXI a Montreal' 
What is the target email address? bortzmeyer@nic.fr
Successfully sent patch bundle to: bortzmeyer@nic.fr.

(Des détails sur l'utilisation de cette fonction se trouvent dans mon article sur la synchronisation de dépôts darcs par courrier.)

Une dernière fonction est plus complexe : une des grandes forces de l'outil ssh est sa capacité à faire suivre les messages du protocole X11 à travers le canal chiffré, ce qui augmente la sécurité, bien sûr, mais rend aussi l'utilisation de X11 bien plus facile (avant, il fallait définir une variable DISPLAY sans se tromper). Mais X11 consomme pas mal de bande passante et, si j'apprécie cette possibilité lorsque je me connecte sur une autre machine du même réseau local, j'en ai moins envie lorsque je travaille à grande distance, via des réseaux lents et peu fiables.

Comment déterminer automatiquement si le serveur est loin ou pas ? Une même machine (par exemple mon PC portable) peut se connecter à beaucoup d'endroits dans le monde et je ne peux donc pas mettre en dur la liste des machines proches ou lointaines. Une idée imparfaite mais qui marche suffisamment est de tester la durée d'aller-retour d'une demande d'écho, envoyée avec la commande ping.

Cela donne :

# Test the RTT (round-trip time) to a remote host with ping and then
# slogin to it, enabling or disabling the X11 forwarding depending on
# the value of the RTT
slogin () {

    if [ "$DISPLAY" = "" ]; then
	command slogin -x $*
	return
    fi
    threshold=35 # Milli-seconds of RTT
    if [ "$1" = "" ]; then
        echo "Usage: $0 host"
	return
    fi
    # A better way to get the last argument?
    for arg in $*; do
	last=$arg
    done
    host=$last
    # TODO: handle the user@host syntax
    if [ -z `which ping` ]; then
	echo "$0 requires ping"
	return
    fi
    # Some pings (Netkit ping on some Linuces) do not like fractional intervals
    ping -c 1 -q -n -i 0.3 $host > /dev/null 2>&1
    if [ $? ]; then
        ping_options="-c 3 -i 0.2"
    else # Less tests, to be faster
        ping_options="-c 2"
    fi
    average_rtt=`ping $ping_options -n -q $host | \
		egrep '^(rtt|round-trip)' | cut -d= -f2 | cut -d/ -f2`
    if [ -z "$average_rtt" ]; then 
	echo "Cannot ping $host, assuming slow link, DISablingX forwarding"
	command slogin -x $*
    else
	if [ `expr $average_rtt "<=" $threshold` = 1  ]; then
	    echo "Fast link to $host, ENabling X forwarding"
	    command slogin -X $*
	else
	    echo "Slow link to $host, DISablingX forwarding"
	    command slogin -x $*
	fi
    fi
    title
}

Comme souvent en programmation, l'essentiel du code est occupé par des détails pour couvrir les 5 % de cas vicieux. Ici, il faut tenir compte du fait qu'il existe plusieurs programmes ping, avec des options différentes (d'où le test du code de retour de ping, $!), que certains coupe-feu bloquent les demandes d'écho, etc.

En résumé, les fonctions sont une technique qui réalise le rêve de l'informatique : automatiser des tâches pénibles pour laisser les humains écrire pour leur blog.

Si vous souhaitez plus de détails en français sur le shell, vous pouvez consulter le cours de Philippe Dax ou bien celui de Linux-Nantes.

Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)

Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)