110

I am trying to echo the last command run inside a bash script. I found a way to do it with some history,tail,head,sed which works fine when commands represent a specific line in my script from a parser standpoint. However under some circumstances I don't get the expected output, for instance when the command is inserted inside a case statement:

The script:

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

The output:

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] Can someone help me find a way to echo the last run command regardless of how/where this command is called within the bash script?

My answer

Despite the much appreciated contributions from my fellow SO'ers, I opted for writing a run function - which runs all its parameters as a single command and display the command and its error code when it fails - with the following benefits:
-I only need to prepend the commands I want to check with run which keeps them on one line and doesn't affect the conciseness of my script
-Whenever the script fails on one of these commands, the last output line of my script is a message that clearly displays which command fails along with its exit code, which makes debugging easier

Example script:

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

The output:

$ ./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

I tested various commands with multiple arguments, bash variables as arguments, quoted arguments... and the run function didn't break them. The only issue I found so far is to run an echo which breaks but I do not plan to check my echos anyway.

jww
  • 97,681
  • 90
  • 411
  • 885
Max
  • 12,794
  • 30
  • 90
  • 142
  • +1, brilliant idea! Note however that `run()` doesn't work properly when quotes are used, for instance this fails: `run ssh-keygen -t rsa -C info@example.org -f ./id_rsa -N ""`. – johndodo Aug 01 '12 at 12:25
  • @johndodo: it could be fixed: just change `"something"` in arguments with `'"something"'` (or, rather, `"'something'"`, to allow `something` (ex: variables) to be interpreted/evaluated at the first level, if needed) – Olivier Dulac Jul 08 '14 at 11:31
  • 2
    I've change the erroneous `run() { $*; … }` into a more nearly correct `run() { "$@"; … }` because the erroneous answer ended up yielding the question [`cp` exits with a 64 error status](http://stackoverflow.com/questions/25790736/cp-exits-with-a-64-error-status), where the problem was that the `$*` broke the command arguments at the spaces in the names, but `"$@"` would not do so. – Jonathan Leffler Sep 11 '14 at 15:17
  • Related question on Unix StackExchange: http://unix.stackexchange.com/questions/21930/last-failed-command-in-bash – haridsv Jul 28 '16 at 17:37
  • `last=$(history | tail -n1 | sed 's/^[[:space:]][0-9]*[[:space:]]*//g')` worked better, at least for zsh and macOS 10.11 – phil pirozhkov Nov 03 '16 at 17:22
  • `fc -nl -1` [from here](http://unix.stackexchange.com/a/22417/9781) works equally well and is much simpler – phil pirozhkov Nov 03 '16 at 17:27
  • In order to simulate !! in a bash script, I add this in my .bash_aliases : `alias lastcmd="history | tail -n 2 | head -n 1 | sed 's/^ *[0-9]* *//'"` So "lastcmd" simulate "!!" – 6pi Feb 09 '22 at 13:09

6 Answers6

241

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
rolling_codes
  • 15,174
  • 22
  • 76
  • 112
groovyspaceman
  • 2,584
  • 2
  • 13
  • 5
  • 15
    The most useful use case I've seen is for [re-running the last command with sudo access](http://xkcd.com/149/), i.e. `sudo !!` – Travesty3 Jan 29 '14 at 12:54
  • 4
    With `set -o history -o histexpand; echo "!!"` in a bash script I still get the error message: `!!: event not found` (It's the same without the quotation marks.) – Suzana Jul 31 '14 at 11:41
  • Is there any way to get this behaviour into the TIMEFORMAT string used by the time function? i.e. export TIMEFORMAT="*** !:0 took %0lR"; /usr/bin/time find -name "*.log" ... which doesn't work because the !:0 is evaluated at the time you run the export :( – Martin Oct 14 '16 at 15:51
  • I need to read more about the use of set -o history -o histexpand. My use of it in a file that I call `bash` on keeps printing !! instead of the last run command. Where is this documented? – Muno Nov 01 '17 at 07:22
  • the history is local to the script file , so if you get "event not found" , its because a command hasn't been run yet and so is empty – tsukimi Feb 05 '18 at 02:05
  • 1
    `!:-1` does not work. And it should not work according to bash's documentation. `!$` can be used instead. See https://www.gnu.org/software/bash/manual/html_node/Word-Designators.html – rwitzel Sep 09 '19 at 12:58
  • @rwitzel: You misunderstood the objective. $:-1 gives you back a range of parameters between 0 and 1. You can also use $:4-8. It's in the POSIX spec but your particular version of bash might not support it. – groovyspaceman Dec 02 '21 at 12:40
68

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.

Gilles 'SO- stop being evil'
  • 104,111
  • 38
  • 209
  • 254
  • 1
    I have tried your DEBUG trap but I can't make it work, can you provide a full example please? `-x` outputs every single command but unfortunately I am only interested to see the commands that fail (which I can achieve with my command if I place it inside a `[ ! "$? == "0" ]` statement. – Max May 24 '11 at 12:55
  • @user359650: Fixed. You need to have saved the previous command before it's overwritten by the current command. To abort your script if a command fails, use `set -e` (often, but not always, the command will produce a good enough error message so you don't need to provide further context). – Gilles 'SO- stop being evil' May 24 '11 at 13:08
  • thanks for your contribution. I ended up writing a custom function (see my post) as your solution was too much overhead. – Max May 25 '11 at 09:50
  • Amazing trick. Definitive +1. I had the set -e part and ERR trap, you gave me the DEBUG part. Thanks a lot! – Philippe A. Feb 23 '12 at 18:02
  • Nice trick, but the answer by groovyspaceman doesn't require `trap` nor any cunning function/command definitions. – KomodoDave Jun 20 '13 at 10:05
  • @KomodoDave But as I mention in my first paragraph, it doesn't do what was required in the original question (I see the question was since then updated with an answer that doesn't meet the requirements…), which was to print simple commands rather than complete parse units. – Gilles 'SO- stop being evil' Jun 20 '13 at 11:46
  • One problem with the above trap is `$BASH_COMMAND` won't have variables evaluated. e.g. `${bad_command} || echo "ERROR: ${command_prev}"` will echo as `ERROR: $bad_command` . To see `$bad_command` evaluated, try this modified trap statement (uses `eval`) **`trap 'command_prev="${command_curr}" ; command_curr=$(eval echo "${BASH_COMMAND}")' DEBUG`** Now, you will see message `ERROR: cp bad-file bad-place` when you do `echo ERROR: ${command_prev}` – JamesThomasMoon Nov 18 '14 at 19:33
  • 1
    @JamesThomasMoon1979 In general `eval echo "${BASH_COMMAND}"` could execute arbitrary code in command substitutions. It's dangerous. Consider a command like `cd $(ls -td | head -n 1)` — and now imagine the command substitution called `rm` or something. – Gilles 'SO- stop being evil' Nov 18 '14 at 19:54
27

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)

Community
  • 1
  • 1
Hercynium
  • 929
  • 9
  • 18
  • 2
    This is absolutely the best solution. Works like a charm for me with "set -euo pipefail" – Vukasin Sep 10 '17 at 20:00
  • You can also use this in an `ERR` "signal" trap, to only print the last command if it failed. – l0b0 Dec 13 '22 at 00:11
11

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
Mark Drago
  • 1,978
  • 14
  • 10
9

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.

Fredrick Brennan
  • 7,079
  • 2
  • 30
  • 61
Guma
  • 155
  • 2
  • 4
  • 1
    Could you do a little explanation what are the commands arguments? It would help understand what you did – Sigrist May 17 '19 at 20:20
  • While this may answer the question it's better to add some description on how this answer may help to solve the issue. Please read [_How do I write a good answer_](https://stackoverflow.com/help/how-to-answer) to know more. – Roshana Pitigala May 17 '19 at 21:10
  • Quick and awesome easy. Tx. – Cymatical Jul 08 '21 at 21:34
  • There are a lot of cases this command might break. For instance, when setting `HISTTIMEFORMAT`, or for multi-line commands, or for histories with more than 99'999 entries. Btw: `history` simply enumerates all commands in the history. The numbers have nothing to do with PIDs. – Socowi Sep 11 '21 at 16:57
  • Note: I edited this to remove 999 from `cut -c8-999` because it worked on my system. Inferior versions of `cut` may need it so am noting the removal. – Fredrick Brennan Aug 17 '22 at 21:13
3

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. :)

WGRM
  • 129
  • 6
  • 1
    Oh dear, you've just to receive them at once and this SEEMS TO BE possible: declare RETURNSTATUS="${?}" LASTCOMMAND="${_}" ; – WGRM Jul 16 '16 at 21:49
  • Works great with one exception. If i have an alias for further parameters, it just displays the parameters. Anyone any conclusions? – WGRM Jul 16 '16 at 22:07
  • Looks like a flexible baseline. Gotta test that! – Cymatical Jul 08 '21 at 21:39
  • 1
    @Cymatical It still works today, although i'm not using the symbols anymore. :) – WGRM Jul 09 '21 at 23:10