Bash ha un hook che viene eseguito prima di eseguire un comando?


111

In bash, posso organizzare l'esecuzione di una funzione prima di eseguire un comando?

C'è $PROMPT_COMMAND, che viene eseguito prima di mostrare un prompt, cioè subito dopo aver eseguito un comando.

Bash's $PROMPT_COMMANDè analogo alla precmdfunzione di zsh ; quindi quello che sto cercando è un bash equivalente a quello di zsh preexec.

Applicazioni di esempio: impostare il titolo del terminale sul comando in esecuzione; aggiungere automaticamente timeprima di ogni comando.


3
bash versione 4.4 ha una PS0variabile che si comporta come PS1ma viene utilizzata dopo aver letto il comando ma prima di eseguirlo. Vedi gnu.org/software/bash/manual/bashref.html#Bash-Variables
glenn jackman

Risposte:


93

Non nativamente, ma può essere hackerato usando la DEBUGtrappola. Questo codice imposta preexece precmdfunziona in modo simile a zsh. La riga di comando viene passata come singolo argomento a preexec.

Ecco una versione semplificata del codice per impostare una precmdfunzione che viene eseguita prima di eseguire ciascun comando.

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

Questo trucco è dovuto a Glyph Lefkowitz ; grazie a bcat per aver localizzato l'autore originale.

Modificare. Una versione aggiornata dell'hack di Glyph è disponibile qui: https://github.com/rcaloras/bash-preexec


Il "$BASH_COMMAND" = "$PROMPT_COMMAND"confronto non funziona per me i.imgur.com/blneCdQ.png
laggingreflex

2
Ho provato a usare questo codice su Cygwin. Purtroppo ha effetti di prestazioni piuttosto intensi lì - l'esecuzione di un semplice comando di benchmark time for i in {1..10}; do true; donerichiede normalmente 0,040 secondi e da 1,400 a 1,600 secondi dopo l'attivazione della trap DEBUG. Fa eseguire il comando trap due volte per loop - e su Cygwin il fork richiesto per eseguire sed è proibitivamente lento a circa 0,030 secondi per il solo fork (differenza di velocità tra echobuiltin e /bin/echo). Qualcosa da tenere a mente forse.
kdb,

2
@kdb Prestazioni di Cygwin per succhiare la forcella. La mia comprensione è che questo è inevitabile su Windows. Se devi eseguire il codice bash su Windows, prova a ridurre il fork.
Gilles,

@DevNull Questo può essere facilmente eluso rimuovendo la trappola. Non esiste una soluzione tecnica per le persone che fanno ciò che sono autorizzati a fare ma che non dovrebbero fare. Esistono rimedi parziali: non dare a quante più persone l'accesso, assicurati che i tuoi backup siano aggiornati, usa il controllo della versione piuttosto che la manipolazione diretta dei file, ... Se vuoi qualcosa che gli utenti non possono disabilitare facilmente, lascia che da solo non può disabilitare affatto, quindi le restrizioni nella shell non ti aiuteranno: possono essere rimosse con la stessa facilità con cui possono essere aggiunte.
Gilles,

1
Se si dispone più comandi in una PROMPT_COMMANDvariabile (es delimitato da ;), potrebbe essere necessario utilizzare pattern matching nella seconda riga della preexec_invoke_execfunzione, proprio come questo: [[ "$PROMPT_COMMAND" =~ "$BASH_COMMAND" ]]. Questo perché BASH_COMMANDrappresenta ciascuno dei comandi separatamente.
jirislav,

20

Puoi usare il trapcomando (da help trap):

Se un SIGNAL_SPEC è DEBUG, ARG viene eseguito prima di ogni semplice comando.

Ad esempio, per modificare dinamicamente il titolo del terminale è possibile utilizzare:

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

Da questa fonte


1
Interessante ... sul mio vecchio server Ubuntu, help trapdice "Se un SIGNAL_SPEC è DEBUG, ARG viene eseguito dopo ogni semplice comando" [enfatizza il mio].
LarsH,

1
Ho usato una combinazione di questa risposta con alcune delle cose speciale nella risposta accettata: trap '[ -n "$COMP_LINE" ] && [ "$BASH_COMMAND" != "$PROMPT_COMMAND" ] && date "+%X";echo -e "\e]0;$BASH_COMMAND\007"' DEBUG. Questo inserisce il comando nel titolo e stampa anche l'ora corrente proprio prima di ogni comando, ma non lo fa durante l'esecuzione $PROMPT_COMMAND.
coredumperror,

1
@CoreDumpError, dal momento che hai refactoring del codice si dovrebbe negare tutte le condizioni: il primo diventa quindi: [ -z "$COMP_LINE" ].
cYrus,

@cYrus Grazie! Non conosco abbastanza programmazione bash per aver notato quel problema.
coredumperror,

@LarsH: quale versione hai? Ho BASH_VERSION = "4.3.11 (1) -release" e dice "ARG viene eseguito prima di ogni semplice comando."
musiphil,

12

Non è una funzione shell che viene eseguita, ma ho contribuito con una $PS0stringa di prompt che viene visualizzata prima dell'esecuzione di ciascun comando. Dettagli qui: http://stromberg.dnsalias.org/~strombrg/PS0-prompt/

$PS0è incluso in bash4.4, anche se ci vorrà un po 'di tempo perché la maggior parte di Linux includa 4.4 - puoi creare 4.4 tu stesso se vuoi; in quel caso, probabilmente dovreste metterla sotto /usr/local, aggiungerlo /etc/shellse chshad esso. Quindi disconnettersi e riconnettersi, magari sshusando te stesso @ localhost o suprima te stesso come test.


11

Di recente ho dovuto risolvere questo esatto problema per un mio progetto collaterale. Ho creato una soluzione abbastanza robusta e resiliente che emula la funzionalità preexec e precm di zsh per bash.

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

Originariamente era basato sulla soluzione di Glyph Lefkowitz, ma l'ho migliorato e aggiornato. Felice di aiutarti o aggiungere una funzione se necessario.


3

Grazie per i suggerimenti! Ho finito per usare questo:

#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

Godere!


Ho avuto un problema con i comandi piph bash che si bloccano ... Ho trovato una soluzione alternativa utilizzando una subshell, ma ciò ha causato che la "cronologia -a" non aggiornava la cronologia al di fuori dell'ambito della subshell ... Alla fine la soluzione era usare una funzione che rilegge la cronologia dopo l'esecuzione della subshell. Funziona come volevo. Come ha scritto Vaidas su jablonskis.org/2011/howto-log-bash-history-to-syslog , è più facile da implementare che rattoppare la bash in C (l'ho fatto anche in passato). ma si verifica un calo delle prestazioni durante la rilettura ogni volta che il file cronologico e la sincronizzazione del disco ...
francois scheurer,

5
Potresti voler tagliare quel codice; attualmente è quasi completamente illeggibile.
l0b0

3

Ho scritto un metodo per registrare tutti i comandi / builtin 'bash' in un file di testo o in un server 'syslog' senza usare una patch o uno strumento eseguibile speciale.

È molto facile da implementare, in quanto è un semplice shellscript che deve essere chiamato una volta durante l'inizializzazione di 'bash'.

Vedi il metodo qui .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.