Does bash have a hook that is run before executing a command?

113

65

In bash, can I arrange for a function to be executed just before running a command?

There is $PROMPT_COMMAND, which is executed before showing a prompt, i.e., just after running a command.

Bash's $PROMPT_COMMAND is analogous to zsh's precmd function; so what I'm looking for is a bash equivalent to zsh's preexec.

Example applications: set your terminal title to the command being executed; automatically add time before every command.

Gilles 'SO- stop being evil'

Posted 2010-08-14T11:09:20.117

Reputation: 58 319

5

bash version 4.4 has a PS0 variable that acts like PS1 but is used after reading the command but before executing it. See https://www.gnu.org/software/bash/manual/bashref.html#Bash-Variables

– glenn jackman – 2017-07-02T13:10:57.177

PS0 can be used to run a command like zsh's precmd as well - e.g. PS0='$(my_precmd)'. To make the prompt and command line a different color from the ouput (for example green=ansi code 32), turn on the green color in the prompt, PS1='\[\e[32m\] \$ ', and then turn it off just before the command executes with PS0='\[\e[0m\]' . – M.W. – 2019-11-22T23:01:42.773

Answers

94

Not natively, but it can be hacked up using the DEBUG trap. This code sets up preexec and precmd functions similar to zsh. The command line is passed as a single argument to preexec.

Here is a simplified version of the code to set up a precmd function that is executed before running each command.

preexec () { :; }
preexec_invoke_exec () {
    [ -n "$COMP_LINE" ] && return  # do nothing if completing
    [ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return # don't cause a preexec for $PROMPT_COMMAND
    local this_command=`HISTTIMEFORMAT= history 1 | sed -e "s/^[ ]*[0-9]*[ ]*//"`;
    preexec "$this_command"
}
trap 'preexec_invoke_exec' DEBUG

This trick is due to Glyph Lefkowitz; thanks to bcat for locating the original author.

Edit. An updated version of Glyph's hack can be found here: https://github.com/rcaloras/bash-preexec

Gilles 'SO- stop being evil'

Posted 2010-08-14T11:09:20.117

Reputation: 58 319

The "$BASH_COMMAND" = "$PROMPT_COMMAND" comparison isn't working for me http://i.imgur.com/blneCdQ.png

– laggingreflex – 2014-09-18T00:25:45.460

2I tried using this code on cygwin. Sadly it has quite intense performance effects there – running a simple benchmark command time for i in {1..10}; do true; done takes 0.040 seconds normally and 1.400 to 1.600 seconds after activating the DEBUG trap. It causes the trap command to be executed twice per loop – and on Cygwin the forking required for executing sed is prohibitively slow at roughly 0.030 seconds for forking alone (speed difference between echo builtin and /bin/echo). Something to keep in mind maybe. – kdb – 2016-05-24T12:15:20.307

2@kdb Cygwin performance for fork sucks. My understanding is that this is unavoidable on Windows. If you need to run bash code on Windows, try to cut down on forking. – Gilles 'SO- stop being evil' – 2016-05-24T12:20:04.187

@DevNull This can be very easily circumvented by removing the trap. There is no technical solution to people doing what they are allowed to do but shouldn't do. There are partial remedies: don't give as many people as much access, make sure your backups are up-to-date, use version control rather than direct file manipulation, … If you want something that users can't disable easily, let alone can't disable at all, then restrictions in the shell won't help you: they can be removed just as easily as they can be added. – Gilles 'SO- stop being evil' – 2016-08-24T06:48:50.297

@Glyph Can this be modified to not run the actual user command if it doesn't meet certain criteria (ie: block a command from running and print a warning if it contains foul language from a dictionary file)? – Cloud – 2016-09-02T23:59:04.720

1If you have more commands in a PROMPT_COMMAND variable (e.g. delimited by ;), you might need to use pattern matching in the second line of the preexec_invoke_exec function, just like this: [[ "$PROMPT_COMMAND" =~ "$BASH_COMMAND" ]]. This is because BASH_COMMAND represents each of the commands separately. – jirislav – 2019-04-19T19:23:04.470

this is great, any way to do something similar for functions though? – qodeninja – 2019-08-29T18:40:50.630

20

You can use the trap command (from help trap):

If a SIGNAL_SPEC is DEBUG, ARG is executed before every simple command.

For example, to change the terminal title dynamically you may use:

trap 'echo -e "\e]0;$BASH_COMMAND\007"' DEBUG

From this source.

cYrus

Posted 2010-08-14T11:09:20.117

Reputation: 18 102

1I used a combination of this answer with some of the special stuff in the accepted answer: trap '[ -n "$COMP_LINE" ] && [ "$BASH_COMMAND" != "$PROMPT_COMMAND" ] && date "+%X";echo -e "\e]0;$BASH_COMMAND\007"' DEBUG. This puts the command into the title and also prints the current time right before every command, but doesn't do so when executing $PROMPT_COMMAND. – coredumperror – 2014-09-16T22:50:25.103

1@CoreDumpError, since you've refactored the code you should negate all the conditions: the first one hence becomes: [ -z "$COMP_LINE" ]. – cYrus – 2014-09-17T08:58:22.817

@cYrus Thanks! I don't know nearly enough bash programming to have noticed that problem. – coredumperror – 2014-09-18T16:46:42.157

@LarsH: Which version do you have? I have BASH_VERSION="4.3.11(1)-release" and it says "ARG is executed before every simple command." – musiphil – 2014-11-20T18:21:33.187

@musiphil: I don't remember. It was a server that probably doesn't exist anymore, at an organization I no longer work for. – LarsH – 2014-11-20T20:47:04.920

1Interesting ... on my old Ubuntu server, help trap says "If a SIGNAL_SPEC is DEBUG, ARG is executed after every simple command" [emphasis mine]. – LarsH – 2013-04-30T21:26:34.440

12

It's not a shell function that gets executed, but I contributed a $PS0 prompt string that is displayed before each command is run. Details here: http://stromberg.dnsalias.org/~strombrg/PS0-prompt/

$PS0 is included in bash 4.4, though it'll take a while for most Linuxes to include 4.4 - you can build 4.4 yourself if you want though; in that case, you probably should put it under /usr/local, add it to /etc/shells and chsh to it. Then log out and back in, perhaps sshing to yourself@localhost or suing to yourself first as a test.

dstromberg

Posted 2010-08-14T11:09:20.117

Reputation: 111

11

I recently had to solve this exact problem for a side project of mine. I made a fairly robust and resilient solution that emulates zsh's preexec and precmd functionality for bash.

https://github.com/rcaloras/bash-preexec

It was originally based off Glyph Lefkowitz's solution, but I've improved on it and brought it up to date. Happy to help or add a feature if needed.

RCCola

Posted 2010-08-14T11:09:20.117

Reputation: 101

4

Thank you for the hints! I ended up using this:

#created by francois scheurer

#sourced by '~/.bashrc', which is the last runned startup script for bash invocation
#for login interactive, login non-interactive and non-login interactive shells.
#note that a user can easily avoid calling this file by using options like '--norc';
#he also can unset or overwrite variables like 'PROMPT_COMMAND'.
#therefore it is useful for audit but not for security.

#prompt & color
#http://www.pixelbeat.org/docs/terminal_colours/#256
#http://www.frexx.de/xterm-256-notes/
_backnone="\e[00m"
_backblack="\e[40m"
_backblue="\e[44m"
_frontred_b="\e[01;31m"
_frontgreen_b="\e[01;32m"
_frontgrey_b="\e[01;37m"
_frontgrey="\e[00;37m"
_frontblue_b="\e[01;34m"
PS1="\[${_backblue}${_frontgreen_b}\]\u@\h:\[${_backblack}${_frontblue_b}\]\w\\$\[${_backnone}${_frontgreen_b}\] "

#'history' options
declare -rx HISTFILE="$HOME/.bash_history"
chattr +a "$HISTFILE" # set append-only
declare -rx HISTSIZE=500000 #nbr of cmds in memory
declare -rx HISTFILESIZE=500000 #nbr of cmds on file
declare -rx HISTCONTROL="" #does not ignore spaces or duplicates
declare -rx HISTIGNORE="" #does not ignore patterns
declare -rx HISTCMD #history line number
history -r #to reload history from file if a prior HISTSIZE has truncated it
if groups | grep -q root; then declare -x TMOUT=3600; fi #timeout for root's sessions

#enable forward search (ctrl-s)
#http://ruslanspivak.com/2010/11/25/bash-history-incremental-search-forward/
stty -ixon

#history substitution ask for a confirmation
shopt -s histverify

#add timestamps in history - obsoleted with logger/syslog
#http://www.thegeekstuff.com/2008/08/15-examples-to-master-linux-command-line-history/#more-130
#declare -rx HISTTIMEFORMAT='%F %T '

#bash audit & traceabilty
#
#
declare -rx AUDIT_LOGINUSER="$(who -mu | awk '{print $1}')"
declare -rx AUDIT_LOGINPID="$(who -mu | awk '{print $6}')"
declare -rx AUDIT_USER="$USER" #defined by pam during su/sudo
declare -rx AUDIT_PID="$$"
declare -rx AUDIT_TTY="$(who -mu | awk '{print $2}')"
declare -rx AUDIT_SSH="$([ -n "$SSH_CONNECTION" ] && echo "$SSH_CONNECTION" | awk '{print $1":"$2"->"$3":"$4}')"
declare -rx AUDIT_STR="[audit $AUDIT_LOGINUSER/$AUDIT_LOGINPID as $AUDIT_USER/$AUDIT_PID on $AUDIT_TTY/$AUDIT_SSH]"
declare -rx AUDIT_SYSLOG="1" #to use a local syslogd
#
#PROMPT_COMMAND solution is working but the syslog message are sent *after* the command execution, 
#this causes 'su' or 'sudo' commands to appear only after logouts, and 'cd' commands to display wrong working directory
#http://jablonskis.org/2011/howto-log-bash-history-to-syslog/
#declare -rx PROMPT_COMMAND='history -a >(tee -a ~/.bash_history | logger -p user.info -t "$AUDIT_STR $PWD")' #avoid subshells here or duplicate execution will occurs!
#
#another solution is to use 'trap' DEBUG, which is executed *before* the command.
#http://superuser.com/questions/175799/does-bash-have-a-hook-that-is-run-before-executing-a-command
#http://www.davidpashley.com/articles/xterm-titles-with-bash.html
#set -o functrace; trap 'echo -ne "===$BASH_COMMAND===${_backvoid}${_frontgrey}\n"' DEBUG
set +o functrace #disable trap DEBUG inherited in functions, command substitutions or subshells, normally the default setting already
#enable extended pattern matching operators
shopt -s extglob
#function audit_DEBUG() {
#  echo -ne "${_backnone}${_frontgrey}"
#  (history -a >(logger -p user.info -t "$AUDIT_STR $PWD" < <(tee -a ~/.bash_history))) && sync && history -c && history -r
#  #http://stackoverflow.com/questions/103944/real-time-history-export-amongst-bash-terminal-windows
#  #'history -c && history -r' force a refresh of the history because 'history -a' was called within a subshell and therefore
#  #the new history commands that are appent to file will keep their "new" status outside of the subshell, causing their logging
#  #to re-occur on every function call...
#  #note that without the subshell, piped bash commands would hang... (it seems that the trap + process substitution interfer with stdin redirection)
#  #and with the subshell
#}
##enable trap DEBUG inherited for all subsequent functions; required to audit commands beginning with the char '(' for a subshell
#set -o functrace #=> problem: completion in commands avoid logging them
function audit_DEBUG() {
    #simplier and quicker version! avoid 'sync' and 'history -r' that are time consuming!
    if [ "$BASH_COMMAND" != "$PROMPT_COMMAND" ] #avoid logging unexecuted commands after Ctrl-C or Empty+Enter
    then
        echo -ne "${_backnone}${_frontgrey}"
        local AUDIT_CMD="$(history 1)" #current history command
        #remove in last history cmd its line number (if any) and send to syslog
        if [ -n "$AUDIT_SYSLOG" ]
        then
            if ! logger -p user.info -t "$AUDIT_STR $PWD" "${AUDIT_CMD##*( )?(+([0-9])[^0-9])*( )}"
            then
                echo error "$AUDIT_STR $PWD" "${AUDIT_CMD##*( )?(+([0-9])[^0-9])*( )}"
            fi
        else
            echo $( date +%F_%H:%M:%S ) "$AUDIT_STR $PWD" "${AUDIT_CMD##*( )?(+([0-9])[^0-9])*( )}" >>/var/log/userlog.info
        fi
    fi
    #echo "===cmd:$BASH_COMMAND/subshell:$BASH_SUBSHELL/fc:$(fc -l -1)/history:$(history 1)/histline:${AUDIT_CMD%%+([^ 0-9])*}===" #for debugging
}
function audit_EXIT() {
    local AUDIT_STATUS="$?"
    if [ -n "$AUDIT_SYSLOG" ]
    then
        logger -p user.info -t "$AUDIT_STR" "#=== bash session ended. ==="
    else
        echo $( date +%F_%H:%M:%S ) "$AUDIT_STR" "#=== bash session ended. ===" >>/var/log/userlog.info
    fi
    exit "$AUDIT_STATUS"
}
#make audit trap functions readonly; disable trap DEBUG inherited (normally the default setting already)
declare -fr +t audit_DEBUG
declare -fr +t audit_EXIT
if [ -n "$AUDIT_SYSLOG" ]
then
    logger -p user.info -t "$AUDIT_STR" "#=== New bash session started. ===" #audit the session openning
else
    echo $( date +%F_%H:%M:%S ) "$AUDIT_STR" "#=== New bash session started. ===" >>/var/log/userlog.info
fi
#when a bash command is executed it launches first the audit_DEBUG(),
#then the trap DEBUG is disabled to avoid a useless rerun of audit_DEBUG() during the execution of pipes-commands;
#at the end, when the prompt is displayed, re-enable the trap DEBUG
declare -rx PROMPT_COMMAND="trap 'audit_DEBUG; trap DEBUG' DEBUG"
declare -rx BASH_COMMAND #current command executed by user or a trap
declare -rx SHELLOPT #shell options, like functrace  
trap audit_EXIT EXIT #audit the session closing

Enjoy!

francois scheurer

Posted 2010-08-14T11:09:20.117

Reputation: 1

I had a problem with piped bash commands that hangs... I found a workaround using a subshell, but this caused the 'history -a' to not refresh the history outside the subshell scope... Finally the solution was to use a function that re-read the history after the subshell execution.

It works as I wanted. As Vaidas wrote on http://jablonskis.org/2011/howto-log-bash-history-to-syslog/, it is more easy to deploy than patching the bash in C (i did that too in the past). but there is some performance drop while re-reading each time the history file and doing a disk 'sync'...

– francois scheurer – 2012-02-07T22:18:05.190

5You might want to trim that code; currently it's almost completely unreadable. – l0b0 – 2012-03-22T11:27:02.520

3

I wrote a method to log all 'bash' commands/builtins into a text-file or a 'syslog' server without using a patch or a special executable tool.

It is very easy to deploy, as it is a simple shellscript that need to be called once at the initialization of the 'bash'.

See the method here.

Francois Scheurer

Posted 2010-08-14T11:09:20.117

Reputation: 5