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:
@@ -36,56 +36,79 @@ void ShellLog::clear() {
|
||||
revision_++;
|
||||
}
|
||||
|
||||
std::vector<std::string> commandWords(const char* helpText) {
|
||||
std::vector<std::string> words;
|
||||
std::string text = helpText ? helpText : "";
|
||||
size_t lineStart = 0;
|
||||
while (lineStart < text.size()) {
|
||||
size_t lineEnd = text.find('\n', lineStart);
|
||||
if (lineEnd == std::string::npos) lineEnd = text.size();
|
||||
std::string line = text.substr(lineStart, lineEnd - lineStart);
|
||||
// The commands stop where the description starts: three spaces.
|
||||
size_t gap = line.find(" ");
|
||||
std::string commands = line.substr(0, gap);
|
||||
size_t at = 0;
|
||||
while (at <= commands.size()) {
|
||||
size_t bar = commands.find(" | ", at);
|
||||
std::string one = commands.substr(at, bar == std::string::npos ? std::string::npos : bar - at);
|
||||
size_t start = one.find_first_not_of(' ');
|
||||
if (start != std::string::npos) {
|
||||
size_t end = one.find(' ', start);
|
||||
std::string word = one.substr(start, end == std::string::npos ? std::string::npos : end - start);
|
||||
// Small letters, and for an App's name a capital first.
|
||||
bool plain = !word.empty() && std::all_of(word.begin() + 1, word.end(), [](char ch) { return ch >= 'a' && ch <= 'z'; }) &&
|
||||
((word[0] >= 'a' && word[0] <= 'z') || (word[0] >= 'A' && word[0] <= 'Z'));
|
||||
if (plain && std::find(words.begin(), words.end(), word) == words.end()) words.push_back(word);
|
||||
}
|
||||
if (bar == std::string::npos) break;
|
||||
at = bar + 3;
|
||||
}
|
||||
lineStart = lineEnd + 1;
|
||||
namespace {
|
||||
|
||||
bool isWord(const std::string& w) {
|
||||
if (w.empty()) return false;
|
||||
for (size_t i = 0; i < w.size(); i++) {
|
||||
bool small = w[i] >= 'a' && w[i] <= 'z', capital = w[i] >= 'A' && w[i] <= 'Z';
|
||||
if (!(small || (i == 0 && capital))) return false;
|
||||
}
|
||||
return words;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string completeCommand(const std::string& typed, const std::vector<std::string>& words, std::vector<std::string>& matches) {
|
||||
matches.clear();
|
||||
if (typed.empty() || typed.find(' ') != std::string::npos) return typed;
|
||||
for (auto& w : words)
|
||||
if (w.rfind(typed, 0) == 0) matches.push_back(w);
|
||||
if (matches.empty()) return typed;
|
||||
if (matches.size() == 1) {
|
||||
std::string only = matches[0] + " ";
|
||||
matches.clear();
|
||||
return only;
|
||||
std::vector<std::string> split(const std::string& text, const std::string& by) {
|
||||
std::vector<std::string> out;
|
||||
size_t at = 0;
|
||||
for (;;) {
|
||||
size_t next = text.find(by, at);
|
||||
out.push_back(text.substr(at, next == std::string::npos ? std::string::npos : next - at));
|
||||
if (next == std::string::npos) return out;
|
||||
at = next + by.size();
|
||||
}
|
||||
}
|
||||
|
||||
// The words a command can have at `index`, given the `index` words before it: added to `out`.
|
||||
void nextWords(const std::string& command, const std::vector<std::string>& before, size_t index, std::vector<std::string>& out) {
|
||||
std::vector<std::string> tokens;
|
||||
for (auto& t : split(command, " "))
|
||||
if (!t.empty()) tokens.push_back(t);
|
||||
for (size_t i = 0; i < tokens.size(); i++) {
|
||||
std::vector<std::string> either = split(tokens[i], "|"); // on|off: either of them
|
||||
bool words = true;
|
||||
for (auto& w : either) words = words && isWord(w);
|
||||
if (!words) return; // an <argument>, an [option], "...": the command's words end here
|
||||
if (i == index) {
|
||||
for (auto& w : either)
|
||||
if (std::find(out.begin(), out.end(), w) == out.end()) out.push_back(w);
|
||||
return;
|
||||
}
|
||||
if (std::find(either.begin(), either.end(), before[i]) == either.end()) return; // another command
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string completeWords(const std::string& typed, const char* helpText, std::vector<std::string>& matches) {
|
||||
matches.clear();
|
||||
std::vector<std::string> words = split(typed, " ");
|
||||
for (size_t i = 0; i + 1 < words.size(); i++)
|
||||
if (words[i].empty()) return typed; // two spaces: not ours to guess
|
||||
const std::string last = words.back();
|
||||
words.pop_back();
|
||||
if (words.empty() && last.empty()) return typed;
|
||||
|
||||
std::vector<std::string> next;
|
||||
for (auto& line : split(helpText ? helpText : "", "\n")) {
|
||||
std::string commands = line.substr(0, line.find(" ")); // the description starts at three spaces
|
||||
for (auto& command : split(commands, " | ")) nextWords(command, words, words.size(), next);
|
||||
}
|
||||
for (auto& w : next)
|
||||
if (w.rfind(last, 0) == 0) matches.push_back(w);
|
||||
if (matches.empty()) return typed;
|
||||
|
||||
std::string common = matches[0];
|
||||
for (auto& m : matches) {
|
||||
size_t n = 0;
|
||||
while (n < common.size() && n < m.size() && common[n] == m[n]) n++;
|
||||
common.resize(n);
|
||||
}
|
||||
return common;
|
||||
std::string head = typed.substr(0, typed.size() - last.size());
|
||||
if (matches.size() == 1) {
|
||||
matches.clear();
|
||||
return head + common + " ";
|
||||
}
|
||||
return head + common;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
Reference in New Issue
Block a user