#include #include #include #include #include #include #include #include #include #include #include #include "session.h" #include "shell.h" #include "theme.h" #include "parser.h" #include "commands.h" #include #include static std::map 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 complete_command(char const *text) { std::vector options; for(auto const &it: commands) { if(!strncmp(it.first.c_str(), text, strlen(text))) options.push_back(it.first); } return options; } std::vector complete_vspace(char const *text, Session &session) { std::vector 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 complete_region(char const *text) { std::vector 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 complete_symbol(char const *text, VirtualSpace *space) { std::vector 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 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 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.");