¿Bash tiene un gancho que se ejecuta antes de ejecutar un comando?

111

En bash, ¿puedo hacer arreglos para que se ejecute una función justo antes de ejecutar un comando?

Existe $PROMPT_COMMAND, que se ejecuta antes de mostrar una solicitud, es decir, justo después de ejecutar un comando.

Bash $PROMPT_COMMANDes análogo a la precmdfunción de zsh ; así que lo que estoy buscando es un bash equivalente a zsh's preexec.

Aplicaciones de ejemplo: establezca el título de su terminal para el comando que se ejecuta; agregar automáticamente timeantes de cada comando.

Gilles
fuente
3
bash versión 4.4 tiene una PS0variable que actúa como PS1pero se usa después de leer el comando pero antes de ejecutarlo. Consulte gnu.org/software/bash/manual/bashref.html#Bash-Variables
glenn jackman el

Respuestas:

93

No de forma nativa, pero se puede hackear usando la DEBUGtrampa. Este código se configura preexecy precmdfunciona de manera similar a zsh. La línea de comando se pasa como un argumento único a preexec.

Aquí hay una versión simplificada del código para configurar una precmdfunción que se ejecuta antes de ejecutar cada 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

Este truco se debe a Glyph Lefkowitz ; Gracias a bcat por localizar al autor original.

Editar. Puede encontrar una versión actualizada del truco de Glyph aquí: https://github.com/rcaloras/bash-preexec

Gilles
fuente
La "$BASH_COMMAND" = "$PROMPT_COMMAND"comparación no funciona para mí i.imgur.com/blneCdQ.png
laggingreflex
2
Intenté usar este código en cygwin. Lamentablemente, tiene efectos de rendimiento bastante intensos allí: ejecutar un comando de referencia simple time for i in {1..10}; do true; donetoma 0.040 segundos normalmente y 1.400 a 1.600 segundos después de activar la trampa DEBUG. Hace que el comando trap se ejecute dos veces por bucle, y en Cygwin la bifurcación requerida para ejecutar sed es prohibitivamente lenta, aproximadamente 0.030 segundos para bifurcar solo (diferencia de velocidad entre echoincorporado y /bin/echo). Algo a tener en cuenta, tal vez.
kdb
2
@kdb Cygwin rendimiento para horquillas chupa. Tengo entendido que esto es inevitable en Windows. Si necesita ejecutar código bash en Windows, intente reducir la bifurcación.
Gilles
@DevNull Esto se puede evitar fácilmente quitando la trampa. No hay una solución técnica para que las personas hagan lo que se les permite hacer, pero que no deberían hacer. Hay remedios parciales: no le dé acceso a tanta gente, asegúrese de que sus copias de seguridad estén actualizadas, use el control de versiones en lugar de la manipulación directa de archivos ... Si desea algo que los usuarios no puedan deshabilitar fácilmente, deje que solo no puede deshabilitarse en absoluto, entonces las restricciones en el shell no lo ayudarán: se pueden eliminar tan fácilmente como se pueden agregar.
Gilles
1
Si usted tiene más comandos en una PROMPT_COMMANDvariable (por ejemplo delimitado por ;), puede que tenga que utilizar la coincidencia de patrones en la segunda línea de la preexec_invoke_execfunción, al igual que este: [[ "$PROMPT_COMMAND" =~ "$BASH_COMMAND" ]]. Esto se debe a que BASH_COMMANDrepresenta cada uno de los comandos por separado.
jirislav
20

Puede usar el trapcomando (desde help trap):

Si un SIGNAL_SPEC es DEBUG, ARG se ejecuta antes de cada comando simple.

Por ejemplo, para cambiar el título del terminal de forma dinámica, puede usar:

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

De esta fuente.

cYrus
fuente
1
Interesante ... en mi antiguo servidor Ubuntu, help trapdice "Si un SIGNAL_SPEC es DEBUG, ARG se ejecuta después de cada comando simple" [énfasis mío].
LarsH
1
He utilizado una combinación de esta respuesta con algunas de las cosas especial en la respuesta aceptada: trap '[ -n "$COMP_LINE" ] && [ "$BASH_COMMAND" != "$PROMPT_COMMAND" ] && date "+%X";echo -e "\e]0;$BASH_COMMAND\007"' DEBUG. Esto coloca el comando en el título y también imprime la hora actual justo antes de cada comando, pero no lo hace cuando se ejecuta $PROMPT_COMMAND.
coredumperror
1
@CoreDumpError, puesto que ya has refactorizado el código que debería negar todas las condiciones: el primero se convierte, por lo tanto: [ -z "$COMP_LINE" ].
cYrus
@cYrus Gracias! No conozco suficiente programación bash para haber notado ese problema.
coredumperror
@LarsH: ¿Qué versión tienes? Tengo BASH_VERSION = "4.3.11 (1) -release" y dice "ARG se ejecuta antes de cada comando simple".
musiphil
12

No se ejecuta una función de shell, pero contribuí con una $PS0cadena de solicitud que se muestra antes de ejecutar cada comando. Detalles aquí: http://stromberg.dnsalias.org/~strombrg/PS0-prompt/

$PS0se incluye en bash4.4, aunque la mayoría de los Linux tardarán un tiempo en incluir 4.4; sin embargo, puede compilar 4.4 usted mismo; en ese caso, probablemente debería ponerlo bajo /usr/local, añadirlo a /etc/shellsy chsha ella. Luego, cierre la sesión y vuelva a iniciarla, quizás como prueba sshpara usted @ localhost o supara usted primero.

dstromberg
fuente
11

Recientemente tuve que resolver este problema exacto para un proyecto paralelo mío. Hice una solución bastante robusta y resistente que emula la funcionalidad preexec y precmd de zsh para bash.

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

Originalmente se basó en la solución de Glyph Lefkowitz, pero la he mejorado y actualizado. Feliz de ayudar o agregar una función si es necesario.

RCCola
fuente
3

Gracias por las pistas! Terminé usando esto:

#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

¡Disfrutar!

Francois Scheurer
fuente
Tuve un problema con los comandos bash entubados que se cuelgan ... Encontré una solución alternativa usando un subshell, pero esto causó que 'history -a' no actualizara el historial fuera del alcance del subshell ... Finalmente, la solución fue usar una función que vuelven a leer el historial después de la ejecución del subshell. Funciona como yo quería. Como Vaidas escribió en jablonskis.org/2011/howto-log-bash-history-to-syslog , es más fácil de implementar que parchar el bash en C (lo hice también en el pasado). pero hay una cierta caída de rendimiento mientras que re-leer cada vez que el archivo de la historia y hacer un disco 'sync' ...
Francois Scheurer
55
Es posible que desee recortar ese código; Actualmente es casi completamente ilegible.
l0b0
3

Escribí un método para registrar todos los comandos / bash 'bash' en un archivo de texto o un servidor 'syslog' sin usar un parche o una herramienta ejecutable especial.

Es muy fácil de implementar, ya que es un simple shellscript que debe llamarse una vez en la inicialización de 'bash'.

Mira el método aquí .

Francois Scheurer
fuente