Earlier this year, I stumbled upon Stephen Brennan’s tutorial on writing a shell in C. It was clear, hands-on, and piqued my interest in Unix internals and systems programming.
While following along, I started wondering — what if I made this shell more interactive?
That’s how gsh was born: a minimal shell that builds on Brennan’s lsh
with features like:
- Real-time input using raw terminal mode
- Arrow-key command history navigation
- A circular history buffer
- Modular code structure
In this post, I’ll walk you through how it works, why I built it, and what’s coming next.
The Starting Point: Brennan’s Minimal Shell
Brennan’s tutorial provides a compact shell implementation that:
- Reads a line of input using
getline()
- Splits it into tokens with
strtok()
- Implements built-ins like
cd
,help
, andexit
- Uses
fork()
andexecvp()
to run external commands
I kept this structure but added a few upgrades:
- A
Makefile
for easy compilation - A
shell.h
header for prototypes and constants - Better memory safety and error handling
Handling Input: Raw Mode vs. Canonical Mode
By default, terminals operate in canonical mode, buffering input until Enter is pressed. That makes it impossible to respond to arrow keys and other keys in real time.
To enable character-by-character input, I switched the shell into raw mode using termios
:
void enable_raw_mode(struct termios *orig) {
tcgetattr(STDIN_FILENO, orig);
struct termios raw = *orig;
raw.c_lflag &= ~(ICANON | ECHO);
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}
void disable_raw_mode(struct termios *orig) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, orig);
}
These are called at the start and end of gsh_read_line() to enable real-time input.
Decoding Arrow Keys with Escape Sequences
In raw mode, arrow keys emit escape sequences:
↑ → ESC [ A
↓ → ESC [ B
To handle this, I read characters one at a time and check for '\x1b' (Escape):
if (c == '\x1b') {
char seq[2];
seq[0] = getchar();
seq[1] = getchar();
if (seq[0] == '[') {
if (seq[1] == 'A') {
// Up arrow logic
} else if (seq[1] == 'B') {
// Down arrow logic
}
}
continue;
}
This allowed me to respond to arrow presses without needing Enter.
Storing Command History: Circular Buffer
I used a circular buffer to store command history:
#define HISTORY_SIZE 100
char *history[HISTORY_SIZE];
int history_head = 0;
int history_size = 0;
int history_index = -1;
When the user presses Enter, the command is stored like this:
if (history[history_head]) free(history[history_head]);
history[history_head] = strdup(buffer);
history_head = (history_head + 1) % HISTORY_SIZE;
if (history_size < HISTORY_SIZE) history_size++;
history_index = -1;
This way, we can store up to 100 commands and overwrite old ones as needed.
Navigating History with ↑ and ↓
When the user presses ↑, we step backward through the buffer:
if (history_size > 0) {
if (history_index < history_size - 1) history_index++;
int idx = (history_head - 1 - history_index + HISTORY_SIZE) % HISTORY_SIZE;
strcpy(buffer, history[idx]);
position = strlen(buffer);
printf("\33[2K\r%s > %s", getcwd(NULL, 0), buffer);
}
When ↓ is pressed, we move forward toward newer commands:
if (history_index > 0) {
history_index--;
// similar index math
} else {
history_index = -1;
buffer[0] = '\0';
position = 0;
printf("\33[2K\r%s > ", getcwd(NULL, 0));
}
This gives a smooth experience like bash/zsh history browsing.
Executing Commands
Once input is complete, it’s split into tokens and checked against built-ins:
char **args = gsh_split_line(line);
if (!strcmp(args[0], "cd")) {
gsh_cd(args);
} else {
gsh_launch(args);
}
gsh_launch()
uses fork()
and execvp()
to launch programs:
pid_t pid = fork();
if (pid == 0) {
execvp(args[0], args);
perror("gsh"); exit(EXIT_FAILURE);
} else {
waitpid(pid, &status, WUNTRACED);
}
What Works So Far
- Basic shell loop: read → parse → execute
- Built-ins: cd, help, exit
- External command execution
- Arrow key history navigation (↑ / ↓)
- Circular history buffer with wraparound
- Backspace support and basic inline editing
- Clean modular codebase (.c and .h files)
- Build system using Makefile
What’s Next
- Left/right arrow support and cursor movement
- Full in-line editing (insert/delete mode)
- Persistent history (.gsh_history) file
- Output redirection (>, <, >>)
- Pipes (|) and background execution (&)
- Tab completion
- A more colorful prompt: user@host:~/dir$
Credits
Stephen Brennan — Write a Shell in C
Thank you for the amazing tutorial. This project would not exist without it.
Try It Yourself 💻
git clone https://github.com/pgirikishore/gsh.git
cd gsh
make
./gsh
Start typing commands, then use ↑ and ↓ to navigate your command history.
Feedback, forks, and pull requests are all welcome!
Top comments (0)