diff --git a/librz/core/cconfig.c b/librz/core/cconfig.c index 0eeda36b205..0af6e83f3e0 100644 --- a/librz/core/cconfig.c +++ b/librz/core/cconfig.c @@ -3353,6 +3353,8 @@ RZ_API int rz_core_config_init(RzCore *core) { SETBPREF("cfg.fortunes.clippy", "false", "Use 'clippy' instead of 'echo'"); SETPREF("cfg.prefixdump", "dump", "Filename prefix for automated dumps"); SETBPREF("cfg.wseek", "false", "Seek after write"); + SETBPREF("cfg.exit.errorcode", "false", "In batch mode (-q/-c/-i) exit with a non-zero process code when the last command fails"); + SETBPREF("cfg.json.status", "false", "Emit a structured JSON status envelope on stdout after each command in JSON output mode (null error on success, error object on failure)"); SETICB("cfg.seek.histsize", 63, NULL, "Maximum size of the seek history"); SETCB("cfg.seek.silent", "false", NULL, "When true, seek movements are not logged in seek history"); SETCB("cfg.bigendian", CFG_DEFAULT_ENDIANNESS, &cb_bigendian, "Use little (false) or big (true) endianness"); diff --git a/librz/core/cmd/cmd.c b/librz/core/cmd/cmd.c index 81784eb7b90..a339ff84573 100644 --- a/librz/core/cmd/cmd.c +++ b/librz/core/cmd/cmd.c @@ -403,6 +403,36 @@ RZ_IPI RzCmdStatus rz_last_output_handler(RzCore *core, int argc, const char **a return RZ_CMD_STATUS_OK; } +RZ_IPI RzCmdStatus rz_last_cmd_status_handler(RzCore *core, int argc, const char **argv, RzOutputMode mode) { + // The status reported is the one of the command executed right before this + // one, so that a script or rzpipe consumer can read it back after running a + // command. Running this command updates the status in turn (to its own OK), + // exactly like reading `$?` in a shell. + RzCmdStatus last = core->last_cmd_status; + switch (mode) { + case RZ_OUTPUT_MODE_JSON: { + PJ *pj = pj_new(); + if (!pj) { + return RZ_CMD_STATUS_ERROR; + } + pj_o(pj); + pj_ks(pj, "status", rz_cmd_status_tostring(last)); + pj_ki(pj, "code", rz_cmd_status2int(last)); + pj_end(pj); + rz_cons_println(pj_string(pj)); + pj_free(pj); + break; + } + case RZ_OUTPUT_MODE_STANDARD: + rz_cons_println(rz_cmd_status_tostring(last)); + break; + default: + rz_warn_if_reached(); + break; + } + return RZ_CMD_STATUS_OK; +} + #if __WINDOWS__ #include #define __CLOSE_DUPPED_PIPES() \ @@ -2834,6 +2864,10 @@ DEFINE_HANDLE_TS_FCN(statements) { rz_core_task_yield(&core->tasks); } core->cons->context->cmd_depth++; + // Track the status of each executed statement so that the `_s` command + // (and rzpipe consumers) can read back the status of the command that + // ran right before, even within a single multi-statement input. + core->last_cmd_status = cmd_res; if (cmd_res == RZ_CMD_STATUS_INVALID) { char *command_str = ts_node_sub_string(command, state->input); RZ_LOG_ERROR("core: Error while executing command: %s\n", command_str); @@ -2954,6 +2988,9 @@ static RzCmdStatus core_cmd_tsrzcmd(RzCore *core, const char *cstr, bool split_l free(input); rz_pvector_fini(&state.saved_input); rz_pvector_fini(&state.saved_tree); + // Remember the status of the (outermost) command line so that scripts and + // rzpipe consumers can read it back after the command has been executed. + core->last_cmd_status = res; return res; } diff --git a/librz/core/cmd/cmd_api.c b/librz/core/cmd/cmd_api.c index f45648566ac..b9b2cf96529 100644 --- a/librz/core/cmd/cmd_api.c +++ b/librz/core/cmd/cmd_api.c @@ -106,6 +106,36 @@ RZ_IPI const char *rz_output_mode_to_summary(RzOutputMode mode) { return ""; } +/** + * \brief Return a stable, machine-readable string code for a \p RzCmdStatus. + * + * The returned codes are part of the structured error envelope emitted in JSON + * output mode (e.g. `"code":"WRONG_ARGS"`) and are meant to be relied upon by + * consumers driving Rizin over rzpipe or scripts, so they must not change + * lightly. + * + * \param status The command status to convert. + * \return A static, non-owned string describing \p status. + */ +RZ_API const char *rz_cmd_status_tostring(RzCmdStatus status) { + switch (status) { + case RZ_CMD_STATUS_OK: + return "OK"; + case RZ_CMD_STATUS_WRONG_ARGS: + return "WRONG_ARGS"; + case RZ_CMD_STATUS_ERROR: + return "ERROR"; + case RZ_CMD_STATUS_INVALID: + return "INVALID"; + case RZ_CMD_STATUS_NONEXISTINGCMD: + return "NONEXISTINGCMD"; + case RZ_CMD_STATUS_EXIT: + return "EXIT"; + } + rz_warn_if_reached(); + return "UNKNOWN"; +} + #define NCMDS (sizeof(cmd->cmds) / sizeof(*cmd->cmds)) RZ_LIB_VERSION(rz_cmd); @@ -721,6 +751,80 @@ static void args_preprocessing(RzCmdDesc *cd, RzCmdParsedArgs *args) { } } +/** + * \brief Return a human-readable message describing a non-OK \p RzCmdStatus. + * + * Used as the `message` field of the structured JSON status envelope. The text + * is generic on purpose: it describes the status category, not the specific + * failure, and is meant to complement the stable `code` from + * \ref rz_cmd_status_tostring. + */ +static const char *cmd_status_message(RzCmdStatus status) { + switch (status) { + case RZ_CMD_STATUS_OK: + return "Command executed successfully"; + case RZ_CMD_STATUS_WRONG_ARGS: + return "Wrong number or type of arguments"; + case RZ_CMD_STATUS_ERROR: + return "Command execution failed"; + case RZ_CMD_STATUS_INVALID: + return "Invalid command or expression"; + case RZ_CMD_STATUS_NONEXISTINGCMD: + return "Command does not exist"; + case RZ_CMD_STATUS_EXIT: + return "Command requested to exit the prompt"; + } + rz_warn_if_reached(); + return "Unknown command status"; +} + +static inline bool output_mode_is_json(RzOutputMode mode) { + return mode == RZ_OUTPUT_MODE_JSON || mode == RZ_OUTPUT_MODE_LONG_JSON; +} + +/** + * \brief Emit a structured JSON status envelope for an executed command. + * + * When a command is invoked in a JSON output mode and the `cfg.json.status` + * config is enabled, this prints a machine-readable status object after the + * command's own output so that consumers driving Rizin over rzpipe or + * `rizin -c` can reliably tell success from failure (and a failure from an + * empty-but-successful result) by always reading the same trailing object. The + * shape is, on success: + * + * {"error":null} + * + * and on failure: + * + * {"error":{"code":"WRONG_ARGS","message":"...","command":"afl"}} + * + * The envelope is reported for every command (success and failure alike) so the + * presence of the object is consistent; only its `error` field changes. + * + * \param cmd_name Name of the command that ran (canonical, without the mode suffix). + * \param status The status returned by the command handler. + */ +static void cmd_print_json_status(RZ_NONNULL const char *cmd_name, RzCmdStatus status) { + rz_return_if_fail(cmd_name); + PJ *pj = pj_new(); + if (!pj) { + return; + } + pj_o(pj); + if (status == RZ_CMD_STATUS_OK) { + pj_knull(pj, "error"); + } else { + pj_ko(pj, "error"); + pj_ks(pj, "code", rz_cmd_status_tostring(status)); + pj_ks(pj, "message", cmd_status_message(status)); + pj_ks(pj, "command", cmd_name); + pj_end(pj); + } + pj_end(pj); + rz_cons_println(pj_string(pj)); + pj_free(pj); +} + static RzCmdStatus argv_call_cb(RzCmd *cmd, RzCmdDesc *cd, RzCmdParsedArgs *args) { if (!rz_cmd_desc_has_handler(cd)) { return RZ_CMD_STATUS_NONEXISTINGCMD; @@ -734,9 +838,15 @@ static RzCmdStatus argv_call_cb(RzCmd *cmd, RzCmdDesc *cd, RzCmdParsedArgs *args RZ_LOG_DEBUG("processed parsed_arg %d: '%s'\n", i, s); } - RzOutputMode mode; + // Output mode of the executed command. It is used after the switch to + // decide whether the command's status should be reported as a structured + // JSON status envelope (see cmd_print_json_status). + RzOutputMode mode = RZ_OUTPUT_MODE_STANDARD; + RzCmdStatus res; switch (cd->type) { case RZ_CMD_DESC_TYPE_ARGV: + // Plain argv commands have no output mode, so they never emit a JSON + // status envelope. if (args->argc < cd->d.argv_data.min_argc || args->argc > cd->d.argv_data.max_argc) { return RZ_CMD_STATUS_WRONG_ARGS; } @@ -747,25 +857,28 @@ static RzCmdStatus argv_call_cb(RzCmd *cmd, RzCmdDesc *cd, RzCmdParsedArgs *args return RZ_CMD_STATUS_NONEXISTINGCMD; } if (args->argc < cd->d.argv_modes_data.min_argc || args->argc > cd->d.argv_modes_data.max_argc) { - return RZ_CMD_STATUS_WRONG_ARGS; + res = RZ_CMD_STATUS_WRONG_ARGS; + break; } - return cd->d.argv_modes_data.cb(cmd->core, args->argc, (const char **)args->argv, mode); - case RZ_CMD_DESC_TYPE_ARGV_STATE: + res = cd->d.argv_modes_data.cb(cmd->core, args->argc, (const char **)args->argv, mode); + break; + case RZ_CMD_DESC_TYPE_ARGV_STATE: { mode = cd_suffix2mode(cd, rz_cmd_parsed_args_cmd(args)); if (!mode) { return RZ_CMD_STATUS_NONEXISTINGCMD; } if (args->argc < cd->d.argv_state_data.min_argc || args->argc > cd->d.argv_state_data.max_argc) { - return RZ_CMD_STATUS_WRONG_ARGS; + res = RZ_CMD_STATUS_WRONG_ARGS; + break; } RzCmdStateOutput state; if (!rz_cmd_state_output_init(&state, mode, cmd->core)) { return RZ_CMD_STATUS_INVALID; } - RzCmdStatus res = cd->d.argv_state_data.cb(cmd->core, args->argc, (const char **)args->argv, &state); + res = cd->d.argv_state_data.cb(cmd->core, args->argc, (const char **)args->argv, &state); if (args->extra && state.mode == RZ_OUTPUT_MODE_TABLE) { - bool res = rz_table_query(state.d.t, args->extra); - if (!res) { + bool ok = rz_table_query(state.d.t, args->extra); + if (!ok) { rz_cmd_state_output_fini(&state); return RZ_CMD_STATUS_INVALID; } @@ -774,10 +887,16 @@ static RzCmdStatus argv_call_cb(RzCmd *cmd, RzCmdDesc *cd, RzCmdParsedArgs *args rz_cmd_state_output_print(&state); } rz_cmd_state_output_fini(&state); - return res; + break; + } default: return RZ_CMD_STATUS_INVALID; } + + if (output_mode_is_json(mode) && core_config_get_b(cmd->core, "cfg.json.status")) { + cmd_print_json_status(cd->name, res); + } + return res; } static RzCmdStatus call_cd(RzCmd *cmd, RzCmdDesc *cd, RzCmdParsedArgs *args) { diff --git a/librz/core/cmd_descs/cmd_descs.c b/librz/core/cmd_descs/cmd_descs.c index 81899c51402..5a6765235ad 100644 --- a/librz/core/cmd_descs/cmd_descs.c +++ b/librz/core/cmd_descs/cmd_descs.c @@ -1079,6 +1079,9 @@ static const RzCmdDescHelp system_to_cons_help = { .args = system_to_cons_args, }; +static const RzCmdDescHelp _help = { + .summary = "Last command introspection", +}; static const RzCmdDescArg last_output_args[] = { { 0 }, }; @@ -1087,6 +1090,14 @@ static const RzCmdDescHelp last_output_help = { .args = last_output_args, }; +static const RzCmdDescArg last_cmd_status_args[] = { + { 0 }, +}; +static const RzCmdDescHelp last_cmd_status_help = { + .summary = "Print the status of the last executed command line", + .args = last_cmd_status_args, +}; + static const RzCmdDescDetailEntry hash_bang_Examples_detail_entries[] = { { .text = "#!", .arg_str = "python", .comment = "Run python commandline" }, { .text = "#!", .arg_str = "python foo.py", .comment = "Run foo.py python script" }, @@ -22518,8 +22529,10 @@ RZ_IPI void rzshell_cmddescs_init(RzCore *core) { RzCmdDesc *system_to_cons_cd = rz_cmd_desc_argv_new(core->rcmd, escl__cd, "!!", rz_system_to_cons_handler, &system_to_cons_help); rz_warn_if_fail(system_to_cons_cd); - RzCmdDesc *last_output_cd = rz_cmd_desc_argv_new(core->rcmd, root_cd, "_", rz_last_output_handler, &last_output_help); - rz_warn_if_fail(last_output_cd); + RzCmdDesc *_cd = rz_cmd_desc_group_new(core->rcmd, root_cd, "_", rz_last_output_handler, &last_output_help, &_help); + rz_warn_if_fail(_cd); + RzCmdDesc *last_cmd_status_cd = rz_cmd_desc_argv_modes_new(core->rcmd, _cd, "_s", RZ_OUTPUT_MODE_STANDARD | RZ_OUTPUT_MODE_JSON, rz_last_cmd_status_handler, &last_cmd_status_help); + rz_warn_if_fail(last_cmd_status_cd); RzCmdDesc *hash_bang_cd = rz_cmd_desc_argv_new(core->rcmd, root_cd, "#!", rz_hash_bang_handler, &hash_bang_help); rz_warn_if_fail(hash_bang_cd); diff --git a/librz/core/cmd_descs/cmd_descs.h b/librz/core/cmd_descs/cmd_descs.h index 2521aa4b607..bccf2823985 100644 --- a/librz/core/cmd_descs/cmd_descs.h +++ b/librz/core/cmd_descs/cmd_descs.h @@ -16,6 +16,8 @@ RZ_IPI RzCmdStatus rz_system_handler(RzCore *core, int argc, const char **argv); RZ_IPI RzCmdStatus rz_system_to_cons_handler(RzCore *core, int argc, const char **argv); // "_" RZ_IPI RzCmdStatus rz_last_output_handler(RzCore *core, int argc, const char **argv); +// "_s" +RZ_IPI RzCmdStatus rz_last_cmd_status_handler(RzCore *core, int argc, const char **argv, RzOutputMode mode); // "#!" RZ_IPI RzCmdStatus rz_hash_bang_handler(RzCore *core, int argc, const char **argv); RZ_IPI RzCmdDescDetail *rz_hash_bang_details_cb(RzCore *core, int argc, const char **argv); diff --git a/librz/core/cmd_descs/cmd_descs.yaml b/librz/core/cmd_descs/cmd_descs.yaml index 8f693852f87..6ef36f31819 100644 --- a/librz/core/cmd_descs/cmd_descs.yaml +++ b/librz/core/cmd_descs/cmd_descs.yaml @@ -67,9 +67,19 @@ commands: summary: Run command via system(3) subcommands: cmd_system - name: _ - cname: last_output - summary: Print last output - args: [] + summary: Last command introspection + subcommands: + - name: _ + cname: last_output + summary: Print last output + args: [] + - name: _s + cname: last_cmd_status + summary: Print the status of the last executed command line + args: [] + modes: + - RZ_OUTPUT_MODE_STANDARD + - RZ_OUTPUT_MODE_JSON - name: "#!" cname: hash_bang summary: Run interpreter diff --git a/librz/core/core.c b/librz/core/core.c index aa3918a55c1..4d28df687c9 100644 --- a/librz/core/core.c +++ b/librz/core/core.c @@ -1658,6 +1658,7 @@ RZ_API bool rz_core_init(RzCore *core) { core->vmode = false; core->lastcmd = NULL; core->cmdlog = NULL; + core->last_cmd_status = RZ_CMD_STATUS_OK; core->stkcmd = NULL; core->cmdqueue = NULL; core->cmdrepeat = true; diff --git a/librz/include/rz_cmd.h b/librz/include/rz_cmd.h index 5e5c892a1d1..edab9059067 100644 --- a/librz/include/rz_cmd.h +++ b/librz/include/rz_cmd.h @@ -557,6 +557,8 @@ static inline int rz_cmd_status2int(RzCmdStatus s) { } } +RZ_API const char *rz_cmd_status_tostring(RzCmdStatus status); + /* RzCmdDescriptor */ RZ_API RzCmdDesc *rz_cmd_desc_argv_new(RzCmd *cmd, RzCmdDesc *parent, const char *name, RzCmdArgvCb cb, const RzCmdDescHelp *help); RZ_API RzCmdDesc *rz_cmd_desc_argv_modes_new(RzCmd *cmd, RzCmdDesc *parent, const char *name, int modes, RzCmdArgvModesCb cb, const RzCmdDescHelp *help); diff --git a/librz/include/rz_core.h b/librz/include/rz_core.h index ac4ba471675..1e65fc5552b 100644 --- a/librz/include/rz_core.h +++ b/librz/include/rz_core.h @@ -337,6 +337,7 @@ struct rz_core_t { bool is_lastcmd; bool is_pipe; char *cmdlog; + RzCmdStatus last_cmd_status; ///< Status of the last command line executed via the rzshell, exposed to scripts/rzpipe int cmdrepeat; // cmd.repeat const char *cmdtimes; // cmd.times RZ_DEPRECATE bool cmd_in_backticks; // whether currently executing a cmd out of backticks diff --git a/librz/main/rizin.c b/librz/main/rizin.c index 380ea640e10..82dc3d0211b 100644 --- a/librz/main/rizin.c +++ b/librz/main/rizin.c @@ -311,18 +311,39 @@ static int main_print_var(RZ_BORROW RZ_NONNULL RzCore *core, const char *var_nam return 0; } -static bool run_commands(RzCore *r, RzList /**/ *cmds, RzList /**/ *files, bool quiet, int do_analysis) { +// A command line is considered failed for process-exit-code purposes when it +// neither succeeded nor simply requested to quit the prompt (e.g. `q`). +static inline bool cmd_status_is_error(RzCmdStatus status) { + return status != RZ_CMD_STATUS_OK && status != RZ_CMD_STATUS_EXIT; +} + +/** + * \brief Run the `-i` scripts and `-c` commands passed on the command line. + * + * \param r The RzCore instance. + * \param cmds List of `-c` command lines to execute, may be NULL. + * \param files List of `-i` script files to execute, may be NULL. + * \param quiet Whether Rizin was started in quiet/batch mode. + * \param do_analysis Whether some analysis was requested on startup. + * \param had_error Optional output set to true if any `-c` command failed, used + * by the caller to return a non-zero process exit code. + * \return true if Rizin should quit right after running the commands, false otherwise. + */ +static bool run_commands(RzCore *core, RzList /**/ *cmds, RzList /**/ *files, bool quiet, int do_analysis, RZ_NULLABLE RZ_OUT bool *had_error) { RzListIter *iter; const char *cmdn; const char *file; int ret; + if (had_error) { + *had_error = false; + } /* -i */ rz_list_foreach (files, iter, file) { if (!rz_file_exists(file)) { RZ_LOG_ERROR("Script '%s' not found.\n", file); goto beach; } - ret = rz_core_run_script(r, file); + ret = rz_core_run_script(core, file); if (ret == -2) { RZ_LOG_ERROR("[c] Cannot open '%s'\n", file); } @@ -333,8 +354,10 @@ static bool run_commands(RzCore *r, RzList /**/ *cmds, RzList /* } /* -c */ rz_list_foreach (cmds, iter, cmdn) { - // rz_core_cmd0 (r, cmdn); - rz_core_cmd_lines(r, cmdn); + RzCmdStatus status = rz_core_cmd_lines_rzshell(core, cmdn); + if (had_error && cmd_status_is_error(status)) { + *had_error = true; + } rz_cons_flush(); } beach: @@ -825,8 +848,8 @@ RZ_API int rz_main_rizin(int argc, const char **argv) { if (rz_config_get_i(r->config, "cfg.plugins")) { rz_core_loadlibs(r, RZ_CORE_LOADLIBS_ALL); } - run_commands(r, NULL, prefiles, false, do_analysis); - run_commands(r, cmds, files, quiet, do_analysis); + run_commands(r, NULL, prefiles, false, do_analysis, NULL); + run_commands(r, cmds, files, quiet, do_analysis, NULL); rz_cmd_state_output_init(&state, RZ_OUTPUT_MODE_STANDARD, r); rz_core_io_plugins_print(r->io, &state); rz_cmd_state_output_print(&state); @@ -893,7 +916,7 @@ RZ_API int rz_main_rizin(int argc, const char **argv) { if (rz_config_get_i(r->config, "cfg.plugins")) { rz_core_loadlibs(r, RZ_CORE_LOADLIBS_ALL); } - run_commands(r, NULL, prefiles, false, do_analysis); + run_commands(r, NULL, prefiles, false, do_analysis, NULL); rz_list_free(prefiles); prefiles = NULL; @@ -1450,16 +1473,21 @@ RZ_API int rz_main_rizin(int argc, const char **argv) { map->perm |= RZ_PERM_W; } } - ret = run_commands(r, cmds, files, quiet, do_analysis); + bool cmd_had_error = false; + bool quit_now = run_commands(r, cmds, files, quiet, do_analysis, &cmd_had_error); rz_list_free(cmds); rz_list_free(evals); rz_list_free(files); cmds = evals = files = NULL; if (forcequit) { - ret = 1; + quit_now = true; } - if (ret) { - ret = 0; + if (quit_now) { + // In batch mode (e.g. `rizin -c ...` or `-i script`) optionally report a + // failing command as a non-zero process exit, so scripts and CI can + // detect it. This is opt-in via `cfg.exit.errorcode` to preserve the + // historical behavior of always exiting with 0 in batch mode. + ret = (cmd_had_error && rz_config_get_b(r->config, "cfg.exit.errorcode")) ? 1 : 0; goto beach; } if (rz_config_get_i(r->config, "scr.prompt")) { diff --git a/test/db/cmd/cmd_error_reporting b/test/db/cmd/cmd_error_reporting new file mode 100644 index 00000000000..14b814a92ea --- /dev/null +++ b/test/db/cmd/cmd_error_reporting @@ -0,0 +1,86 @@ +NAME=last command status readback after success (_s and _sj) +FILE== +CMDS=<