amiro-os / os / core / src / aos_shell.c @ e545e620
History | View | Annotate | Download (31.5 KB)
1 | e545e620 | Thomas Schöpping | /*
|
---|---|---|---|
2 | AMiRo-OS is an operating system designed for the Autonomous Mini Robot (AMiRo) platform.
|
||
3 | Copyright (C) 2016..2018 Thomas Schöpping et al.
|
||
4 | |||
5 | This program is free software: you can redistribute it and/or modify
|
||
6 | it under the terms of the GNU General Public License as published by
|
||
7 | the Free Software Foundation, either version 3 of the License, or
|
||
8 | (at your option) any later version.
|
||
9 | |||
10 | This program is distributed in the hope that it will be useful,
|
||
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
13 | GNU General Public License for more details.
|
||
14 | |||
15 | You should have received a copy of the GNU General Public License
|
||
16 | along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
17 | */
|
||
18 | |||
19 | #include <aos_shell.h> |
||
20 | |||
21 | #include <aos_debug.h> |
||
22 | #include <aos_time.h> |
||
23 | #include <aos_system.h> |
||
24 | #include <chprintf.h> |
||
25 | #include <string.h> |
||
26 | #include <aos_thread.h> |
||
27 | |||
28 | /**
|
||
29 | * @brief Enumerator of special keyboard keys.
|
||
30 | */
|
||
31 | typedef enum special_key { |
||
32 | KEY_UNKNOWN, /**< any/unknow key */
|
||
33 | KEY_AMBIGUOUS, /**< key is ambiguous */
|
||
34 | KEY_TAB, /**< tabulator key */
|
||
35 | KEY_ESCAPE, /**< escape key */
|
||
36 | KEY_BACKSPACE, /**< backspace key */
|
||
37 | KEY_INSERT, /**< insert key */
|
||
38 | KEY_DELETE, /**< delete key */
|
||
39 | KEY_HOME, /**< home key */
|
||
40 | KEY_END, /**< end key */
|
||
41 | KEY_PAGE_UP, /**< page up key */
|
||
42 | KEY_PAGE_DOWN, /**< page down key */
|
||
43 | KEY_ARROW_UP, /**< arrow up key */
|
||
44 | KEY_ARROW_DOWN, /**< arrow down key */
|
||
45 | KEY_ARROW_LEFT, /**< arrow left key */
|
||
46 | KEY_ARROW_RIGHT, /**< arrow right key */
|
||
47 | } special_key_t; |
||
48 | |||
49 | /**
|
||
50 | * @brief Enumerator for case (in)sensitive character matching.
|
||
51 | */
|
||
52 | typedef enum charmatch { |
||
53 | CHAR_MATCH_NOT = 0, /**< Characters do not match at all. */ |
||
54 | CHAR_MATCH_NCASE = 1, /**< Characters would match case insensitive. */ |
||
55 | CHAR_MATCH_CASE = 2, /**< Characters do match with case. */ |
||
56 | } charmatch_t; |
||
57 | |||
58 | /**
|
||
59 | * @brief Print the shell prompt
|
||
60 | * @details Depending on the configuration flags, the system uptime is printed before the prompt string.
|
||
61 | *
|
||
62 | * @param[in] shell Pointer to the shell object.
|
||
63 | */
|
||
64 | static void _printPrompt(aos_shell_t* shell) |
||
65 | { |
||
66 | aosDbgCheck(shell != NULL);
|
||
67 | aosDbgCheck(shell->stream != NULL);
|
||
68 | |||
69 | // print the system uptime before prompt is configured
|
||
70 | if (shell->config & AOS_SHELL_CONFIG_PROMPT_UPTIME) {
|
||
71 | // get current system uptime
|
||
72 | aos_timestamp_t uptime; |
||
73 | aosSysGetUptime(&uptime); |
||
74 | |||
75 | chprintf(shell->stream, "[%01u:%02u:%02u:%02u:%03u:%03u] ",
|
||
76 | (uint32_t)(uptime / MICROSECONDS_PER_DAY), |
||
77 | (uint8_t)(uptime % MICROSECONDS_PER_DAY / MICROSECONDS_PER_HOUR), |
||
78 | (uint8_t)(uptime % MICROSECONDS_PER_HOUR / MICROSECONDS_PER_MINUTE), |
||
79 | (uint8_t)(uptime % MICROSECONDS_PER_MINUTE / MICROSECONDS_PER_SECOND), |
||
80 | (uint16_t)(uptime % MICROSECONDS_PER_SECOND / MICROSECONDS_PER_MILLISECOND), |
||
81 | (uint16_t)(uptime % MICROSECONDS_PER_MILLISECOND / MICROSECONDS_PER_MICROSECOND)); |
||
82 | } |
||
83 | |||
84 | // print the actual prompt string
|
||
85 | if (shell->prompt && !(shell->config & AOS_SHELL_CONFIG_PROMPT_MINIMAL)) {
|
||
86 | chprintf(shell->stream, "%s$ ", shell->prompt);
|
||
87 | } else {
|
||
88 | chprintf(shell->stream, "%>$ ");
|
||
89 | } |
||
90 | |||
91 | return;
|
||
92 | } |
||
93 | |||
94 | /**
|
||
95 | * @brief Interprete a escape sequence
|
||
96 | *
|
||
97 | * @param[in] seq Character sequence to interprete.
|
||
98 | * Must be terminated by NUL byte.
|
||
99 | *
|
||
100 | * @return A @p special_key value.
|
||
101 | */
|
||
102 | static special_key_t _interpreteEscapeSequence(const char seq[]) |
||
103 | { |
||
104 | // local variables
|
||
105 | bool ambiguous = false; |
||
106 | int cmp = 0; |
||
107 | |||
108 | // TAB
|
||
109 | /* not supported yet; use "\x09" instead */
|
||
110 | |||
111 | // BACKSPACE
|
||
112 | /* not supported yet; use "\x08" instead */
|
||
113 | |||
114 | // ESCAPE
|
||
115 | cmp = strcmp(seq, "\x1B");
|
||
116 | if (cmp == 0) { |
||
117 | return KEY_ESCAPE;
|
||
118 | } else {
|
||
119 | ambiguous |= (cmp < 0);
|
||
120 | } |
||
121 | |||
122 | // INSERT
|
||
123 | cmp = strcmp(seq, "\x1B\x5B\x32\x7E");
|
||
124 | if (cmp == 0) { |
||
125 | return KEY_INSERT;
|
||
126 | } else {
|
||
127 | ambiguous |= (cmp < 0);
|
||
128 | } |
||
129 | |||
130 | // DELETE
|
||
131 | cmp = strcmp(seq, "\x1B\x5B\x33\x7E");
|
||
132 | if (cmp == 0) { |
||
133 | return KEY_DELETE;
|
||
134 | } else {
|
||
135 | ambiguous |= (cmp < 0);
|
||
136 | } |
||
137 | |||
138 | // HOME
|
||
139 | cmp = strcmp(seq, "\x1B\x4F\x48");
|
||
140 | if (cmp == 0) { |
||
141 | return KEY_HOME;
|
||
142 | } else {
|
||
143 | ambiguous |= (cmp < 0);
|
||
144 | } |
||
145 | |||
146 | // END
|
||
147 | cmp = strcmp(seq, "\x1B\x4F\x46");
|
||
148 | if (cmp == 0) { |
||
149 | return KEY_END;
|
||
150 | } else {
|
||
151 | ambiguous |= (cmp < 0);
|
||
152 | } |
||
153 | |||
154 | // PAGE UP
|
||
155 | cmp = strcmp(seq, "\x1B\x5B\x35\x7E");
|
||
156 | if (cmp == 0) { |
||
157 | return KEY_PAGE_UP;
|
||
158 | } else {
|
||
159 | ambiguous |= (cmp < 0);
|
||
160 | } |
||
161 | |||
162 | // PAGE DOWN
|
||
163 | cmp = strcmp(seq, "\x1B\x5B\x36\x7E");
|
||
164 | if (cmp == 0) { |
||
165 | return KEY_PAGE_DOWN;
|
||
166 | } else {
|
||
167 | ambiguous |= (cmp < 0);
|
||
168 | } |
||
169 | |||
170 | // ARROW UP
|
||
171 | cmp = strcmp(seq, "\x1B\x5B\x41");
|
||
172 | if (cmp == 0) { |
||
173 | return KEY_ARROW_UP;
|
||
174 | } else {
|
||
175 | ambiguous |= (cmp < 0);
|
||
176 | } |
||
177 | |||
178 | // ARROW DOWN
|
||
179 | cmp = strcmp(seq, "\x1B\x5B\x42");
|
||
180 | if (cmp == 0) { |
||
181 | return KEY_ARROW_DOWN;
|
||
182 | } else {
|
||
183 | ambiguous |= (cmp < 0);
|
||
184 | } |
||
185 | |||
186 | // ARROW LEFT
|
||
187 | cmp = strcmp(seq, "\x1B\x5B\x44");
|
||
188 | if (cmp == 0) { |
||
189 | return KEY_ARROW_LEFT;
|
||
190 | } else {
|
||
191 | ambiguous |= (cmp < 0);
|
||
192 | } |
||
193 | |||
194 | // ARROW RIGHT
|
||
195 | cmp = strcmp(seq, "\x1B\x5B\x43");
|
||
196 | if (cmp == 0) { |
||
197 | return KEY_ARROW_RIGHT;
|
||
198 | } else {
|
||
199 | ambiguous |= (cmp < 0);
|
||
200 | } |
||
201 | |||
202 | return ambiguous ? KEY_AMBIGUOUS : KEY_UNKNOWN;
|
||
203 | } |
||
204 | |||
205 | /**
|
||
206 | * @brief Move the cursor in the terminal
|
||
207 | *
|
||
208 | * @param[in] shell Pointer to the shell object.
|
||
209 | * @param[in] from Starting position of the cursor.
|
||
210 | * @param[in] to Target position to move the cursor to.
|
||
211 | *
|
||
212 | * @return The number of positions moved.
|
||
213 | */
|
||
214 | static int _moveCursor(aos_shell_t* shell, const size_t from, const size_t to) |
||
215 | { |
||
216 | aosDbgCheck(shell != NULL);
|
||
217 | aosDbgCheck(shell->stream != NULL);
|
||
218 | |||
219 | // local variables
|
||
220 | size_t pos = from; |
||
221 | |||
222 | // move cursor left by printing backspaces
|
||
223 | while (pos > to) {
|
||
224 | streamPut(shell->stream, '\b');
|
||
225 | --pos; |
||
226 | } |
||
227 | |||
228 | // move cursor right by printing line content
|
||
229 | while (pos < to) {
|
||
230 | streamPut(shell->stream, shell->line[pos]); |
||
231 | ++pos; |
||
232 | } |
||
233 | |||
234 | return (int)pos - (int)from; |
||
235 | } |
||
236 | |||
237 | /**
|
||
238 | * @brief Print content of the shell line
|
||
239 | *
|
||
240 | * @param[in] shell Pointer to the shell object.
|
||
241 | * @param[in] from First position to start printing from.
|
||
242 | * @param[in] to Position after the last character to print.
|
||
243 | *
|
||
244 | * @return Number of characters printed.
|
||
245 | */
|
||
246 | static inline size_t _printLine(aos_shell_t* shell, const size_t from, const size_t to) |
||
247 | { |
||
248 | aosDbgCheck(shell != NULL);
|
||
249 | aosDbgCheck(shell->stream != NULL);
|
||
250 | |||
251 | // local variables
|
||
252 | size_t cnt; |
||
253 | |||
254 | for (cnt = 0; from + cnt < to; ++cnt) { |
||
255 | streamPut(shell->stream, shell->line[from + cnt]); |
||
256 | } |
||
257 | |||
258 | return cnt;
|
||
259 | } |
||
260 | |||
261 | /**
|
||
262 | * @brief Compare two characters.
|
||
263 | *
|
||
264 | * @param[in] lhs First character to compare.
|
||
265 | * @param[in] rhs Second character to compare.
|
||
266 | *
|
||
267 | * @return How well the characters match.
|
||
268 | */
|
||
269 | static inline charmatch_t _charcmp(char lhs, char rhs) |
||
270 | { |
||
271 | // if lhs is a upper case letter and rhs is a lower case letter
|
||
272 | if (lhs >= 'A' && lhs <= 'Z' && rhs >= 'a' && rhs <= 'z') { |
||
273 | return (lhs == (rhs - 'a' + 'A')) ? CHAR_MATCH_NCASE : CHAR_MATCH_NOT; |
||
274 | } |
||
275 | // if lhs is a lower case letter and rhs is a upper case letter
|
||
276 | else if (lhs >= 'a' && lhs <= 'z' && rhs >= 'A' && rhs <= 'Z') { |
||
277 | return ((lhs - 'a' + 'A') == rhs) ? CHAR_MATCH_NCASE : CHAR_MATCH_NOT; |
||
278 | } |
||
279 | // default
|
||
280 | else {
|
||
281 | return (lhs == rhs) ? CHAR_MATCH_CASE : CHAR_MATCH_NOT;
|
||
282 | } |
||
283 | } |
||
284 | |||
285 | /**
|
||
286 | * @brief Maps an character from ASCII to a modified custom encoding.
|
||
287 | * @details The custom character encoding is very similar to ASCII and has the following structure:
|
||
288 | * 0x00=NULL ... 0x40='@' (identically to ASCII)
|
||
289 | * 0x4A='a'; 0x4B='A'; 0x4C='b'; 0x4D='B' ... 0x73='z'; 0x74='Z' (custom letter order)
|
||
290 | * 0x75='[' ... 0x7A='`' (0x5B..0x60 is ASCII)
|
||
291 | * 0x7B='{' ... 0x7F=DEL (identically to ASCII)
|
||
292 | *
|
||
293 | * @param[in] c Character to map to the custom encoding.
|
||
294 | *
|
||
295 | * @return The customly encoded character.
|
||
296 | */
|
||
297 | static inline char _mapAscii2Custom(const char c) |
||
298 | { |
||
299 | if (c >= 'A' && c <= 'Z') { |
||
300 | return ((c - 'A') * 2) + 'A' + 1; |
||
301 | } else if (c > 'Z' && c < 'a') { |
||
302 | return c + ('z' - 'a') + 1; |
||
303 | } else if (c >= 'a' && c <= 'z') { |
||
304 | return ((c - 'a') * 2) + 'A'; |
||
305 | } else {
|
||
306 | return c;
|
||
307 | } |
||
308 | } |
||
309 | |||
310 | /**
|
||
311 | * @brief Compares two strings wrt letter case.
|
||
312 | * @details Comparisson uses a custom character encoding or mapping.
|
||
313 | * See @p _mapAscii2Custom for details.
|
||
314 | *
|
||
315 | * @param[in] str1 First string to compare.
|
||
316 | * @param[in] str2 Second string to compare.
|
||
317 | * @param[in] cs Flag indicating whether comparison shall be case sensitive.
|
||
318 | * @param[in,out] n Maximum number of character to compare (in) and number of matching characters (out).
|
||
319 | * If a null pointer is specified, this parameter is ignored.
|
||
320 | * If the value pointed to is zero, comarison will not be limited.
|
||
321 | * @param[out] m Optional indicator whether there was at least one case mismatch.
|
||
322 | *
|
||
323 | * @return Integer value indicating the relationship between the strings.
|
||
324 | * @retval <0 The first character that does not match has a lower value in str1 than in str2.
|
||
325 | * @retval 0 The contents of both strings are equal.
|
||
326 | * @retval >0 The first character that does not match has a greater value in str1 than in str2.
|
||
327 | */
|
||
328 | static int _strccmp(const char *str1, const char *str2, bool cs, size_t* n, charmatch_t* m) |
||
329 | { |
||
330 | aosDbgCheck(str1 != NULL);
|
||
331 | aosDbgCheck(str2 != NULL);
|
||
332 | |||
333 | // initialize variables
|
||
334 | if (m) {
|
||
335 | *m = CHAR_MATCH_NOT; |
||
336 | } |
||
337 | size_t i = 0;
|
||
338 | |||
339 | // iterate through the strings
|
||
340 | while ((n == NULL) || (*n == 0) || (*n > 0 && i < *n)) { |
||
341 | // break on NUL
|
||
342 | if (str1[i] == '\0' || str2[i] == '\0') { |
||
343 | if (n) {
|
||
344 | *n = i; |
||
345 | } |
||
346 | break;
|
||
347 | } |
||
348 | // compare character
|
||
349 | const charmatch_t match = _charcmp(str1[i], str2[i]);
|
||
350 | if ((match == CHAR_MATCH_CASE) || (!cs && match == CHAR_MATCH_NCASE)) {
|
||
351 | if (m != NULL && *m != CHAR_MATCH_NCASE) { |
||
352 | *m = match; |
||
353 | } |
||
354 | ++i; |
||
355 | } else {
|
||
356 | if (n) {
|
||
357 | *n = i; |
||
358 | } |
||
359 | break;
|
||
360 | } |
||
361 | } |
||
362 | |||
363 | return _mapAscii2Custom(str1[i]) - _mapAscii2Custom(str2[i]);
|
||
364 | } |
||
365 | |||
366 | /**
|
||
367 | * @brief Reads a line from input stream
|
||
368 | * @details The line is directly written to the given shell object.
|
||
369 | *
|
||
370 | * @param[in] shell Pointer to the shell object.
|
||
371 | *
|
||
372 | * @return A status indicator.
|
||
373 | * @retval AOS_SUCCESS Input sucessfully read, line is valid.
|
||
374 | * @retval AOS_ERROR An I/O error occurred.
|
||
375 | */
|
||
376 | static aos_status_t _readLine(aos_shell_t* shell)
|
||
377 | { |
||
378 | aosDbgCheck(shell != NULL);
|
||
379 | |||
380 | /*
|
||
381 | * Enumerator to encode a function.
|
||
382 | */
|
||
383 | typedef enum { |
||
384 | READ_CHAR, |
||
385 | AUTOFILL, |
||
386 | SUGGEST, |
||
387 | INS_TOGGLE, |
||
388 | DELETE_FORWARD, |
||
389 | DELETE_BACKWARD, |
||
390 | RECALL_LAST, |
||
391 | CLEAR, |
||
392 | CURSOR2START, |
||
393 | CURSOR2END, |
||
394 | CURSOR_LEFT, |
||
395 | CURSOR_RIGHT, |
||
396 | EXECUTE, |
||
397 | ESC_START, |
||
398 | NONE, |
||
399 | } func_t; |
||
400 | |||
401 | // local variables
|
||
402 | func_t func = NONE; |
||
403 | func_t lastfunc = NONE; |
||
404 | bool noinput = true; |
||
405 | size_t lineend = 0;
|
||
406 | size_t cursorpos = 0;
|
||
407 | char c;
|
||
408 | uint8_t escp = 0;
|
||
409 | char escseq[5] = {'\0'}; |
||
410 | |||
411 | // read character by character from stream
|
||
412 | while (streamRead(shell->stream, (uint8_t*)&c, 1)) { |
||
413 | special_key_t key = KEY_UNKNOWN; |
||
414 | |||
415 | // parse escape sequence
|
||
416 | if (escp > 0) { |
||
417 | escseq[escp] = c; |
||
418 | ++escp; |
||
419 | key = _interpreteEscapeSequence(escseq); |
||
420 | if (key == KEY_AMBIGUOUS) {
|
||
421 | // read next byte to resolve ambiguity
|
||
422 | continue;
|
||
423 | } else {
|
||
424 | // if the escape sequence could either be parsed sucessfully
|
||
425 | // or there is no match (KEY_UNKNOWN),
|
||
426 | // reset the sequence variables and interprete key/character
|
||
427 | escp = 0;
|
||
428 | memset(escseq, '\0', sizeof(escseq)); |
||
429 | } |
||
430 | } |
||
431 | |||
432 | // interprete keys or characters
|
||
433 | { |
||
434 | func = NONE; // default
|
||
435 | if (key == KEY_UNKNOWN &&
|
||
436 | (/* printable character */ c >= '\x20' && c <= '\x7E') ) { |
||
437 | func = READ_CHAR; |
||
438 | } else if (key == KEY_TAB || |
||
439 | /* horizontal tab ('\t') */ c == '\x09') { |
||
440 | // pressing tab once applies auto fill,
|
||
441 | // presing a second time prints suggestions
|
||
442 | if (lastfunc == AUTOFILL || lastfunc == SUGGEST) {
|
||
443 | func = SUGGEST; |
||
444 | } else {
|
||
445 | func = AUTOFILL; |
||
446 | } |
||
447 | } else if (key == KEY_INSERT) { |
||
448 | func = INS_TOGGLE; |
||
449 | } else if (key == KEY_DELETE || |
||
450 | /* [DEL] */ c == '\x7F') { |
||
451 | // ignore of cursor is very right
|
||
452 | if (cursorpos < lineend) {
|
||
453 | func = DELETE_FORWARD; |
||
454 | } |
||
455 | } else if (key == KEY_BACKSPACE || |
||
456 | /* backpace ('\b') */c == '\x08') { |
||
457 | // ignore if cursor is very left
|
||
458 | if (cursorpos > 0) { |
||
459 | func = DELETE_BACKWARD; |
||
460 | } |
||
461 | } else if (key == KEY_PAGE_UP || |
||
462 | key == KEY_ARROW_UP) { |
||
463 | // ignore if there was some input
|
||
464 | if (noinput) {
|
||
465 | func = RECALL_LAST; |
||
466 | } |
||
467 | } else if (key == KEY_PAGE_DOWN || |
||
468 | key == KEY_ARROW_DOWN || |
||
469 | /* end of test */ c == '\x03' || |
||
470 | /* end of transmission */ c == '\x04') { |
||
471 | // ignore if line is empty
|
||
472 | if (lineend > 0) { |
||
473 | func = CLEAR; |
||
474 | } |
||
475 | } else if (key == KEY_HOME) { |
||
476 | // ignore if cursor is very left
|
||
477 | if (cursorpos > 0) { |
||
478 | func = CURSOR2START; |
||
479 | } |
||
480 | } else if (key == KEY_END) { |
||
481 | // ignore if cursor is very right
|
||
482 | if (cursorpos < lineend) {
|
||
483 | func = CURSOR2END; |
||
484 | } |
||
485 | } else if (key == KEY_ARROW_LEFT) { |
||
486 | // ignore if cursor is very left
|
||
487 | if (cursorpos > 0) { |
||
488 | func = CURSOR_LEFT; |
||
489 | } |
||
490 | } else if (key == KEY_ARROW_RIGHT) { |
||
491 | // ignore if cursor is very right
|
||
492 | if (cursorpos < lineend) {
|
||
493 | func = CURSOR_RIGHT; |
||
494 | } |
||
495 | } else if (/* carriage return ('\r') */c == '\x0D' || |
||
496 | /* line feed ('\n') */ c == '\x0A') { |
||
497 | func = EXECUTE; |
||
498 | } else if (key == KEY_ESCAPE || |
||
499 | /* [ESCAPE] */ c == '\x1B') { |
||
500 | func = ESC_START; |
||
501 | } |
||
502 | } |
||
503 | |||
504 | /* handle function */
|
||
505 | switch (func) {
|
||
506 | case READ_CHAR:
|
||
507 | // line is full
|
||
508 | if (lineend + 1 >= shell->linesize) { |
||
509 | _moveCursor(shell, cursorpos, lineend); |
||
510 | chprintf(shell->stream, "\n\tmaximum line width reached\n");
|
||
511 | _printPrompt(shell); |
||
512 | _printLine(shell, 0, lineend);
|
||
513 | _moveCursor(shell, lineend, cursorpos); |
||
514 | } |
||
515 | // read character
|
||
516 | else {
|
||
517 | // clear old line content on first input
|
||
518 | if (noinput) {
|
||
519 | memset(shell->line, '\0', shell->linesize);
|
||
520 | noinput = false;
|
||
521 | } |
||
522 | // overwrite content
|
||
523 | if (shell->config & AOS_SHELL_CONFIG_INPUT_OVERWRITE) {
|
||
524 | shell->line[cursorpos] = c; |
||
525 | ++cursorpos; |
||
526 | lineend = (cursorpos > lineend) ? cursorpos : lineend; |
||
527 | streamPut(shell->stream, c); |
||
528 | } |
||
529 | // insert character
|
||
530 | else {
|
||
531 | memmove(&(shell->line[cursorpos+1]), &(shell->line[cursorpos]), lineend - cursorpos);
|
||
532 | shell->line[cursorpos] = c; |
||
533 | ++lineend; |
||
534 | _printLine(shell, cursorpos, lineend); |
||
535 | ++cursorpos; |
||
536 | _moveCursor(shell, lineend, cursorpos); |
||
537 | } |
||
538 | } |
||
539 | break;
|
||
540 | |||
541 | case AUTOFILL:
|
||
542 | { |
||
543 | const char* fill = shell->line; |
||
544 | size_t cmatch = cursorpos; |
||
545 | charmatch_t matchlevel = CHAR_MATCH_NOT; |
||
546 | size_t n; |
||
547 | // iterate through command list
|
||
548 | for (aos_shellcommand_t* cmd = shell->commands; cmd != NULL; cmd = cmd->next) { |
||
549 | // compare current match with command
|
||
550 | n = cmatch; |
||
551 | charmatch_t mlvl = CHAR_MATCH_NOT; |
||
552 | _strccmp(fill, cmd->name, shell->config & AOS_SHELL_CONFIG_MATCH_CASE, (n == 0) ? NULL : &n, &mlvl); |
||
553 | const int cmp = (n < cmatch) ? |
||
554 | (n - cmatch) : |
||
555 | (cmd->name[n] != '\0') ?
|
||
556 | strlen(cmd->name) - n : |
||
557 | 0;
|
||
558 | // if an exact match was found
|
||
559 | if (cmatch + cmp == cursorpos) {
|
||
560 | cmatch = cursorpos; |
||
561 | fill = cmd->name; |
||
562 | // break the loop only if there are no case mismatches with the input
|
||
563 | n = cursorpos; |
||
564 | _strccmp(fill, shell->line, false, &n, &mlvl);
|
||
565 | if (mlvl == CHAR_MATCH_CASE) {
|
||
566 | break;
|
||
567 | } |
||
568 | } |
||
569 | // if a not exact match was found
|
||
570 | else if (cmatch + cmp > cursorpos) { |
||
571 | // if this is the first one
|
||
572 | if (fill == shell->line) {
|
||
573 | cmatch += cmp; |
||
574 | fill = cmd->name; |
||
575 | } |
||
576 | // if this is a worse one
|
||
577 | else if ((cmp < 0) || (cmp == 0 && mlvl == CHAR_MATCH_CASE)) { |
||
578 | cmatch += cmp; |
||
579 | } |
||
580 | } |
||
581 | // non matching commands are ignored
|
||
582 | else {}
|
||
583 | } |
||
584 | // evaluate if there are case mismatches
|
||
585 | n = cmatch; |
||
586 | _strccmp(shell->line, fill, shell->config & AOS_SHELL_CONFIG_MATCH_CASE, &n, &matchlevel); |
||
587 | // print the auto fill if any
|
||
588 | if (cmatch > cursorpos || (cmatch == cursorpos && matchlevel == CHAR_MATCH_NCASE)) {
|
||
589 | noinput = false;
|
||
590 | // limit auto fill so it will not overflow the line width
|
||
591 | if (lineend + (cmatch - cursorpos) > shell->linesize) {
|
||
592 | cmatch = shell->linesize - lineend + cursorpos; |
||
593 | } |
||
594 | // move trailing memory further in the line
|
||
595 | memmove(&(shell->line[cmatch]), &(shell->line[cursorpos]), lineend - cursorpos); |
||
596 | lineend += cmatch - cursorpos; |
||
597 | // if there was no incorrect case when matching
|
||
598 | if (matchlevel == CHAR_MATCH_CASE) {
|
||
599 | // insert fill command name to line
|
||
600 | memcpy(&(shell->line[cursorpos]), &(fill[cursorpos]), cmatch - cursorpos); |
||
601 | // print the output
|
||
602 | _printLine(shell, cursorpos, lineend); |
||
603 | } else {
|
||
604 | // overwrite line with fill command name
|
||
605 | memcpy(shell->line, fill, cmatch); |
||
606 | // reprint the whole line
|
||
607 | _moveCursor(shell, cursorpos, 0);
|
||
608 | _printLine(shell, 0, lineend);
|
||
609 | } |
||
610 | // move cursor to the end of the matching sequence
|
||
611 | cursorpos = cmatch; |
||
612 | _moveCursor(shell, lineend, cursorpos); |
||
613 | } |
||
614 | break;
|
||
615 | } |
||
616 | |||
617 | case SUGGEST:
|
||
618 | { |
||
619 | unsigned int matches = 0; |
||
620 | // iterate through command list
|
||
621 | for (aos_shellcommand_t* cmd = shell->commands; cmd != NULL; cmd = cmd->next) { |
||
622 | // compare line content with command, excpet if cursorpos=0
|
||
623 | size_t i = cursorpos; |
||
624 | if (cursorpos > 0) { |
||
625 | _strccmp(shell->line, cmd->name, true, &i, NULL); |
||
626 | } |
||
627 | const int cmp = (i < cursorpos) ? |
||
628 | (i - cursorpos) : |
||
629 | (cmd->name[i] != '\0') ?
|
||
630 | strlen(cmd->name) - i : |
||
631 | 0;
|
||
632 | // if a match was found
|
||
633 | if (cmp > 0) { |
||
634 | // if this is the first one
|
||
635 | if (matches == 0) { |
||
636 | _moveCursor(shell, cursorpos, lineend); |
||
637 | streamPut(shell->stream, '\n');
|
||
638 | } |
||
639 | // print the command
|
||
640 | chprintf(shell->stream, "\t%s\n", cmd->name);
|
||
641 | ++matches; |
||
642 | } |
||
643 | } |
||
644 | // reprint the prompt and line if any matches have been found
|
||
645 | if (matches > 0) { |
||
646 | _printPrompt(shell); |
||
647 | _printLine(shell, 0, lineend);
|
||
648 | _moveCursor(shell, lineend, cursorpos); |
||
649 | noinput = false;
|
||
650 | } |
||
651 | break;
|
||
652 | } |
||
653 | |||
654 | case INS_TOGGLE:
|
||
655 | if (shell->config & AOS_SHELL_CONFIG_INPUT_OVERWRITE) {
|
||
656 | shell->config &= ~AOS_SHELL_CONFIG_INPUT_OVERWRITE; |
||
657 | } else {
|
||
658 | shell->config |= AOS_SHELL_CONFIG_INPUT_OVERWRITE; |
||
659 | } |
||
660 | break;
|
||
661 | |||
662 | case DELETE_FORWARD:
|
||
663 | --lineend; |
||
664 | memmove(&(shell->line[cursorpos]), &(shell->line[cursorpos+1]), lineend - cursorpos);
|
||
665 | _printLine(shell, cursorpos, lineend); |
||
666 | streamPut(shell->stream, ' ');
|
||
667 | _moveCursor(shell, lineend + 1, cursorpos);
|
||
668 | break;
|
||
669 | |||
670 | case DELETE_BACKWARD:
|
||
671 | --cursorpos; |
||
672 | memmove(&(shell->line[cursorpos]), &(shell->line[cursorpos+1]), lineend - cursorpos);
|
||
673 | --lineend; |
||
674 | shell->line[lineend] = '\0';
|
||
675 | _moveCursor(shell, cursorpos + 1, cursorpos);
|
||
676 | _printLine(shell, cursorpos, lineend); |
||
677 | streamPut(shell->stream, ' ');
|
||
678 | _moveCursor(shell, lineend+1, cursorpos);
|
||
679 | break;
|
||
680 | |||
681 | case RECALL_LAST:
|
||
682 | { |
||
683 | // replace any intermediate NUL bytes with spaces
|
||
684 | lineend = 0;
|
||
685 | size_t nul_start = 0;
|
||
686 | size_t nul_end = 0;
|
||
687 | // search line for a NUL byte
|
||
688 | while (nul_start < shell->linesize) {
|
||
689 | if (shell->line[nul_start] == '\0') { |
||
690 | nul_end = nul_start + 1;
|
||
691 | // keep searcjing for a byte that is not NUL
|
||
692 | while (nul_end < shell->linesize) {
|
||
693 | if (shell->line[nul_end] != '\0') { |
||
694 | // an intermediate NUL sequence was found
|
||
695 | memset(&(shell->line[nul_start]), ' ', nul_end - nul_start);
|
||
696 | lineend = nul_end + 1;
|
||
697 | break;
|
||
698 | } else {
|
||
699 | ++nul_end; |
||
700 | } |
||
701 | } |
||
702 | nul_start = nul_end + 1;
|
||
703 | } else {
|
||
704 | ++lineend; |
||
705 | ++nul_start; |
||
706 | } |
||
707 | } |
||
708 | cursorpos = lineend; |
||
709 | // print the line
|
||
710 | noinput = _printLine(shell, 0, lineend) == 0; |
||
711 | break;
|
||
712 | } |
||
713 | |||
714 | case CLEAR:
|
||
715 | // clear output
|
||
716 | _moveCursor(shell, cursorpos, 0);
|
||
717 | for (cursorpos = 0; cursorpos < lineend; ++cursorpos) { |
||
718 | streamPut(shell->stream, ' ');
|
||
719 | } |
||
720 | _moveCursor(shell, lineend, 0);
|
||
721 | cursorpos = 0;
|
||
722 | lineend = 0;
|
||
723 | noinput = true;
|
||
724 | break;
|
||
725 | |||
726 | case CURSOR2START:
|
||
727 | _moveCursor(shell, cursorpos, 0);
|
||
728 | cursorpos = 0;
|
||
729 | break;
|
||
730 | |||
731 | case CURSOR2END:
|
||
732 | _moveCursor(shell, cursorpos, lineend); |
||
733 | cursorpos = lineend; |
||
734 | break;
|
||
735 | |||
736 | case CURSOR_LEFT:
|
||
737 | _moveCursor(shell, cursorpos, cursorpos-1);
|
||
738 | --cursorpos; |
||
739 | break;
|
||
740 | |||
741 | case CURSOR_RIGHT:
|
||
742 | _moveCursor(shell, cursorpos, cursorpos+1);
|
||
743 | ++cursorpos; |
||
744 | break;
|
||
745 | |||
746 | case EXECUTE:
|
||
747 | streamPut(shell->stream, '\n');
|
||
748 | // return a warning if there was no input
|
||
749 | if (noinput) {
|
||
750 | return AOS_WARNING;
|
||
751 | } else {
|
||
752 | // fill the remainder of the line with NUL bytes
|
||
753 | memset(&(shell->line[lineend]), '\0', shell->linesize - lineend);
|
||
754 | return AOS_SUCCESS;
|
||
755 | } |
||
756 | break;
|
||
757 | |||
758 | case ESC_START:
|
||
759 | escseq[0] = c;
|
||
760 | ++escp; |
||
761 | break;
|
||
762 | |||
763 | case NONE:
|
||
764 | default:
|
||
765 | // do nothing (ignore input) and read next byte
|
||
766 | continue;
|
||
767 | break;
|
||
768 | } |
||
769 | |||
770 | lastfunc = func; |
||
771 | } /* end of while */
|
||
772 | |||
773 | /* This code is only executed when some error occurred.
|
||
774 | * The reason may be:
|
||
775 | * - The input stream was disabled (streamRead() returned 0)
|
||
776 | * - Parsing of input failed unexpectedly
|
||
777 | */
|
||
778 | return AOS_ERROR;
|
||
779 | } |
||
780 | |||
781 | /**
|
||
782 | * @brief Parses the content of the input buffer (line) to separate arguments.
|
||
783 | *
|
||
784 | * @param[in] shell Pointer to the shell object.
|
||
785 | *
|
||
786 | * @return Number of arguments found.
|
||
787 | */
|
||
788 | static size_t _parseArguments(aos_shell_t* shell)
|
||
789 | { |
||
790 | aosDbgCheck(shell != NULL);
|
||
791 | |||
792 | /*
|
||
793 | * States for a very small FSM.
|
||
794 | */
|
||
795 | typedef enum { |
||
796 | START, |
||
797 | SPACE, |
||
798 | TEXT, |
||
799 | END, |
||
800 | } state_t; |
||
801 | |||
802 | // local variables
|
||
803 | state_t state = START; |
||
804 | size_t arg = 0;
|
||
805 | |||
806 | // iterate through the line
|
||
807 | for (char* c = shell->line; c < shell->line + shell->linesize; ++c) { |
||
808 | // terminate at first NUL byte
|
||
809 | if (*c == '\0') { |
||
810 | state = END; |
||
811 | break;
|
||
812 | } |
||
813 | // spaces become NUL bytes
|
||
814 | else if (*c == ' ') { |
||
815 | *c = '\0';
|
||
816 | state = SPACE; |
||
817 | } |
||
818 | // handle non-NUL bytes
|
||
819 | else {
|
||
820 | switch (state) {
|
||
821 | case START:
|
||
822 | case SPACE:
|
||
823 | // ignore too many arguments
|
||
824 | if (arg < shell->arglistsize) {
|
||
825 | shell->arglist[arg] = c; |
||
826 | } |
||
827 | ++arg; |
||
828 | break;
|
||
829 | case TEXT:
|
||
830 | case END:
|
||
831 | default:
|
||
832 | break;
|
||
833 | } |
||
834 | state = TEXT; |
||
835 | } |
||
836 | } |
||
837 | |||
838 | // set all remaining argument pointers to NULL
|
||
839 | for (size_t a = arg; a < shell->arglistsize; ++a) {
|
||
840 | shell->arglist[a] = NULL;
|
||
841 | } |
||
842 | |||
843 | return arg;
|
||
844 | } |
||
845 | |||
846 | /**
|
||
847 | * @brief Initializes a shell object with the specified parameters.
|
||
848 | *
|
||
849 | * @param[in] shell Pointer to the shell object.
|
||
850 | * @param[in] stream I/O stream to use.
|
||
851 | * @param[in] prompt Prompt line to print (NULL = use default prompt).
|
||
852 | * @param[in] line Pointer to the input buffer.
|
||
853 | * @param[in] linesize Size of the input buffer.
|
||
854 | * @param[in] arglist Pointer to the argument buffer.
|
||
855 | * @param[in] arglistsize Size of te argument buffer.
|
||
856 | */
|
||
857 | void aosShellInit(aos_shell_t* shell, BaseSequentialStream* stream, const char* prompt, char* line, size_t linesize, char** arglist, size_t arglistsize) |
||
858 | { |
||
859 | aosDbgCheck(shell != NULL);
|
||
860 | aosDbgCheck(stream != NULL);
|
||
861 | aosDbgCheck(line != NULL);
|
||
862 | aosDbgCheck(arglist != NULL);
|
||
863 | |||
864 | // set parameters
|
||
865 | shell->thread = NULL;
|
||
866 | chEvtObjectInit(&shell->eventSource); |
||
867 | shell->stream = stream; |
||
868 | shell->prompt = prompt; |
||
869 | shell->commands = NULL;
|
||
870 | shell->execstatus.command = NULL;
|
||
871 | shell->execstatus.retval = 0;
|
||
872 | shell->line = line; |
||
873 | shell->linesize = linesize; |
||
874 | shell->arglist = arglist; |
||
875 | shell->arglistsize = arglistsize; |
||
876 | shell->config = 0x00;
|
||
877 | |||
878 | // initialize arrays
|
||
879 | memset(shell->line, '\0', shell->linesize);
|
||
880 | for (size_t a = 0; a < shell->arglistsize; ++a) { |
||
881 | shell->arglist[a] = NULL;
|
||
882 | } |
||
883 | |||
884 | return;
|
||
885 | } |
||
886 | |||
887 | /**
|
||
888 | * @brief Inserts a command to the shells list of commands.
|
||
889 | *
|
||
890 | * @param[in] shell Pointer to the shell object.
|
||
891 | * @param[in] cmd Pointer to the command to add.
|
||
892 | *
|
||
893 | * @return A status value.
|
||
894 | * @retval AOS_SUCCESS The command was added successfully.
|
||
895 | * @retval AOS_ERROR Another command with identical name already exists.
|
||
896 | */
|
||
897 | aos_status_t aosShellAddCommand(aos_shell_t *shell, aos_shellcommand_t *cmd) |
||
898 | { |
||
899 | aosDbgCheck(shell != NULL);
|
||
900 | aosDbgCheck(cmd != NULL);
|
||
901 | aosDbgCheck(cmd->name != NULL && strlen(cmd->name) > 0 && strchr(cmd->name, ' ') == NULL && strchr(cmd->name, '\t') == NULL); |
||
902 | aosDbgCheck(cmd->callback != NULL);
|
||
903 | aosDbgCheck(cmd->next == NULL);
|
||
904 | |||
905 | aos_shellcommand_t* prev = NULL;
|
||
906 | aos_shellcommand_t** curr = &(shell->commands); |
||
907 | |||
908 | // insert the command to the list wrt lexographical order (exception: lower case characters preceed upper their uppercase counterparts)
|
||
909 | while (1) { |
||
910 | // if the end of the list was reached, append the command
|
||
911 | if (*curr == NULL) { |
||
912 | *curr = cmd; |
||
913 | return AOS_SUCCESS;
|
||
914 | } else {
|
||
915 | // iterate through the list as long as the command names are 'smaller'
|
||
916 | const int cmp = _strccmp((*curr)->name, cmd->name, true, NULL, NULL); |
||
917 | if (cmp < 0) { |
||
918 | prev = *curr; |
||
919 | curr = &((*curr)->next); |
||
920 | continue;
|
||
921 | } |
||
922 | // error if the command already exists
|
||
923 | else if (cmp == 0) { |
||
924 | return AOS_ERROR;
|
||
925 | } |
||
926 | // insert the command as soon as a 'larger' name was found
|
||
927 | else /* if (cmpval > 0) */ { |
||
928 | cmd->next = *curr; |
||
929 | // special case: the first command is larger
|
||
930 | if (prev == NULL) { |
||
931 | shell->commands = cmd; |
||
932 | } else {
|
||
933 | prev->next = cmd; |
||
934 | } |
||
935 | return AOS_SUCCESS;
|
||
936 | } |
||
937 | } |
||
938 | } |
||
939 | } |
||
940 | |||
941 | /**
|
||
942 | * @brief Removes a command from the shells list of commands.
|
||
943 | *
|
||
944 | * @param[in] shell Pointer to the shell object.
|
||
945 | * @param[in] cmd Name of the command to removde.
|
||
946 | * @param[out] removed Optional pointer to the command that was removed.
|
||
947 | *
|
||
948 | * @return A status value.
|
||
949 | * @retval AOS_SUCCESS The command was removed successfully.
|
||
950 | * @retval AOS_ERROR The command name was not found.
|
||
951 | */
|
||
952 | aos_status_t aosShellRemoveCommand(aos_shell_t *shell, char *cmd, aos_shellcommand_t **removed)
|
||
953 | { |
||
954 | aosDbgCheck(shell != NULL);
|
||
955 | aosDbgCheck(cmd != NULL && strlen(cmd) > 0); |
||
956 | |||
957 | aos_shellcommand_t* prev = NULL;
|
||
958 | aos_shellcommand_t** curr = &(shell->commands); |
||
959 | |||
960 | // iterate through the list and seach for the specified command name
|
||
961 | while (curr != NULL) { |
||
962 | const int cmpval = strcmp((*curr)->name, cmd); |
||
963 | // iterate through the list as long as the command names are 'smaller'
|
||
964 | if (cmpval < 0) { |
||
965 | prev = *curr; |
||
966 | curr = &((*curr)->next); |
||
967 | continue;
|
||
968 | } |
||
969 | // remove the command when found
|
||
970 | else if (cmpval == 0) { |
||
971 | // special case: the first command matches
|
||
972 | if (prev == NULL) { |
||
973 | shell->commands = (*curr)->next; |
||
974 | } else {
|
||
975 | prev->next = (*curr)->next; |
||
976 | } |
||
977 | (*curr)->next = NULL;
|
||
978 | // set the optional output argument
|
||
979 | if (removed != NULL) { |
||
980 | *removed = *curr; |
||
981 | } |
||
982 | return AOS_SUCCESS;
|
||
983 | } |
||
984 | // break the loop if the command names are 'larger'
|
||
985 | else /* if (cmpval > 0) */ { |
||
986 | break;
|
||
987 | } |
||
988 | } |
||
989 | |||
990 | // if the command was not found, return an error
|
||
991 | return AOS_ERROR;
|
||
992 | } |
||
993 | |||
994 | /**
|
||
995 | * @brief Thread main function.
|
||
996 | *
|
||
997 | * @param[in] aosShellThread Name of the function;
|
||
998 | * @param[in] shell Pointer to the shell object.
|
||
999 | */
|
||
1000 | THD_FUNCTION(aosShellThread, shell) |
||
1001 | { |
||
1002 | aosDbgCheck(shell != NULL);
|
||
1003 | |||
1004 | // local variables
|
||
1005 | size_t nargs = 0;
|
||
1006 | aos_status_t readlval; |
||
1007 | |||
1008 | // fire start event
|
||
1009 | chEvtBroadcastFlags(&(((aos_shell_t*)shell)->eventSource), AOS_SHELL_EVTFLAG_START); |
||
1010 | |||
1011 | // enter thread loop
|
||
1012 | while (!chThdShouldTerminateX()) {
|
||
1013 | // print the prompt
|
||
1014 | _printPrompt((aos_shell_t*)shell); |
||
1015 | |||
1016 | // read input line
|
||
1017 | readlval = _readLine((aos_shell_t*)shell); |
||
1018 | if (readlval == AOS_ERROR) {
|
||
1019 | // emit an error event
|
||
1020 | chEvtBroadcastFlags(&(((aos_shell_t*)shell)->eventSource), AOS_SHELL_EVTFLAG_IOERROR); |
||
1021 | // erither break the loop or try again afte some time
|
||
1022 | if (chThdShouldTerminateX()) {
|
||
1023 | break;
|
||
1024 | } else {
|
||
1025 | aosThdSSleep(1);
|
||
1026 | } |
||
1027 | } |
||
1028 | |||
1029 | // parse input line to arguments only if reading the line was successful
|
||
1030 | nargs = (readlval == AOS_SUCCESS) ? _parseArguments((aos_shell_t*)shell) : 0;
|
||
1031 | if (nargs > ((aos_shell_t*)shell)->arglistsize) {
|
||
1032 | // error: too many arguments
|
||
1033 | chprintf(((aos_shell_t*)shell)->stream, "\tERROR: too many arguments\n");
|
||
1034 | continue;
|
||
1035 | } |
||
1036 | |||
1037 | // skip if there are no arguments
|
||
1038 | if (nargs > 0) { |
||
1039 | // search command list for arg[0] and execure callback function
|
||
1040 | aos_shellcommand_t* cmd = ((aos_shell_t*)shell)->commands; |
||
1041 | while (cmd != NULL) { |
||
1042 | if (strcmp(((aos_shell_t*)shell)->arglist[0], cmd->name) == 0) { |
||
1043 | ((aos_shell_t*)shell)->execstatus.command = cmd; |
||
1044 | chEvtBroadcastFlags(&(((aos_shell_t*)shell)->eventSource), AOS_SHELL_EVTFLAG_EXEC); |
||
1045 | ((aos_shell_t*)shell)->execstatus.retval = cmd->callback(((aos_shell_t*)shell)->stream, nargs, ((aos_shell_t*)shell)->arglist); |
||
1046 | chEvtBroadcastFlags(&(((aos_shell_t*)shell)->eventSource), AOS_SHELL_EVTFLAG_DONE); |
||
1047 | // append a line break so the next print will start from the very left
|
||
1048 | // usually this should just add an empty line, which is visually appealing
|
||
1049 | chprintf(((aos_shell_t*)shell)->stream, "\n");
|
||
1050 | // notify if the command was not successful
|
||
1051 | if (((aos_shell_t*)shell)->execstatus.retval != 0) { |
||
1052 | chprintf(((aos_shell_t*)shell)->stream, "command returned exit status %d\n", ((aos_shell_t*)shell)->execstatus.retval);
|
||
1053 | } |
||
1054 | break;
|
||
1055 | } |
||
1056 | cmd = cmd->next; |
||
1057 | } |
||
1058 | // if no matching command was found, print an error
|
||
1059 | if (cmd == NULL) { |
||
1060 | chprintf(((aos_shell_t*)shell)->stream, "%s: command not found\n", ((aos_shell_t*)shell)->arglist[0]); |
||
1061 | } |
||
1062 | } |
||
1063 | |||
1064 | } /* end of while loop */
|
||
1065 | |||
1066 | // fire event and exit the thread
|
||
1067 | chSysLock(); |
||
1068 | chEvtBroadcastFlagsI(&(((aos_shell_t*)shell)->eventSource), AOS_SHELL_EVTFLAG_EXIT); |
||
1069 | chThdExitS(MSG_OK); |
||
1070 | // no chSysUnlock() required since the thread has been terminated an all waiting threads have been woken up
|
||
1071 | } |