ChatGPT解决这个技术问题 Extra ChatGPT

Reusing output from last command in Bash

Is the output of a Bash command stored in any register? E.g. something similar to $? capturing the output instead of the exit status.

I could assign the output to a variable with:

output=$(command)

but that's more typing...

since it doesn't seem to be possible with bash, are there other shells that can do that?

B
Boris Verkhovskiy

You can use $(!!) to recompute (not re-use) the output of the last command.

The !! on its own executes the last command.

$ echo pierre
pierre
$ echo my name is $(!!)
echo my name is $(echo pierre)
my name is pierre

You can save yourself from typing $(your_cmd) by using backticks: `your_cmd` According to the Gnu Bash manual they are functionally identical. Of course this is also subject to the warning raised by @memecs
In practice, the output of commands is deterministic so I wouldn't agonize over it recomputing. It's certainly useful when you want to do something like git diff $(!!) where the previous command was a find invocation.
Apparently there are good reasons to use $() instead of backticks. But probably nothing to lose sleep over. github.com/koalaman/shellcheck/wiki/SC2006
@user2065875 although backticks still work in Bash, they are deprecated in favor of $().
What's the difference between recomputing ($(!!)) and executing (!!)?
k
konsolebox

The answer is no. Bash doesn't allocate any output to any parameter or any block on its memory. Also, you are only allowed to access Bash by its allowed interface operations. Bash's private data is not accessible unless you hack it.


Is it possible to have some custom bash 'middleware' function which runs before every command, where all it does is save the resulting output to something that acts as an internal clipboard (say, BASH_CLIPBOARD)?
@PatNeedham Not sure. Check loadable modules. See if you can write one.
C
Connor

Very Simple Solution

One that I've used for years.

Script (add to your .bashrc or .bash_profile)

# capture the output of a command so it can be retrieved with ret
cap () { tee /tmp/capture.out; }

# return the output of the most recent command that was captured by cap
ret () { cat /tmp/capture.out; }

Usage

$ find . -name 'filename' | cap
/path/to/filename

$ ret
/path/to/filename

I tend to add | cap to the end of all of my commands. This way when I find I want to do text processing on the output of a slow running command I can always retrieve it with ret.


The oneline function definition is wrong. To avoid bash error use cap () { tee /tmp/capture.out; } and ret () { cat /tmp/capture.out; }
You can also do this with xclip: xci() { xclip -i; xclip -o; }; xco() { xclip -o; };.
Did not know about xclip! Thank you
I've tried this wit zsh and it's not working.
@TeddyC that would be really nice, but I don't know a way to do it. If you find a way please let me know
c
codeforester

One way of doing that is by using trap DEBUG:

f() { bash -c "$BASH_COMMAND" >& /tmp/out.log; }
trap 'f' DEBUG

Now most recently executed command's stdout and stderr will be available in /tmp/out.log

Only downside is that it will execute a command twice: once to redirect output and error to /tmp/out.log and once normally. Probably there is some way to prevent this behavior as well.


so when I type rm -rf * && cd .., not only the current directory, but also the parent dir will be erased?? this approach seems highly dangerous to me
Now how do I undo this action?
@KartikChauhan: Just do trap '' DEBUG
R
Raghu Dodda

If you are on mac, and don't mind storing your output in the clipboard instead of writing to a variable, you can use pbcopy and pbpaste as a workaround.

For example, instead of doing this to find a file and diff its contents with another file:

$ find app -name 'one.php' 
/var/bar/app/one.php

$ diff /var/bar/app/one.php /var/bar/two.php

You could do this:

$ find app -name 'one.php' | pbcopy
$ diff $(pbpaste) /var/bar/two.php

The string /var/bar/app/one.php is in the clipboard when you run the first command.

By the way, pb in pbcopy and pbpaste stand for pasteboard, a synonym for clipboard.


on linux $ find app -name 'one.php' | xclip $ diff $(xclip -o) /var/bar/two.php
interesting, I'd never thought of using pbpaste together with command substitution.
What if I do the first and later realise it was inefficient, how do I save computational time without rerunning with the second option?
o
olivecoder

Inspired by anubhava's answer, which I think is not actually acceptable as it runs each command twice.

save_output() { 
   exec 1>&3 
   { [ -f /tmp/current ] && mv /tmp/current /tmp/last; }
   exec > >(tee /tmp/current)
}

exec 3>&1
trap save_output DEBUG

This way the output of last command is in /tmp/last and the command is not called twice.


One downside I've found to this is that none of your commands think they are running in a tty anymore, which means you need special attention to get color codes working for utilities like grep or git diff +1 anyway though
This is the real answer
p
phil294

Like konsolebox said, you'd have to hack into bash itself. Here is a quite good example on how one might achieve this. The stderred repository (actually meant for coloring stdout) gives instructions on how to build it.

I gave it a try: Defining some new file descriptor inside .bashrc like

exec 41>/tmp/my_console_log

(number is arbitrary) and modify stderred.c accordingly so that content also gets written to fd 41. It kind of worked, but contains loads of NUL bytes, weird formattings and is basically binary data, not readable. Maybe someone with good understandings of C could try that out.

If so, everything needed to get the last printed line is tail -n 1 [logfile].


H
Himanshu Tanwar

Yeah, why type extra lines each time; agreed. You can redirect the returned from a command to input by pipeline, but redirecting printed output to input (1>&0) is nope, at least not for multiple line outputs. Also you won't want to write a function again and again in each file for the same. So let's try something else.

A simple workaround would be to use printf function to store values in a variable.

printf -v myoutput "`cmd`"

such as

printf -v var "`echo ok;
  echo fine;
  echo thankyou`"
echo "$var" # don't forget the backquotes and quotes in either command.

Another customizable general solution (I myself use) for running the desired command only once and getting multi-line printed output of the command in an array variable line-by-line.

If you are not exporting the files anywhere and intend to use it locally only, you can have Terminal set-up the function declaration. You have to add the function in ~/.bashrc file or in ~/.profile file. In second case, you need to enable Run command as login shell from Edit>Preferences>yourProfile>Command.

Make a simple function, say:

get_prev() # preferably pass the commands in quotes. Single commands might still work without.
{
    # option 1: create an executable with the command(s) and run it
    #echo $* > /tmp/exe
    #bash /tmp/exe > /tmp/out
    
    # option 2: if your command is single command (no-pipe, no semi-colons), still it may not run correct in some exceptions.
    #echo `"$*"` > /tmp/out
    
    # option 3: (I actually used below)
    eval "$*" > /tmp/out # or simply "$*" > /tmp/out
    
    # return the command(s) outputs line by line
    IFS=$(echo -en "\n\b")
    arr=()
    exec 3</tmp/out
    while read -u 3 -r line
    do
        arr+=($line)
        echo $line
    done
    exec 3<&-
}

So what we did in option 1 was print the whole command to a temporary file /tmp/exe and run it and save the output to another file /tmp/out and then read the contents of the /tmp/out file line-by-line to an array. Similar in options 2 and 3, except that the commands were exectuted as such, without writing to an executable to be run.

In main script:

#run your command:

cmd="echo hey ya; echo hey hi; printf `expr 10 + 10`'\n' ; printf $((10 + 20))'\n'"
get_prev $cmd

#or simply
get_prev "echo hey ya; echo hey hi; printf `expr 10 + 10`'\n' ; printf $((10 + 20))'\n'"

Now, bash saves the variable even outside previous scope, so the arr variable created in get_prev function is accessible even outside the function in the main script:

#get previous command outputs in arr
for((i=0; i<${#arr[@]}; i++))
do
    echo ${arr[i]}
done
#if you're sure that your output won't have escape sequences you bother about, you may simply print the array
printf "${arr[*]}\n"

Edit:

get_prev()
{
    usage()
    {
        echo "Usage: alphabet [ -h | --help ]
        [ -s | --sep SEP ]
        [ -v | --var VAR ] \"command\""
    }
    
    ARGS=$(getopt -a -n alphabet -o hs:v: --long help,sep:,var: -- "$@")
    if [ $? -ne 0 ]; then usage; return 2; fi
    eval set -- $ARGS
    
    local var="arr"
    IFS=$(echo -en '\n\b')
    for arg in $*
    do
        case $arg in
            -h|--help)
                usage
                echo " -h, --help : opens this help"
                echo " -s, --sep  : specify the separator, newline by default"
                echo " -v, --var  : variable name to put result into, arr by default"
                echo "  command   : command to execute. Enclose in quotes if multiple lines or pipelines are used."
                shift
                return 0
                ;;
            -s|--sep)
                shift
                IFS=$(echo -en $1)
                shift
                ;;
            -v|--var)
                shift
                var=$1
                shift
                ;;
            -|--)
                shift
                ;;
            *)
                cmd=$option
                ;;
        esac
    done
    if [ ${#} -eq 0 ]; then usage; return 1; fi
    
    ERROR=$( { eval "$*" > /tmp/out; } 2>&1 )
    if [ $ERROR ]; then echo $ERROR; return 1; fi
    
    local a=()
    exec 3</tmp/out
    while read -u 3 -r line
    do
        a+=($line)
    done
    exec 3<&-
    
    eval $var=\(\${a[@]}\)
    print_arr $var # comment this to suppress output
}

print()
{
    eval echo \${$1[@]}
}

print_arr()
{
    eval printf "%s\\\n" "\${$1[@]}"
}

Ive been using this to print space-separated outputs of multiple/pipelined/both commands as line separated:

get_prev -s " " -v myarr "cmd1 | cmd2; cmd3 | cmd4"

For example:

get_prev -s ' ' -v myarr whereis python # or "whereis python"
# can also be achieved (in this case) by
whereis python | tr ' ' '\n'

Now tr command is useful at other places as well, such as

echo $PATH | tr ':' '\n'

But for multiple/piped commands... you know now. :)

-Himanshu


j
jscs

Not sure exactly what you're needing this for, so this answer may not be relevant. You can always save the output of a command: netstat >> output.txt, but I don't think that's what you're looking for.

There are of course programming options though; you could simply get a program to read the text file above after that command is run and associate it with a variable, and in Ruby, my language of choice, you can create a variable out of command output using 'backticks':

output = `ls`                       #(this is a comment) create variable out of command

if output.include? "Downloads"      #if statement to see if command includes 'Downloads' folder
print "there appears to be a folder named downloads in this directory."
else
print "there is no directory called downloads in this file."
end

Stick this in a .rb file and run it: ruby file.rb and it will create a variable out of the command and allow you to manipulate it.


"Not sure exactly what you're needing this for" Well, say you (by which I mean I) just ran a long-duration script, want to search the output, and stupidly forgot to redirect the output before executing. Now I want to try to remedy my stupidity without re-running the script.
G
Giuliano

If you don't want to recompute the previous command you can create a macro that scans the current terminal buffer, tries to guess the -supposed- output of the last command, copies it to the clipboard and finally types it to the terminal.

It can be used for simple commands that return a single line of output (tested on Ubuntu 18.04 with gnome-terminal).

Install the following tools: xdootool, xclip , ruby

In gnome-terminal go to Preferences -> Shortcuts -> Select all and set it to Ctrl+shift+a.

Create the following ruby script:

cat >${HOME}/parse.rb <<EOF
#!/usr/bin/ruby
stdin = STDIN.read
d = stdin.split(/\n/)
e = d.reverse
f = e.drop_while { |item| item == "" }
g = f.drop_while { |item| item.start_with? "${USER}@" }
h = g[0] 
print h
EOF

In the keyboard settings add the following keyboard shortcut:

bash -c '/bin/sleep 0.3 ; xdotool key ctrl+shift+a ; xdotool key ctrl+shift+c ; ( (xclip -out | ${HOME}/parse.rb ) > /tmp/clipboard ) ; (cat /tmp/clipboard | xclip -sel clip ) ; xdotool key ctrl+shift+v '

The above shortcut:

copies the current terminal buffer to the clipboard

extracts the output of the last command (only one line)

types it into the current terminal


W
William Navarre

I have an idea that I don't have time to try to implement immediately.

But what if you do something like the following:

$ MY_HISTORY_FILE = `get_temp_filename`
$ MY_HISTORY_FILE=$MY_HISTORY_FILE bash -i 2>&1 | tee $MY_HISTORY_FILE
$ some_command
$ cat $MY_HISTORY_FILE
$ # ^You'll want to filter that down in practice!

There might be issues with IO buffering. Also the file might get too huge. One would have to come up with a solution to these problems.


t
the sudhakar

I think using script command might help. Something like,

script -c bash -qf fifo_pid Using bash features to set after parsing.


p
patraulea

Demo for non-interactive commands only: http://asciinema.org/a/395092

For also supporting interactive commands, you'd have to hack the script binary from util-linux to ignore any screen-redrawing console codes, and run it from bashrc to save your login session's output to a file.


R
Rikkas

You can use -exec to run a command on the output of a command. So it will be a reuse of the output as an example given with a find command below:

find . -name anything.out -exec rm {} \;

you are saying here -> find a file called anything.out in the current folder, if found, remove it. If it is not found, the remaining after -exec will be skipped.


Downvoted, because -exec is just a find-specific option, not a generic construct. Also, there's nothing here to reuse find's or rm's output afterwards.