- Updated: March 17, 2026
- 9 min read
Building a Simple C Shell – Introducing andsh
Building a Simple C Toy Shell (andsh): A Hands‑On Guide for System Programmers
Answer: The andsh project is a minimal yet functional Unix‑style shell written in C that demonstrates a REPL loop, tokenization, fork/exec execution, built‑in commands like cd, environment‑variable expansion, pipelines, and integration with the readline library.
Why Build Your Own Shell?
Most developers interact with a shell daily but rarely consider what happens under the hood. Building a toy shell forces you to confront low‑level system calls (fork, execvp, pipe, dup2) and to design a clean read‑eval‑print loop (REPL). If you’re curious about the mechanics behind bash or zsh, the original tutorial by Andrew Healey is an excellent starting point. This article expands on that tutorial, adds context for modern AI‑enhanced development, and shows how you can prototype similar tools on the UBOS homepage.
Project Overview: The andsh Toy Shell
The andsh (short for “Andrew’s shell”) is deliberately lightweight. Its goals are:
- Provide an interactive prompt that reads user input.
- Parse commands into an argument vector (
argv). - Execute external programs via
forkandexecvp. - Support a few built‑ins (
cd,exit). - Expand environment variables like
$HOMEand$?. - Handle simple pipelines using the
|operator. - Offer line editing, history, and tab completion through
readline.
Even though it lacks advanced features such as quoting, redirection, or job control, the shell is fully functional for everyday tasks like ls, grep, and printf pipelines.
Core Features Explained
1. The REPL Loop
The heart of any interactive shell is the REPL (Read‑Eval‑Print Loop). In andsh it looks like this:
int shell_run(Shell *shell) {
char *line = NULL;
size_t capacity = 0;
while (shell->running) {
int rc = read_line(&line, &capacity, shell);
if (rc <= 0) break;
eval_line(shell, line);
}
free(line);
return shell->last_status;
}
This loop continuously reads a line, evaluates it, and prints the result. Signal handling is installed once at startup to keep the shell responsive to Ctrl‑C and Ctrl‑Z.
2. Tokenization and Argument Vector
Before execution, the raw input must be split into tokens. The toy tokenizer is intentionally simple: it separates on whitespace and treats the pipe character | as a distinct token.
static char **tokenize_line(const char *line, int *count_out) {
// Skip leading spaces, split on spaces/tabs, treat ‘|’ as a token.
// Returns a NULL‑terminated array of strings.
}
While this approach cannot handle quoted strings or escaped spaces, it is sufficient for the majority of basic commands.
3. Fork/Exec Execution Model
To run an external command, andsh forks a child process and replaces its image with the target program using execvp. The parent waits for the child to finish, preserving the shell’s state.
pid_t pid = fork();
if (pid == 0) {
execvp(argv[0], argv);
perror(argv[0]); // exec failed
_exit(errno == ENOENT ? 127 : 126);
}
waitpid(pid, &status, 0);
The use of _exit prevents the child from running any atexit handlers that belong to the parent.
4. Built‑in Commands (cd, exit)
Some commands must affect the shell process itself. cd changes the current working directory, and exit terminates the REPL.
if (strcmp(argv[0], "cd") == 0) {
const char *target = (argc == 1) ? getenv("HOME") : argv[1];
if (chdir(target) != 0) perror("cd");
continue;
}
if (strcmp(argv[0], "exit") == 0) {
shell->running = 0;
continue;
}
5. Environment Variable Expansion
Before execution, each token (except the pipe symbol) is examined for a leading $. The shell replaces $HOME with the user’s home directory and $? with the exit status of the previous command.
static char *expand_word(const Shell *shell, const char *word) {
if (strcmp(word, "$?") == 0) {
char buf[16];
snprintf(buf, sizeof(buf), "%d", shell->last_status);
return strdup(buf);
}
if (word[0] != '$') return strdup(word);
const char *val = getenv(word + 1);
return val ? strdup(val) : strdup("");
}
6. Pipeline Handling
When the tokenizer encounters |, it groups commands into a pipeline. For a pipeline of n commands, the shell creates n‑1 pipes and connects the stdout of each command to the stdin of the next.
int prev_read = -1;
for (int i = 0; i < cmd_count; ++i) {
int pipefd[2] = {-1, -1};
if (i + 1 < cmd_count) pipe(pipefd);
pid_t pid = fork();
if (pid == 0) {
if (prev_read != -1) dup2(prev_read, STDIN_FILENO);
if (pipefd[1] != -1) dup2(pipefd[1], STDOUT_FILENO);
// close unused fds …
execvp(cmd[i][0], cmd[i]);
_exit(127);
}
if (prev_read != -1) close(prev_read);
if (pipefd[1] != -1) close(pipefd[1]);
prev_read = pipefd[0];
}
wait_for_all_children();
This logic enables commands like printf abc | tr a-z A-Z | rev to work seamlessly.
7. Readline Integration for a Polished UI
Without readline, the shell would treat every keystroke as raw input, making navigation impossible. By linking against readline, andsh gains:
- Line editing (←, →, Home, End)
- History navigation (↑, ↓)
- Tab completion for files and executables
The integration code is straightforward:
if (shell->interactive) {
free(*line);
*line = readline("andsh$ ");
if (*line && **line) add_history(*line);
}
Missing Features & Future Improvements
While andsh covers many fundamentals, a production‑grade shell would need additional capabilities:
- Quoting & Escaping: Proper handling of single/double quotes and backslashes.
- Redirection: Support for
>,<, and>>to manipulate file descriptors. - Job Control: Background execution with
&,fg,bg, and signal forwarding. - Advanced Built‑ins:
export,alias,source, etc. - Configuration Files: Loading
.profileor.bashrc-style scripts. - Performance Optimizations: Caching
PATHlookups, reducing system calls for tab completion.
Implementing these features would transform the toy into a fully‑featured shell, but each addition also introduces complexity that must be managed carefully.
Technical Deep‑Dive: Selected Code Snippets
Below are concise excerpts that illustrate the most instructive parts of the source.
Shell Structure Definition
typedef struct {
int last_status; // Exit status of last command
int running; // 1 while REPL is active
int interactive; // 1 if attached to a TTY
} Shell;
Reading a Line with Readline
int read_line(char **line, size_t *capacity, Shell *shell) {
if (shell->interactive) {
free(*line);
*line = readline("andsh$ ");
if (!*line) return 0; // EOF (Ctrl‑D)
if (**line) add_history(*line);
return 1;
}
// Fallback to getline() for non‑interactive use
return getline(line, capacity, stdin) != -1;
}
Evaluating a Simple Command
int eval_line(Shell *shell, const char *input) {
if (line_is_blank(input)) return 0;
int word_count;
char **words = tokenize_line(input, &word_count);
// Expand env vars
for (int i = 0; i < word_count; ++i) {
if (strcmp(words[i], "|") == 0) continue;
char *exp = expand_word(shell, words[i]);
free(words[i]); words[i] = exp;
}
// Detect built‑ins
if (strcmp(words[0], "cd") == 0) return run_builtin_cd(shell, words);
if (strcmp(words[0], "exit") == 0) { shell->running = 0; return 0; }
// Otherwise execute external command (or pipeline)
return execute_pipeline(shell, words, word_count);
}
Putting It All Together: A Sample Session
When you compile and run andsh, a typical interaction looks like this:
andsh$ pwd
/Users/alex/projects/andsh
andsh$ echo $HOME
/Users/alex
andsh$ printf abc | tr a-z A-Z | rev
CBA
andsh$ cd /tmp
andsh$ pwd
/tmp
andsh$ nosuchcommand
nosuchcommand: No such file or directory
andsh$ echo $?
127
andsh$ ^D
The session demonstrates directory navigation, environment expansion, pipeline processing, error handling, and graceful termination.
Why Build on UBOS? Leveraging AI‑Powered Development Tools
Developing a shell from scratch is a great learning exercise, but modern SaaS platforms can accelerate prototyping. UBOS platform overview offers a low‑code environment where you can embed C snippets, orchestrate workflows, and instantly expose your tool as a web service.
For example, you could wrap the andsh binary in a Docker container, then use the Workflow automation studio to trigger shell commands based on webhook events, Slack messages, or scheduled jobs.
AI‑Enhanced Extensions
UBOS’s ecosystem includes ready‑made AI integrations that can enrich your shell:
- OpenAI ChatGPT integration – add natural‑language command parsing.
- Chroma DB integration – store command histories with vector search.
- ElevenLabs AI voice integration – speak command output aloud.
- Telegram integration on UBOS – control the shell remotely via a bot.
- ChatGPT and Telegram integration – let ChatGPT suggest commands in chat.
Real‑World Use Cases: From Startups to Enterprises
Whether you’re a solo developer, a growing startup, or an enterprise IT team, a custom shell can solve niche problems.
Startups
Rapid prototyping often requires ad‑hoc scripts. Using UBOS for startups, you can turn those scripts into a unified CLI, enforce security policies, and expose them via a web UI.
SMBs
Small‑to‑medium businesses benefit from UBOS solutions for SMBs that bundle shell automation with monitoring dashboards, reducing manual admin overhead.
Enterprises
Large organizations need a scalable, auditable command platform. The Enterprise AI platform by UBOS provides role‑based access, logging, and AI‑driven command recommendations.
Boost Your Development with UBOS Templates
UBOS’s template marketplace accelerates the creation of AI‑enhanced tools. A few relevant templates you might combine with your shell project include:
- AI SEO Analyzer – embed SEO checks into your CI pipeline.
- AI Article Copywriter – generate documentation from shell output.
- AI Video Generator – turn command‑line demos into tutorial videos.
- AI LinkedIn Post Optimization – automatically craft posts about new shell features.
Conclusion: What You Gain by Building a Toy Shell
Creating andsh forces you to master the core Unix process model, understand how shells parse and execute commands, and appreciate the value of built‑ins versus external programs. By extending the project with UBOS’s AI integrations, you can transform a learning exercise into a production‑ready automation hub that scales from a single developer’s laptop to an enterprise‑wide command platform.
Take the Next Step
Ready to experiment?
- Clone the original repository and explore the code.
- Deploy a containerized version on UBOS homepage using the Web app editor on UBOS.
- Integrate OpenAI ChatGPT integration to add conversational command suggestions.
- Join the UBOS partner program to get early access to new AI modules.
Whether you’re polishing your C skills or building the next AI‑augmented CLI, the journey starts with a simple REPL. Happy hacking!