Czy bash ma hak, który jest uruchamiany przed wykonaniem polecenia?

111

Czy w bash mogę zorganizować wykonanie funkcji tuż przed uruchomieniem polecenia?

Jest $PROMPT_COMMANDwykonywany przed wyświetleniem monitu, tj. Zaraz po uruchomieniu polecenia.

Bash's $PROMPT_COMMANDjest analogiczny do precmdfunkcji zsh ; więc szukam bashowego odpowiednika zsh preexec.

Przykładowe aplikacje: ustaw tytuł terminala na wykonywane polecenie; automatycznie dodaj timeprzed każdym poleceniem.

Gilles
źródło
3
Wersja bash 4.4 ma PS0zmienną, która działa jak, PS1ale jest używana po odczytaniu polecenia, ale przed jego wykonaniem. Zobacz gnu.org/software/bash/manual/bashref.html#Bash-Variables
Glenn Jackman

Odpowiedzi:

93

Nie natywnie, ale można go zhakować za pomocą DEBUGpułapki. Ten kod konfiguruje się preexeci precmddziała podobnie do zsh. Wiersz poleceń jest przekazywany jako pojedynczy argument do preexec.

Oto uproszczona wersja kodu do skonfigurowania precmdfunkcji, która jest wykonywana przed uruchomieniem każdego polecenia.

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

Ta sztuczka wynika z Glypha Lefkowitza ; dzięki bcat za zlokalizowanie oryginalnego autora.

Edytować. Zaktualizowaną wersję hacka Glypha można znaleźć tutaj: https://github.com/rcaloras/bash-preexec

Gilles
źródło
"$BASH_COMMAND" = "$PROMPT_COMMAND"Porównanie nie działa dla mnie i.imgur.com/blneCdQ.png
laggingreflex
2
Próbowałem użyć tego kodu na cygwin. Niestety ma tam dość intensywne efekty wydajnościowe - uruchomienie prostej komendy testu porównawczego time for i in {1..10}; do true; donezajmuje normalnie 0,040 sekundy i 1,400 do 1,600 sekundy po aktywacji pułapki DEBUG. Powoduje to, że polecenie pułapki jest wykonywane dwa razy na pętlę - a w Cygwin rozwidlanie wymagane do wykonania sed jest zbyt wolne na około 0,030 sekundy dla samego rozwidlenia (różnica prędkości między echowbudowanym a /bin/echo). Może coś, o czym warto pamiętać.
kdb
2
@kdb Wydajność Cygwin dla widelca do bani. Rozumiem, że jest to nieuniknione w systemie Windows. Jeśli chcesz uruchomić kod bash w systemie Windows, spróbuj ograniczyć rozwidlanie.
Gilles
@DevNull Można to bardzo łatwo obejść, usuwając pułapkę. Nie ma technicznego rozwiązania dla ludzi, którzy robią to, co wolno, ale nie powinni. Istnieją częściowe środki zaradcze: nie udzielaj tylu osób tyle samo dostępu, upewnij się, że kopie zapasowe są aktualne, używaj kontroli wersji zamiast bezpośredniej manipulacji plikami ... Jeśli chcesz czegoś, czego użytkownicy nie mogą łatwo wyłączyć, pozwól sam nie może w ogóle wyłączyć, wtedy ograniczenia w powłoce nie pomogą: można je usunąć tak łatwo, jak można je dodać.
Gilles
1
Jeśli masz więcej poleceń w PROMPT_COMMANDzmiennej (np ograniczona ;), może trzeba użyć dopasowywania wzorców w drugim wierszu preexec_invoke_execfunkcji, podobnie jak to: [[ "$PROMPT_COMMAND" =~ "$BASH_COMMAND" ]]. Jest tak, ponieważ BASH_COMMANDreprezentuje każde z poleceń osobno.
jirislav
20

Możesz użyć trappolecenia (z help trap):

Jeśli SIGNAL_SPEC to DEBUG, ARG jest wykonywany przed każdą prostą komendą.

Na przykład, aby dynamicznie zmienić tytuł terminala, możesz użyć:

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

Z tego źródła

cYrus
źródło
1
Interesujące ... na moim starym serwerze Ubuntu help trapmówi: „Jeśli DEBUGA SIGNAL_SPEC, ARG jest wykonywany po każdym prostym poleceniu” [moje podkreślenie].
LarsH
1
Użyłem kombinacji tej odpowiedzi z niektórych specjalnego materiału w przyjętym odpowiedzi: trap '[ -n "$COMP_LINE" ] && [ "$BASH_COMMAND" != "$PROMPT_COMMAND" ] && date "+%X";echo -e "\e]0;$BASH_COMMAND\007"' DEBUG. To umieszcza polecenie w tytule, a także drukuje aktualny czas przed każdym poleceniem, ale nie robi tego podczas wykonywania $PROMPT_COMMAND.
coredumperror
1
@CoreDumpError, odkąd refactored kod należy negować wszystkie warunki: pierwszy staje się zatem: [ -z "$COMP_LINE" ].
cyr
@cYrus Thanks! Nie znam wystarczająco dużo programowania bash, aby zauważyć ten problem.
coredumperror
@LarsH: Którą wersję posiadasz? Mam BASH_VERSION = „4.3.11 (1) -release” i mówi: „ARG jest wykonywany przed każdym prostym poleceniem”.
musiphil
12

To nie jest funkcja powłoki, która jest wykonywana, ale napisałem wiersz $PS0zachęty, który jest wyświetlany przed uruchomieniem każdego polecenia. Szczegóły tutaj: http://stromberg.dnsalias.org/~strombrg/PS0-prompt/

$PS0jest uwzględniony w bash4.4, choć większość Linuksów zajmie trochę czasu 4.4 - możesz sam zbudować 4.4, jeśli chcesz; w takim przypadku prawdopodobnie powinieneś to podłożyć /usr/local, dodać do /etc/shellsi chshdo niego. Następnie wyloguj się i zaloguj ponownie, być może najpierw sshdo siebie @ localhost lub sudo siebie jako test.

dstromberg
źródło
11

Niedawno musiałem rozwiązać ten dokładny problem dla mojego pobocznego projektu. Stworzyłem dość solidne i odporne rozwiązanie, które emuluje pashxec i precmd zsh dla funkcjonalności bash.

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

Pierwotnie był oparty na rozwiązaniu Glypha Lefkowitza, ale ulepszyłem go i zaktualizowałem. Chętnie pomogę lub w razie potrzeby doda funkcję.

RCCola
źródło
3

Dziękuję za podpowiedzi! Skończyło się na tym:

#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

Cieszyć się!

Francois Scheurer
źródło
Miałem problem z zawieszonymi poleceniami bash, które się zawiesiły ... Znalazłem obejście za pomocą podpowłoki, ale to spowodowało, że 'historia -a' nie odświeżyła historii poza zakresem podpowłoki ... Wreszcie rozwiązaniem było użycie funkcji które ponownie czytają historię po wykonaniu podpowłoki. Działa tak, jak chciałem. Jak napisał Vaidas na jablonskis.org/2011/howto-log-bash-history-to-syslog , łatwiej jest go wdrożyć niż załatać bash w C (ja też to robiłem w przeszłości). ale występuje pewien spadek wydajności podczas ponownego odczytu pliku historii i synchronizacji dysku ...
Francois Scheurer
5
Możesz przyciąć ten kod; obecnie jest prawie całkowicie nieczytelny.
l0b0
3

Napisałem metodę rejestrowania wszystkich poleceń / wbudowanych „bash” w pliku tekstowym lub na serwerze „syslog” bez użycia łaty lub specjalnego narzędzia wykonywalnego.

Jest bardzo łatwy do wdrożenia, ponieważ jest to prosty skrypt powłoki, który należy wywołać raz podczas inicjalizacji „bash”.

Zobacz metodę tutaj .

Francois Scheurer
źródło