Skip to content

Instantly share code, notes, and snippets.

@douxxtech
Last active May 21, 2026 09:37
Show Gist options
  • Select an option

  • Save douxxtech/77169672f96cd8bd04565b90b730b862 to your computer and use it in GitHub Desktop.

Select an option

Save douxxtech/77169672f96cd8bd04565b90b730b862 to your computer and use it in GitHub Desktop.
A simple shell implementation in C. https://aka.dbo.one/shell
/*
A simple shell implementation in C.
https://aka.dbo.one/shell
https://gist.github.com/douxxtech/77169672f96cd8bd04565b90b730b862
*/
#include <glob.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
#define MAX_INPUT 1024
#define MAX_ARGS 64
volatile sig_atomic_t got_sigint = 0;
extern char **environ; // will store the environment vars
// system / environment
char *get_user_home()
{
struct passwd *pw = getpwuid(getuid());
char *home = pw->pw_dir;
return home;
}
char *get_hostname()
{
static char host[256];
if (gethostname(host, sizeof(host)) == -1)
return "unknown";
return host;
}
// if the command already has a slash, it's a path (e.g. ./foo or /bin/ls) — use it directly
char *find_in_path(char *command)
{
if (strchr(command, '/'))
return command;
char *path_env = getenv("PATH");
if (!path_env)
return NULL;
// strtok modifies the string in place, so we dupe it first
char *path_copy = strdup(path_env);
char *dir = strtok(path_copy, ":");
static char full_path[1024];
while (dir != NULL)
{
// build the candidate path: dir + "/" + command
snprintf(full_path, sizeof(full_path), "%s/%s", dir, command);
struct stat sb;
// stat() the candidate, if it exists and is user-executable, we're done
if (stat(full_path, &sb) == 0 && sb.st_mode & S_IXUSR)
{
free(path_copy);
return full_path;
}
dir = strtok(NULL, ":");
}
free(path_copy);
return NULL;
}
// prompt
char get_prompt_char()
{
if (getuid() == 0)
return '#';
return '$';
}
// replaces the home prefix with ~ so the prompt stays short
char *get_curr_dir()
{
char *cwd = getcwd(NULL, 0);
char *home = get_user_home();
// check if cwd starts with home
size_t home_len = strlen(home);
if (strncmp(cwd, home, home_len) == 0)
{
char *result = malloc(strlen(cwd) - home_len + 2);
snprintf(result, strlen(cwd) - home_len + 2, "~%s", cwd + home_len);
free(cwd);
return result;
}
return cwd;
}
// doubles as a SIGINT handler and a normal prompt printer (sig=0 for the latter)
void spawn_prompt(int sig)
{
if (sig == SIGINT)
got_sigint = 1;
// reset terminal to a good state (`su` breaks it with its pwd prompt)
struct termios t;
tcgetattr(STDIN_FILENO, &t);
t.c_lflag |= (ECHO | ICANON); // make sure input is visible and line-bufred
tcsetattr(STDIN_FILENO, TCSANOW, &t);
char *dir = get_curr_dir();
char prompt = get_prompt_char();
char *hostname = get_hostname();
struct passwd *pw = getpwuid(getuid());
printf("\n%s@%s %s %c ", pw->pw_name, hostname, dir, prompt);
free(dir);
fflush(stdout);
}
// parsing
char *expand_tilde(char *str)
{
if (str[0] != '~')
return strdup(str);
char *home = get_user_home();
char *result = malloc(
strlen(home) + strlen(str)); // ~ is replaced so -1 +1 for \0 cancel out
// str+1 skips the ~, so we get home + the rest of the path (e.g. ~/foo -> /home/user/foo)
snprintf(result, strlen(home) + strlen(str), "%s%s", home, str + 1);
return result;
}
// expands a single glob pattern (e.g. *.c) into matching filenames, appends into args[] starting at *i
void expand_glob(char *str, char **args, int *i)
{
glob_t g;
// GLOB_NOCHECK: if nothing matches, glob() returns the pattern itself unchanged
if (glob(str, GLOB_NOCHECK, NULL, &g) != 0)
{
args[(*i)++] = strdup(str); // no match, keep as-is
return;
}
// each match becomes its own arg, same as bash would do
for (size_t j = 0; j < g.gl_pathc && *i < MAX_ARGS - 1; j++)
args[(*i)++] = strdup(g.gl_pathv[j]);
globfree(&g); // free the glob result struct
}
char **parse(char *line)
{
// first pass: raw tokens
char *raw[MAX_ARGS];
int raw_count = 0;
char *token = strtok(line, " \t\n");
while (token != NULL && raw_count < MAX_ARGS - 1)
{
raw[raw_count++] = token;
token = strtok(NULL, " \t\n");
}
// second pass: expand each token
char **args = malloc(MAX_ARGS * sizeof(char *));
int i = 0;
for (int j = 0; j < raw_count && i < MAX_ARGS - 1; j++)
{
char *t = raw[j];
// tilde expand first, then glob
char *tilded = expand_tilde(t);
if (strchr(tilded, '*') || strchr(tilded, '?'))
expand_glob(tilded, args, &i);
else
args[i++] = tilded;
}
args[i] = NULL;
return args;
}
// execution
int execute(char **args)
{
pid_t pid = fork();
if (pid == 0)
{
signal(SIGINT, SIG_DFL); // restore ^C behavior
char *resolved = find_in_path(args[0]);
if (!resolved)
{
fprintf(stderr, "^shell: command not found: %s\n", args[0]);
exit(127);
}
if (execve(resolved, args, environ) == -1)
{
perror("^shell");
exit(126);
}
}
else if (pid > 0)
{
int status;
wait(&status);
if (WIFEXITED(status))
return WEXITSTATUS(status);
return -1;
}
else
{
perror("fork");
return -1;
}
return 0;
}
void append_history(char *line)
{
FILE *history_file = fopen(expand_tilde("~/.cshell_history"), "a");
fprintf(history_file, line);
fclose(history_file);
}
// main loop
int main()
{
signal(SIGINT, spawn_prompt); // call spawn_prompt on ^C
char input[MAX_INPUT];
char **args;
while (1)
{
if (!got_sigint)
spawn_prompt(0); // 0 = not a real signal, just drawing the prompt
got_sigint = 0;
if (!fgets(input, MAX_INPUT, stdin))
{ // capture stdin
printf("\nexit"); // EOF
fflush(stdout);
break;
}
if (input[0] == '\n')
continue;
args = parse(input); // parse input
if (args[0] == NULL)
{
free(args);
continue;
}
// builtins are handled here instead of forking, since they need to
// affect the shell's own state (exit, cwd, etc.)
if (strcmp(args[0], "exit") == 0)
{
free(args);
break;
}
if (strcmp(args[0], "cd") == 0)
{
if (args[1] == NULL)
fprintf(stderr, "myshell: cd: missing argument\n");
else if (chdir(args[1]) == -1) // change dir syscall failed
perror("myshell: cd");
free(args);
continue;
}
int code = execute(args);
if (code != 127 && code != 126)
append_history(input);
free(args); // free the allocated memory for args
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment