mardi 30 novembre 2010

Espace disque

Météo.
Nous créons le dossier test et nous y plaçons le fichier "météo" dont voici le contenu:
Belle journée ensoleillée.
Températures de 24 à 28 degrés.
Vent modéré.
Examinons maintenant le contenu du dossier avec la commande ls et les options:
g: pour un affichage au format long mais sans le propriétaire
o: idem sans le groupe
a: pour afficher les fichiers cachés
i: pour afficher le numéro d'inode
s: pour afficher la taille en blocs
toto@rigel:~$ ls -goais test
total 12
1308818 4 drwxr-xr-x  2 4096 2010-11-26 10:10 .
1308748 4 drwxr-xr-x 39 4096 2010-11-26 10:04 ..
 442198 4 -rw-r--r--  1   79 2010-11-26 10:10 météo
La première colonne contient les identifiants numériques (numéro d'inode) des fichiers. Ensuite vient une colonne avec l'espace disque occupé par chaque fichier, en blocs de 1024 octets. Ces blocs n'ont aucun rapport avec les blocs du système de fichier (ext3, taille 4096 octets), ni avec les blocs du disque (taille 512 octets). A gauche des dates, on trouve une colonne avec des tailles en octets: 79 pour le fichier météo. Pour ceux qui veulent compter, ne pas oublier les caractères de contrôle "fin de ligne". Ne pas oublier, aussi que les caractères accentués comptent double (Nous sommes en UTF-8).
Les deux premières lignes de la liste se rapportent à des dossiers comme l'indique le premier caractère de la colonne 3, c'est-à-dire d.
Quels sont ces dossiers?
La commande find permet de faire une recherche sur les identifiants numériques:
toto@rigel:~$ find ~/ -inum 1308818 ; find ~/ -inum 1308748
/home/toto/test
/home/toto/
Le premier fichier, "." n'est rien d'autre que le dossier test lui-même, tandis que le deuxième fichier, ".." est le dossier parent. Ce sont des résultats bien connus.

Ce n'est pas mon frère, c'est moi.
Procédons à quelques manipulations avant de relancer notre commande ls:
toto@rigel:~$ cd test
toto@rigel:~/test$ ln météo prévisions ; cp météo le_temps
toto@rigel:~/test$ ls -goais
total 20
1308818 4 drwxr-xr-x  2 4096 2010-11-26 12:36 .
1308748 4 drwxr-xr-x 39 4096 2010-11-26 10:04 ..
 442199 4 -rw-r--r--  1   79 2010-11-26 12:36 le_temps
 442198 4 -rw-r--r--  2   79 2010-11-26 10:10 météo
 442198 4 -rw-r--r--  2   79 2010-11-26 10:10 prévisions
Nous avons maintenant semble-t-il, 3 fichiers identiques de 79 octets.
Mais attention, les apparences sont trompeuses. En effet l'identifiant 442198 est répété. Donc nous avons dans le répertoire, deux entrées, deux noms différents (météo et prévisions) pour le même fichier (ce qui est indiqué par le chiffre 2 qui précède la taille en octets).
Vérifions cela:
toto@rigel:~/test$ echo 'Bonne soirée' >> prévisions
toto@rigel:~/test$ ls -goais
total 20
1308818 4 drwxr-xr-x  2 4096 2010-11-26 12:36 .
1308748 4 drwxr-xr-x 39 4096 2010-11-26 10:04 ..
 442199 4 -rw-r--r--  1   79 2010-11-26 12:36 le_temps
 442198 4 -rw-r--r--  2   93 2010-11-26 12:50 météo
 442198 4 -rw-r--r--  2   93 2010-11-26 12:50 prévisions
La taille de météo et de prévisions passe à 93, ce qui montre bien qu'il s'agit de deux noms portés par un même fichier. Le lien existant entre météo et prévisions est appelé un lien en dur.
Mais attendez, cela implique que le total repris en ligne 1 de l'output est faux, ou en tout cas qu'il ne correspond pas au nombre de blocs (de 1 K) occupés par les fichiers du dossier, puisque le fichier 442198 est compté deux fois.
Pour un total correct, il faut procéder avec la commande du. Utilisons l'option -a pour que les fichiers comptabilisés apparaissent dans l'output:
toto@rigel:~$ du -a test
4       test/météo
4       test/le_temps
12      test
Le chiffre 12 correspond au volume occupé par le fichier 442198 (météo), le fichier 442199 (le_temps) et le fichier 1308818 (le dossier test lui-même). L'entrée "prévisions" est ignorée. Le fichier 1308748, qui correspond au répertoire parent n'est pas repris. Deux entrées en moins (celles qui ne sont pas surlignées en saumon), à 4 K l'entrée: voilà qui explique pourquoi le total passe de 20 K à 12 K. Quant au fichier 1308748 (répertoire parent), du en tiendra compte uniquement pour le calcul du volume occupé par /home/toto ou par tout autre répertoire supérieur.

Pour vivre heureux, vivons cachés.
Effectuons encore quelques manipulations avant d'exécuter de nouveau la commande ls (avec l'option supplémentaire -R pour la récursivité):
toto@rigel:~/test$ mkdir test.d1 test.d2
toto@rigel:~/test$ mv prévisions test.d1 ; mv météo test.d2
toto@rigel:~/test$ ls -Rgoais ~/test
/home/toto/test:
total 20
1308818 4 drwxr-xr-x  4 4096 2010-11-27 11:42 .
1308748 4 drwxr-xr-x 39 4096 2010-11-26 10:04 ..
 442199 4 -rw-r--r--  1   79 2010-11-26 12:36 le_temps
1309068 4 drwxr-xr-x  2 4096 2010-11-27 11:42 test.d1
1309591 4 drwxr-xr-x  2 4096 2010-11-27 11:42 test.d2

/home/toto/test/test.d1:
total 12
1309068 4 drwxr-xr-x 2 4096 2010-11-27 11:42 .
1308818 4 drwxr-xr-x 4 4096 2010-11-27 11:42 ..
 442198 4 -rw-r--r-- 2   93 2010-11-26 12:50 prévisions

/home/toto/test/test.d2:
total 12
1309591 4 drwxr-xr-x 2 4096 2010-11-27 11:42 .
1308818 4 drwxr-xr-x 4 4096 2010-11-27 11:42 ..
 442198 4 -rw-r--r-- 2   93 2010-11-26 12:50 météo
Nous avons explicitement avec la commande "ln météo prévisions", créé un lien en dur. Mais de tels liens sont naturellement présents dans le système. Ainsi pour le fichier 1308818, le nombre 4 - en rouge, colonne 4 - indique qu'il lui correspond 4 entrées, dont 3 figurent dans l'output ci-dessous (il manque l'entrée dans /home/toto). Le fichier 1308818 contribue donc à chacun des 3 totaux.
Que donne la commande du?
toto@rigel:~/test$ du -a ~/test
4       /home/toto/test/test.d1/prévisions
8       /home/toto/test/test.d1
4       /home/toto/test/le_temps
4       /home/toto/test/test.d2
20      /home/toto/test
Remarquons en premier que si le total /home/toto/test est le même dans les deux cas (20) il s'agit d'un hasard: vu l'existence de sous-dossiers (non-vides), les deux nombres ont une signification différente. D'un côté  (commande ls), il s'agit du volume occupé par les fichiers qui ont une entrée dans le répertoire test et de l'autre (commande du), il s'agit du volume occupé par le dossier test et tout ce qu'il contient. Le fichier 1308748 (/home/toto) est bien comptabilisé par ls, mais pas par du (comme précédemment). Pour le fichier 442198 (prévisions), il contribue au total calculé par du, mais pas au total 20 de ls puisqu'il n'y a plus d'entrée pour ce fichier au niveau du répertoire test. Il suffirait que 442198 occupe plus de 4 K pour que les deux totaux ne soient plus égaux.
Logiquement, du, contrairement à ls, ne tient pas compte du fichier 1308818 (/home/toto/test) pour le calcul du volume occupé par test.d1 et test.d2 (il en tient compte pour le volume occupé par test). Comme précédemment, nous avons surligné (en saumon) les items de l'output de ls dont la commande du tient compte.

C'est trop injuste!
Nous constatons que cette fois "météo" est ignoré par du au profit de "prévisions". Évidemment, il y a là un certain arbitraire. Pourquoi compter 8 K pour test.d1, 4 K pour test.d2 et non pas le contraire? D'ailleurs, si on a:
toto@rigel:~/test$ du -ac test.d1 test.d2
4       test.d1/prévisions
8       test.d1
4       test.d2
12      total
on a aussi:
toto@rigel:~/test$ du -ac test.d2 test.d1
4       test.d2/météo
8       test.d2
4       test.d1
12      total
Mais le principal est que le total des blocs occupés (affiché grâce à l'option -c) soit le bon. Attention, pour obtenir 12, il ne faut pas tenir compte des 4 K du fichier prévisions (météo) puisque ceux-ci sont inclus dans  les 8 K du dossier test-d1 (test-d2).
Donc si on pouvait avoir l'output sans les fichiers, ce serait cool. Et bien il suffit de supprimer l'option -a:
toto@rigel:~/test$ du -c test.d1 test.d2
8       test.d1
4       test.d2
12      total
toto@rigel:~/test$ du -c test.d2 test.d1
8       test.d2
4       test.d1
12      total
Mais avec la commande du ~/test, on a un problème:
toto@rigel:~/test$ du ~/test
8       /home/toto/test/test.d1
4       /home/toto/test/test.d2
20      /home/toto/test
Le fichier le_temps a disparu, alors pour expliquer le 20 final...
Comment procéder pour afficher le_temps sans pour autant faire réapparaitre prévisions, c'est-à-dire sans afficher le détail de test.d1 et test.d2 ?
La solution se trouve dans l'emploi de l'option --max-depth:
toto@rigel:~/test$ du ~/test -a --max-depth=1
8       /home/toto/test/test.d1
4       /home/toto/test/le_temps
4       /home/toto/test/test.d2
20      /home/toto/test

Creuser, toujours creuser!
L'option --max-depth=1 n'empêche pas du d'explorer l'arborescence plus en profondeur.
Ainsi, après avoir procédé comme ci-dessous,
toto@rigel:~/test$ mkdir test.d1/test.d3
toto@rigel:~/test$ echo salut > test.d1/test.d3/salut
toto@rigel:~/test$ echo hello > test.d1/test.d3/hello
ce qui a ajouté test.d3 à l'arborescence:
.
├── le_temps
├── test.d1
│   ├── prévisions
│   └── test.d3
│       ├── hello
│       └── salut
└── test.d2
    └── météo
nous constatons que l'output de la commande du, même avec --max-depth=1, est modifié en conséquence
toto@rigel:~/test$ du ~/test -a --max-depth=1
20      /home/toto/test/test.d1
4       /home/toto/test/le_temps
4       /home/toto/test/test.d2
32      /home/toto/test
On peut utiliser l'option -S. De la sorte, le volume figurant devant chaque nom de dossier se rapportera seulement au volume de ce dossier et aux fichiers qu'il contient: le volume devant test.d1 retrouvera son ancienne valeur (celle d'avant la création de test.d3). Par contre, le total qui peut être obtenu en ajoutant l'option c se rapportera à toute l'arborescence:
toto@rigel:~/test$ du ~/test -acS --max-depth=1
8       /home/toto/test/test.d1
4       /home/toto/test/le_temps
4       /home/toto/test/test.d2
8       /home/toto/test
32      total
Si seul le total nous intéresse et en aucune façon une quelconque ventilation, il suffit d'utiliser l'option s:
toto@rigel:~$ du -s ~/test
32      /home/toto/test

Quelle belle partition!
Parmi les options de la commande du, on peut encore citer:
-h pour afficher les volumes de manière facilement lisible
-x pour rester dans le même système de fichiers.
Exemple:
root@rigel:~# du -hx --max-depth=1 /
3,4G    /usr
76K     /tmp
8,0K    /home
15M     /etc
4,0K    /srv
2,4G    /var
0       /sys
12K     /mnt
275M    /opt
6,6M    /bin
255M    /lib
8,0K    /media
45M     /boot
4,0K    /cdrom
4,0K    /selinux
7,6M    /sbin
0       /dev
0       /proc
16K     /lost+found
628K    /root
6,3G    /
La commande demande un certain temps pour s'exécuter. Rappelons que l'option --max-depth=1 ne limite nullement la profondeur du scan.
Le volume de 8 K qui apparait devant /home montre bien que la partition montée en ce point n'a pas été explorée, et ce grâce à l'option -x.
En ce qui concerne le total de la partition, un résultat peut être obtenu beaucoup plus rapidement avec la commande df:
toto@rigel:~$ df -h /
Sys. de fich.       Taille  Uti. Disp. Uti% Monté sur
/dev/sdc1              11G  6,5G  3,6G  65% /
df n'effectue aucun scan et tire directement ses informations du superbloc. Le montant obtenu n'est pas le même (6,5 G au lieu de 6,3 G). Pour df, il s'agit de l'espace occupé par le système de fichier et non de l'espace occupé par l'ensemble des fichiers. Et pour un système de fichier qui vient d'être créé, l'espace disponible n'est déjà plus égal à la taille.

mardi 9 novembre 2010

Les secrets de cat

La commande cat copie vers la sortie standard le contenu du ou des fichiers indiqués sur la ligne de commande. Une simple redirection permet donc d'obtenir un fichier qui est la concaténation de plusieurs fichiers:

toto@rigel:~/test$ cat garçons
Toto
Riri
toto@rigel:~/test$ cat filles
Tata
Titine
toto@rigel:~/test$ cat garçons filles > toutlemonde
toto@rigel:~/test$ cat toutlemonde
Toto
Riri
Tata
Titine
toto@rigel:~/test$

C'est la finalité première de cat comme d'ailleurs annoncé au début de la page man:

cat - concatenate files and print on the standard output

Si aucun fichier n'est indiqué sur la ligne de commande, cat copie alors l'entrée standard vers la sortie standard:

cat est en attente de ce que nous allons saisir au clavier (entrée standard) pour l'envoyer vers la sortie standard.
Nous saisissons "hello"


et tapons sur Enter pour envoyer le texte saisi vers cat:

A l'écran figure ce que nous avons tapé au clavier, mais aussi ce qui a été renvoyé par cat via stdout (c'est évidement le même texte). cat continue à attendre la suite.
Nous interrompons le flux d'entrée par appui sur CTRL-D.
On peut s'amuser de la même façon avec tr par exemple:



Seul un appui sur Enter sépare les deux dernières copies d'écran.
Ce genre de manipulation est d'une utilité toute relative sauf si nous redirigeons stdout nous donnant ainsi la possibilité de créer un petit fichier texte:

Cette fois évidemment les lignes n'apparaissent pas en double puisque l'output se fait vers le fichier salut. Comme précédemment, indiquons à cat avec CTRL-D qu'il ne doit plus rien attendre et puis vérifions (avec cat toujours) que le fichier salut contient bien ce qu'il est censé contenir.


On peut intercaler la lecture de stdin entre la lecture de plusieurs fichiers en utilisant le signe "-":


cat a lu le premier fichier, envoyé son contenu dans toutlemonde et est en attente de la suite qui doit venir de stdin. Saisissons "++++++", puis appuyons sur Enter:


"++++++" se trouve déjà dans toutlemonde. Il reste à appuyer sur CTRL-D pour terminer:


Attention, il s'agit de terminer proprement et non pas en utilisant CTRL-C, faute de quoi cat ne lira pas le second fichier.

Considérons maintenant cette commande:


Aucun nom de fichier n'est indiqué, donc cat devrait lire l'entrée standard. Mais l'instruction de redirection << FIN signifie à bash que ce qui suit, donc ici ce qui sera écrit à l'écran, constituera le flux d'entrée et ce jusqu'au mot FIN. cat va donc lire non pas l'entrée standard, mais le contenu de l'écran jusqu'à la rencontre du délimiteur FIN:




Un simple appui sur Enter sépare ces deux copies d'écran.
Le texte

hello
ola
hi

est ce qui s'appelle un here document.

L'emploi de here documents est surtout utile dans les scripts: il permet d'éviter la multiplication des commandes echo.
Soit le script courses:

#!/bin/bash

echo
echo Bonjour $USER

cat  << COURSES

Voici la liste des courses:

Riz
Grains
Saucisses
Tomates
Piments verts

echo 1
COURSES

echo 2

Voici ce que donne son exécution:


Nous avons volontairement, pour des raisons pédagogiques, pollué le script avec "echo 1" et "echo 2".
"echo 1" est lu par cat et ensuite renvoyé vers stdout.
"echo 2" est une commande qui est exécutée.

lundi 1 novembre 2010

redirections (encore)

Les descripteurs de fichiers
Le répertoire /proc contient un sous-répertoire pour chaque processus qui tourne sur le système, sous-répertoire nommé d'après le pid de ce processus.
Chacun de ces répertoires contient à son tour un sous-répertoire fd où se trouvent les descripteurs de fichier ouverts par le processus.
Sachant que echo $$ renvoie le pid du shell bash courant, examinons quels y sont les descripteurs de fichier ouverts:

/proc/1897/fd/0 correspond à l'entrée standard (stdin)
/proc/1897/fd/1 correspond à la sortie standard (stdout)
/proc/1897/fd/2  correspond à l'erreur standard (stderr)

Ce sont les descripteurs de fichiers par défaut.
Nous avons déjà traité de stdout et stderr dans le billet précédent.
Quant à l'entrée standard, elle correspond au clavier.
Lorsque nous exécutons une commande, il lui correspond un processus qui est différent du processus du shell dans lequel la commande a été lancée. Ce processus hérite des descripteurs tels qu'ils existent au niveau du shell, puis les redirections éventuelles sont effectuées. Mais comment avoir un état de la situation? Il faudrait connaître le pid de ce processus! Et bien pas vraiment: il suffit d'examiner le contenu du répertoire /proc/self/fd:


Nous voyons que /proc/self/fd est en fait /proc/2310/fd où 2310 est le pid du processus d'exécution de la commande. Ce répertoire /proc/2310/fd n'existe que pendant la durée de vie du processus. Les messages d'erreurs de la première commande ls (comme celui généré par le fait que le fichier tata est inexistant) n'apparaissent pas à l'écran. Ils ont été redirigés vers le fichier erreurs, ce que montre bien l'examen du contenu de /proc/2310/fd. La deuxième commande ls a hérité des descripteurs du shell qui sont restés inchangés et cette fois le message d'erreur (prouvant que /proc/2310 n'existe plus) arrive à l'écran.

Les canaux de communication avant et après un pipe.
Dans le cas d'un pipe, chacune des deux commandes, avant et après le pipe, a son propre  système de canaux de communication. En principe stdout de la commande en aval est connecté à stdin de la commande en amont, comme dans ce cas simple où l'entrée standard de la commande tr n'est plus le clavier, mais la sortie standard de la commande cat:


Mais en cas de redirections, la situation peut-être plus compliquée:


Ici, stdout en aval (processus 2109) a été redirigé vers le fichier output et c'est le descripteur 2 auquel le canal 1 a été affecté qui est connecté au pipe. En amont (processus 2110), stdin (fd/0) est connecté au pipe. Bien sûr, le pipe ici ne sert absolument à rien. En effet la commande ll en aval ne produit aucun message d'erreur qui serait envoyé dans le canal 1. Donc rien ne transite par le pipe et de toute façon ls en amont ignore stdin. Cette ligne de commandes n'a d'autre utilité que de permettre la visualisation de connections en amont et en aval d'un pipe.

Filtrer stderr.
Imaginons maintenant que nous désirions filtrer les messages d'erreurs avec grep. Nous voudrions aussi que tout arrive à l'écran, aussi bien stderr que stdout. Dans le billet précédant nous avons proposé une solution à ce genre de problème. En voici une autre:


correspondant au schéma:
Cette solution, comme celle du billet précédant, demande un descripteur supplémentaire (3). Mais cette fois, il est créé au niveau de la commande et il n'existe que pendant la durée d'exécution de la commande, alors qu'auparavant il avait été défini au niveau du shell.
Il ne faut pas se laisser leurrer par le schéma et lui accorder une signification qu'il n'a pas: les différentes instructions de redirections s'évaluent bien de gauche à droite. La première instruction 3>&1 établit un lien du descripteur 3 vers le canal 1, lien vers le canal 1 qui sans cela serait perdu suite à la deuxième instruction qui lie le descripteur 1 au canal 2. La dernière instruction 2>&3  affecte au descripteur 2 le canal précédemment affecté au descripteur 3, c'est-à-dire le canal 1.

Rediriger stdout au niveau du shell
Si nous redirigeons stdout vers un fichier au niveau d'un shell bash, chaque commande lancée dans ce shell, héritant des descripteurs existants, enverra son output dans ce fichier. Plus rien n'arrivera à l'écran, sauf les messages d'erreurs. Avant de faire pointer le descripteur 1 vers le fichier, il convient de sauvegarder le lien vers le canal 1:
toto@rigel:~/test$ exec 3>&1 1> output
De la sorte on pourra restaurer la situation initiale en faisant pointer le descripteur 1 vers le canal associé au descripteur 3 et en clôturant ensuite ce descripteur 3:
toto@rigel:~/test$ exec 1>&3 3>&-
Procédons:



Rediriger stdin
Nous n'avons pas jusqu'à présent donné d'exemple de redirection de stdin.
Revenons sur la commande où nous avons mis en majuscules le contenu du fichier erreurs. On aurait pu procéder comme ceci:


< erreurs est mis pour 0< erreurs: l'entrée standard a été redirigée vers le fichier erreurs. On peut enchaîner les redirections comme ceci:


La sortie standard est redirigée vers Erreurs qui va donc contenir le message modifié.
Ce genre d'expression:
commande < fichier-in > fichier-out
est relativement courant.
Continuons avec un autre exemple:


La commande read est en attente de ce que nous allons saisir au clavier pour le mettre dans la variable readvar. Nous saisissons par exemple "hello", puis nous tapons sur Enter afin d'envoyer le texte saisi. Puis nous vérifions le contenu de readvar:

Si nous voulons lire le fichier toto dont voici le contenu:

Je m'appelle Toto.
Je vais à l'école avec Tata
Nous allons à pied car ce n'est pas loin.

nous devons rediriger stdin vers ce fichier.
Procédons:

readvar contient la première ligne du fichier toto. Si nous relançons la commande read rien ne changera. Ce sera encore la première ligne qui sera lue, car à chaque fois le fichier toto est réouvert et la lecture reprend au début . En fait ce qu'il faudrait, c'est pouvoir rediriger stdin vers le fichier toto au niveau du shell bash, comme cela a été fait pour stdout. Mais c'est impossible (plantage garanti). Par contre nous pouvons procéder au niveau d'un script qui contiendra les commandes read, comme dans le script suivant (appelé readtoto):

#!/bin/bash

exec 4<&0 < toto # sauvegarde du lien vers le canal 0
                 # redirige stdin vers toto
sleep 10
read readvar
echo $readvar
read readvar
echo $readvar

exec 0<&4 4<&-   # retablit stdin et clôture fd/4

La commande sleep est là pour nous permettre d'examiner la situation depuis un autre shell bash. Procédons après avoir rendu le script exécutable (chmod +x readtoto):

echo $PPID nous a donné le pid du processus parent et pendant que le script était arrêté sur la commande sleep nous avons lancé la commande pstree (dans un autre shell).


La commande sleep tourne dans un processus à part, de même que les deux commandes read suivantes qui vont à chaque fois hériter des canaux de communication définis au niveau du processus 2569 (readtoto). Comme toto est ouvert une seule fois en début de script, les différentes lignes du fichier sont lues les unes après les autres.

Ouverture en lecture avec un descripteur
Il existe une autre façon de procéder pour arriver au même résultat: ouvrir toto en lecture en le reliant à un nouveau descripteur qui est alors créé et ajouter après chaque read une instruction redirigeant stdin vers le fichier lié à ce nouveau descripteur.



(La dernière commande est la commande de fermeture du fichier)

De nouveau le fichier est ouvert une seule fois ce qui nous permet de lire au delà de la première ligne.

Ouverture en écriture avec un descripteur
Nous ouvert le fichier toto en lecture via un descripteur.
Il est possible de procéder de la sorte pour ouvrir un fichier en écriture.
Ouvrons par exemple le fichier erreurs en écriture en l'affectant au descripteur 4.
Attention, de ce fait nous supprimons le contenu éventuel du fichier.
Nous commençons par écrire dans le fichier via une redirection de stdout vers le fichier affecté au descripteur 4, ensuite via une redirection de stderr:


On pourrait arriver au même résultat sans utiliser fd/4 et en remplaçant chaque >&4 par >> erreurs.

lundi 18 octobre 2010

stderr, stdout et redirections

En vue de réaliser quelques expériences, nous créons un dossier test puis nous nous y rendons:

[toto@rigel ~]$ mkdir test
[toto@rigel ~]$ cd test
[toto@rigel test]$

Définissons l'alias ll pour le cas où il n'existerait pas:

[toto@rigel test]$ alias ll='ls -l'
[toto@rigel test]$

Commençons les expériences:

[toto@rigel test]$ touch toto 
[toto@rigel test]$ ll toto aime titine
ls: ne peut accéder aime: Aucun fichier ou dossier de ce type
ls: ne peut accéder titine: Aucun fichier ou dossier de ce type
-rw-rw-r--. 1 toto toto 0 oct 17 11:11 toto
[toto@rigel test]$

Nous créons le fichier toto et demandons via la commande ll des informations sur les fichiers toto, aime et titine. L'ordre d'arrivée à l'écran des communications envoyées par ll est assez interpelant. On s'attendrait à trouver quelque chose comme:

-rw-rw-r--. 1 toto toto 0 oct 17 11:11 toto
ls: ne peut accéder aime: Aucun fichier ou dossier de ce type
ls: ne peut accéder titine: Aucun fichier ou dossier de ce type

ce qui correspond à l'ordre des arguments de la commande.
L'expérience montre que les messages arrivent sur l'écran par deux canaux différents dont l'un, celui par lequel passent les messages d'erreur, est cette fois prioritaire. Mais attention, ce n'est pas toujours le cas et si l'ordre naturel avait été respecté, on n'aurait rien pu en déduire.

Interceptons le flux sortant de ll par un pipe afin de le faire traiter par le programme tr. Le traitement consistera (par exemple) à remplacer tous les "t" par des "b".

[toto@rigel test]$ ll toto aime titine | tr t b
ls: ne peut accéder aime: Aucun fichier ou dossier de ce type
ls: ne peut accéder titine: Aucun fichier ou dossier de ce type
-rw-rw-r--. 1 bobo bobo 0 ocb 17 11:11 bobo
[toto@rigel test]$

Nous constatons que les messages d'erreur ne sont pas passés par la moulinette tr. Il y a bien deux canaux. Le canal par lequel transite l'output normal est la sortie standard (stdout). L'autre canal, pour les messages d'erreur est l'erreur standard (stderr). Le pipe à intercepté uniquement stdout.
Ce qu'il advient de l'output normal après l'exécution d'une commande est géré par le descripteur 1. A ce descripteur correspond un canal, le canal 1 (stdout). De même, ce qu'il advient des messages d'erreur est géré par le descripteur 2 auquel correspond le canal 2 (stderr).
On peut rediriger stderr vers stdout, comme ceci:

[toto@rigel test]$ ll toto aime titine 2>&1 | tr t b
ls: ne peub accéder aime: Aucun fichier ou dossier de ce bype
ls: ne peub accéder bibine: Aucun fichier ou dossier de ce bype
-rw-rw-r--. 1 bobo bobo 0 ocb 17 11:11 bobo
[toto@rigel test]$

L'instruction 2>&1 affecte au descripteur 2 le canal correspondant au descripteur 1.
En clair, stderr est redirigé vers stdout. Grâce au traitement par tr, on constate que cette fois, contrairement à ce qui se passait dans le cas précédent, la totalité des messages transite par le pipe.
Maintenant, nous aimerions trouver comment procéder pour que seul stderr transite par le pipe.
Tout d'abord, montrons que dans le cas de plusieurs redirections successives , l'ordre dans lequel elles interviennent est primordial:

[toto@rigel test]$ ll toto aime titine > info 2>&1
[toto@rigel test]$ cat info
ls: ne peut accéder aime: Aucun fichier ou dossier de ce type
ls: ne peut accéder titine: Aucun fichier ou dossier de ce type
-rw-rw-r--. 1 toto toto 0 oct 17 11:11 toto
[toto@rigel test]$

ce qui correspond au schéma:
L'instruction > info est mise pour 1> info (1 est le descripteur de fichier par défaut).
Elle affecte au descripteur 1 le fichier info: stdout est redirigé vers info.
Ensuite 2>&1 affecte au descripteur 2 le canal correspondant au descripteur 1. Ce canal a déjà été changé. Ce n'est plus le canal 1 initial. En fait de canal, il s'agit maintenant du fichier info:
stderr est à son tour redirigé vers le fichier info.
Rien n'arrive sur l'écran.
Permutons les deux redirections:

[toto@rigel test]$ ll toto aime titine 2>&1 > info
ls: ne peut accéder aime: Aucun fichier ou dossier de ce type
ls: ne peut accéder titine: Aucun fichier ou dossier de ce type
[toto@rigel test]$ cat info
-rw-rw-r--. 1 toto toto 0 oct 17 11:11 toto
[toto@rigel test]$

Le schéma correspondant est celui- ci:

2>&1 affecte au descripteur 2 le canal 1 qui est le canal initial correspondant au descripteur 1. Celui-ci n'a pas encore changé: stderr est redirigé vers ce canal 1. Ensuite > info (c'est-à-dire 1> info) affecte au descripteur 1 le fichier info: stdout est redirigé vers info. En fait la première redirection ne sert absolument à rien car de toute façon, stderr serait arrivé à l'écran. Oui mais cette fois stderr arrive sur l'écran via le canal 1, ce qui nous donne l'occasion de le faire transiter par tr, comme représenté sur ce schéma:




Essayons la formule:

[toto@rigel test]$ ll toto aime titine 2>&1 > info | tr t b
ls: ne peub accéder aime: Aucun fichier ou dossier de ce bype
ls: ne peub accéder bibine: Aucun fichier ou dossier de ce bype
[toto@rigel test]$ cat info
-rw-rw-r--. 1 toto toto 0 oct 17 11:11 toto
[toto@rigel test]$

Bingo, ça fonctionne! Cependant, on voudrait une solution sans redirection vers un fichier, on voudrait que tout arrive à l'écran, non seulement stderr mais aussi stdout. Pour cela nous avons besoin d'un nouveau descripteur de fichier: 3.

[toto@rigel test]$ echo toto >&3
-bash: 3: Mauvais descripteur de fichier
[toto@rigel test]$

Et bien oui: on veut rediriger stdout vers le canal 3, c'est-à-dire affecter au descripteur 1, le canal correspondant au descripteur 3. Mais ce descripteur n'existe pas, pas encore.

[toto@rigel test]$ exec 3>&1
[toto@rigel test]$

Maintenant, il existe:

[toto@rigel test]$ echo toto >&3
toto
[toto@rigel test]$

Remarquons que

[toto@rigel test]$ echo toto >&3 | tr t r
toto
[toto@rigel test]$

Alors que

[toto@rigel test]$ echo toto | tr t r
roro
[toto@rigel test]$

Nous avons notre solution:

[toto@rigel test]$ ll toto aime titine 2>&1 1>&3 | tr t b
ls: ne peub accéder aime: Aucun fichier ou dossier de ce bype
ls: ne peub accéder bibine: Aucun fichier ou dossier de ce bype
-rw-rw-r--. 1 toto toto 0 oct 17 11:11 toto

qui correspond au schéma:


jeudi 14 octobre 2010

Formulaires dans OOo Base

Dans le présent billet, nous allons nous intéresser à la création (et à l'utilisation) de formulaires dans OpenOffice.org Base (OOo Base). Pour ce faire, nous allons travailler avec la base de données postgreSQL créée par toto (voir billets précédents). On peut créer des formulaires qui servent uniquement à encoder de nouveaux enregistrements et d'autres qui servent à la consultation et à la mise à jour. Mais comment procéder pour afficher à l'écran l'enregistrement voulu? En effet: soit une table avec la clef primaire "matricule". Si nous saisissons un numéro de matricule dans le champ "matricule" du formulaire, OOo Base réagit comme si nous voulions mettre à jour le le matricule de l'enregistrement courant:
La  mise à jour n'a pas eu lieu car nous nous sommes connecté en tant qu'utilisateur aux droits limités, ce qui est toujours préférable. Le mieux est d'ailleurs en guise de protection supplémentaire de mettre le champ matricule du formulaire en "lecture seule".
Mais comment procéder alors? On peut ajouter au formulaire ce qui s'appelle un "contrôle de table":

Il suffit alors de cliquer sur le numéro de dossier souhaité pour l'amener instantanément à l'écran. Cela fonctionne très bien tant que le nombre d'enregistrements n'est pas trop important. Dans le cas contraire, faire défiler un grand nombre de matricules jusqu'à trouver celui qui nous intéresse peut s'avérer fastidieux. Dans le formulaire que nous allons construire nous utiliserons une technique basée sur l'utilisation d'une table intermédiaire, à une seule rangée qui sera constamment mise à jour.
D'autre part, il faut savoir que les différents éléments d'un formulaire sont regroupés dans un à plusieurs forms, chaque form étant lié à une et une seule table. L'assistant de création de formulaire permet seulement la création de formulaires à deux forms maximum. Nous verrons comment nous affranchir de cette limite.
Nous montrerons comment utiliser ListBox, ComboBox, Bouton radio etc...
Mais pour pouvoir utiliser au maximum les possibilités offertes, nous devons d'abord encore effectuer quelques aménagements dans notre base de données.
Encore quelques aménagements.
Tout d'abord, travaillant dans un terminal psql, créons la table services:
CREATE TABLE services
(service_id CHAR(2) PRIMARY KEY,
 dénomination CHAR(25))
;
Nous y mettons les données suivantes:
00    Direction
01    Secrétariat
02    Gestion du personnel
03    Relations publiques
04    Production
05    Recherches
06    Ventes 
figurant dans le fichier services par utilisation de la méta-commande:
toto=> \copy services from services
Reste à accorder le privilège qui convient:
toto=> grant select on services to public;
La table services va nous permettre de montrer l'utilité du contrôle "zone de liste" (ListBox).
Ajoutons également quelques colonnes à la table entrh:
ALTER TABLE entrh
ADD COLUMN service CHAR(2)
REFERENCES services
;
ALTER TABLE entrh
ADD COLUMN sexe CHAR(1)
CHECK (sexe IN ('M','F'))
;
ALTER TABLE entrh
ADD COLUMN naissance DATE
;
Les privilèges sur cette table ayant été accordés sur base des colonnes, nous devons les mettre à jour:
GRANT SELECT (service,sexe,naissance)
ON entrh TO public
;
GRANT UPDATE (service,sexe,naissance)
ON entrh TO gestionrh
;
Rappelons que le groupe gestionrh contient les utilisateurs habilités à traiter les données (et que ceux-ci n'ont pas le droit de modifier le matricule).
On peut aussi recréer la table entrh et accorder les privilèges adéquats en exécutant les commandes:
DROP TABLE entrh
;
CREATE TABLE entrh
(matricule SERIAL PRIMARY KEY,
 nom CHAR(50),
 adresse CHAR(50),
 code_postal CHAR(5),
 cp_seq SMALLINT,
 naissance DATE,
 sexe CHAR(1) CHECK (sexe IN ('M','F')),
 service CHAR(2) REFERENCES services,
 salaire numeric(13,2),
 FOREIGN KEY (code_postal,cp_seq) REFERENCES frcp)
;
ALTER SEQUENCE entrh_matricule_seq RESTART 1005
;
GRANT SELECT (matricule, nom, adresse, code_postal, cp_seq, naissance, sexe, service)
ON TABLE entrh TO PUBLIC
;
CREATE ROLE gestionrh
;
GRANT SELECT,INSERT ON entrh TO gestionrh
;
GRANT UPDATE (nom, adresse, code_postal, cp_seq, naissance, sexe, service, salaire)
ON entrh TO gestionrh
;
GRANT UPDATE ON SEQUENCE entrh_matricule_seq TO gestionrh
;              
Si ces commandes sont contenues dans le fichier Centrh-nd, il suffit de faire:
toto=> \i Centrh-nd
Bien sûr, les autres tables référencées doivent avoir été créées suivant les instructions qui figurent dans ce billet et dans le billet intitulé 'PostgreSQL initiation" .
Si le groupe gestionrh existe déjà, l'exécution des commandes du fichier produira une erreur sans conséquences. S'il vient juste d'être créé, il sera évidemment vide et il faudra y placer un où plusieurs gestionnaires.
Par exemple:
toto=> ALTER GROUP gestionrh ADD user titine
Pour travailler dans open office, il est préférable de se connecter à notre base de données en utilisant le nom d'un utilisateur du groupe gestionrh. De la sorte les changements que nous pourrions apporter aux données seront encadrés.

La requête Rcpdepreg
Dans le formulaire que nous allons créer, nous aimerions y voir figurer en clair, le nom de la localité et pourquoi pas aussi le nom du département et même de la région. Pour ne pas avoir à multiplier les sous-formulaires, nous allons créer, en mode ébauche, une requête que nous utiliserons dans le formulaire.
On ajoute les tables frcp, frdepartem,  frreg. Les liens s'établissent automatiquement:

Reste à choisir les colonnes figurant dans la requête:

et à enregistrer la requête sous le nom Rcpdepreg.
Voilà, nous sommes prêts à entamer la création de notre formulaire de gestion de la table entrh.


Le formulaire entrh
Utilisons l'assistant de création de formulaires
A l'étape N°1 choisissons la table entrh et sélectionnons tous les champs disponibles pour être inclus dans le formulaire.

A l'étape N°2 cochons "Ajouter un sous-formulaire"

A l'étape N° 3 sélectionnons la requête Rcpdepreg. Nous ne prenons pas code_postal et cp_seq en tant que champ du formulaire: inutile qu'ils apparaissent deux fois.
A l'étape N°4: l'assistant ne permet pas l'établissement des jointures car code_postal et cp_seq n'ont pas été repris dans l'étape précédente:
Nous établirons la jointure plus tard.
A l'étape N°5, notre choix s'effectue comme montré ci-dessous:

A l'étape N°6  cochons "Ne pas autoriser la suppression des données existantes".

Ensuite nous gardons les options par défaut. Nous créons le formulaire en demandant de le modifier avant toute utilisation:

En priorité, il convient d'établir un lien entre les deux parties du formulaire, ce qui n'a pas été possible dans l'assistant. Ouvrons le navigateur de formulaires. On le trouve dans la barre d'outils horizontale en bas de la fenêtre de travail:
Cliquons droit sur SubForm et par la voie du menu contextuel qui surgit, affichons ses propriétés. Dans l'onglet "Données", nous avons alors la possibilité d'établir le lien voulu en appuyant sur le bouton aux 3 petits points qui se trouve à droite de l'item "Établir un lien depuis":
SubForm correspond à des données qui ne sont pas censées être modifiées: code postaux, noms des localités etc. Aussi profitons que nous avons à l'écran les propriétés de SubForm, pour indiquer que nous ne voulons pas d'ajouts et pas de modifications (pas de suppression est déjà mentionné):
Affichons maintenant depuis le navigateur de formulaire les propriétés de MainForm afin de pouvoir trier les enregistrements suivant le matricule. On procède comme précédemment, appui sur le bouton aux trois petits points:

Occupons nous maintenant des différents champs.
Cliquons sur la zone de saisie salaire, CTRL étant enfoncé, de manière à la sélectionner sans son étiquette. Dans le menu contextuel obtenu par clic droit, choisissons "Remplacer par", puis "Champ monétaire":
Ensuite dans la fenêtre "Propriétés" (obtenue via menu contextuel puis "Contrôle"), demandons pour avoir un séparateur de millier:
 Nous avons même la possibilité de placer le symbole monétaire (€) devant le nombre.
(Dans le billet précédent, nous avons indiqué une autre méthode pour formater le champ salaire)
En ce qui concerne la donnée sexe, nous allons utiliser le contrôle "bouton radio".
On le trouve avec les autres contrôles dans la barre d'outils verticale de gauche:

Supprimons la zone de saisie sexe et plaçons en dessous de son étiquette deux boutons radio côte à côte. Nous les configurons via le panneau de leurs propriétés: étiquette et valeur référentielle: M pour un, F pour l'autre:


Supprimons également la zone de saisie service et mettons à la place une zone de liste (ListBox). Pourquoi ne pas utiliser "Remplacer par" ? Et bien parce que dans ce cas l'assistant ne s'ouvre pas. Ici l'assistant s'ouvre dès que nous avons terminé de dimensionner notre zone de liste. Voici en images les différentes étapes:


 
En ce qui concerne le champ matricule, n'oublions pas de le mettre en lecture seule car nous avons prévu une génération automatique des matricules:

Plaçons sur le formulaire un bouton appartenant à SubForm: étiquette "Actualiser localité" et
action "Rafraîchir le formulaire". Pour vérifier l'appartenance du bouton, on le sélectionne, le navigateur de formulaire étant ouvert. SubForm devrait alors se déployer:

Si ce n'est pas le cas, il suffit de repérer l'icône "PushButton" dans le navigateur de formulaires et de faire un "glisser-déposer" sur l'icône SubForm".
Ce bouton est utile pour rafraîchir l'écran en cas de modification du code postal et (ou) de la séquence qui suit.
Nous disposons et redimensionnons les différents champs du formulaire à notre guise, modifions les étiquettes.
Il reste à adapter l'ordre d'activation.
On trouve l'outil qui convient dans le bas de la fenêtre de travail:



Tri automatique devrait donner un résultat qui convient:

Lors de chaque appui sur "Enter", le curseur sautera d'une zone à l'autre suivant l'ordre indiqué ici. Donc si on commence par naissance, le curseur ira ensuite sur salaire, puis service (c'est la ListBox), puis nom etc. A la fin du cycle, les données sont enregistrées puis on passe à l'enregistrement suivant.
Sauvegardons notre travail avant d'ouvrir le formulaire pour utilisation. On devrait pouvoir obtenir quelque chose qui ressemble à ceci:

C'est une première étape, mais le travail est loin d'être fini. Nous aimerions que l'encodeur puisse disposer d'une liste donnant les diverses possibilités de localités pour un code postal donné. Pour ce faire nous allons ajouter un nouveau sous-formulaire à notre formulaire. Rappelons que l'assistant permet de créer des formulaires avec seulement un sous-formulaire.
Dans le navigateur de formulaire, cliquons droit sur SubForm, ensuite dans le menu contextuel nous choisissons Nouveau, puis Formulaire:
Nous renommons le sous-formulaire créé en CpForm. Cliquant droit dessus nous arrivons à la fenêtre Propriétés. Le contenu de CpForm viendra de la table frcp. Le lien avec SubForm sera basé sur code_postal. Et bien sûr, pas d'ajouts, modifications, suppressions:
Nous imposons également un tri sur cp_seq:
Cliquer sur "Autres contrôles" dans la barre d'outils verticale, ouvre une nouvelle boite d'outils où se trouve le contrôle "Contrôle de table":

Déployons ce contrôle dans la partie inférieure du formulaire. Un assistant s'ouvre dont le rôle sera très bref: il permet juste de choisir les colonnes à afficher:

Ajustons les propriétés du contrôle: pas de Barre de navigation et pas de Marqueur d'enregistrement:
Cliquer droit sur l'entête de la colonne et choisir colonne dans le menu contextuel, permet d'afficher les propriétés de la colonne: 
Pour la colonne cp_seq, on veillera à mettre le nombre de décimales à zéro:
Plaçons également dans la partie MainForm du formulaire, un autre contrôle de table avec une seule colonne: matricule. On ajuste ses propriétés comme pour le contrôle de table précédent. Utilisant le formulaire après une sauvegarde des modifications, on obtient quelque chose qui pourrait ressembler à:
Mais comme déjà indiqué au début de ce billet, la sélection des enregistrements par un contrôle de table tel que celui qui figure ici (en haut à droite), n'est pas vraiment un bon choix.
Aussi nous allons garder ce formulaire uniquement pour encoder de nouveaux enregistrements en modifiant les propriétés de MainForm:

"N'ajouter que des données" est basculé sur Oui.
De la sorte, seuls seront visibles les enregistrements encodés pendant la session en cours.

La table "Trucs et astuces" selection.
En vue de sélectionner les enregistrements à afficher nous allons utiliser une table intermédiaire que nous appellerons selection. Cette table ne contiendra jamais qu'une seule rangée avec deux colonnes: une colonne id, et une colonne qui contiendra le matricule du dossier sélectionné.. Procédons à la création de la table selection dans un terminal psql et exécutons ensuite les instructions SQL qui conviennent:


On peut se poser la question: pourquoi une clef primaire?
Et bien parce que en l'absence de celle-ci, la table s'ouvre en read only dans openoffice base!
La technique d'utilisation de la table selection est la suivante: un formulaire peut contenir plusieurs forms, chacun étant associée à une et une seule table. Le form principal sera associé à la table selection. La table que l'on veut gérer sera associé à un form secondaire lié au form principal par le champ matricule. A chaque sélection de fichier, la colonne matricule de la table selection sera mise à jour, et ensuite l'enregistrement correspondant de entrh sera affiché..

Le formulaire entrh pour consulter et mettre à jour
Bien sûr nous n'allons pas repartir de rien. Faisons une copie du formulaire précédant que nous collons sous un autre nom.
Basculons sur Non dans MainForm "N'ajouter que des données" et "Autoriser les ajouts" et supprimons le Contrôle de table sur matricule.
Nous allons ajouter au formulaire un form maître duquel va dépendre tous les autres et dont la table selection constituera le contenu.
Cliquons droit sur Formulaire dans le navigateur et créons un nouveau formulaire:
En fait nous créons un nouveau form (appelé Standard par défaut) qui est sur un pied d'égalité avec MainForm:
Faisons un glisser-déposer de MainForm dans Standard de manière à arriver à ceci:

MainForm porte maintenant bien mal son nom, mais qu'importe!
Reste à fixer les propriétés de Standard: Table selection, Ajouts Non, Suppressions Non:
Il faut encore lier Standard à MainForm:
Plaçons là où se trouvait le contrôle de table qui a été supprimé une boite combinée (Combox). Si Standard est sélectionnée dans le navigateur, cette boite combinée devrait lui appartenir. Lorsqu'on a fini de la dimensionner l'assistant s'ouvre. La table cible est selection, table du sous-formulaire Standard: on n'a pas le choix. La table source sera entrh:
le champ d'affichage entrh.matricule:
qui sera enregistré dans selection.matricule:
On termine en ajustant les propriétés de la zone combinée.
"Déroulante" Non:
Tri sur matricule:
On peut déjà utiliser le formulaire: il suffit après avoir choisi un dossier dans la boite combinée de cliquer sur "Rafraîchir le contrôle", puis "Actualiser":

Le mieux est de remplacer ces deux actions par un appui sur un seul bouton qui sera ajouté à MainForm: étiquette "Valider", action "Rafraîchir le formulaire". Il est important de vérifier que ce bouton fait bien partie de MainForm et non pas de Standard.
Et voilà:
L'utilisation de la boite combinée est la suivante: on commence à taper le matricule souhaité dans la zone de saisie supérieure et dès que celui-ci apparaît dans la liste on clique dessus plutôt que de continuer à le taper, ce qui peut être très intéressant dans le cas d'un matricule à rallonge (par exemple 12 caractères).