通过函数或包装器运行每个 bash 命令

通过函数或包装器运行每个 bash 命令

所以我希望我运行的任何命令都通过管道传输到另一个命令,并且我尝试使用 .bashrc 函数:

* () {
    ${@} | [my command]
}

例如,我真正想要实现的是一种将您运行的最后一个命令的标准输出存储在临时文件中的方法,您可以使用另一个命令调用它:

* () {
    ${@} | tee /tmp/last_command
}

另一种方式来解释我想要实现的目标:类似ALIAS [any_command]='[any_command] | tee /tmp/last_command'

下面是我通过该示例实现的实际示例tee

$ tail -5 /etc/passwd
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething

运行命令时实际发生的情况(但我不必每次都写):

$ tail -5 /etc/passwd | tee /tmp/last_command

这是一个完整的例子:

$ tail -5 /etc/passwd
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething
$ cat /tmp/last_command
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething
somethingsomethingsomethingsomething
$ date
Tue Jan 14 13:26:04 EET 2020
$ cat /etc/last_command
Tue Jan 14 13:26:04 EET 2020

答案1

Bash 不太灵活,您可以在每个命令行之前和之后运行命令/函数 - 但仅在 bash 中捕获输出并不简单。

一些问题包括:

  • 写入管道与写入终端或文件不同(例如,,,ioctl()),许多程序都会检测到这一点(例如,甚至)。您不会想将其与交互式编辑器一起使用,例如.fseek()fstat()manlsvi
  • time诸如“ ”、管道甚至 shell 循环之类的特殊命令可能会使解决方案变得复杂,并且管道会改变返回代码
  • 你不能轻易地用别名“劫持”每个可能的命令,这样的别名会破坏命令行选项/参数,因为alias只有从词汇上来说替换命令
  • 您不能只(覆盖)写入单个文件,即当您发出命令检查最后一个输出时是否存在竞争?
  • 如果你想捕获stdout并且stderr可能会遇到交错问题
  • 如果您使用管道,请记住,当存在管道和某些其他重定向时,bash 会创建子 shell,一个直接的副作用是变量分配:这些变量会在子 shell 中分配(然后被丢弃,在当前 shell 中保持不变) 。 “ .” ( source) 也exit同样受到影响。 (在下面使用 readline 的选项中,Ctrl-j可用于运行此类命令。)

这(大致)是我用于类似问题、称为 的函数等的方法+++因此我可以仅在我想要捕获/处理其输出的命令(例如 X 选择或剪贴板)中添加前缀:

function +() (
  set -f  # no double expand
  eval "$@" | tee >( tr -s "\n" " " | xclip )
  return ${PIPESTATUS[0]}  # real rc
)  

例如,+ find /tmp -name "*.pdf" -mtime -30 它不完美,但基本上满足了我的需要。

上面,@mosvy 建议script,这会创建一个新的 tty(终端),在其中运行一个程序,从而回避所有这些问题。它将所有输出写入父 tty,同时也将其记录到文件中。它记录整个会话或单个命令,因此您仍然面临如何自动调用它的问题。

相反,使用screen(因为与它不同的script是可以与正在运行的实例交互并控制它)和 bash 的PROMPT_COMMAND

if [[ "$TERM" -eq "screen" && -n "$STY" ]]; then
    PROMPT_COMMAND=screenlog
    SCREENLOG="${HOME}/screenlog"
fi

function screenlog() {
    [[ -n "$STY" ]] && {
        screen -S "$STY" -X log off
        [[ -f "${SCREENLOG:=$HOME/screenlog}" ]] && mv "$SCREENLOG" /tmp/last_command
        screen -S "$STY" -X logfile "$SCREENLOG"
        screen -S "$STY" -X log on
    }
    return 0
}

如果您符合上述要求.bashrc,那么您应该可以开始了screen。每次 bash 显示新的命令提示符时都会调用上述函数。它首先检查它是否在屏幕下运行,然后与屏幕对话以发出停止日志记录的命令,将最后的输出写入/tmp/last_command,然后告诉屏幕开始日志记录。

需要注意的是,由于PROMPT_COMMANDs 是在显示提示之前调用的,因此提示和命令(包括诸如退格键之类的编辑)都将出现在日志文件的开头。解决这个问题的一种方法是使用 DEBUG 陷阱模拟“pre-exec”来运行上面的代码,例如: 在执行之前通过程序修改所有 bash 命令。如果您运行“交互式”或终端感知程序(例如mantop),您也会记录终端控制序列,如果您想播放输出,这很有用(尽管script/scriptreplay在这方面更好)。


在底层,bash 使用 readline,这允许将键绑定到 readline 函数、其他键的序列或 shell 函数(但不是这三种类型的组合)。这相对容易做到:

bind '"\C-e": end-of-line'
bind '"\C-j": accept-line'
bind '"\C-m": "\C-e | tee /tmp/last_command\C-j"'

前两行只是偏执地确保Ctrl-ECtrl-J按预期绑定(如果你有一个没有accept-line绑定的终端,你就不再有终端了......)。最后一行将通常绑定到, 的Ctrl- (又名“Return”)替换为以下序列:Maccept-line

  1. Ctrl- E(行结束)
  2. | tee /tmp/last_command(文字)
  3. Ctrl- J(接受线)

当发出的命令本身发生更改时,这在屏幕和历史记录中可见。缺点是这种简单的方法没有“自我意识”,并且盲目地改变每一个命令,甚至是历史上已经应用了更改的命令。

这是一个更复杂的版本,它使用一个函数在发送命令时修改命令:

bind '"\C-e": end-of-line'
bind '"\C-j": accept-line'
bind -x '"\e\C-a": _recorder'

function _record() {
    tee /tmp/this_command
    [[ -f /tmp/this_command ]] && mv /tmp/this_command /tmp/last_command
}

function _recorder() {
    local text="| _record"
    [[ -z "${READLINE_LINE}" ]] && return 0
    [[ "$READLINE_LINE" =~ "${text}"$ ]] || READLINE_LINE+=" $text"
    return 0
}
bind '"\C-m": "\C-e\e\C-a\C-j"'

第 2 步被 bash 函数替换_recorder,绑定到Esc, Ctrl- a(您不需要键入它,它只需要绑定到击键,因为宏只能包含击键)。

这个函数可以是任意智能的,这里它处理输出文件竞争,并在更改输入行之前检查管道是否已经存在。如果存在其他重定向,您也可以将整个命令行包装在子 shell 中(尽管您很快就会发现在 bash 中解析 bash 命令行很复杂)。

您可以通过即时修复历史记录来进一步(!)复合黑客行为:

function myprompt() {
    local _seq _cmdline
    local text="| _record"
    read _seq _cmdline < <(HISTTIMEFORMAT= history 1)  # previous command
    [[ "${_cmdline}" =~ (.*)" ${text}"$ ]] && {
        _cmdline="${BASH_REMATCH[1]}"
        history -d $_seq        # delete entry
        history -s "$_cmdline"  # restore original
    }
}
PROMPT_COMMAND=myprompt

readline 方法还有一个重要的缺陷:通常可以在多行上输入的非简单命令会被这种方法破坏,例如 ( while, for)。

答案2

- 代替“每个命令之后” - “下一个提示之前”就足够了吗?考虑 ( 中bash)

   PROMPT_COMMAND
          If set, the value is executed as a command prior to issuing each primary prompt.

man bash) 帮助?

相关内容