Make "time" a job prefix

In particular, this allows `true && time true`, or `true; and time true`,
and both `time not true` as well as `not time true` (like bash).

time is valid only as job _prefix_, so `true | time true` could call
`/bin/time` (same in bash)

See discussion in #6442
This commit is contained in:
Johannes Altmanninger 2019-12-21 11:45:07 +01:00 committed by Mahmoud Al-Qudsi
parent c1140bc436
commit 3de95038b0
13 changed files with 101 additions and 42 deletions

View file

@ -44,6 +44,7 @@
#include "reader.h" #include "reader.h"
#include "redirection.h" #include "redirection.h"
#include "signal.h" #include "signal.h"
#include "timer.h"
#include "trace.h" #include "trace.h"
#include "wutil.h" // IWYU pragma: keep #include "wutil.h" // IWYU pragma: keep
@ -1049,6 +1050,7 @@ bool exec_job(parser_t &parser, const shared_ptr<job_t> &j, const job_lineage_t
parser.set_last_statuses(statuses_t::just(status)); parser.set_last_statuses(statuses_t::just(status));
return false; return false;
} }
cleanup_t timer = push_timer(j->flags().has_time_prefix);
// Get the deferred process, if any. We will have to remember its pipes. // Get the deferred process, if any. We will have to remember its pipes.
autoclose_pipes_t deferred_pipes; autoclose_pipes_t deferred_pipes;

View file

@ -1211,6 +1211,10 @@ highlighter_t::color_array_t highlighter_t::highlight() {
this->color_node(node, highlight_role_t::statement_terminator); this->color_node(node, highlight_role_t::statement_terminator);
break; break;
} }
case symbol_optional_time: {
this->color_node(node, highlight_role_t::operat);
break;
}
case symbol_plain_statement: { case symbol_plain_statement: {
tnode_t<g::plain_statement> stmt(&parse_tree, &node); tnode_t<g::plain_statement> stmt(&parse_tree, &node);
// Get the decoration from the parent. // Get the decoration from the parent.

View file

@ -52,6 +52,7 @@ enum parse_token_type_t : uint8_t {
symbol_redirection, symbol_redirection,
symbol_optional_background, symbol_optional_background,
symbol_optional_newlines, symbol_optional_newlines,
symbol_optional_time,
symbol_end_command, symbol_end_command,
// Terminal types. // Terminal types.
parse_token_type_string, parse_token_type_string,
@ -157,12 +158,14 @@ enum parse_job_decoration_t {
parse_job_decoration_none, parse_job_decoration_none,
parse_job_decoration_and, parse_job_decoration_and,
parse_job_decoration_or, parse_job_decoration_or,
parse_job_decoration_time,
}; };
// Whether a statement is backgrounded. // Whether a statement is backgrounded.
enum parse_optional_background_t { parse_no_background, parse_background }; enum parse_optional_background_t { parse_no_background, parse_background };
// Whether a job is prefixed with "time".
enum parse_optional_time_t { parse_optional_time_no_time, parse_optional_time_time };
// Parse error code list. // Parse error code list.
enum parse_error_code_t { enum parse_error_code_t {
parse_error_none, parse_error_none,

View file

@ -122,13 +122,14 @@ tnode_t<g::plain_statement> parse_execution_context_t::infinite_recursive_statem
// Here's the statement node we find that's infinite recursive. // Here's the statement node we find that's infinite recursive.
tnode_t<grammar::plain_statement> infinite_recursive_statement; tnode_t<grammar::plain_statement> infinite_recursive_statement;
// Get the list of plain statements. // Ignore the jobs variable assigment and "time" prefixes.
// Ignore statements with decorations like 'builtin' or 'command', since those tnode_t<g::statement> statement = first_job.child<2>();
// are not infinite recursion. In particular that is what enables 'wrapper functions'. tnode_t<g::job_continuation> continuation = first_job.child<3>();
tnode_t<g::statement> statement = first_job.child<1>();
tnode_t<g::job_continuation> continuation = first_job.child<2>();
const null_environment_t nullenv{}; const null_environment_t nullenv{};
while (statement) { while (statement) {
// Get the list of plain statements.
// Ignore statements with decorations like 'builtin' or 'command', since those
// are not infinite recursion. In particular that is what enables 'wrapper functions'.
tnode_t<g::plain_statement> plain_statement = tnode_t<g::plain_statement> plain_statement =
statement.try_get_child<g::decorated_statement, 0>() statement.try_get_child<g::decorated_statement, 0>()
.try_get_child<g::plain_statement, 0>(); .try_get_child<g::plain_statement, 0>();
@ -203,10 +204,10 @@ maybe_t<eval_result_t> parse_execution_context_t::check_end_execution() const {
/// Return whether the job contains a single statement, of block type, with no redirections. /// Return whether the job contains a single statement, of block type, with no redirections.
bool parse_execution_context_t::job_is_simple_block(tnode_t<g::job> job_node) const { bool parse_execution_context_t::job_is_simple_block(tnode_t<g::job> job_node) const {
tnode_t<g::statement> statement = job_node.child<1>(); tnode_t<g::statement> statement = job_node.child<2>();
// Must be no pipes. // Must be no pipes.
if (job_node.child<2>().try_get_child<g::tok_pipe, 0>()) { if (job_node.child<3>().try_get_child<g::tok_pipe, 0>()) {
return false; return false;
} }
@ -975,8 +976,10 @@ eval_result_t parse_execution_context_t::populate_not_process(
job_t *job, process_t *proc, tnode_t<g::not_statement> not_statement) { job_t *job, process_t *proc, tnode_t<g::not_statement> not_statement) {
auto &flags = job->mut_flags(); auto &flags = job->mut_flags();
flags.negate = !flags.negate; flags.negate = !flags.negate;
auto optional_time = not_statement.require_get_child<g::optional_time, 2>();
if (optional_time.tag() == parse_optional_time_time) flags.has_time_prefix = true;
return this->populate_job_process( return this->populate_job_process(
job, proc, not_statement.require_get_child<g::statement, 2>(), job, proc, not_statement.require_get_child<g::statement, 3>(),
not_statement.require_get_child<g::variable_assignments, 1>()); not_statement.require_get_child<g::variable_assignments, 1>());
} }
@ -1111,18 +1114,20 @@ eval_result_t parse_execution_context_t::populate_job_from_job_node(
// We are going to construct process_t structures for every statement in the job. Get the first // We are going to construct process_t structures for every statement in the job. Get the first
// statement. // statement.
tnode_t<g::statement> statement = job_node.child<1>(); tnode_t<g::optional_time> optional_time = job_node.child<0>();
tnode_t<g::variable_assignments> variable_assignments = job_node.child<0>(); tnode_t<g::variable_assignments> variable_assignments = job_node.child<1>();
tnode_t<g::statement> statement = job_node.child<2>();
// Create processes. Each one may fail. // Create processes. Each one may fail.
process_list_t processes; process_list_t processes;
processes.emplace_back(new process_t()); processes.emplace_back(new process_t());
if (optional_time.tag() == parse_optional_time_time) j->mut_flags().has_time_prefix = true;
eval_result_t result = eval_result_t result =
this->populate_job_process(j, processes.back().get(), statement, variable_assignments); this->populate_job_process(j, processes.back().get(), statement, variable_assignments);
// Construct process_ts for job continuations (pipelines), by walking the list until we hit the // Construct process_ts for job continuations (pipelines), by walking the list until we hit the
// terminal (empty) job continuation. // terminal (empty) job continuation.
tnode_t<g::job_continuation> job_cont = job_node.child<2>(); tnode_t<g::job_continuation> job_cont = job_node.child<3>();
assert(job_cont); assert(job_cont);
while (auto pipe = job_cont.try_get_child<g::tok_pipe, 0>()) { while (auto pipe = job_cont.try_get_child<g::tok_pipe, 0>()) {
if (result != eval_result_t::ok) { if (result != eval_result_t::ok) {
@ -1212,7 +1217,9 @@ eval_result_t parse_execution_context_t::run_1_job(tnode_t<g::job> job_node,
// However, if there are no redirections, then we can just jump into the block directly, which // However, if there are no redirections, then we can just jump into the block directly, which
// is significantly faster. // is significantly faster.
if (job_is_simple_block(job_node)) { if (job_is_simple_block(job_node)) {
tnode_t<g::variable_assignments> variable_assignments = job_node.child<0>(); tnode_t<g::optional_time> optional_time = job_node.child<0>();
cleanup_t timer = push_timer(optional_time.tag() == parse_optional_time_time);
tnode_t<g::variable_assignments> variable_assignments = job_node.child<1>();
const block_t *block = nullptr; const block_t *block = nullptr;
eval_result_t result = eval_result_t result =
this->apply_variable_assignments(nullptr, variable_assignments, &block); this->apply_variable_assignments(nullptr, variable_assignments, &block);
@ -1220,7 +1227,7 @@ eval_result_t parse_execution_context_t::run_1_job(tnode_t<g::job> job_node,
if (block) parser->pop_block(block); if (block) parser->pop_block(block);
}); });
tnode_t<g::statement> statement = job_node.child<1>(); tnode_t<g::statement> statement = job_node.child<2>();
const parse_node_t &specific_statement = statement.get_child_node<0>(); const parse_node_t &specific_statement = statement.get_child_node<0>();
assert(specific_statement_type_is_redirectable_block(specific_statement)); assert(specific_statement_type_is_redirectable_block(specific_statement));
if (result == eval_result_t::ok) { if (result == eval_result_t::ok) {
@ -1389,7 +1396,6 @@ eval_result_t parse_execution_context_t::run_job_list(tnode_t<Type> job_list,
static_assert(Type::token == symbol_job_list || Type::token == symbol_andor_job_list, static_assert(Type::token == symbol_job_list || Type::token == symbol_andor_job_list,
"Not a job list"); "Not a job list");
static std::vector<timer_snapshot_t> active_timers;
eval_result_t result = eval_result_t::ok; eval_result_t result = eval_result_t::ok;
while (auto job_conj = job_list.template next_in_list<g::job_conjunction>()) { while (auto job_conj = job_list.template next_in_list<g::job_conjunction>()) {
if (auto reason = check_end_execution()) { if (auto reason = check_end_execution()) {
@ -1399,26 +1405,11 @@ eval_result_t parse_execution_context_t::run_job_list(tnode_t<Type> job_list,
// Maybe skip the job if it has a leading and/or. // Maybe skip the job if it has a leading and/or.
// Skipping is treated as success. // Skipping is treated as success.
bool timer_started = false;
if (get_decorator(job_conj) == parse_job_decoration_time) {
active_timers.emplace_back(timer_snapshot_t::take());
timer_started = true;
}
if (should_skip(get_decorator(job_conj))) { if (should_skip(get_decorator(job_conj))) {
result = eval_result_t::ok; result = eval_result_t::ok;
} else { } else {
result = this->run_job_conjunction(job_conj, associated_block); result = this->run_job_conjunction(job_conj, associated_block);
} }
if (timer_started) {
auto t1 = active_timers.back();
active_timers.pop_back();
auto t2 = timer_snapshot_t::take();
// Well, this is awkward. By defining `time` as a decorator and not a built-in, there's
// no associated stream for its output!
auto output = timer_snapshot_t::print_delta(t1, t2, true);
std::fwprintf(stderr, L"%S\n", output.c_str());
}
} }
// Returns the result of the last job executed or skipped. // Returns the result of the last job executed or skipped.

View file

@ -210,7 +210,6 @@ DEF_ALT(job_list) {
DEF_ALT(job_decorator) { DEF_ALT(job_decorator) {
using ands = single<keyword<parse_keyword_and>>; using ands = single<keyword<parse_keyword_and>>;
using ors = single<keyword<parse_keyword_or>>; using ors = single<keyword<parse_keyword_or>>;
using times = single<keyword<parse_keyword_time>>;
using empty = grammar::empty; using empty = grammar::empty;
ALT_BODY(job_decorator, ands, ors, empty); ALT_BODY(job_decorator, ands, ors, empty);
}; };
@ -225,13 +224,20 @@ DEF_ALT(job_conjunction_continuation) {
ALT_BODY(job_conjunction_continuation, andands, orors, empty); ALT_BODY(job_conjunction_continuation, andands, orors, empty);
}; };
/// The time builtin.
DEF_ALT(optional_time) {
using empty = grammar::empty;
using time = single<keyword<parse_keyword_time>>;
ALT_BODY(optional_time, empty, time);
};
// A job is a non-empty list of statements, separated by pipes. (Non-empty is useful for cases // A job is a non-empty list of statements, separated by pipes. (Non-empty is useful for cases
// like if statements, where we require a command). To represent "non-empty", we require a // like if statements, where we require a command). To represent "non-empty", we require a
// statement, followed by a possibly empty job_continuation, and then optionally a background // statement, followed by a possibly empty job_continuation, and then optionally a background
// specifier '&' // specifier '&'
DEF(job) DEF(job)
produces_sequence<variable_assignments, statement, job_continuation, optional_background>{ produces_sequence<optional_time, variable_assignments, statement, job_continuation,
BODY(job)}; optional_background>{BODY(job)};
DEF_ALT(job_continuation) { DEF_ALT(job_continuation) {
using piped = using piped =
@ -322,8 +328,9 @@ produces_sequence<keyword<parse_keyword_function>, argument, argument_list, tok_
BODY(function_header)}; BODY(function_header)};
DEF_ALT(not_statement) { DEF_ALT(not_statement) {
using nots = seq<keyword<parse_keyword_not>, variable_assignments, statement>; using nots = seq<keyword<parse_keyword_not>, variable_assignments, optional_time, statement>;
using exclams = seq<keyword<parse_keyword_exclam>, variable_assignments, statement>; using exclams =
seq<keyword<parse_keyword_exclam>, variable_assignments, optional_time, statement>;
ALT_BODY(not_statement, nots, exclams); ALT_BODY(not_statement, nots, exclams);
}; };

View file

@ -31,6 +31,7 @@ ELEM(argument)
ELEM(redirection) ELEM(redirection)
ELEM(optional_background) ELEM(optional_background)
ELEM(optional_newlines) ELEM(optional_newlines)
ELEM(optional_time)
ELEM(end_command) ELEM(end_command)
ELEM(freestanding_argument_list) ELEM(freestanding_argument_list)
#undef ELEM #undef ELEM

View file

@ -82,10 +82,6 @@ RESOLVE(job_decorator) {
*out_tag = parse_job_decoration_or; *out_tag = parse_job_decoration_or;
return production_for<ors>(); return production_for<ors>();
} }
case parse_keyword_time: {
*out_tag = parse_job_decoration_time;
return production_for<times>();
}
default: { default: {
*out_tag = parse_job_decoration_none; *out_tag = parse_job_decoration_none;
return production_for<empty>(); return production_for<empty>();
@ -400,6 +396,19 @@ RESOLVE(optional_background) {
} }
} }
RESOLVE(optional_time) {
UNUSED(token2);
switch (token1.keyword) {
case parse_keyword_time:
*out_tag = parse_optional_time_time;
return production_for<time>();
default:
*out_tag = parse_optional_time_no_time;
return production_for<empty>();
}
}
const production_element_t *parse_productions::production_for_token(parse_token_type_t node_type, const production_element_t *parse_productions::production_for_token(parse_token_type_t node_type,
const parse_token_t &input1, const parse_token_t &input1,
const parse_token_t &input2, const parse_token_t &input2,

View file

@ -955,7 +955,7 @@ void parse_ll_t::accept_tokens(parse_token_t token1, parse_token_t token2) {
case symbol_job: case symbol_job:
variable_assignments = variable_assignments =
tnode_t<grammar::job>(&nodes, parent) tnode_t<grammar::job>(&nodes, parent)
.try_get_child<grammar::variable_assignments, 0>(); .try_get_child<grammar::variable_assignments, 1>();
break; break;
case symbol_job_continuation: case symbol_job_continuation:
variable_assignments = variable_assignments =

View file

@ -421,6 +421,9 @@ class job_t {
/// This job is disowned, and should be removed from the active jobs list. /// This job is disowned, and should be removed from the active jobs list.
bool disown_requested{false}; bool disown_requested{false};
/// Whether to print timing for this job.
bool has_time_prefix{false};
} job_flags{}; } job_flags{};
/// Access the job flags. /// Access the job flags.

View file

@ -195,3 +195,22 @@ wcstring timer_snapshot_t::print_delta(timer_snapshot_t t1, timer_snapshot_t t2,
return output; return output;
}; };
static std::vector<timer_snapshot_t> active_timers;
static void pop_timer() {
auto t1 = std::move(active_timers.back());
active_timers.pop_back();
auto t2 = timer_snapshot_t::take();
// Well, this is awkward. By defining `time` as a decorator and not a built-in, there's
// no associated stream for its output!
auto output = timer_snapshot_t::print_delta(std::move(t1), std::move(t2), true);
std::fwprintf(stderr, L"%S\n", output.c_str());
}
cleanup_t push_timer(bool enabled) {
if (!enabled) return {[]() {}};
active_timers.emplace_back(timer_snapshot_t::take());
return {[]() { pop_timer(); }};
}

View file

@ -13,6 +13,8 @@
class parser_t; class parser_t;
struct io_streams_t; struct io_streams_t;
cleanup_t push_timer(bool enabled);
struct timer_snapshot_t { struct timer_snapshot_t {
public: public:
struct rusage cpu_fish; struct rusage cpu_fish;

View file

@ -114,7 +114,7 @@ arguments_node_list_t get_argument_nodes(tnode_t<grammar::arguments_or_redirecti
} }
bool job_node_is_background(tnode_t<grammar::job> job) { bool job_node_is_background(tnode_t<grammar::job> job) {
tnode_t<grammar::optional_background> bg = job.child<3>(); tnode_t<grammar::optional_background> bg = job.child<4>();
return bg.tag() == parse_background; return bg.tag() == parse_background;
} }
@ -144,7 +144,7 @@ pipeline_position_t get_pipeline_position(tnode_t<grammar::statement> st) {
// Check if we're the beginning of a job, and if so, whether that job // Check if we're the beginning of a job, and if so, whether that job
// has a non-empty continuation. // has a non-empty continuation.
tnode_t<job_continuation> jc = st.try_get_parent<job>().child<2>(); tnode_t<job_continuation> jc = st.try_get_parent<job>().child<3>();
if (jc.try_get_child<statement, 3>()) { if (jc.try_get_child<statement, 3>()) {
return pipeline_position_t::first; return pipeline_position_t::first;
} }

View file

@ -25,3 +25,21 @@ time echo 'foo -s bar'
#CHECKERR: Executed in {{[\d,.\s]*}} {{millis|micros|secs}} {{\s*}}fish {{\s*}}external #CHECKERR: Executed in {{[\d,.\s]*}} {{millis|micros|secs}} {{\s*}}fish {{\s*}}external
#CHECKERR: usr time {{[\d,.\s]*}} {{millis|micros|secs}} {{[\d,.\s]*}} {{millis|micros|secs}} {{[\d,.\s]*}} {{millis|micros|secs}} #CHECKERR: usr time {{[\d,.\s]*}} {{millis|micros|secs}} {{[\d,.\s]*}} {{millis|micros|secs}} {{[\d,.\s]*}} {{millis|micros|secs}}
#CHECKERR: sys time {{[\d,.\s]*}} {{millis|micros|secs}} {{[\d,.\s]*}} {{millis|micros|secs}} {{[\d,.\s]*}} {{millis|micros|secs}} #CHECKERR: sys time {{[\d,.\s]*}} {{millis|micros|secs}} {{[\d,.\s]*}} {{millis|micros|secs}} {{[\d,.\s]*}} {{millis|micros|secs}}
true && time a=b not builtin true | true
#CHECKERR: ___{{.*}}
#CHECKERR: {{.*}}
#CHECKERR: {{.*}}
#CHECKERR: {{.*}}
PATH= time true
#CHECKERR: fish: Unknown command: time
#CHECKERR: {{.*}}
#CHECKERR: PATH= time true
#CHECKERR: ^
not time true
#CHECKERR: ___{{.*}}
#CHECKERR: {{.*}}
#CHECKERR: {{.*}}
#CHECKERR: {{.*}}