Public Access
Shell: Tab completes every word of a command, and * and ? stand for several files (#67)
Tab used to complete a command's first word only. It now follows the help text word by word: `lora st` gives `lora status`, `gnss track ` lists `start stop`. The words are read from the help text as written, so a new command completes with no table to keep; the Shell's own words are added in the same notation. `*` and `?` in the last part of a path, for ls, du, rm, cp and mv, from the Shell and both consoles. The command runs once for each name matched, lined up and run by the main loop as each finishes; `cancel` empties the line-up. 64 matches at most, refused whole past that. In the Shell, rm with a pattern asks once, with the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -128,6 +128,43 @@ void test_rm_takes_its_switches_like_unix() {
|
||||
TEST_ASSERT_EQUAL_STRING("", parseRm("-rf").path.c_str());
|
||||
}
|
||||
|
||||
void test_a_pattern_matches_names() {
|
||||
TEST_ASSERT_TRUE(globMatch("*.txt", "notes.txt"));
|
||||
TEST_ASSERT_TRUE(globMatch("*.TXT", "notes.txt")); // the card doesn't tell cases apart
|
||||
TEST_ASSERT_FALSE(globMatch("*.txt", "notes.txt.bak"));
|
||||
TEST_ASSERT_TRUE(globMatch("*", "anything"));
|
||||
TEST_ASSERT_TRUE(globMatch("*", ""));
|
||||
TEST_ASSERT_TRUE(globMatch("a?c", "abc"));
|
||||
TEST_ASSERT_FALSE(globMatch("a?c", "ac"));
|
||||
TEST_ASSERT_FALSE(globMatch("a?c", "abbc"));
|
||||
TEST_ASSERT_TRUE(globMatch("2026-10-0?.log", "2026-10-07.log"));
|
||||
TEST_ASSERT_TRUE(globMatch("a*b*c", "a-then-b-then-c"));
|
||||
TEST_ASSERT_TRUE(globMatch("a*b*c", "abbbc"));
|
||||
TEST_ASSERT_FALSE(globMatch("a*b*c", "a-then-c"));
|
||||
TEST_ASSERT_TRUE(globMatch("*a", "banana")); // the star has to give back and try again
|
||||
TEST_ASSERT_FALSE(globMatch("*ab", "banana"));
|
||||
TEST_ASSERT_TRUE(globMatch("file-000?.txt", "file-0001.txt"));
|
||||
TEST_ASSERT_TRUE(globMatch("exact", "EXACT"));
|
||||
TEST_ASSERT_FALSE(globMatch("", "x"));
|
||||
TEST_ASSERT_TRUE(globMatch("**", "x"));
|
||||
}
|
||||
|
||||
void test_a_path_with_a_pattern_is_taken_apart() {
|
||||
std::string folder, pattern;
|
||||
TEST_ASSERT_TRUE(splitGlob("/notes/*.txt", folder, pattern));
|
||||
TEST_ASSERT_EQUAL_STRING("/notes", folder.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("*.txt", pattern.c_str());
|
||||
TEST_ASSERT_TRUE(splitGlob("/*", folder, pattern));
|
||||
TEST_ASSERT_EQUAL_STRING("/", folder.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("*", pattern.c_str());
|
||||
TEST_ASSERT_FALSE(splitGlob("/notes/a.txt", folder, pattern)); // no pattern
|
||||
TEST_ASSERT_FALSE(splitGlob("/*/a.txt", folder, pattern)); // not in a folder's name
|
||||
TEST_ASSERT_FALSE(splitGlob("/n*/a?.txt", folder, pattern));
|
||||
TEST_ASSERT_FALSE(splitGlob("*.txt", folder, pattern)); // a path starts at the top
|
||||
TEST_ASSERT_TRUE(hasGlob("a?"));
|
||||
TEST_ASSERT_FALSE(hasGlob("/plain/path"));
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_path_parts);
|
||||
@@ -139,5 +176,7 @@ int main() {
|
||||
RUN_TEST(test_kinds);
|
||||
RUN_TEST(test_looks_like_text);
|
||||
RUN_TEST(test_rm_takes_its_switches_like_unix);
|
||||
RUN_TEST(test_a_pattern_matches_names);
|
||||
RUN_TEST(test_a_path_with_a_pattern_is_taken_apart);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
@@ -46,35 +46,76 @@ void test_the_oldest_lines_go_past_4_kb() {
|
||||
|
||||
static const char* kHelp =
|
||||
"info firmware, uptime, memory\n"
|
||||
"ls [folder] | du <path> | mkdir <path> | rm <path> the SD card\n"
|
||||
"lora probe | lora status | lora rx on|off the LoRa radio\n"
|
||||
"ls [folder] | du <path> | mkdir <path> | rm [-r] [-f] <path> the SD card\n"
|
||||
"lora probe | lora status | lora rx on|off | lora preset <name> the LoRa radio\n"
|
||||
"lora sweep on [from MHz] [to MHz] | lora sweep off | lora sweep dump RSSI across a band\n"
|
||||
"gnss track start|stop | gnss nmea on|off | gnss send <sentence without $ and checksum>\n"
|
||||
"log level <0-5> ESP-IDF log level\n"
|
||||
"wifi ip <ssid> dhcp | wifi ip ... try <seconds> | wifi ip keep a Saved Network's IP setting\n"
|
||||
"sd card | sd list | cat <path> | log <text> | burst\n"
|
||||
"debug on | debug token <16 to 64 characters> | debug token new (USB serial only)\n"
|
||||
"get <path> | put <path> <size> <sha256> | screenshot (Debug Console only) binary\n"
|
||||
"Irc | Notes | Storage open that App\n";
|
||||
|
||||
void test_command_words_come_from_the_help_text() {
|
||||
auto words = commandWords(kHelp);
|
||||
const char* expected[] = {"info", "ls", "du", "mkdir", "rm", "lora", "log", "sd", "cat", "burst", "get", "put", "screenshot", "Irc", "Notes", "Storage"};
|
||||
TEST_ASSERT_EQUAL(sizeof expected / sizeof expected[0], words.size());
|
||||
for (size_t i = 0; i < words.size(); i++) TEST_ASSERT_EQUAL_STRING(expected[i], words[i].c_str());
|
||||
static std::string tab(const std::string& typed, std::vector<std::string>* candidates = nullptr) {
|
||||
std::vector<std::string> matches;
|
||||
std::string out = completeWords(typed, kHelp, matches);
|
||||
if (candidates) *candidates = matches;
|
||||
return out;
|
||||
}
|
||||
|
||||
void test_tab_completes_the_first_word() {
|
||||
auto words = commandWords(kHelp);
|
||||
std::vector<std::string> matches;
|
||||
TEST_ASSERT_EQUAL_STRING("info ", completeCommand("in", words, matches).c_str()); // the only one: with its space
|
||||
TEST_ASSERT_EQUAL(0, matches.size());
|
||||
TEST_ASSERT_EQUAL_STRING("l", completeCommand("l", words, matches).c_str()); // ls, lora, log: they agree on no more
|
||||
TEST_ASSERT_EQUAL(3, matches.size());
|
||||
TEST_ASSERT_EQUAL_STRING("mkdir ", completeCommand("m", words, matches).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("lo", completeCommand("lo", words, matches).c_str()); // lora, log
|
||||
TEST_ASSERT_EQUAL(2, matches.size());
|
||||
TEST_ASSERT_EQUAL_STRING("zz", completeCommand("zz", words, matches).c_str()); // nothing: as it was
|
||||
TEST_ASSERT_EQUAL_STRING("ls /no", completeCommand("ls /no", words, matches).c_str()); // past the first word
|
||||
TEST_ASSERT_EQUAL_STRING("", completeCommand("", words, matches).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("Notes ", completeCommand("N", words, matches).c_str()); // an App: a capital
|
||||
TEST_ASSERT_EQUAL_STRING("n", completeCommand("n", words, matches).c_str()); // and no command starts with n here
|
||||
std::vector<std::string> m;
|
||||
TEST_ASSERT_EQUAL_STRING("info ", tab("in", &m).c_str()); // the only one: with its space
|
||||
TEST_ASSERT_EQUAL(0, m.size());
|
||||
TEST_ASSERT_EQUAL_STRING("l", tab("l", &m).c_str()); // ls, lora, log: they agree on no more
|
||||
TEST_ASSERT_EQUAL(3, m.size());
|
||||
TEST_ASSERT_EQUAL_STRING("lo", tab("lo", &m).c_str()); // lora, log
|
||||
TEST_ASSERT_EQUAL(2, m.size());
|
||||
TEST_ASSERT_EQUAL_STRING("mkdir ", tab("m").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("zz", tab("zz", &m).c_str()); // nothing: as it was
|
||||
TEST_ASSERT_EQUAL(0, m.size());
|
||||
TEST_ASSERT_EQUAL_STRING("", tab("").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("Notes ", tab("N").c_str()); // an App: a capital
|
||||
TEST_ASSERT_EQUAL_STRING("n", tab("n").c_str()); // and no command starts with n here
|
||||
TEST_ASSERT_EQUAL_STRING("screenshot ", tab("sc").c_str());
|
||||
}
|
||||
|
||||
// Every word of a command, not only the first.
|
||||
void test_tab_completes_the_words_after_the_first() {
|
||||
std::vector<std::string> m;
|
||||
TEST_ASSERT_EQUAL_STRING("lora status ", tab("lora st").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("lora s", tab("lora s", &m).c_str()); // status, sweep
|
||||
TEST_ASSERT_EQUAL(2, m.size());
|
||||
TEST_ASSERT_EQUAL_STRING("lora ", tab("lora ", &m).c_str()); // everything lora can be followed by
|
||||
const char* afterLora[] = {"probe", "status", "rx", "preset", "sweep"};
|
||||
TEST_ASSERT_EQUAL(5, m.size());
|
||||
for (size_t i = 0; i < m.size(); i++) TEST_ASSERT_EQUAL_STRING(afterLora[i], m[i].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("lora rx o", tab("lora rx o", &m).c_str()); // on|off: either
|
||||
TEST_ASSERT_EQUAL(2, m.size());
|
||||
TEST_ASSERT_EQUAL_STRING("lora rx off ", tab("lora rx of").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("lora sweep dump ", tab("lora sweep d").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("gnss track st", tab("gnss track st", &m).c_str()); // start, stop
|
||||
TEST_ASSERT_EQUAL(2, m.size());
|
||||
TEST_ASSERT_EQUAL_STRING("gnss track stop ", tab("gnss track sto").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("log level ", tab("log l").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("wifi ip keep ", tab("wifi ip k").c_str()); // past the <ssid> of its neighbours
|
||||
TEST_ASSERT_EQUAL_STRING("debug token new ", tab("debug token n").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("sd ", tab("sd ", &m).c_str());
|
||||
TEST_ASSERT_EQUAL(2, m.size()); // card, list
|
||||
}
|
||||
|
||||
// Where a command's words end, Tab has nothing to say: the Shell then tries a path.
|
||||
void test_tab_stops_where_the_arguments_start() {
|
||||
std::vector<std::string> m;
|
||||
TEST_ASSERT_EQUAL_STRING("ls /no", tab("ls /no", &m).c_str());
|
||||
TEST_ASSERT_EQUAL(0, m.size());
|
||||
TEST_ASSERT_EQUAL_STRING("rm ", tab("rm ", &m).c_str());
|
||||
TEST_ASSERT_EQUAL(0, m.size());
|
||||
TEST_ASSERT_EQUAL_STRING("lora preset Lo", tab("lora preset Lo").c_str()); // a <name>: not a word of the command
|
||||
TEST_ASSERT_EQUAL_STRING("lora status x", tab("lora status x").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("nope st", tab("nope st").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("lora st", tab("lora st").c_str()); // two spaces
|
||||
}
|
||||
|
||||
void test_a_later_word_is_taken_apart_as_a_path() {
|
||||
@@ -133,8 +174,9 @@ int main(int, char**) {
|
||||
RUN_TEST(test_lines_arrive_in_pieces);
|
||||
RUN_TEST(test_the_heap_line_is_never_kept);
|
||||
RUN_TEST(test_the_oldest_lines_go_past_4_kb);
|
||||
RUN_TEST(test_command_words_come_from_the_help_text);
|
||||
RUN_TEST(test_tab_completes_the_first_word);
|
||||
RUN_TEST(test_tab_completes_the_words_after_the_first);
|
||||
RUN_TEST(test_tab_stops_where_the_arguments_start);
|
||||
RUN_TEST(test_a_later_word_is_taken_apart_as_a_path);
|
||||
RUN_TEST(test_tab_completes_a_path);
|
||||
return UNITY_END();
|
||||
|
||||
Reference in New Issue
Block a user