From 2c8abdf5cbddc9b6dd7e8ef1672fa5519928f045 Mon Sep 17 00:00:00 2001 From: zabereer Date: Sun, 24 Feb 2019 21:45:59 -0800 Subject: [PATCH] add `$pipestatus` support --- CHANGELOG.md | 1 + sphinx_doc_src/index.rst | 2 + sphinx_doc_src/tutorial.rst | 2 +- src/env.cpp | 14 +++++-- src/event.cpp | 2 + src/exec.cpp | 12 +++++- src/input.cpp | 2 + src/parse_execution.cpp | 1 + src/proc.cpp | 30 ++++++++++++++ src/proc.h | 8 ++++ tests/pipestatus.err | 18 +++++++++ tests/pipestatus.expect | 79 +++++++++++++++++++++++++++++++++++++ tests/pipestatus.expect.err | 0 tests/pipestatus.expect.out | 0 tests/pipestatus.in | 59 +++++++++++++++++++++++++++ tests/pipestatus.out | 60 ++++++++++++++++++++++++++++ 16 files changed, 285 insertions(+), 5 deletions(-) create mode 100644 tests/pipestatus.err create mode 100644 tests/pipestatus.expect create mode 100644 tests/pipestatus.expect.err create mode 100644 tests/pipestatus.expect.out create mode 100644 tests/pipestatus.in create mode 100644 tests/pipestatus.out diff --git a/CHANGELOG.md b/CHANGELOG.md index 6693e19c1..c46951ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## Notable fixes and improvements - Fixed infinite recursion triggered if a custom `fish_title` function calls `read` interactively +- Add `$pipestatus` support ### Syntax changes and new commands - None yet. diff --git a/sphinx_doc_src/index.rst b/sphinx_doc_src/index.rst index e4c0e055b..02a05840f 100644 --- a/sphinx_doc_src/index.rst +++ b/sphinx_doc_src/index.rst @@ -1193,6 +1193,8 @@ The user can change the settings of ``fish`` by changing the values of certain v - ``status``, the `exit status <#variables-status>`_ of the last foreground job to exit. If the job was terminated through a signal, the exit status will be 128 plus the signal number. +- ``pipestatus``, an array of exit statuses of all processes that made up the last executed pipe. + - ``USER``, the current username. This variable can be changed by the user. - ``CMD_DURATION``, the runtime of the last command in milliseconds. diff --git a/sphinx_doc_src/tutorial.rst b/sphinx_doc_src/tutorial.rst index 30b22a29d..0cc4a6d28 100644 --- a/sphinx_doc_src/tutorial.rst +++ b/sphinx_doc_src/tutorial.rst @@ -246,7 +246,7 @@ Unlike other shells, ``fish`` stores the exit status of the last command in ``$s 1 -Zero is considered success, and non-zero is failure. +Zero is considered success, and non-zero is failure. There is also a ``$pipestatus`` array variable for the exit statues of processes in a pipe. Exports (Shell Variables) diff --git a/src/env.cpp b/src/env.cpp index 83cb2cf32..48b91be06 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -316,8 +316,8 @@ bool string_set_contains(const T &set, const wchar_t *val) { /// Check if a variable may not be set using the set command. static bool is_read_only(const wchar_t *val) { const string_set_t env_read_only = { - L"PWD", L"SHLVL", L"history", L"status", L"version", - L"FISH_VERSION", L"fish_pid", L"hostname", L"_", L"fish_private_mode"}; + L"PWD", L"SHLVL", L"history", L"pipestatus", L"status", L"version", + L"FISH_VERSION", L"fish_pid", L"hostname", L"_", L"fish_private_mode"}; return string_set_contains(env_read_only, val) || (in_private_mode() && wcscmp(L"fish_history", val) == 0); } @@ -330,7 +330,7 @@ static bool variable_should_auto_pathvar(const wcstring &name) { } /// Table of variables whose value is dynamically calculated, such as umask, status, etc. -static const string_set_t env_electric = {L"history", L"status", L"umask"}; +static const string_set_t env_electric = {L"history", L"pipestatus", L"status", L"umask"}; static bool is_electric(const wcstring &key) { return contains(env_electric, key); } @@ -1391,6 +1391,14 @@ maybe_t env_stack_t::get(const wcstring &key, env_mode_flags_t mode) wcstring_list_t result; if (history) history->get_history(result); return env_var_t(L"history", result); + } else if (key == L"pipestatus") { + const auto& js = proc_get_last_job_statuses(); + wcstring_list_t result; + result.reserve(js->size()); + for (auto&& i : *js) { + result.emplace_back(to_string(i)); + } + return env_var_t(L"pipestatus", result); } else if (key == L"status") { return env_var_t(L"status", to_string(proc_get_last_status())); } else if (key == L"umask") { diff --git a/src/event.cpp b/src/event.cpp index 7ff0465af..63c4a8972 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -275,6 +275,7 @@ static void event_fire_internal(const event_t &event) { // non-interactive. proc_push_interactive(0); int prev_status = proc_get_last_status(); + const auto& saved_job_statuses = proc_get_last_job_statuses(); parser_t &parser = parser_t::principal_parser(); event_block_t *b = parser.push_block(event); @@ -282,6 +283,7 @@ static void event_fire_internal(const event_t &event) { parser.pop_block(b); proc_pop_interactive(); proc_set_last_status(prev_status); + proc_set_last_job_statuses(std::move(saved_job_statuses)); } } diff --git a/src/exec.cpp b/src/exec.cpp index 9b29ed8f1..217f416da 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -824,6 +824,7 @@ static bool exec_block_or_func_process(parser_t &parser, std::shared_ptr } int status = proc_get_last_status(); + p->status = status; // If we have a block output buffer, populate it now. if (!block_output_bufferfill) { @@ -1056,6 +1057,7 @@ bool exec_job(parser_t &parser, shared_ptr j) { } j->continue_job(false); + proc_set_last_job_statuses(*j); return true; } @@ -1064,6 +1066,7 @@ static int exec_subshell_internal(const wcstring &cmd, parser_t &parser, wcstrin ASSERT_IS_MAIN_THREAD(); bool prev_subshell = is_subshell; const int prev_status = proc_get_last_status(); + const auto& prev_job_statuses = proc_get_last_job_statuses(); bool split_output = false; const auto ifs = parser.vars().get(L"IFS"); @@ -1090,7 +1093,14 @@ static int exec_subshell_internal(const wcstring &cmd, parser_t &parser, wcstrin // If the caller asked us to preserve the exit status, restore the old status. Otherwise set the // status of the subcommand. - proc_set_last_status(apply_exit_status ? subcommand_status : prev_status); + if (apply_exit_status) { + proc_set_last_status(subcommand_status); + } + else { + proc_set_last_job_statuses(std::move(prev_job_statuses)); + proc_set_last_status(prev_status); + } + is_subshell = prev_subshell; if (lst == NULL || !buffer) { diff --git a/src/input.cpp b/src/input.cpp index ea9deb0cb..24eec3f52 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -367,10 +367,12 @@ static void input_mapping_execute(const input_mapping_t &m, bool allow_commands) // FIXME(snnw): if commands add stuff to input queue (e.g. commandline -f execute), we won't // see that until all other commands have also been run. int last_status = proc_get_last_status(); + const auto& last_job_statuses = proc_get_last_job_statuses(); for (const wcstring &cmd : m.commands) { parser_t::principal_parser().eval(cmd, io_chain_t(), TOP); } proc_set_last_status(last_status); + proc_set_last_job_statuses(std::move(last_job_statuses)); input_common_next_ch(R_NULL); } else { // Invalid binding, mixed commands and functions. We would need to execute these one by diff --git a/src/parse_execution.cpp b/src/parse_execution.cpp index cb2fa3b08..c4935df02 100644 --- a/src/parse_execution.cpp +++ b/src/parse_execution.cpp @@ -718,6 +718,7 @@ parse_execution_result_t parse_execution_context_t::handle_command_not_found( // Set the last proc status appropriately. proc_set_last_status(err_code == ENOENT ? STATUS_CMD_UNKNOWN : STATUS_NOT_EXECUTABLE); + proc_set_last_job_statuses(proc_get_last_status()); return parse_execution_errored; } diff --git a/src/proc.cpp b/src/proc.cpp index 19db27d0c..4bc504426 100644 --- a/src/proc.cpp +++ b/src/proc.cpp @@ -53,6 +53,9 @@ /// Status of last process to exit. static int last_status = 0; +/// Statuses of last job's processes to exit - ensure we start off with one entry of 0. +static std::shared_ptr> last_job_statuses = std::make_shared>(1); + /// The signals that signify crashes to us. static const int crashsignals[] = {SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGSYS}; @@ -139,6 +142,33 @@ void proc_set_last_status(int s) { int proc_get_last_status() { return last_status; } +void proc_set_last_job_statuses(const job_t &last_job) { + ASSERT_IS_MAIN_THREAD(); + auto ljs = std::make_shared>(); + ljs->reserve(last_job.processes.size()); + for (auto &&p : last_job.processes) { + ljs->emplace_back(p->pid ? proc_format_status(p->status) : p->status); + } + last_job_statuses = std::move(ljs); +} + +void proc_set_last_job_statuses(std::shared_ptr> job_statuses) { + ASSERT_IS_MAIN_THREAD(); + last_job_statuses = std::move(job_statuses); +} + +void proc_set_last_job_statuses(const int job_status) { + ASSERT_IS_MAIN_THREAD(); + auto ljs = std::make_shared>(1); + (*ljs)[0] = job_status; + last_job_statuses = std::move(ljs); +} + +std::shared_ptr> proc_get_last_job_statuses() { + ASSERT_IS_MAIN_THREAD(); + return last_job_statuses; +} + // Basic thread safe job IDs. The vector consumed_job_ids has a true value wherever the job ID // corresponding to that slot is in use. The job ID corresponding to slot 0 is 1. static owning_lock> locked_consumed_job_ids; diff --git a/src/proc.h b/src/proc.h index 3e986f160..a8adf73db 100644 --- a/src/proc.h +++ b/src/proc.h @@ -416,6 +416,14 @@ void proc_set_last_status(int s); /// Returns the status of the last process to exit. int proc_get_last_status(); +/// Sets the status of the last job's processes to exit from last_job. +void proc_set_last_job_statuses(const job_t &last_job); +void proc_set_last_job_statuses(std::shared_ptr>); +void proc_set_last_job_statuses(const int); // for errors where pipe is unknown + +/// Returns the status of the last job's processes to exit. +std::shared_ptr> proc_get_last_job_statuses(); + /// Notify the user about stopped or terminated jobs. Delete terminated jobs from the job list. /// /// \param interactive whether interactive jobs should be reaped as well diff --git a/tests/pipestatus.err b/tests/pipestatus.err new file mode 100644 index 000000000..9ac758b4a --- /dev/null +++ b/tests/pipestatus.err @@ -0,0 +1,18 @@ + +#################### +# pipestatus variable - builtins only + +#################### +# pipestatus variable - no builtins + +#################### +# pipestatus variable - mixed + +#################### +# pipestatus variable - non-pipe + +#################### +# pipestatus variable - negate + +#################### +# pipestatus variable - block diff --git a/tests/pipestatus.expect b/tests/pipestatus.expect new file mode 100644 index 000000000..bc2333872 --- /dev/null +++ b/tests/pipestatus.expect @@ -0,0 +1,79 @@ +# vim: set filetype=expect: +# +# Verify `$pipestatus` is preserved to next prompt. + +spawn $fish + +expect_prompt + +# no pipe +send_line "true" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "0 : 0" {} +send_line "false" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "1 : 1" {} + +# no pipe but negated +send_line "! true" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "0 : 1" {} +send_line "! false" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "1 : 0" {} + +# pipe +send_line "false | true | false" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "1 0 1 : 1" {} + +# pipe negated +send_line "! false | true | false" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "1 0 1 : 0" {} + +# pipe mixed with builtins and external +send_line "command true | false | true | true" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "0 1 0 0 : 0" {} +send_line "command true | command false | command false" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "0 1 1 : 1" {} +send_line "sh -c 'exit 3' | command false | sh -c 'exit 5'" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "3 1 5 : 5" {} + +# negated pipe with mixed builtin and external +send_line "! sh -c 'exit 3' | command false | sh -c 'exit 5'" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "3 1 5 : 0" {} + +# block +send_line "command false | begin; command true; end | true" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "1 0 0 : 0" {} +send_line "command false | begin; ! true; end | true" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "1 1 0 : 0" {} + +# syntax error +send_line "syntax terror" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "127 : 127" {} +send_line "syntax | terror" +expect_prompt +send_line "echo \$pipestatus : \$status" +expect_prompt "127 : 127" {} diff --git a/tests/pipestatus.expect.err b/tests/pipestatus.expect.err new file mode 100644 index 000000000..e69de29bb diff --git a/tests/pipestatus.expect.out b/tests/pipestatus.expect.out new file mode 100644 index 000000000..e69de29bb diff --git a/tests/pipestatus.in b/tests/pipestatus.in new file mode 100644 index 000000000..d6ceea9a8 --- /dev/null +++ b/tests/pipestatus.in @@ -0,0 +1,59 @@ +logmsg "pipestatus variable - builtins only" + +false | false | false; echo $pipestatus : $status +true | true | true; echo $pipestatus : $status +false | true | false; echo $pipestatus : $status +true | false | true; echo $pipestatus : $status + +logmsg "pipestatus variable - no builtins" + +command false | command false | command false; echo $pipestatus : $status +command true | command true | command true; echo $pipestatus : $status +command false | command true | command false; echo $pipestatus : $status +command true | command false | command true; echo $pipestatus : $status + +logmsg "pipestatus variable - mixed" + +command false | command false | false; echo $pipestatus : $status +command true | true | command true; echo $pipestatus : $status +false | command true | command false; echo $pipestatus : $status +true | false | command true; echo $pipestatus : $status +sh -c 'exit 5' | sh -c 'exit 2'; echo $pipestatus : $status +sh -c 'exit 3' | false | sh -c 'exit 6'; echo $pipestatus : $status +sh -c 'exit 9' | true | sh -c 'exit 3' | false; echo $pipestatus : $status + +logmsg "pipestatus variable - non-pipe" + +true; echo $pipestatus : $status +false; echo $pipestatus : $status +command true; echo $pipestatus : $status +command false; echo $pipestatus : $status +sh -c 'exit 4'; echo $pipestatus : $status + +logmsg "pipestatus variable - negate" + +! true; echo $pipestatus : $status +! false; echo $pipestatus : $status +! false | false | false; echo $pipestatus : $status +! true | command true | true; echo $pipestatus : $status +! false | true | command false; echo $pipestatus : $status +! command true | command false | command true; echo $pipestatus : $status +! sh -c 'exit 9' | true | sh -c 'exit 3'; echo $pipestatus : $status + +logmsg "pipestatus variable - block" + +begin; true; end; echo $pipestatus : $status +begin; false; end; echo $pipestatus : $status +begin; ! true; end; echo $pipestatus : $status +begin; ! false; end; echo $pipestatus : $status +true | begin; true; end; echo $pipestatus : $status +false | begin; false; end; echo $pipestatus : $status +true | begin; ! true; end; echo $pipestatus : $status +false | begin; ! false; end; echo $pipestatus : $status +begin; true | false; end; echo $pipestatus : $status +begin; false | true; end; echo $pipestatus : $status +begin; ! true; end | false ; echo $pipestatus : $status +begin; ! false; end | true; echo $pipestatus : $status +begin; sh -c 'exit 3'; end | begin; sh -c 'exit 5'; end; echo $pipestatus : $status +begin; ! sh -c 'exit 3'; end | begin; sh -c 'exit 5'; end; echo $pipestatus : $status +begin; ! sh -c 'exit 3'; end | begin; ! sh -c 'exit 5'; end; echo $pipestatus : $status diff --git a/tests/pipestatus.out b/tests/pipestatus.out new file mode 100644 index 000000000..bf5e7e03e --- /dev/null +++ b/tests/pipestatus.out @@ -0,0 +1,60 @@ + +#################### +# pipestatus variable - builtins only +1 1 1 : 1 +0 0 0 : 0 +1 0 1 : 1 +0 1 0 : 0 + +#################### +# pipestatus variable - no builtins +1 1 1 : 1 +0 0 0 : 0 +1 0 1 : 1 +0 1 0 : 0 + +#################### +# pipestatus variable - mixed +1 1 1 : 1 +0 0 0 : 0 +1 0 1 : 1 +0 1 0 : 0 +5 2 : 2 +3 1 6 : 6 +9 0 3 1 : 1 + +#################### +# pipestatus variable - non-pipe +0 : 0 +1 : 1 +0 : 0 +1 : 1 +4 : 4 + +#################### +# pipestatus variable - negate +0 : 1 +1 : 0 +1 1 1 : 0 +0 0 0 : 1 +1 0 1 : 0 +0 1 0 : 1 +9 0 3 : 0 + +#################### +# pipestatus variable - block +0 : 0 +1 : 1 +0 : 1 +1 : 0 +0 0 : 0 +1 1 : 1 +0 1 : 1 +1 0 : 0 +0 1 : 1 +1 0 : 0 +1 1 : 1 +0 0 : 0 +3 5 : 5 +0 5 : 5 +0 0 : 0