# avoid some accidental Ctrl-D hits IGNOREEOF=2 # warn about background jobs before exit shopt -s checkjobs # immediate reporting terminated background jobs (a DEBUG trap turns if off while a foreground program is running) set -b __bash_parse_jobs__() { # Parse bash jobs and store them in global variables (see below). # Outdated data are intentionally kept in some of them to able # to find finished job's PID etc. If there is not status in # JOBSTAT_BY_PID[pid], the job is finished. local job_ids=`LANG=C jobs | sed -ne 's/^\[\([0-9]\+\)\].*/\1/ p'` local job_id pid job jobspec job_cmd jobsymbol idx declare -g -A JOBSPEC_BY_PID=() declare -g -A JOBSTAT_BY_PID=() declare -g -A JOB_CMD_BY_PID_HIST declare -g -A JOBID_BY_PID_HIST declare -g -A JOB_PID_BY_JOBID=() declare -g -A JOB_PID_BY_JOBID_HIST declare -g -a JOB_CURRENT_PID_HIST declare -g -a JOB_PREVIOUS_PID_HIST for job_id in $job_ids do pid=`jobs -p "$job_id" 2>/dev/null` if [ -n "$pid" ] then job=`set -o posix; LANG=C jobs "$job_id"` if [[ $job =~ ^\[([^\]]+)\]([^\ ]*)[\ ]+([^\ ]+)[\ ]+(.*) ]] # FIXME: this regexp captures the command too early if the status string has space in it. # bash padds the status string to LONGEST_SIGNAL_DESC width, which is 24 in my case. then jobspec=${BASH_REMATCH[1]}${BASH_REMATCH[2]} jobsymbol=${BASH_REMATCH[2]} jobstat=${BASH_REMATCH[3]} job_cmd=${BASH_REMATCH[4]} JOBSPEC_BY_PID[$pid]=$jobspec JOBSTAT_BY_PID[$pid]=$jobstat JOB_CMD_BY_PID_HIST[$pid]=$job_cmd JOBID_BY_PID_HIST[$pid]=$job_id JOB_PID_BY_JOBID[$job_id]=$pid JOB_PID_BY_JOBID_HIST[$job_id]=$pid if [ -n "$jobsymbol" ] then JOB_PID_BY_JOBID[$jobsymbol]=$pid if [ "$jobsymbol" = + ] then bash_global_array_delete_item JOB_CURRENT_PID_HIST "$pid" JOB_CURRENT_PID_HIST+=("$pid") elif [ "$jobsymbol" = - ] then bash_global_array_delete_item JOB_PREVIOUS_PID_HIST "$pid" JOB_PREVIOUS_PID_HIST+=("$pid") fi fi fi fi done } advanced_shelljob_management_commands="suspendjob, continuejob, bgjob, fgjob, joboutput, lsjobs, forgetjob, jobstatus" __bash_canonical_jobspec__() { local jobspec=$1 jobspec=${jobspec##+(%)} jobspec=%${jobspec:-+} echo "$jobspec" } suspendjob() { if [ "$1" = -h -o "$1" == --help ] then echo "Usage: ${FUNCNAME[0]} [ [ ...]]" echo "Pause the shell job(s)." echo "See also: $advanced_shelljob_management_commands" return fi local jobspec for jobspec in "${@:-%+}" do kill -TSTP "$(__bash_canonical_jobspec__ "$jobspec")" done } continuejob() { if [ "$1" = -h -o "$1" == --help ] then echo "Usage: ${FUNCNAME[0]} [ [ ...]]" echo "Make the shell job(s) continue to run in the background." echo "See also: $advanced_shelljob_management_commands" return fi local jobspec for jobspec in "${@:-%+}" do kill -CONT "$(__bash_canonical_jobspec__ "$jobspec")" done } bgjob() { if [ "$1" = -h -o "$1" == --help ] then echo "Usage: ${FUNCNAME[0]} []" echo "Put the shell job into the background and redirects stdout and stderr into files." # TODO: save the exit code of bg jobs echo "See also: $advanced_shelljob_management_commands" return fi local jobspec=`__bash_canonical_jobspec__ "$1"` local pid=`jobs -p "$jobspec"` [ -z "$pid" ] && return -1 mkdir -p ~/.cache/bash-bgjob/ if [ -e ~/.cache/bash-bgjob/$pid.rst ] then echo "Process $pid is already in the background. Call 'forgetjob' if stale." >&2 return -1 fi reredirect -o ~/.cache/bash-bgjob/$pid.out -e ~/.cache/bash-bgjob/$pid.err $pid > ~/.cache/bash-bgjob/$pid.rst cat /proc/$pid/cmdline > ~/.cache/bash-bgjob/$pid.cmd kill -CONT "$jobspec" } fgjob() { if [ "$1" = -h -o "$1" == --help ] then echo "Usage: ${FUNCNAME[0]} []" echo "Bring the background shell job (see 'bgjob') back to the foreground." echo "See also: $advanced_shelljob_management_commands" return fi local jobspec=`__bash_canonical_jobspec__ "$1"` local pid=`jobs -p "$jobspec"` if [ -n "$pid" ] then if [ -e ~/.cache/bash-bgjob/$pid.rst ] then . ~/.cache/bash-bgjob/$pid.rst || return -1 rm ~/.cache/bash-bgjob/$pid.rst fi fi builtin fg "$jobspec" } alias fg=fgjob __bash_get_job_pid__() { # output PID of jobspec, or PID if jobspec is not given. # fail if both given. local pid=$1 local jobspec=$2 if [ -n "$pid" -a -n "$jobspec" ] then echo "Need to specify either PID or JOBSPEC, not both." >&2 return -1 fi if [ -z "$pid" ] then jobspec=`__bash_canonical_jobspec__ "$jobspec"` if [ "$jobspec" = '%+' ] && [ ${#JOB_CURRENT_PID_HIST[@]} != 0 ] then pid=${JOB_CURRENT_PID_HIST[-1]} elif [ "$jobspec" = '%-' ] && [ ${#JOB_PREVIOUS_PID_HIST[@]} != 0 ] then pid=${JOB_PREVIOUS_PID_HIST[-1]} else local job=${jobspec#%} pid=${JOB_PID_BY_JOBID_HIST[$job]} fi fi if [ -z "$pid" ] then echo "${FUNCNAME[1]}: no such job found" >&2 return -1 fi echo "$pid" } joboutput() { if [ "$1" = -h -o "$1" == --help ] then echo "Usage: ${FUNCNAME[0]} [-e|--error] [ | [-p|--pid] ]" echo "Print the given shell job's stdout (or stderr for '-e' option)." echo "Enter '--pid PID' instead of JOBSPEC if job ID is no longer available." echo "See also: $advanced_shelljob_management_commands" return fi local jobspec='' local pid='' local ext=out while [ $# -gt 0 ] do case "$1" in -e|--error) ext=err ;; -p|--pid) shift; pid=$1 ;; *) jobspec=$1 ;; esac shift done pid=`__bash_get_job_pid__ "$pid" "$jobspec"` || return $? cat ~/.cache/bash-bgjob/$pid.$ext } lsjobs() { local global='' local showcommands=1 while [ $# -gt 0 ] do case "$1" in -h|--help) echo "Usage: ${FUNCNAME[0]} [-g|--global] [-n|--no-command]" echo "Show current shell's background jobs." echo "Option '-g' shows all jobs put into the background by bgjobs in any shell session." echo "On option '-n', commands won't be shown." echo "In the JOB column in the output, the meanings of numbers, plus, and minus sign are the same as in 'jobs' command; an asterisk indicates that it's a job managed by bgjob, otherwise it's an ordinary shell job." echo "See also: $advanced_shelljob_management_commands" return;; -g|--global) global=1;; -n|--no-command) showcommands='';; *) echo "${FUNCNAME[0]}: unknown option: $1" >&2; return -1;; esac shift done # don't run 'jobs' here, leave it with the PROMPT_COMMAND and DEBUG trap # __bash_parse_jobs__ local cmdfile cmdline status jobspec job_id pid symbol echo "JOB PID STATUS${showcommands:+ COMMAND}" ( shopt -s nullglob declare -A shown_job_pid=() for cmdfile in ~/.cache/bash-bgjob/*.cmd do pid=`basename "$cmdfile" .cmd` cmdline='' if [ -n "${JOBSTAT_BY_PID[$pid]}" ] then # live job in this shell session jobspec=${JOBSPEC_BY_PID[$pid]} status=${JOBSTAT_BY_PID[$pid]} [ ! $showcommands ] || cmdline=${JOB_CMD_BY_PID_HIST[$pid]} else # not in this shell session or finished job job_id=${JOBID_BY_PID_HIST[$pid]} if [ -n "$job_id" ] then # finished or disowned job in this shell session if [ "${JOB_PID_BY_JOBID_HIST[$job_id]}" != $pid ] then # a live job or a finished (disowned) one is there under the same job id jobspec='_' else # it's a job id currently not taken by anyone else in this shell session jobspec=$job_id if [ ${#JOB_CURRENT_PID_HIST[@]} != 0 ] && [ "${JOB_CURRENT_PID_HIST[-1]}" = $pid ] then jobspec=$jobspec+ elif [ ${#JOB_PREVIOUS_PID_HIST[@]} != 0 ] && [ "${JOB_PREVIOUS_PID_HIST[-1]}" = $pid ] then jobspec=$jobspec- fi fi [ -d /proc/$pid ] && status=Disowned || status=Ended [ ! $showcommands ] || cmdline=${JOB_CMD_BY_PID_HIST[$pid]} else # foreign job if [ $global ] then jobspec='_' [ -d /proc/$pid ] && status='?' || status=Ended [ ! $showcommands ] || cmdline=`cat "$cmdfile" | tr "\000" " "` else pid='' fi fi fi if [ -n "$pid" ] then echo "$jobspec* $pid $status $cmdline" shown_job_pid[$pid]=1 fi done # enumerate jobs not managed by bgjob for pid in "${!JOBSPEC_BY_PID[@]}" do if [ ! ${shown_job_pid[$pid]} ] then jobspec=${JOBSPEC_BY_PID[$pid]} if [ $showcommands ] then cmdline=${JOB_CMD_BY_PID_HIST[$pid]//$'\n'/ } else cmdline='' fi status=${JOBSTAT_BY_PID[$pid]} echo "$jobspec $pid $status $cmdline" fi done ) } # set extglob shell option to parse '+(...))' expressions well in the following function # restore the original state after shopt_extglob=`shopt -p extglob` shopt -s extglob forgetjob() { if [ "$1" = -h -o "$1" == --help ] then echo "Usage: ${FUNCNAME[0]} [--all | --global | [ | -p [ | -p ...]] ]" echo "Remove data of shell job specified by JOBSPEC or py PID, %+ by default. Does not remove running process's data." echo "See also: $advanced_shelljob_management_commands" return fi local pid jobid idx local pids=() local count=0 local all='' global='' if [ $# = 0 ] then set %+ fi while [ $# -gt 0 ] do case "$1" in --all|-a) all=1;; --global|-g) global=1;; -p) shift pids+=("$1") ;; %+([0-9])|+([0-9])) jobid=${1#%} pid=${JOB_PID_BY_JOBID_HIST[$jobid]} if [ -n "$pid" ] then pids+=($pid) else echo "${FUNCNAME[0]}: job not found: $1" >&2 fi;; %+|+) if [ ${#JOB_CURRENT_PID_HIST[@]} != 0 ] then pids+=(${JOB_CURRENT_PID_HIST[-1]}) fi ;; %-|-) if [ ${#JOB_PREVIOUS_PID_HIST[@]} != 0 ] then pids+=(${JOB_PREVIOUS_PID_HIST[-1]}) fi ;; *) echo "${FUNCNAME[0]}: unknown argument: $1" >&2 return -1;; esac shift done if [ $all ] then pids+=(${!JOBID_BY_PID_HIST[@]}) elif [ $global ] then local file local shopt_nullglob=`shopt -p nullglob` shopt -s nullglob for file in ~/.cache/bash-bgjob/*.cmd do pids+=("$(basename "$file" .cmd)") done eval $shopt_nullglob fi for pid in "${pids[@]}" do if [ -d /proc/$pid ] then echo "Process $pid is running." >&2 else rm ~/.cache/bash-bgjob/$pid.{rst,out,err,cmd} # remove references to this job from helper arrays unset JOB_CMD_BY_PID_HIST[$pid] unset JOBID_BY_PID_HIST[$pid] bash_global_array_delete_item JOB_CURRENT_PID_HIST "$pid" bash_global_array_delete_item JOB_PREVIOUS_PID_HIST "$pid" count=$[count+1] echo "Forgot process $pid." >&2 fi done if [ $count = 0 ] then echo "${FUNCNAME[0]}: no job is forgotten" >&2 return 1 fi } jobstatus() { # this function utilizes JOB_PIPESTATUS array which is provided by a # special patch to bash 4.4 (git a0c0a00), which records all job's exit # status in this array: # diff --git a/jobs.c b/jobs.c # index cef3c79..6a1eb5c 100644 # --- a/jobs.c # +++ b/jobs.c # @@ -3619,9 +3619,45 @@ itrace("waitchld: waitpid returns %d block = %d children_exited = %d", pid, bloc # if (asynchronous_notification && interactive) # notify_of_job_status (); # # + remember_job_exit_status(); # + # return (children_exited); # } # # +void remember_job_exit_status() # +{ # + register int job; # + PROCESS *pipe; # + SHELL_VAR *v; # + ARRAY *a; # + ARRAY_ELEMENT *ae; # + register int i; # + char *t, tbuf[INT_STRLEN_BOUND(int) + 1]; # + # + v = find_variable ("JOB_PIPESTATUS"); # + if (v == 0) # + v = make_new_array_variable ("JOB_PIPESTATUS"); # + if (array_p (v) == 0) # + return; /* Do nothing if not an array variable. */ # + a = array_cell (v); # + # + for (job = 0; job < js.j_jobslots; job++) # + { # + if(jobs[job] && IS_FOREGROUND (job) == 0) # + { # + t = &tbuf; # + for(pipe = jobs[job]->pipe; ; pipe = pipe->next) # + { # + if(pipe->running){ t += sprintf(t, "-"); } # + else{ t += sprintf(t, "%d", WSTATUS(pipe->status)); } # + if(pipe->next == jobs[job]->pipe) break; # + t += sprintf(t, " "); # + } # + array_insert (a, jobs[job]->pipe->pid, tbuf); # + } # + } # +} # + # /* Set the status of JOB and perform any necessary cleanup if the job is # marked as JDEAD. if [ "$1" = -h -o "$1" == --help ] then echo "Usage: ${FUNCNAME[0]} [ | [-p|--pid] ]" echo "Print the exit codes for each process in the given job's pipeline." echo "A dash \"-\" indicates that the process has not finished yet." echo "Enter '--pid PID' instead of JOBSPEC if job ID is no longer available." echo "See also: $advanced_shelljob_management_commands" return fi local jobspec='' local pid='' while [ $# -gt 0 ] do case "$1" in -p|--pid) shift; pid=$1 ;; *) jobspec=$1 ;; esac shift done pid=`__bash_get_job_pid__ "$pid" "$jobspec"` || return $? declare -a codes=() local code for code in ${JOB_PIPESTATUS[$pid]} do [[ $code =~ ^[0-9] ]] && codes+=($[code >> 8]) || codes+=("$code") done bash_join ' ' "${codes[@]}" } # restore original extglob state eval $shopt_extglob unset shopt_extglob