Liệu bash có một hook được chạy trước khi thực hiện một lệnh không?


111

Trong bash, tôi có thể sắp xếp để một hàm được thực thi ngay trước khi chạy lệnh không?

$PROMPT_COMMAND, được thực thi trước khi hiển thị một dấu nhắc, tức là, ngay sau khi chạy lệnh.

Bash's $PROMPT_COMMANDtương tự như precmdchức năng của zsh ; Vì vậy, những gì tôi đang tìm kiếm là một bash tương đương với zsh preexec.

Các ứng dụng ví dụ: đặt tiêu đề đầu cuối của bạn thành lệnh đang được thực thi; tự động thêm timetrước mỗi lệnh.


3
bash phiên bản 4.4 có một PS0biến hoạt động như thế PS1nhưng được sử dụng sau khi đọc lệnh nhưng trước khi thực hiện nó. Xem gnu.org/software/bash/manual/bashref.html#Bash-Variables
glenn jackman

Câu trả lời:


93

Không phải tự nhiên, nhưng nó có thể bị hack bằng cách sử dụng DEBUGbẫy. Mã này thiết lập preexecvà các precmdchức năng tương tự như zsh. Dòng lệnh được truyền dưới dạng một đối số duy nhất preexec.

Đây là một phiên bản đơn giản của mã để thiết lập một precmdchức năng được thực thi trước khi chạy mỗi lệnh.

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

Thủ thuật này là do Glyph Lefkowitz ; cảm ơn bcat đã định vị tác giả gốc

Biên tập. Có thể tìm thấy phiên bản cập nhật của hack Glyph tại đây: https://github.com/rcaloras/bash-preexec


Việc "$BASH_COMMAND" = "$PROMPT_COMMAND"so sánh không hiệu quả với tôi i.imgur.com/blneCdQ.png
laggingreflex

2
Tôi đã thử sử dụng mã này trên cygwin. Đáng buồn thay, nó có hiệu ứng hiệu năng khá mãnh liệt ở đó - chạy một lệnh điểm chuẩn đơn giản chỉ time for i in {1..10}; do true; donemất 0,040 giây và 1.400 đến 1.600 giây sau khi kích hoạt bẫy DEBUG. Nó khiến lệnh bẫy được thực thi hai lần mỗi vòng lặp - và trên Cygwin, việc sử dụng cần thiết để thực hiện sed rất chậm ở khoảng 0,030 giây khi chỉ sử dụng một lần (chênh lệch tốc độ giữa tích hợp echo/bin/echo). Một cái gì đó để ghi nhớ có thể.
kdb

2
@kdb Hiệu suất Cygwin cho ngã ba hút. Tôi hiểu rằng điều này là không thể tránh khỏi trên Windows. Nếu bạn cần chạy mã bash trên Windows, hãy thử cắt giảm việc sử dụng.
Gilles

@DevNull Điều này có thể rất dễ bị phá vỡ bằng cách loại bỏ bẫy. Không có giải pháp kỹ thuật cho những người làm những gì họ được phép làm nhưng không nên làm. Có một số biện pháp khắc phục: không cung cấp cho nhiều người quyền truy cập, đảm bảo các bản sao lưu của bạn được cập nhật, sử dụng kiểm soát phiên bản thay vì thao tác với tệp trực tiếp, nếu bạn muốn thứ gì đó mà người dùng không thể vô hiệu hóa dễ dàng, hãy để một mình không thể vô hiệu hóa, sau đó các hạn chế trong trình bao sẽ không giúp bạn: chúng có thể được gỡ bỏ dễ dàng như chúng có thể được thêm vào.
Gilles

1
Nếu bạn có nhiều lệnh hơn trong một PROMPT_COMMANDbiến (ví dụ: được phân định bởi ;), bạn có thể cần sử dụng khớp mẫu trong dòng thứ hai của preexec_invoke_exechàm, giống như sau : [[ "$PROMPT_COMMAND" =~ "$BASH_COMMAND" ]]. Điều này là do BASH_COMMANDđại diện cho mỗi lệnh riêng biệt.
jirislav

20

Bạn có thể sử dụng traplệnh (từ help trap):

Nếu một TÍN HIỆU là DEBUG, ARG được thực thi trước mỗi lệnh đơn giản.

Ví dụ: để thay đổi tiêu đề thiết bị đầu cuối một cách linh hoạt, bạn có thể sử dụng:

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

Từ nguồn này .


1
Thú vị ... trên máy chủ Ubuntu cũ của tôi, help trapcho biết "Nếu một TÍN HIỆU là DEBUG, ARG được thực thi sau mỗi lệnh đơn giản" [nhấn mạnh của tôi].
LarsH

1
Tôi đã sử dụng kết hợp câu trả lời này với một số nội dung đặc biệt trong câu trả lời được chấp nhận : trap '[ -n "$COMP_LINE" ] && [ "$BASH_COMMAND" != "$PROMPT_COMMAND" ] && date "+%X";echo -e "\e]0;$BASH_COMMAND\007"' DEBUG. Điều này đặt lệnh vào tiêu đề và cũng in thời gian hiện tại ngay trước mỗi lệnh, nhưng không làm như vậy khi thực thi $PROMPT_COMMAND.
coredumperror

1
@CoreDumpError, vì bạn đã cấu trúc lại mã, bạn nên phủ nhận tất cả các điều kiện: do đó, điều đầu tiên sẽ trở thành : [ -z "$COMP_LINE" ].
cYrus

@cYrus Cảm ơn! Tôi không biết lập trình bash gần như đủ để nhận thấy vấn đề đó.
coredumperror

@LarsH: Bạn có phiên bản nào? Tôi có BASH_VERSION = "4.3.11 (1) -release" và thông báo "ARG được thực thi trước mỗi lệnh đơn giản."
musiphil

12

Đây không phải là một hàm shell được thực thi, nhưng tôi đã đóng góp một $PS0chuỗi dấu nhắc được hiển thị trước khi mỗi lệnh được chạy. Chi tiết tại đây: http://stromberg.dnsalias.org/~strombrg/PS0-prompt/

$PS0được bao gồm trong bash4.4, mặc dù sẽ mất một thời gian để hầu hết các Linux có thể bao gồm 4.4 - bạn có thể tự xây dựng 4.4 nếu muốn; trong trường hợp đó, có lẽ bạn nên đặt nó bên dưới /usr/local, thêm nó vào /etc/shellschshcho nó. Sau đó đăng xuất và đăng nhập lại, có thể là sshvào chính bạn @ localhost hoặc sutự mình kiểm tra trước.


11

Gần đây tôi đã phải giải quyết vấn đề chính xác này cho một dự án phụ của tôi. Tôi đã thực hiện một giải pháp khá mạnh mẽ và linh hoạt, mô phỏng chức năng preexec và premd của zsh cho bash.

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

Ban đầu nó dựa trên giải pháp của Glyph Lefkowitz, nhưng tôi đã cải thiện nó và cập nhật nó. Rất vui khi được giúp đỡ hoặc thêm một tính năng nếu cần.


3

Cảm ơn bạn đã gợi ý! Tôi đã kết thúc bằng cách sử dụng này:

#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

Thưởng thức!


Tôi gặp vấn đề với các lệnh bash bị treo ... Tôi đã tìm thấy một cách giải quyết bằng cách sử dụng một lớp con, nhưng điều này khiến 'history -a' không làm mới lịch sử bên ngoài phạm vi của lớp con ... Cuối cùng, giải pháp là sử dụng một hàm đọc lại lịch sử sau khi thực hiện subshell. Nó hoạt động như tôi muốn. Như Vaidas đã viết trên jablonskis.org/2011/howto-log-bash-history-to-syslog , nó dễ triển khai hơn so với vá bash trong C (tôi cũng đã làm điều đó trong quá khứ). nhưng có một số giảm hiệu suất trong khi đọc lại mỗi lần tệp lịch sử và thực hiện một đĩa 'đồng bộ hóa' ...
francois

5
Bạn có thể muốn cắt mã đó; hiện tại nó gần như không thể đọc được.
l0b0

3

Tôi đã viết một phương pháp để ghi nhật ký tất cả các lệnh / bash 'vào tệp văn bản hoặc máy chủ' syslog 'mà không cần sử dụng một bản vá hoặc một công cụ thực thi đặc biệt.

Nó rất dễ triển khai, vì nó là một shellscript đơn giản cần được gọi một lần khi khởi tạo 'bash'.

Xem phương pháp tại đây .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.