Add a $status_generation variable that's incremented for each interactive command that produces a status.

This can be used to determine whether the previous command produced a real status, or just carried over the status from the command before it. Backgrounded commands and variable assignments will not increment status_generation, all other commands will.
This commit is contained in:
Soumya 2020-08-02 13:37:19 -07:00 committed by ridiculousfish
parent 54823c9243
commit a2b2bcef6e
10 changed files with 140 additions and 7 deletions

View file

@ -70,6 +70,7 @@ Interactive improvements
- Control-Z is now available for binding (#7152).
- ``fish_key_reader`` sets the exit status to 0 when used with ``--help`` or ``--version`` (#6964).
- ``fish_key_reader`` and ``fish_indent`` send output from ``--version`` to standard output, matching other fish binaries (#6964).
- A new variable ``$status_generation`` is incremented only when the previous command produces a status. This can be used, for example, to check whether a failure status is a holdover due to a background job, or actually produced by the last run command.
New or improved bindings

View file

@ -1225,6 +1225,8 @@ You can change the settings of ``fish`` by changing the values of certain variab
- ``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.
- ``status_generation``, the "generation" count of ``$status``. This will be incremented only when the previous command produced an explicit status. (For example, background jobs will not increment this).
- ``USER``, the current username. This variable can be changed by the user.
- ``version``, the version of the currently running fish (also available as ``FISH_VERSION`` for backward compatibility).

View file

@ -95,6 +95,7 @@ static const std::vector<electric_var_t> electric_variables{
{L"hostname", electric_var_t::freadonly},
{L"pipestatus", electric_var_t::freadonly | electric_var_t::fcomputed},
{L"status", electric_var_t::freadonly | electric_var_t::fcomputed},
{L"status_generation", electric_var_t::freadonly | electric_var_t::fcomputed},
{L"umask", electric_var_t::fcomputed},
{L"version", electric_var_t::freadonly},
};
@ -701,6 +702,9 @@ maybe_t<env_var_t> env_scoped_impl_t::try_get_computed(const wcstring &key) cons
} else if (key == L"status") {
const auto &js = perproc_data().statuses;
return env_var_t(L"status", to_string(js.status));
} else if (key == L"status_generation") {
auto status_generation = reader_status_count();
return env_var_t(L"status_generation", to_string(status_generation));
} else if (key == L"fish_kill_signal") {
const auto &js = perproc_data().statuses;
return env_var_t(L"fish_kill_signal", to_string(js.kill_signal));

View file

@ -393,6 +393,7 @@ end_execution_reason_t parse_execution_context_t::run_function_statement(
buffered_output_stream_t errs(0);
io_streams_t streams(outs, errs);
int err = builtin_function(*parser, streams, arguments, pstree, statement);
parser->libdata().status_count++;
parser->set_last_statuses(statuses_t::just(err));
wcstring errtext = errs.contents();

View file

@ -649,7 +649,8 @@ eval_res_t parser_t::eval(const parsed_source_ref_t &ps, const io_chain_t &io,
auto status = proc_status_t::from_exit_code(get_last_status());
bool break_expand = false;
bool was_empty = true;
return eval_res_t{status, break_expand, was_empty};
bool no_status = true;
return eval_res_t{status, break_expand, was_empty, no_status};
}
}
@ -713,8 +714,10 @@ eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, const T &node,
// Check the exec count so we know if anything got executed.
const size_t prev_exec_count = libdata().exec_count;
const size_t prev_status_count = libdata().status_count;
end_execution_reason_t reason = execution_context->eval_node(node, scope_block);
const size_t new_exec_count = libdata().exec_count;
const size_t new_status_count = libdata().status_count;
exc.restore();
this->pop_block(scope_block);
@ -728,7 +731,8 @@ eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, const T &node,
auto status = proc_status_t::from_exit_code(this->get_last_status());
bool break_expand = (reason == end_execution_reason_t::error);
bool was_empty = !break_expand && prev_exec_count == new_exec_count;
return eval_res_t{status, break_expand, was_empty};
bool no_status = prev_status_count == new_status_count;
return eval_res_t{status, break_expand, was_empty, no_status};
}
}

View file

@ -142,6 +142,9 @@ struct library_data_t {
/// A counter incremented every time a command executes.
uint64_t exec_count{0};
/// A counter incremented every time a command produces a $status.
uint64_t status_count{0};
/// Last reader run count.
uint64_t last_exec_run_counter{UINT64_MAX};
@ -219,9 +222,12 @@ struct eval_res_t {
/// If set, no commands were executed and there we no errors.
bool was_empty{false};
/// If set, no commands produced a $status value.
bool no_status{false};
/* implicit */ eval_res_t(proc_status_t status, bool break_expand = false,
bool was_empty = false)
: status(status), break_expand(break_expand), was_empty(was_empty) {}
bool was_empty = false, bool no_status = false)
: status(status), break_expand(break_expand), was_empty(was_empty), no_status(no_status) {}
};
class parser_t : public std::enable_shared_from_this<parser_t> {

View file

@ -998,6 +998,7 @@ void job_t::continue_job(parser_t &parser, bool in_foreground) {
const auto &p = processes.back();
if (p->status.normal_exited() || p->status.signal_exited()) {
parser.set_last_statuses(get_statuses());
parser.libdata().status_count++;
}
}
}

View file

@ -2171,7 +2171,7 @@ void set_env_cmd_duration(struct timeval *after, struct timeval *before, env_sta
vars.set_one(ENV_CMD_DURATION, ENV_UNEXPORT, std::to_wstring((secs * 1000) + (usecs / 1000)));
}
void reader_run_command(parser_t &parser, const wcstring &cmd) {
eval_res_t reader_run_command(parser_t &parser, const wcstring &cmd) {
struct timeval time_before, time_after;
wcstring ft = tok_command(cmd);
@ -2185,7 +2185,7 @@ void reader_run_command(parser_t &parser, const wcstring &cmd) {
gettimeofday(&time_before, nullptr);
parser.eval(cmd, io_chain_t{});
auto eval_res = parser.eval(cmd, io_chain_t{});
job_reap(parser, true);
gettimeofday(&time_after, nullptr);
@ -2202,6 +2202,8 @@ void reader_run_command(parser_t &parser, const wcstring &cmd) {
if (have_proc_stat()) {
proc_update_jiffies(parser);
}
return eval_res;
}
static parser_test_error_bits_t reader_shell_test(parser_t &parser, const wcstring &b) {
@ -2457,6 +2459,13 @@ static relaxed_atomic_t<uint64_t> run_count{0};
/// Returns the current interactive loop count
uint64_t reader_run_count() { return run_count; }
static relaxed_atomic_t<uint64_t> status_count{0};
/// Returns the current "generation" of interactive status.
/// This is not incremented if the command being run produces no status,
/// (e.g. background job, or variable assignment).
uint64_t reader_status_count() { return status_count; }
/// Read interactively. Read input from stdin while providing editing facilities.
static int read_i(parser_t &parser) {
reader_config_t conf;
@ -2499,8 +2508,11 @@ static int read_i(parser_t &parser) {
data->command_line_changed(&data->command_line);
wcstring_list_t argv(1, command);
event_fire_generic(parser, L"fish_preexec", &argv);
reader_run_command(parser, command);
auto eval_res = reader_run_command(parser, command);
signal_clear_cancel();
if (!eval_res.no_status) {
status_count++;
}
event_fire_generic(parser, L"fish_postexec", &argv);
// Allow any pending history items to be returned in the history array.
if (data->history) {

View file

@ -278,4 +278,8 @@ wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flag
/// been executed between invocations of code.
uint64_t reader_run_count();
/// Returns the current "generation" of interactive status. Useful for determining whether the
/// previous command produced a status.
uint64_t reader_status_count();
#endif

View file

@ -0,0 +1,98 @@
#!/usr/bin/env python3
from pexpect_helper import SpawnedProc
sp = SpawnedProc()
sendline, expect_prompt, expect_str = sp.sendline, sp.expect_prompt, sp.expect_str
# Test fish_postexec and $status_generation for interactive shells.
expect_prompt()
sendline("function test_fish_postexec --on-event fish_postexec; printf 'pipestatus:%s, generation:%d, command:%s\\n' (string join '|' $pipestatus) $status_generation $argv; end")
expect_prompt()
generation = 1
# fish_postexec is called when foreground job ends.
sendline("true")
generation += 1
expect_str("pipestatus:0, generation:%d, command:true" % generation)
expect_prompt()
# Command has multiple jobs.
sendline("true;false")
generation += 1
expect_str("pipestatus:1, generation:%d, command:true;false" % generation)
expect_prompt()
# Command is a pipeline.
sendline("true|false")
generation += 1
expect_str("pipestatus:0|1, generation:%d, command:true|false" % generation)
expect_prompt()
# Does not increment $status_generation for background jobs.
# status, pipestatus remain unchanged
sendline("sleep 1000 &")
expect_str("pipestatus:0|1, generation:%d, command:sleep 1000 &" % generation)
expect_prompt()
# multiple backgrounded jobs
sendline("sleep 1000 &; sleep 2000 &")
expect_str("pipestatus:0|1, generation:%d, command:sleep 1000 &; sleep 2000 &" % generation)
expect_prompt()
# Increments $status_generation if any job was foreground.
sendline("false|true; sleep 1000 &")
generation += 1
expect_str("pipestatus:1|0, generation:%d, command:false|true; sleep 1000 &" % generation)
expect_prompt()
sendline("sleep 1000 &; true|false|true")
generation += 1
expect_str("pipestatus:0|1|0, generation:%d, command:sleep 1000 &; true|false|true" % generation)
expect_prompt()
# Increments $status_generation for empty if/while blocks.
sendline("if true;end")
generation += 1
expect_str("pipestatus:0, generation:%d, command:if true;end" % generation)
expect_prompt()
sendline("while false;end")
generation += 1
expect_str("pipestatus:0, generation:%d, command:while false;end" % generation)
expect_prompt()
# or non-matching if.
sendline("if false;false;end")
generation += 1
expect_str("pipestatus:0, generation:%d, command:if false;false;end" % generation)
expect_prompt()
# or a function definition.
# This is an implementation detail, but the test case should prevent regressions.
sendline("function fail; false; end")
generation += 1
expect_str("pipestatus:0, generation:%d, command:function fail; false; end" % generation)
expect_prompt()
# This is just to set a memorable pipestatus.
sendline("true|false|true")
generation += 1
expect_str("pipestatus:0|1|0")
expect_prompt()
# Does not increment $status_generation for empty begin/end block.
sendline("begin;end")
expect_str("pipestatus:0|1|0, generation:%d, command:begin;end" % generation)
expect_prompt()
# Or begin/end block with only backgrounded jobs.
sendline("begin; sleep 200 &; sleep 400 &; end")
expect_str("pipestatus:0|1|0, generation:%d, command:begin; sleep 200 &; sleep 400 &; end" % generation)
expect_prompt()
# Or a combination of begin/end block and backgrounded job.
sendline("begin; sleep 200 &; end; sleep 400 &")
expect_str("pipestatus:0|1|0, generation:%d, command:begin; sleep 200 &; end; sleep 400 &" % generation)
expect_prompt()