Last active
May 21, 2026 09:37
-
-
Save douxxtech/77169672f96cd8bd04565b90b730b862 to your computer and use it in GitHub Desktop.
A simple shell implementation in C. https://aka.dbo.one/shell
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| 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