13

Is it possible to forcibly add a timing alias (for lack of a better way to phrase it) to every command in bash?

For example, I would like to have a specific user who, whenever a command is run, it is always wrapped either with date before and after, or time.

Is this possible, and, if so, how?

warren
  • 1,848
  • I looked before, and couldn't find a way to do exactly this. As Caleb says, you can use preexec, but you don't want to run it inside the preexec (e.g. preexec() { time $1; }), because the shell still runs it after preexec returns. So the best we can do is something similar. – Mikel Apr 26 '11 at 23:35
  • @Mikel I think you could use the preexec() function to actually wrapper whatever was executing by fetching the command, running it yourself from inside the function, then returning some sort of error so that the shell doesn't go on to execute the command itself. – Caleb Sep 11 '12 at 22:02

6 Answers6

10

You can record the time a command line is started and the time a prompt is displayed. Bash already keeps track of the starting date of each command line in its history, and you can note the time when you display the next prompt.

print_command_wall_clock_time () {
  echo Wall clock time: \
    $(($(date +%s) - $(HISTTIMEFORMAT="%s ";
                       set -o noglob;
                       set $(history 1); echo $2)))
}
PROMPT_COMMAND=print_command_wall_clock_time$'\n'"$PROMPT_COMMAND"

This only gives you second resolution, and only the wall clock time. If you want better resolution, you need to use an external date command that supports the %N format for nanoseconds, and the DEBUG trap to call date before running the command to time.

call_date_before_command () {
  date_before=$(date +%s.%N)
}
print_wall_clock_time () {
  echo "Wall clock time: $(date +"%s.%N - $date_before" | bc)"
}
trap call_date_before_command DEBUG
PROMPT_COMMAND=print_wall_clock_time

Even with the DEBUG trap, I don't think there's a way of automatically displaying processor times for each command, or being more discriminating than prompt to prompt.


If you're willing to use a different shell, here's how to get a time report for every command in zsh (this doesn't generalize to other tasks):

REPORTTIME=0

You can set REPORTTIME to any integer value, the timing information will only be displayed for commands that used more than this many seconds of processor time.

Zsh took this feature from csh where the variable is called time.

wjandrea
  • 658
  • The second snippet doesn't seem to work. Firstly there are three typos, which I've submitted an edit for, but the main problem is that the DEBUG trap seems to get called before the PROMPT_COMMAND as well, meaning the time gets reset. Maybe that didn't happen in older versions of Bash, idk. – wjandrea Aug 29 '20 at 18:18
  • The history method doesn't work when you have HISTCONTROL=ignoredups enabled and run the same command twice in a row. – wjandrea Aug 29 '20 at 20:11
3

Your options here are going to depend on your shell. In zsh there a convenient hook function called preexec() that is run right before any interactive shell commands. By creating a function with this name, you can cause things to be executed. You can also follow up with a function called precmd() which will run just before the next prompt is drawn, which will be right after your command finishes.

By creating this pair of functions, you can have whatever arbitrary commands you want run before and after whatever commands are issued at the prompt. You could use this to log shell usage, create locks, test the environment, or as in your example calculate time or resources spent while a command runs.

In this example, we will create ourselves a benchmark timestamp before running a command using preexec() then calculate the time spent executing the command using precmd() and output it before the prompt or log it away. Example:

preexec() {
   CMDSTART=$(date +%s%N)
}
precmd() {
   CMDRUNTIME=$(($(date +%s%N)-$CMDSTART))
   echo "Last command ran for $CMDRUNTIME nanoseconds."
}

Note: For this particular example, there is an even easier builtin function. All you have to do is turn on runtime reporting in ZSH and it will do this automatically.

$ export REPORTTIME=0
$ ls -d
./
ls -BF --color=auto -d  0.00s user 0.00s system 0% cpu 0.002 total

In a more practical implementation of preexec(), I use it see if the shell is running inside tmux or screen and, if so, to send information about the currently running command upstream to be displayed in the tab name.

Unfortunately in bash this little mechanism doesn't exist. Here is one man's attempt to replicate it. Also see Gilles's answer for similar nifty little hack.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
Caleb
  • 70,105
  • 1
  • See also http://unix.stackexchange.com/questions/8607/how-can-i-play-a-sound-when-script-execution-is-ready/8611#8611 – Mikel Apr 26 '11 at 23:30
  • wish this was available in bash! – warren Apr 27 '11 at 19:46
  • See Gilles's and others links, it is implementable in bash with a little extra fiddling. The again, why don't you just run zsh? It's a rocking shell with more good reasons to switch than just this! – Caleb Apr 27 '11 at 19:48
  • 2
    If you're using zsh there is an even better way to do it. The REPORTTIME environment variable when set will output the execution time info (as if you had run the command with 'time' in front of it) for any command taking longer than $REPORTTIME seconds. Just set it to 0 and it should tell you the time for every command, with the user/sys breakdown to boot. – Joseph Garvin Apr 27 '11 at 23:54
  • @Joseph: Wow where did you pull that out of? If I had asked that question that would have been THE answer I was looking for. It might be worth adding as a separate answer even though it is zsh because I don't see another thread covering these issues and some people looking for the functionality might be able to switch shells even if this poster cannot. – Caleb Apr 28 '11 at 07:00
1

The easiest way would probably to set PROMPT_COMMAND. See Bash Variables:

PROMPT_COMMAND
If set, the value is interpreted as a command to execute before the printing of each primary prompt ($PS1).

For example, to avoid overwriting any existing prompt command, you could do:

PROMPT_COMMAND="date ; ${PROMPT_COMMAND}"
cjm
  • 27,160
  • did not know about that one - looks like a good start, @cjm – warren Apr 26 '11 at 18:12
  • 2
    It's a start, but doesn't address the problem of knowing when a command was run. The prompt itself might be drawn minutes or hours or days before a command was typed and run. – Caleb Apr 26 '11 at 23:17
1

csh/tcsh has the best support for this feature (and has always had it).

  The `time` shell variable can be set to execute the time builtin  command
  after the completion of any process that takes more than a given number
  of CPU seconds.

In other words, set time=1 will print out the time consumed (system, user, elapsed) by any command that took more than 1 second of cpu time. Plain set time will enable printing out the time for all commands.

alexis
  • 5,759
1

As pointed out by wjandrea, Gilles’s answer doesn’t work,

  • partly because of cosmetic reasons, which wjandrea has fixed, and
  • partly because the DEBUG trap command gets run immediately before the PROMPT_COMMAND command gets run, so you lose the start time of the user-typed command whose elapsed time you are trying to measure.

Luckily, the DEBUG trap command can tell when this is happening and can guard against it.  So, building on Gilles’s answer (combining it with pieces of the DNA of my very similar answer here), we can construct

call_date_before_command () {
  if [ "$BASH_COMMAND" != "print_wall_clock_time" ]
  then
        command_flag=1
        date_before=$(date +%s.%N)
  fi
}
print_wall_clock_time () {
  if [ "$command_flag" ]
  then
        echo "Wall clock time: $(date +"%s.%N - $date_before" | bc)"
  fi
  command_flag=
}
trap call_date_before_command DEBUG
PROMPT_COMMAND=print_wall_clock_time

where call_date_before_command doesn’t do anything if it’s being called just before print_wall_clock_time gets run, and print_wall_clock_time reports information only if the user has actually run a command (and not just pressed Enter).

You might want to rename call_date_before_command, command_flag and print_wall_clock_time to something like Twas_brillig_and_the_slithy_toves, Did_gyre_and_gimble_in_the_wabe and All_mimsy_were_the_borogoves, to reduce the chance that you will inadvertently redefine one of the interactively.

  • Instead of random names, just add a leading underscore: _call_date_before_command, etc. BTW nice answer! I posted my own fixing Gilles's other method too. – wjandrea Aug 29 '20 at 22:05
1

This is based on print_command_wall_clock_time from Gilles's answer, but changed to accommodate duplicate commands, and times when you don't actually run a new command, just press enter on the command line. It also saves the timings in an array. And I also cleaned up the method of getting info from history.

# don't put lines starting with space in the history.
HISTCONTROL=ignorespace

_last_command_time () { # Get the real time the last command took to run, in seconds. # # Must be set up in PROMPT_COMMAND to get the correct end time. # # Uses "history" to get the start time, so won't work properly for # duplicate commands if you have "HISTCONTROL=ignoredups" enabled. # # Records times by history number in global array "COMMAND_TIMES".

local n prev

# Get command number and start time in seconds
read -r n prev _ <<< "$(HISTTIMEFORMAT="%s "; history 1)"

if ! [[ ${COMMAND_TIMES[$n]} ]]; then  # If it's not already recorded
    COMMAND_TIMES+=( [$n]=$(( $(date +%s) - prev )) )
fi

printf 'Last command time: %s\n' "${COMMAND_TIMES[$n]}"

}

PROMPT_COMMAND=_last_command_time

wjandrea
  • 658
  • (1) An earlier draft of this comment began “Today I learned that Bash version 4.1 doesn’t support %s in HISTTIMEFORMAT.  But I dug a little deeper, and discovered that that’s not quite right.  My five-year-old installation of Cygwin came with a version of strftime that doesn’t support %s; therefore, it doesn’t work in Bash.  (Although, curiously, date +%s does work.)  The good news is that your answer doesn’t care.  The bad news is that your answer doesn’t notice, and reports things like “Last command time: 1598908445”. … (Cont’d) – Scott - Слава Україні Sep 01 '20 at 23:18
  • (Cont’d) …  Actually, typing a number as a command (e.g., 500000000 -R) interferes amusingly with your code, and some other things (e.g., a command that begins with ., -, * or /, or that contains quoted character(s)) cause it to throw errors. (2) You say that your answer doesn’t issue reports for empty commands, but when I hit Enter (in another system, where %s works), I get “Last command time: nn”, where nn repeats the value from the last non-blank command. – Scott - Слава Україні Sep 01 '20 at 23:18
  • @Scott (2) No, I said it accommodates empty commands, meaning it keeps printing the previous value, instead of calculating the time again, incorrectly, like Gilles's method does. I wanted it like that so my PS1 can print the current command number and previous time together. But you could easily modify it to not print the time if it's already in the array. Just move the printf inside the if block. – wjandrea Sep 02 '20 at 15:23
  • @Scott (1) I can't reproduce the behaviour with numbers or special characters on Bash 4.4 on WSL Ubuntu 18.04. And of course, I also can't reproduce the behaviour with %s, but it turns out C89 doesn't provide %s, so I guess you can't rely on it. I'm not sure how to fix that. Use GNU strftime I guess? – wjandrea Sep 02 '20 at 15:37