revs412@portfolio:~/notes/building-a-minimal-unix-shell-in-c$cat building-a-minimal-unix-shell-in-c.md

Pourquoi cette note existe

Cette note documente le processus de construction d'un shell minimal Unix en C.

Le projet est précieux parce qu'un shell se trouve près du système d'exploitation. Même une petite version vous oblige à comprendre comment Linux démarre les programmes, passe les arguments, gère les variables d'environnement, attend les processus enfant et signale les échecs.

Le but n'était pas de remplacer Bash ou Zsh.

Le but était de comprendre les mécanismes de niveau inférieur derrière une commande comme:

ls -la

Un utilisateur normal voit une commande. Un shell doit :

read input
split arguments
find the executable
create a child process
run the command
wait for it
return to the prompt

Cela en fait une note de programmation de systèmes utile.

Contexte du projet

Le shell a été construit en C comme un projet fondamental axé sur le comportement du processus Linux.

Il fait partie d'un portefeuille comme note technique de niveau inférieur, pas comme un projet d'entreprise/client.

La valeur est de montrer la compréhension de:

  • C programmation
  • Création de processus Unix
  • analyse de commande
  • recherche exécutable
  • variables environnementales
  • Gestion des erreurs
  • répartition de la mémoire
  • cas bord
  • simples programmes interactifs

Ce projet est différent des notes d'infrastructure comme WireGuard, OpenWrt, ou l'hébergement de serveur de jeux. Il montre le côté système d'exploitation de la pile.

Ce que ce projet veut prouver

  • un shell est un gestionnaire de processus, et pas seulement une invite de texte
  • L'exécution de la commande Linux dépend de fork, exec et wait
  • arguments doivent être analysés avant l'exécution
  • commandes sans / besoin de résolution PATH
  • les commandes intégrées doivent être gérées par le shell lui-même
  • la mémoire doit être attribuée et libérée avec soin
  • les messages d'erreur devraient être prévisibles
  • les petits programmes C exigent une discipline parce qu'il n'y a pas de filet de sécurité

Pioche et outils utilisés

Langue et temps d'exécution

  • C
  • Linux
  • Appels système de type POSIX
  • GCC
  • bibliothèque standard C

Appels et fonctions système

  • fork
  • execve
  • wait / waitpid
  • getline
  • malloc
  • free
  • strtok ou tokenisation manuelle
  • access
  • stat
  • gestion variable de l'environnement

Concepts Shell

  • boucle rapide
  • analyse de commande
  • vecteur d'argument
  • Recherche PATH
  • exécution de processus pour enfants
  • commandes intégrées
  • État de sortie
  • Gestion des erreurs

Construction prévue

La construction prévue est un shell minimal qui peut:

  • afficher une invite
  • lire la saisie utilisateur
  • analyse une commande en arguments
  • exécuter des binaires
  • Recherche par PATH
  • gérer les erreurs de commande
  • soutien intégré de base
  • sortie proprement
  • éviter les fuites de mémoire évidentes
  • travail en mode interactif et non interactif

Un shell minimal n'a pas besoin de fonctionnalités avancées au début.

Elle n'a pas besoin de soutenir:

  • tuyaux
  • redirection
  • contrôle de l'emploi
  • historique des commandes
  • fin de l'onglet
  • syntaxe de script
  • alias
  • citation avancée

Ce sont des couches plus tard.

Loop Shell de base

La boucle du noyau est simple dans le concept:

while shell is running:
    display prompt
    read line
    parse line into arguments
    check if command is built-in
    if built-in, run inside shell
    else fork child process
    child executes command
    parent waits

Cette boucle est au cœur du programme.

Le défi n'est pas d'écrire la boucle une fois. Le défi est de gérer tous les cas bizarres autour de lui.

Entrée de lecture

Un shell doit lire des lignes complètes à partir de l'entrée standard.

L'utilisation de getline est pratique car elle peut gérer l'entrée de longueur variable.

La coque doit gérer:

  • Entrée normale
  • lignes vides
  • fin de fichier
  • entrée non interactive
  • défaillance de l'allocation mémoire

Exemple de comportement :

$ ls
$
$ exit

Si l'utilisateur appuie sur Ctrl+D, le shell devrait sortir proprement plutôt que s'écraser.

Parsing Commands

Entrée comme & #160;:

ls -la /tmp

doit devenir un vecteur d'arguments:

argv[0] = "ls"
argv[1] = "-la"
argv[2] = "/tmp"
argv[3] = NULL

Le NULL final compte parce que execve attend un tableau d'arguments null-terminé.

Un tokenizer simple peut se diviser sur les espaces et les onglets.

L'analyse plus avancée supporte les citations et l'évasion, mais un shell minimal peut commencer par des règles plus simples.

Exécution des commandes

Si la commande contient un chemin :

/bin/ls

le shell peut essayer de l'exécuter directement.

Si la commande ne contient pas de slash :

ls

le shell doit chercher dans PATH.

Cela signifie :

read PATH environment variable
split PATH by :
join each directory with command name
check if executable exists
run the first match

Exemple :

/usr/local/bin
/usr/bin
/bin

La coque vérifie :

/usr/local/bin/ls
/usr/bin/ls
/bin/ls

jusqu'à ce qu'il trouve un exécutable valide.

Fourche, Exec, attendez

Le modèle de processus de base Unix est:

fork creates a child process
exec replaces the child process image with the target program
wait lets the parent wait for the child to finish

Le shell parent ne devrait pas devenir la commande.

Au lieu de :

parent shell
  ↓ fork
child process
  ↓ execve("/bin/ls", argv, envp)
parent shell
  ↓ wait
prompt returns

C'est pourquoi exec est habituellement appelé dans le processus de l'enfant, pas le parent.

Si le parent appelle directement exec, le shell disparaît et devient la commande.

Commandes intégrées

Certaines commandes ne peuvent pas être gérées en exécutant simplement un autre exécutable.

Exemples:

cd
exit
env

cd doit changer le répertoire de travail du processus shell lui-même.

Si cd fonctionne uniquement à l'intérieur d'un processus enfant, l'enfant change de répertoire et sort, mais le shell parent reste dans l'ancien répertoire.

C'est pourquoi les éléments intégrés sont manipulés avant fork.

Un débit minimal intégré:

if command is "exit":
    clean up and quit

if command is "cd":
    call chdir in parent shell

if command is "env":
    print environment variables

Résolution PATH

La résolution PATH est l'un des premiers bugs apparaissent.

La coque doit être manipulée:

  • PATH manquant
  • Entrées PATH vides
  • commande avec chemin absolu
  • commande avec chemin relatif
  • exécutable existe mais la permission est refusée
  • commande non trouvée
  • répertoires confondus avec les commandes

Un shell fiable ne devrait pas supposer que chaque commande vit dans /bin.

Il devrait utiliser l'environnement.

Gestion de l'environnement

Un shell transmet les variables d'environnement aux processus enfant.

Par exemple, les commandes dépendent souvent de :

PATH
HOME
USER
PWD
SHELL

Un shell minimal peut utiliser l'environnement hérité et le transmettre à execve.

Si le shell prend en charge la modification des variables d'environnement plus tard, cela devient une fonctionnalité plus grande.

Au minimum, le shell doit comprendre que l'environnement fait partie de l'exécution de commande.

Gestion des erreurs

Les erreurs doivent être claires et cohérentes.

Erreurs courantes & #160;:

command not found
permission denied
no such file or directory
fork failed
malloc failed
execve failed

Un mauvais obus s'écrase dessus.

Un meilleur shell signale l'erreur et retourne à l'invite si possible.

Exemple de comportement :

$ unknowncmd
unknowncmd: not found
$

L'obus devrait rester en vie après un échec à moins que l'échec ne soit fatal.

État de sortie

Les vrais obus suivent l'état de sortie.

Une version minimale peut commencer en attendant le processus de l'enfant et en lisant son statut.

Cas utiles:

  • la commande sort normalement
  • sorties de commande avec statut non-zéro
  • le commandement est tué par un signal
  • execve échoue

Même si le shell n'expose pas $?, comprendre l'état de sortie est important.

Gestion de la mémoire

C fait de la gestion de la mémoire une partie du projet.

Le shell peut attribuer la mémoire pour:

  • ligne d'entrée
  • tableau token
  • chaînes copiées
  • Répertoires PATH
  • chemin de commande complet
  • tampons temporaires

Chaque allocation a besoin d'un chemin de nettoyage.

Un bug shell commun fuit la mémoire à chaque boucle.

Une règle plus sûre :

allocate for one command
execute command
free command resources
return to prompt

Les shells à long terme rendent les fuites visibles parce que le processus ne sort pas après chaque commande.

Mode interactif et mode non interactif

Un shell peut fonctionner de manière interactive :

./shell
$ ls
$ exit

ou non interactifs:

echo "ls" | ./shell

Le mode interactif affiche habituellement une invite.

Le mode non-interactif ne devrait souvent pas être rapide, selon les besoins.

Cette distinction est importante pour les tests.

Liste de vérification

Entrée de base

  • ligne vide
  • espaces réservés
  • une commande
  • commande avec arguments
  • Ctrl+D
  • exit

Exécution des commandes

  • /bin/ls
  • ls
  • pwd
  • commande invalide
  • commande avec chemin relatif
  • commande sans autorisation d'exécution

Gestion du PATH

  • PATH normal
  • PATH manquant
  • PATH vide
  • commande dans /bin
  • commande dans /usr/bin
  • répertoire invalide dans PATH

Éléments intégrés

  • exit
  • env
  • cd
  • cd sans argument si supporté
  • cible cd non valide

Gestion des erreurs

  • direction de défaillance du malloc
  • direction de rupture de fourche
  • défaillance exec
  • autorisation refusée
  • fichier non trouvé
  • répertoire passé comme commande

Mémoire

  • commandes répétées
  • longue session
  • Valgrind vérifier si disponible
  • nettoyage après erreurs
  • nettoyage avant sortie

Bogues courantes

Shell disparaît après avoir exécuté la commande

Cause:

exec was called in parent instead of child

Correction :

fork first, exec only in child

cd ne fonctionne pas

Cause:

cd was executed in a child process

Correction :

handle cd as a built-in in the parent shell

Commande fonctionne avec /bin/ls mais pas ls

Cause:

PATH lookup not implemented

Correction :

search directories listed in PATH

Défaut de segmentation sur entrée vide

Cause:

parser assumes argv[0] exists

Correction :

check for empty command before execution

La mémoire grandit pour toujours

Cause:

allocated command data not freed after each loop

Correction :

free line/token/path buffers consistently

Message d'erreur incorrect

Cause:

all failures are treated as command not found

Correction :

distinguish not found, permission denied, and execution failure

Décisions pratiques

Commencez par un petit ensemble de fonctionnalités

Un shell minimal devrait exécuter les commandes de manière fiable avant d'ajouter des pipes, des redirections ou de l'historique.

Poignée intégrée séparément

Des commandes comme cd et exit appartiennent au processus shell lui-même.

Traiter la recherche PATH comme une véritable fonctionnalité

La plupart des commandes types utilisateurs ne sont pas des chemins absolus.

Continuez à analyser en premier

L'analyse basique de l'espace blanc suffit pour une première version.

Vérifier chaque allocation

En C, le défaut d'attribution doit être pris en considération.

Mémoire gratuite par commande

Une coquille est de longue durée, de petites fuites se répètent pour toujours.

Préférez des erreurs claires

Une petite coquille devrait encore expliquer ce qui a échoué.

Ce qu'une version terminée devrait montrer

Un shell minimal fortement fini devrait montrer:

  • prompt interactif
  • commande boucle de lecture
  • analyse des arguments
  • exécution directe du chemin
  • Recherche PATH
  • fork / execve / wait
  • exit intégré
  • env intégré
  • cd intégré si implémenté
  • Gestion propre des erreurs
  • nettoyage de la mémoire
  • support d'entrée non interactif
  • README avec des exemples
  • essais ou cas d'essais documentés

Preuves à retenir

Voici quelques éléments de preuve utiles à cette note :

  • shell prompt screenshot
  • exécutant /bin/ls
  • exécuter ls à travers la recherche PATH
  • sortie de commande invalide
  • comportement cd
  • Essai non interactif
  • arbre source
  • extrait de code de boucle principal
  • Extrait de code de recherche PATH
  • Sortie Valgrind si disponible
  • Section d'utilisation du README

Hypothèses techniques

Cette note suppose un environnement semblable à Linux/POSIX.

Il suppose que le shell est écrit en C et se concentre sur les fondamentaux, pas la pleine compatibilité Bash.

Il suppose également que le projet vise à démontrer la gestion des processus et la discipline de programmation de bas niveau.

Principaux risques

  • appelant exec dans le processus parent
  • ne pas gérer l'entrée vide
  • argv n'a pas de valeur nulle
  • fuites de mémoire dans la boucle de commande
  • Mauvaise manipulation du PATH
  • confusion entre les intégrations et les commandes externes
  • mauvais environnement passé à l'enfant processus
  • ignorant les erreurs de fork/exec
  • s'écraser sur Ctrl+D
  • essayant d'implémenter des fonctionnalités shell avancées trop tôt

État actuel

Cette note représente un projet de programmation de systèmes axé sur la compréhension du fonctionnement interne des shells Unix.

Il n'est pas aussi orienté vers l'entreprise que l'ERP, le VPN ou l'outil de déploiement, mais il ajoute une couche inférieure utile au portefeuille.

Il montre que la base technique n'est pas seulement les applications web et la configuration du serveur, mais aussi le comportement Linux au niveau du processus.

Ce que la présente note ne prétend pas

La présente note ne prétend pas remplacer Bash, Zsh, Fish ou d'autres coquilles réelles.

Il ne prétend pas supporter la syntaxe complète du shell.

Il ne prétend pas inclure toutes les fonctionnalités avancées comme les pipes, la redirection, les tâches, les signaux, les alias ou les scripts.

Il documente un shell minimal C utilisé pour comprendre la mécanique de processus de base Unix.

À emporter pratique

La leçon utile est:

Un shell est une boucle qui transforme le texte en processus.

Pour construire même un petit, vous devez comprendre:

  • lecture des entrées
  • analyse des jetons
  • vecteurs d'arguments
  • Recherche PATH
  • processus pour enfants
  • remplacement exécutable
  • Attendre
  • intégrés
  • erreurs
  • nettoyage de la mémoire

Cela fait du projet un élément fondamental important lorsqu'il est conçu comme une programmation de systèmes, et non comme un exercice de formation générique.