Files
MentOS/programs/shell.c
T
Enrico Fraccaroli (Galfurian) 7ce609fddc Fix compilation with clang 15.0.7
2023-11-28 10:10:28 -05:00

782 lines
25 KiB
C

/// @file shell.c
/// @brief Implement shell functions.
/// @copyright (c) 2014-2023 This file is distributed under the MIT License.
/// See LICENSE.md for details.
#include <sys/unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <libgen.h>
#include <sys/stat.h>
#include <signal.h>
#include <io/debug.h>
#include <io/ansi_colors.h>
#include <sys/bitops.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <strerror.h>
#include <termios.h>
#include <limits.h>
#include <sys/utsname.h>
#include <ctype.h>
/// Maximum length of commands.
#define CMD_LEN 64
/// Maximum lenght of the history.
#define HISTORY_MAX 10
// Required by `export`
#define ENV_NORM 1
#define ENV_BRAK 2
#define ENV_PROT 3
// The input command.
static char cmd[CMD_LEN] = { 0 };
// The index of the cursor.
static size_t cmd_cursor_index = 0;
// History of commands.
static char history[HISTORY_MAX][CMD_LEN] = { 0 };
// The current write index inside the history.
static int history_write_index = 0;
// The current read index inside the history.
static int history_read_index = 0;
// Boolean used to check if the history is full.
static bool_t history_full = false;
static inline int __is_separator(char c)
{
return ((c == '\0') || (c == ' ') || (c == '\t') || (c == '\n') || (c == '\r'));
}
static inline int __count_words(const char *sentence)
{
int result = 0;
bool_t inword = false;
const char *it = sentence;
do {
if (__is_separator(*it)) {
if (inword) {
inword = false;
result++;
}
} else {
inword = true;
}
} while (*it++);
return result;
}
static inline void __set_echo(bool_t active)
{
struct termios _termios;
tcgetattr(STDIN_FILENO, &_termios);
if (active) {
_termios.c_lflag |= (ICANON | ECHO);
} else {
_termios.c_lflag &= ~(ICANON | ECHO);
}
tcsetattr(STDIN_FILENO, 0, &_termios);
}
static inline int __folder_contains(
const char *folder,
const char *entry,
int accepted_type,
dirent_t *result)
{
int fd = open(folder, O_RDONLY | O_DIRECTORY, 0);
if (fd == -1) {
return 0;
}
// Prepare the variables for the search.
dirent_t dent;
size_t entry_len = strlen(entry);
int found = 0;
while (getdents(fd, &dent, sizeof(dirent_t)) == sizeof(dirent_t)) {
if (accepted_type && (accepted_type != dent.d_type)) {
continue;
}
if (strncmp(entry, dent.d_name, entry_len) == 0) {
*result = dent;
found = 1;
break;
}
}
close(fd);
return found;
}
static inline int __search_in_path(const char *entry, dirent_t *result)
{
// Determine the search path.
char *PATH_VAR = getenv("PATH");
if (PATH_VAR == NULL)
PATH_VAR = "/bin:/usr/bin";
// Copy the path.
char *path = strdup(PATH_VAR);
// Iterate through the path entries.
char *token = strtok(path, ":");
if (token == NULL) {
free(path);
return 0;
}
do {
if (__folder_contains(token, entry, DT_REG, result))
return 1;
} while ((token = strtok(NULL, ":")));
free(path);
return 0;
}
/// @brief Prints the prompt.
static inline void __prompt_print(void)
{
// Get the current working directory.
char CWD[PATH_MAX];
getcwd(CWD, PATH_MAX);
// If the current working directory is equal to HOME, show ~.
char *HOME = getenv("HOME");
if (HOME != NULL)
if (strcmp(CWD, HOME) == 0)
strcpy(CWD, "~\0");
// Get the user.
char *USER = getenv("USER");
if (USER == NULL) {
USER = "error";
}
time_t rawtime = time(NULL);
tm_t *timeinfo = localtime(&rawtime);
// Get the hostname.
char *HOSTNAME;
utsname_t buffer;
if (uname(&buffer) < 0) {
HOSTNAME = "error";
} else {
HOSTNAME = buffer.nodename;
}
printf(FG_GREEN "%s" FG_WHITE "@" FG_CYAN "%s " FG_BLUE_BRIGHT "[%02d:%02d:%02d]" FG_WHITE " [%s] " FG_WHITE_BRIGHT "\n-> %% ",
USER, HOSTNAME, timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec, CWD);
}
static void __expand_env(char *str, char *buf, size_t buf_len)
{
// Buffer where we store the name of the variable.
char buffer[BUFSIZ] = { 0 };
// Flags used to keep track of the special characters.
unsigned flags = 0;
// We keep track of where teh
char *env_start = NULL;
// Where we store the retrieved environmental variable value.
char *ENV = NULL;
// Get the length of the string.
size_t str_len = strlen(str);
// Position where we are writing on the buffer.
int b_pos = 0;
// Iterate the string.
for (int s_pos = 0; s_pos < str_len; ++s_pos) {
if ((s_pos == 0) && str[s_pos] == '"')
continue;
if ((s_pos == (str_len - 1)) && str[s_pos] == '"')
continue;
// If we find the backslash, we need to protect the next character.
if (str[s_pos] == '\\') {
if (bit_check(flags, ENV_PROT))
buf[b_pos++] = '\\';
else
bit_set_assign(flags, ENV_PROT);
continue;
}
// If we find the dollar, we need to catch the meaning.
if (str[s_pos] == '$') {
// If the previous character is a backslash, we just need to print the dollar.
if (bit_check(flags, ENV_PROT)) {
buf[b_pos++] = '$';
} else if ((s_pos < (str_len - 2)) && ((str[s_pos + 1] == '{'))) {
// Toggle the open bracket method of accessing env variables.
bit_set_assign(flags, ENV_BRAK);
// We need to skip both the dollar and the open bracket `${`.
env_start = &str[s_pos + 2];
} else {
// Toggle the normal method of accessing env variables.
bit_set_assign(flags, ENV_NORM);
// We need to skip the dollar `$`.
env_start = &str[s_pos + 1];
}
continue;
}
if (bit_check(flags, ENV_BRAK)) {
if (str[s_pos] == '}') {
// Copy the environmental variable name.
strncpy(buffer, env_start, &str[s_pos] - env_start);
// Search for the environmental variable, and print it.
if ((ENV = getenv(buffer)))
for (int k = 0; k < strlen(ENV); ++k)
buf[b_pos++] = ENV[k];
// Remove the flag.
bit_clear_assign(flags, ENV_BRAK);
}
continue;
}
if (bit_check(flags, ENV_NORM)) {
if (str[s_pos] == ':') {
// Copy the environmental variable name.
strncpy(buffer, env_start, &str[s_pos] - env_start);
// Search for the environmental variable, and print it.
if ((ENV = getenv(buffer)))
for (int k = 0; k < strlen(ENV); ++k)
buf[b_pos++] = ENV[k];
// Copy the `:`.
buf[b_pos++] = str[s_pos];
// Remove the flag.
bit_clear_assign(flags, ENV_NORM);
}
continue;
}
buf[b_pos++] = str[s_pos];
}
if (bit_check(flags, ENV_NORM)) {
// Copy the environmental variable name.
strcpy(buffer, env_start);
// Search for the environmental variable, and print it.
if ((ENV = getenv(buffer)))
for (int k = 0; k < strlen(ENV); ++k)
buf[b_pos++] = ENV[k];
// Remove the flag.
bit_clear_assign(flags, ENV_NORM);
}
}
static int __export(int argc, char *argv[])
{
char name[BUFSIZ] = { 0 }, value[BUFSIZ] = { 0 };
char *first, *last;
size_t name_len, value_start;
for (int i = 1; i < argc; ++i) {
// Get a pointer to the first and last occurrence of `=` inside the argument.
first = strchr(argv[i], '='), last = strrchr(argv[i], '=');
// Check validity of first and last, and check that they are the same.
if (!first || !last || (last < argv[i]) || (first != last))
continue;
// Length of the name.
name_len = last - argv[i];
// Set where the value starts.
value_start = (last + 1) - argv[i];
// Copy the name.
strncpy(name, argv[i], name_len);
// Expand the environmental variables inside the argument.
__expand_env(&argv[i][value_start], value, BUFSIZ);
// Try to set the environmental variable.
if ((strlen(name) > 0) && (strlen(value) > 0)) {
if (setenv(name, value, 1) == -1) {
printf("Failed to set environmental variable.\n");
return 1;
}
}
}
return 0;
}
static int __cd(int argc, char *argv[])
{
if (argc > 2) {
printf("%s: too many arguments\n\n", argv[0]);
return 1;
}
const char *path = NULL;
if (argc == 2) {
path = argv[1];
} else {
path = getenv("HOME");
if (path == NULL) {
printf("cd: There is no home directory set.\n\n");
return 1;
}
}
int fd = open(path, O_RDONLY | O_DIRECTORY, S_IXUSR);
if (fd == -1) {
printf("cd: %s\n\n", strerror(errno), path);
return 1;
}
// Set current working directory.
chdir(path);
close(fd);
// Get the updated working directory.
char cwd[PATH_MAX];
getcwd(cwd, PATH_MAX);
// Update the environmental variable.
if (setenv("PWD", cwd, 1) == -1) {
printf("cd: Failed to set current working directory.\n");
return 1;
}
putchar('\n');
return 0;
}
/// @brief Push the command inside the history.
static inline void __hst_push(char *_cmd)
{
// Reset the read index.
history_read_index = history_write_index;
// Check if it is a duplicated entry.
if (history_write_index > 0) {
if (strcmp(history[history_write_index - 1], _cmd) == 0) {
return;
}
}
// Insert the node.
strcpy(history[history_write_index], _cmd);
if (++history_write_index >= HISTORY_MAX) {
history_write_index = 0;
history_full = true;
}
// Reset the read index.
history_read_index = history_write_index;
}
/// @brief Give the key allows to navigate through the history.
static char *__hst_fetch(bool_t up)
{
if ((history_write_index == 0) && (history_full == false)) {
return NULL;
}
// If the history is empty do nothing.
char *_cmd = NULL;
// Update the position inside the history.
int next_index = history_read_index + (up ? -1 : +1);
// Check the next index.
if (history_full) {
if (next_index < 0) {
next_index = HISTORY_MAX - 1;
} else if (next_index >= HISTORY_MAX) {
next_index = 0;
}
// Do no read where ne will have to write next.
if (next_index == history_write_index) {
next_index = history_read_index;
return NULL;
}
} else {
if (next_index < 0) {
next_index = 0;
} else if (next_index >= history_write_index) {
next_index = history_read_index;
return NULL;
}
}
history_read_index = next_index;
_cmd = history[history_read_index];
// Return the command.
return _cmd;
}
/// @brief Completely delete the current command.
static inline void __cmd_clr(void)
{
// First we need to get back to the end of the line.
while (cmd[cmd_cursor_index] != 0) {
++cmd_cursor_index;
puts("\033[1C");
}
// Then we delete all the character.
for (size_t it = 0; it < cmd_cursor_index; ++it) {
putchar('\b');
}
// Reset the index.
cmd_cursor_index = 0;
}
/// @brief Sets the new command.
static inline void __cmd_set(char *_cmd)
{
// Outputs the command.
printf(_cmd);
// Moves the cursore.
cmd_cursor_index += strlen(_cmd);
// Copies the command.
strcpy(cmd, _cmd);
}
/// @brief Erases one character from the console.
static inline void __cmd_ers(char c)
{
if ((c == '\b') && (cmd_cursor_index > 0)) {
strcpy(cmd + cmd_cursor_index - 1, cmd + cmd_cursor_index);
putchar('\b');
--cmd_cursor_index;
} else if ((c == 0x7F) && (cmd[0] != 0) && ((cmd_cursor_index + 1) < CMD_LEN)) {
strcpy(cmd + cmd_cursor_index, cmd + cmd_cursor_index + 1);
putchar(0x7F);
}
}
/// @brief Appends the character `c` on the command.
static inline int __cmd_app(char c)
{
if ((cmd_cursor_index + 1) < CMD_LEN) {
// If at the current index there is a character, shift the entire
// command ahead.
if (cmd[cmd_cursor_index] != 0) {
// Move forward the entire string.
for (unsigned long i = strlen(cmd); i > cmd_cursor_index; --i) {
cmd[i] = cmd[i - 1];
}
}
// Place the new character.
cmd[cmd_cursor_index++] = c;
return 1;
}
return 0;
}
static inline void __cmd_sug(dirent_t *suggestion, size_t starting_position)
{
if (suggestion) {
for (size_t i = starting_position; i < strlen(suggestion->d_name); ++i) {
if (__cmd_app(suggestion->d_name[i])) {
putchar(suggestion->d_name[i]);
}
}
// If we suggested a directory, append a slash.
if (suggestion->d_type == DT_DIR) {
if (__cmd_app('/')) {
putchar('/');
}
}
}
}
/// @brief Gets the inserted command.
static void __cmd_get(void)
{
// Re-Initialize the cursor index.
cmd_cursor_index = 0;
// Initializing the current command line buffer
memset(cmd, '\0', CMD_LEN);
char cwd[PATH_MAX];
getcwd(cwd, PATH_MAX);
__set_echo(false);
do {
int c = getchar();
// Return Key
if (c == '\n') {
putchar('\n');
// Break the while loop.
break;
}
// It is a special character.
if (c == '\033') {
c = getchar();
if (c == '[') {
c = getchar(); // Get the char.
if ((c == 'A') || (c == 'B')) {
char *old_cmd = __hst_fetch(c == 'A');
if (old_cmd != NULL) {
// Clear the current command.
__cmd_clr();
// Sets the command.
__cmd_set(old_cmd);
}
} else if (c == 'D') {
if (cmd_cursor_index > 0) {
--cmd_cursor_index;
puts("\033[1D");
}
} else if (c == 'C') {
if ((cmd_cursor_index + 1) < CMD_LEN && (cmd_cursor_index + 1) <= strlen(cmd)) {
++cmd_cursor_index;
puts("\033[1C");
}
} else if (c == 'H') {
// Move the cursor back to the beginning.
printf("\033[%dD", cmd_cursor_index);
// Reset the cursor position.
cmd_cursor_index = 0;
} else if (c == 'F') {
// Compute the offest to the end of the line, and move only if necessary.
size_t offset = strlen(cmd) - cmd_cursor_index;
if (offset > 0) {
printf("\033[%dC", offset);
// Reset the cursor position.
cmd_cursor_index += offset;
}
} else if (c == '3') {
c = getchar(); // Get the char.
if (c == '~') {
__cmd_ers(0x7F);
}
}
}
} else if (c == '\b') {
__cmd_ers('\b');
} else if (c == '\t') {
// Get the lenght of the command.
size_t cmd_len = strlen(cmd);
// Count the number of words.
int words = __count_words(cmd);
// If there are no words, skip.
if (words == 0) {
continue;
}
// Determines if we are at the beginning of a new argument, last character is space.
if (__is_separator(cmd[cmd_len - 1])) {
continue;
}
// If the last two characters are two dots `..` append a slash `/`,
// and continue.
if ((cmd_len >= 2) && ((cmd[cmd_len - 2] == '.') && (cmd[cmd_len - 1] == '.'))) {
if (__cmd_app('/')) {
putchar('/');
continue;
}
}
// Determines if we are executing a command from current directory.
int is_run_cmd = (words == 1) && (cmd[0] == '.') && (cmd_len > 3) && (cmd[1] == '/');
// Determines if we are entering an absolute path.
int is_abs_path = (words == 1) && (cmd[0] == '/');
// Prepare the dirent variable.
dirent_t dent;
// If there is only one word, we are searching for a command.
if (is_run_cmd) {
if (__folder_contains(cwd, cmd + 2, 0, &dent)) {
__cmd_sug(&dent, cmd_len - 2);
}
} else if (is_abs_path) {
char _dirname[PATH_MAX];
if (!dirname(cmd, _dirname, sizeof(_dirname))) {
continue;
}
const char *_basename = basename(cmd);
if (!_basename) {
continue;
}
if ((*_dirname == 0) || (*_basename == 0)) {
continue;
}
if (__folder_contains(_dirname, _basename, 0, &dent)) {
__cmd_sug(&dent, strlen(_basename));
}
} else if (words == 1) {
if (__search_in_path(cmd, &dent)) {
__cmd_sug(&dent, cmd_len);
}
} else {
// Search the last occurrence of a space, from there on
// we will have the last argument.
char *last_argument = strrchr(cmd, ' ');
// We need to move ahead of one character if we found the space.
last_argument = last_argument ? last_argument + 1 : NULL;
// If there is no last argument.
if (last_argument == NULL) {
continue;
}
char _dirname[PATH_MAX];
if (!dirname(last_argument, _dirname, sizeof(_dirname))) {
continue;
}
const char *_basename = basename(last_argument);
if (!_basename) {
continue;
}
if ((*_dirname != 0) && (*_basename != 0)) {
if (__folder_contains(_dirname, _basename, 0, &dent)) {
__cmd_sug(&dent, strlen(_basename));
}
} else if (*_basename != 0) {
if (__folder_contains(cwd, _basename, 0, &dent)) {
__cmd_sug(&dent, strlen(_basename));
}
}
}
} else if (c == 127) {
if ((cmd_cursor_index + 1) <= strlen(cmd)) {
strcpy(cmd + cmd_cursor_index, cmd + cmd_cursor_index + 1);
putchar(127);
}
} else if (iscntrl(c)) {
if (c == CTRL('C')) {
// Re-set the index to the beginning.
cmd_cursor_index = 0;
// Go to the new line.
printf("\n\n");
// Sets the command.
__cmd_set("\0");
// Break the while loop.
break;
} else if (c == CTRL('U')) {
// Clear the current command.
__cmd_clr();
// Re-set the index to the beginning.
cmd_cursor_index = 0;
// Sets the command.
__cmd_set("\0");
} else if (c == CTRL('D')) {
// Go to the new line.
printf("\n");
exit(0);
}
} else if ((c > 0) && (c != '\n')) {
if (__cmd_app(c)) {
putchar(c);
}
} else {
pr_debug("Unrecognized character %02x (%c)\n", c, c);
}
} while (cmd_cursor_index < CMD_LEN);
// Cleans all blanks at the beginning of the command.
trim(cmd);
__set_echo(true);
}
/// @brief Gets the options from the command.
/// @param command The executed command.
static void __alloc_argv(char *command, int *argc, char ***argv)
{
(*argc) = __count_words(command);
// Get the number of arguments, return if zero.
if ((*argc) == 0) {
return;
}
(*argv) = (char **)malloc(sizeof(char *) * ((*argc) + 1));
bool_t inword = false;
const char *cit = command;
size_t argcIt = 0, argIt = 0;
do {
if (__is_separator(*cit)) {
if (inword) {
inword = false;
(*argv)[argcIt++][argIt] = '\0';
argIt = 0;
}
} else {
// Allocate string for argument.
if (!inword) {
(*argv)[argcIt] = (char *)malloc(sizeof(char) * CMD_LEN);
}
inword = true;
(*argv)[argcIt][argIt++] = (*cit);
}
} while (*cit++);
(*argv)[argcIt] = NULL;
}
static inline void __free_argv(int argc, char **argv)
{
for (int i = 0; i < argc; ++i) {
free(argv[i]);
}
free(argv);
}
void wait_for_child(int signum)
{
wait(NULL);
}
int main(int argc, char *argv[])
{
setsid();
struct termios _termios;
tcgetattr(STDIN_FILENO, &_termios);
_termios.c_lflag &= ~ISIG;
tcsetattr(STDIN_FILENO, 0, &_termios);
char *USER = getenv("USER");
if (USER == NULL) {
printf("shell: There is no user set.\n");
return 1;
}
if (getenv("PATH") == NULL) {
if (setenv("PATH", "/bin:/usr/bin", 0) == -1) {
printf("shell: Failed to set PATH.\n");
return 1;
}
}
// Set the signal handler to handle the termination of the child.
sigaction_t action;
memset(&action, 0, sizeof(action));
action.sa_handler = wait_for_child;
if (sigaction(SIGCHLD, &action, NULL) == -1) {
printf("Failed to set signal handler (%s).\n", SIGCHLD, strerror(errno));
return 1;
}
// Move inside the home directory.
__cd(0, NULL);
#pragma clang diagnostic push
#pragma ide diagnostic ignored "EndlessLoop"
while (true) {
// First print the prompt.
__prompt_print();
// Get the input command.
__cmd_get();
// Check if the command is empty.
if (strlen(cmd) <= 0) {
continue;
}
// Retrieve the options from the command.
// The current number of arguments.
int _argc = 1;
// The vector of arguments.
char **_argv;
__alloc_argv(cmd, &_argc, &_argv);
// Check if the command is empty.
if (_argc == 0) {
continue;
}
// Add the command to the history.
__hst_push(cmd);
if (!strcmp(_argv[0], "init")) {
} else if (!strcmp(_argv[0], "cd")) {
__cd(_argc, _argv);
} else if (!strcmp(_argv[0], "..")) {
const char *__argv[] = { "cd", "..", NULL };
__cd(2, (char **)__argv);
} else if (!strcmp(_argv[0], "export")) {
__export(_argc, _argv);
} else {
bool_t blocking = true;
if (strcmp(_argv[_argc - 1], "&") == 0) {
free(_argv[_argc - 1]);
_argv[_argc - 1] = NULL;
blocking = false;
_argc -= 1;
}
// Is a shell path, execute it!
int status;
pid_t cpid = fork();
if (cpid == 0) {
// Makes the new process a group leader
pid_t pid = getpid();
setpgid(cpid, pid);
if (execvp(_argv[0], _argv) == -1) {
printf("\nUnknown command: %s\n", _argv[0]);
exit(1);
}
}
if (blocking) {
waitpid(cpid, &status, 0);
if (WIFSIGNALED(status)) {
printf(FG_RED "\nExit status %d, killed by signal %d\n" FG_RESET, WEXITSTATUS(status), WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
printf(FG_YELLOW "\nExit status %d, stopped by signal %d\n" FG_RESET, WEXITSTATUS(status), WSTOPSIG(status));
}
}
}
// Free up the memory reserved for the arguments.
__free_argv(_argc, _argv);
}
#pragma clang diagnostic pop
return 0;
}