toaruos/apps/sh.c

1984 lines
42 KiB
C

/* vim: tabstop=4 shiftwidth=4 noexpandtab
* This file is part of ToaruOS and is released under the terms
* of the NCSA / University of Illinois License - see LICENSE.md
* Copyright (C) 2013-2018 K. Lange
*
* E-Shell
*
* This is "experimental shell" - a vaguely-unix-like command
* interface. It has a very rudimentary parser that understands
* some things like pipes or writing out to a file. It has a
* handful of built-in commands, including ones that implement
* some more useful shell syntax such as loops and conditionals.
* There is support for tab completion of filenames and commands.
*/
#define _XOPEN_SOURCE 500
#define _POSIX_C_SOURCE 200112L
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <dirent.h>
#include <signal.h>
#include <getopt.h>
#include <termios.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sys/stat.h>
#include <toaru/list.h>
#include <toaru/hashmap.h>
#include <toaru/kbd.h>
#include <toaru/rline.h>
#include <toaru/rline_exp.h>
#ifndef environ
extern char **environ;
#endif
#define PIPE_TOKEN "\xFF\xFFPIPE\xFF\xFF"
#define STAR_TOKEN "\xFF\xFFSTAR\xFF\xFF"
#define WRITE_TOKEN "\xFF\xFFWRITE\xFF\xFF"
#define APPEND_TOKEN "\xFF\xFF""APPEND\xFF"
/* A shell command is like a C program */
typedef uint32_t(*shell_command_t) (int argc, char ** argv);
/* We have a static array that fits a certain number of them. */
int SHELL_COMMANDS = 64;
char ** shell_commands; /* Command names */
shell_command_t * shell_pointers; /* Command functions */
char ** shell_descript; /* Command descriptions */
/* This is the number of actual commands installed */
int shell_commands_len = 0;
int shell_interactive = 1;
int last_ret = 0;
char ** shell_argv = NULL;
int shell_argc = 0;
int experimental_rline = 1;
static int current_line = 0;
static char * current_file = NULL;
int pid; /* Process ID of the shell */
int is_subshell = 0;
void set_pgid(int pgid) {
if (shell_interactive == 1) {
setpgid(0, pgid);
}
}
void set_pgrp(int pgid) {
if (shell_interactive == 1 && !is_subshell) {
tcsetpgrp(STDIN_FILENO, pgid);
}
}
void reset_pgrp() {
if (shell_interactive == 1 && !is_subshell) {
tcsetpgrp(STDIN_FILENO, pid);
}
}
void shell_install_command(char * name, shell_command_t func, char * desc) {
if (shell_commands_len == SHELL_COMMANDS) {
SHELL_COMMANDS *= 2;
shell_commands = realloc(shell_commands, sizeof(char *) * SHELL_COMMANDS);
shell_pointers = realloc(shell_pointers, sizeof(shell_command_t) * SHELL_COMMANDS);
shell_descript = realloc(shell_descript, sizeof(char *) * SHELL_COMMANDS);
}
shell_commands[shell_commands_len] = name;
shell_pointers[shell_commands_len] = func;
shell_descript[shell_commands_len] = desc;
shell_commands_len++;
}
shell_command_t shell_find(char * str) {
for (int i = 0; i < shell_commands_len; ++i) {
if (!strcmp(str, shell_commands[i])) {
return shell_pointers[i];
}
}
return NULL;
}
void install_commands();
/* Maximum command length */
#define LINE_LEN 4096
/* Current working directory */
char cwd[1024] = {'/',0};
/* Username */
char username[1024];
/* Hostname for prompt */
char _hostname[256];
/* function to update the cached username */
void getuser() {
char * tmp = getenv("USER");
if (tmp) {
strcpy(username, tmp);
} else {
sprintf(username, "%d", getuid());
}
}
/* function to update the cached hostname */
void gethost() {
struct utsname buf;
uname(&buf);
int len = strlen(buf.nodename);
memcpy(_hostname, buf.nodename, len+1);
}
void print_extended_ps(char * format, char * buffer, int * display_width) {
/* Get the time */
struct tm * timeinfo;
struct timeval now;
gettimeofday(&now, NULL); //time(NULL);
timeinfo = localtime((time_t *)&now.tv_sec);
/* Format the date and time for prompt display */
char date_buffer[80];
strftime(date_buffer, 80, "%m/%d", timeinfo);
char time_buffer[80];
strftime(time_buffer, 80, "%H:%M:%S", timeinfo);
/* Collect the current working directory */
getcwd(cwd, 512);
char _cwd[512];
strcpy(_cwd, cwd);
/* Collect the user's home directory and apply it to cwd */
char * home = getenv("HOME");
if (home && strstr(cwd, home) == cwd) {
char * c = cwd + strlen(home);
if (*c == '/' || *c == 0) {
sprintf(_cwd, "~%s", c);
}
}
char ret[80] = {0};
if (last_ret != 0) {
sprintf(ret, "%d ", last_ret);
}
size_t offset = 0;
int is_visible = 1;
*display_width = 0;
while (*format) {
if (*format == '\\') {
format++;
switch (*format) {
case '\\':
buffer[offset++] = *format;
(*display_width) += is_visible ? 1 : 0;
format++;
break;
case '[':
is_visible = 0;
format++;
break;
case ']':
is_visible = 1;
format++;
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
{
int i = (*format) - '0';
format++;
if (*format >= '0' && *format <= '7') {
i *= 8;
i += (*format) - '0';
format++;
if (*format >= '0' && *format <= '7') {
i *= 8;
i += (*format) - '0';
format++;
}
}
buffer[offset++] = i;
(*display_width) += is_visible ? 1 : 0;
}
break;
case 'e':
buffer[offset++] = '\033';
(*display_width) += is_visible ? 1 : 0;
format++;
break;
case 'd':
{
int size = sprintf(buffer+offset, "%s", date_buffer);
offset += size;
(*display_width) += is_visible ? size : 0;
}
format++;
break;
case 't':
{
int size = sprintf(buffer+offset, "%s", time_buffer);
offset += size;
(*display_width) += is_visible ? size : 0;
}
format++;
break;
case 'h':
{
int size = sprintf(buffer+offset, "%s", _hostname);
offset += size;
(*display_width) += is_visible ? size : 0;
}
format++;
break;
case 'u':
{
int size = sprintf(buffer+offset, "%s", username);
offset += size;
(*display_width) += is_visible ? size : 0;
}
format++;
break;
case 'w':
{
int size = sprintf(buffer+offset, "%s", _cwd);
offset += size;
(*display_width) += is_visible ? size : 0;
}
format++;
break;
case '$':
buffer[offset++] = (getuid() == 0 ? '#' : '$');
(*display_width) += is_visible ? 1 : 0;
format++;
break;
case 'U': /* prompt color string */
{
int size = sprintf(buffer+offset, "%s", getuid() == 0 ? "\033[1;38;5;196m" : "\033[1;38;5;47m");
offset += size;
/* Does not affect size */
}
format++;
break;
case 'r':
{
int size = sprintf(buffer+offset, "%s", ret);
offset += size;
(*display_width) += is_visible ? size : 0;
}
format++;
break;
default:
{
int size = sprintf(buffer+offset, "\\%c", *format);
offset += size;
(*display_width) += is_visible ? size : 0;
}
format++;
break;
}
} else {
buffer[offset++] = *format;
(*display_width) += is_visible ? 1 : 0;
format++;
}
}
buffer[offset] = '\0';
}
#define FALLBACK_PS1 "\\u@\\h \\w\\$ "
/* Draw the user prompt */
void draw_prompt(void) {
char * ps1 = getenv("PS1");
char buf[1024];
int display_width;
print_extended_ps(ps1 ? ps1 : FALLBACK_PS1, buf, &display_width);
fprintf(stdout, "%s", buf);
fflush(stdout);
}
volatile int break_while = 0;
uint32_t child = 0;
pid_t suspended_pid = 0;
hashmap_t * job_hash = NULL;
void sig_pass(int sig) {
/* Interrupt handler */
if (child) {
kill(child, sig);
}
break_while = sig;
}
void redraw_prompt_func(rline_context_t * context) {
draw_prompt();
}
void draw_prompt_c() {
char * ps2 = getenv("PS2");
if (ps2) {
char buf[1024];
int display_width;
print_extended_ps(ps2, buf, &display_width);
fprintf(stdout, "%s", buf);
} else {
printf("> ");
}
fflush(stdout);
}
void redraw_prompt_func_c(rline_context_t * context) {
draw_prompt_c();
}
void tab_complete_func(rline_context_t * c) {
char * dup = malloc(LINE_LEN);
memcpy(dup, c->buffer, LINE_LEN);
char *pch, *save;
char *argv[1024];
int argc = 0;
int cursor = 0;
pch = strtok_r(dup, " ", &save);
if (!pch) {
argv[0] = "";
argc = 0;
}
while (pch != NULL) {
if (pch - dup <= c->offset) cursor = argc;
argv[argc] = pch;
++argc;
pch = strtok_r(NULL, " ", &save);
}
argv[argc] = NULL;
if (c->offset && c->buffer[c->offset-1] == ' ' && argc) {
cursor++;
}
char * word = argv[cursor];
int word_offset = word ? (c->offset - (argv[cursor] - dup)) : 0;
char * prefix = malloc(word_offset + 1);
if (word) memcpy(prefix, word, word_offset);
prefix[word_offset] = '\0';
/* Complete file path */
list_t * matches = list_create();
char * match = NULL;
int free_matches = 0;
int no_space_if_only = 0;
/* TODO custom auto-complete as a configuration file? */
#define COMPLETE_FILE 1
#define COMPLETE_COMMAND 2
#define COMPLETE_CUSTOM 3
#define COMPLETE_VARIABLE 4
int complete_mode = COMPLETE_FILE;
int with_dollar = 0;
int command_adj = 0;
int cursor_adj = cursor;
/* sudo should shift commands */
if (cursor_adj > command_adj && (!strcmp(argv[command_adj], "sudo") || !strcmp(argv[command_adj], "gsudo"))) {
cursor_adj -= 1;
command_adj += 1;
}
/* initial tab completion should be commands, unless typing a file path */
if (cursor_adj == 0 && !strchr(prefix,'/')) {
complete_mode = COMPLETE_COMMAND;
}
if (cursor_adj >= 1 && !strcmp(argv[command_adj], "toggle-abs-mouse")) {
complete_mode = COMPLETE_CUSTOM;
}
if (cursor_adj >= 1 && !strcmp(argv[command_adj], "unset")) {
complete_mode = COMPLETE_VARIABLE;
}
/* complete variable names */
if (*prefix == '$') {
complete_mode = COMPLETE_VARIABLE;
with_dollar = 1;
}
if (complete_mode == COMPLETE_COMMAND) {
/* Complete a command name */
for (int i = 0; i < shell_commands_len; ++i) {
if (strstr(shell_commands[i], prefix) == shell_commands[i]) {
list_insert(matches, shell_commands[i]);
match = shell_commands[i];
}
}
} else if (complete_mode == COMPLETE_FILE) {
/* Complete a file path */
free_matches = 1;
char * tmp = strdup(prefix);
char * last_slash = strrchr(tmp, '/');
DIR * dirp;
char * compare = prefix;
if (last_slash) {
*last_slash = '\0';
word = word + (last_slash - tmp) + 1;
word_offset = word_offset - (last_slash - tmp + 1);
compare = word;
if (last_slash == tmp) {
dirp = opendir("/");
} else {
dirp = opendir(tmp);
}
} else {
dirp = opendir(".");
}
if (!dirp) {
free(tmp);
goto finish_tab;
}
struct dirent * ent = readdir(dirp);
while (ent != NULL) {
if (ent->d_name[0] != '.' || compare[0] == '.') {
if (!word || strstr(ent->d_name, compare) == ent->d_name) {
struct stat statbuf;
/* stat it */
if (last_slash) {
char * x = malloc(strlen(tmp) + 1 + strlen(ent->d_name) + 1);
sprintf(x,"%s/%s",tmp,ent->d_name);
lstat(x, &statbuf);
free(x);
} else {
lstat(ent->d_name, &statbuf);
}
char * s;
if (S_ISDIR(statbuf.st_mode)) {
s = malloc(strlen(ent->d_name) + 2);
sprintf(s,"%s/", ent->d_name);
no_space_if_only = 1;
} else {
s = strdup(ent->d_name);
}
list_insert(matches, s);
match = s;
}
}
ent = readdir(dirp);
}
closedir(dirp);
free(tmp);
} else if (complete_mode == COMPLETE_CUSTOM) {
char ** completions = NULL;
char * toggle_abs_mouse_completions[] = {"relative","absolute",NULL};
if (!strcmp(argv[command_adj],"toggle-abs-mouse")) {
completions = toggle_abs_mouse_completions;
}
while (*completions) {
if (strstr(*completions, prefix) == *completions) {
list_insert(matches, *completions);
match = *completions;
}
completions++;
}
} else if (complete_mode == COMPLETE_VARIABLE) {
char ** envvar = environ;
free_matches = 1;
while (*envvar) {
char * tmp = strdup(*envvar);
char * c = strchr(tmp, '=');
*c = '\0';
if (strstr(tmp, prefix+with_dollar) == tmp) {
char * m = malloc(strlen(tmp)+1+with_dollar);
sprintf(m, "%s%s", with_dollar ? "$" : "", tmp);
list_insert(matches, m);
match = m;
}
free(tmp);
envvar++;
}
}
if (matches->length == 1) {
/* Insert */
rline_insert(c, &match[word_offset]);
if (word && word_offset == (int)strlen(word) && !no_space_if_only) {
rline_insert(c, " ");
}
rline_redraw(c);
} else if (matches->length > 1) {
if (!c->tabbed) {
/* see if there is a minimum subset we can fill in */
size_t j = word_offset;
do {
char d = match[j];
int diff = 0;
foreach(node, matches) {
char * match = (char *)node->value;
if (match[j] != d || match[j] == '\0') diff = 1;
}
if (diff) break;
j++;
} while (j < (size_t)c->requested);
if (j > (size_t)word_offset) {
char * tmp = strdup(match);
tmp[j] = '\0';
rline_insert(c, &tmp[word_offset]);
rline_redraw(c);
free(tmp);
} else {
c->tabbed = 1;
}
} else {
/* Print matches */
fprintf(stderr,"\n\033[0m");
size_t j = 0;
foreach(node, matches) {
char * match = (char *)node->value;
fprintf(stderr, "%s", match);
++j;
if (j < matches->length) {
fprintf(stderr, ", ");
}
}
fprintf(stderr,"\n");
c->callbacks->redraw_prompt(c);
fprintf(stderr, "\033[s");
rline_redraw(c);
}
}
finish_tab:
if (free_matches) list_destroy(matches);
list_free(matches);
free(prefix);
free(dup);
}
void add_argument(list_t * argv, char * buf) {
char * c = malloc(strlen(buf) + 1);
memcpy(c, buf, strlen(buf) + 1);
list_insert(argv, c);
}
int read_entry(char * buffer) {
if (experimental_rline) {
char lprompt[1024], rprompt[1024];
int lwidth, rwidth;
char * ps1 = getenv("PS1_LEFT");
print_extended_ps(ps1 ? ps1 : FALLBACK_PS1, lprompt, &lwidth);
char * ps1r = getenv("PS1_RIGHT");
print_extended_ps(ps1r ? ps1r : "", rprompt, &rwidth);
rline_exit_string="exit";
rline_exp_set_syntax("esh");
rline_exp_set_prompts(lprompt, rprompt, lwidth, rwidth);
rline_exp_set_shell_commands(shell_commands, shell_commands_len);
rline_exp_set_tab_complete_func(tab_complete_func);
return rline_experimental(buffer, LINE_LEN);
} else {
rline_callbacks_t callbacks = {
tab_complete_func, redraw_prompt_func, NULL,
NULL, NULL, NULL, NULL, NULL
};
draw_prompt();
return rline((char *)buffer, LINE_LEN, &callbacks);
}
}
int read_entry_continued(char * buffer) {
if (experimental_rline) {
rline_exit_string="exit";
rline_exp_set_syntax("esh");
rline_exp_set_prompts("> ", "", 2, 0);
rline_exp_set_shell_commands(shell_commands, shell_commands_len);
rline_exp_set_tab_complete_func(tab_complete_func);
return rline_experimental(buffer, LINE_LEN);
} else {
rline_callbacks_t callbacks = {
tab_complete_func, redraw_prompt_func_c, NULL,
NULL, NULL, NULL, NULL, NULL
};
draw_prompt_c();
return rline((char *)buffer, LINE_LEN, &callbacks);
}
}
int variable_char(uint8_t c) {
if (c >= 'A' && c <= 'Z') return 1;
if (c >= 'a' && c <= 'z') return 1;
if (c >= '0' && c <= '9') return 1;
if (c == '_') return 1;
if (c == '?') return 1;
if (c == '$') return 1;
return 0;
}
struct alternative {
const char * command;
const char * replacement;
const char * description;
};
#define ALT_BIM "bim", "vi-like text editor"
#define ALT_FETCH "fetch", "URL downloader"
#define ALT_NETIF "cat /proc/netif", "to see network configuration"
static struct alternative cmd_alternatives[] = {
/* Propose bim as an alternative for common text editors */
{"vim", ALT_BIM},
{"vi", ALT_BIM},
{"emacs", ALT_BIM},
{"nano", ALT_BIM},
/* Propose fetch for some common URL-getting tools */
{"curl", ALT_FETCH},
{"wget", ALT_FETCH},
/* We don't have ip or ifconfig commands, suggest cat /proc/netif */
{"ifconfig", ALT_NETIF},
{"ipconfig", ALT_NETIF},
{"ip", ALT_NETIF},
/* Some random other stuff */
{"grep", "fgrep", "non-regex-capable grep"},
{"more", "bim -", "paging to a text editor"},
{"less", "bim -", "paging to a text editor"},
{NULL, NULL, NULL},
};
void run_cmd(char ** args) {
int i = execvp(*args, args);
shell_command_t func = shell_find(*args);
if (func) {
int argc = 0;
while (args[argc]) {
argc++;
}
i = func(argc, args);
} else {
if (i != 0) {
fprintf(stderr, "%s: Command not found\n", *args);
for (struct alternative * alt = cmd_alternatives; alt->command; alt++) {
if (!strcmp(*args, alt->command)) {
fprintf(stderr, "Consider this alternative:\n\n\t%s -- \033[3m%s\033[0m\n\n",
alt->replacement,
alt->description);
break;
}
}
i = 127;
}
}
exit(i);
}
int is_number(const char * c) {
while (*c) {
if (!isdigit(*c)) return 0;
c++;
}
return 1;
}
/**
* Prints "Segmentation fault", etc.
*/
static void handle_status(int ret_code) {
if (WIFSIGNALED(ret_code)) {
char str[256] = {0};
switch (WTERMSIG(ret_code)) {
case SIGILL:
sprintf(str, "Illegal instruction");
break;
case SIGSEGV:
sprintf(str, "Segmentation fault");
break;
case SIGINT:
/* Do nothing */
return;
case SIGPIPE:
/* Do nothing */
return;
default:
sprintf(str, "Killed by unhandled signal %d",WTERMSIG(ret_code));
break;
}
if (shell_interactive == 1) {
fprintf(stderr, "%s\n", str);
} else if (shell_interactive == 2) {
fprintf(stderr, "%s: line %d: %s\n", current_file, current_line, str);
}
}
}
int shell_exec(char * buffer, size_t size, FILE * file, char ** out_buffer) {
*out_buffer = NULL;
/* Read previous history entries */
if (buffer[0] == '!') {
int x = atoi((char *)((uintptr_t)buffer + 1));
if (x > 0 && x <= rline_history_count) {
buffer = rline_history_get(x - 1);
} else {
fprintf(stderr, "esh: !%d: event not found\n", x);
return 0;
}
}
char * argv[1024];
int tokenid = 0;
char quoted = 0;
char backtick = 0;
char buffer_[512] = {0};
int collected = 0;
int force_collected = 0;
list_t * args = list_create();
int have_star = 0;
while (1) {
char * p = buffer;
while (*p) {
switch (*p) {
case '$':
if (quoted == '\'') {
goto _just_add;
} else {
if (backtick) {
goto _just_add;
}
p++;
char var[100];
int coll = 0;
if (*p == '{') {
p++;
while (*p != '}' && *p != '\0' && (coll < 100)) {
var[coll] = *p;
coll++;
var[coll] = '\0';
p++;
}
if (*p == '}') {
p++;
}
} else {
while (*p != '\0' && variable_char(*p) && (coll < 100)) {
var[coll] = *p;
coll++;
var[coll] = '\0';
if (coll == 0 && (isdigit(*p) || *p == '?')) {
p++;
break; /* Don't let these keep going */
}
p++;
}
}
/* Special cases */
char *c = NULL;
char tmp[128];
if (!strcmp(var, "?")) {
sprintf(tmp,"%d",last_ret);
c = tmp;
} else if (!strcmp(var, "$")) {
sprintf(tmp,"%d",getpid());
c = tmp;
} else if (is_number(var)) {
int a = atoi(var);
if (a >= 0 && a < shell_argc) {
c = shell_argv[a];
}
} else {
c = getenv(var);
}
if (c) {
backtick = 0;
for (int i = 0; i < (int)strlen(c); ++i) {
if (c[i] == ' ' && !quoted) {
/* If we are not quoted and we reach a space, it signals a new argument */
if (collected || force_collected) {
buffer_[collected] = '\0';
add_argument(args, buffer_);
buffer_[0] = '\0';
have_star = 0;
collected = 0;
force_collected = 0;
}
} else {
buffer_[collected] = c[i];
collected++;
}
}
buffer_[collected] = '\0';
}
continue;
}
case '\"':
force_collected = 1;
if (quoted == '\"') {
if (backtick) {
goto _just_add;
}
quoted = 0;
goto _next;
} else if (!quoted) {
if (backtick) {
goto _just_add;
}
quoted = *p;
goto _next;
}
goto _just_add;
case '\'':
force_collected = 1;
if (quoted == '\'') {
if (backtick) {
goto _just_add;
}
quoted = 0;
goto _next;
} else if (!quoted) {
if (backtick) {
goto _just_add;
}
quoted = *p;
goto _next;
}
goto _just_add;
case '*':
if (quoted) {
goto _just_add;
}
if (backtick) {
goto _just_add;
}
if (have_star) {
goto _just_add; /* TODO multiple globs */
}
have_star = 1;
collected += sprintf(&buffer_[collected], STAR_TOKEN);
goto _next;
case '\\':
if (quoted == '\'') {
goto _just_add;
}
if (backtick) {
goto _just_add;
}
backtick = 1;
goto _next;
case ' ':
if (backtick) {
goto _just_add;
}
if (!quoted) {
goto _new_arg;
}
goto _just_add;
case '\n':
if (!quoted) {
goto _done;
}
goto _just_add;
case '|':
if (!quoted && !backtick) {
if (collected || force_collected) {
add_argument(args, buffer_);
}
force_collected = 0;
collected = sprintf(buffer_, "%s", PIPE_TOKEN);
goto _new_arg;
}
goto _just_add;
case '>':
if (!quoted && !backtick) {
if (collected || force_collected) {
add_argument(args, buffer_);
}
force_collected = 0;
collected = sprintf(buffer_, "%s", WRITE_TOKEN);
goto _new_arg;
}
goto _just_add;
case ';':
if (!quoted && !backtick) {
*out_buffer = ++p;
goto _done;
}
case '#':
if (!quoted && !backtick) {
goto _done; /* Support comments; must not be part of an existing arg */
}
goto _just_add;
default:
if (backtick) {
buffer_[collected] = '\\';
collected++;
buffer_[collected] = '\0';
}
_just_add:
backtick = 0;
buffer_[collected] = *p;
collected++;
buffer_[collected] = '\0';
goto _next;
}
_new_arg:
backtick = 0;
if (collected || force_collected) {
add_argument(args, buffer_);
buffer_[0] = '\0';
have_star = 0;
collected = 0;
force_collected = 0;
}
_next:
p++;
}
_done:
if (quoted || backtick) {
backtick = 0;
if (shell_interactive == 1) {
read_entry_continued(buffer);
rline_history_append_line(buffer);
continue;
} else if (shell_interactive == 2) {
fgets(buffer, size, file);
continue;
} else {
fprintf(stderr, "Syntax error: Unterminated quoted string.\n");
return 127;
}
}
if (collected || force_collected) {
add_argument(args, buffer_);
break;
}
break;
}
int cmdi = 0;
char ** arg_starts[100] = { &argv[0], NULL };
char * output_files[100] = { NULL };
int file_args[100] = {0};
int argcs[100] = {0};
int next_is_file = 0;
int i = 0;
foreach(node, args) {
char * c = node->value;
if (next_is_file) {
if (next_is_file == 1 && !strcmp(c, WRITE_TOKEN)) {
next_is_file = 2;
file_args[cmdi] = O_WRONLY | O_CREAT | O_APPEND;
continue;
}
output_files[cmdi] = c;
continue;
}
if (!strcmp(c, WRITE_TOKEN)) {
next_is_file = 1;
file_args[cmdi] = O_WRONLY | O_CREAT | O_TRUNC;
continue;
}
if (!strcmp(c, PIPE_TOKEN)) {
if (arg_starts[cmdi] == &argv[i]) {
fprintf(stderr, "Syntax error: Unexpected pipe token\n");
return 2;
}
argv[i] = 0;
i++;
cmdi++;
arg_starts[cmdi] = &argv[i];
continue;
}
char * glob = strstr(c, STAR_TOKEN);
if (glob) {
/* Globbing */
glob[0] = '\0';
glob[1] = '\0';
char * before = c;
char * after = &glob[8];
char * dir = NULL;
int has_before = !!strlen(before);
int has_after = !!strlen(after);
if (1) {
/* read current directory, add all */
DIR * dirp;
char * prepend = "";
if (has_before) {
dir = strrchr(before,'/');
if (!dir) {
dirp = opendir(".");
} else if (dir == before) {
dirp = opendir("/");
prepend = before;
before++;
} else {
*dir = '\0';
dirp = opendir(before);
prepend = before;
before = dir+1;
}
} else {
dirp = opendir(".");
}
int before_i = i;
if (dirp) {
struct dirent * ent = readdir(dirp);
while (ent != NULL) {
if (ent->d_name[0] != '.') {
char * s = malloc(sizeof(char) * (strlen(ent->d_name) + 1));
memcpy(s, ent->d_name, strlen(ent->d_name) + 1);
char * t = s;
if (has_before) {
if (strstr(s,before) != s) {
free(s);
goto _nope;
}
t = &s[strlen(before)];
}
if (has_after) {
if (strlen(t) >= strlen(after)) {
if (!strcmp(after,&t[strlen(t)-strlen(after)])) {
char * out = malloc(strlen(s) + 2 + strlen(prepend));
sprintf(out,"%s%s%s", prepend, !!*prepend ? "/" : "", s);
argv[i] = out;
i++;
argcs[cmdi]++;
}
}
} else {
char * out = malloc(strlen(s) + 2 + strlen(prepend));
sprintf(out,"%s%s%s", prepend, !!*prepend ? "/" : "", s);
argv[i] = out;
i++;
argcs[cmdi]++;
}
free(s);
}
_nope:
ent = readdir(dirp);
}
closedir(dirp);
}
if (before_i == i) {
/* no matches */
glob[0] = '*';
if (dir) {
*dir = '/';
}
memmove(&glob[1], after, strlen(after)+1);
argv[i] = c;
i++;
argcs[cmdi]++;
} else {
free(c);
}
}
} else {
argv[i] = c;
i++;
argcs[cmdi]++;
}
}
argv[i] = NULL;
if (i == 0) {
return -1;
}
list_free(args);
if (!*arg_starts[cmdi]) {
fprintf(stderr, "Syntax error: Unexpected end of input\n");
return 2;
}
char * cmd = *arg_starts[0];
tokenid = i;
unsigned int child_pid;
int last_child;
int nowait = (!strcmp(argv[tokenid-1],"&"));
if (nowait) {
argv[tokenid-1] = NULL;
}
int pgid = 0;
if (cmdi > 0) {
int last_output[2];
pipe(last_output);
child_pid = fork();
if (!child_pid) {
is_subshell = 1;
set_pgid(0);
dup2(last_output[1], STDOUT_FILENO);
close(last_output[0]);
run_cmd(arg_starts[0]);
}
pgid = child_pid;
for (int j = 1; j < cmdi; ++j) {
int tmp_out[2];
pipe(tmp_out);
if (!fork()) {
is_subshell = 1;
set_pgid(pgid);
dup2(tmp_out[1], STDOUT_FILENO);
dup2(last_output[0], STDIN_FILENO);
close(tmp_out[0]);
close(last_output[1]);
run_cmd(arg_starts[j]);
}
close(last_output[0]);
close(last_output[1]);
last_output[0] = tmp_out[0];
last_output[1] = tmp_out[1];
}
last_child = fork();
if (!last_child) {
is_subshell = 1;
set_pgid(pgid);
if (output_files[cmdi]) {
int fd = open(output_files[cmdi], file_args[cmdi], 0666);
if (fd < 0) {
fprintf(stderr, "sh: %s: %s\n", output_files[cmdi], strerror(errno));
return -1;
} else {
dup2(fd, STDOUT_FILENO);
}
}
dup2(last_output[0], STDIN_FILENO);
close(last_output[1]);
run_cmd(arg_starts[cmdi]);
}
close(last_output[0]);
close(last_output[1]);
/* Now execute the last piece and wait on all of them */
} else {
shell_command_t func = shell_find(*arg_starts[0]);
if (func) {
return func(argcs[0], arg_starts[0]);
} else {
child_pid = fork();
if (!child_pid) {
is_subshell = 1;
set_pgid(0);
if (output_files[cmdi]) {
int fd = open(output_files[cmdi], file_args[cmdi], 0666);
if (fd < 0) {
fprintf(stderr, "sh: %s: %s\n", output_files[cmdi], strerror(errno));
return -1;
} else {
dup2(fd, STDOUT_FILENO);
}
}
run_cmd(arg_starts[0]);
}
pgid = child_pid;
last_child = child_pid;
}
}
set_pgrp(pgid);
int ret_code = 0;
if (!nowait) {
child = child_pid;
int pid;
int tmp;
do {
pid = waitpid(-pgid, &tmp, 0);
if (pid == last_child) ret_code = tmp;
if (WIFSTOPPED(tmp)) {
suspended_pid = pgid;
hashmap_set(job_hash, (void*)pgid, strdup(arg_starts[0][0]));
fprintf(stderr, "[%d] Stopped\t\t%s\n", pgid, arg_starts[0][0]);
break;
}
} while (pid != -1 || (pid == -1 && errno != ECHILD));
child = 0;
}
reset_pgrp();
free(cmd);
handle_status(ret_code);
return WEXITSTATUS(ret_code);
}
void add_path_contents(char * path) {
DIR * dirp = opendir(path);
if (!dirp) return; /* Failed to load directly */
struct dirent * ent = readdir(dirp);
while (ent != NULL) {
if (ent->d_name[0] != '.') {
char * s = malloc(sizeof(char) * (strlen(ent->d_name) + 1));
memcpy(s, ent->d_name, strlen(ent->d_name) + 1);
shell_install_command(s, NULL, NULL);
}
ent = readdir(dirp);
}
closedir(dirp);
}
struct command {
char * string;
void * func;
char * desc;
};
static int comp_shell_commands(const void *p1, const void *p2) {
return strcmp(((struct command *)p1)->string, ((struct command *)p2)->string);
}
void sort_commands() {
struct command commands[SHELL_COMMANDS];
for (int i = 0; i < shell_commands_len; ++i) {
commands[i].string = shell_commands[i];
commands[i].func = shell_pointers[i];
commands[i].desc = shell_descript[i];
}
qsort(&commands, shell_commands_len, sizeof(struct command), comp_shell_commands);
for (int i = 0; i < shell_commands_len; ++i) {
shell_commands[i] = commands[i].string;
shell_pointers[i] = commands[i].func;
shell_descript[i] = commands[i].desc;
}
}
void show_version(void) {
printf("esh 1.3.0\n");
}
void show_usage(int argc, char * argv[]) {
printf(
"Esh: The Experimental Shell\n"
"\n"
"usage: %s [-lha] [path]\n"
"\n"
" -c \033[4mcmd\033[0m \033[3mparse and execute cmd\033[0m\n"
//-c cmd \033[...
" -R \033[3mdisable experimental line editor\033[0m\n"
" -v \033[3mshow version information\033[0m\n"
" -? \033[3mshow this help text\033[0m\n"
"\n", argv[0]);
}
void add_path(void) {
char * envvar = getenv("PATH");
if (!envvar) {
add_path_contents("/bin");
return;
}
char * tmp = strdup(envvar);
do {
char * end = strstr(tmp,":");
if (end) {
*end = '\0';
end++;
}
add_path_contents(tmp);
tmp = end;
} while (tmp);
free(tmp);
}
int run_script(FILE * f) {
current_line = 1;
while (!feof(f)) {
char buf[LINE_LEN] = {0};
fgets(buf, LINE_LEN, f);
int ret;
char * out = NULL;
char * b = buf;
do {
ret = shell_exec(b, LINE_LEN, f, &out);
b = out;
} while (b);
current_line++;
if (ret >= 0) last_ret = ret;
}
fclose(f);
return last_ret;
}
void source_eshrc(void) {
char * home = getenv("HOME");
if (!home) return;
char tmp[512];
sprintf(tmp, "%s/.eshrc", home);
FILE * f = fopen(tmp, "r");
if (!f) return;
current_file = tmp;
run_script(f);
}
int main(int argc, char ** argv) {
pid = getpid();
signal(SIGINT, sig_pass);
signal(SIGWINCH, sig_pass);
job_hash = hashmap_create_int(10);
getuser();
gethost();
install_commands();
if (argc > 1) {
int c;
while ((c = getopt(argc, argv, "Rc:v?")) != -1) {
switch (c) {
case 'R':
experimental_rline = 0;
break;
case 'c':
shell_interactive = 0;
{
char * out = NULL;
do {
last_ret = shell_exec(optarg, strlen(optarg), NULL, &out);
optarg = out;
} while (optarg);
}
return (last_ret == -1) ? 0 : last_ret;
case 'v':
show_version();
return 0;
case '?':
show_usage(argc, argv);
return 0;
}
}
}
if (optind < argc) {
shell_interactive = 2;
FILE * f = fopen(argv[optind],"r");
if (!f) {
fprintf(stderr, "%s: %s: %s\n", argv[0], argv[optind], strerror(errno));
return 1;
}
shell_argc = argc - 1;
shell_argv = &argv[optind];
current_file = argv[optind];
return run_script(f);
}
shell_interactive = 1;
source_eshrc();
add_path();
sort_commands();
while (1) {
char buffer[LINE_LEN] = {0};
read_entry(buffer);
char * history = malloc(strlen(buffer) + 1);
memcpy(history, buffer, strlen(buffer) + 1);
if (buffer[0] != ' ' && buffer[0] != '\n' && buffer[0] != '!') {
rline_history_insert(history);
} else {
free(history);
}
int ret;
char * out = NULL;
char * b = buffer;
do {
ret = shell_exec(b, LINE_LEN, stdin, &out);
b = out;
} while (b);
if (ret >= 0) last_ret = ret;
rline_scroll = 0;
}
return 0;
}
/*
* cd [path]
*/
uint32_t shell_cmd_cd(int argc, char * argv[]) {
if (argc > 1) {
if (chdir(argv[1])) {
goto cd_error;
} /* else success */
} else /* argc < 2 */ {
char * home = getenv("HOME");
if (home) {
if (chdir(home)) {
goto cd_error;
}
} else {
char home_path[512];
sprintf(home_path, "/home/%s", username);
if (chdir(home_path)) {
goto cd_error;
}
}
}
return 0;
cd_error:
fprintf(stderr, "%s: could not cd '%s': no such file or directory\n", argv[0], argv[1]);
return 1;
}
/*
* history
*/
uint32_t shell_cmd_history(int argc, char * argv[]) {
for (int i = 0; i < rline_history_count; ++i) {
printf("%d\t%s\n", i + 1, rline_history_get(i));
}
return 0;
}
uint32_t shell_cmd_export(int argc, char * argv[]) {
if (argc > 1) {
putenv(argv[1]);
}
return 0;
}
uint32_t shell_cmd_exit(int argc, char * argv[]) {
if (argc > 1) {
exit(atoi(argv[1]));
} else {
exit(0);
}
return -1;
}
uint32_t shell_cmd_help(int argc, char * argv[]) {
show_version();
printf("\nThis shell is not POSIX-compliant, please be careful.\n\n");
printf("Built-in commands:\n");
/* First, determine max width of command names */
unsigned int max_len = 0;
for (int i = 0; i < shell_commands_len; ++i) {
if (!shell_descript[i]) continue;
if (strlen(shell_commands[i]) > max_len) {
max_len = strlen(shell_commands[i]);
}
}
/* Then print the commands their help text */
for (int i = 0; i < shell_commands_len; ++i) {
if (!shell_descript[i]) continue;
printf(" %-*s - %s\n", max_len + 1, shell_commands[i], shell_descript[i]);
}
return 0;
}
uint32_t shell_cmd_if(int argc, char * argv[]) {
char ** if_args = &argv[1];
char ** then_args = NULL;
char ** else_args = NULL;
for (int i = 2; i < argc; ++i) {
if (!strcmp(argv[i],"then")) {
argv[i] = NULL;
then_args = &argv[i+1];
} else if (!strcmp(argv[i],"else")) {
argv[i] = NULL;
else_args = &argv[i+1];
}
}
if (!then_args) {
fprintf(stderr, "%s: syntax error: expected 'then' clause\n", argv[0]);
return 1;
}
if (else_args && else_args < then_args) {
fprintf(stderr, "%s: syntax error: 'else' clause before 'then' clase\n", argv[0]);
return 1;
}
pid_t child_pid = fork();
if (!child_pid) {
is_subshell = 1;
set_pgid(0);
run_cmd(if_args);
}
set_pgrp(child_pid);
child = child_pid;
int pid, ret_code = 0;
do {
pid = waitpid(-1, &ret_code, 0);
} while (pid != -1 || (pid == -1 && errno != ECHILD));
child = 0;
handle_status(ret_code);
if (WEXITSTATUS(ret_code) == 0) {
shell_command_t func = shell_find(*then_args);
if (func) {
int argc = 0;
while (then_args[argc]) {
argc++;
}
return func(argc, then_args);
} else {
child_pid = fork();
if (!child_pid) {
is_subshell = 1;
set_pgid(0);
run_cmd(then_args);
}
set_pgrp(child_pid);
child = child_pid;
do {
pid = waitpid(-1, &ret_code, 0);
} while (pid != -1 || (pid == -1 && errno != ECHILD));
child = 0;
reset_pgrp();
handle_status(ret_code);
return WEXITSTATUS(ret_code);
}
} else if (else_args) {
shell_command_t func = shell_find(*else_args);
if (func) {
int argc = 0;
while (else_args[argc]) {
argc++;
}
return func(argc, else_args);
} else {
child_pid = fork();
if (!child_pid) {
is_subshell = 1;
set_pgid(0);
run_cmd(else_args);
}
set_pgrp(child_pid);
child = child_pid;
do {
pid = waitpid(-1, &ret_code, 0);
} while (pid != -1 || (pid == -1 && errno != ECHILD));
child = 0;
handle_status(ret_code);
return WEXITSTATUS(ret_code);
}
}
reset_pgrp();
return 0;
}
uint32_t shell_cmd_while(int argc, char * argv[]) {
char ** while_args = &argv[1];
char ** do_args = NULL;
for (int i = 2; i < argc; ++i) {
if (!strcmp(argv[i],"do")) {
argv[i] = NULL;
do_args = &argv[i+1];
}
}
if (!do_args) {
fprintf(stderr, "%s: syntax error: expected 'do' clause\n", argv[0]);
return 1;
}
break_while = 0;
reset_pgrp();
do {
pid_t child_pid = fork();
if (!child_pid) {
is_subshell = 1;
set_pgid(0);
run_cmd(while_args);
}
set_pgrp(child_pid);
child = child_pid;
int pid, ret_code = 0;
do {
pid = waitpid(-1, &ret_code, 0);
} while (pid != -1 || (pid == -1 && errno != ECHILD));
child = 0;
handle_status(ret_code);
if (WEXITSTATUS(ret_code) == 0) {
child_pid = fork();
if (!child_pid) {
is_subshell = 1;
set_pgid(0);
run_cmd(do_args);
}
child = child_pid;
do {
pid = waitpid(-1, &ret_code, 0);
} while (pid != -1 || (pid == -1 && errno != ECHILD));
child = 0;
} else {
reset_pgrp();
return WEXITSTATUS(ret_code);
}
} while (!break_while);
return 127;
}
uint32_t shell_cmd_export_cmd(int argc, char * argv[]) {
if (argc < 3) {
fprintf(stderr, "%s: syntax error: not enough arguments\n", argv[0]);
return 1;
}
int pipe_fds[2];
pipe(pipe_fds);
pid_t child_pid = fork();
if (!child_pid) {
is_subshell = 1;
set_pgid(0);
dup2(pipe_fds[1], STDOUT_FILENO);
close(pipe_fds[0]);
run_cmd(&argv[2]);
}
close(pipe_fds[1]);
set_pgrp(child_pid);
char buf[1024];
size_t accum = 0;
do {
int r = read(pipe_fds[0], buf+accum, 1023-accum);
if (r == 0) break;
if (r < 0) {
return -r;
}
accum += r;
} while (accum < 1023);
reset_pgrp();
buf[accum] = '\0';
if (accum && buf[accum-1] == '\n') {
buf[accum-1] = '\0';
}
setenv(argv[1], buf, 1);
return 0;
}
uint32_t shell_cmd_empty(int argc, char * argv[]) {
for (int i = 1; i < argc; i++) {
if (argv[i] && *argv[i]) return 1;
}
return 0;
}
uint32_t shell_cmd_equals(int argc, char * argv[]) {
if (argc < 3) return 1;
return !!strcmp(argv[1], argv[2]);
}
uint32_t shell_cmd_return(int argc, char * argv[]) {
if (argc < 2) return 0;
return atoi(argv[1]);
}
uint32_t shell_cmd_source(int argc, char * argv[]) {
if (argc < 2) return 0;
FILE * f = fopen(argv[1], "r");
if (!f) {
fprintf(stderr, "%s: %s: %s", argv[0], argv[1], strerror(errno));
}
current_file = argv[1];
return run_script(f);
}
uint32_t shell_cmd_exec(int argc, char * argv[]) {
if (argc < 2) return 1;
return execvp(argv[1], &argv[1]);
}
uint32_t shell_cmd_not(int argc, char * argv[]) {
if (argc < 2) {
fprintf(stderr, "%s: expected command argument\n", argv[0]);
return 1;
}
int ret_code = 0;
pid_t child_pid = fork();
if (!child_pid) {
is_subshell = 1;
set_pgid(0);
run_cmd(&argv[1]);
}
set_pgrp(child_pid);
child = child_pid;
do {
pid = waitpid(-1, &ret_code, 0);
} while (pid != -1 || (pid == -1 && errno != ECHILD));
child = 0;
reset_pgrp();
handle_status(ret_code);
return !WEXITSTATUS(ret_code);
}
uint32_t shell_cmd_unset(int argc, char * argv[]) {
if (argc < 2) {
fprintf(stderr, "%s: expected command argument\n", argv[0]);
return 1;
}
return unsetenv(argv[1]);
}
uint32_t shell_cmd_read(int argc, char * argv[]) {
int raw = 0;
int i = 1;
char * var = "REPLY";
if (i < argc && !strcmp(argv[i], "-r")) {
raw = 1;
i++;
}
if (i < argc) {
var = argv[i];
}
char tmp[4096];
fgets(tmp, 4096, stdin);
if (*tmp && tmp[strlen(tmp)-1] == '\n') {
tmp[strlen(tmp)-1] = '\0';
}
if (raw) {
setenv(var, tmp, 1);
return 0;
}
char tmp2[4096] = {0};
char * out = tmp2;
char * in = tmp;
/* TODO: This needs to actually read more if a \ at the end of the line is found */
while (*in) {
if (*in == '\\') {
in++;
if (*in == '\n') {
in++;
}
} else {
*out = *in;
out++;
in++;
}
}
setenv(var, tmp2, 1);
return 0;
}
uint32_t shell_cmd_fg(int argc, char * argv[]) {
int ret_code;
int pid;
if (argc < 2) {
if (!suspended_pid) {
list_t * keys = hashmap_keys(job_hash);
foreach(node, keys) {
suspended_pid = (int)node->value;
break;
}
list_free(keys);
free(keys);
if (!suspended_pid) {
fprintf(stderr, "no current job\n");
return 1;
}
}
pid = suspended_pid;
} else {
pid = atoi(argv[1]);
}
if (!hashmap_has(job_hash, (void*)pid)) {
fprintf(stderr, "invalid job");
return 0;
}
if (kill(-pid, SIGCONT) < 0) {
fprintf(stderr, "no current job / bad pid\n");
hashmap_remove(job_hash, (void*)pid);
return 1;
}
set_pgrp(pid);
child = pid;
int outpid;
do {
outpid = waitpid(-pid, &ret_code, 0);
if (WIFSTOPPED(ret_code)) {
suspended_pid = pid;
fprintf(stderr, "[%d] Stopped\t\t%s\n", pid, (char*)hashmap_get(job_hash, (void*)pid));
break;
} else {
suspended_pid = 0;
hashmap_remove(job_hash, (void*)pid);
}
} while (outpid != -1 || (outpid == -1 && errno != ECHILD));
child = 0;
reset_pgrp();
handle_status(ret_code);
return WEXITSTATUS(ret_code);
}
uint32_t shell_cmd_jobs(int argc, char * argv[]) {
list_t * keys = hashmap_keys(job_hash);
foreach(node, keys) {
int pid = (int)node->value;
char * c = hashmap_get(job_hash, (void*)pid);
fprintf(stdout, "%5d %s\n", pid, c);
}
list_free(keys);
free(keys);
return 0;
}
void install_commands() {
shell_commands = malloc(sizeof(char *) * SHELL_COMMANDS);
shell_pointers = malloc(sizeof(shell_command_t) * SHELL_COMMANDS);
shell_descript = malloc(sizeof(char *) * SHELL_COMMANDS);
shell_install_command("cd", shell_cmd_cd, "change directory");
shell_install_command("exit", shell_cmd_exit, "exit the shell");
shell_install_command("export", shell_cmd_export, "set environment variables: export VAR=value");
shell_install_command("help", shell_cmd_help, "display this help text");
shell_install_command("history", shell_cmd_history, "list command history");
shell_install_command("if", shell_cmd_if, "if ... then ... [else ...]");
shell_install_command("while", shell_cmd_while, "while ... do ...");
shell_install_command("empty?", shell_cmd_empty, "empty? args...");
shell_install_command("equals?", shell_cmd_equals, "equals? arg1 arg2");
shell_install_command("return", shell_cmd_return, "return status code");
shell_install_command("export-cmd", shell_cmd_export_cmd, "set variable to result of command: export-cmd VAR command...");
shell_install_command("source", shell_cmd_source, "run a shell script in the context of this shell");
shell_install_command("exec", shell_cmd_exec, "replace shell (or subshell) with command");
shell_install_command("not", shell_cmd_not, "invert status of command");
shell_install_command("unset", shell_cmd_unset, "unset variable");
shell_install_command("read", shell_cmd_read, "read user input");
shell_install_command("fg", shell_cmd_fg, "resume a suspended job");
shell_install_command("jobs", shell_cmd_jobs, "list stopped jobs");
}