Mecredi 30 novembre 2022
Par Sylvain Chiron, Université de Bordeaux, Licence 3 Informatique groupe A42
Les scripts hébergés sur ce site sont sous licence AGPL v3.
Téléchargez l’archive du projet
Cliquez sur le nom du fichier source en haut des extraits de code pour voir le fichier complet.
Utilisez les flèches ← et → pour changer de diapositive.
En l’absence de clavier, cliquez sur le titre pour faire apparaitre des boutons fléchés.
-Wall
qui permet de repérer immédiatement de nombreuses erreurs.-Wwrite-strings
et -Wcast-qual
augmentent l’intérêt de const
.-ll
par -lfl
pour l’édition de liens avec Flex
(visiblement plus portable).
$var
, var=
),
des commandes entre accents graves (`cmd`
) et des métacaractères (*?
).
Comme vous le verrez dans cette présentation, j’ai modifié tous les fichiers du projet. Je n’en ai épargné aucun.
Objectif principal des modifications : permettre à l’analyseur d’accepter davantage de caractères sans altérer sa visée initiale de fournir des paramètres prêts à être utilisés.
YY_NO_UNPUT
et YY_NO_INPUT
pour éviter deux avertissements inutiles à propos de fonctions inutilisées.
COMMENT
: absorption des #
en début de paramètre et de tout ce qui les suit pour les ignorer.
ARG_NOQUOTES
: des caractères autres que \n\t `"'<>&|;()\
, ainsi que
des \
(caractères d’échappement) chacun suivi par un caractère quelconque ;
ARG_BACKTICKS
: une suite de caractères entre accents graves ;ARG_DBLQUOTES
: une suite de caractères entre guillemets doubles ("
),
comprenant éventuellement des guillemets du même type échappés par \
,
et dans lesquels les \
doivent être échappés ;
ARG_SINGLEQUOTES
: une suite de caractères entre guillemets simples ('
),
dans laquelle il n’est pas nécessaire d’échapper les caractères
(mais qui ne peut pas comprendre de '
).
&
à la fin).yy_create_buffer
,
il faut appeler yy_delete_buffer
(je l’ai identifié avec valgrind
).
L’analyseur d’origine n’acceptait que deux formes de paramètres (même si on les collait, c’était considéré comme plusieurs paramètres) :
-.$%=/\*?
, les 26 lettres majuscules et minuscules ASCII et
les chiffres de 0 à 9 ;
Remarques (défauts par rapport à Bash) :
`...`
dans un paramètre ;$
(donc impossible de faire $(cmd)
et $(( calcul ))
) ;
$
et =
et
les métacaractères *
et ?
seront toujours interprétés ;\
sont toujours considérés comme des caractères d’échappement (même entre guillemets),
et sont donc supprimés à moins d’être précédés d’un autre \
;
\\\"
à la place de \"
) ;
<<
(mais on peut utiliser cat |
) ;&>&
(ça, ce n’est pas un défaut).Faites vos tests ! Mettez en forme vos paramètres et regardez ce qui apparait entre crochets. Puis comparez avec le comportement de Bash.
echo # abc echo# echo \# "#" echo "#" echo \ echo \\ echo 1 2 1\ 2 "1 2" 1" "2 echo ` echo " echo "`" "\"" echo `"` `\` `\"` echo \\\"\& '\\"&' ...
;
afin qu’il ait une priorité plus basse
que tous les autres opérateurs (comportement identique à celui de Bash).
À l’origine, il avait une priorité inférieure à &
et identique à &&
et ||
.
Ainsi la ligne :
echo; echo && echo &était interprétée comme suit :
((echo; echo) && echo) &et est maintenant interprétée comme :
echo; ((echo && echo) &)Je me suis rendu compte du problème après avoir codé la fonction
linearizeExpr
du module Display,
qui fait apparaitre les parenthèses.
argsList
et de singleArg
pour pouvoir les allouer dynamiquement
(et ainsi dépasser les limites de 50 paramètres par commande et 500 caractères par paramètre).
Expression
.
La macro alloc
appelle malloc
et memcpy
.
Défauts par rapport à Bash : les redirections de descripteurs de fichiers sont évaluées de droite à gauche (comme s’il y avait des parenthèses) et doivent forcément être après tous les paramètres (alors que Bash les accepte n’importe où dans la liste des paramètres).
Expérience : essayez echo; echo && echo &
sans puis avec la modification de priorité de ;
.
Dans l’entête :
shell
(de type Shell
) regroupe quelques paramètres
liés au shell au premier plan, notamment les paramètres fournis au programme.
Ces données sont parfois modifiées temporairement, notamment lors de l’utilisation de la commande source
.
yyparse_string
est déclarée (utilisée par InternalCommands et ArgsParsing).
commandExecution
contient les instructions de la boucle principale
(elle est appelée par la commande source
).
interactiveMode
est déclaré (utilisé par Proc).readline
.Dans le fichier source :
readline
:
shift
) ;
isatty(STDIN_FILENO)
;sigaction
).commandExecution
. Elle comporte quelques nouveautés :
evaluationFinished
est appelée (elle vérifie les tâches de fond) ;
J’ai déplacé dans ce module ce qui concerne le type Expression
.
REDIR_OUT
).
ArgsList
permet de gérer les listes de paramètres plus efficacement :
le nombre de paramètres est précalculé dans le champ len
et
la taille allouée est stockée car on alloue toujours des gros blocs.
argc
et argv
sont des synonymes de argsList.len
et argsList.args
.
redirect
fournit toutes les informations nécessaires pour une redirection.Dans l’entête :
isMainProc
est déclaré : il est à true
dans le processus principal,
tous les processus enfants l’initialisent à false
.
Dans le fichier source :
SIGINT
)
et d’ignorer les arrestations (Ctrl+Z, SIGTSTP
;
signaux du TTY : SIGTTIN
, SIGTTOU
).
Puis on isole le processus du shell dans son propre groupe de processus (setpgid
),
et on donne à ce groupe le contrôle du terminal (tcsetpgrp
) :
readline
,
on redémarre juste avant readline
(fuite de mémoire ?).
Sinon, on lève un drapeau (isInterrupted
).
checkChild
, mais seulement pour le processus principal
et seulement en mode interactif. Dans les autres cas, on demande au processus enfant de reprendre immédiatement.
Remarque : la fonction peut prendre en paramètre soit une commande (son texte) soit une expression.
SIGINT
,
wait
renvoie -1 et met la valeur EINTR
dans la variable globale errno
,
et le processus demeure en tant que zombie. Dans ce cas, on continue bien évidemment les wait
.
(Dans les autres cas où wait
renvoie -1, elle donne une autre valeur à errno
.)
currentJob
est une variable utilisée pour stocker quelques informations
sur la dernière tâche au premier plan.
endOfChild
exécute waitpid
aussi mais ce n’est pas un problème.
Si on est dans le processus principal, alors on met le processus enfant dans son propre groupe
et on lui donne le terminal, puis le shell reprend le contrôle du terminal à la fin.
Défaut par rapport à Bash : envoyer SIGTERM
sur un processus enfant stoppé
n’arrête pas ce dernier immédiatement.
Essayez de lancer des commandes de toutes sortes et de les interrompre ou les stopper avec Ctrl+C ou Ctrl+Z.
Dans l’entête :
Dans le fichier source :
BG
, on vérifie si le processus lancé en arrière-plan
s’est terminé avant la fin de l’exécution d’addJob
;
la fonction addJob
prend une description de la commande en paramètre :
SIMPLE
est nettement complexifié (et donc porte très mal son nom) :
$
) par la valeur de celles-ci
dans les paramètres (putEnvValues
).
``
),
pour la mettre à la place de ces commandes (putCmdsOutput
).
putCmdsOutput
), on quitte immédiatement
(toujours en renvoyant 130).
findCommandFct
). Si oui, on exécute cette commande (sans fork
) et on quitte.
On fournit à la commande une copie des pointeurs vers les paramètres, et non pas directement ces pointeurs
au cas où ils seraient modifiés (ce qui peut arriver si un fichier lu par la commande source
utilise la commande shift
, et aurait pour conséquence que la mémoire utilisée
pour stocker les paramètres ne serait pas libérée).
kill
, on remplace les désignations de tâches (%1, %2, %3…)
par l’identifiant du processus (putJobPidsInArgs
).
waitForChild
.PIPE
, on met la tâche dans un fork
principal, et dedans,
on fait un fork
pour chaque côté du tube (pour pouvoir gérer leur arrêt et leur reprise),
puis on attend la fin des deux et on renvoie le code de retour du deuxième (la fonction de traitement
de SIGCHLD
dans le fork
principal est éliminée pour éviter les ennuis) :
defaultFD
).
Si l’opérateur de redirection commence par &
, elle rajoute STDERR_FILENO
.
Si l’opérateur commence par un nombre, c’est le descripteur de fichier correspondant qui est redirigé.
SIMPLE
).
&
,
le nom de fichier est interprété comme un numéro de descripteur de fichier.
Si l’ouverture du fichier échoue, déclaration d’erreur et abandon.
fork
et on termine de façon classique.
Attention au cas où le numéro de descripteur de fichier utilisé pour ouvrir le fichier est le numéro à rediriger.
isInterrupted
.
Sans la fonction evaluationFinished
, on pouvait mettre ces instructions à la fin
d’evaluateExpr
en les conditionnant par expr == shell.parsedExpr
.
Défauts par rapport à Bash :
fork
sont effectués, de sorte qu’en cas de Ctrl+Z, ce soit vraiment toute la commande
qui soit stoppée. Bash semble ne faire de fork
que pour le cas SIMPLE
,
et chaque processus enfant commence par se configurer en fonction de l’expression qui l’entoure ;
cette configuration est donc enregistrée par Bash pour être appliquée au dernier moment,
tandis que Mini-Shell n’enregistre rien, il applique directement.
&; fg
en fin de commande.
Amusez-vous ! Regardez ce qui se passe quand vous exécutez les commandes suivantes, et quand vous les interrompez ou les stoppez, ou les exécutez en arrière-plan.
ls | (sleep 3 && cat) | cat -n echo >& 3 3> file.txt ...
J’ai déplacé les fonctions de gestion des tâches de fond dans ce module.
Dans l’entête :
Job
, ajout de trois champs :
currentJob
est prévue pour être utilisée comme tampon à divers endroits
(avant, il y avait juste un tampon pour la valeur de retour) :
jobStateTexts
) :
addJob
prend en charge de nouvelles informations et
affiche le numéro, l’identifiant de processus et le texte de la commande de la tâche nouvellement ajoutée
(seulement dans le processus principal, hors duquel les tâches de fond ont toutes le numéro 0
pour être différenciées) :
%
)
par leur identifiant de processus (utilisée pour la commande kill
) :
Différences par rapport à Bash :
addJob
) et une indiquant l’état « Stopped » ;
Il y a bien entendu des défauts par rapport à Bash dans la linéarisation des expressions :
les parenthèses ne sont plus telles quelles
(elles sont rajoutées artificiellement pour le respect de l’ordre des opérations reçu)
et les caractères d’échappement (barres obliques inversées et guillemets : \"'
) ne sont pas remis.
Dans l’entête :
main
.
Dans le fichier source :
chdir
.
Une fois que c’est fait, la commande cd
met à jour la variable PWD
en exécutant une commande.
source
, qui exécute le contenu d’un script dans la session :
La fonction commence par ouvrir le fichier donné en paramètre et par remplacer les paramètres de programme globaux
par les paramètres locaux. Ensuite, elle lit des lignes de taille arbitraire (en doublant la taille de son tampon
chaque fois que c’est nécessaire) et les exécute grâce aux mêmes appels que dans la boucle principale
de la fonction main
du module Shell. À la fin, les anciens paramètres globaux ainsi que
la précédente expression analysée sont restaurés, et le fichier est fermé.
Quelques différences par rapport à Bash :
read
(mais à la place de read var
,
on peut simplement écrire var=`cat`
) ;
echo
, true
, false
,
test
/[
, pwd
;
on peut voir la différence en tapant par exemple echo --version
),
même s’il était explicitement proposé dans le sujet de recoder echo
.
shift
affiche plus de messages d’erreurs que celle de Bash
(cas de 0 et de nombres trop grands).
bg
traite toujours les tâches de fond dans l’ordre de leurs numéros,
pas dans l’ordre dans lequel ils sont indiqués dans les paramètres (afin d’éviter un copier-coller
comme c’est fait pour jobs
). De plus, bg
sans paramètre
traite toutes les tâches de fond (alors que Bash maintient une pile pour savoir quelle tâche traiter par défaut).
fg
n’accepte pas les paramètres inutiles. De plus, en l’absence de paramètre,
elle prend simplement la dernière tâche de fond (alors que Bash maintient une pile pour savoir
quelle tâche traiter par défaut).
0
.
%
comme préfixe des numéros de tâche.
Maintenant, il est temps de s’amuser avec ces commandes ! Mini-Shell n’affiche pas automatiquement
le répertoire de travail ; vous devez utiliser la commande pwd
pour obtenir le chemin.
On peut voir dans l’entête que le module propose quatre fonctions :
l’une (setEnvValues
) lit dans les paramètres des instructions pour modifier l’environnement,
les trois autres transforment les paramètres.
La fonction putEnvValues
remplace les désignations de variables (commençant par $
)
par les valeurs de ces variables. Les variables spéciales ?
, #
, $
,
*
et @
sont prises en charge ; leur contenu est calculé la première fois qu’elles sont vues
dans le texte du paramètre. À part ça, les paramètres fournis au shell, à indices numériques, sont pris en charge.
Et les variables d’environnement classiques sont prises en charge, grâce à la fonction getenv
.
La fonction accepte que le nom de la variable soit entouré d’accolades.
La présence d’espaces dans la valeur d’une variable ne conduit pas à découper le paramètre
(c’est équivalent au comportement de Bash lorsqu’on met la désignation de la variable entre guillemets doubles).
La fonction putCmdsOutput
remplace les commandes entre accents graves par la sortie de ces commandes.
Une seule commande est exécutée à la fois, elle peut être interrompue mais pas stoppée (comportement identique
à celui de Bash). Seulement 1023 caractères au maximum sont récupérés. La présence d’espaces dans la sortie
ne conduit pas à découper le paramètre (c’est équivalent au comportement de Bash lorsqu’on met les accents graves
et la commande entre guillemets doubles).
La fonction setEnvValues
est la plus courte du module : elle parcourt les paramètres
tant que chaque paramètre vu commence par un nom de variable correct suivi du signe =
.
Elle renvoie le nombre de paramètres ainsi lus.
Enfin, la fonction expandWildCards
utilise la fonction récursive addCorrespondingPaths
pour remplacer les métacaractères *
et ?
par les noms de fichiers/dossiers
qui correspondent au motif, s’il y en a. Cette fois, le nombre de paramètres augmente potentiellement,
c’est pourquoi la fonction prend en paramètre la liste de tous les paramètres, et renvoie une toute nouvelle
ArgsList
. La fonction fnmatch
vue au TD 2 est utilisée.
Si un paramètre génère plusieurs paramètres, ces derniers sont triés lexicographiquement
(sinon, ils sont ordonnés n’importe comment).
Quelques défauts par rapport à Bash :
[]
, les accolades {}
et les tildes ~
ne sont pas traités de façon particulière.
À l’attaque !
var=truc echo $var var=`echo A` echo $var* echo $$ $# $* cat > truc.sh echo $$ $# $* echo $var2 [Ctrl+D] var2=machin . truc.sh a b c var2=machin sh truc.sh -s d e f var2=machin ./Shell g h i < truc.sh cat | var2=machin sh -s j k l echo $var2 $# $* [Ctrl+D] ...
C’est la fin ! Merci ! N’hésitez pas à poser des questions et à rapporter des anomalies !