重复 Bash 中的最后一次命令运行?

我试图在 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 ]有人可以帮助我找到一种方法来回显最后一个 run 命令,而不管这个命令在 bash 脚本中如何/在哪里被调用?

我的回答

尽管我的同事们做出了非常值得赞赏的贡献,我还是选择编写了一个 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函数没有破坏它们。到目前为止,我发现的唯一问题是运行一个中断的回声,但我不打算检查我的回声无论如何。

130753 次浏览

The command history is an interactive feature. Only complete commands are entered in the history. For example, the case construct is entered as a whole, when the shell has finished parsing it. Neither looking up the history with the history built-in (nor printing it through shell expansion (!:p)) does what you seem to want, which is to print invocations of simple commands.

The DEBUG trap lets you execute a command right before any simple command execution. A string version of the command to execute (with words separated by spaces) is available in the BASH_COMMAND variable.

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

Note that previous_command will change every time you run a command, so save it to a variable in order to use it. If you want to know the previous command's return status as well, save both in a single command.

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

Furthermore, if you only want to abort on a failed commands, use set -e to make your script exit on the first failed command. You can display the last command from the EXIT trap.

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

Note that if you're trying to trace your script to see what it's doing, forget all this and use set -x.

I was able to achieve this by using set -x in the main script (which makes the script print out every command that is executed) and writing a wrapper script which just shows the last line of output generated by set -x.

This is the main script:

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

And this is the wrapper script:

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

Running the wrapper script produces this as output:

echo last command

Bash has built in features to access the last command executed. But that's the last whole command (e.g. the whole case command), not individual simple commands like you originally requested.

!:0 = the name of command executed.

!:1 = the first parameter of the previous command

!:4 = the fourth parameter of the previous command

!:* = all of the parameters of the previous command

!^ = the first parameter of the previous command (same as !:1)

!$ = the final parameter of the previous command

!:-3 = all parameters in range 0-3 (inclusive)

!:2-5 = all parameters in range 2-5 (inclusive)

!! = the previous command line

etc.

So, the simplest answer to the question is, in fact:

echo !!

...alternatively:

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

Try it yourself!

echo this is a test
echo !!

In a script, history expansion is turned off by default, you need to enable it with

set -o history -o histexpand

After reading the answer from Gilles, I decided to see if the $BASH_COMMAND var was also available (and the desired value) in an EXIT trap - and it is!

So, the following bash script works as expected:

#!/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"

The output is

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

bar is never printed because set -e causes bash to exit the script when a command fails and the false command always fails (by definition). The 12345 passed to false is just there to show that the arguments to the failed command are captured as well (the false command ignores any arguments passed to it)

There is a racecondition between the last command ($_) and last error ( $?) variables. If you try to store one of them in an own variable, both encountered new values already because of the set command. Actually, last command hasn't got any value at all in this case.

Here is what i did to store (nearly) both informations in own variables, so my bash script can determine if there was any error AND setting the title with the last run command:

   # 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

This script will retain the information, if an error occured and will obtain the last run command. Because of the racecondition i can not store the actual value. Besides, most commands actually don't even care for error noumbers, they just return something different from '0'. You'll notice that, if you use the errono extention of bash.

It should be possible with something like a "intern" script for bash, like in bash extention, but i'm not familiar with something like that and it wouldn't be compatible as well.

CORRECTION

I didn't think, that it was possible to retrieve both variables at the same time. Although i like the style of the code, i assumed it would be interpreted as two commands. This was wrong, so my answer devides down to:

   # 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

Although my post might not get any positive rating, i solved my problem myself, finally. And this seems appropriate regarding the intial post. :)

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

tail -2 returns the last two command lines from history head -1 returns just first line cut -c8- returns just command line, removing PID and spaces.