O bash possui um gancho que é executado antes de executar um comando?

111

No bash, posso providenciar para que uma função seja executada antes de executar um comando?

Existe $PROMPT_COMMAND, que é executado antes de mostrar um prompt, ou seja, logo após a execução de um comando.

O Bash's $PROMPT_COMMANDé análogo à precmdfunção do zsh ; então o que estou procurando é um equivalente equivalente ao zsh preexec.

Exemplos de aplicações: defina o título do seu terminal para o comando que está sendo executado; adicionar automaticamente timeantes de cada comando.

Gilles
fonte
3
O bash versão 4.4 possui uma PS0variável que age como PS1mas é usada após a leitura do comando, mas antes de executá-lo. Veja gnu.org/software/bash/manual/bashref.html#Bash-Variables
glenn jackman

Respostas:

93

Não nativamente, mas pode ser hackeado usando a DEBUGarmadilha. Este código é configurado preexece precmdfunciona de forma semelhante ao zsh. A linha de comando é passada como um único argumento para preexec.

Aqui está uma versão simplificada do código para configurar uma precmdfunção que é executada antes de executar 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

Esse truque é devido a Glyph Lefkowitz ; graças ao bcat por localizar o autor original.

Editar. Uma versão atualizada do hack do Glyph pode ser encontrada aqui: https://github.com/rcaloras/bash-preexec

Gilles
fonte
A "$BASH_COMMAND" = "$PROMPT_COMMAND"comparação não está funcionando para mim i.imgur.com/blneCdQ.png
laggingreflex
2
Eu tentei usar esse código no cygwin. Infelizmente, ele tem efeitos de desempenho bastante intensos - executar um comando de benchmark simples time for i in {1..10}; do true; doneleva 0,040 segundos normalmente e 1,400 a 1,600 segundos após a ativação da interceptação DEBUG. Isso faz com que o comando trap seja executado duas vezes por loop - e no Cygwin a forquilha necessária para executar o sed é proibitivamente lenta em aproximadamente 0,030 segundos para forquear sozinha (diferença de velocidade entre o echoembutido e /bin/echo). Algo para se lembrar, talvez.
Kdb 24/05
2
@kdb O desempenho do Cygwin para garfos é péssimo. Meu entendimento é que isso é inevitável no Windows. Se você precisar executar o código do bash no Windows, tente reduzir o uso do garfo.
Gilles
@ DevNull Isso pode ser facilmente contornado removendo a armadilha. Não há solução técnica para as pessoas fazerem o que têm permissão para fazer, mas não deveriam fazer. Existem soluções parciais: não dê acesso a tantas pessoas, verifique se seus backups estão atualizados, use o controle de versão em vez da manipulação direta de arquivos ... Se você deseja algo que os usuários não possam desativar facilmente, sozinho não pode ser desativado, as restrições no shell não o ajudarão: elas podem ser removidas com a mesma facilidade com que podem ser adicionadas.
Gilles
1
Se você tem mais comandos em uma PROMPT_COMMANDvariável (por exemplo delimitado por ;), você pode precisar usar correspondência de padrões na segunda linha da preexec_invoke_execfunção, tal como este: [[ "$PROMPT_COMMAND" =~ "$BASH_COMMAND" ]]. Isso ocorre porque BASH_COMMANDrepresenta cada um dos comandos separadamente.
jirislav 19/04
20

Você pode usar o trapcomando (de help trap):

Se um SIGNAL_SPEC for DEBUG, o ARG será executado antes de cada comando simples.

Por exemplo, para alterar o título do terminal dinamicamente, você pode usar:

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

A partir desta fonte.

cYrus
fonte
1
Interessante ... no meu antigo servidor Ubuntu, help trapdiz "Se um SIGNAL_SPEC for DEBUG, o ARG será executado após cada comando simples" [grifo meu].
Larsh
1
Eu usei uma combinação de esta resposta com algumas das coisas especial na resposta aceita: trap '[ -n "$COMP_LINE" ] && [ "$BASH_COMMAND" != "$PROMPT_COMMAND" ] && date "+%X";echo -e "\e]0;$BASH_COMMAND\007"' DEBUG. Isso coloca o comando no título e também imprime a hora atual antes de cada comando, mas não o faz durante a execução $PROMPT_COMMAND.
Coredumperror #
1
@CoreDumpError, desde que você tenha refatorado o código que você deve negar todas as condições: a primeira, portanto, torna-se: [ -z "$COMP_LINE" ].
precisa
@cYrus Thanks! Não sei quase o suficiente programação de bash para ter notado esse problema.
Coredumperror
@LarsH: Qual versão você tem? Eu tenho BASH_VERSION = "4.3.11 (1) -release" e diz "ARG é executado antes de cada comando simples".
Musiphil
12

Não é uma função shell que é executada, mas contribuí com uma $PS0string de prompt que é exibida antes de cada comando ser executado. Detalhes aqui: http://stromberg.dnsalias.org/~strombrg/PS0-prompt/

$PS0está incluído no bash4.4, embora demore um tempo para a maioria dos Linux incluir o 4.4 - você pode criar o 4.4 se quiser; nesse caso, você provavelmente deve colocá-lo em baixo /usr/local, adicioná-lo a /etc/shellse chsha ele. Em seguida, efetue logout e logon novamente, talvez sshpara você mesmo @ localhost ou supara si mesmo primeiro como teste.

dstromberg
fonte
11

Recentemente, tive que resolver esse problema exato para um projeto paralelo meu. Criei uma solução bastante robusta e resiliente que emula a funcionalidade preexec e precmd do zsh para o bash.

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

Ele foi originalmente baseado na solução de Glyph Lefkowitz, mas eu a aprimorei e a atualizei. É um prazer ajudar ou adicionar um recurso, se necessário.

RCCola
fonte
3

Obrigado pelas dicas! Acabei usando isso:

#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

Desfrutar!

Francois Scheurer
fonte
Eu tive um problema com os comandos do piph bash que travam ... Encontrei uma solução alternativa usando um subshell, mas isso fez com que o 'history -a' não atualizasse o histórico fora do escopo do subshell ... Finalmente, a solução foi usar uma função que releiam o histórico após a execução do subshell. Funciona como eu queria. Como a Vaidas escreveu em jablonskis.org/2011/howto-log-bash-history-to-syslog , é mais fácil de implantar do que corrigir o bash em C (eu fiz isso também no passado). mas há alguma queda de desempenho enquanto re-leitura de cada vez que o arquivo de histórico e fazer um disco 'sync' ...
François Scheurer
5
Você pode cortar esse código; atualmente é quase completamente ilegível.
L0b0 22/03/12
3

Eu escrevi um método para registrar todos os comandos / bash 'bash' em um servidor de arquivo de texto ou 'syslog' sem usar um patch ou uma ferramenta executável especial.

É muito fácil de implantar, pois é um shellscript simples que precisa ser chamado uma vez na inicialização do 'bash'.

Veja o método aqui .

Francois Scheurer
fonte