fxos/shell/main.cpp

447 lines
13 KiB
C++

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <csignal>
#include <csetjmp>
#include <functional>
#include <getopt.h>
#include <readline/readline.h>
#include <readline/history.h>
#include <fmt/core.h>
#include <fmt/color.h>
#include "session.h"
#include "shell.h"
#include "theme.h"
#include "parser.h"
#include "commands.h"
#include <fxos/util/log.h>
#include <fxos/semantics.h>
static std::map<std::string, ShellCommand *> commands;
ShellCommand::ShellCommand(std::string _name, ShellFunction _function,
ShellCompleter _completer, std::string _shortd, std::string _longd):
function {_function},
completer {_completer}, short_description {_shortd}
{
/* Left trim */
_longd.erase(0, _longd.find_first_not_of("\n"));
/* Right trim */
int r = _longd.size();
while(r > 0 && _longd[--r] == '\n') {}
_longd.erase(r + 1, _longd.size());
this->long_description = _longd;
commands[_name] = this;
}
static Session global_session;
//---
// Autocompletion
//---
std::vector<std::string> complete_command(char const *text)
{
std::vector<std::string> options;
for(auto const &it: commands) {
if(!strncmp(it.first.c_str(), text, strlen(text)))
options.push_back(it.first);
}
return options;
}
std::vector<std::string> complete_vspace(char const *text, Session &session)
{
std::vector<std::string> options;
for(auto const &it: session.spaces) {
if(!strncmp(it.first.c_str(), text, strlen(text)))
options.push_back(it.first);
}
return options;
}
std::vector<std::string> complete_region(char const *text)
{
std::vector<std::string> options;
for(auto const &it: MemoryRegion::all()) {
if(!strncmp(it->name.c_str(), text, strlen(text)))
options.push_back(it->name);
}
return options;
}
std::vector<std::string> complete_symbol(char const *text, VirtualSpace *space)
{
std::vector<std::string> options;
for(auto const &it: space->symbols.symbols) {
if(!strncmp(it.name.c_str(), text, strlen(text)))
options.push_back(it.name);
}
return options;
}
Parser::CompletionRequest parse_until_autocomplete(
Session &session, char const *line_buffer, int point)
{
/* Parse partial input and try to get a category of suggestions */
std::string input(line_buffer, point);
lex_repl(input);
Parser p(true);
p.start();
/* Read commands until the parser runs out of input and throws a
CompletionRequest */
try {
do {
p.skip_separators();
std::string cmd = p.symbol("command");
if(commands.count(cmd) && commands[cmd]->completer)
commands[cmd]->completer(session, p);
}
while(!lex_idle());
}
catch(Parser::CompletionRequest r) {
return r;
}
catch(Parser::SyntaxError &e) {
/* Ignore syntax errors, just don't autocomplete */
}
return Parser::CompletionRequest("", "");
}
char *autocomplete(char const *text, int state)
{
static std::vector<std::string> options;
static size_t i = 0;
if(state == 0) {
Parser::CompletionRequest const &r = parse_until_autocomplete(
global_session, rl_line_buffer, rl_point);
if(r.category == "command")
options = complete_command(text);
else if(r.category == "vspace_name")
options = complete_vspace(text, global_session);
else if(r.category == "memory_region")
options = complete_region(text);
else if(r.category == "symbol" && r.space != nullptr)
options = complete_symbol(text, r.space);
else if(r.category == "_error") {
options.clear();
options.push_back("(ERROR) " + r.value);
options.push_back(".");
}
else
options.clear();
i = 0;
}
return (i < options.size()) ? strdup(options[i++].c_str()) : NULL;
}
//---
// Shell routine
//---
static std::string read_interactive(Session const &s, bool &leave)
{
std::string prompt = "no_vspace> ";
if(s.current_space) {
std::string name = "(none)";
for(auto &it: s.spaces) {
if(it.second.get() == s.current_space)
name = it.first;
}
prompt = fmt::format("{} @ 0x{:08x}> ", name, s.current_space->cursor);
}
/* We need to insert RL_PROMPT_{START,END}_IGNORE into the color
formatting, so we trick a little bit by using a space */
std::string color = fmt::format(theme(9), " ");
int space_pos = color.find(' ');
std::string SC = color.substr(0, space_pos);
std::string EC = color.substr(space_pos + 1);
std::string SI(1, RL_PROMPT_START_IGNORE);
std::string EI(1, RL_PROMPT_END_IGNORE);
prompt = SI + SC + EI + prompt + SI + EC + EI;
/* Get a command to execute */
char *cmd_ptr = readline(prompt.c_str());
if(!cmd_ptr) {
leave = true;
return "";
}
std::string cmd = cmd_ptr;
if(strlen(cmd_ptr) > 0)
add_history(cmd_ptr);
free(cmd_ptr);
return cmd;
}
int norc = 0;
std::string execute_arg = "";
char *extra_rom = NULL;
int log_level = FxOS::LOG_LEVEL_WRN;
char const *help_message =
R"(Usage: fxos [OPTION]... [FILE]
Open the fxos disassembler, optionally with [FILE] mapped into a new empty
space at ROM and ROM_P2.
Options:
-e, --execute=CMD execute CMD and exit, can be used multiple times
--norc do not execute any fxosrc files
-l, --log=LEVEL set log level (debug, warning, error; default warning)
-h, --help display this help and exit
)";
static void parse_options(int argc, char **argv)
{
/* List of options */
static struct option long_options[] = {{"help", no_argument, 0, 'h'},
{"execute", required_argument, 0, 'e'}, {"norc", no_argument, &norc, 1},
{"log", required_argument, 0, 'l'},
/* This is here as a fallback if nothing is passed */
{0, 0, 0, 0}};
bool show_help = false;
int opt, option_index;
while(1) {
/* Get the next command-line option */
opt = getopt_long(argc, argv, "he:l:", long_options, &option_index);
if(opt == -1)
break;
/* Handle options */
switch(opt) {
case 0:
/* This happens if we store the argument to a variable (e.g. --norc) */
break;
case 'h':
show_help = true;
break;
case 'e':
execute_arg += ";";
execute_arg += optarg;
break;
case 'l':
if(!strcmp(optarg, "debug"))
log_level = FxOS::LOG_LEVEL_LOG;
else if(!strcmp(optarg, "warning"))
log_level = FxOS::LOG_LEVEL_LOG;
else if(!strcmp(optarg, "error"))
log_level = FxOS::LOG_LEVEL_ERR;
else {
FxOS_log(ERR, "invalid log level '%s'", optarg);
printf("Try %s --help for more information.\n", argv[0]);
exit(1);
}
break;
case '?':
printf("Try %s --help for more information.\n", argv[0]);
exit(1);
default:
printf("Try %s --help for more information.\n", argv[0]);
exit(1);
}
}
/* Print help message if --help is used at least once */
if(show_help) {
printf("%s", help_message);
exit(0);
}
/* Handle positional arguments */
if(optind < argc) {
if((argc - optind) > 1) {
printf("%s only supports 1 positional argument, FILE.\n", argv[0]);
printf("Try %s --help for more information.\n", argv[0]);
exit(1);
}
extra_rom = argv[optind++];
}
}
int main(int argc, char **argv)
{
/* Parse command-line options first */
parse_options(argc, argv);
FxOS::log_setminlevel(log_level);
Session &s = global_session;
theme_builtin("tomorrow-night");
rl_completion_entry_function = autocomplete;
rl_basic_word_break_characters
= " \t\n\"\\'`@$><=;|&{(" /* Readline's default */
"+-*%)"; /* Word breaks with special characters in fxos */
/* Load path into the session */
char const *fxos_path_env = std::getenv("FXOS_PATH");
if(fxos_path_env) {
std::string fxos_path = fxos_path_env;
size_t offset = 0, end;
while(true) {
offset = fxos_path.find_first_not_of(":", offset);
if(offset >= fxos_path.size())
break;
end = fxos_path.find_first_of(":", offset);
s.path.push_back(fxos_path.substr(offset, end - offset));
offset = end;
}
}
else {
fmt::print("warning: no FXOS_PATH in environment, using WD\n");
s.path.push_back(fs::current_path());
}
/* Clear readline input when receiving SIGINT */
static sigjmp_buf sigint_buf;
std::signal(SIGINT, [](int) {
rl_free_line_state();
rl_cleanup_after_signal();
rl_line_buffer[rl_point = rl_end = rl_mark = 0] = 0;
printf("\n");
siglongjmp(sigint_buf, 1);
});
rl_set_signals();
/* Load command history */
char histfile[128] = "";
if(std::getenv("HOME"))
snprintf(histfile, 128, "%s/.fxos_history", std::getenv("HOME"));
if(histfile[0])
read_history(histfile);
/* Load fxosrc files from all library folders if wanted */
if(!norc) {
std::vector<std::string> fxosrc_files;
for(auto const &path: s.path) {
if(fs::exists(path / "fxosrc"))
fxosrc_files.push_back(path / "fxosrc");
}
if(fxosrc_files.size() > 0)
_dot(s, fxosrc_files, true);
}
/* Stores whether we have already idled once, handling extra rom
* and execute options */
bool has_idled = false;
/* Shell main loop */
while(true) {
/* Get a command if there isn't a file being executed */
if(lex_idle()) {
/* If we passed in FILE and we haven't handled it yet */
if(extra_rom && !has_idled) {
/* Create new 'file' virtual space and switch to it */
_vc(s, "file");
_vs(s, "file");
/* Add FILE as a ROM and ROM_P2 */
_vm(s, extra_rom, {MemoryRegion::ROM, MemoryRegion::ROM_P2});
}
/* If we need to execute a command */
if(!execute_arg.empty()) {
if(has_idled)
exit(0);
else
lex_repl(execute_arg);
}
else {
while(sigsetjmp(sigint_buf, 1) != 0)
;
bool leave = false;
std::string cmdline = read_interactive(s, leave);
if(leave)
break;
lex_repl(cmdline);
}
/* We have already handled FILE and --execute, don't do it again */
has_idled = true;
}
Parser parser(false);
try {
/* Read the next command from the lexer */
parser.start();
parser.skip_separators();
/* Read the command name */
if(parser.lookahead().type == T::END)
continue;
if(parser.lookahead().type == '?') {
for(auto const &it: commands) {
fmt::print(
" {:5s} {}\n", it.first, it.second->short_description);
}
continue;
}
std::string cmd = parser.symbol("command");
if(cmd == "q" && parser.lookahead().type != '?')
break;
if(cmd == "p") {
parser.dump_command();
}
else if(commands.count(cmd)) {
if(parser.lookahead().type == '?') {
fmt::print("{}: {}\n\n{}\n", cmd,
commands[cmd]->short_description,
commands[cmd]->long_description);
continue;
}
if(commands[cmd]->function)
commands[cmd]->function(s, parser);
}
else {
FxOS_log(ERR, "unknown command '%s'", cmd);
}
}
catch(std::exception &e) {
FxOS_log(ERR, "%s", e.what());
}
/* Exhaust command input (if not all used by the command) */
while(true) {
try {
parser.exhaust_until_separator();
break;
}
catch(std::exception &e) {
FxOS_log(ERR, "%s", e.what());
}
}
}
/* Save command history */
if(histfile[0])
write_history(histfile);
return 0;
}
/* Register a no-op quit command for auto-completion */
static ShellCommand _q_cmd("q", NULL, NULL, "Quit", "Quits fxos.");