SkillAgentSearch skills...

Minishell

This is my final 42's project Minishell, I implemented my own shell in C, which supports redirections, pipes, environment variables and several builtins

Install / Use

/learn @iciamyplant/Minishell
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Lancement :

make && ./minishell

Précisions :

  • Ce projet a été fait avant la mise à jour du sujet, il n'utilise pas les termcaps et contient plusieurs variables globales.
  • Ce readme n'expliquera que la partie dont je me suis occupé. C'est à dire : le parsing (séparations, protections, redirections, variables d'environnement) ainsi que le buitin exit et $?. + quelques informations concernant env, export et unset + quelques liens pour les pipes.
  • Il y a des algos bien plus efficaces pour faire le parsing que ce que j'ai fait : Lexical Analysis (voir le projet de @mkayumba : explication de son projet)

Plan :

I - Qu'est ce que Minishell ?

  • Le sujet
  • Appréhender le projet

II - Avant le parsing

III - Le parsing

  • .1. Les séparations
  • .2. Les pipes
  • .3. Commande et arguments
  • .4. Les protections
  • .5. Les redirections <, >, >>
  • .6. Les variables d'environnement
    • Exemples de tests

IV - L'exécution

  • .1. Les redirections
  • .2. Env, export, unset
  • .3. Exit et $?
  • .4. Liens pipes/signaux/processus
    • Les tests tricky de @frthierr

V - Tester + Leaks et errors utils

VI - Utils du shell

  • Rappels cmds
  • Rappels chmod et droits
  • Ln et liens
  • Rappels valeur MAX/MIN

I - Qu'est ce que Minishell ?

Le sujet

L'objectif de Minishell est de créer un shell.

C'est quoi un shell ? En gros dans un système d’exploitation y a ces deux éléments :

  • Le noyau (= kernel) : qui effectue les fonctions fondamentales du système d'exploitation telles que la gestion de la mémoire, des processus, des fichiers...
  • Le shell : l'interface entre l'utilisateur et le noyau, qui permet à l'utilisateur de communiquer avec le système d’exploitation par l'intermédiaire d'un langage de commandes. L'utilisateur peut entrer des commandes dans le terminal. Grâce à l’interpréteur de ligne de commande (tel que bash ou zsh qui sont des shells), les commandes entrées sont exécutées. --> On va créer notre propre petit shell.

Appréhender le projet

J'ai lu le man de bash, qui est très long, mais en vrai j'ai trouvé ça trop utile.

II - Avant le parsing

  • Récupérer toutes les variables d'environnement :

Quand tu tappes env dans le terminal tu vois toutes les variables d'environnement. En parametre du main, env est un char** qui contient toutes ces variables d'environnement sous la forme : env[0] = TMPDIR=/var/folders/7g/g6ksr7hd0mjcyjwkj_mqdmgm0000gn/T/ . Une valeur à 0 indique la fin du tableau.

int	main(int ac, char **av, char **env)
  • Récupérer PATH (qui est dans env) dans un char :**

PATH = variable utilisée par le système d'exploitation pour localiser les fichiers exécutables des commandes. Genre imagine quand tu fais ls et que PATH=/usr/local/bin:/usr/bin:/bin:, ca veut dire le systeme va chercher un fichier executable qui s'appelle ls qui correspond a ls et il va chercher dans /usr/local/bin s'il trouve pas il va aller dans /usr/bin puis dans /bin. Donc quand l'utilisateur va tapper des commandes qui sont pas dans nos builtins on va avoir besoin de connaitre les chemins de PATH.

ft_split(PATH, ':');
  • Récupérer la ligne de commande et écrire le prompt :

Prompt = c'est l'invit de commande. On va faire une loop avec gnl et dans cette loop on va ecrire le prompt

while (get_next_line(0, &line) > 0)
{
  parsing
  write(0, "~$ ", 3);
}

III - Le parsing

1. Les séparations

Les commandes séparées par un ';' sont exécutées successivement, l'interpréteur attend que chaque commande se termine avant de lancer la suivante

  • J'ai parsé les éléments entre “;” dans un char** que j'ai mis dans une liste chaînée. Pour comprendre facilement les listes chaînées : vidéo

Exemple : echo Hello; echo World

char **str;
str = ft_split(line, ';'); // str[0] = echo Hello, str[1] = echo World
while (str[++i])
  list = add_cell(list, str[i], i); // deux cellules, dans chaqune on met str[i]

Fonctions qui créent les cellules :

t_sep	*create_cell(char *cmd_sep)
{
  t_sep	*cell;

  cell = malloc(sizeof(t_sep));
  if (!(cell))
  	return (NULL);
  cell->prev = NULL;
  cell->next = NULL;
  cell->pipcell = NULL;
  cell->cmd_sep = cmd_sep;
  return (cell);
}
t_sep	*add_cell(t_sep *list, char *cmd_sep, int pos)
{
  t_sep	*prec;
  t_sep	*cur;
  t_sep	*cell;
  int		i;

  cur = list;
  i = 0;
  cell = create_cell(cmd_sep);
  if (list == NULL)
  	return (cell);
  while (i < pos)
  {
  	i++;
  	prec = cur;
  	cur = cur->next;
  }
  prec->next = cell;
  cell->next = cur;
  return (list);
}

Imprimer les cellules :

void	print_list(t_sep *list)
{
  int		i;

  i = 0;
  while (list)
  {
  	printf("-----------------------------------\n");
  	printf("| i = %d                            \n", i);
  	printf("| list->cmd_sep : %s            \n", list->cmd_sep);
  	printf("-----------------------------------\n");
  	list = list->next;
  	i++;
  }
}

2. Les pipes

Exemple : echo bonjour ; ls | sort ; echo hey

  • echo bonjour est executé
  • ls est executé et son stdout est le stdin de sort, qui est executé à son tour (En gros pour commande1 | commande2 : la sortie (stdout) de la commande1 est l'entrée (stdin) de la commande2)
  • echo hey est executé

On parcours list où chaque cellule contient cmd_sep (parsé en haut). Si cmd_sep contient un pipe on crée une list chaînée list->pipcell dans la cellule en question. Dans list->pipcell on fait une cellule par commande entre pipes. D'où :

  • [x] : Check dans chaque cmd_sep de chaque cellule de t_sep *list si y a des pipes
  • [x] : Si y en a split de ‘|’ dans un char **
  • [x] : On rentre ce char** dans une liste chaînée (list->pipcell) à l'intérieur de la cellule où cmd_sep contient des pipes
  • [x] : Ensuite on parcours notre list : Si list->pipcell == NULL, ca veut dire que y a pas de pipe, on peut exécuter direct de qui est dans list->cmd_sep. Par contre si list->pipcell != NULL, y a des pipes donc on va executer chaque list->pipcell->cmd_pipe. Avant de passer à la cellule suivante de list.

3. Commande et arguments

Soit la commande est dans nos builtins, soit la commande n'est pas dans nos builtins. Dans ce deuxième cas, il faudra faire un appel système avec execve. => Donc j'ai parsé dans un char** (pour les deux possibilités) direct prêt à être envoyé à execve si besoin.

Exemple : echo -n bonjour

  • ici echo est chez nous, mais sinon execve(file, argv)
  • argv[0] : c’est la commande, echo
  • argv[1] : le premier argument, -n (les options ca peut être collé -lRa dans un char* où séparé chaque option dans un char* -l -R -a)
  • arg[2] : le deuxième argument, bonjour
  • argv[3] : on finit toujours par un NULL

A faire :

  • [x] : Créer le char**
  • [x] : Malloquer le char** à chaque tour car on sait pas à l'avance combien d'arguments
  • [x] : Dès que y a un espace qui n’est pas protégé on passe à l'argument suivant
  • [x] : Attention aux protections

Attention : la fin d'un argument c'est un espace qui est pas dans des quotes et qui n'est pas précédé par un caractère d'échappement :

4. Les protections

Quotes

| | dans des simples quotes | dans des doubles quotes | |----------|-------|-------| | ‘ | nombre impair de simple quote c’est pas bon, même si y a un \ (bash-3.2$ echo c'o\'u'cou)| les quotes simples dans une double quote perdent leurs signification, donc même si y a un nombre impair de quotes simples c’est ok, même si y a un \ (bash-3.2$ ec"ho" bon"'j'o'u"r)(bash-3.2$ ec"ho" bon"j'o\'u"r) | | “ | les doubles quotes dans des simples quotes perdent leurs signification donc même si y a un nombre impair de doubles quotes c'est ok, même si y a un \ (bash-3.2$ echo co'c"o"u') (bash-3.2$ echo co'"c\"o"u')| nombre impair de double quote c’est pas bon (bash-3.2$ ec"ho" bon"jo"u"r). attention : sauf si y a un \” c’est bon (bash-3.2$ ec"ho" bon"jo\"u"r)| | $ | ne conserve pas sa signification spéciale d’environnement (bash-3.2$ '$PATH')|conserve sa signification spéciale d’environnement (bash-3.2$ “$PATH”) | | \ | ne conserve pas sa signification (bash-3.2$ echo bo'njou\$r') | conserve sa signification que lorsqu'il est suivi par $, ", \ (bash-3.2$ echo bo"njou\$r") (bash-3.2$ ec"ho" bon"jo\"u"r) (bash-3.2$ echo "\\")|

Donc à l'intérieur d’une double quote :

  • \\ : faut imprimer \
  • \$ : faut imprimer $
  • \” : faut imprimer “ :
  • $ : faut appeler la variable d’environnement

Caractère d'échappement

| Le caractère qui suit a sa valeur littérale. Donc si \ devant un $ pas d'appel à variable d’environnement (ex : \$PATH)| |--------------------| | bash-3.2$ echo \\coucou | | bash-3.2$ echo \\\coucou | | bash-3.2$ echo \ \ \ \ \ \ mdr : attention les espaces ne sont pas comptés comme des spérateurs entre les arguments avec le \ devant|

5. Les redirections <, >, >>

trop bien expliqué : article

Pour capter stdin stdout stderr, je me dis que à chaque fois que je tappe une commande, elle reçoit un stdin, elle s'éxecute, le résultat s'imprime sur stdout et le message d'erreur s'imprime sur stderr. De base stdin c'est le clavier, et stdout et stderr cest direct dans le terminal (l'écran). Mais on peut rediriger vers d'autres sources autres que le clavier ou l'écran. Par exemple, rediriger une sortie standard vers un fichier. Pour cela, les numéros des descripteurs de flux sont utilisés.

  • entrée standard (fd = 0)
  • sortie standard (fd = 1)
  • sortie erreur (fd = 2)

| Exemple | Description | |----------|-------| | ls > liste_fichiers.txt | créé le fichier liste_fichiers.txt et écrit la sortie dans le fichier | | ls >> liste_fichiers.txt | enregistré à la fin du fichier au lieu de l'écraser s'il existe déjà | | cat < notes.csv | Permet de lire des données depuis un fichier et de l

View on GitHub
GitHub Stars118
CategoryCustomer
Updated8d ago
Forks7

Languages

C

Security Score

80/100

Audited on Mar 20, 2026

No findings