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