122

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.

7
  • 1
    +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 [email protected] -f ./id_rsa -N "".
    – johndodo
    Commented Aug 1, 2012 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) Commented Jul 8, 2014 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, where the problem was that the $* broke the command arguments at the spaces in the names, but "$@" would not do so. Commented Sep 11, 2014 at 15:17
  • Related question on Unix StackExchange: unix.stackexchange.com/questions/21930/…
    – haridsv
    Commented Jul 28, 2016 at 17:37
  • last=$(history | tail -n1 | sed 's/^[[:space:]][0-9]*[[:space:]]*//g') worked better, at least for zsh and macOS 10.11 Commented Nov 3, 2016 at 17:22

6 Answers 6

265

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
9
  • 17
    The most useful use case I've seen is for re-running the last command with sudo access, i.e. sudo !!
    – Travesty3
    Commented Jan 29, 2014 at 12:54
  • 5
    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
    Commented Jul 31, 2014 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
    Commented Oct 14, 2016 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
    Commented Nov 1, 2017 at 7:22
  • 2
    !:-1 does not work. And it should not work according to bash's documentation. !$ can be used instead. See gnu.org/software/bash/manual/html_node/Word-Designators.html
    – rwitzel
    Commented Sep 9, 2019 at 12:58
71

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.

8
  • 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
    Commented May 24, 2011 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). Commented May 24, 2011 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
    Commented May 25, 2011 at 9:50
  • Amazing trick. Definitive +1. I had the set -e part and ERR trap, you gave me the DEBUG part. Thanks a lot! Commented Feb 23, 2012 at 18:02
  • 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. Commented Nov 18, 2014 at 19:54
30

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)

2
  • 2
    This is absolutely the best solution. Works like a charm for me with "set -euo pipefail"
    – Vukasin
    Commented Sep 10, 2017 at 20:00
  • 1
    You can also use this in an ERR "signal" trap, to only print the last command if it failed.
    – l0b0
    Commented Dec 13, 2022 at 0: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
1
  • This looks like a slick approach to it. Any Post on your profile for more details? Tx.
    – Cymatical
    Commented Jul 8, 2021 at 21:33
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.

5
  • 1
    Could you do a little explanation what are the commands arguments? It would help understand what you did
    – Sigrist
    Commented May 17, 2019 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 to know more. Commented May 17, 2019 at 21:10
  • Quick and awesome easy. Tx.
    – Cymatical
    Commented Jul 8, 2021 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
    Commented Sep 11, 2021 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. Commented Aug 17, 2022 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. :)

4
  • 1
    Oh dear, you've just to receive them at once and this SEEMS TO BE possible: declare RETURNSTATUS="${?}" LASTCOMMAND="${_}" ;
    – WGRM
    Commented Jul 16, 2016 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
    Commented Jul 16, 2016 at 22:07
  • Looks like a flexible baseline. Gotta test that!
    – Cymatical
    Commented Jul 8, 2021 at 21:39
  • 1
    @Cymatical It still works today, although i'm not using the symbols anymore. :)
    – WGRM
    Commented Jul 9, 2021 at 23:10

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.