題 bash是否有一個在執行命令之前運行的鉤子?


在bash中,我可以在運行命令之前安排執行函數嗎?

$PROMPT_COMMAND在顯示提示之前執行,即在運行命令之後執行。

bash的 $PROMPT_COMMAND 類似於zsh的 precmd 功能;所以我正在尋找的是一個等同於zsh的bash preexec

示例應用程序:將終端標題設置為正在執行的命令;自動添加 time 在每個命令之前。


99


起源


bash版本4.4有一個 PS0 行為的變量 PS1 但是在讀取命令之後但在執行之前使用。看到 gnu.org/software/bash/manual/bashref.html#Bash-Variables - glenn jackman


答案:


不是原生的,但它可以使用 DEBUG 陷阱。 這段代碼 設置 preexec 和 precmd 功能類似於zsh。命令行作為單個參數傳遞給 preexec

以下是設置a的代碼的簡化版本 precmd 在運行每個命令之前執行的函數。

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

這個伎倆是由於 Glyph Lefkowitz;謝謝 BCAT 找到原作者。

編輯。可以在此處找到Glyph's hack的更新版本: https://github.com/rcaloras/bash-preexec


84



該 "$BASH_COMMAND" = "$PROMPT_COMMAND" 比較不適合我 i.imgur.com/blneCdQ.png - laggingreflex
我嘗試在cygwin上使用這個代碼。可悲的是,它有相當強烈的性能影響 - 運行一個簡單的基準命令 time for i in {1..10}; do true; done激活DEBUG陷阱後,正常情況下需要0.040秒,需要1.400到1.600秒。它導致trap命令每個循環執行兩次 - 而在Cygwin上執行sed所需的分支在大約0.030秒時非常慢,僅用於分叉(速度差異) echo 內置和 /bin/echo)。也許要記住一些事情。 - kdb
@kdb Cygwin表現糟糕糟糕。我的理解是,這在Windows上是不可避免的。如果您需要在Windows上運行bash代碼,請嘗試減少分叉。 - Gilles
@DevNull通過移除陷阱可以很容易地避開這種情況。沒有技術解決方案讓人們做他們被允許做但不應做的事情。有部分補救措施:不要給盡可能多的人訪問,確保備份是最新的,使用版本控製而不是直接文件操作,...如果你想要用戶無法輕易禁用的東西,讓單獨無法禁用,然後shell中的限制將無法幫助您:它們可以像添加它們一樣輕鬆刪除。 - Gilles
@Glyph可以修改為不運行實際用戶命令,如果它不符合某些條件(即:阻止命令運行並打印警告,如果它包含字典文件中的粗言穢語)? - DevNull


你可以使用 trap 命令(來自 help trap):

如果SIGNAL_SPEC是DEBUG,則在每個簡單命令之前執行ARG。

例如,要動態更改終端標題,您可以使用:

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

這個 資源。


16



有趣......在我的舊Ubuntu服務器上, help trap 說“如果SIGNAL_SPEC是DEBUG,則執行ARG 後 每一個簡單的命令“[強調我的]。 - LarsH
我將這個答案的組合與已接受答案中的一些特殊內容結合使用: trap '[ -n "$COMP_LINE" ] && [ "$BASH_COMMAND" != "$PROMPT_COMMAND" ] && date "+%X";echo -e "\e]0;$BASH_COMMAND\007"' DEBUG。這會將命令放入標題中,並在每個命令之前打印當前時間,但在執行時不會這樣做 $PROMPT_COMMAND。 - CoreDumpError
@CoreDumpError,因為你重構了代碼,你應該否定所有條件:第一個因此變成: [ -z "$COMP_LINE" ]。 - cYrus
@cYrus謝謝!我不知道有足夠的bash編程注意到這個問題。 - CoreDumpError
@LarsH:你有哪個版本?我有BASH_VERSION =“4.3.11(1)-release”,它說“ARG已被執行 之前 每個簡單的命令。“ - musiphil


我最近不得不為我的一個側面項目解決這個問題。我製作了一個相當強大且有彈性的解決方案,可以為bash模擬zsh的preexec和precmd功能。

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

它最初是以Glyph Lefkowitz的解決方案為基礎的,但我對它進行了改進並使其更新。很高興在需要時提供幫助或添加功能。


10





它不是一個執行的shell函數,但是我提供了一個$ PS0提示字符串,它在每個命令運行之前顯示。詳情如下: http://stromberg.dnsalias.org/~strombrg/PS0-prompt/

$ PS0包含在bash 4.4中,雖然大多數Linux需要一段時間來包含4.4 - 如果你願意,你可以自己構建4.4;在這種情況下,你可能應該將它放在/ usr / local下,將它添加到/ etc / shells並chsh到它。然後退出並重新登錄,或許首先對自己@ localhost ssh'ing或首先作為測試suuing給自己。


9





謝謝你的提示! 我最終使用了這個:

#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

請享用!


3



我遇到了掛起bash命令的問題......我找到了使用子shell的解決方法,但這導致'history -a'不刷新子shell範圍之外的歷史...最後解決方案是使用函數在子shell執行後重新讀取歷史記錄。它按我的意願工作。正如Vaidas寫的那樣 jablonskis.org/2011/howto-log-bash-history-to-syslog,它比在C中修補bash更容易部署(我過去也做過)。但每次重新讀取歷史文件並執行磁盤“同步”時,性能會有所下降... - francois scheurer
您可能想要修剪該代碼;目前它幾乎完全不可讀。 - l0b0


我寫了一個方法,將所有'bash'命令/ builtins記錄到文本文件或'syslog'服務器中,而不使用補丁或特殊的可執行工具。

它很容易部署,因為它是一個簡單的shellcript,需要在'bash'的初始化時調用一次。

看方法 這裡


3