├── .gitignore ├── Makefile ├── conf.py ├── execution.rst ├── index.rst ├── interactions.rst ├── lexer.rst ├── parser.rst └── structure.rst /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *~ 3 | \#*\# 4 | _build 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = shell-implem-tips 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Serve _build/html with Python built-in server 18 | serve: html 19 | cd $(BUILDDIR)/html && python3 -m http.server 20 | 21 | deploy: html 22 | scp -r $(BUILDDIR)/html/* multun@multun.net:/srv/www/shell.multun.net/ 23 | 24 | # Catch-all target: route all unknown targets to Sphinx using the new 25 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 26 | %: Makefile 27 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 28 | -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # camisole documentation build configuration file, created by 5 | # sphinx-quickstart on Wed Jan 11 21:56:30 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | import sys 35 | import pathlib 36 | root = pathlib.Path(__file__) 37 | sys.path.insert(0, str(root.parent)) 38 | sys.path.insert(0, str(root.parent.parent)) 39 | extensions = [ 40 | 'sphinx.ext.autodoc', 41 | 'sphinx.ext.viewcode', 42 | ] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = '.rst' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = 'shell-implem-tips' 58 | copyright = '2019, Victor Collod' 59 | author = 'Victor Collod' 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = '1.0' 67 | # The full version, including alpha/beta/rc tags. 68 | release = '1.0' 69 | 70 | rst_epilog = '.. |project| replace:: *%s*' % project 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | # 75 | # This is also used if you do content translation via gettext catalogs. 76 | # Usually you set "language" from the command line for these cases. 77 | language = None 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | # This patterns also effect to html_static_path and html_extra_path 82 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # If true, `todo` and `todoList` produce output, else they produce nothing. 88 | todo_include_todos = False 89 | 90 | 91 | # -- Options for HTML output ---------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | # 96 | html_theme = 'alabaster' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | # 102 | # https://alabaster.readthedocs.io/en/latest/customization.html#theme-options 103 | html_theme_options = { 104 | "show_relbars": True, 105 | "fixed_sidebar": True, 106 | "github_user": "multun", 107 | "github_repo": "shell-implem-tips", 108 | "github_button": True, 109 | "github_type": "star", 110 | # "description": "A Python package for assisting neural network production with TensorFlow.", 111 | } 112 | 113 | html_show_sourcelink = False 114 | # html_sidebars = { '**': ['globaltoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'], } 115 | 116 | # Add any paths that contain custom static files (such as style sheets) here, 117 | # relative to this directory. They are copied after the builtin static files, 118 | # so a file named "default.css" will overwrite the builtin "default.css". 119 | # html_static_path = ['_static'] 120 | 121 | # -- Options for HTMLHelp output ------------------------------------------ 122 | 123 | # Output file base name for HTML help builder. 124 | htmlhelp_basename = 'camisoledoc' 125 | 126 | 127 | # -- Options for LaTeX output --------------------------------------------- 128 | 129 | latex_elements = { 130 | # The paper size ('letterpaper' or 'a4paper'). 131 | # 132 | # 'papersize': 'letterpaper', 133 | 134 | # The font size ('10pt', '11pt' or '12pt'). 135 | # 136 | # 'pointsize': '10pt', 137 | 138 | # Additional stuff for the LaTeX preamble. 139 | # 140 | # 'preamble': '', 141 | 142 | # Latex figure (float) alignment 143 | # 144 | # 'figure_align': 'htbp', 145 | } 146 | -------------------------------------------------------------------------------- /execution.rst: -------------------------------------------------------------------------------- 1 | Exécution 2 | ========= 3 | 4 | Maintenant que vous avez un AST, il faut en faire quelque chose. 5 | Il vous faut parcourir récursivement votre AST, en effectuant les actions associées pour chaque nœud. 6 | 7 | - L'exécution d'un nœeud retourne un entier, qui représente le succès de l'opération s'il vaut zéro, 8 | un échec autrement. Vu que vous n'exécuterez toujours qu'un nœuds à la fois, vous pouvez en faire 9 | une valeur globale. 10 | 11 | - Certaines de ces instructions, comme ``mavariable=valeur`` modifient des variables shell. Il vous faut 12 | alors modifier la table de hash associant le nom de la variable à sa valeur en conséquence. 13 | 14 | - Certaines chaines de caractère, au contraire, vont accéder à ces variables durant le processus 15 | d'expansion, détaillé plus tard. 16 | 17 | L'expansion 18 | ----------- 19 | 20 | L'expansion, c'est le processus qui permet de passer d'une chaine de caractère non traitée, comme 21 | ``"$mavariable"`` à sa version finale, cad ``valeurdemavariable``. 22 | 23 | Ce processus implique entre autres : 24 | 25 | - le traitement particulier des différents types d'échapement (``'"``) 26 | - le parsing des subshells (``$()``) 27 | - le parsing de l'expansion arithmétique (``$(())``) 28 | - l'expansion de variables (``$mavariable``) 29 | - la découpe du résultat avec l'IFS, dans les portions du mot où c'est applicable. 30 | par exemple: ``a='un test'; printf 'mot: %s\n' 'ceci est'$a`` 31 | mais ``a='un test'; printf 'mot: %s\n' 'ceci est'"$a"`` 32 | 33 | Il s'agit en fait de parcourir les mots comme lors du lexing, mais en les traduisant cette fois-ci 34 | en opérations concrètes. 35 | 36 | Deux solutions possibles: 37 | 38 | - Vous devez faire la même chose que le lexer, mais celui-ci n'est pas assez modulaire ? Copié collé, modifié, et hop ! 39 | - Si vous avez suivi la méthode propre du lexer, vous devriez avoir une base commune permettant d'implémenter l'expansion sans trop de diffiicultés. 40 | 41 | Les command substitutions 42 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 43 | 44 | Les substitutions de commande se présentent sous deux formes: 45 | 46 | - :literal:`echo \`subshell\`` 47 | - :literal:`echo $(subshell)` 48 | 49 | Analyse 50 | ####### 51 | 52 | L'exécution de la première forme ne requiert pas de précaution particulière. Lorsque le premier backtick est rencontré, on cherche simplement celui de fin. 53 | La deuxième forme est plus complexe à délimiter. Il faut relancer un lexeur à partir du ``$(`` ouvrant, lancer le subshell avec comme donnée le résultat du lexing, et recommencer l'expansion là où il s'est terminé. 54 | 55 | Exécution 56 | ######### 57 | 58 | On crée un pipe, on fork, on attache la sortie standard du fils dans l'entrée du pipe et on lit dans la sortie avec le parent. 59 | Le fils parse, lexe et exécute ensuite le contenu du subshell, dans un boucle. Il est important d'exit et de ne pas continuer d'exécuter l'ast dans le fils une fois terminé. 60 | Le parent lit les données du fils et les rajoute dans le résultat d'expansion. Une fois la lecture terminée, il wait le process et continue l'expansion. 61 | 62 | L'expansion arithmétique 63 | ~~~~~~~~~~~~~~~~~~~~~~~~ 64 | 65 | L'expansion arithmétique du shell est définie comme suivant la spécification C. Il faut construire un mini lexeur et un mini parseur pour évaluer ces sections. 66 | Attention, il n'est pas nécessaire de faire un AST! tout peut être évalué au fur et à mesure si on utilise le bon type de parseur. 67 | 68 | `Les parseurs de pratt s'y prêtent particulièrement bien `_. 69 | 70 | Les commandes 71 | ------------- 72 | 73 | Les commandes sont composées de trois éléments: 74 | 75 | - des assignations 76 | - des arguments 77 | - des redirections 78 | 79 | Lors du parsing, ces trois types d'éléments sont ajoutés dans trois tableaux. 80 | Lors de l'exécution, ces éléments sont traités dans l'ordre suivant: 81 | 82 | - les redirections sont effectuées de gauche à droite 83 | - les arguments de la commande sont expand (``echo $var`` est transformé en ``echo varcontent``) 84 | - les assignations sont effectuées (après le fork si il existe, tant pis sinon) 85 | - la commande est exécutée 86 | - les redirections sont annulées 87 | 88 | Les redirections 89 | ---------------- 90 | 91 | `Conférences Redirection et Pipe 42SH - 2019 `_ 92 | 93 | .. code-block:: none 94 | 95 | terminal 96 | 97 | + ^ ^ 98 | +--------+---+ | | | 99 | | | 0 +<-----+ | | 100 | | +---+ | | 101 | | | 1 +----------+ | 102 | | sh +---+ | 103 | | | 2 +--------------+ 104 | | +---+ 105 | | | … | 106 | +--------+---+ 107 | 108 | 109 | Le schéma ci-dessus représente un scénario typique d'exécution d'un shell. Celui-ci lit ses commandes depuis stdin (file descriptor 0), écrit normalement sur stdout (file descriptor 1), et sur stderr (file descriptor 2) en cas d'erreur. 110 | 111 | Chaque process dispose d'un tableau de pointeurs vers des ressources. Ce tableau n'étant pas directement accessible, on utilise à la place l'index dans ce tableau. Cet index est appelé file descriptor. 112 | 113 | Après un ``open("monfichier")``, ce tableau aura été transformé comme suit: 114 | 115 | .. code-block:: none 116 | 117 | terminal 118 | 119 | + ^ ^ 120 | +--------+---+ | | | 121 | | | 0 +<-----+ | | 122 | | +---+ | | 123 | | | 1 +----------+ | 124 | | sh +---+ | 125 | | | 2 +--------------+ 126 | | +---+ +------------+ 127 | | | 3 +------------------->+ monfichier | 128 | +--------+---+ +------------+ 129 | 130 | On peut effectuer différentes opérations sur ce tableau: 131 | 132 | - ``close(fd)`` supprime un lien 133 | - ``dup(fd)`` fait une copie du lien 134 | - ``dup2(oldfd, newfd)`` fait une copie de oldfd, et la met à l'index de newfd. si la case est déjà prise, l'ancien fd est close. 135 | 136 | Après un ``dup2(3, 2)``, le nouvel état sera: 137 | 138 | .. code-block:: none 139 | 140 | terminal 141 | 142 | + ^ 143 | +--------+---+ | | 144 | | | 0 +<-----+ | 145 | | +---+ | 146 | | | 1 +----------+ 147 | | sh +---+ 148 | | | 2 +------------+ 149 | | +---+ +---v--------+ 150 | | | 3 +------->+ monfichier | 151 | +--------+---+ +------------+ 152 | 153 | Après un ``close(3)`` le nouvel état sera: 154 | 155 | .. code-block:: none 156 | 157 | terminal 158 | 159 | + ^ 160 | +--------+---+ | | 161 | | | 0 +<-----+ | 162 | | +---+ | 163 | | | 1 +----------+ 164 | | sh +---+ 165 | | | 2 +------------+ 166 | | +---+ +---v--------+ 167 | | | 3 | | monfichier | 168 | +--------+---+ +------------+ 169 | 170 | Ce qui est plus ou moins équivent à l'état nécessaire à un ``sh 2>monfichier``. 171 | 172 | Attention toutefois ! les redirections doivent pouvoir être annulées. Il faut dupliquer le file descriptor qui va être écrasé par le dup2 pour pouvoir ensuite le restaurer. Sinon, les redirections persisteront pendant le reste de l'exécution du shell. On peut être tenté d'exécuter les redirections après avoir fork pour exécuter une commande, mais cela ne fonctionnera pas lorsqu'on ne fork pas (lors de l'exécution des fonctions et des builtins). 173 | 174 | Les variables locales à une commande 175 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 176 | 177 | Lorsque vous faites ``a=b macommande``, la variable a ne vaudra b que pendant l'exécution de ``macommande``. Attention toutefois, si macommande est une fonction, cette règle ne s'applique pas, et il n'est pas nécessaire de restaurer la valeur de a. 178 | 179 | Les fonctions 180 | ------------- 181 | 182 | Le support des fonction se décompose en deux partie. Leur définition, et leur appel. 183 | 184 | Définition 185 | ~~~~~~~~~~ 186 | Lors de l'exécution, il faut soit recopier le bout d'ast du corps de la fonction dans un tableau associant le nom de la fonction à son code. 187 | 188 | Il y a plusieurs moyen de faire en sorte que le bout d'AST de la fonction ne soit pas free avec le reste: 189 | 190 | - le retirer de l'AST 191 | - le marquer comme ne devant pas être free d'une manière ou d'une autre 192 | - faire du reference counting 193 | - garder les AST de toutes les commandes en mémoire (plutôt bof) 194 | 195 | La première méthode est sûrement la plus simple. 196 | 197 | Appel 198 | ~~~~~ 199 | Les fonctions sont gérées légèrement différemment du reste des commandes simples, sans pour autant diverger énorment : 200 | la fonction en charge de lancer une commande vérifie d'abord si une fonction du nom demandé existe, et l'appelle si c'est le cas. 201 | dans le cas contraire, le processus continue comme à son habitude. 202 | 203 | Les redirections sont effectuées comme d'habitude, et si les assignations doivent être visibles à l'intérieur de la fonction, 204 | elles peuvent aussi l'être une fois l'appel terminé (au choix). 205 | 206 | Les pipes 207 | --------- 208 | 209 | `Conférences Redirection et Pipe 42SH - 2019 `_ 210 | 211 | **Errata**: 212 | 213 | - j'ai par erreur interverti les deux extrémités du pipe lors de la conférence (``fd[0]`` et ``fd[1]``) 214 | - il est préférable de marquer les sauvegardes de file descriptor avec ``CLOEXEC`` pour éviter que les process enfants en héritent 215 | 216 | .. admonition:: Allez plus loin 217 | 218 | La conférence présente une version volontairement simplifiée de l'algorithme, qui considère que le pipe est un opérateur binaire. 219 | C'est tout à fait acceptable et fonctionnel, mais devient problématique si vous souhaitez implémenter du job control. 220 | 221 | **Attention, le job control est une fonctionnalité très avancée** 222 | 223 | https://www.linusakesson.net/programming/tty/index.php 224 | 225 | https://blog.nelhage.com/2010/01/a-brief-introduction-to-termios-signaling-and-job-control/ 226 | 227 | 228 | Pipelines 229 | ~~~~~~~~~ 230 | 231 | La manière la plus simple d'exécuter une suite de pipes se fait de la même manière qu'on exécute un opérateur pipe binaire: 232 | 233 | .. code-block:: none 234 | 235 | a | b | c 236 | 237 | | 238 | / \ 239 | | c 240 | / \ 241 | a b 242 | 243 | on exécute d'abord le nœud pipe racine, puis les autres, récursivement. Comme n'importe quel autre nœud d'ast. 244 | 245 | 246 | La boucle read / eval 247 | --------------------- 248 | 249 | Vous avez besoin de faire une boucle qui va soit lire une string et appeler votre parseur dessus, 250 | pour la méthode sale, soit appeller votre parseur avec un lexeur configuré avec un backend readline, 251 | pour la méthode propre. 252 | 253 | C'est aussi pas mal d'avoir un état, histoire de pouvoir se trimballer des fonctions / variables entre 254 | deux lignes / AST. 255 | 256 | Faites attention de ne pas free les bouts d'AST utilisés pour les fonctions. 257 | 258 | 259 | FAQ 260 | --- 261 | 262 | Mon programme fait N fois la même chose 263 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 264 | 265 | Quand l'exécution d'une commande rate (``execvpe`` n'a pas fonctionné) ou qu'un sous-shell se termine, 266 | le processus créé par ``fork`` est toujours là. Il faut afficher un message d'erreur 267 | et ``exit`` avec le bon code de retour. 268 | 269 | 270 | Les dernières lignes de mon fichier sont lues plusieurs fois 271 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 272 | 273 | Si le fichier depuis lequel vous lisez les commandes n'est pas 274 | close avant d'``exit``, la bibliothèque standard va lseek vers le début du fichier 275 | pour prendre en compte le fait que des données on été lues en avance par le buffering. 276 | 277 | Étant donné que le process parent partage le descripteur de fichier, le ``lseek`` 278 | agira également sur son instance de fichier ouvert. Quand le parent aura vidé son buffer, 279 | il lira à nouveau dans le fichier à partir de l'index corrigé par le fils. 280 | 281 | À vous d'utiliser l'exemple suivant pour régler votre problème: 282 | 283 | .. code-block:: c 284 | 285 | #define _GNU_SOURCE 286 | 287 | #include 288 | #include 289 | #include 290 | 291 | #include 292 | #include 293 | #include 294 | 295 | static ssize_t next_line(FILE *f) 296 | { 297 | char *line = NULL; 298 | size_t size = 0; 299 | ssize_t line_size = 0; 300 | if ((line_size = getline(&line, &size, f)) <= 0) 301 | printf("no more lines\n"); 302 | else 303 | printf("> %.*s\n", (int)line_size, line); 304 | free(line); 305 | return line_size; 306 | } 307 | 308 | int main(void) 309 | { 310 | FILE *f = fopen("test.txt", "r+"); 311 | 312 | next_line(f); 313 | pid_t pid = fork(); 314 | if (pid == 0) { 315 | // UNCOMMENT ME: fclose(f); 316 | return 2; 317 | } 318 | 319 | int status; 320 | waitpid(pid, &status, 0); 321 | printf("child exited with status %d\n", WEXITSTATUS(status)); 322 | 323 | while (next_line(f) > 0) 324 | continue; 325 | 326 | fclose(f); 327 | } 328 | -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | Implémenter un shell POSIX 2 | ========================== 3 | 4 | Voici un ensemble de retours qui je l'espère, vous aideront à avoir un shell 5 | sur des rails ne menant pas à une voie de garage. Si un de ces conseils vous 6 | semble trop compliqué, oubliez-le : le projet est long et compliqué, et mieux 7 | vaut un demi-shell qui n'aurait pas pu être terminé que rien du tout bien 8 | commencé. 9 | 10 | Ce document était initialement destiné aux étudiants d'EPITA, qui doivent 11 | implémenter un shell POSIX lors de leur troisième année d'étude. 12 | 13 | Contents 14 | -------- 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | 19 | structure 20 | interactions 21 | lexer 22 | parser 23 | execution 24 | 25 | Indices and tables 26 | ------------------ 27 | 28 | * :ref:`search` 29 | -------------------------------------------------------------------------------- /interactions.rst: -------------------------------------------------------------------------------- 1 | Intéractions entre les composants 2 | ================================= 3 | 4 | Interactions lexeur-entrée (IO) 5 | ------------------------------- 6 | 7 | C'est peut-être la partie la plus déterminante du projet. Vous devez choisir entre 8 | avoir un lexeur qui appelle un backend qui lit le programme, et lire le programme d'abord, 9 | et le donner au lexeur. 10 | 11 | Dites-vous que faire cette partie à moitié, c'est diviser sa note par deux. 12 | 13 | La méthode sale 14 | ~~~~~~~~~~~~~~~ 15 | 16 | Une solution possible, mais incomplète, consiste à utiliser un lexeur qui ne gère qu'un 17 | type d'entrée: une string, terminée par un 0. Il faudra alors batailler pour convertir 18 | le reste vers ce format : 19 | 20 | - avec la command line, tout vas bien 21 | - avec un fichier en argument, tout vas bien, vous le lisez en entier dans un gros buffer 22 | - en lisant sur l'entrée standard, ça se complique. 23 | - si l'entrée standard n'est pas interactive, on peut lire jusqu'à EOF 24 | - pour le mode interactif, vous pouvez lire l'entrée ligne par ligne 25 | - n'espérez pas passer les tests de commande sur plusieurs lignes en mode interactif 26 | avec cette méthode 27 | 28 | La méthode propre 29 | ~~~~~~~~~~~~~~~~~ 30 | 31 | Le lexer interagit avec un backend d'entrée sortie, similaire aux [FILE streams génériques]( 32 | https://www.gnu.org/software/libc/manual/html_node/Streams-and-Cookies.html). 33 | L'idée étant d'avoir un ensemble d'opérations (next, look) identique pour toutes les 34 | entrées possibles: si ces bases marchent, le reste fonctionnera uniformément. 35 | 36 | Cela permet de gérer les commandes interactives sur plusieurs lignes, et tout ce que vous voudriez faire d'autre. 37 | 38 | il s'agit d'avoir une structure du genre: 39 | 40 | .. code-block:: c 41 | 42 | struct cstream 43 | { 44 | /* ... */ 45 | }; 46 | 47 | /* creates a new stream associated with the given string */ 48 | struct cstream *stream cstream_open_string(char *string); 49 | 50 | /** 51 | ** \brief looks at the next char in the stream 52 | ** 53 | ** The function does not modify the character stream. 54 | ** 55 | ** \code 56 | ** struct cstream *cs = cstream_open_string("test"); 57 | ** assert(cstream_look(cs) == 't'); 58 | ** assert(cstream_look(cs) == 't'); 59 | ** \endcode 60 | ** \return the next character in the stream 61 | */ 62 | char cstream_look(const struct cstream *stream); 63 | 64 | /** 65 | ** \brief gets the next char in the stream, and moves forward 66 | ** 67 | ** \code 68 | ** struct cstream *cs = cstream_open_string("test"); 69 | ** assert(cstream_next(cs) == 't'); 70 | ** assert(cstream_look(cs) == 'e'); 71 | ** assert(cstream_next(cs) == 'e'); 72 | ** assert(cstream_next(cs) == 's'); 73 | ** \endcode 74 | ** \return the next character in the stream 75 | */ 76 | char cstream_next(const struct cstream *stream); 77 | 78 | Interactions parseur-lexeur 79 | --------------------------- 80 | 81 | La méthode sale 82 | ~~~~~~~~~~~~~~~ 83 | 84 | Tout lexer d'abord, puis tout donner au parseur. Vous ne pourrez par gérer les alias, et consommerez plus de mémoire. 85 | 86 | La méthode propre 87 | ~~~~~~~~~~~~~~~~~ 88 | 89 | Faire en sorte que le parseur demande des tokens au lexeur, qui lexe au fur et à mesure. 90 | -------------------------------------------------------------------------------- /lexer.rst: -------------------------------------------------------------------------------- 1 | Lexeur 2 | ====== 3 | 4 | Le gros challenge de cette partie est de délimiter les bornes des mots. Encore une fois, la 5 | version simple mais incomplète est beaucoup plus accessible et rentable en temps. 6 | 7 | C'est probablement la partie la plus dure à faire parfaitement. 8 | 9 | - Faites des tests 10 | - Beaucoup de tests 11 | - Testez ce que vous ne savez pas 12 | 13 | Méthode simple 14 | -------------- 15 | 16 | Voici un algorithme de lexing minimal : 17 | 18 | 1) On saute les espaces 19 | 2) Si le caractère actuel commence un opérateur, on lexe et retourne un nouveau token 20 | 3) Sinon, on fait partie d'un mot. On continue à lexer tant que le caractère n'est pas une opérateur / un espace. 21 | 22 | Cet algorithme ne gère pas les quotes. Il est fortement conseillé d'utiliser une approche récursive pour gérer celles-ci. 23 | 24 | Méthode propre 25 | -------------- 26 | 27 | Le lexeur devrait être réutilisable pour la partie expansion. Sinon, vous pourriez interprêter différement deux 28 | chaines, et échouer avec perte et fracas. 29 | 30 | Au premier abord, la tache est difficile: la tache du lexeur est de délimiter les strings, et celle de l'expansion 31 | de les interprêter. Il s'agit donc de leur construire une base commune 32 | 33 | Vous avez envie de le construire récursivement, de telle sorte que les différents contextes (``', ", $(), \```) 34 | soient gérés sans cas particulier. Idéalement, le code doit être réutilisable pour la partie expansion (quand 35 | vous allez réutiliser les strings. De plus, (si vous voulez les gérer, c'est à dire certainement pas), les 36 | HEREDOC ne devraient pas être un type de token différent du point de vue du parseur, mais plutôt une sorte de 37 | promesse future de mot. 38 | 39 | Une solution intéressante peut être de construire deux lexeurs, le premier servant de base commune au second et 40 | à l'expansion. 41 | 42 | 43 | Les alias 44 | --------- 45 | 46 | Les alias sont gérés lors du lexing. L'environnement d'exécution contient un tableau associant le nom de l'alias à la liste de tokens qui devront le remplacer. 47 | 48 | Considérez l'exemple suivant: 49 | 50 | .. code-block:: shell 51 | 52 | alias oops='mafonction (' 53 | oops) { 54 | echo hm 55 | } 56 | 57 | Le shell fait comme suit: 58 | 59 | 1) il parse la première commande comme le mot alias suivit d'un unique argument: ``oops=mafonction (``. 60 | 2) il exécute la première commande, qui rajoute enregistre ``oops`` dans la table des alias. La valeur de l'alias est lexée pour pouvoir être plus facilement substituée par la suite. 61 | 3) il commence à parser la seconde ligne. Comme le token oops commence une commande, le shell regarde si il existe un alias à ce nom. comme c'est le cas, il remplace le token par la liste de tokens contenue dans l'alias. 62 | 4) ``mafonction`` commence une commande. Il n'y a pas d'alias à ce nom, le parsing continue. Une fonction est reconnue. 63 | 64 | Quelques exemples de comportements importants: 65 | 66 | .. code-block:: shell 67 | 68 | # les alias travaillent au niveau des tokens 69 | alias atroce='func(' 70 | # la ligne suivante donne une erreur de syntaxe, 71 | # car func(\n n'est pas du shell valide. 72 | 73 | atroce # ERROR: syntax error near unexpected token `newline' 74 | 75 | # ceci est une déclaration de fonction valide. 76 | # les tokens sont substitués car en début de ligne 77 | atroce ) { 78 | echo vraiment atroce 79 | } 80 | 81 | if true; then 82 | alias machin='echo hm ok' 83 | 84 | # la commande suivante n'est pas trouvée car 85 | # l'alias n'était pas encore là quand le mot 86 | # a été lexé. 87 | machin # ERROR: machin: command not found 88 | fi 89 | 90 | # l'alias déclaré au dessus est maintenant actif 91 | machin # "hm ok" 92 | 93 | # ATTENTION: cet alias s'appelle lui-même. 94 | # Cela doit être détecté lors de l'exécution. 95 | alias echo='echo bidule' 96 | alias machin='echo truc' 97 | 98 | machin # "bidule truc" 99 | 100 | # les alias ne sont substitués qu'en début de ligne 101 | printf '%s\n' echo # "echo" 102 | 103 | # la recherche d'alias se fait forcément avant l'expansion 104 | # (on est au lexing). "\echo" n'est pas littéralement un nom 105 | # d'alias, et ne le deviendra qu'après expansion. 106 | \echo vrai echo # "vrai echo" 107 | -------------------------------------------------------------------------------- /parser.rst: -------------------------------------------------------------------------------- 1 | Parseur 2 | ======= 3 | 4 | Le plus gros challenge de cette partie est de ne pas : 5 | 6 | - leak 7 | - violer la coding style 8 | 9 | Vous devez prendre des tokens, et les convertir en nœuds d'AST. 10 | 11 | Lire une grammaire 12 | ------------------ 13 | 14 | Une grammaire est un arbre de choix qui décrit comment un produire un programme syntaxiquement valide. 15 | Ainsi, le passage de la grammaire à un parseur récursif n'est pas forcément évident. 16 | 17 | Quelques pistes: 18 | 19 | - une partie du travail consiste à chercher les cas d'arrêt. par exemple, une simple command shell peut être arrêtée par différents tokens: `; `, `\n` ou `)`. pour trouver les différents cas d'arrêts, on cherche les tokens qui peuvent suivre une règle parmis tous les chemins qui y mènent dans la grammaire. 20 | - parfois, le token suivant ne suffit pas à décider quelle règle s'applique. par exemple: 21 | 22 | .. code-block:: shell 23 | 24 | macommande 25 | macommande() { 26 | echo oops 27 | } 28 | 29 | dans l'exemple précédent, lire le premier token de la ligne ne suffit pas à savoir s'il s'agit d'une commande ou d'une définition de fonction. 30 | 31 | 32 | Exemple de fonction de parsing 33 | ------------------------------ 34 | 35 | Une manière raisonnable d'écrire des fonctions de parsing est la suivante : 36 | 37 | .. code-block:: c 38 | 39 | enum token_type 40 | { 41 | TOKEN_WHILE, 42 | TOKEN_DO, 43 | TOKEN_DONE, 44 | }; 45 | 46 | enum parser_status 47 | { 48 | PARSER_DONE = 0, 49 | PARSER_NO_MATCH, 50 | PARSER_ERROR, 51 | }; 52 | 53 | struct ast_node_while *ast_node_while_attach(struct ast_node **res) 54 | { 55 | // depending on the ast implementation 56 | struct ast_node_while *node = ast_node_while_alloc(); 57 | *res = &node->base; 58 | return node; 59 | 60 | // OR 61 | 62 | *res = ast_node_alloc(); 63 | (*res)->type = AST_WHILE; 64 | return &(*res)->data.while_data; 65 | } 66 | 67 | enum parser_status parse_rule_while(struct ast_node **result, struct lexer *lexer) 68 | { 69 | int rc; 70 | // is the next token isn't a while token, the rule doesn't match 71 | if (!parser_consume(lexer, TOKEN_WHILE)) 72 | return PARSER_NO_MATCH; 73 | 74 | // create a new AST node, and attach it to the result pointer 75 | struct ast_node_while *while_node = ast_node_while_attach(result); 76 | 77 | // parse the condition, and return the error is a failure occurs 78 | if ((parse_compound_list(&while_node->condition, lexer))) 79 | return rc; 80 | 81 | // return the error if there's no "do" 82 | if ((rc = parser_consume(lexer, TOKEN_DO))) 83 | return rc; 84 | 85 | // parse the while body 86 | if ((rc = parse_compound_list(&while_node->body, lexer))) 87 | return rc; 88 | 89 | // return the error if there's no "done" 90 | if ((rc = parser_consume(lexer, TOKEN_DONE))) 91 | return rc; 92 | return 0; 93 | } 94 | 95 | 96 | Type de parseur 97 | --------------- 98 | 99 | Un [parseur à descente récursive](https://en.wikipedia.org/wiki/Recursive_descent_parser) suffit. Vous pouvez faire autre chose si vous voulez, mais ça sera difficilement plus rentable en temps que ce type là. 100 | 101 | L'AST 102 | ----- 103 | 104 | Un AST shell **n'est pas un arbre binaire**. Chaque type de nœud (commande, if, …) 105 | dispose de champs particulier. Il doit y avoir un moyen de déterminer le type d'un nœud 106 | lors d'un parcours de l'arbre. 107 | 108 | Méthode 1 109 | ~~~~~~~~~ 110 | Un nœud d'AST pourrait ressembler à la chose suivante : 111 | 112 | .. code-block:: c 113 | 114 | struct ast_node 115 | { 116 | enum node_type 117 | { 118 | NODE_IF, 119 | NODE_COMMAND, 120 | ... 121 | } type; 122 | 123 | union 124 | { 125 | struct node_if node_if; 126 | struct node_command node_command; 127 | /* ... */ 128 | } data; 129 | }; 130 | 131 | N'hésitez-pas à créer une fonction dédiée à l'allocation de nœuds: vous l'utiliserez tellement que chaque 132 | ligne compte. 133 | 134 | Par exemple: 135 | 136 | .. code-block:: c 137 | 138 | struct ast_node *node_if = alloc_node(NODE_IF); 139 | if (parse_if(lexer, &node_if->data.node_if) == 0) 140 | return node_if; 141 | 142 | // handle error 143 | return NULL; 144 | 145 | Méthode 2 146 | ~~~~~~~~~ 147 | 148 | On peut implémenter de l'héritage simple avec fonctions virtuelles: 149 | 150 | .. code-block:: c 151 | 152 | typedef void (*node_free)(struct ast_node *node); 153 | typedef void (*node_print)(struct ast_node *node); 154 | typedef void (*node_exec)(struct ast_node *node, struct sh_context *context); 155 | 156 | struct node_type { 157 | const char *name; 158 | node_free free; 159 | node_print print; 160 | node_exec exec; 161 | }; 162 | 163 | extern struct node_type node_if; 164 | extern struct node_type node_command; 165 | 166 | struct ast_node 167 | { 168 | struct node_type *type; 169 | // champs communs à tous les nœuds 170 | }; 171 | 172 | /* ... */ 173 | 174 | struct ast_node_if 175 | { 176 | struct ast_node base; 177 | // les champs nécessaires au if 178 | }; 179 | 180 | Beaucoup plus propre, il faut une fonction d'allocation par nœud. 181 | 182 | Méthode 3 183 | ~~~~~~~~~ 184 | 185 | 186 | .. code-block:: c 187 | 188 | enum ast_node_type 189 | { 190 | AST_NODE_IF, 191 | }; 192 | 193 | struct ast_node 194 | { 195 | struct ast_node_type type; 196 | // champs communs à tous les nœuds 197 | }; 198 | 199 | /* ... */ 200 | 201 | struct ast_node_if 202 | { 203 | struct ast_node base; 204 | // les champs nécessaires au if 205 | }; 206 | 207 | Cette dernière méthode est probablement la plus adaptée au problème. 208 | 209 | Voici un exemple d'usage de ce type d'ast : 210 | 211 | .. code-block:: c 212 | 213 | void ast_free_if(struct ast_node *node) 214 | { 215 | struct ast_node_if *if_node = (struct ast_node_if *)node; 216 | /* ... */ 217 | free(if_node); 218 | } 219 | 220 | typedef void (*ast_free_f)(struct node *); 221 | 222 | ast_free_f ast_free_func[] = 223 | { 224 | [NODE_IF] = ast_free_if, 225 | } 226 | 227 | void ast_free(struct node *ast_node) 228 | { 229 | return ast_free_func[ast_node->type](node); 230 | } 231 | 232 | Simplifier sa gestion d'erreur 233 | ------------------------------ 234 | 235 | Un des éléments les plus problématiques du parseur shell est sa gestion d'erreur: 236 | pratiquement chaque opération peut mener à une erreur qu'il faudrait prendre en charge. 237 | 238 | Il faut donc trouver des moyens de rendre plus simple sa gestion d'erreur. 239 | 240 | Le free par le haut 241 | ~~~~~~~~~~~~~~~~~~~ 242 | 243 | La méthode la plus souvent utilisée pour allouer des AST implique de retourner le nœud créé par la fonction de parsing. 244 | Ainsi, en cas d'erreur, cette fonction devra se charger de libérer toute la mémoire déjà allouée. 245 | 246 | Une méthode moins lourde est de passer en argument l'endroit où rattacher le nœud d'ast à créer. Ainsi, en cas d'erreur, 247 | on peut directement sortir de la fonction sans free, et appeller une seule fois une fonction de free après être sorti du parseur. 248 | 249 | Vous pouvez voir un exemple de cette technique dans la fonction de parsing plus haut. 250 | 251 | La méthode des free list 252 | ~~~~~~~~~~~~~~~~~~~~~~~~ 253 | 254 | Vos pouvez rajouter une liste d'addresses à free à votre AST. La méthode est peu élégante mais efficace. 255 | Mettre cette liste dans une variable globale / indépendante de l'AST est par contre absurde. 256 | 257 | La méthode des exceptions 258 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 259 | 260 | Implémentez des exceptions (setjmp / longjmp) permet de ne pas avoir à vérifier à la main le code de retour des fonctions appelées. 261 | Si une erreur fatale se produit, le parseur s'arrêtera. Il est nécessaire de combiner cette méthode avec celle du free par le haut 262 | pour éviter les leaks. 263 | 264 | **/!\\ Attention, cette méthode est très pratique mais difficile à implémenter /!\\** 265 | -------------------------------------------------------------------------------- /structure.rst: -------------------------------------------------------------------------------- 1 | Structure générale 2 | ================== 3 | 4 | .. code-block:: none 5 | 6 | +---> file input 7 | +----------+ +---------+ +------------+ | 8 | | | | | | | | 9 | +---------->+ parser +---->+ lexer +---->+ IO backend +------> readline input 10 | | | | | | | | | 11 | +---+---+ +----------+ +---------+ +------------+ | 12 | | | +---> string input 13 | | Read | 14 | | Eval | 15 | | Loop | 16 | | | 17 | +---+---+ +-----------+ +-----------+ 18 | | | | | | 19 | +---------->+ execution +------>+ expansion | 20 | | | | | 21 | +-----------+ +-----------+ 22 | 23 | - Le lexeur découpe votre suite de charactère en mots. Il donne du sens basique à ces mots: 24 | 25 | * points-virgule / retours à la ligne (``; \n``) 26 | * IO number (pour les redirections) (``1>``) 27 | * mot ``toto if then`` 28 | * mot d'assignation ``A=B`` 29 | * nom générique ``0abc`` 30 | * opérateurs ``|`` 31 | 32 | - Le parseur prend ces mots, et analyse leur sens pour les organiser en arbre 33 | - Le composant d'exécution prend cet arbre et un état initial (variables), effectue les 34 | actions spécifiées par l'arbre et modifie l'état en conséquence. 35 | --------------------------------------------------------------------------------