是否在Bash中运行最后一条命令?


84

我试图回显在bash脚本中运行的最后一个命令。history,tail,head,sed从解析器的角度来看,当命令代表脚本中的特定行时,我找到了一种可以很好用的方法。但是在某些情况下,例如在将命令插入到case语句中时,我得不到预期的输出:

剧本:

#!/bin/bash
set -o history
date
last=$(echo `history |tail -n2 |head -n1` | sed 's/[0-9]* //')
echo "last command is [$last]"

case "1" in
  "1")
  date
  last=$(echo `history |tail -n2 |head -n1` | sed 's/[0-9]* //')
  echo "last command is [$last]"
  ;;
esac

输出:

Tue May 24 12:36:04 CEST 2011
last command is [date]
Tue May 24 12:36:04 CEST 2011
last command is [echo "last command is [$last]"]

[Q]有人可以帮助我找到一种方法来回显上次运行的命令,而不管该命令在bash脚本中的调用方式/位置如何?

我的答案

尽管我的SO同事们做出了令人赞赏的贡献,但我选择编写一个run函数-该函数将所有参数作为一个命令运行,并在失败时显示该命令及其错误代码-具有以下好处:
-我只需要将要检查的命令run放在一行的前面,并且不影响脚本的简洁性-
每当脚本在这些命令之一中失败时,脚本的最后输出行就是一条消息,清楚地显示了哪个命令失败及其退出代码,这使调试更加容易

示例脚本:

#!/bin/bash
die() { echo >&2 -e "\nERROR: $@\n"; exit 1; }
run() { "$@"; code=$?; [ $code -ne 0 ] && die "command [$*] failed with error code $code"; }

case "1" in
  "1")
  run ls /opt
  run ls /wrong-dir
  ;;
esac

输出:

$ ./test.sh
apacheds  google  iptables
ls: cannot access /wrong-dir: No such file or directory

ERROR: command [ls /wrong-dir] failed with error code 2

我测试了带有多个参数,bash变量作为参数,带引号的参数...的各种命令,但该run函数并未破坏它们。到目前为止,我发现的唯一问题是运行回声中断,但无论如何我都不打算检查回声。


+1,好主意!但是请注意,run()当使用引号时,这将无法正常工作,例如,此操作将失败:run ssh-keygen -t rsa -C info@example.org -f ./id_rsa -N ""
johndodo 2012年

@johndodo:它可能是固定的:只需更改"something"参数即可'"something"'(或者,如果需要"'something'",可以允许something在第一级解释/评估(例如:变量))
Olivier Dulac 2014年

2
我将错误更改run() { $*; … }为更接近正确run() { "$@"; … }的错误,因为错误答案最终导致问题cp退出并显示64错误状态,问题是$*名称中的空格中断了命令参数,但"$@"不会这样做。
Jonathan Leffler 2014年

在Unix StackExchange相关问题:unix.stackexchange.com/questions/21930/...
haridsv

last=$(history | tail -n1 | sed 's/^[[:space:]][0-9]*[[:space:]]*//g')至少在zsh和macOS 10.11上效果更好
phil pirozhkov

Answers:


60

命令历史记录是一种交互式功能。历史记录中仅输入完整的命令。例如,case当外壳解析完该结构后,将其作为一个整体输入。看起来既不使用history内置功能查找历史记录(也不通过外壳扩展(!:p)打印历史记录),这似乎并没有达到您想要的目的,即打印简单命令的调用。

DEBUG陷阱让你任何简单的命令执行前执行命令权。BASH_COMMAND变量中提供了要执行的命令的字符串版本(单词之间用空格分隔)。

trap 'previous_command=$this_command; this_command=$BASH_COMMAND' DEBUG
…
echo "last command is $previous_command"

请注意,这previous_command将在您每次运行命令时更改,因此请将其保存到变量中以使用它。如果您还想知道前一个命令的返回状态,请将二者保存在一个命令中。

cmd=$previous_command ret=$?
if [ $ret -ne 0 ]; then echo "$cmd failed with error code $ret"; fi

此外,如果只想中止失败的命令,请使用set -e来使脚本在第一个失败的命令上退出。您可以显示EXIT陷阱中的最后一个命令。

set -e
trap 'echo "exit $? due to $previous_command"' EXIT

请注意,如果您试图跟踪脚本以查看其作用,请忘记所有这些内容并使用set -x


1
我已经尝试过您的DEBUG陷阱,但无法正常工作,请提供完整的示例吗?-x输出每个命令,但不幸的是,我只希望看到失败的命令(如果将它放在一条[ ! "$? == "0" ]语句中,则可以用我的命令实现
Max

@ user359650:固定。您需要先保存上一个命令,然后才能将其覆盖当前命令。要在命令失败时中止脚本,请使用set -e(通常但并非总是如此,该命令会产生足够好的错误消息,因此您无需提供其他上下文)。
吉尔(Gilles)'所以

感谢您的贡献。我最终写了一个自定义函数(请参阅我的文章),因为您的解决方案开销太大。
最大

惊人的把戏。确定性+1。我有set -e部分和ERR陷阱,您给了我DEBUG部分。非常感谢!
Philippe A.

1
@ JamesThomasMoon1979通常eval echo "${BASH_COMMAND}"可以在命令替换中执行任意代码。这很危险。考虑一个类似cd $(ls -td | head -n 1)—的命令,现在想象命令替换称为rm或类似的东西。
吉尔(Gilles)'所以

170

Bash具有访问最后执行的命令的内置功能。但这是最后一个完整的命令(例如整个case命令),而不是您最初请求的单个简单命令。

!:0 =执行命令的名称。

!:1 =上一个命令的第一个参数

!:* =上一条命令的所有参数

!:-1 =上一条命令的最终参数

!! =前一个命令行

等等

因此,最简单的答案是:

echo !!

...或者:

echo "Last command run was ["!:0"] with arguments ["!:*"]"

自己尝试!

echo this is a test
echo !!

在脚本中,历史记录扩展默认情况下处于关闭状态,您需要使用以下命令启用它

set -o history -o histexpand

7
我见过的最有用的用例是使用sudo访问重新运行最后一个命令,即sudo !!
Travesty3

1
随着set -o history -o histexpand; echo "!!"在bash脚本我仍然得到错误信息:!!: event not found(这是不带引号相同。)
苏珊娜

2
set -o history -o histexpand在脚本中->救生员!谢谢!
AlbertoMegía'15

有什么办法可以将这种行为转化为time函数使用的TIMEFORMAT字符串?即export TIMEFORMAT =“ ***!:0花费%0lR”; / usr / bin / time查找-name“ * .log” ...,该命令不起作用,因为在运行导出时评估了!:0 :(
Martin

我需要阅读更多有关set -o history -o histexpand的用法。我在调用的文件中使用它bash继续打印!而不是最后一个运行命令。这在哪里记录?
Muno

17

阅读了Gilles回答后,我决定看看var是否在陷阱中也可用(以及所需的值)-的确如此!$BASH_COMMANDEXIT

因此,以下bash脚本按预期工作:

#!/bin/bash

exit_trap () {
  local lc="$BASH_COMMAND" rc=$?
  echo "Command [$lc] exited with code [$rc]"
}

trap exit_trap EXIT
set -e

echo "foo"
false 12345
echo "bar"

输出是

foo
Command [false 12345] exited with code [1]

bar永远不会打印出来,因为set -e导致bash在命令失败并且false命令总是失败时(根据定义)退出脚本。该12345传递给false在这里只用来表明该参数传递给失败的命令被捕获,以及(在false命令忽略传递给它的任何参数)


这绝对是最好的解决方案。作品对我来说就像魅力与“集-euo pipefail”
Vukasin

8

我可以通过set -x在主脚本中使用(使脚本打印出执行的每个命令)并编写一个包装脚本来实现此目的,该脚本仅显示了生成的最后一行输出set -x

这是主要脚本:

#!/bin/bash
set -x
echo some command here
echo last command

这是包装器脚本:

#!/bin/sh
./test.sh 2>&1 | grep '^\+' | tail -n 1 | sed -e 's/^\+ //'

运行包装器脚本将其作为输出:

echo last command

3

history | tail -2 | head -1 | cut -c8-999

tail -2返回历史记录的最后两个命令行 head -1仅返回第一行 cut -c8-999仅返回命令行,删除PID和空格。


1
您能否解释一下命令参数是什么?这将有助于了解您的工作
Sigrist,

尽管这可以回答问题,但最好添加一些有关此回答如何有助于解决问题的说明。请阅读如何写一个好的答案以了解更多信息。
Roshana Pitigala,

1

最后一个命令($ _)和最后一个错误($?)变量之间存在竞争条件。如果尝试将其中一个存储在自己的变量中,则由于set命令,两个都已经遇到了新值。实际上,在这种情况下,最后一个命令根本没有任何值。

这是我(几乎)将这两种信息存储在自己的变量中的操作,因此我的bash脚本可以确定是否有任何错误,并使用最后一个运行命令设置标题:

   # This construct is needed, because of a racecondition when trying to obtain
   # both of last command and error. With this the information of last error is
   # implied by the corresponding case while command is retrieved.

   if   [[ "${?}" == 0 && "${_}" != "" ]] ; then
    # Last command MUST be retrieved first.
      LASTCOMMAND="${_}" ;
      RETURNSTATUS='✓' ;
   elif [[ "${?}" == 0 && "${_}" == "" ]] ; then
      LASTCOMMAND='unknown' ;
      RETURNSTATUS='✓' ;
   elif [[ "${?}" != 0 && "${_}" != "" ]] ; then
    # Last command MUST be retrieved first.
      LASTCOMMAND="${_}" ;
      RETURNSTATUS='✗' ;
      # Fixme: "$?" not changing state until command executed.
   elif [[ "${?}" != 0 && "${_}" == "" ]] ; then
      LASTCOMMAND='unknown' ;
      RETURNSTATUS='✗' ;
      # Fixme: "$?" not changing state until command executed.
   fi

如果发生错误,此脚本将保留该信息,并将获取上一个运行命令。由于竞争条件,我无法存储实际值。此外,大多数命令实际上甚至都不关心错误号,它们只是返回与“ 0”不同的值。您会注意到,如果使用bash的错误扩展。

像bash扩展中那样的bash诸如“ intern”脚本之类的东西应该是可能的,但是我对这样的东西并不熟悉,并且它也不兼容。

更正

我没想到,可以同时检索两个变量。尽管我喜欢代码的样式,但我认为它将被解释为两个命令。这是错误的,因此我的答案主要归结为:

   # Because of a racecondition, both MUST be retrieved at the same time.
   declare RETURNSTATUS="${?}" LASTCOMMAND="${_}" ;

   if [[ "${RETURNSTATUS}" == 0 ]] ; then
      declare RETURNSYMBOL='✓' ;
   else
      declare RETURNSYMBOL='✗' ;
   fi

尽管我的帖子可能未获得任何正面评价,但最终我还是自己解决了问题。对于初始职位,这似乎是适当的。:)


1
哦,亲爱的,您只需要一次接收它们,这就能做到:声明RETURNSTATUS =“ $ {?}” LASTCOMMAND =“ $ {_}”;
WGRM '16

例外,效果很好。如果我有其他参数的别名,则仅显示参数。任何结论吗?
WGRM '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.