From c4225f7bdfa84a38b15a105b6dcd70d3636bb373 Mon Sep 17 00:00:00 2001 From: EnderIce2 Date: Fri, 21 Mar 2025 01:27:09 +0000 Subject: [PATCH] feat(userspace/coreutils): improve fennix shell implementation Signed-off-by: EnderIce2 --- Userspace/coreutils/CMakeLists.txt | 8 + Userspace/coreutils/src/sh.c | 714 +++++++++++++++++++++++++---- 2 files changed, 637 insertions(+), 85 deletions(-) diff --git a/Userspace/coreutils/CMakeLists.txt b/Userspace/coreutils/CMakeLists.txt index 074c3606..f9440128 100644 --- a/Userspace/coreutils/CMakeLists.txt +++ b/Userspace/coreutils/CMakeLists.txt @@ -57,3 +57,11 @@ if(UNIX AND TARGET test) \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/bin/[\" )") endif() + +if(UNIX AND TARGET sh) + install(CODE "execute_process( + COMMAND ${CMAKE_COMMAND} -E create_symlink + sh + \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/bin/fsh\" + )") +endif() diff --git a/Userspace/coreutils/src/sh.c b/Userspace/coreutils/src/sh.c index aba8e7f4..7214379b 100644 --- a/Userspace/coreutils/src/sh.c +++ b/Userspace/coreutils/src/sh.c @@ -15,6 +15,7 @@ along with Fennix C Library. If not, see . */ +#define _GNU_SOURCE #include #include #include @@ -27,16 +28,22 @@ #include #include #include +#include +#include +#include +#include +#include #define MAX_LINE_LEN 1024 #define MAX_HISTORY 128 -#define CTRL_KEY(k) ((k) & 0x1f) +#define CTRL_KEY(k) ((k) & 0x1F) +#define MAX_COMPLETIONS 128 typedef struct { char *items[MAX_HISTORY]; int count; - int index; + int Index; } History; typedef enum @@ -49,43 +56,85 @@ typedef struct { char line[MAX_LINE_LEN]; int cursor; - int len; + int length; InputMode mode; History history; - struct termios orig_termios; + struct termios origTermios; + char prompt[PATH_MAX + 128]; + int promptLength; } ShellState; +typedef struct +{ + char *items[MAX_COMPLETIONS]; + int count; +} CompletionList; + +static ShellState GlobalShellState; +static int progIsFsh = 0; + +void DisableRawMode(ShellState *state); +void SaveHistory(ShellState *state); +void InitializeShell(ShellState *state); +void FreeCompletionList(CompletionList *list); +CompletionList GetCompletions(const char *partial); +void DisplayCompletions(ShellState *state, CompletionList *list); +void UpdatePrompt(ShellState *state); +int GetVisibleLength(const char *str); + +void CleanupAndExit(int code) +{ + SaveHistory(&GlobalShellState); + DisableRawMode(&GlobalShellState); + exit(code); +} + void HandleSignalInterrupt(int sig) { (void)sig; - write(STDOUT_FILENO, "\n", 1); + write(STDOUT_FILENO, "\n\r", 2); + CleanupAndExit(130); } void EnableRawMode(ShellState *state) { - tcgetattr(STDIN_FILENO, &state->orig_termios); - struct termios raw = state->orig_termios; - raw.c_lflag &= ~(ECHO | ICANON); + tcgetattr(STDIN_FILENO, &state->origTermios); + struct termios raw = state->origTermios; + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.c_oflag &= ~(OPOST); + raw.c_cflag |= (CS8); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + raw.c_cc[VMIN] = 1; + raw.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); } void DisableRawMode(ShellState *state) { - tcsetattr(STDIN_FILENO, TCSAFLUSH, &state->orig_termios); + tcsetattr(STDIN_FILENO, TCSAFLUSH, &state->origTermios); } void AddHistory(ShellState *state, const char *line) { + if (!line || strlen(line) == 0) + return; + + char *newEntry = strdup(line); + if (!newEntry) + return; + if (state->history.count < MAX_HISTORY) - state->history.items[state->history.count++] = strdup(line); + state->history.items[state->history.count++] = newEntry; else { free(state->history.items[0]); memmove(state->history.items, state->history.items + 1, (MAX_HISTORY - 1) * sizeof(char *)); - state->history.items[MAX_HISTORY - 1] = strdup(line); + state->history.items[MAX_HISTORY - 1] = newEntry; } - state->history.index = state->history.count; + state->history.Index = state->history.count; + + SaveHistory(state); } void LoadHistory(ShellState *state) @@ -94,20 +143,42 @@ void LoadHistory(ShellState *state) if (!home) return; - char path[256]; - snprintf(path, sizeof(path), "%s/.sh_history", home); + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/.fsh_history", home); FILE *fp = fopen(path, "r"); if (!fp) return; + char **tmpHistory = malloc(MAX_HISTORY * sizeof(char *)); + int tmpCount = 0; + char line[MAX_LINE_LEN]; while (fgets(line, sizeof(line), fp)) { - line[strcspn(line, "\n")] = '\0'; - AddHistory(state, line); + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') + line[len - 1] = '\0'; + + if (strlen(line) > 0) + { + if (tmpCount < MAX_HISTORY) + tmpHistory[tmpCount++] = strdup(line); + else + { + free(tmpHistory[0]); + memmove(tmpHistory, tmpHistory + 1, (MAX_HISTORY - 1) * sizeof(char *)); + tmpHistory[MAX_HISTORY - 1] = strdup(line); + } + } } fclose(fp); + + for (int i = 0; i < tmpCount; i++) + state->history.items[i] = tmpHistory[i]; + state->history.count = tmpCount; + state->history.Index = tmpCount; + free(tmpHistory); } void SaveHistory(ShellState *state) @@ -116,18 +187,39 @@ void SaveHistory(ShellState *state) if (!home) return; - char path[256]; - snprintf(path, sizeof(path), "%s/.sh_history", home); + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/.fsh_history", home); - FILE *fp = fopen(path, "w"); - if (!fp) + int fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0600); + if (fd < 0) return; - for (int i = 0; i < state->history.count; i++) + struct flock fl = { + .l_type = F_WRLCK, + .l_whence = SEEK_SET, + .l_start = 0, + .l_len = 0}; + + if (fcntl(fd, F_SETLKW, &fl) == -1) { - fprintf(fp, "%s\n", state->history.items[i]); + close(fd); + return; } - fclose(fp); + + if (state->history.count > 0) + { + int last = state->history.count - 1; + if (state->history.items[last]) + { + char *cmd = state->history.items[last]; + write(fd, cmd, strlen(cmd)); + write(fd, "\n", 1); + } + } + + fl.l_type = F_UNLCK; + fcntl(fd, F_SETLK, &fl); + close(fd); } void ProcessViCommand(ShellState *state, char c) @@ -139,31 +231,27 @@ void ProcessViCommand(ShellState *state, char c) state->cursor--; break; case 'l': - if (state->cursor < state->len) + if (state->cursor < state->length) state->cursor++; break; case 'k': - { - if (state->history.index > 0) + if (state->history.Index > 0) { - state->history.index--; - strcpy(state->line, state->history.items[state->history.index]); - state->len = strlen(state->line); - state->cursor = state->len; + state->history.Index--; + strncpy(state->line, state->history.items[state->history.Index], MAX_LINE_LEN); + state->length = strlen(state->line); + state->cursor = state->length; } break; - } case 'j': - { - if (state->history.index < state->history.count - 1) + if (state->history.Index < state->history.count - 1) { - state->history.index++; - strcpy(state->line, state->history.items[state->history.index]); - state->len = strlen(state->line); - state->cursor = state->len; + state->history.Index++; + strncpy(state->line, state->history.items[state->history.Index], MAX_LINE_LEN); + state->length = strlen(state->line); + state->cursor = state->length; } break; - } case 'i': state->mode = MODE_INSERT; break; @@ -173,94 +261,471 @@ void ProcessViCommand(ShellState *state, char c) } } +void FreeCompletionList(CompletionList *list) +{ + for (int i = 0; i < list->count; i++) + free(list->items[i]); + list->count = 0; +} + +CompletionList GetCompletions(const char *partial) +{ + CompletionList list = {0}; + char *path = getenv("PATH"); + if (!path) + return list; + + char *pathCopy = strdup(path); + char *dir = strtok(pathCopy, ":"); + + while (dir && list.count < MAX_COMPLETIONS) + { + DIR *d = opendir(dir); + if (!d) + { + dir = strtok(NULL, ":"); + continue; + } + + struct dirent *entry; + while ((entry = readdir(d)) && list.count < MAX_COMPLETIONS) + { + if (strncmp(entry->d_name, partial, strlen(partial)) == 0) + { + char fullPath[PATH_MAX]; + snprintf(fullPath, sizeof(fullPath), "%s/%s", dir, entry->d_name); + + struct stat st; + if (stat(fullPath, &st) == 0 && (st.st_mode & S_IXUSR)) + list.items[list.count++] = strdup(entry->d_name); + } + } + closedir(d); + dir = strtok(NULL, ":"); + } + + free(pathCopy); + return list; +} + +void DisplayCompletions(ShellState *state, CompletionList *list) +{ + if (list->count == 0) + return; + + write(STDOUT_FILENO, "\n\r", 2); + + for (int i = 0; i < list->count; i++) + { + write(STDOUT_FILENO, list->items[i], strlen(list->items[i])); + write(STDOUT_FILENO, " ", 2); + } + write(STDOUT_FILENO, "\n\r", 2); + + write(STDOUT_FILENO, state->prompt, strlen(state->prompt)); + write(STDOUT_FILENO, state->line, state->length); +} + void ReadLine(ShellState *state) { - state->len = 0; + state->length = 0; state->cursor = 0; state->line[0] = '\0'; state->mode = MODE_INSERT; - printf("$ "); - fflush(stdout); + UpdatePrompt(state); + + write(STDOUT_FILENO, "\r", 1); + write(STDOUT_FILENO, state->prompt, strlen(state->prompt)); EnableRawMode(state); while (1) { - char c = '\0'; - if (read(STDIN_FILENO, &c, 1) != 1) - break; + char c; + ssize_t nread = read(STDIN_FILENO, &c, 1); + if (nread <= 0) + continue; if (state->mode == MODE_COMMAND) ProcessViCommand(state, c); else { - if (c == CTRL_KEY('c')) + if (c == '\t') { - printf("\n"); + + char *wordStart = state->line; + for (int i = state->cursor - 1; i >= 0; i--) + { + if (state->line[i] == ' ') + { + wordStart = &state->line[i + 1]; + break; + } + } + + char partial[MAX_LINE_LEN]; + int len = state->cursor - (wordStart - state->line); + strncpy(partial, wordStart, len); + partial[len] = '\0'; + + CompletionList completions = GetCompletions(partial); + + if (completions.count == 1) + { + + int restLength = strlen(completions.items[0]) - len; + if (restLength > 0) + { + memmove(&state->line[state->cursor + restLength], + &state->line[state->cursor], + state->length - state->cursor + 1); + memcpy(&state->line[state->cursor], + &completions.items[0][len], + restLength); + state->cursor += restLength; + state->length += restLength; + } + } + else if (completions.count > 1) + { + + int prefixLength = len; + int canExtend = 1; + + while (canExtend) + { + char nextChar = completions.items[0][prefixLength]; + if (nextChar == '\0') + break; + + for (int i = 1; i < completions.count; i++) + { + if (completions.items[i][prefixLength] != nextChar) + { + canExtend = 0; + break; + } + } + if (canExtend) + prefixLength++; + } + + if (prefixLength > len) + { + + int restLength = prefixLength - len; + memmove(&state->line[state->cursor + restLength], + &state->line[state->cursor], + state->length - state->cursor + 1); + memcpy(&state->line[state->cursor], + &completions.items[0][len], + restLength); + state->cursor += restLength; + state->length += restLength; + } + + DisplayCompletions(state, &completions); + } + + FreeCompletionList(&completions); + continue; + } + else if (c == 0x1B) + { + char seq[3]; + if (read(STDIN_FILENO, &seq[0], 1) != 1) + continue; + + if (read(STDIN_FILENO, &seq[1], 1) != 1) + continue; + + if (seq[0] == '[') + { + switch (seq[1]) + { + case 'A': + if (state->history.Index > 0) + { + state->history.Index--; + strncpy(state->line, state->history.items[state->history.Index], MAX_LINE_LEN - 1); + state->length = strlen(state->line); + state->cursor = state->length; + } + break; + case 'B': + if (state->history.Index < state->history.count) + { + state->history.Index++; + if (state->history.Index == state->history.count) + { + state->line[0] = '\0'; + state->length = 0; + state->cursor = 0; + } + else + { + strncpy(state->line, state->history.items[state->history.Index], MAX_LINE_LEN - 1); + state->length = strlen(state->line); + state->cursor = state->length; + } + } + break; + case 'C': + if (state->cursor < state->length) + state->cursor++; + break; + case 'D': + if (state->cursor > 0) + state->cursor--; + break; + case 'H': + state->cursor = 0; + break; + case 'F': + state->cursor = state->length; + break; + case '3': + { + if (read(STDIN_FILENO, &seq[2], 1) != 1) + continue; + + if (seq[2] == '~' && state->cursor < state->length) + { + memmove(&state->line[state->cursor], &state->line[state->cursor + 1], + state->length - state->cursor); + state->length--; + } + break; + } + } + } + else if (seq[0] == 'O') + { + switch (seq[1]) + { + case 'H': + state->cursor = 0; + break; + case 'F': + state->cursor = state->length; + break; + } + } + } + else if (c == CTRL_KEY('c')) + { + write(STDOUT_FILENO, "\n\r", 2); + write(STDOUT_FILENO, state->prompt, strlen(state->prompt)); + state->length = 0; + state->cursor = 0; + state->line[0] = '\0'; + } + else if (c == '\r' || c == '\n') + { + write(STDOUT_FILENO, "\n", 1); + state->line[state->length] = '\0'; + DisableRawMode(state); + if (state->length > 0) + { + AddHistory(state, state->line); + state->history.Index = state->history.count; + } return; } - else if (c == '\n') - { - printf("\n"); - break; - } - else if (c == 127) + else if (c == 0x7F) { if (state->cursor > 0) { - memmove(&state->line[state->cursor - 1], - &state->line[state->cursor], - state->len - state->cursor + 1); + memmove(&state->line[state->cursor - 1], &state->line[state->cursor], + state->length - state->cursor + 1); state->cursor--; - state->len--; + state->length--; } } - else if (state->len < MAX_LINE_LEN - 1) + else if (c == CTRL_KEY('d') && state->length == 0) { - memmove(&state->line[state->cursor + 1], - &state->line[state->cursor], - state->len - state->cursor + 1); + write(STDOUT_FILENO, "\n", 1); + DisableRawMode(state); + exit(EXIT_SUCCESS); + } + else if (isprint(c) && state->length < MAX_LINE_LEN - 1) + { + memmove(&state->line[state->cursor + 1], &state->line[state->cursor], + state->length - state->cursor + 1); state->line[state->cursor] = c; state->cursor++; - state->len++; + state->length++; } } - printf("\r\x1B[2K$ %s", state->line); - fflush(stdout); - printf("\r\x1B[%dC", state->cursor + 3); + write(STDOUT_FILENO, "\r", 1); + write(STDOUT_FILENO, "\x1b[K", 3); + write(STDOUT_FILENO, state->prompt, strlen(state->prompt)); + write(STDOUT_FILENO, state->line, state->length); + + int promptVisibleLength = GetVisibleLength(state->prompt); + char buf[32]; + snprintf(buf, sizeof(buf), "\r\x1b[%dC", promptVisibleLength + state->cursor); + write(STDOUT_FILENO, buf, strlen(buf)); + } +} + +int GetVisibleLength(const char *str) +{ + int len = 0; + int inEscape = 0; + + while (*str) + { + if (*str == '\x1b') + inEscape = 1; + else if (inEscape) + { + if ((*str >= 'A' && *str <= 'Z') || (*str >= 'a' && *str <= 'z')) + inEscape = 0; + } + else + len++; + str++; + } + return len; +} + +void UpdatePrompt(ShellState *state) +{ + char hostname[256] = {0}; + gethostname(hostname, sizeof(hostname)); + + struct passwd *pw = getpwuid(getuid()); + char *username = pw ? pw->pw_name : "user"; + + char cwd[PATH_MAX]; + if (!getcwd(cwd, sizeof(cwd))) + strcpy(cwd, "~"); + + if (pw && pw->pw_dir && strncmp(cwd, pw->pw_dir, strlen(pw->pw_dir)) == 0) + { + size_t home_len = strlen(pw->pw_dir); + if (strlen(cwd) == home_len) + strcpy(cwd, "~"); + else if (cwd[home_len] == '/') + { + memmove(cwd + 1, cwd + home_len, strlen(cwd) - home_len + 1); + cwd[0] = '~'; + } } - DisableRawMode(state); - AddHistory(state, state->line); + char *customPrompt = getenv("SHELL_PROMPT"); + if (customPrompt) + { + snprintf(state->prompt, sizeof(state->prompt), "%s", customPrompt); + } + else + { + // snprintf(state->prompt, sizeof(state->prompt), + // "\x1b[;32m┌──(%s@%s)-[\x1b[0;1m%s\x1b[;32m]\n\r└─\x1b[;32m$\x1b[00m ", + // username, hostname, cwd); + + // snprintf(state->prompt, sizeof(state->prompt), + // "\x1b[;32m%s@%s\x1b[0;1m:\x1b[01;34m%s\x1b[0;1m$\x1b[00m ", + // username, hostname, cwd); + + if (progIsFsh) + { + snprintf(state->prompt, sizeof(state->prompt), + "\x1b[1;34m%s\x1b[0;1m:\x1b[01;35m%s\x1b[0;1m$\x1b[00m ", + username, cwd); + } + else + { + snprintf(state->prompt, sizeof(state->prompt), + "$ "); + } + } + + state->promptLength = GetVisibleLength(state->prompt); } void ExecuteCommand(char **args) { - if (args[0] == NULL) + if (!args[0]) return; + for (int i = 0; args[i] != NULL; i++) + { + char *arg = args[i]; + size_t len = strlen(arg); + if (len >= 2 && arg[0] == '"' && arg[len - 1] == '"') + { + arg[len - 1] = '\0'; + memmove(arg, arg + 1, len - 1); + } + } + if (strcmp(args[0], "exit") == 0) exit(0); - if (strcmp(args[0], "cd") == 0) + else if (strcmp(args[0], "cd") == 0) { - if (args[1] == NULL) - fprintf(stderr, "cd: missing argument\n"); - else if (chdir(args[1])) + char *targetDirectory = args[1]; + if (!targetDirectory || strcmp(targetDirectory, "~") == 0) + { + struct passwd *pw = getpwuid(getuid()); + if (pw && pw->pw_dir) + targetDirectory = pw->pw_dir; + else + { + fprintf(stderr, "cd: HOME not set and no password directory available\n"); + return; + } + } + else if (targetDirectory[0] == '~') + { + struct passwd *pw = getpwuid(getuid()); + if (pw && pw->pw_dir) + { + char newPath[PATH_MAX]; + snprintf(newPath, sizeof(newPath), "%s%s", pw->pw_dir, targetDirectory + 1); + targetDirectory = newPath; + } + } + + if (chdir(targetDirectory)) perror("cd"); + else + UpdatePrompt(&GlobalShellState); return; } pid_t pid = fork(); if (pid == 0) { + struct termios term; + tcgetattr(STDIN_FILENO, &term); + term.c_lflag |= (ECHO | ICANON | IEXTEN | ISIG); + term.c_oflag |= (OPOST | ONLCR); + tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); + write(STDOUT_FILENO, "\r", 1); + execvp(args[0], args); - perror("execvp"); + if (errno == ENOENT) + { + write(STDERR_FILENO, "\r", 1); + fprintf(stderr, "%s: command not found\n", args[0]); + } + else + { + write(STDERR_FILENO, "\r", 1); + perror(args[0]); + } exit(EXIT_FAILURE); } else if (pid > 0) - wait(NULL); + { + int status; + waitpid(pid, &status, 0); + write(STDOUT_FILENO, "\r\n", 2); + } else perror("fork"); } @@ -273,36 +738,113 @@ void ShellLoop(ShellState *state) while (1) { ReadLine(state); - if (state->len == 0) + if (state->length == 0) continue; + char *trimmed = state->line; + while (*trimmed && isspace(*trimmed)) + trimmed++; + + char *end = trimmed + strlen(trimmed) - 1; + while (end > trimmed && isspace(*end)) + *end-- = '\0'; + + if (*trimmed == '\0') + continue; + + memmove(state->line, trimmed, strlen(trimmed) + 1); + state->length = strlen(state->line); + char *args[MAX_LINE_LEN / 2 + 1]; - char *token = strtok(state->line, " "); int i = 0; - while (token != NULL) + char *p = state->line; + int inQuotes = 0; + char *start = p; + + while (*p) { - args[i++] = token; - token = strtok(NULL, " "); + if (*p == '"') + inQuotes = !inQuotes; + else if ((*p == ' ' || *p == '\t') && !inQuotes) + { + if (p > start) + { + *p = '\0'; + args[i++] = start; + } + start = p + 1; + } + p++; } + + if (p > start) + args[i++] = start; args[i] = NULL; - ExecuteCommand(args); + if (i > 0) + ExecuteCommand(args); } - - SaveHistory(state); } void InitializeShell(ShellState *state) { memset(state, 0, sizeof(ShellState)); + UpdatePrompt(state); LoadHistory(state); signal(SIGINT, HandleSignalInterrupt); + + printf("\x1b[01;35mFennix Shell v%s\n\r", PROGRAM_VERSION); + printf("\x1b[;31mEarly development version!\x1b[0m\n\r"); +} + +void DisableRawModeAtExit(void) +{ + DisableRawMode(&GlobalShellState); +} + +void PrintHelp() +{ + printf("Usage: sh [OPTION]... [SCRIPT]\n"); + printf("A simple shell implementation.\n\n"); + printf("Options:\n"); + printf(" -c COMMAND execute COMMAND and exit\n"); + printf(" --help display this help and exit\n"); + printf(" --version output version information and exit\n\n"); + printf("If SCRIPT is provided, execute commands from the script file.\n"); + printf("Otherwise, run in interactive mode.\n\n"); + printf("Environment variables:\n"); + printf(" SHELL_PROMPT custom prompt format (default: user@host:path$ )\n"); } int main(int argc, char *argv[]) { - ShellState state = {0}; - InitializeShell(&state); + if (argc > 1) + { + if (strcmp(argv[1], "--help") == 0) + { + PrintHelp(); + exit(EXIT_SUCCESS); + } + else if (strcmp(argv[1], "--version") == 0) + { + PRINTF_VERSION; + exit(EXIT_SUCCESS); + } + } + + char *basename = strrchr(argv[0], '/'); + if (basename == NULL) + basename = argv[0]; + else + basename++; + if (strcmp(basename, "fsh") == 0) + progIsFsh = 1; + + memset(&GlobalShellState, 0, sizeof(ShellState)); + InitializeShell(&GlobalShellState); + + signal(SIGINT, HandleSignalInterrupt); + atexit(DisableRawModeAtExit); if (argc > 1) { @@ -314,8 +856,9 @@ int main(int argc, char *argv[]) if (!fp) { perror("fopen"); - exit(EXIT_FAILURE); + CleanupAndExit(EXIT_FAILURE); } + char line[MAX_LINE_LEN]; while (fgets(line, sizeof(line), fp)) { @@ -327,7 +870,8 @@ int main(int argc, char *argv[]) } } else - ShellLoop(&state); + ShellLoop(&GlobalShellState); + CleanupAndExit(EXIT_SUCCESS); return EXIT_SUCCESS; }