A small Unix shell written in C, with builtins, pipes, I/O redirection,
background jobs, and job control (fg/bg/jobs, Ctrl+C, Ctrl+Z).
make
This builds ./minishell from main.c, minishell.c, job_control.c,
signals.c, and redirection.c. make clean removes build artifacts.
./minishell
You'll get a colored minishell$ prompt. Set a custom prompt with:
PS1="myshell> "
- Builtins:
cd,pwd,echo(supports$$,$?,$SHELL),clear,exit,jobs,fg [job_id],bg [job_id]. - External commands via
execvp/PATHlookup. - Pipelines:
cmd1 | cmd2 | cmd3 | ...(any number of stages). - I/O redirection:
<,>,>>, usable on any external command and on any stage of a pipeline (e.g.sort < in.txt | uniq > out.txt). - Background jobs: append
&to run a command without blocking the shell, e.g.sleep 30 &. The shell prints[job_id] pid. - Job control:
jobs— lists background/suspended jobs with id, state (Running/Stopped), and full command line, bash-style:[1]+ Stopped sleep 30 [2]+ Running sleep 60 &fg [job_id]— brings a job into the foreground and gives it the terminal. Omit the id to use the most recently added job.bg [job_id]— resumes a stopped job in the background, printing[id]+ command &.- Ctrl+C (
SIGINT) interrupts whatever currently owns the terminal. - Ctrl+Z (
SIGTSTP) suspends the foreground job, adds it to the job list, and prints a bash-style notice:[1]+ Stopped sleep 30 - Asynchronous completion notices. When a background job (one
started with
&or resumed withbg) finishes on its own — even while you've since moved on to other commands — the shell announces it right before the next prompt, exactly like bash:The job you're actively watching in the foreground is never announced this way (you can see it finish yourself); only jobs running unattended in the background get this notice.[1]+ Done sleep 5 [2]+ Exit 1 false [3]+ Terminated some_long_job
- Colored UI: the prompt, error messages, and job-control
notifications (
[Suspended], background job start,jobsoutput, etc.) are colorized with ANSI escape codes. Program output itself is never colorized.
- Background execution of a pipeline (
cmd1 | cmd2 &) isn't supported by the job-tracking data structure (a job only records one pid); the shell prints a notice and runs it in the foreground instead. - No argument quoting/escaping (
"a b",'a b') outside of the specialPS1="..."syntax — arguments are split on whitespace only. - Builtins (
cd,echo, etc.) run in the shell's own process, so they only get their special in-process handling when they're the entire command. If they appear inside a pipeline (echo hi | grep h), they run as the real/bin/echo-style external program instead, same as most shells do for coreutils-backed builtins. - Fixed-size buffers: input lines and stored job command strings are
capped at
MAX_INPUT_SIZE(1024 bytes), and pipelines/argument lists are capped atMAX_ARGS(64) tokens.
This codebase had several bugs beyond styling; the notable ones:
fgnever foregrounded anything. Both thefgandbgbuiltins calledsend_job_to_background().fgnow callsbring_job_to_foreground().sigactionnever actually installed the signal handler.sa_sigactionwas set, butSA_SIGINFOwas missing fromsa_flags. Without it, the kernel treats the handler as the plain one-argument form (sa_handler, which shares a union withsa_sigaction) — undefined behavior. Fixed by addingSA_SIGINFO | SA_RESTART.- Pipelines were never waited on. In
call_n_pipe, the line that setsforeground_pid = retfor the last pipeline stage was nested one brace too deep, inside a condition that could never be true at the same time (i == command_count - 1nested insidei < command_count - 1). The shell's wait loop sawforeground_pid == 0immediately and never actually waited for the pipeline. - No process groups → job control couldn't work.
tcsetpgrp()requires the target to already be its own process group leader. Children were never given their own group, so everytcsetpgrp()call infgwas silently failing. Every fork site now callssetpgid()(both in the child and the parent, the standard race-free idiom), and the shell claims its own process group and the terminal on startup ininit_shell(). fgpermanently broke terminal safety after first use.bring_job_to_foreground()used to resetSIGTTIN/SIGTTOUback toSIG_DFLat the end. The shell must ignore those signals forever, or a latertcsetpgrp()call can stop the shell itself. Removed the reset; ignoring is now set once, permanently, ininit_shell().- Race between
fg's directwaitpid()and the asyncSIGCHLDhandler.bring_job_to_foreground()used to callwaitpid()directly while aSIGCHLDhandler was also callingwaitpid(-1, ...)asynchronously — whichever reaped the child first left the other withECHILD. All waiting now goes through theSIGCHLDhandler exclusively. pause()-based wait loops had a lost-wakeup race.while (foreground_pid != 0) pause();can hang forever if theSIGCHLDthat would clear the flag arrives beforepause()is called. Replaced with asigprocmask()+sigsuspend()pattern (wait_for_foreground()) that closes this window.- Jobs only ever showed
argv[0], not the full command.jobsand the[Suspended]notice used to show just the program name (e.g.ls) instead of the full line (e.g.ls -la /tmp). The raw input line is now captured before tokenization and threaded through to the job list andcurrent_fg_cmd. - No way to actually background a job. Nothing parsed a trailing
&;add_job()was only ever called from the Ctrl+Z path. Addedstrip_background()inminishell.cto detect and strip a trailing&and launch the command without waiting. handle_redirection_and_pipingimplemented no redirection. Despite the name, there was no</>/>>handling anywhere. Addedredirection.c(extract_redirection/apply_redirection), wired into both the single-command and each pipeline-stage exec paths.bg's "no such job" message saidfg:. Cosmetic but misleading; fixed to saybg:.- Pipelines whose first word matched a builtin name never actually
piped.
is_builtin_command()only checkedargv[0], soecho hi | grep hwas dispatched straight to theechobuiltin, which printedhi | grep hliterally instead of running a pipeline.execute_command()now checks for a|anywhere on the line first.