├── .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 |
--------------------------------------------------------------------------------