commit 10d9c9dec9270a1a652b369e5e184d5df4a02fd0
Author: night0721 <[email protected]>
Date: Wed, 31 Jan 2024 01:02:32 +0000
Initial commit
Diffstat:
A | Makefile | | | 4 | ++++ |
A | README.md | | | 36 | ++++++++++++++++++++++++++++++++++++ |
A | color.c | | | 22 | ++++++++++++++++++++++ |
A | color.h | | | 10 | ++++++++++ |
A | constants.h | | | 9 | +++++++++ |
A | history.c | | | 91 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | history.h | | | 8 | ++++++++ |
A | rush | | | 0 | |
A | rush.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;
+}