From 242cfcca12688433f4aec720bbb98a1277cbd28f Mon Sep 17 00:00:00 2001 From: Jonas 'Sortie' Termansen Date: Fri, 27 Dec 2013 00:27:02 +0100 Subject: [PATCH] Add line editing, history and tab completion to shell. --- doc/user-guide | 8 + sh/sh.cpp | 1548 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 1413 insertions(+), 143 deletions(-) diff --git a/doc/user-guide b/doc/user-guide index 037c9bbf..6f6754dc 100644 --- a/doc/user-guide +++ b/doc/user-guide @@ -106,6 +106,9 @@ Despite that, it does have some key features. Here are the features that are supported: * Processes can be started in the usual Unix manner. +* Tab-completion. +* Line editing. +* History. * Background tasks ('&'). * Standard output redirection ('>'). * Piping stdin from a task to stdin of another ('|'). @@ -125,6 +128,11 @@ supported: * Basic wildcards ('foo*.bar'). * Clearing the screen (Control-L, '^L'). * Deleting the last typed word (Control-W, '^W'). +* Deleting everything before the cursor (Control-U, '^U'). +* Deleting everything after the cursor (Control-K, '^K'). +* Go to start of line (Control-A, '^A', Home). +* Go to end of line (Control-E, '^E', End). +* Move between words (Control-arrows). These features are missing from the shell: diff --git a/sh/sh.cpp b/sh/sh.cpp index b95b3aba..c50bd905 100644 --- a/sh/sh.cpp +++ b/sh/sh.cpp @@ -20,44 +20,1037 @@ *******************************************************************************/ +#include #include #include +#include +#include #include #include #include #include #include +#include +#include #include #include #include #include #include #include +#include +#include + +static const unsigned int NORMAL_TERMMODE = + TERMMODE_UNICODE | + TERMMODE_SIGNAL | + TERMMODE_UTF8 | + TERMMODE_LINEBUFFER | + TERMMODE_ECHO; + +const char* builtin_commands[] = +{ + "cd", + "exit", + "unset", + "clearenv", + (const char*) NULL, +}; + +// TODO: Predict the terminal colors as well! +struct cursor_predict +{ + bool escaped; +}; + +struct wincurpos predict_cursor(struct cursor_predict* cursor_predict, + struct wincurpos wcp, + struct winsize ws, + wchar_t c) +{ + if ( c == L'\0' ) + return wcp; + + if ( cursor_predict->escaped ) + { + if ( (L'a' <= c && c <= L'z') || (L'A' <= c && c <= L'Z') ) + cursor_predict->escaped = false; + return wcp; + } + + if ( c == L'\e' ) + { + cursor_predict->escaped = true; + return wcp; + } + + if ( c == L'\n' || ws.ws_col <= wcp.wcp_col + 1 ) + { + wcp.wcp_col = 0; + if ( wcp.wcp_row + 1 < ws.ws_row ) + wcp.wcp_row++; + } + else + { + wcp.wcp_col++; + } + + return wcp; +} + +bool predict_will_scroll(struct cursor_predict cursor_predict, + struct wincurpos wcp, + struct winsize ws, + wchar_t c) +{ + if ( c == L'\0' ) + return false; + if ( cursor_predict.escaped ) + return false; + return (c == L'\n' || ws.ws_col <= wcp.wcp_col + 1) && + !(wcp.wcp_row + 1 < ws.ws_row); +} + +struct show_line +{ + struct wincurpos wcp_start; + struct wincurpos wcp_current; + struct winsize ws; + int out_fd; + char* current_line; + size_t current_cursor; + bool invalidated; +}; + +void show_line_begin(struct show_line* show_state, int out_fd) +{ + memset(show_state, 0, sizeof(*show_state)); + show_state->out_fd = out_fd; + show_state->current_line = NULL; + show_state->current_cursor = 0; + tcgetwincurpos(out_fd, &show_state->wcp_start); + show_state->wcp_current = show_state->wcp_start; + tcgetwinsize(show_state->out_fd, &show_state->ws); +} + +bool show_line_is_weird(const char* line) +{ + for ( size_t i = 0; line[i]; i++ ) + { + if ( line[i] == '\e' ) + { + i++; + if ( line[i] != '[' ) + return true; + i++; + while ( ('0' <= line[i] && line[i] <= '9') || line[i] == ';' ) + i++; + switch ( line[i] ) + { + case 'm': break; + default: return true; + } + continue; + } + + switch ( line[i] ) + { + case '\a': return true; + case '\b': return true; + case '\f': return true; + case '\r': return true; + case '\t': return true; // TODO: This isn't weird. + case '\v': return true; + default: break; + } + } + + return false; +} + +void show_line_change_cursor(struct show_line* show_state, struct wincurpos wcp) +{ + if ( wcp.wcp_col == show_state->wcp_current.wcp_col && + wcp.wcp_row == show_state->wcp_current.wcp_row ) + return; + + if ( wcp.wcp_col == 0 ) + dprintf(show_state->out_fd, "\e[%zuH", wcp.wcp_row + 1); + else + dprintf(show_state->out_fd, "\e[%zu;%zuH", wcp.wcp_row + 1, wcp.wcp_col+ 1); + + show_state->wcp_current = wcp; +} + +bool show_line_optimized(struct show_line* show_state, const char* line, size_t cursor) +{ + struct winsize ws = show_state->ws; + + mbstate_t old_ps; + mbstate_t new_ps; + memset(&old_ps, 0, sizeof(old_ps)); + memset(&new_ps, 0, sizeof(new_ps)); + struct wincurpos old_wcp = show_state->wcp_start; + struct wincurpos new_wcp = show_state->wcp_start; + struct cursor_predict old_cursor_predict; + struct cursor_predict new_cursor_predict; + memset(&old_cursor_predict, 0, sizeof(old_cursor_predict)); + memset(&new_cursor_predict, 0, sizeof(new_cursor_predict)); + size_t old_line_offset = 0; + size_t new_line_offset = 0; + const char* old_line = show_state->current_line; + const char* new_line = line; + + struct wincurpos cursor_wcp = show_state->wcp_start; + + while ( true ) + { + if ( cursor == new_line_offset ) + cursor_wcp = new_wcp; + + wchar_t old_wc; + wchar_t new_wc; + + size_t old_num_bytes = mbrtowc(&old_wc, old_line + old_line_offset, SIZE_MAX, &old_ps); + size_t new_num_bytes = mbrtowc(&new_wc, new_line + new_line_offset, SIZE_MAX, &new_ps); + assert(old_num_bytes != (size_t) -2); + assert(new_num_bytes != (size_t) -2); + assert(old_num_bytes != (size_t) -1); + assert(new_num_bytes != (size_t) -1); + if ( old_num_bytes == 0 && new_num_bytes == 0 ) + break; + + bool will_scroll = predict_will_scroll(new_cursor_predict, new_wcp, ws, new_wc); + bool can_scroll = show_state->wcp_start.wcp_row != 0; + + if ( will_scroll && !can_scroll ) + { + if ( new_line_offset < cursor ) + cursor_wcp = new_wcp; + break; + } + + if ( predict_will_scroll(old_cursor_predict, old_wcp, ws, old_wc) ) + break; + + struct wincurpos next_old_wcp = predict_cursor(&old_cursor_predict, old_wcp, ws, old_wc); + struct wincurpos next_new_wcp = predict_cursor(&new_cursor_predict, new_wcp, ws, new_wc); + + if ( old_wc != new_wc || + old_wcp.wcp_row != new_wcp.wcp_row || + old_wcp.wcp_col != new_wcp.wcp_col ) + { + // TODO: Use a reliable write instead! + + if ( old_wc == L'\n' && new_wc == L'\n' ) + { + // Good enough as newlines are invisible. + } + else if ( old_wc == L'\n' && new_wc != L'\0' ) + { + show_line_change_cursor(show_state, new_wcp); + write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); + show_state->wcp_current = next_new_wcp; + old_num_bytes = 0; + } + else if ( old_wc != L'\0' && new_wc == '\n' ) + { + show_line_change_cursor(show_state, old_wcp); + write(show_state->out_fd, " ", 1); + show_state->wcp_current = next_old_wcp; + new_num_bytes = 0; + } + else if ( old_wc == L'\n' && new_wc == L'\0' ) + { + // No need to do anything here as newlines are visible. + } + else if ( old_wc == L'\0' && new_wc == L'\n' ) + { + show_line_change_cursor(show_state, new_wcp); + write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); + show_state->wcp_current = next_new_wcp; + } + else if ( old_wcp.wcp_row != new_wcp.wcp_row || + old_wcp.wcp_col != new_wcp.wcp_col ) + return false; + else if ( new_wc == L'\0' && old_wc != L'\0' ) + { + show_line_change_cursor(show_state, old_wcp); + write(show_state->out_fd, " ", 1); + show_state->wcp_current = next_old_wcp; + } + else if ( new_wc != L'\0' ) + { + show_line_change_cursor(show_state, new_wcp); + write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); + show_state->wcp_current = next_new_wcp; + } + } + + if ( will_scroll && can_scroll ) + { + cursor_wcp.wcp_row--; + next_old_wcp.wcp_row--; + show_state->wcp_start.wcp_row--; + } + + old_wcp = next_old_wcp; + new_wcp = next_new_wcp; + + old_line_offset += old_num_bytes; + new_line_offset += new_num_bytes; + } + + show_line_change_cursor(show_state, cursor_wcp); + + free(show_state->current_line); + show_state->current_line = strdup(line); + assert(show_state->current_line); + show_state->current_cursor = cursor; + + return true; +} + +void show_line(struct show_line* show_state, const char* line, size_t cursor) +{ + // TODO: We don't currently invalidate on SIGWINCH. + struct winsize ws; + tcgetwinsize(show_state->out_fd, &ws); + if ( ws.ws_col != show_state->ws.ws_col || + ws.ws_row != show_state->ws.ws_row ) + { + // TODO: What if wcp_start isn't inside the window any longer? + show_state->invalidated = true; + show_state->ws = ws; + } + + // Attempt to do an optimized line re-rendering reusing the characters + // already present on the console. Bail out if this turns out to be harder + // than expected and re-render everything from scratch instead. + if ( !show_state->invalidated && + show_state->current_line && + !show_line_is_weird(show_state->current_line) && + !show_line_is_weird(line) ) + { + if ( show_line_optimized(show_state, line, cursor) ) + return; + show_state->invalidated = true; + } + + show_line_change_cursor(show_state, show_state->wcp_start); + + dprintf(show_state->out_fd, "\e[m"); + + if ( show_state->invalidated || show_state->current_line ) + dprintf(show_state->out_fd, "\e[0J"); + + struct cursor_predict cursor_predict; + memset(&cursor_predict, 0, sizeof(cursor_predict)); + struct wincurpos wcp = show_state->wcp_start; + struct wincurpos cursor_wcp = wcp; + + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + for ( size_t i = 0; true; ) + { + if ( cursor == i ) + cursor_wcp = wcp; + wchar_t wc; + size_t num_bytes = mbrtowc(&wc, line + i, SIZE_MAX, &ps); + assert(num_bytes != (size_t) -2); + assert(num_bytes != (size_t) -1); + if ( num_bytes == 0 ) + break; + bool will_scroll = predict_will_scroll(cursor_predict, wcp, ws, wc); + bool can_scroll = show_state->wcp_start.wcp_row != 0; + if ( will_scroll && !can_scroll ) + { + if ( i < cursor ) + cursor_wcp = wcp; + break; + } + // TODO: Use a reliable write. + write(show_state->out_fd, line + i, num_bytes); + if ( will_scroll && can_scroll ) + { + cursor_wcp.wcp_row--; + show_state->wcp_start.wcp_row--; + } + wcp = predict_cursor(&cursor_predict, wcp, ws, wc); + i += num_bytes; + } + + dprintf(show_state->out_fd, "\e[%zu;%zuH", + cursor_wcp.wcp_row + 1, + cursor_wcp.wcp_col + 1); + + show_state->wcp_current = wcp; + + free(show_state->current_line); + show_state->current_line = strdup(line); + assert(show_state->current_line); + show_state->current_cursor = cursor; + + show_state->invalidated = false; +} + +void show_line_clear(struct show_line* show_state) +{ + dprintf(show_state->out_fd, "\e[H\e[2J"); + + show_state->wcp_start.wcp_row = 0; + show_state->wcp_start.wcp_col = 0; + show_state->invalidated = true; + + show_line(show_state, show_state->current_line, strlen(show_state->current_line)); +} + +void show_line_abort(struct show_line* show_state) +{ + free(show_state->current_line); + show_state->current_line = NULL; + show_state->current_cursor = 0; +} + +void show_line_finish(struct show_line* show_state) +{ + show_line(show_state, show_state->current_line, strlen(show_state->current_line)); + dprintf(show_state->out_fd, "\n"); + + show_line_abort(show_state); +} + +struct edit_line +{ + const char* ps1; + const char* ps2; + struct show_line show_state; + wchar_t* line; + size_t line_offset; + size_t line_used; + size_t line_length; + char** history; + size_t history_offset; + size_t history_used; + size_t history_length; + size_t history_target; + void* check_input_incomplete_context; + bool (*check_input_incomplete)(void*, const char*); + void* trap_eof_opportunity_context; + void (*trap_eof_opportunity)(void*); + void* complete_context; + size_t (*complete)(char***, size_t*, size_t*, void*, const char*, size_t); + int in_fd; + int out_fd; + bool editing; + bool abort_editing; + bool eof_condition; + bool double_tab; + // TODO: Should these be stored here, or outside the line editing context? + bool left_control; + bool right_control; +}; + +void edit_line_show(struct edit_line* edit_state) +{ + size_t line_length = 0; + + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + line_length += strlen(edit_state->ps1); + + for ( size_t i = 0; i < edit_state->line_used; i++ ) + { + char mb[MB_CUR_MAX]; + line_length += wcrtomb(mb, edit_state->line[i], &ps); + if ( edit_state->line[i] == L'\n' ) + line_length += strlen(edit_state->ps2); + } + + char* line = (char*) malloc(line_length + 1); + assert(line); + + size_t cursor = 0; + size_t line_offset = 0; + memset(&ps, 0, sizeof(ps)); + + strcpy(line + line_offset, edit_state->ps1); + line_offset += strlen(edit_state->ps1); + + for ( size_t i = 0; i < edit_state->line_used; i++ ) + { + if ( edit_state->line_offset == i ) + cursor = line_offset; + line_offset += wcrtomb(line + line_offset, edit_state->line[i], &ps); + if ( edit_state->line[i] == L'\n' ) + { + strcpy(line + line_offset, edit_state->ps2); + line_offset += strlen(edit_state->ps2); + } + } + + if ( edit_state->line_offset == edit_state->line_used ) + cursor = line_offset; + + line[line_offset] = '\0'; + + show_line(&edit_state->show_state, line, cursor); + + free(line); +} + +char* edit_line_result(struct edit_line* edit_state) +{ + size_t result_length = 0; + + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + for ( size_t i = 0; i < edit_state->line_used; i++ ) + { + char mb[MB_CUR_MAX]; + result_length += wcrtomb(mb, edit_state->line[i], &ps); + } + + char* result = (char*) malloc(result_length + 1); + if ( !result ) + return NULL; + size_t result_offset = 0; + + memset(&ps, 0, sizeof(ps)); + + for ( size_t i = 0; i < edit_state->line_used; i++ ) + result_offset += wcrtomb(result + result_offset, edit_state->line[i], &ps); + + result[result_offset] = '\0'; + + return result; +} + +bool edit_line_can_finish(struct edit_line* edit_state) +{ + if ( !edit_state->check_input_incomplete ) + return true; + char* line = edit_line_result(edit_state); + assert(line); + bool result = !edit_state->check_input_incomplete( + edit_state->check_input_incomplete_context, line); + free(line); + return result; +} + +void edit_line_append_history(struct edit_line* edit_state, const char* line) +{ + if ( edit_state->history_used == edit_state->history_length ) + { + size_t new_length = 2 * edit_state->history_length; + if ( new_length == 0 ) + new_length = 16; + // TODO: Use reallocarray instead of realloc. + size_t new_size = sizeof(char*) * new_length; + char** new_history = (char**) realloc(edit_state->history, new_size); + assert(new_history); + edit_state->history = new_history; + edit_state->history_length = new_length; + } + + size_t history_index = edit_state->history_used++; + edit_state->history[history_index] = strdup(line); + assert(edit_state->history[history_index]); +} + +void edit_line_type_use_record(struct edit_line* edit_state, const char* record) +{ + free(edit_state->line); + edit_state->line_offset = 0; + edit_state->line_used = 0; + edit_state->line_length = 0; + + size_t line_length; + + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + size_t record_offset = 0; + for ( line_length = 0; true; line_length++ ) + { + size_t num_bytes = mbrtowc(NULL, record + record_offset, SIZE_MAX, &ps); + assert(num_bytes != (size_t) -2); + assert(num_bytes != (size_t) -1); + if ( num_bytes == 0 ) + break; + record_offset += num_bytes; + } + + // TODO: Avoid multiplication overflow. + wchar_t* line = (wchar_t*) malloc(sizeof(wchar_t) * line_length); + assert(line); + size_t line_used; + + memset(&ps, 0, sizeof(ps)); + + record_offset = 0; + for ( line_used = 0; line_used < line_length; line_used++ ) + { + size_t num_bytes = mbrtowc(&line[line_used], record + record_offset, SIZE_MAX, &ps); + assert(num_bytes != (size_t) -2); + assert(num_bytes != (size_t) -1); + assert(num_bytes != (size_t) 0); + record_offset += num_bytes; + } + + edit_state->line = line; + edit_state->line_offset = line_used; + edit_state->line_used = line_used; + edit_state->line_length = line_length; +} + +void edit_line_type_history_save_at(struct edit_line* edit_state, size_t index) +{ + assert(index <= edit_state->history_used); + + char* saved_line = edit_line_result(edit_state); + assert(saved_line); + if ( index == edit_state->history_used ) + { + edit_line_append_history(edit_state, saved_line); + free(saved_line); + } + else + { + free(edit_state->history[index]); + edit_state->history[index] = saved_line; + } +} + +void edit_line_type_history_save_current(struct edit_line* edit_state) +{ + edit_line_type_history_save_at(edit_state, edit_state->history_offset); +} + +void edit_line_type_history_prev(struct edit_line* edit_state) +{ + if ( edit_state->history_offset == 0 ) + return; + + edit_line_type_history_save_current(edit_state); + + const char* record = edit_state->history[--edit_state->history_offset]; + assert(record); + edit_line_type_use_record(edit_state, record); +} + +void edit_line_type_history_next(struct edit_line* edit_state) +{ + if ( edit_state->history_used - edit_state->history_offset <= 1 ) + return; + + edit_line_type_history_save_current(edit_state); + + const char* record = edit_state->history[++edit_state->history_offset]; + assert(record); + edit_line_type_use_record(edit_state, record); +} + +void edit_line_type_codepoint(struct edit_line* edit_state, wchar_t wc) +{ + if ( wc == L'\n' && edit_line_can_finish(edit_state)) + { + if ( edit_state->line_used ) + edit_line_type_history_save_at(edit_state, edit_state->history_target); + edit_state->editing = false; + return; + } + + if ( edit_state->line_used == edit_state->line_length ) + { + size_t new_length = 2 * edit_state->line_length; + if ( !new_length ) + new_length = 16; + // TODO: Use reallocarray instead of realloc. + size_t new_size = sizeof(wchar_t) * new_length; + wchar_t* new_line = (wchar_t*) realloc(edit_state->line, new_size); + assert(new_line); + edit_state->line = new_line; + edit_state->line_length = new_length; + } + + assert(edit_state->line_offset <= edit_state->line_used); + assert(edit_state->line_used <= edit_state->line_length); + + for ( size_t i = edit_state->line_used; i != edit_state->line_offset; i-- ) + edit_state->line[i] = edit_state->line[i-1]; + + edit_state->line[edit_state->line_used++, edit_state->line_offset++] = wc; + + assert(edit_state->line_offset <= edit_state->line_used); + assert(edit_state->line_used <= edit_state->line_length); +} + +void line_edit_type_home(struct edit_line* edit_state) +{ + edit_state->line_offset = 0; +} + +void line_edit_type_left(struct edit_line* edit_state) +{ + if ( edit_state->line_offset == 0 ) + return; + edit_state->line_offset--; +} + +void line_edit_type_right(struct edit_line* edit_state) +{ + if ( edit_state->line_offset == edit_state->line_used ) + return; + edit_state->line_offset++; +} + +void line_edit_type_end(struct edit_line* edit_state) +{ + edit_state->line_offset = edit_state->line_used; +} + +void line_edit_type_backspace(struct edit_line* edit_state) +{ + if ( edit_state->line_offset == 0 ) + return; + edit_state->line_used--; + edit_state->line_offset--; + for ( size_t i = edit_state->line_offset; i < edit_state->line_used; i++ ) + edit_state->line[i] = edit_state->line[i+1]; +} + +void line_edit_type_previous_word(struct edit_line* edit_state) +{ + while ( edit_state->line_offset && + iswspace(edit_state->line[edit_state->line_offset-1]) ) + edit_state->line_offset--; + while ( edit_state->line_offset && + !iswspace(edit_state->line[edit_state->line_offset-1]) ) + edit_state->line_offset--; +} + +void line_edit_type_next_word(struct edit_line* edit_state) +{ + while ( edit_state->line_offset != edit_state->line_used && + iswspace(edit_state->line[edit_state->line_offset]) ) + edit_state->line_offset++; + while ( edit_state->line_offset != edit_state->line_used && + !iswspace(edit_state->line[edit_state->line_offset]) ) + edit_state->line_offset++; +} + +void line_edit_type_delete(struct edit_line* edit_state) +{ + if ( edit_state->line_offset == edit_state->line_used ) + return; + edit_state->line_used--; + for ( size_t i = edit_state->line_offset; i < edit_state->line_used; i++ ) + edit_state->line[i] = edit_state->line[i+1]; +} + +void line_edit_type_eof_or_delete(struct edit_line* edit_state) +{ + if ( edit_state->line_used ) + return line_edit_type_delete(edit_state); + edit_state->editing = false; + edit_state->eof_condition = true; + if ( edit_state->trap_eof_opportunity ) + edit_state->trap_eof_opportunity(edit_state->trap_eof_opportunity_context); +} + +void edit_line_type_interrupt(struct edit_line* edit_state) +{ + dprintf(edit_state->out_fd, "^C\n"); + edit_state->editing = false; + edit_state->abort_editing = true; +} + +void edit_line_type_kill_after(struct edit_line* edit_state) +{ + while ( edit_state->line_offset < edit_state->line_used ) + line_edit_type_delete(edit_state); +} + +void edit_line_type_kill_before(struct edit_line* edit_state) +{ + while ( edit_state->line_offset ) + line_edit_type_backspace(edit_state); +} + +void edit_line_type_clear(struct edit_line* edit_state) +{ + show_line_clear(&edit_state->show_state); +} + +void edit_line_type_delete_word_before(struct edit_line* edit_state) +{ + while ( edit_state->line_offset && + iswspace(edit_state->line[edit_state->line_offset-1]) ) + line_edit_type_backspace(edit_state); + while ( edit_state->line_offset && + !iswspace(edit_state->line[edit_state->line_offset-1]) ) + line_edit_type_backspace(edit_state); +} + +int edit_line_completion_sort(const void* a_ptr, const void* b_ptr) +{ + const char* a = *(const char**) a_ptr; + const char* b = *(const char**) b_ptr; + return strcmp(a, b); +} + +void edit_line_type_complete(struct edit_line* edit_state) +{ + if ( !edit_state->complete ) + return; + + char* partial = edit_line_result(edit_state); + if ( !partial ) + return; + + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + size_t complete_at = 0; + for ( size_t i = 0; i < edit_state->line_offset; i++ ) + { + char mb[MB_CUR_MAX]; + size_t num_bytes = wcrtomb(mb, edit_state->line[i], &ps); + assert(num_bytes != (size_t) -1); + assert(num_bytes != (size_t) 0); + complete_at += num_bytes; + } + + char** completions; + size_t used_before; + size_t used_after; + size_t num_completions = edit_state->complete( + &completions, + &used_before, + &used_after, + edit_state->complete_context, + partial, + complete_at); + + qsort(completions, num_completions, sizeof(char*), edit_line_completion_sort); + + size_t lcp = 0; + bool similar = true; + while ( num_completions && similar ) + { + char c = completions[0][lcp]; + if ( c == '\0' ) + break; + for ( size_t i = 1; similar && i < num_completions; i++ ) + { + if ( completions[i][lcp] != c ) + similar = false; + } + if ( similar ) + lcp++; + } + + bool prefix_ends_with_slash = false; + memset(&ps, 0, sizeof(ps)); + for ( size_t i = 0; i < lcp; ) + { + const char* completion = completions[0]; + wchar_t wc; + size_t num_bytes = mbrtowc(&wc, completion + i, lcp - i, &ps); + if ( num_bytes == (size_t) -2 ) + break; + assert(num_bytes != (size_t) -1); + assert(num_bytes != (size_t) 0); + edit_line_type_codepoint(edit_state, wc); + prefix_ends_with_slash = wc == L'/'; + i += num_bytes; + } + + if ( num_completions == 1 && !prefix_ends_with_slash ) + { + edit_line_type_codepoint(edit_state, ' '); + } + + if ( 2 <= num_completions && lcp == 0 && edit_state->double_tab ) + { + bool first = true; + for ( size_t i = 0; i < num_completions; i++ ) + { + const char* completion = completions[i]; + size_t length = used_before + strlen(completion) + used_after; + if ( !length ) + continue; + if ( first ) + show_line_finish(&edit_state->show_state); + // TODO: Use a reliable write. + if ( !first ) + write(edit_state->out_fd, " ", 1); + write(edit_state->out_fd, partial + complete_at - used_before, used_before); + write(edit_state->out_fd, completion, strlen(completion)); + write(edit_state->out_fd, partial + complete_at, used_after); + first = false; + } + if ( !first) + { + write(edit_state->out_fd, "\n", 1); + show_line_begin(&edit_state->show_state, edit_state->out_fd); + edit_line_show(edit_state); + } + } + + edit_state->double_tab = true; + + (void) used_before; + (void) used_after; + + for ( size_t i = 0; i < num_completions; i++ ) + free(completions[i]); + free(completions); + + free(partial); +} + +void edit_line_kbkey(struct edit_line* edit_state, int kbkey) +{ + if ( kbkey != KBKEY_TAB && kbkey != -KBKEY_TAB ) + edit_state->double_tab = false; + + if ( edit_state->left_control || edit_state->right_control ) + { + switch ( kbkey ) + { + case KBKEY_LEFT: line_edit_type_previous_word(edit_state); return; + case KBKEY_RIGHT: line_edit_type_next_word(edit_state); return; + }; + } + + switch ( kbkey ) + { + case KBKEY_HOME: line_edit_type_home(edit_state); return; + case KBKEY_LEFT: line_edit_type_left(edit_state); return; + case KBKEY_RIGHT: line_edit_type_right(edit_state); return; + case KBKEY_UP: edit_line_type_history_prev(edit_state); return; + case KBKEY_DOWN: edit_line_type_history_next(edit_state); return; + case KBKEY_END: line_edit_type_end(edit_state); return; + case KBKEY_BKSPC: line_edit_type_backspace(edit_state); return; + case KBKEY_DELETE: line_edit_type_delete(edit_state); return; + case KBKEY_TAB: edit_line_type_complete(edit_state); return; + case -KBKEY_LCTRL: edit_state->left_control = false; return; + case +KBKEY_LCTRL: edit_state->left_control = true; return; + case -KBKEY_RCTRL: edit_state->right_control = false; return; + case +KBKEY_RCTRL: edit_state->right_control = true; return; + }; +} + +void edit_line_codepoint(struct edit_line* edit_state, wchar_t wc) +{ + if ( (edit_state->left_control || edit_state->right_control) && + ((L'a' <= wc && wc <= L'z') || (L'A' <= wc && wc <= L'Z')) ) + { + if ( wc == L'a' || wc == L'A' ) + line_edit_type_home(edit_state); + if ( wc == L'b' || wc == L'B' ) + line_edit_type_left(edit_state); + if ( wc == L'c' || wc == L'C' ) + edit_line_type_interrupt(edit_state); + if ( wc == L'd' || wc == L'D' ) + line_edit_type_eof_or_delete(edit_state); + if ( wc == L'e' || wc == L'E' ) + line_edit_type_end(edit_state); + if ( wc == L'f' || wc == L'F' ) + line_edit_type_right(edit_state); + if ( wc == L'k' || wc == L'K' ) + edit_line_type_kill_after(edit_state); + if ( wc == L'l' || wc == L'L' ) + show_line_clear(&edit_state->show_state); + if ( wc == L'u' || wc == L'U' ) + edit_line_type_kill_before(edit_state); + if ( wc == L'w' || wc == L'W' ) + edit_line_type_delete_word_before(edit_state); + return; + } + + if ( wc == L'\b' ) + return; + if ( wc == L'\t' ) + return; + + edit_line_type_codepoint(edit_state, wc); +} + +void edit_line(struct edit_line* edit_state) +{ + edit_state->editing = true; + edit_state->abort_editing = false; + edit_state->eof_condition = false; + edit_state->double_tab = false; + + free(edit_state->line); + edit_state->line = NULL; + edit_state->line_offset = 0; + edit_state->line_used = 0; + edit_state->line_length = 0; + edit_state->history_offset = edit_state->history_used; + edit_state->history_target = edit_state->history_used; + + settermmode(edit_state->in_fd, TERMMODE_KBKEY | TERMMODE_UNICODE); + + show_line_begin(&edit_state->show_state, edit_state->out_fd); + + while ( edit_state->editing ) + { + edit_line_show(edit_state); + + uint32_t codepoint; + if ( read(0, &codepoint, sizeof(codepoint)) != sizeof(codepoint) ) + { + edit_state->eof_condition = true; + edit_state->abort_editing = true; + break; + } + + if ( int kbkey = KBKEY_DECODE(codepoint) ) + edit_line_kbkey(edit_state, kbkey); + else + edit_line_codepoint(edit_state, (wchar_t) codepoint); + } + + if ( edit_state->abort_editing ) + show_line_abort(&edit_state->show_state); + else + { + edit_line_show(edit_state); + show_line_finish(&edit_state->show_state); + } + + settermmode(edit_state->in_fd, NORMAL_TERMMODE); +} int status = 0; +char* strdup_safe(const char* string) +{ + return string ? strdup(string) : NULL; +} + const char* getenv_safe(const char* name, const char* def = "") { const char* ret = getenv(name); return ret ? ret : def; } -void on_sigint(int /*signum*/) +void update_pwd() { - printf("^C\n"); + char* wd = get_current_dir_name(); + setenv("PWD", wd ? wd : "?", 1); + free(wd); } -void updatepwd() -{ - const size_t CWD_SIZE = 512; - char cwd[CWD_SIZE]; - const char* wd = getcwd(cwd, CWD_SIZE); - if ( !wd ) { wd = "?"; } - setenv("PWD", wd, 1); -} - -void updateenv() +void update_env() { char str[128]; struct winsize ws; @@ -209,7 +1202,7 @@ readcmd: cmdnext = cmdend + 1; argv = (char**) (tokens + cmdstart); - updateenv(); + update_env(); char statusstr[32]; snprintf(statusstr, sizeof(statusstr), "%i", status); setenv("?", statusstr, 1); @@ -237,7 +1230,7 @@ readcmd: error(0, errno, "cd: %s", newdir); internalresult = 1; } - updatepwd(); + update_pwd(); } if ( strcmp(argv[0], "exit") == 0 ) { @@ -361,131 +1354,12 @@ out: return result; } -int get_and_run_command(FILE* fp, const char* fpname, bool interactive, - bool exit_on_error, bool* exitexec) +int run_command(char* command, + bool interactive, + bool exit_on_error, + bool* exitexec) { - int fd = fileno(fp); - - if ( interactive ) - { - unsigned termmode = TERMMODE_UNICODE - | TERMMODE_SIGNAL - | TERMMODE_UTF8 - | TERMMODE_LINEBUFFER - | TERMMODE_ECHO; - settermmode(fd, termmode); - const char* print_username = getlogin(); - if ( !print_username ) - print_username = getuid() == 0 ? "root" : "?"; - char hostname[256]; - if ( gethostname(hostname, sizeof(hostname)) < 0 ) - strlcpy(hostname, "(none)", sizeof(hostname)); - const char* print_dir = getenv_safe("PWD", "?"); - const char* home_dir = getenv_safe("HOME", ""); - size_t home_dir_len = strlen(home_dir); - printf("\e[32m"); - printf("%s", print_username); - printf("@"); - printf("%s", hostname); - printf(" "); - printf("\e[36m"); - if ( home_dir_len && strncmp(print_dir, home_dir, home_dir_len) == 0 ) - printf("~%s", print_dir + home_dir_len); - else - printf("%s", print_dir); - printf(" "); - printf("#"); - printf("\e[37m "); - fflush(stdout); - } - - static char* command = NULL; - static size_t commandlen = 1024; - if ( !command ) - commandlen = 1024, - command = (char*) malloc((commandlen+1) * sizeof(char)); - if ( !command ) - error(64, errno, "malloc"); - - size_t commandused = 0; - bool commented = false; - bool escaped = false; - while (true) - { - char c; - if ( fd < 0 ) - { - int ic = fgetc(fp); - if ( ic == EOF ) - { - if ( ferror(fp) ) - error(64, errno, "fgetc %s", fpname); - if ( commandused ) - break; - return *exitexec = true, status; - } - c = (char) (unsigned char) ic; - } - else - { - ssize_t bytesread = read(fd, &c, sizeof(c)); - if ( bytesread < 0 && errno == EINTR ) - return status; - if ( bytesread < 0 ) - error(64, errno, "read %s", fpname); - if ( !bytesread && !interactive ) - return *exitexec = true, status; - if ( !bytesread && interactive ) - { - const char* init_pid_str = getenv("INIT_PID"); - if ( !init_pid_str ) - init_pid_str = "1"; - pid_t init_pid = (pid_t) strtoimax(init_pid_str, NULL, 10); - if ( !init_pid ) - init_pid = 1; - if ( getppid() == init_pid ) - { - printf("\nType exit to shutdown the system.\n"); - return status; - } - printf("exit\n"); - return *exitexec = true, status; - } - } - if ( !c ) - continue; - if ( c == '\n' && !escaped ) - break; - if ( commented ) - continue; - if ( c == '\\' && !escaped ) - { - escaped = true; - continue; - } - if ( !escaped ) - { - if ( c == '#' ) - { - commented = true; - continue; - } - } - escaped = false; - if ( commandused == commandlen ) - { - size_t newlen = commandlen * 2; - size_t newsize = (newlen+1) * sizeof(char); - char* newcommand = (char*) realloc(command, newsize); - if ( !newcommand ) - error(64, errno, "realloc"); - command = newcommand; - commandlen = newsize; - } - command[commandused++] = c; - } - - command[commandused] = '\0'; + size_t commandused = strlen(command); if ( command[0] == '\0' ) return status; @@ -507,7 +1381,7 @@ int get_and_run_command(FILE* fp, const char* fpname, bool interactive, argv[0] = NULL; bool lastwasspace = true; - escaped = false; + bool escaped = false; for ( size_t i = 0; i <= commandused; i++ ) { switch ( command[i] ) @@ -557,6 +1431,390 @@ int get_and_run_command(FILE* fp, const char* fpname, bool interactive, return status; } +bool is_shell_input_ready(const char* input) +{ + bool commented = false; + bool escaped = false; + for ( size_t i = 0; input[i]; i++ ) + { + if ( !commented && !escaped && input[i] == '\\' ) + escaped = true; + else if ( !commented && !escaped && input[i] == '#' ) + commented = true; + else if ( !commented ) + escaped = false; + } + return !escaped; +} + +bool does_line_editing_need_another_line(void*, const char* line) +{ + return !is_shell_input_ready(line); +} + +bool is_parent_init() +{ + const char* init_pid_str = getenv("INIT_PID"); + if ( !init_pid_str) + init_pid_str = "1"; + pid_t init_pid = (pid_t) atol(init_pid_str); + if ( !init_pid ) + init_pid = 1; + return getppid() == init_pid; +} + +void on_trap_eof(void* edit_state_ptr) +{ + if ( is_parent_init() ) + return; + struct edit_line* edit_state = (struct edit_line*) edit_state_ptr; + edit_line_type_codepoint(edit_state, L'e'); + edit_line_type_codepoint(edit_state, L'x'); + edit_line_type_codepoint(edit_state, L'i'); + edit_line_type_codepoint(edit_state, L't'); +} + +bool is_usual_char_for_completion(char c) +{ + return !isspace(c) && + c != ';' && c != '&' && c != '|' && + c != '<' && c != '>' && c != '#' && c != '$'; +} + +size_t do_complete(char*** completions_ptr, + size_t* used_before_ptr, + size_t* used_after_ptr, + void*, + const char* partial, + size_t complete_at) +{ + size_t used_before = 0; + size_t used_after = 0; + + while ( complete_at - used_before && + is_usual_char_for_completion(partial[complete_at - (used_before+1)]) ) + used_before++; + +#if 0 + while ( partial[complete_at + used_after] && + is_usual_char_for_completion(partial[complete_at + used_after]) ) + used_after++; +#endif + + enum complete_type + { + COMPLETE_TYPE_FILE, + COMPLETE_TYPE_EXECUTABLE, + COMPLETE_TYPE_DIRECTORY, + COMPLETE_TYPE_PROGRAM, + COMPLETE_TYPE_VARIABLE, + }; + + enum complete_type complete_type = COMPLETE_TYPE_FILE; + + if ( complete_at - used_before && partial[complete_at - used_before-1] == '$' ) + { + complete_type = COMPLETE_TYPE_VARIABLE; + used_before++; + } + else + { + size_t type_offset = complete_at - used_before; + while ( type_offset && isspace(partial[type_offset-1]) ) + type_offset--; + + if ( 2 <= type_offset && + strncmp(partial + type_offset - 2, "cd", 2) == 0 && + (type_offset == 2 || !is_usual_char_for_completion(partial[type_offset-2-1])) ) + complete_type = COMPLETE_TYPE_DIRECTORY; + else if ( !type_offset || + partial[type_offset-1] == ';' || + partial[type_offset-1] == '&' || + partial[type_offset-1] == '|' ) + { + if ( memchr(partial + complete_at - used_before, '/', used_before) ) + complete_type = COMPLETE_TYPE_EXECUTABLE; + else + complete_type = COMPLETE_TYPE_PROGRAM; + } + } + + // TODO: Use reallocarray. + char** completions = (char**) malloc(sizeof(char**) * 1024 /* TODO: HARD-CODED! */); + size_t num_completions = 0; + + if ( complete_type == COMPLETE_TYPE_PROGRAM ) do + { + for ( size_t i = 0; builtin_commands[i]; i++ ) + { + const char* builtin = builtin_commands[i]; + if ( strncmp(builtin, partial + complete_at - used_before, used_before) != 0 ) + continue; + // TODO: Add allocation check! + completions[num_completions++] = strdup(builtin + used_before); + } + char* path = strdup_safe(getenv("PATH")); + if ( !path ) + { + complete_type = COMPLETE_TYPE_FILE; + break; + } + char* path_input = path; + char* saved_ptr; + char* component; + while ( (component = strtok_r(path_input, " ", &saved_ptr)) ) + { + if ( DIR* dir = opendir(component) ) + { + while ( struct dirent* entry = readdir(dir) ) + { + if ( strncmp(entry->d_name, partial + complete_at - used_before, used_before) != 0 ) + continue; + if ( used_before == 0 && entry->d_name[0] == '.' ) + continue; + // TODO: Add allocation check! + completions[num_completions++] = strdup(entry->d_name + used_before); + } + closedir(dir); + } + path_input = NULL; + } + free(path); + } while ( false ); + + if ( complete_type == COMPLETE_TYPE_FILE || + complete_type == COMPLETE_TYPE_EXECUTABLE || + complete_type == COMPLETE_TYPE_DIRECTORY ) do + { + const char* pattern = partial + complete_at - used_before; + size_t pattern_length = used_before; + + char* dirpath_alloc = NULL; + const char* dirpath; + if ( !memchr(pattern, '/', pattern_length) ) + dirpath = "."; + else if ( pattern_length && pattern[pattern_length-1] == '/' ) + { + dirpath_alloc = strndup(pattern, pattern_length); + if ( !dirpath_alloc ) + break; + dirpath = dirpath_alloc; + pattern += pattern_length; + pattern_length = 0; + } + else + { + dirpath_alloc = strndup(pattern, pattern_length); + if ( !dirpath_alloc ) + break; + dirpath = dirname(dirpath_alloc); + const char* last_slash = (const char*) memrchr(pattern, '/', pattern_length); + size_t last_slash_offset = (uintptr_t) last_slash - (uintptr_t) pattern; + pattern += last_slash_offset + 1; + pattern_length -= last_slash_offset + 1; + } + used_before = pattern_length; + DIR* dir = opendir(dirpath); + if ( !dir ) + { + free(dirpath_alloc); + break; + } + while ( struct dirent* entry = readdir(dir) ) + { + if ( strncmp(entry->d_name, pattern, pattern_length) != 0 ) + continue; + if ( pattern_length == 0 && entry->d_name[0] == '.' ) + continue; + struct stat st; + bool is_directory = entry->d_type == DT_DIR || + (entry->d_type == DT_UNKNOWN && + !fstatat(dirfd(dir), entry->d_name, &st, 0) && + S_ISDIR(st.st_mode)); + bool is_executable = complete_type == COMPLETE_TYPE_EXECUTABLE && + !fstatat(dirfd(dir), entry->d_name, &st, 0) && + st.st_mode & 0111; + if ( complete_type == COMPLETE_TYPE_DIRECTORY && !is_directory ) + continue; + if ( complete_type == COMPLETE_TYPE_EXECUTABLE && + !(is_directory || is_executable) ) + continue; + size_t name_length = strlen(entry->d_name); + char* completion = (char*) malloc(name_length - pattern_length + 1 + 1); + if ( !completion ) + continue; + strcpy(completion, entry->d_name + pattern_length); + if ( is_directory ) + strcat(completion, "/"); + completions[num_completions++] = completion; + } + closedir(dir); + free(dirpath_alloc); + } while ( false ); + + if ( complete_type == COMPLETE_TYPE_VARIABLE ) do + { + const char* pattern = partial + complete_at - used_before + 1; + size_t pattern_length = used_before - 1; + if ( memchr(pattern, '=', pattern_length) ) + break; + for ( size_t i = 0; environ[i]; i++ ) + { + if ( strncmp(pattern, environ[i], pattern_length) != 0 ) + continue; + const char* rest = environ[i] + pattern_length; + size_t equal_offset = strcspn(rest, "="); + if ( rest[equal_offset] != '=' ) + continue; + completions[num_completions++] = strndup(rest, equal_offset); + } + } while ( false ); + + *used_before_ptr = used_before; + *used_after_ptr = used_after; + + return *completions_ptr = completions, num_completions; +} + +int get_and_run_command_interactive(bool exit_on_error, bool* exitexec) +{ + update_pwd(); + update_env(); + + static struct edit_line edit_state; + + edit_state.in_fd = 0; + edit_state.out_fd = 1; + edit_state.check_input_incomplete_context = NULL; + edit_state.check_input_incomplete = does_line_editing_need_another_line; + edit_state.trap_eof_opportunity_context = &edit_state; + edit_state.trap_eof_opportunity = on_trap_eof; + edit_state.complete_context = NULL; + edit_state.complete = do_complete; + + const char* print_username = getlogin(); + if ( !print_username ) + print_username = getuid() == 0 ? "root" : "?"; + char hostname[256]; + if ( gethostname(hostname, sizeof(hostname)) < 0 ) + strlcpy(hostname, "(none)", sizeof(hostname)); + const char* print_hostname = hostname; + const char* print_dir = getenv_safe("PWD", "?"); + const char* home_dir = getenv_safe("HOME", ""); + + const char* print_dir_1 = print_dir; + const char* print_dir_2 = ""; + + size_t home_dir_len = strlen(home_dir); + if ( home_dir_len && strncmp(print_dir, home_dir, home_dir_len) == 0 ) + { + print_dir_1 = "~"; + print_dir_2 = print_dir + home_dir_len; + } + + char* ps1; + asprintf(&ps1, "\e[32m%s@%s \e[36m%s%s #\e[37m ", + print_username, + print_hostname, + print_dir_1, + print_dir_2); + + edit_state.ps1 = ps1; + edit_state.ps2 = "> "; + + edit_line(&edit_state); + + int result = status; + + if ( edit_state.eof_condition ) + { + if ( is_parent_init() ) + printf("Type exit to shutdown the system.\n"); + else + *exitexec = true; + } + else if ( !edit_state.abort_editing ) + { + char* command = edit_line_result(&edit_state); + for ( size_t i = 0; command[i]; i++ ) + if ( command[i + 0] == '\\' && command[i + 1] == '\n' ) + command[i + 0] = ' ', + command[i + 1] = ' '; + result = run_command(command, true, exit_on_error, exitexec); + free(command); + } + + free(ps1); + + return result; +} + +int get_and_run_command_non_interactive(FILE* fp, + const char* fpname, + bool exit_on_error, + bool* exitexec) +{ + int fd = fileno(fp); + + static char* command = NULL; + static size_t commandlen = 1024; + if ( !command ) + commandlen = 1024, + command = (char*) malloc((commandlen+1) * sizeof(char)); + if ( !command ) + error(64, errno, "malloc"); + + command[0] = '\0'; + + size_t commandused = 0; + while ( true ) + { + char c; + ssize_t bytesread; + if ( 0 <= fd ) + bytesread = read(fd, &c, sizeof(c)); + else + { + int ic = fgetc(fp); + if ( ic == EOF && ferror(fp) ) + bytesread = -1; + else if ( ic == EOF ) + bytesread = 0; + else + c = (char) (unsigned char) ic, bytesread = 1; + } + if ( bytesread < 0 && errno == EINTR ) + return status; + if ( bytesread < 0 ) + error(64, errno, "read %s", fpname); + if ( !bytesread ) + { + if ( commandused ) + break; + *exitexec = true; + return status; + } + if ( !c ) + continue; + if ( c == '\n' && is_shell_input_ready(command) ) + break; + if ( commandused == commandlen ) + { + size_t newlen = commandlen * 2; + size_t newsize = (newlen+1) * sizeof(char); + char* newcommand = (char*) realloc(command, newsize); + if ( !newcommand ) + error(64, errno, "realloc"); + command = newcommand; + commandlen = newsize; + } + command[commandused++] = c; + command[commandused] = '\0'; + } + + return run_command(command, false, exit_on_error, exitexec); +} + void load_argv_variables(int argc, char* argv[]) { char varname[sizeof(int) * 3]; @@ -572,15 +1830,19 @@ int run(FILE* fp, int argc, char* argv[], const char* name, bool interactive, bool exitexec = false; int exitstatus; do - exitstatus = get_and_run_command(fp, name, interactive, exit_on_error, - &exitexec); + { + if ( interactive ) + exitstatus = get_and_run_command_interactive(exit_on_error, &exitexec); + else + exitstatus = + get_and_run_command_non_interactive(fp, name, exit_on_error, &exitexec); + } while ( !exitexec ); return exitstatus; } int run_interactive(int argc, char* argv[], bool exit_on_error) { - signal(SIGINT, on_sigint); return run(stdin, argc, argv, "", true, exit_on_error); } @@ -626,7 +1888,7 @@ int main(int argc, char* argv[]) setenv("$", pidstr, 1); setenv("PPID", ppidstr, 1); setenv("?", "0", 1); - updatepwd(); + update_pwd(); if ( 2 <= argc && !strcmp(argv[1], "-c") ) return run_string(argc, argv, argv[2], exit_on_error); if ( 1 < argc )