Minishell
Minishell
A small bash-like command interpreter written from scratch in C. It reads a line, splits it into tokens, expands variables and quotes, builds a pipeline of commands with their redirections and runs it with fork, pipe, dup2 and execve. Exit statuses, the environment and terminal signals behave the way they do in a real shell.
Project Requirements
- Executable:
minishell, built by aMakefilethat compiles with-Wall -Wextra -Werrorand does not relink. - Behaviour:
bashis the reference. Quoting, expansion, redirections, pipes, exit codes and signals must match it. - Global Variables: Exactly one, and it may only hold the number of a received signal.
- Memory Management: No leaks and no leftover file descriptors, in the parent or in any forked child.
- The Norm: Every function is 25 lines or less, with at most 5 functions per file.
Features
- Interactive & Scriptable: A
readlineprompt with line editing and history. When input is piped in, no prompt is printed and the first syntax error ends the run with status2, just like a non-interactivebash. - Quoting: Single quotes are fully literal. Inside double quotes, everything except
$is literal. - Expansion:
$VAR,$?and word splitting of unquoted results. Unset variables expand to nothing. - Pipelines:
cmd1 | cmd2 | cmd3 …of any length, with every stage running in its own process. - Redirections:
<,>,>>and<<heredocs. A heredoc expands variables unless its delimiter is quoted. - Builtins:
echo -n,cd(includingcd -),pwd,export,unset,envandexit. - Exit Codes:
127for command not found,126for a file that cannot be executed and128 + signofor a process killed by a signal. - Signals:
Ctrl-Cgives you a fresh prompt,Ctrl-Dexits andCtrl-\leaves the shell running but still reaches a running child. Heredocs react the same way.
Implementation Details
1. From Line to Commands
Every line goes through three stages before anything is executed: the tokenizer, the expander and the parser. The result is either an array of commands or an error message, so the REPL has a single place where it reports failures.
t_ms_parse_res *ms_parse(char *input)
{
// ...
// 1. Tokenizer: raw line -> T_WORD, T_PIPE, T_IN, T_OUT, T_HERE_DOC, ...
tkns = ms_tokenize(input);
result->source.error_msg = ms_syntax_check(tkns);
if ((result->source.error_msg != NULL))
result->exit_code = 2;
// 2. Expander: $VAR, $?, word splitting and quote removal
if (result->exit_code == 0)
result->exit_code = ms_expand(&tkns, &(result->source.error_msg));
// 3. Parser: tokens -> NULL-terminated array of t_ms_cmd (argv + redirections)
if (result->exit_code == 0)
result->source.cmds = build_cmd_arr(tkns);
// ...
}
2. Quote-Safe Expansion
One subtle case: if a variable’s value contains quotes (export X="'hi'"), echo $X has to print 'hi'. The quote-removal pass must not strip quotes the user never typed. To prevent that, quotes coming from a variable are swapped for invisible marker bytes before quote removal and swapped back afterwards.
// hides the quote characters of a variable value, so that the quote
// removal pass does not eat quotes that the user never typed
char *ms_quote_hide(char *value)
{
// ...
while (result[i] != '\0')
{
if (result[i] == '\'')
result[i] = MS_HIDDEN_SQ;
else if (result[i] == '\"')
result[i] = MS_HIDDEN_DQ;
i++;
}
return (result);
}
3. Spawning a Pipeline
Each stage gets a stdin (the read end of the previous pipe) and a stdout (a fresh pipe, or the terminal for the last stage). All stages run at the same time. The shell then waits for all of them, and the status of the last one becomes $?.
while (cmds[0] != NULL)
{
// the previous pipe's read end becomes this stage's stdin
ms_close_unless(stdenv[R], STDIN_FILENO);
stdenv[R] = out_pipe[R];
// every stage except the last writes into a new pipe
if (cmds[1] != NULL)
stdenv[W] = ms_pipe((int *)out_pipe)[W];
else
stdenv[W] = STDOUT_FILENO;
fr = ms_fork();
if (fr == 0)
ms_exec_child(cmds[0], stdenv); // dup2, redirections, execve
add_pid(&result, fr);
ms_close_unless(out_pipe[W], STDOUT_FILENO);
cmds++;
}
Builtins that change the shell’s own state (cd, export, unset, exit) run in the parent when they are not part of a pipeline. Everything else is forked.
4. Resource Bookkeeping
Forking copies every open fd and every heap pointer into the child, which makes leaks easy. To avoid them, the shell keeps three small trackers:
m3records every live allocation.bwrecords every open file descriptor.kg(the “kindergarten”) records every child pid.
Every allocation, open, dup, pipe and fork goes through an ms_safe_* wrapper that registers the result with the matching tracker, so a single call can release everything:
pid_t ms_fork(void)
{
// ...
result = fork();
if (result < 0 || errno != 0)
ms_error_out(EXIT_FAILURE, NULL, errno);
if (result > 0)
{
kg_add(result); // parent: remember the new child
return (result);
}
ms_signal_child(); // child: Ctrl-C / Ctrl-\ end it again
kg_cleanup(false); // child: forget the parent's children
return (0);
}
void ms_exit(int exit_code)
{
ms_repl_history_clear();
ft_bw_cleanup(); // close every tracked fd
ms_stdenv_close();
kg_cleanup(true); // wait for every child still running
ft_m3_cleanup(); // free every allocation still alive
exit(exit_code);
}
5. Signals with a Single Global
The signal handler only stores the signal number, which is the safest thing a handler can do. The actual reaction happens later, at a safe point: inside readline’s event hook, which runs regularly while the shell waits for input.
volatile sig_atomic_t g_ms_signal;
static void ms_real_sh(int sig)
{
g_ms_signal = sig;
}
int ms_rl_main_event_hook(void)
{
int sig;
sig = ms_signal_consume();
if (sig == SIGINT)
{
// Ctrl-C: drop the current line and show a fresh prompt
(void)write(1, "\n", 1);
rl_replace_line("", 0);
rl_on_new_line();
rl_redisplay();
ms_signal_listen(0);
}
return (0);
}
Heredocs use a different hook that sets rl_done instead. Ctrl-C then cancels the heredoc and the whole command line, and the shell itself keeps running. The collected lines are later fed to the command through a pipe by a small writer process, so no temporary files are needed.
Usage
Compilation
Requires the GNU readline library (sudo apt install libreadline-dev on Debian/Ubuntu). libft is bundled in the repo and built automatically.
make
Running the Shell
-
Interactive mode:
./minishellminishell %> export NAME=42 minishell %> echo "hello $NAME" | tr a-z A-Z HELLO 42 minishell %> cat << EOF minishell_heredoc %> the shell level is $SHLVL minishell_heredoc %> EOF the shell level is 2 minishell %> exit 42 -
Non-interactive mode:
printf 'export GREETING=hi\necho $GREETING world\n' | ./minishellOutput:
hi world
Skills Demonstrated
- Process Management: Using
fork,execveandwaitpid, and passing exit statuses and signal deaths back up the same waybashdoes. - File Descriptor Plumbing: Wiring pipes and redirections with
dup2so that every fd is closed in the right process at the right time. - Language Parsing: A hand-written tokenizer, expander and parser for shell syntax, with all its quoting edge cases.
- Signal Handling:
sigaction, an async-signal-safe handler andreadlineevent hooks working together without race conditions. - Resource Safety: Central trackers for memory, fds and child processes, so a single exit path leaves no leaks, even in forked children.
- Teamwork: Splitting a large C codebase between two developers under strict style rules and reviewing each other’s work through git.
Authors
- GitHub: cuteKittenArri
- GitHub: iggi42