| // Copyright 2016 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <dirent.h> |
| #include <limits.h> |
| #include <stdbool.h> |
| #include <stddef.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| #include <linenoise/linenoise.h> |
| #include <zircon/assert.h> |
| |
| #include "shell.h" |
| #include "nodes.h" |
| |
| #include "exec.h" |
| #include "memalloc.h" |
| #include "var.h" |
| |
| typedef struct { |
| // An index into the tokenized string which points at the first |
| // character of the last token (ie space separated component) of |
| // the line. |
| size_t start; |
| // Whether there are multiple non-enviroment components of the |
| // line to tokenize. For example: |
| // foo # found_command = false; |
| // foo bar # found_command = true; |
| // FOO=BAR quux # found_command = false; |
| bool found_command; |
| // Whether the end of the line is in a space-free string of the |
| // form 'FOO=BAR', which is the syntax to set an environment |
| // variable. |
| bool in_env; |
| } token_t; |
| |
| static token_t tokenize(const char* line, size_t line_length) { |
| token_t token = { |
| .start = 0u, |
| .found_command = false, |
| .in_env = false, |
| }; |
| bool in_token = false; |
| |
| for (size_t i = 0; i < line_length; i++) { |
| if (line[i] == ' ') { |
| token.start = i + 1; |
| |
| if (in_token && !token.in_env) { |
| token.found_command = true; |
| } |
| |
| in_token = false; |
| token.in_env = false; |
| continue; |
| } |
| |
| in_token = true; |
| token.in_env = token.in_env || line[i] == '='; |
| } |
| |
| return token; |
| } |
| |
| typedef struct { |
| const char* line_prefix; |
| const char* line_separator; |
| const char* file_prefix; |
| } completion_state_t; |
| |
| // Generate file name completions. |dir| is the directory to for |
| // matching filenames. File names must match |state->file_prefix| in |
| // order to be entered into |completions|. |state->line_prefix| and |
| // |state->line_separator| begin the line before the file completion. |
| static void complete_at_dir(DIR* dir, completion_state_t* state, |
| linenoiseCompletions* completions) { |
| ZX_DEBUG_ASSERT(strchr(state->file_prefix, '/') == NULL); |
| size_t file_prefix_len = strlen(state->file_prefix); |
| |
| struct dirent *de; |
| while ((de = readdir(dir)) != NULL) { |
| if (strncmp(state->file_prefix, de->d_name, file_prefix_len)) { |
| continue; |
| } |
| if (!strcmp(de->d_name, ".")) { |
| continue; |
| } |
| if (!strcmp(de->d_name, "..")) { |
| continue; |
| } |
| |
| char completion[LINE_MAX]; |
| strncpy(completion, state->line_prefix, sizeof(completion)); |
| completion[sizeof(completion) - 1] = '\0'; |
| size_t remaining = sizeof(completion) - strlen(completion) - 1; |
| strncat(completion, state->line_separator, remaining); |
| remaining = sizeof(completion) - strlen(completion) - 1; |
| strncat(completion, de->d_name, remaining); |
| |
| linenoiseAddCompletion(completions, completion); |
| } |
| } |
| |
| void tab_complete(const char* line, linenoiseCompletions* completions) { |
| size_t input_line_length = strlen(line); |
| |
| token_t token = tokenize(line, input_line_length); |
| |
| if (token.in_env) { |
| // We can't tab complete environment variables. |
| return; |
| } |
| |
| char buf[LINE_MAX]; |
| size_t token_length = input_line_length - token.start; |
| if (token_length >= sizeof(buf)) { |
| return; |
| } |
| strncpy(buf, line, sizeof(buf)); |
| char* partial_path = buf + token.start; |
| |
| // The following variables are set by the following block of code |
| // in each of three different cases: |
| // |
| // 1. There is no slash in the last token, and we are giving an |
| // argument to a command. An example: |
| // foo bar ba |
| // We are searching the current directory (".") for files |
| // matching the prefix "ba", to join with a space to the line |
| // prefix "foo bar". |
| // |
| // 2. There is no slash in the only token. An example: |
| // fo |
| // We are searching the PATH environment variable for files |
| // matching the prefix "fo". There is no line prefix or |
| // separator in this case. |
| // |
| // 3. There is a slash in the last token. An example: |
| // foo bar baz/quu |
| // In this case, we are searching the directory specified by |
| // the token (up until the final '/', so "baz" in this case) |
| // for files with the prefix "quu", to join with a slash to the |
| // line prefix "foo bar baz". |
| completion_state_t completion_state; |
| const char** paths = NULL; |
| |
| // |paths| for cases 1 and 3 respectively. |
| const char* local_paths[] = { ".", NULL }; |
| const char* partial_paths[] = { partial_path, NULL }; |
| |
| char* file_prefix = strrchr(partial_path, '/'); |
| if (file_prefix == NULL) { |
| file_prefix = partial_path; |
| if (token.found_command) { |
| // Case 1. |
| // Because we are in a command, partial_path[-1] is a |
| // space we want to zero out. |
| ZX_DEBUG_ASSERT(token.start > 0); |
| ZX_DEBUG_ASSERT(partial_path[-1] == ' '); |
| partial_path[-1] = '\0'; |
| |
| completion_state.line_prefix = buf; |
| completion_state.line_separator = " "; |
| completion_state.file_prefix = file_prefix; |
| paths = local_paths; |
| } else { |
| // Case 2. |
| completion_state.line_prefix = ""; |
| completion_state.line_separator = ""; |
| completion_state.file_prefix = file_prefix; |
| } |
| } else { |
| // Case 3. |
| // Because we are in a multiple component file path, |
| // *file_prefix is a '/' we want to zero out. |
| ZX_DEBUG_ASSERT(*file_prefix == '/'); |
| *file_prefix = '\0'; |
| |
| completion_state.line_prefix = buf; |
| completion_state.line_separator = "/"; |
| completion_state.file_prefix = file_prefix + 1; |
| paths = partial_paths; |
| |
| // If the partial path is empty, it means we were given |
| // something like "/foo". We should therefore set the path to |
| // search to "/". |
| if (strlen(paths[0]) == 0) { |
| paths[0] = "/"; |
| } |
| } |
| |
| if (paths) { |
| for (; *paths != NULL; paths++) { |
| DIR* dir = opendir(*paths); |
| if (dir == NULL) { |
| continue; |
| } |
| complete_at_dir(dir, &completion_state, completions); |
| closedir(dir); |
| } |
| } else { |
| const char* path_env = pathval(); |
| char* pathname; |
| while ((pathname = padvance(&path_env, "")) != NULL) { |
| DIR* dir = opendir(pathname); |
| stunalloc(pathname); |
| if (dir == NULL) { |
| continue; |
| } |
| complete_at_dir(dir, &completion_state, completions); |
| closedir(dir); |
| } |
| } |
| } |
| |
| #ifdef mkinit |
| INCLUDE "tab.h" |
| INCLUDE <linenoise/linenoise.h> |
| INIT { |
| linenoiseSetCompletionCallback(tab_complete); |
| } |
| #endif |