Shell: Tab completes every word of a command, and * and ? stand for several files (#67)
CI / build (pull_request) Successful in 1m38s
Site / build (pull_request) Successful in 9s

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:
2026-10-07 12:47:10 +02:00
co-authored by Claude Opus 5.5
parent d17d10948d
commit 0fdbb5b1ed
16 changed files with 368 additions and 112 deletions
+29
View File
@@ -115,4 +115,33 @@ RmArgs parseRm(const std::string& args) {
return out;
}
bool hasGlob(const std::string& text) { return text.find_first_of("*?") != std::string::npos; }
bool globMatch(const std::string& pattern, const std::string& name) {
auto lower = [](char c) { return c >= 'A' && c <= 'Z' ? static_cast<char>(c - 'A' + 'a') : c; };
size_t p = 0, n = 0, star = std::string::npos, mark = 0;
while (n < name.size()) {
if (p < pattern.size() && (pattern[p] == '?' || lower(pattern[p]) == lower(name[n]))) {
p++;
n++;
} else if (p < pattern.size() && pattern[p] == '*') {
star = p++; // try it as nothing first; come back here to let it take one more
mark = n;
} else if (star != std::string::npos) {
p = star + 1;
n = ++mark;
} else return false;
}
while (p < pattern.size() && pattern[p] == '*') p++;
return p == pattern.size();
}
bool splitGlob(const std::string& path, std::string& folder, std::string& pattern) {
size_t slash = path.rfind('/');
if (slash == std::string::npos) return false;
pattern = path.substr(slash + 1);
folder = slash == 0 ? "/" : path.substr(0, slash);
return hasGlob(pattern) && !hasGlob(folder);
}
} // namespace roro::files
+8
View File
@@ -47,4 +47,12 @@ struct RmArgs {
};
RmArgs parseRm(const std::string& args);
// Patterns in a path (issue #67): * for any run of characters, ? for one, in the last part of the
// path only (/notes/*.txt, not /*/a.txt). Cases aren't told apart, as on the card.
bool hasGlob(const std::string& text);
bool globMatch(const std::string& pattern, const std::string& name);
// "/notes/*.txt" taken apart: the folder ("/notes", or "/" at the top) and the pattern ("*.txt").
// False if there's no pattern in it, or if the folder has one too.
bool splitGlob(const std::string& path, std::string& folder, std::string& pattern);
} // namespace roro::files