Shell: Add support for simple aliases

Aliases do not support chained commands with pipes, &&, ... but this is
a good start.
This commit is contained in:
Bananymous 2024-10-07 04:09:38 +03:00
parent c54d9b3f60
commit 1824988b9a
1 changed files with 204 additions and 75 deletions

View File

@ -1,4 +1,5 @@
#include <BAN/HashMap.h> #include <BAN/HashMap.h>
#include <BAN/HashSet.h>
#include <BAN/Optional.h> #include <BAN/Optional.h>
#include <BAN/Sort.h> #include <BAN/Sort.h>
#include <BAN/String.h> #include <BAN/String.h>
@ -60,6 +61,8 @@ struct BuiltinCommand
}; };
static BAN::HashMap<BAN::String, BuiltinCommand> s_builtin_commands; static BAN::HashMap<BAN::String, BuiltinCommand> s_builtin_commands;
static BAN::HashMap<BAN::String, BAN::Vector<BAN::String>> s_aliases;
static BAN::StringView strip_whitespace(BAN::StringView sv) static BAN::StringView strip_whitespace(BAN::StringView sv)
{ {
size_t leading = 0; size_t leading = 0;
@ -205,108 +208,179 @@ static BAN::Optional<BAN::String> parse_dollar(BAN::StringView command, size_t&
return temp; return temp;
} }
static PipedCommand parse_piped_command(BAN::StringView command_view) static SingleCommand parse_single_command(BAN::StringView command_view, bool parse_aliases = true)
{ {
enum class State constexpr auto can_escape =
{ [](char c)
Normal, {
SingleQuote, switch (c)
DoubleQuote, {
}; case 'e':
case 'n':
case 't':
case 'r':
case '"':
case '\'':
case ' ':
return true;
}
return false;
};
command_view = strip_whitespace(command_view); constexpr auto parse_escaped =
[](char c) -> char
{
switch (c)
{
case 'e': return '\e';
case 'n': return '\n';
case 't': return '\t';
case 'r': return '\r';
case '"': return '"';
case '\'': return '\'';
case ' ': return ' ';
}
ASSERT_NOT_REACHED();
};
State state = State::Normal; while (!command_view.empty() && isspace(command_view.front()))
SingleCommand current_command; command_view = command_view.substring(1);
while (!command_view.empty() && isspace(command_view.back()))
command_view = command_view.substring(0, command_view.size() - 1);
SingleCommand result;
BAN::String current_argument; BAN::String current_argument;
PipedCommand result;
for (size_t i = 0; i < command_view.size(); i++) for (size_t i = 0; i < command_view.size(); i++)
{ {
char c = command_view[i]; const char current = command_view[i];
if (i + 1 < command_view.size() && c == '\\') if (isspace(current))
{ {
char next = command_view[i + 1]; MUST(result.arguments.push_back(BAN::move(current_argument)));
if (next == '\'' || next == '"' || next == ' ') current_argument.clear();
{ while (i + 1 < command_view.size() && isspace(command_view[i + 1]))
if (i + 1 < command_view.size())
MUST(current_argument.push_back(next));
i++; i++;
continue; continue;
}
} }
switch (state) switch (current)
{ {
case State::Normal: case '\\':
if (c == '\'') if (i + 1 < command_view.size() && can_escape(command_view[i + 1]))
state = State::SingleQuote; MUST(current_argument.push_back(parse_escaped(command_view[++i])));
else if (c == '"') else
state = State::DoubleQuote; MUST(current_argument.push_back('\\'));
else if (c == '$') break;
case '$':
if (auto expansion = parse_dollar(command_view, i); expansion.has_value())
MUST(current_argument.append(expansion.release_value()));
else
{ {
auto expansion = parse_dollar(command_view, i); fprintf(stderr, "bad substitution\n");
if (!expansion.has_value()) return {};
{
fprintf(stderr, "bad substitution\n");
return {};
}
MUST(current_argument.append(expansion.value()));
} }
else if (c == '|') break;
{ case '~':
if (!current_argument.empty()) if (i == 0 || (isspace(command_view[i - 1]) && (i == 1 || command_view[i - 2] != '\\')))
MUST(current_command.arguments.push_back(current_argument));
current_argument.clear();
MUST(result.commands.push_back(current_command));
current_command.arguments.clear();
}
else if (c == '~' && (i == 0 || isspace(command_view[i - 1])))
{ {
const char* home_env = getenv("HOME"); const char* home_env = getenv("HOME");
if (home_env) if (home_env)
{
MUST(current_argument.append(home_env)); MUST(current_argument.append(home_env));
break;
}
} }
else if (!isspace(c)) MUST(current_argument.push_back('~'));
MUST(current_argument.push_back(c)); break;
else case '\'':
while (++i < command_view.size())
{ {
if (!current_argument.empty()) if (command_view[i] == current)
break;
if (command_view[i] == '\\' && i + 1 < command_view.size() && can_escape(command_view[i + 1]))
MUST(current_argument.push_back(parse_escaped(command_view[++i])));
else
MUST(current_argument.push_back(command_view[i]));
}
break;
case '"':
while (++i < command_view.size())
{
if (command_view[i] == current)
break;
if (command_view[i] == '\\' && i + 1 < command_view.size() && can_escape(command_view[i + 1]))
MUST(current_argument.push_back(parse_escaped(command_view[++i])));
else if (!(current == '"' && command_view[i] == '$'))
MUST(current_argument.push_back(command_view[i]));
else
{ {
MUST(current_command.arguments.push_back(current_argument)); if (auto expansion = parse_dollar(command_view, i); expansion.has_value())
current_argument.clear(); MUST(current_argument.append(expansion.release_value()));
else
{
fprintf(stderr, "bad substitution\n");
return {};
}
} }
} }
break; break;
case State::SingleQuote: default:
if (c == '\'') MUST(current_argument.push_back(command_view[i]));
state = State::Normal;
else
MUST(current_argument.push_back(c));
break;
case State::DoubleQuote:
if (c == '"')
state = State::Normal;
else if (c != '$')
MUST(current_argument.push_back(c));
else
{
auto expansion = parse_dollar(command_view, i);
if (!expansion.has_value())
{
fprintf(stderr, "bad substitution\n");
return {};
}
MUST(current_argument.append(expansion.value()));
}
break; break;
} }
} }
// FIXME: handle state != State::Normal MUST(result.arguments.push_back(BAN::move(current_argument)));
MUST(current_command.arguments.push_back(BAN::move(current_argument)));
MUST(result.commands.push_back(BAN::move(current_command))); if (parse_aliases)
{
BAN::HashSet<BAN::String> matched_aliases;
while (!result.arguments.empty() && !matched_aliases.contains(result.arguments.front()))
{
auto it = s_aliases.find(result.arguments.front());
if (it == s_aliases.end())
break;
MUST(matched_aliases.insert(result.arguments.front()));
result.arguments.remove(0);
for (size_t i = 0; i < it->value.size(); i++)
MUST(result.arguments.insert(i, it->value[i]));
}
}
return BAN::move(result);
}
static PipedCommand parse_piped_command(BAN::StringView command_view)
{
PipedCommand result;
for (size_t i = 0; i < command_view.size(); i++)
{
const char current = command_view[i];
switch (current)
{
case '\\':
i++;
break;
case '\'':
case '"':
while (++i < command_view.size())
{
if (command_view[i] == current)
break;
if (command_view[i] == '\\')
i++;
}
break;
case '|':
MUST(result.commands.emplace_back(parse_single_command(command_view.substring(0, i))));
command_view = command_view.substring(i + 1);
i = -1;
break;
}
}
MUST(result.commands.emplace_back(parse_single_command(command_view)));
return BAN::move(result); return BAN::move(result);
} }
@ -456,6 +530,56 @@ static void install_builtin_commands()
} }
)); ));
MUST(s_builtin_commands.emplace("alias"_sv,
[](const SingleCommand& command, FILE* fout, int, int) -> int
{
const auto print_alias =
[fout](const BAN::String& alias, const BAN::Vector<BAN::String>& value)
{
fprintf(fout, "%s='", alias.data());
for (size_t i = 0; i < value.size(); i++)
{
if (i != 0)
fprintf(fout, " ");
fprintf(fout, "%s", value[i].data());
}
fprintf(fout, "'\n");
};
if (command.arguments.size() == 1)
{
for (const auto& [alias, value] : s_aliases)
print_alias(alias, value);
return 0;
}
for (size_t i = 1; i < command.arguments.size(); i++)
{
auto idx = command.arguments[i].sv().find('=');
if (idx.has_value() && idx.value() == 0)
continue;
if (!idx.has_value())
{
auto it = s_aliases.find(command.arguments[i]);
if (it != s_aliases.end())
print_alias(command.arguments[i], it->value);
}
else
{
auto alias = command.arguments[i].sv().substring(0, idx.value());
auto value = command.arguments[i].sv().substring(idx.value() + 1);
auto parsed_alias = parse_single_command(value, false);
if (s_aliases.contains(alias))
s_aliases.remove(alias);
MUST(s_aliases.insert(alias, BAN::move(parsed_alias.arguments)));
}
}
return 0;
}
));
MUST(s_builtin_commands.emplace("source"_sv, MUST(s_builtin_commands.emplace("source"_sv,
[](const SingleCommand& command, FILE* fout, int, int) -> int [](const SingleCommand& command, FILE* fout, int, int) -> int
{ {
@ -961,7 +1085,12 @@ static TabCompletion list_tab_completion_entries(BAN::StringView command)
MUST(result.emplace_back(builtin_name.sv().substring(last_argument.size()))); MUST(result.emplace_back(builtin_name.sv().substring(last_argument.size())));
} }
// TODO: match aliases when added for (const auto& [alias_name, _] : s_aliases)
{
if (!alias_name.sv().starts_with(last_argument))
continue;
MUST(result.emplace_back(alias_name.sv().substring(last_argument.size())));
}
break; break;
} }