1 // Copyright 2016 The Fuchsia Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include <dirent.h>
6 #include <limits.h>
7 #include <stdbool.h>
8 #include <stddef.h>
9 #include <stdlib.h>
10 #include <string.h>
11 
12 #include <linenoise/linenoise.h>
13 #include <zircon/assert.h>
14 
15 #include "shell.h"
16 #include "nodes.h"
17 
18 #include "exec.h"
19 #include "memalloc.h"
20 #include "var.h"
21 
22 typedef struct {
23     // An index into the tokenized string which points at the first
24     // character of the last token (ie space separated component) of
25     // the line.
26     size_t start;
27     // Whether there are multiple non-enviroment components of the
28     // line to tokenize. For example:
29     //     foo          # found_command = false;
30     //     foo bar      # found_command = true;
31     //     FOO=BAR quux # found_command = false;
32     bool found_command;
33     // Whether the end of the line is in a space-free string of the
34     // form 'FOO=BAR', which is the syntax to set an environment
35     // variable.
36     bool in_env;
37 } token_t;
38 
tokenize(const char * line,size_t line_length)39 static token_t tokenize(const char* line, size_t line_length) {
40     token_t token = {
41         .start = 0u,
42         .found_command = false,
43         .in_env = false,
44     };
45     bool in_token = false;
46 
47     for (size_t i = 0; i < line_length; i++) {
48         if (line[i] == ' ') {
49             token.start = i + 1;
50 
51             if (in_token && !token.in_env) {
52                 token.found_command = true;
53             }
54 
55             in_token = false;
56             token.in_env = false;
57             continue;
58         }
59 
60         in_token = true;
61         token.in_env = token.in_env || line[i] == '=';
62     }
63 
64     return token;
65 }
66 
67 typedef struct {
68     const char* line_prefix;
69     const char* line_separator;
70     const char* file_prefix;
71 } completion_state_t;
72 
73 // Generate file name completions. |dir| is the directory to for
74 // matching filenames. File names must match |state->file_prefix| in
75 // order to be entered into |completions|. |state->line_prefix| and
76 // |state->line_separator| begin the line before the file completion.
complete_at_dir(DIR * dir,completion_state_t * state,linenoiseCompletions * completions)77 static void complete_at_dir(DIR* dir, completion_state_t* state,
78                             linenoiseCompletions* completions) {
79     ZX_DEBUG_ASSERT(strchr(state->file_prefix, '/') == NULL);
80     size_t file_prefix_len = strlen(state->file_prefix);
81 
82     struct dirent *de;
83     while ((de = readdir(dir)) != NULL) {
84         if (strncmp(state->file_prefix, de->d_name, file_prefix_len)) {
85             continue;
86         }
87         if (!strcmp(de->d_name, ".")) {
88             continue;
89         }
90         if (!strcmp(de->d_name, "..")) {
91             continue;
92         }
93 
94         char completion[LINE_MAX];
95         strncpy(completion, state->line_prefix, sizeof(completion));
96         completion[sizeof(completion) - 1] = '\0';
97         size_t remaining = sizeof(completion) - strlen(completion) - 1;
98         strncat(completion, state->line_separator, remaining);
99         remaining = sizeof(completion) - strlen(completion) - 1;
100         strncat(completion, de->d_name, remaining);
101 
102         linenoiseAddCompletion(completions, completion);
103     }
104 }
105 
tab_complete(const char * line,linenoiseCompletions * completions)106 void tab_complete(const char* line, linenoiseCompletions* completions) {
107     size_t input_line_length = strlen(line);
108 
109     token_t token = tokenize(line, input_line_length);
110 
111     if (token.in_env) {
112         // We can't tab complete environment variables.
113         return;
114     }
115 
116     char buf[LINE_MAX];
117     size_t token_length = input_line_length - token.start;
118     if (token_length >= sizeof(buf)) {
119         return;
120     }
121     strncpy(buf, line, sizeof(buf));
122     char* partial_path = buf + token.start;
123 
124     // The following variables are set by the following block of code
125     // in each of three different cases:
126     //
127     // 1. There is no slash in the last token, and we are giving an
128     //    argument to a command. An example:
129     //        foo bar ba
130     //    We are searching the current directory (".") for files
131     //    matching the prefix "ba", to join with a space to the line
132     //    prefix "foo bar".
133     //
134     // 2. There is no slash in the only token. An example:
135     //        fo
136     //    We are searching the PATH environment variable for files
137     //    matching the prefix "fo". There is no line prefix or
138     //    separator in this case.
139     //
140     // 3. There is a slash in the last token. An example:
141     //        foo bar baz/quu
142     //    In this case, we are searching the directory specified by
143     //    the token (up until the final '/', so "baz" in this case)
144     //    for files with the prefix "quu", to join with a slash to the
145     //    line prefix "foo bar baz".
146     completion_state_t completion_state;
147     const char** paths = NULL;
148 
149     // |paths| for cases 1 and 3 respectively.
150     const char* local_paths[] = { ".", NULL };
151     const char* partial_paths[] = { partial_path, NULL };
152 
153     char* file_prefix = strrchr(partial_path, '/');
154     if (file_prefix == NULL) {
155         file_prefix = partial_path;
156         if (token.found_command) {
157             // Case 1.
158             // Because we are in a command, partial_path[-1] is a
159             // space we want to zero out.
160             ZX_DEBUG_ASSERT(token.start > 0);
161             ZX_DEBUG_ASSERT(partial_path[-1] == ' ');
162             partial_path[-1] = '\0';
163 
164             completion_state.line_prefix = buf;
165             completion_state.line_separator = " ";
166             completion_state.file_prefix = file_prefix;
167             paths = local_paths;
168         } else {
169             // Case 2.
170             completion_state.line_prefix = "";
171             completion_state.line_separator = "";
172             completion_state.file_prefix = file_prefix;
173         }
174     } else {
175         // Case 3.
176         // Because we are in a multiple component file path,
177         // *file_prefix is a '/' we want to zero out.
178         ZX_DEBUG_ASSERT(*file_prefix == '/');
179         *file_prefix = '\0';
180 
181         completion_state.line_prefix = buf;
182         completion_state.line_separator = "/";
183         completion_state.file_prefix = file_prefix + 1;
184         paths = partial_paths;
185 
186         // If the partial path is empty, it means we were given
187         // something like "/foo". We should therefore set the path to
188         // search to "/".
189         if (strlen(paths[0]) == 0) {
190             paths[0] = "/";
191         }
192     }
193 
194     if (paths) {
195         for (; *paths != NULL; paths++) {
196             DIR* dir = opendir(*paths);
197             if (dir == NULL) {
198                 continue;
199             }
200             complete_at_dir(dir, &completion_state, completions);
201             closedir(dir);
202         }
203     } else {
204         const char* path_env = pathval();
205         char* pathname;
206         while ((pathname = padvance(&path_env, "")) != NULL) {
207             DIR* dir = opendir(pathname);
208             stunalloc(pathname);
209             if (dir == NULL) {
210                 continue;
211             }
212             complete_at_dir(dir, &completion_state, completions);
213             closedir(dir);
214         }
215     }
216 }
217 
218 #ifdef mkinit
219 INCLUDE "tab.h"
220 INCLUDE <linenoise/linenoise.h>
221 INIT {
222     linenoiseSetCompletionCallback(tab_complete);
223 }
224 #endif
225