Add fish_job_summary, called whenever a job ends, stops, or is signalled

This allows users to customise the behaviour of the shell by redefining the function. This is similar to how fish_title or fish_greeting behave, where the default implementation can be easily overridden.

The function receives as arguments the job id, command line, signal name and signal description.
This commit is contained in:
Soumya 2020-04-29 21:14:12 -07:00 committed by Johannes Altmanninger
parent e3c4692031
commit 324fa64114
6 changed files with 114 additions and 53 deletions

View File

@ -11,6 +11,7 @@
- A new variable, `$fish_vi_force_cursor`, has been added. This can be set to force `fish_vi_cursor` to attempt changing the cursor shape in vi mode, regardless of terminal. Additionally, the `fish_vi_cursor` option `--force-iterm` has been deprecated; all usages can be replaced by setting `$fish_vi_force_cursor`.
- The history file is now created with user-private permissions, matching other shells (#6926). The directory containing the history file remains private, so there should not have been any private date revealed.
- fish no longer disables flow control after every command. Enterprising users can now enable it for external commands with `stty`. (#2315)
- Added a `fish_job_summary` function which is called whenever a background job stops or ends, or any job terminates from a signal. The default behaviour can now be customized by redefining this function.
### Syntax changes and new commands

View File

@ -0,0 +1,38 @@
function fish_job_summary -a job_id cmd_line signal_or_end_name signal_desc proc_pid proc_name
# job_id: ID of the job that stopped/terminated/ended.
# cmd_line: The command line of the job.
# signal_or_end_name: If terminated by signal, the name of the signal (e.g. SIGTERM).
# If ended, the string "ENDED". If stopped, the string "STOPPED".
# signal_desc: A description of the signal (e.g. "Polite quite request").
# Not provided if the job stopped or ended without a signal.
# If the job has more than one process:
# proc_pid: the pid of the process affected.
# proc_name: the name of that process.
# If the job has only one process, these two arguments will not be provided.
set -l ellipsis '...'
if string match -iqr 'utf.?8' -- $LANG
set ellipsis \u2026
end
set -l max_cmd_len 32
if test (string length $cmd_line) -gt $max_cmd_len
set -l truncated_len (math $max_cmd_len - (string length $ellipsis))
set cmd_line (string trim (string sub -l $truncated_len $cmd_line))$ellipsis
end
switch $signal_or_end_name
case "STOPPED"
printf ( _ "fish: Job %s, '%s' has stopped\n" ) $job_id $cmd_line
case "ENDED"
printf ( _ "fish: Job %s, '%s' has ended\n" ) $job_id $cmd_line
case 'SIG*'
if test -n "$proc_pid"
printf ( _ "fish: Process %s, '%s' from job %s, '%s' terminated by signal %s (%s)\n" ) \
$proc_pid $proc_name $job_id $cmd_line $signal_or_end_name $signal_desc
else
printf ( _ "fish: Job %s, '%s' terminated by signal %s (%s)\n" ) \
$job_id $cmd_line $signal_or_end_name $signal_desc
end
end >&2
end

View File

@ -453,40 +453,34 @@ static void process_mark_finished_children(parser_t &parser, bool block_ok) {
reap_disowned_pids();
}
/// Given a command like "cat file", truncate it to a reasonable length.
static wcstring truncate_command(const wcstring &cmd) {
const size_t max_len = 32;
if (cmd.size() <= max_len) {
// No truncation necessary.
return cmd;
/// Call the fish_job_summary function with the given args.
static void print_job_summary(parser_t &parser, const wcstring_list_t &args) {
wcstring buffer = wcstring(L"fish_job_summary");
for (const wcstring &arg : args) {
buffer.push_back(L' ');
buffer.append(escape_string(arg, ESCAPE_ALL));
}
event_t event(event_type_t::generic);
event.desc.str_param1 = L"fish_job_summary";
// Truncation required.
const wchar_t *ellipsis_str = get_ellipsis_str();
const size_t ellipsis_length = std::wcslen(ellipsis_str); // no need for wcwidth
size_t trunc_length = max_len - ellipsis_length;
// Eat trailing whitespace.
while (trunc_length > 0 && iswspace(cmd.at(trunc_length - 1))) {
trunc_length -= 1;
}
wcstring result = wcstring(cmd, 0, trunc_length);
// Append ellipsis.
result.append(ellipsis_str);
return result;
auto prev_statuses = parser.get_last_statuses();
block_t *b = parser.push_block(block_t::event_block(event));
parser.eval(buffer, io_chain_t());
parser.pop_block(b);
parser.set_last_statuses(std::move(prev_statuses));
}
/// Format information about job status for the user to look at.
using job_status_t = enum { JOB_STOPPED, JOB_ENDED };
static void print_job_status(const job_t *j, job_status_t status) {
const wchar_t *msg = L"Job %d, '%ls' has ended"; // this is the most common status msg
if (status == JOB_STOPPED) msg = L"Job %d, '%ls' has stopped";
outputter_t outp;
outp.writestr("\r");
outp.writestr(format_string(_(msg), j->job_id(), truncate_command(j->command()).c_str()));
if (clr_eol) outp.term_puts(clr_eol, 1);
outp.writestr(L"\n");
fflush(stdout);
outp.flush_to(STDOUT_FILENO);
static void print_job_status(parser_t &parser, const job_t *j, job_status_t status) {
wcstring_list_t args = {
format_string(L"%d", j->job_id()),
j->command(),
status == JOB_STOPPED ? L"STOPPED" : L"ENDED",
};
print_job_summary(parser, args);
}
event_t proc_create_event(const wchar_t *msg, event_type_t type, pid_t pid, int status) {
@ -517,8 +511,8 @@ void remove_disowned_jobs(job_list_t &jobs) {
/// Given a a process in a job, print the status message for the process as appropriate, and then
/// mark the status code so we don't print again. Populate any events into \p exit_events.
/// \return true if we printed a status message, false if not.
static bool try_clean_process_in_job(process_t *p, job_t *j, std::vector<event_t> *exit_events,
bool only_one_job) {
static bool try_clean_process_in_job(parser_t &parser, process_t *p, job_t *j,
std::vector<event_t> *exit_events) {
if (!p->completed || !p->pid) {
return false;
}
@ -556,27 +550,17 @@ static bool try_clean_process_in_job(process_t *p, job_t *j, std::vector<event_t
// preference.
bool printed = false;
if (s.signal_code() != SIGINT || !j->is_foreground()) {
if (proc_is_job) {
// We want to report the job number, unless it's the only job, in which case
// we don't need to.
const wcstring job_number_desc =
only_one_job ? wcstring() : format_string(_(L"Job %d, "), j->job_id());
std::fwprintf(stdout, _(L"%ls: %ls\'%ls\' terminated by signal %ls (%ls)"),
program_name, job_number_desc.c_str(),
truncate_command(j->command()).c_str(), sig2wcs(s.signal_code()),
signal_get_desc(s.signal_code()));
} else {
const wcstring job_number_desc =
only_one_job ? wcstring() : format_string(L"from job %d, ", j->job_id());
const wchar_t *fmt =
_(L"%ls: Process %d, \'%ls\' %ls\'%ls\' terminated by signal %ls (%ls)");
std::fwprintf(stdout, fmt, program_name, p->pid, p->argv0(), job_number_desc.c_str(),
truncate_command(j->command()).c_str(), sig2wcs(s.signal_code()),
signal_get_desc(s.signal_code()));
wcstring_list_t args;
args.reserve(proc_is_job ? 4 : 6);
args.push_back(format_string(L"%d", j->job_id()));
args.push_back(j->command());
args.push_back(sig2wcs(s.signal_code()));
args.push_back(signal_get_desc(s.signal_code()));
if (!proc_is_job) {
args.push_back(format_string(L"%d", p->pid));
args.push_back(p->argv0());
}
if (clr_eol) outputter_t::stdoutput().term_puts(clr_eol, 1);
std::fwprintf(stdout, L"\n");
print_job_summary(parser, args);
printed = true;
}
// Clear status so it is not reported more than once.
@ -635,7 +619,6 @@ static bool process_clean_after_marking(parser_t &parser, bool allow_interactive
};
// Print status messages for completed or stopped jobs.
const bool only_one_job = parser.jobs().size() == 1;
for (const auto &j : parser.jobs()) {
if (!should_process_job(j)) continue;
@ -643,14 +626,14 @@ static bool process_clean_after_marking(parser_t &parser, bool allow_interactive
// Note this may print the message on behalf of the job, affecting the result of
// job_wants_message().
for (process_ptr_t &p : j->processes) {
if (try_clean_process_in_job(p.get(), j.get(), &exit_events, only_one_job)) {
if (try_clean_process_in_job(parser, p.get(), j.get(), &exit_events)) {
printed = true;
}
}
// Print the message if we need to.
if (job_wants_message(j) && (j->is_completed() || j->is_stopped())) {
print_job_status(j.get(), j->is_completed() ? JOB_ENDED : JOB_STOPPED);
print_job_status(parser, j.get(), j->is_completed() ? JOB_ENDED : JOB_STOPPED);
j->mut_flags().notified = true;
printed = true;
}

39
tests/job_summary.expect Normal file
View File

@ -0,0 +1,39 @@
# vim: set filetype=expect:
#
# Test job summary for interactive shells.
set pid [spawn $fish]
expect_prompt
send_line "function fish_job_summary; string join ':' \$argv; end"
expect_prompt
# fish_job_summary is called when background job ends.
send_line "sleep 0.5 &"
sleep 0.050
expect_prompt
sleep 0.550
expect -re "\[0-9]+:sleep 0.5 &:ENDED"
send_line ""
expect_prompt
# fish_job_summary is called when background job is signalled.
# cmd_line correctly prints only the actually backgrounded job.
send_line "false; sleep 10 &; true"
sleep 0.100
expect_prompt
exec -- pkill -TERM sleep -P $pid
sleep 0.100
expect -re "\[0-9]+:sleep 10 &:SIGTERM:Polite quit request"
send_line ""
expect_prompt
# fish_job_summary is called when foreground job is signalled.
# cmd_line contains the entire pipeline. proc_id and proc_name are set in a pipeline.
send_line "true | sleep 6"
sleep 0.100
exec -- pkill -KILL sleep -P $pid
sleep 0.100
expect -re "\[0-9]+:true | sleep 6:SIGKILL:Forced quit:\[0-9]+:sleep"
send_line ""
expect_prompt

View File

View File