Est-ce que bash a un hook qui est exécuté avant d'exécuter une commande?

111

Sous bash, puis-je organiser une fonction juste avant d'exécuter une commande?

Il y en a $PROMPT_COMMAND, qui est exécuté avant d'afficher une invite, c'est-à-dire juste après l'exécution d'une commande.

Bash $PROMPT_COMMANDest analogue à la precmdfonction de zsh ; donc ce que je recherche, c’est un équivalent bash de zsh preexec.

Exemples d'applications: définissez le titre de votre terminal sur la commande en cours d'exécution; ajouter automatiquement timeavant chaque commande.

Gilles
la source
3
La version 4.4 de bash contient une PS0variable qui agit comme PS1mais qui est utilisée après la lecture de la commande mais avant son exécution. Voir gnu.org/software/bash/manual/bashref.html#Bash-Variables
glenn jackman le

Réponses:

93

Pas nativement, mais on peut le pirater à l'aide du DEBUGpiège. Ce code configure preexecet precmdfonctionne de manière similaire à zsh. La ligne de commande est transmise en tant qu’argument unique preexec.

Voici une version simplifiée du code pour configurer une precmdfonction qui est exécutée avant d’exécuter chaque commande.

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

Cette astuce est due à Glyph Lefkowitz ; merci à bcat d' avoir localisé l'auteur original.

Modifier. Une version mise à jour du hack de Glyph peut être trouvée ici: https://github.com/rcaloras/bash-preexec

Gilles
la source
La "$BASH_COMMAND" = "$PROMPT_COMMAND"comparaison ne fonctionne pas pour moi i.imgur.com/blneCdQ.png
laggingreflex
2
J'ai essayé d'utiliser ce code sur Cygwin. Malheureusement, les effets sur les performances y sont assez intenses: l'exécution d'une simple commande de benchmark time for i in {1..10}; do true; doneprend 0,040 seconde normalement et 1 400 à 1 600 secondes après l'activation du piège DEBUG. La commande trap est exécutée deux fois par boucle - et sur Cygwin, le forking requis pour l'exécution de sed est excessivement lent à environ 0,030 seconde pour le forking seul (différence de vitesse entre echoBuiltin et /bin/echo). Quelque chose à garder à l'esprit peut-être.
kdb
2
@kdb La performance Cygwin pour les fourches est nulle. Si j'ai bien compris, cela est inévitable sous Windows. Si vous avez besoin d'exécuter du code bash sous Windows, essayez de réduire le nombre de forking.
Gilles
@DevNull Cela peut être très facilement contourné en retirant le piège. Il n’existe aucune solution technique permettant aux personnes de faire ce qu’elles sont autorisées à faire mais qu’elles ne devraient pas faire. Il existe des solutions partielles: ne donnez pas autant d'accès à autant de personnes, assurez-vous que vos sauvegardes sont à jour, utilisez le contrôle de version plutôt que la manipulation directe de fichier,… Si vous voulez quelque chose que les utilisateurs ne peuvent pas désactiver facilement, laissez-le seul ne peut pas du tout désactiver, les restrictions dans le shell ne vous aideront pas: elles peuvent être supprimées aussi facilement qu’elles peuvent être ajoutées.
Gilles
1
Si vous avez plus de commandes dans une PROMPT_COMMANDvariable (par exemple , délimitée par ;), vous devrez peut - être utiliser la correspondance de motif dans la deuxième ligne de la preexec_invoke_execfonction, comme ceci: [[ "$PROMPT_COMMAND" =~ "$BASH_COMMAND" ]]. C'est parce que BASH_COMMANDreprésente chacune des commandes séparément.
jirislav
20

Vous pouvez utiliser la trapcommande (depuis help trap):

Si SIGNAL_SPEC est DEBUG, ARG est exécuté avant chaque commande simple.

Par exemple, pour changer le titre du terminal de manière dynamique, vous pouvez utiliser:

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

De cette source.

cYrus
la source
1
Intéressant ... sur mon ancien serveur Ubuntu, help trapdit "Si un SIGNAL_SPEC est DEBUG, ARG est exécuté après chaque commande simple" [non souligné dans l'original].
LarsH
1
J'ai utilisé une combinaison de cette réponse avec certaines des choses spéciales dans la réponse acceptée: trap '[ -n "$COMP_LINE" ] && [ "$BASH_COMMAND" != "$PROMPT_COMMAND" ] && date "+%X";echo -e "\e]0;$BASH_COMMAND\007"' DEBUG. Cela place la commande dans le titre et affiche également l'heure actuelle juste avant chaque commande, mais ne le fait pas lors de l'exécution $PROMPT_COMMAND.
Coredumperror
1
@CoreDumpError, puisque vous avez refactorisé le code vous devez annuler toutes les conditions: le premier devient donc: [ -z "$COMP_LINE" ].
cYrus
@cYrus Merci! Je ne connais pas assez la programmation bash pour avoir remarqué ce problème.
Coredumperror
@LarsH: Quelle version avez-vous? J'ai BASH_VERSION = "4.3.11 (1) -release" et il est écrit "ARG est exécuté avant chaque commande simple".
musiphil
12

Ce n'est pas une fonction shell qui est exécutée, mais j'ai fourni une $PS0chaîne d'invite qui est affichée avant l'exécution de chaque commande. Détails ici: http://stromberg.dnsalias.org/~strombrg/PS0-prompt/

$PS0est inclus dans bash4.4, bien que la plupart des Linux prennent un peu de temps pour inclure 4.4 - vous pouvez le construire vous-même si vous le souhaitez; dans ce cas, vous devriez probablement le mettre sous /usr/local, l'ajouter à /etc/shellset chshà celui-ci. Ensuite, déconnectez-vous et reconnectez-vous, peut- sshêtre par vous-même @ localhost ou supar vous-même d'abord comme test.

dstromberg
la source
11

J'ai récemment eu à résoudre ce problème exact pour un projet parallèle. J'ai créé une solution assez robuste et résiliente qui imite les fonctionnalités preexec et precmd de zsh pour bash.

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

Il était à l'origine basé sur la solution de Glyph Lefkowitz, mais je l'ai amélioré et mis à jour. Heureux d'aider ou d'ajouter une fonctionnalité si nécessaire.

RCCola
la source
3

Merci pour les conseils! J'ai fini par utiliser ceci:

#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

Prendre plaisir!

Francois Scheurer
la source
J'ai eu un problème avec les commandes piped bash qui se bloquent ... J'ai trouvé une solution de contournement à l'aide d'un sous-shell, mais cela a amené l'historique à ne pas actualiser l'historique en dehors de la portée du sous-shell ... Enfin, la solution a été d'utiliser une fonction qui relit l'historique après l'exécution du sous-shell. Cela fonctionne comme je voulais. Comme l'a écrit Vaidas sur jablonskis.org/2011/howto-log-bash-history-to-syslog , il est plus facile à déployer que de corriger la bash en C (je l'avais déjà fait par le passé). mais il y a une baisse de la performance en relisant chaque fois que le fichier d'historique et de faire un disque « synchronisation » ...
francois Scheurer
5
Vous voudrez peut-être couper ce code; actuellement c'est presque complètement illisible.
l0b0
3

J'ai écrit une méthode pour consigner toutes les commandes / commandes intégrées dans un fichier texte ou un serveur 'syslog' sans utiliser de correctif ni d'exécutable spécial.

Il est très facile à déployer, car il s’agit d’un simple script shell qui doit être appelé une fois à l’initialisation du 'bash'.

Voir la méthode ici .

Francois Scheurer
la source