90s

Minimalist, customizable shell written in C99 with syntax highlighting
git clone https://codeberg.org/night0721/90s
Log | Files | Refs | README | LICENSE

commit 10d9c9dec9270a1a652b369e5e184d5df4a02fd0
Author: night0721 <[email protected]>
Date:   Wed, 31 Jan 2024 01:02:32 +0000

Initial commit

Diffstat:
AMakefile | 4++++
AREADME.md | 36++++++++++++++++++++++++++++++++++++
Acolor.c | 22++++++++++++++++++++++
Acolor.h | 10++++++++++
Aconstants.h | 9+++++++++
Ahistory.c | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahistory.h | 8++++++++
Arush | 0
Arush.c | 362+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 542 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,4 @@ +rush: rush.c color.c constants.h history.c + gcc -o rush rush.c color.c history.c +all: + rush diff --git a/README.md b/README.md @@ -0,0 +1,36 @@ +# rush + +rush is a minimalistic shell for Unix systems written in C. + +# Dependencies +- gcc + +# Building +```sh +$ make +``` + +# Usage +```sh +$ ./rush +``` + +# Features +- Showing current time and directory with custom color +- syntax highlighting on valid commands using ANSI colors +- history navigation using up and down keys + +# Todo Features +- Pipe +- stdin, stdout, stderr redirect +- background jobs +- editing using left and right arrow keys +- history command +- export command to setenv +- tab completion + +# Credits +- [Tutorial - Write a shell in C](https://brennan.io/2015/01/16/write-a-shell-in-c/) +- [dash](https://github.com/danishprakash/dash) +- [Shell assignment](https://www.cs.cornell.edu/courses/cs414/2004su/homework/shell/shell.html) +- [khol](https://github.com/SanketDG/khol/) diff --git a/color.c b/color.c @@ -0,0 +1,22 @@ +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +// color str in place + +void color_text(char str[], const char *color) { + int size = snprintf(NULL, 0, "\x1b[38;2;%sm%s\x1b[0m", color, str) + 1; // calculate size that is needed for colored string + if (size < 0) { + fprintf(stderr, "rush: snprintf failed\n"); + exit(EXIT_FAILURE); + } + char *buf = malloc(size); + if (buf == NULL) { + fprintf(stderr, "rush: Memory allocation failed\n"); + exit(EXIT_FAILURE); + } + + snprintf(buf, size, "\x1b[38;2;%sm%s\x1b[0m", color, str); // format string to buf + strcpy(str, buf); + free(buf); +} diff --git a/color.h b/color.h @@ -0,0 +1,10 @@ +#ifndef COLOR_H_ +#define COLOR_H_ + +const char *lavender = "174;174;255"; +const char *pink = "255;210;239"; +const char *blue = "137;180;250"; + +void color_text(char str[], const char *color); + +#endif diff --git a/constants.h b/constants.h @@ -0,0 +1,9 @@ +#ifndef CONSTANTS_H_ +#define CONSTANTS_H_ + +#define HISTFILE ".rush_history" // history file name +#define TOK_BUFSIZE 64 // buffer size of each token +#define RL_BUFSIZE 1024 +#define TOK_DELIM " \t\r\n\a" // delimiter for token + +#endif diff --git a/history.c b/history.c @@ -0,0 +1,91 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "constants.h" + +FILE *history_file; +char *histfile_path; +int cmd_count = 0; + +void check_history_file() { + char *env_home; + env_home = getenv("XDG_CONFIG_HOME"); + if (env_home == NULL) { + // fallback to $HOME if $XDG_CONFIG_HOME is null + env_home = getenv("HOME"); + } + int env_home_len = strlen(env_home); + int histfilename_len = strlen(HISTFILE); + int path_len = env_home_len + histfilename_len + 2; // 2 for slash and null byte + histfile_path = malloc(sizeof(char) * path_len); + // concatenate home and history file name to a path + strcat(histfile_path, env_home); + strcat(histfile_path, "/"); + strcat(histfile_path, HISTFILE); + histfile_path[path_len] = '\0'; + if (access(histfile_path, F_OK) != 0) { // check for file existence + history_file = fopen(histfile_path, "w"); // read and write, if doesn't exist, create + if (history_file == NULL) { + fprintf(stderr, "rush: Error opening history file\n"); + exit(EXIT_FAILURE); + } + fclose(history_file); + } +} + +void save_command_history(char *command) { + history_file = fopen(histfile_path, "a+"); + if (history_file == NULL) { + fprintf(stderr, "rush: Error opening history file\n"); + exit(EXIT_FAILURE); + } + int cmd_len = strlen(command); + command[cmd_len] = '\n'; // put new line feed to split commands + // ptr to first obj, size of each obj, number of obj, file ptr + fwrite(command, sizeof(char), cmd_len + 1, history_file); + fclose(history_file); +} + +char *read_command(int direction) { + history_file = fopen(histfile_path, "rb"); // read binary mode + if (history_file == NULL) { + fprintf(stderr, "rush: Error opening history file\n"); + exit(EXIT_FAILURE); + } + // normal bufsize is 1024, we serach for 1025 bytes for new line feed + int search_len = RL_BUFSIZE + 1; + char search[search_len]; + fseek(history_file, -search_len, SEEK_END); // go back 1025 characters from end of file + int count = fread(search, 1, search_len - 1, history_file); // try to read 1025 characters from file, returning count number of bytes + search[count] = '\0'; + char *last_nlf = strrchr(search, '\n'); // locate last occurence of \n in a searching string + if (last_nlf == NULL) { + // no history + return NULL; + } + if (direction == 1) { // up + cmd_count++; + } else { // down + if (cmd_count == 0) { + return NULL; + } else { + cmd_count--; + } + } + for (int i = 0; i < cmd_count; i++) { + search[last_nlf - search] = '\0'; // terminate string earlier to find second last \n, search points to first char and last_nlf is last \n, difference is the index of \n + last_nlf = strrchr(search, '\n'); // call strrchr 2 times to get second last new line feed in search string as every life is new line feed + if ((last_nlf - search) == (strchr(search, '\n') - search)) { + // check if the first \n is the last \n we searching for, if yes it is first command + cmd_count--; + search[last_nlf - search] = '\0'; // terminate string earlier to find second last \n, search points to first char and last_nlf is last \n, difference is the index of \n + char *first_cmd = malloc(sizeof(char) * (last_nlf - search) + 1); + strcpy(first_cmd, search); + return first_cmd; + } + } + fclose(history_file); + return last_nlf + 1; // return the string from the new line feed +} diff --git a/history.h b/history.h @@ -0,0 +1,8 @@ +#ifndef HISTORY_H_ +#define HISTORY_H_ + +void save_command_history(char *command); +void check_history_file(); +char *read_command(int direction); + +#endif diff --git a/rush b/rush Binary files differ. diff --git a/rush.c b/rush.c @@ -0,0 +1,362 @@ +#include <sys/wait.h> +#include <termios.h> +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <limits.h> +#include <time.h> +#include <stdbool.h> +#include <errno.h> + +#include "color.h" +#include "constants.h" +#include "history.h" + +/* + * Function Declarations for builtin shell commands: + */ +int rush_cd(char **args); +int help(char **args); +int rush_exit(char **args); + + +/* + * List of builtin commands, followed by their corresponding functions. + */ +char *builtin_str[] = { + "cd", + "help", + "exit" +}; + +int (*builtin_func[]) (char **) = { + &rush_cd, + &help, + &rush_exit +}; + +int rush_num_builtins() { + return sizeof(builtin_str) / sizeof(char *); +} + +// change directory +int rush_cd(char **args) { + if (args[1] == NULL) { + char *home = getenv("HOME"); + if (chdir(home) != 0) { + perror("rush"); + } + } else { + if (chdir(args[1]) != 0) { + perror("rush"); + } + } + return 1; +} + +// show help menu +int help(char **args) { + printf("rush v0.01-alpha\n"); + printf("Built in commands:\n"); + + for (int i = 0; i < rush_num_builtins(); i++) { + printf(" %s\n", builtin_str[i]); + } + + printf("Use 'man' to read manual of programs\n"); + printf("Licensed under GPL v3\n"); + return 1; +} + +int rush_exit(char **args) { + return 0; // exit prompting loop, which also the shell +} + +/** + @brief Launch a program and wait for it to terminate. + @param args Null terminated list of arguments (including program). + @return Always returns 1, to continue execution. + */ +int rush_launch(char **args) { + pid_t pid, wpid; + int status; + + pid = fork(); + if (pid == 0) { + // Child process + if (execvp(args[0], args) == -1) { + if (errno == ENOENT) { + fprintf(stderr, "rush: command not found: %s\n", args[0]); + } + } + exit(EXIT_FAILURE); + } else if (pid < 0) { + perror("Cannot fork"); + } else { + // Parent process + do { + wpid = waitpid(pid, &status, WUNTRACED); + } while (!WIFEXITED(status) && !WIFSIGNALED(status)); + } + + return 1; +} + +/** + @brief Execute shell built-in or launch program. + @param args Null terminated list of arguments. + @return 1 if the shell should continue running, 0 if it should terminate + */ +int rush_execute(char **args) { + if (args[0] == NULL) { + // An empty command was entered. + return 1; + } + + for (int i = 0; i < rush_num_builtins(); i++) { + if (strcmp(args[0], builtin_str[i]) == 0) { + return (*builtin_func[i])(args); + } + } + + return rush_launch(args); +} + +void change_terminal_attribute(int option) { + static struct termios oldt, newt; + tcgetattr(STDIN_FILENO, &oldt); + if (option) { + newt = oldt; + newt.c_lflag &= ~(ICANON | ECHO); // allows getchar without pressing enter key and echoing the character twice + tcsetattr(STDIN_FILENO, TCSANOW, &newt); // set settings to stdin + } else { + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); // restore to old settings + } +} + +char **setup_path_variable() { + char *envpath = getenv("PATH"); + char *path_cpy = malloc(sizeof(char) * (strlen(envpath) + 1)); + char *path = malloc(sizeof(char) * (strlen(envpath) + 1)); + strcpy(path_cpy, envpath); + strcpy(path, envpath); + int path_count = 0; + while (*path_cpy != '\0') { + // count number of : to count number of elements + if (*path_cpy == ':') { + path_count++; + } + path_cpy++; + } + path_count += 2; // adding one to be correct and one for terminator + char **paths = malloc(sizeof(char *) * path_count); + char *token = strtok(path, ":"); + int counter = 0; + while (token != NULL) { + paths[counter] = token; // set element to the pointer of start of path + token = strtok(NULL, ":"); + counter++; + } + paths[counter] = NULL; + return paths; +} + +bool find_command(char **paths, char *command) { + if (strcmp(command, "") == 0) { + return false; + } + int counter = 0; + while (*paths != NULL) { + char current_path[PATH_MAX]; + sprintf(current_path, "%s/%s", *paths, command); + if (access(current_path, X_OK) == 0) { + // command is executable + return true; + } + paths++; + } + return false; +} + +char *rush_read_line(char **paths) { + int bufsize = RL_BUFSIZE; + int position = 0; + char *buffer = malloc(sizeof(char) * bufsize); + int c; + + if (!buffer) { + fprintf(stderr, "rush: allocation error\n"); + exit(EXIT_FAILURE); + } + + buffer[0] = '\0'; + while (1) { + c = getchar(); // read a character + int buf_len = strlen(buffer); + if (buf_len > 0) { + printf("\033[%dD", strlen(buffer)); // move cursor to the beginning + printf("\033[K"); // clear line to the right of cursor + } + // check each character user has input + // printf("%i\n", c); + switch (c) { + case EOF: + exit(EXIT_SUCCESS); + case 10: // enter/new line feed + buffer[buf_len] = '\0'; + // clear all characters after the command + for (int start = buf_len + 1; buffer[start] != '\0'; start++) { + buffer[start] = '\0'; + } + printf("%s\n", buffer); // print back the command in prompt + save_command_history(buffer); + return buffer; + case 127: // backspace + if (buf_len >= 1) { + buffer[buf_len - 1] = '\0'; // putting null character at last character to act as backspace + } + break; + case 27: // arrow keys comes at three characters, 27, 91, then 65-68 + if (getchar() == 91) { + int arrow_key = getchar(); + if (arrow_key == 65) { // up + // read history file and fill prompt with latest command + char *last_command = read_command(1); + if (last_command != NULL) { + strcpy(buffer, last_command); + buf_len = strlen(buffer); + } + break; + } else if (arrow_key == 66) { // down + char *last_command = read_command(0); + if (last_command != NULL) { + strcpy(buffer, last_command); + buf_len = strlen(buffer); + } + break; + } else if (arrow_key == 67) { // right + break; + } else if (arrow_key == 68) { // left + break; + } + } + default: + if (c > 31 && c < 127) { + buffer[buf_len] = c; + buffer[buf_len + 1] = '\0'; // make sure printf don't print random characters + } + } + char *cmd_part = strchr(buffer, ' '); + bool valid; + if (cmd_part != NULL) { + char *cmd = malloc(sizeof(char) * (cmd_part - buffer + 1)); + for (int i = 0; i < (cmd_part - buffer); i++) { + cmd[i] = buffer[i]; + } + cmd[cmd_part - buffer] = '\0'; + valid = find_command(paths, cmd); + } else { + valid = find_command(paths, buffer); + } + if (valid) { + printf("\x1b[38;2;000;255;000m%s\x1b[0m", buffer); // print green as valid command + } else { + printf("\x1b[38;2;255;000;000m%s\x1b[0m", buffer); // print red as sinvalid command + } + fflush(stdout); + + // If we have exceeded the buffer, reallocate. + if ((buf_len + 1) >= bufsize) { + bufsize += RL_BUFSIZE; + buffer = realloc(buffer, bufsize); + if (!buffer) { + fprintf(stderr, "rush: allocation error\n"); + exit(EXIT_FAILURE); + } + } + } +} + +// split line into arguments +char **rush_split_line(char *line) { + int bufsize = TOK_BUFSIZE, position = 0; + char **tokens = malloc(bufsize * sizeof(char*)); + char *token; + + if (!tokens) { + fprintf(stderr, "rush: Allocation error\n"); + exit(EXIT_FAILURE); + } + + token = strtok(line, TOK_DELIM); + while (token != NULL) { + tokens[position] = token; + position++; + + if (position >= bufsize) { + bufsize += TOK_BUFSIZE; + tokens = realloc(tokens, bufsize * sizeof(char*)); + if (!tokens) { + fprintf(stderr, "rush: Allocation error\n"); + exit(EXIT_FAILURE); + } + } + + token = strtok(NULL, TOK_DELIM); + } + tokens[position] = NULL; + return tokens; +} + +// continously prompt for command and execute it +void command_loop(char **paths) { + char *line; + char **args; + int status = 1; + + while (status) { + time_t t = time(NULL); + struct tm* current_time = localtime(&t); // get current time + char timestr[256]; + char cwdstr[PATH_MAX]; + if (strftime(timestr, sizeof(timestr), "[%H:%M:%S]", current_time) == 0) { // format time string + return; + } + if (getcwd(cwdstr, sizeof(cwdstr)) == NULL) { // get current working directory + return; + } + char time[256]; + strcpy(time, timestr); + color_text(time, lavender); // lavender colored time string + char *cwd = malloc(sizeof(char) * PATH_MAX); + sprintf(cwd, "[%s]", cwdstr); + color_text(cwd, pink); // pink colored current directory + char arrow[32] = "ยป"; + color_text(arrow, blue); + printf("%s %s %s ", time, cwd, arrow); + + + line = rush_read_line(paths); + args = rush_split_line(line); + status = rush_execute(args); + + free(line); + free(args); + free(cwd); + }; +} + + +int main(int argc, char **argv) { + // setup + check_history_file(); + char **paths = setup_path_variable(); + change_terminal_attribute(1); // turn off echoing and disabling getchar requires pressing enter key to return + + command_loop(paths); + + // cleanup + change_terminal_attribute(0); // change back to default settings + return EXIT_SUCCESS; +}