Apakah bash memiliki pengait yang dijalankan sebelum menjalankan perintah?

111

Dalam bash, dapatkah saya mengatur agar fungsi dijalankan sebelum menjalankan perintah?

Ada $PROMPT_COMMAND, yang dieksekusi sebelum menampilkan prompt, yaitu, hanya setelah menjalankan perintah.

Bash $PROMPT_COMMANDadalah analog dengan precmdfungsi zsh ; jadi apa yang saya cari adalah bash setara dengan zsh preexec.

Contoh aplikasi: atur judul terminal Anda ke perintah yang dijalankan; secara otomatis ditambahkan timesebelum setiap perintah.

Gilles
sumber
3
bash versi 4.4 memiliki PS0variabel yang berfungsi seperti PS1tetapi digunakan setelah membaca perintah tetapi sebelum menjalankannya. Lihat gnu.org/software/bash/manual/bashref.html#Bash-Variables
glenn jackman

Jawaban:

93

Tidak asli, tetapi bisa diretas menggunakan DEBUGperangkap. Kode ini set up preexecdan precmdfungsi mirip dengan zsh. Baris perintah diteruskan sebagai argumen tunggal ke preexec.

Berikut adalah versi kode yang disederhanakan untuk mengatur precmdfungsi yang dieksekusi sebelum menjalankan setiap perintah.

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

Trik ini disebabkan oleh Glyph Lefkowitz ; terima kasih kepada bcat untuk menemukan penulis asli.

Sunting. Versi terbaru dari peretasan Glyph dapat ditemukan di sini: https://github.com/rcaloras/bash-preexec

Gilles
sumber
The "$BASH_COMMAND" = "$PROMPT_COMMAND"perbandingan tidak bekerja untuk saya i.imgur.com/blneCdQ.png
laggingreflex
2
Saya mencoba menggunakan kode ini di cygwin. Sayangnya itu memiliki efek kinerja yang cukup kuat di sana - menjalankan perintah benchmark sederhana time for i in {1..10}; do true; donemembutuhkan waktu 0,040 detik secara normal dan 1,400 hingga 1,600 detik setelah mengaktifkan perangkap DEBUG. Ini menyebabkan perintah trap dieksekusi dua kali per loop - dan pada Cygwin forking yang diperlukan untuk mengeksekusi sed sangat lambat sekitar 0,030 detik untuk forking saja (perbedaan kecepatan antara echobuiltin dan /bin/echo). Sesuatu yang perlu diingat mungkin.
kdb
2
@kdb Cygwin kinerja untuk garpu menyebalkan. Pemahaman saya adalah bahwa ini tidak dapat dihindari pada Windows. Jika Anda perlu menjalankan kode bash di Windows, cobalah untuk mengurangi forking.
Gilles
@DevNull Ini bisa dengan mudah dielakkan dengan menghapus jebakan. Tidak ada solusi teknis untuk orang-orang melakukan apa yang mereka boleh lakukan tetapi tidak boleh dilakukan. Ada beberapa solusi: jangan beri orang sebanyak mungkin akses, pastikan cadangan Anda mutakhir, gunakan kontrol versi daripada manipulasi file langsung, ... Jika Anda menginginkan sesuatu yang tidak dapat dinonaktifkan dengan mudah oleh pengguna, biarkan sendirian tidak dapat menonaktifkan sama sekali, maka pembatasan dalam shell tidak akan membantu Anda: mereka dapat dihapus semudah mereka dapat ditambahkan.
Gilles
1
Jika Anda memiliki perintah lebih dalam PROMPT_COMMANDvariabel (misalnya dibatasi oleh ;), Anda mungkin perlu menggunakan pencocokan pola di baris kedua dari preexec_invoke_execfungsi, seperti ini: [[ "$PROMPT_COMMAND" =~ "$BASH_COMMAND" ]]. Ini karena BASH_COMMANDmewakili masing-masing perintah secara terpisah.
jirislav
20

Anda dapat menggunakan trapperintah (dari help trap):

Jika SIGNAL_SPEC DEBUG, ARG dijalankan sebelum setiap perintah sederhana.

Misalnya, untuk mengubah judul terminal secara dinamis, Anda dapat menggunakan:

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

Dari sumber ini .

cYrus
sumber
1
Menarik ... di server Ubuntu lama saya, help trapmengatakan "Jika SIGNAL_SPEC adalah DEBUG, ARG dieksekusi setelah setiap perintah sederhana" [penekanan milikku].
LarsH
1
Saya menggunakan kombinasi jawaban ini dengan beberapa hal khusus dalam jawaban yang diterima: trap '[ -n "$COMP_LINE" ] && [ "$BASH_COMMAND" != "$PROMPT_COMMAND" ] && date "+%X";echo -e "\e]0;$BASH_COMMAND\007"' DEBUG. Ini menempatkan perintah ke dalam judul dan juga mencetak waktu saat ini tepat sebelum setiap perintah, tetapi tidak melakukannya saat mengeksekusi $PROMPT_COMMAND.
coredumperror
1
@CoreDumpError, karena Anda telah refactored kode Anda harus meniadakan semua kondisi: yang pertama maka menjadi: [ -z "$COMP_LINE" ].
cYrus
@ cYrus Terima kasih! Saya tidak tahu pemrograman bash yang cukup untuk memperhatikan masalah itu.
coredumperror
@ LarsH: Versi apa yang Anda miliki? Saya memiliki BASH_VERSION = "4.3.11 (1) -release" dan dikatakan "ARG dijalankan sebelum setiap perintah sederhana."
musiphil
12

Ini bukan fungsi shell yang dieksekusi, tapi saya berkontribusi $PS0string prompt yang ditampilkan sebelum setiap perintah dijalankan. Detail di sini: http://stromberg.dnsalias.org/~strombrg/PS0-prompt/

$PS0disertakan dalam bash4.4, meskipun akan memakan waktu cukup lama bagi sebagian besar Linux untuk memasukkan 4.4 - Anda dapat membangun 4.4 sendiri jika Anda mau; dalam hal ini, Anda mungkin harus meletakkannya di bawah /usr/local, tambahkan ke /etc/shellsdan chshke sana. Kemudian logout dan kembali, mungkin sshke diri sendiri @ localhost atau suke diri sendiri terlebih dahulu sebagai ujian.

dstromberg
sumber
11

Saya baru-baru ini harus menyelesaikan masalah yang tepat ini untuk proyek sampingan saya. Saya membuat solusi yang cukup kuat dan tangguh yang mengemulasi fungsionalitas preexec dan precmd zsh untuk bash.

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

Ini awalnya didasarkan pada solusi Glyph Lefkowitz, tetapi saya telah memperbaikinya dan membawanya terkini. Senang membantu atau menambahkan fitur jika diperlukan.

RCCola
sumber
3

Terima kasih atas petunjuknya! Saya akhirnya menggunakan ini:

#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

Nikmati!

Francois Scheurer
sumber
Saya punya masalah dengan perintah bash pipa yang hang ... Saya menemukan solusi menggunakan subkulit, tetapi ini menyebabkan 'sejarah -a' untuk tidak menyegarkan sejarah di luar ruang lingkup subkulit ... Akhirnya solusinya adalah menggunakan fungsi yang membaca kembali sejarah setelah eksekusi subkulit. Ini berfungsi seperti yang saya inginkan. Seperti yang ditulis Vaidas di jablonskis.org/2011/howto-log-bash-history-to-syslog , lebih mudah digunakan daripada menambal bash di C (saya juga pernah melakukannya di masa lalu). tetapi ada beberapa penurunan kinerja saat membaca kembali setiap kali file riwayat dan melakukan 'sinkronisasi' disk ...
francois scheurer
5
Anda mungkin ingin memotong kode itu; saat ini hampir sepenuhnya tidak dapat dibaca.
l0b0
3

Saya menulis metode untuk mencatat semua perintah / built 'bash' ke dalam file teks atau server 'syslog' tanpa menggunakan patch atau alat yang dapat dieksekusi khusus.

Sangat mudah untuk digunakan, karena ini adalah shellscript sederhana yang perlu dipanggil sekali pada inisialisasi 'bash'.

Lihat metodenya di sini .

Francois Scheurer
sumber