90s

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

90s.c (16567B)


      1 #include <termios.h>
      2 #include <unistd.h>
      3 #include <stdlib.h>
      4 #include <stdio.h>
      5 #include <string.h>
      6 #include <linux/limits.h>
      7 #include <time.h>
      8 #include <stdbool.h>
      9 #include <signal.h>
     10 #include <ctype.h>
     11 
     12 #include "color.h"
     13 #include "constants.h"
     14 #include "history.h"
     15 #include "commands.h"
     16 
     17 void *memalloc(size_t size) {
     18     void *ptr = malloc(size);
     19     if (!ptr) {
     20         fputs("90s: Error allocating memory\n", stderr);
     21         exit(EXIT_FAILURE);
     22     }
     23     return ptr;
     24 }
     25 
     26 void change_terminal_attribute(int option) {  
     27     static struct termios oldt, newt;
     28     tcgetattr(STDIN_FILENO, &oldt);
     29     if (option) {
     30         newt = oldt;
     31         newt.c_lflag &= ~(ICANON | ECHO); // allows getchar without pressing enter key and echoing the character twice
     32         tcsetattr(STDIN_FILENO, TCSANOW, &newt); // set settings to stdin
     33     } else {
     34         tcsetattr(STDIN_FILENO, TCSANOW, &oldt); // restore to old settings
     35     }
     36 }  
     37 
     38 char **setup_path_variable() {
     39     char *envpath = getenv("PATH");
     40     if (envpath == NULL) {
     41         fprintf(stderr, "90s: PATH environment variable is missing\n");
     42         exit(EXIT_FAILURE);
     43     }
     44     char *path_cpy = memalloc(sizeof(char) * (strlen(envpath) + 1));
     45     char *path = memalloc(sizeof(char) * (strlen(envpath) + 1));
     46     strcpy(path_cpy, envpath);
     47     strcpy(path, envpath);
     48     int path_count = 0;
     49     while (*path_cpy != '\0') {
     50         // count number of : to count number of elements
     51         if (*path_cpy == ':') {
     52             path_count++;
     53         }
     54         path_cpy++;
     55     }
     56     path_count += 2; // adding one to be correct and one for terminator
     57     char **paths = memalloc(sizeof(char *) * path_count);
     58     char *token = strtok(path, ":");
     59     int counter = 0;
     60     while (token != NULL) {
     61         paths[counter] = token; // set element to the pointer of start of path
     62         token = strtok(NULL, ":");
     63         counter++;
     64     }
     65     paths[counter] = NULL;
     66     return paths;
     67 }
     68 
     69 bool find_command(char **paths, char *command) {
     70     if (strncmp(command, "", 1) == 0) {
     71         return false;
     72     }
     73     while (*paths != NULL) {
     74         char current_path[PATH_MAX];
     75         current_path[0] = '\0';
     76         sprintf(current_path, "%s/%s", *paths, command);
     77         if (access(current_path, X_OK) == 0) {
     78             // command is executable
     79             return true;
     80         } else {
     81             if (is_builtin(command)) {
     82                 return true;
     83             }
     84         }
     85         paths++;
     86     }
     87     return false;
     88 }
     89 
     90 void shiftleft(int chars) {
     91     printf("\033[%dD", chars);
     92 }
     93 
     94 void shiftright(int chars) {
     95     printf("\033[%dC", chars);
     96 }
     97 
     98 void clearline() {
     99     printf("\033[K"); // clear line to the right of cursor
    100 }
    101 
    102 void highlight(char *buffer, char **paths) {
    103     char *cmd_part = strchr(buffer, ' ');
    104     char *command_without_arg = NULL;
    105     int cmd_len = 0;
    106     bool valid;
    107 
    108     if (cmd_part != NULL) {
    109         cmd_len = cmd_part - buffer;
    110         char *cmd = memalloc(sizeof(char) * (cmd_len + 1));
    111         command_without_arg = memalloc(sizeof(char) * (cmd_len + 1));
    112         for (int i = 0; i < (cmd_part - buffer); i++) {
    113             cmd[i] = buffer[i];
    114         }
    115         strcpy(command_without_arg, cmd);
    116         cmd[cmd_len] = '\0';
    117         command_without_arg[cmd_len] = '\0';
    118         valid = find_command(paths, cmd);
    119         free(cmd);
    120     } else {
    121         valid = find_command(paths, buffer);
    122     }
    123 
    124     if (valid) {
    125         if (command_without_arg != NULL) {
    126             buffer += cmd_len;
    127             printf("\x1b[38;2;137;180;250m%s\x1b[0m\x1b[38;2;255;255;255m%s\x1b[0m", command_without_arg, buffer); // print green as valid command, but only color the command, not the arguments
    128             buffer -= cmd_len;
    129         } else {
    130             printf("\x1b[38;2;137;180;250m%s\x1b[0m", buffer); // print green as valid command
    131         }
    132     } else {
    133         if (command_without_arg != NULL) {
    134             buffer += cmd_len;
    135             printf("\x1b[38;2;243;139;168m%s\x1b[0m\x1b[38;2;255;255;255m%s\x1b[0m", command_without_arg, buffer); // print green as valid command, but only color the command, not the arguments
    136             buffer -= cmd_len;
    137         } else {
    138             printf("\x1b[38;2;243;139;168m%s\x1b[0m", buffer); // print red as invalid command
    139         }
    140     }
    141     free(command_without_arg);
    142 }
    143 
    144 char *readline(char **paths) {
    145     int bufsize = RL_BUFSIZE;
    146     int position = 0;
    147     char *buffer = memalloc(sizeof(char) * bufsize);
    148 
    149     bool moved = false;
    150     bool backspaced = false;
    151     bool navigated = false;
    152     bool insertatmiddle = false;
    153     bool replaced = false;
    154 
    155     buffer[0] = '\0';
    156     while (1) {
    157         int c = getchar(); // read a character
    158         int buf_len = strlen(buffer);
    159         
    160         // check each character user has input
    161         switch (c) {
    162             case EOF:
    163                 exit(EXIT_SUCCESS);
    164             case 10: {
    165                 // enter/new line feed
    166                 if (buf_len == 0) {
    167                     return NULL;
    168                 }
    169                 // check if command includes !!
    170                 if (strstr(buffer, "!!") != NULL) {
    171                     char *last_command = read_command(1);
    172                     if (last_command != NULL) {
    173                         // replace !! with the last command
    174                         char *replace = strstr(buffer, "!!");
    175                         int replace_len = strlen(replace);
    176                         int last_command_len = strlen(last_command);
    177                         int buffer_len = strlen(buffer);
    178                         if (last_command_len > replace_len) {
    179                             buffer = realloc(buffer, buffer_len + last_command_len - replace_len + 1);
    180                             if (!buffer) {
    181                                 fprintf(stderr, "90s: Error allocating memory\n");
    182                                 exit(EXIT_FAILURE);
    183                             }
    184                         }
    185                         memmove(replace + last_command_len, replace + replace_len, buffer_len - (replace - buffer) - replace_len + 1);
    186                         memcpy(replace, last_command, last_command_len);
    187                         position += last_command_len - replace_len;
    188                         shiftright(last_command_len - replace_len);
    189                         replaced = true;
    190                         break;
    191                     }
    192                 } else {
    193                     buffer[buf_len] = '\0';
    194                     // clear all characters after the command
    195                     for (int start = buf_len + 1; buffer[start] != '\0'; start++) {
    196                         buffer[start] = '\0';
    197                     }
    198                     printf("\n"); // give space for response
    199                     position = 0;
    200                     return buffer;
    201                 }
    202             }
    203             case 127: // backspace
    204                 if (buf_len >= 1) {
    205                     position--;
    206                     for (int i = position; i < buf_len; i++) {
    207                         // shift the buffer
    208                         buffer[i] = buffer[i + 1];
    209                     }
    210                     backspaced = true;
    211                     moved = false;
    212                 }
    213                 break;
    214             case 27: // arrow keys comes at three characters, 27, 91, then 65-68
    215                 if (getchar() == 91) {
    216                     int arrow_key = getchar();
    217                     if (arrow_key == 65) { // up
    218                         // read history file and fill prompt with latest command
    219                         char *last_command = read_command(1);
    220                         if (last_command != NULL) {
    221                             strcpy(buffer, last_command);
    222                         }
    223                         navigated = true;
    224                         moved = false;
    225                         break;
    226                     } else if (arrow_key == 66) { // down
    227                         char *last_command = read_command(0);
    228                         if (last_command != NULL) {
    229                             strcpy(buffer, last_command);
    230                         }
    231                         navigated = true;
    232                         moved = false;
    233                         break;
    234                     } else if (arrow_key == 67) { // right
    235                         if (position < buf_len) {
    236                             shiftright(1);
    237                             position++;
    238                         }
    239                         moved = true;
    240                         break;
    241                     } else if (arrow_key == 68) { // left
    242                         if (position >= 1) {
    243                             shiftleft(1);
    244                             position--;
    245                         }
    246                         moved = true;
    247                         break;
    248                     } 
    249                 }
    250             default:
    251                 if (c > 31 && c < 127) {
    252                     if (position == buf_len) {
    253                         // Append character to the end of the buffer
    254                         buffer[buf_len] = c;
    255                         buffer[buf_len + 1] = '\0';
    256                         moved = false;
    257                         navigated = false;
    258                     } else {
    259                         // Insert character at the current position
    260                         memmove(&buffer[position + 1], &buffer[position], buf_len - position + 1);
    261                         buffer[position] = c;
    262                         shiftright(1);
    263                         insertatmiddle = true;
    264                     }
    265                     position++;
    266                 }
    267         }
    268 
    269         buf_len = strlen(buffer);
    270 
    271         if (replaced) {
    272             shiftleft(buf_len);
    273             clearline();
    274         }
    275 
    276         if (navigated && buf_len >= 1) {
    277             if (position > 0) {
    278                 shiftleft(position);  // move cursor to the beginning
    279                 clearline();
    280             }
    281             position = buf_len;
    282         }
    283 
    284         if (moved) {
    285             moved = false;
    286             continue;
    287         }
    288 
    289         if (!navigated && !replaced) {
    290             if (position != buf_len) {
    291                 // not at normal place
    292                 if (backspaced) {
    293                     shiftleft(position + 1);
    294                 } else {
    295                     shiftleft(position);  // move cursor to the beginning
    296                 }
    297             } else if (buf_len > 1) {
    298                 if (backspaced) {
    299                     shiftleft(buf_len + 1);  // move cursor to the beginning
    300                 } else {
    301                     shiftleft(buf_len - 1);  // move cursor to the beginning
    302                 }
    303             } else if (buf_len == 1) {
    304                 if (backspaced) {
    305                     shiftleft(2);
    306                 }
    307             } else if (buf_len == 0) {
    308                 if (backspaced) {
    309                     shiftleft(1);    
    310                 }
    311             }
    312             clearline();
    313         } else {
    314             navigated = false;
    315         }
    316 
    317         highlight(buffer, paths);
    318 
    319         if (backspaced) {
    320             if (buf_len != position) {
    321                 shiftleft(buf_len - position);
    322             }
    323             backspaced = false;
    324         }
    325         if (insertatmiddle) {
    326             shiftleft(buf_len - position); // move cursor back to where it was
    327             insertatmiddle = false;
    328         }
    329         if (replaced) {
    330             replaced = false;
    331         }
    332 
    333         // If we have exceeded the buffer, reallocate.
    334         if ((buf_len + 1) >= bufsize) {
    335             bufsize += RL_BUFSIZE;
    336             buffer = realloc(buffer, bufsize);
    337             if (!buffer) {
    338                 fprintf(stderr, "90s: Error allocating memory\n");
    339                 exit(EXIT_FAILURE);
    340             }
    341         }
    342     }
    343 }
    344 
    345 // split line into arguments
    346 char **argsplit(char *line) {
    347     int bufsize = TOK_BUFSIZE, position = 0;
    348     char **tokens = memalloc(bufsize * sizeof(char*));
    349     char *token;
    350 
    351     token = strtok(line, TOK_DELIM);
    352     while (token != NULL) {
    353         tokens[position] = token;
    354         position++;
    355 
    356         if (position >= bufsize) {
    357             bufsize += TOK_BUFSIZE;
    358             tokens = realloc(tokens, bufsize * sizeof(char*));
    359             if (!tokens) {
    360                 fprintf(stderr, "90s: Error allocating memory\n");
    361                 exit(EXIT_FAILURE);
    362             }
    363         }
    364 
    365         token = strtok(NULL, TOK_DELIM);
    366     }
    367     tokens[position] = NULL;
    368     return tokens;
    369 }
    370 
    371 char **modifyargs(char **args) {
    372     int num_arg = 0;
    373 
    374     // check if command is ls, diff, or grep, if so, add --color=auto to the arguments
    375     // this is to make ls, diff, and grep have color without user typing it
    376     // this is to make the shell more user friendly
    377     while (args[num_arg] != NULL) {
    378         num_arg++;
    379     }
    380     for (int i = 0; i < num_arg; i++) {
    381         // makes ls and diff and grep have color without user typing it
    382         if (strncmp(args[i], "ls", 2) == 0 || strncmp(args[i], "diff", 4) == 0 || strncmp(args[i], "grep", 4) == 0) {
    383             for (int j = num_arg; j > i; j--) {
    384                 args[j + 1] = args[j];
    385             }
    386             args[i + 1] = "--color=auto";
    387             num_arg++;
    388         }
    389     }
    390 
    391     return args;
    392 }
    393 
    394 char *trimws(char *str) {
    395 	char *end;
    396 	while (isspace((unsigned char) *str))
    397         str++;
    398 	if(*str == 0)
    399 		return str;
    400 	end = str + strlen(str) - 1;
    401 	while (end > str && isspace((unsigned char) *end))
    402         end--;
    403 	*(end+1) = 0;
    404 	return str;
    405 }
    406 
    407 char ***pipe_argsplit(char *line) {
    408     char ***cmdv = memalloc(sizeof(char **) * 128); // 127 commands, 1 for NULL
    409     char **cmds = memalloc(sizeof(char *) * 128); // 127 arguments, 1 for NULL
    410     int num_arg = 0;
    411     char *pipe = strtok(line, "|");
    412     while (pipe != NULL) {
    413         pipe = trimws(pipe);
    414         cmds[num_arg] = strdup(pipe);
    415         pipe = strtok(NULL, "|");
    416         num_arg++;
    417     }
    418     cmds[num_arg] = NULL;
    419 
    420     for (int i = 0; i < num_arg; i++) {
    421         char **splitted = argsplit(cmds[i]);
    422         cmdv[i] = modifyargs(splitted);
    423 
    424     }
    425     cmdv[num_arg] = NULL;
    426     free(cmds);
    427     return cmdv;
    428 }
    429 
    430 // continously prompt for command and execute it
    431 void command_loop(char **paths) {
    432     char *line;
    433     char **args;
    434     int status = 1;
    435 
    436     while (status) {
    437         time_t t = time(NULL);
    438         struct tm* current_time = localtime(&t); // get current time
    439         char timestr[256];
    440         char cwdstr[PATH_MAX];
    441         if (strftime(timestr, sizeof(timestr), "[%H:%M:%S]", current_time) == 0) { // format time string
    442             return;
    443         }
    444         if (getcwd(cwdstr, sizeof(cwdstr)) == NULL) { // get current working directory
    445             return;
    446         }
    447         char time[256];
    448         strcpy(time, timestr);
    449         char *modtime = memalloc(sizeof(char) * 256);
    450         modtime = color_text(time, lavender); // lavender colored time string
    451         char *cwd = memalloc(sizeof(char) * (PATH_MAX + 2));
    452         sprintf(cwd, "[%s]", cwdstr);
    453         cwd = replace_absolute_home(cwd);
    454         cwd = color_text(cwd, pink); // pink colored current directory
    455         char *arrow = memalloc(sizeof(char) * 32);
    456         strcpy(arrow, "ยป");
    457         arrow = color_text(arrow, blue);
    458         printf("%s %s %s ", modtime, cwd, arrow);
    459 
    460         cmd_count = 0; // upward arrow key resets command count
    461         line = readline(paths);
    462         if (line == NULL) {
    463             printf("\n");
    464             continue;
    465         }
    466         save_command_history(line);
    467         bool has_pipe = false;
    468         for (int i = 0; line[i] != '\0'; i++) {
    469             if (line[i] == '|') {
    470                 has_pipe = true;
    471                 break;
    472             }
    473         }
    474         if (has_pipe) {
    475             char ***pipe_args = pipe_argsplit(line);
    476             status = execute_pipe(pipe_args);
    477             while (*pipe_args != NULL) {
    478                 free(*pipe_args);
    479                 pipe_args++;
    480             }
    481         } else {
    482             args = argsplit(line);
    483             args = modifyargs(args);
    484             status = execute(args, STDOUT_FILENO, OPT_FGJ);
    485             free(args);
    486         }
    487         free(line);
    488         free(modtime);
    489         free(cwd);
    490         free(arrow);
    491     };
    492 }
    493 
    494 void quit_sig(int sig) {
    495     exit(EXIT_SUCCESS);
    496 }
    497 
    498 int main(int argc, char **argv) {
    499     // setup
    500     signal(SIGINT, quit_sig);
    501     signal(SIGTERM, quit_sig);
    502     signal(SIGQUIT, quit_sig);
    503     check_history_file();
    504     char **paths = setup_path_variable();
    505     change_terminal_attribute(1); // turn off echoing and disabling getchar requires pressing enter key to return
    506 
    507     command_loop(paths);
    508 
    509     // cleanup
    510     free(paths);
    511     change_terminal_attribute(0); // change back to default settings
    512     return EXIT_SUCCESS;
    513 }