Есть ли у bash ловушка, которая запускается перед выполнением команды?

111

В bash могу ли я организовать выполнение функции непосредственно перед выполнением команды?

Существует $PROMPT_COMMAND, который выполняется перед отображением приглашения, т. Е. Сразу после запуска команды.

Bash $PROMPT_COMMANDаналогичен precmdфункции Zsh ; так что я ищу bash, эквивалентный Zsh preexec.

Примеры приложений: установите заголовок терминала для выполняемой команды; автоматически добавлять timeперед каждой командой.

Жиль
источник
3
В bash версии 4.4 есть PS0переменная, которая действует аналогично, PS1но используется после чтения команды, но перед ее выполнением. См. Gnu.org/software/bash/manual/bashref.html#Bash-Variables
Гленн Джекман

Ответы:

93

Не изначально, но его можно взломать с помощью DEBUGловушки. Этот код настраивается preexecи precmdфункционирует подобно zsh. Командная строка передается как один аргумент preexec.

Вот упрощенная версия кода для настройки 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

Этот трюк из-за Глифа Лефковица ; спасибо bcat за поиск оригинального автора.

Редактировать. Обновленную версию взлома Glyph можно найти здесь: https://github.com/rcaloras/bash-preexec

Жиль
источник
"$BASH_COMMAND" = "$PROMPT_COMMAND"Сравнение не работает для меня i.imgur.com/blneCdQ.png
laggingreflex
2
Я пытался использовать этот код на Cygwin. К сожалению, он имеет довольно сильный эффект производительности - выполнение простой команды тестирования time for i in {1..10}; do true; doneобычно занимает 0,040 секунды и от 1,400 до 1,600 секунд после активации ловушки DEBUG. Это приводит к тому, что команда trap выполняется дважды за цикл - и на Cygwin разветвление, необходимое для выполнения sed, непомерно медленно - примерно 0,030 секунды для разветвления в одиночку (разница в скорости между echoвстроенным и /bin/echo). Что-то, чтобы иметь в виду, может быть.
KDB
2
@kdb Cygwin производительность для вилки отстой. Насколько я понимаю, это неизбежно в Windows. Если вам нужно запустить bash-код в Windows, попробуйте сократить количество разветвлений.
Жиль
@DevNull Это можно очень легко обойти, удалив ловушку. Не существует технического решения для людей, которые делают то, что им разрешено, но не должны делать. Есть частичные меры: не предоставляйте столько людей, сколько доступа, убедитесь, что ваши резервные копии обновлены, используйте контроль версий, а не прямое манипулирование файлами ... Если вы хотите что-то, что пользователи не могут легко отключить, позвольте одни не могут отключить вообще, тогда ограничения в оболочке вам не помогут: их можно удалить так же легко, как и добавить.
Жиль
1
Если у вас есть несколько команд в PROMPT_COMMANDпеременной (например , ограниченная ;), вам может понадобиться использовать сопоставление с образцом во второй строке preexec_invoke_execфункции, так же , как это: [[ "$PROMPT_COMMAND" =~ "$BASH_COMMAND" ]]. Это потому, что BASH_COMMANDпредставляет каждую из команд в отдельности.
Юрислав
20

Вы можете использовать trapкоманду (из help trap):

Если SIGNAL_SPEC - DEBUG, ARG выполняется перед каждой простой командой.

Например, для динамического изменения названия терминала вы можете использовать:

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

Из этого источника.

cYrus
источник
1
Интересно ... на моем старом сервере Ubuntu help trapговорится: «Если SIGNAL_SPEC - DEBUG, ARG выполняется после каждой простой команды» [выделено мной].
LarsH
1
Я использовал комбинацию этого ответа с некоторыми из специального материала , в принятом ответе: trap '[ -n "$COMP_LINE" ] && [ "$BASH_COMMAND" != "$PROMPT_COMMAND" ] && date "+%X";echo -e "\e]0;$BASH_COMMAND\007"' DEBUG. Это помещает команду в заголовок, а также печатает текущее время перед каждой командой, но не выполняет ее при выполнении $PROMPT_COMMAND.
coredumperror
1
@CoreDumpError, так как вы переработан код , который вы должны свести на нет всех условий: первое , следовательно , становится: [ -z "$COMP_LINE" ].
cyrus
@cYrus Спасибо! Я не знаю почти достаточно программирования на bash, чтобы заметить эту проблему.
coredumperror
@LarsH: Какая у вас версия? У меня есть BASH_VERSION = "4.3.11 (1) -release" и там написано "ARG выполняется перед каждой простой командой".
Musiphil
12

Это не функция оболочки, которая выполняется, но я $PS0добавил строку приглашения, которая отображается перед выполнением каждой команды. Подробности здесь: http://stromberg.dnsalias.org/~strombrg/PS0-prompt/

$PS0включен в bash4.4, хотя большинству Linux потребуется некоторое время, чтобы включить 4.4 - хотя вы можете собрать 4.4 самостоятельно, если хотите; в этом случае, вы, вероятно, должны положить его /usr/local, добавить /etc/shellsи chshк нему. Затем выйдите из системы и снова войдите в нее, возможно, sshдля себя @ localhost или suсначала для себя в качестве теста.

dstromberg
источник
11

Недавно мне пришлось решить эту проблему для моего стороннего проекта. Я сделал довольно надежное и гибкое решение, которое имитирует функции zexe preexec и precmd для bash.

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

Первоначально он был основан на решении Глифа Лефковица, но я усовершенствовал его и обновил его. Рад помочь или добавить функцию, если это необходимо.

RCCola
источник
3

Спасибо за подсказки! Я закончил тем, что использовал это:

#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

Наслаждайтесь!

Франсуа Шойрер
источник
У меня была проблема с зависшими командными командами bash ... Я нашел обходной путь с использованием subshell, но это привело к тому, что «history -a» не обновляет историю за пределами области действия subshell ... Наконец, решение заключалось в использовании функции что перечитать историю после выполнения подоболочки. Это работает, как я хотел. Как писал Вайдас на jablonskis.org/2011/howto-log-bash-history-to-syslog , его легче развернуть, чем исправление bash в C (я тоже так делал в прошлом). но есть некоторое падение производительности при повторном чтении файла истории и выполнении синхронизации диска ...
Франсуа Шойрер
5
Возможно, вы захотите урезать этот код; в настоящее время это почти полностью нечитаемо.
10
3

Я написал метод для записи всех команд / встроенных команд bash в текстовый файл или сервер 'syslog' без использования патча или специального исполняемого инструмента.

Его очень легко развернуть, так как это простой сценарий оболочки, который нужно вызывать один раз при инициализации 'bash'.

Смотрите метод здесь .

Francois Scheurer
источник