18

I know I can run the last command in bash, with !!, but how can I run the last line of output?

I am thinking of the use case of this output:

The program 'git' is currently not installed. You can install it by typing:
sudo apt-get install git

But I don't know how I can run that. I'm thinking of something like the !!, maybe @@ or similar?


Super User has this question too.

Tim
  • 32,861
  • 27
  • 118
  • 178
  • 1
    Why don't you just copy it and paste? – Pilot6 May 09 '15 at 21:52
  • 1
    @Tim You want ways to run the last line of a program's output, but for this particular use case, there's also the approach of changing the command_not_found_handle shell function or a script it runs (usually just /usr/lib/command-not-found). I think you could make it so you're prompted interactively with the option to automatically install the package, or (probably better) so that the name of the package is stored somewhere and can be installed by running a short command. That's different enough from what you've asked here, you might consider posting a separate question about it. – Eliah Kagan May 10 '15 at 14:57
  • 2
    @EliahKagan http://askubuntu.com/questions/621912/how-to-install-a-program-bash-suggests-i-install – Tim May 10 '15 at 15:05
  • 2
    Might also be relevant: https://github.com/nvbn/thefuck (Ignore the name) this tool auto corrects git/apt/etc commands :) – JaDogg Jul 20 '18 at 09:17

5 Answers5

21

The command $(!! |& tail -1) should do:

$ git something
The program 'git' is currently not installed. You can install it by typing:
sudo apt-get install git

$ $(!! |& tail -1)
$(git something |& tail -1)
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following extra packages will be installed:
git-man liberror-perl

As you can see the sudo apt-get install git command is running.

EDIT : Breaking down $(!! |& tail -1)

  • $() is the bash command substitution pattern

  • bash will expand !! to the last executed command

  • |& part is tricky. Normally pipe | will take the STDOUT of the left sided command and pass it as STDIN to the command on the right side of |, in your case the previous command prints its output on STDERR as an error message. So, doing | would not help, we need to pass both the STDOUT and STDERR (or only STDERR) to the right sided command. |& will pass the STDOUT and STDERR both as the STDIN to the right sided command. Alternately, better you can only pass the STDERR:

    $(!! 2>&1 >/dev/null | tail -1)
    
  • tail -1 as usual will print the last line from its input. Here rather than printing the last line you can be more precise like printing the line that contains the apt-get install command:

    $(!! 2>&1 >/dev/null | grep 'apt-get install')
    
heemayl
  • 91,753
  • 2
    Your approach assume the output will be identical the second time around. Some commands can produce a different output the next time around, in which case it is very difficult to predict what executing the last line of its output is actually going to do. – kasperd May 10 '15 at 12:46
  • 1
    @kasperd You are right..althouh as far as this specific example is concerned, the output should remain the same.. – heemayl May 10 '15 at 13:15
10

TL;DR: alias @@='$($(fc -ln -1) |& tail -1)'

Bash's history interaction facilities don't offer any mechanism to examine the output of commands. The shell doesn't store that, and history expansion is specifically for commands you have yourself run, or parts of those commands.

This leaves the approach of rerunning the last command and piping both stdout and stderr (|&) into a command substitution. heemayl's answer achieves this, but cannot be used in an an alias because the shell performs history expansion before expanding aliases, and not after.

I can't get history expansion to work in a shell function either, even by enabling it in the function with set -H. I suspect !! in a function will never be expanded, and I'm not sure what it would be expanded to if it were, but right now I'm not sure precisely why it isn't.

Therefore, if you want to set things up so you can do this with very little typing, you should use the fc shell builtin instead of history expansion to extract the last command from the history. This has the additional advantage that it works even when history expansion is disabled.

As shown in Gordon Davisson's answer to Creating an alias containing bash history expansion (on Super User), $(fc -ln -1) simulates !!. Plugging this in for !! in heemayl's command $(!! |& tail -1) yields:

$($(fc -ln -1) |& tail -1)

This works like $(!! |& tail -1) but can go in an alias definition:

alias @@='$($(fc -ln -1) |& tail -1)'

After you run that definition, or put it in .bash_aliases or .bashrc and start a new shell, you can simply type @@ (or whatever you named the alias) to attempt to execute the last line of output from the last command.

ek@Io:~$ alias @@='$($(fc -ln -1) |& tail -1)'
ek@Io:~$ evolution
The program 'evolution' is currently not installed. You can install it by typing:
sudo apt-get install evolution
ek@Io:~$ @@
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following extra packages will be installed:
  evolution-common evolution-data-server evolution-data-server-online-accounts
....
Eliah Kagan
  • 117,780
  • Is there any option to move this to a separate script? I tried that and finally failed, because fc outputs nothing... As fc follows the history of the current shell session, so moving it to a separate script opens a different session there with empty history, of course... I added a few commands above the fc line in the script, and one of them was executed. So, I wonder whether there is any option to tell the script, that its "context" is the bash session, which executed it. Like, some arguments for #!/bin/bash or something... – Exterminator13 Jun 19 '19 at 16:47
  • @Exterminator13 If you mean a separate script that you run--like./script, not that you source with the . or source builtin--I'm unsure. Bash appends to a file on shell exit or when you run history with -a or -w (see help history). If it did, commands in multiple interactive shells would be interleaved (appearing in the order you ran them). You can make it append immediately.. But I think there's more to do to make fc work in a script. I suggest posting a new question about this. If you do, please feel free to comment here with a link to it. – Eliah Kagan Jun 19 '19 at 18:20
3

If you are using a terminal emulator under X, like gnome-terminal, konsole or xterm, you can use the X selection for something like copy and paste:

Use the primary selection

Select the whole line to run with a triple left click.

Then paste it to the command line using a middle button click.

This should work in most terminal emulators. It makes use of the primary selection, without touching the clipboard - they are separate.
It's selecting, and then combined copy and paste in one operation, technically.

Volker Siegel
  • 13,065
  • 5
  • 49
  • 65
2

As Eliah Kagan mentioned, the shell does not store the command output. However, there is a way to get the last command's last line of output, without running the command again, if you run screen.

Put the following lines into your ~/.screenrc:

register t "^a[k0y$ ^a]^a^L"
bind t process t

Then you can press Ctrl+at to insert the previous line in the current prompt. It would be possible to make this also automatically press enter, but it's probably a better idea to check it before you run and just press enter manually.

How it works:

  • register t registers a string into a buffer named t. The string following that is a key sequence.
  • bind t binds to the key t (i.e. execute using Ctrl+at), process t means evaluate the content of the buffer named t.
  • The sequence itself means:
    • ^a[ (= Ctrla[): enter copy mode
    • k go one line up, 0 go to the beginning of the line, y$ copy until end of the line, (space) complete the command
    • ^a] (= Ctrla]): paste copied content
    • ^a^L (= CtrlaCtrlL) refresh the screen (otherwise the pasted content won't show up, because you didn't type it yourself
atextor
  • 21
0

TL;DR: Should you need to run the secondlast line of output of the last command:

alias @@='$($(fc -ln -1) |& tail -2 | head -1)'

In Ubuntu 19.10, when a command is not found and the sudo apt install command is displayed, there's a blank line before the next command prompt so nothing happens when using: alias @@='$($(fc -ln -1) |& tail -1)'

net@net:~$ pip install -e ".[uvloop]"

Command 'pip' not found, but can be installed with:

sudo apt install python-pip

net@net:~$ @@
net@net:~$

In this case you have to grab the secondlast line of output of the last command: alias @@='$($(fc -ln -1) |& tail -2 | head -1)'

    net@net:~$ pip install -e ".[uvloop]"                                                                  

Command 'pip' not found, but can be installed with:

sudo apt install python-pip

net@net:~$ @@
[sudo] password for net: 
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following additional packages will be installed:
...
...
StefTN
  • 331
  • 2
  • 5