29

I often use find or locate to find out about paths.

(~) locate foobar.mmpz
/home/progo/lmms/projects/foobar.mmpz

The next step is often to open or otherwise manipulate the files. In a happy case like above, I can do this:

(~) ls `!!`
ls `locate foobar.mmpz`
/home/progo/lmms/projects/foobar.mmpz

But nobody's too happy when there are many lines of output, some of which may not be paths or something else of that kind. Besides, rerunning potentially wasteful commands is not that elegant either.

Would there be a way to hook up zsh to store the stdout into an array for later manipulation? After all, it's the shell's job to redirect the streams to the user. I'm thinking it could store the first N and last N lines in a variable for immediate later use, like $? and others.

Ok so this is pretty cool: https://unix.stackexchange.com/a/59704/5674. I'm now asking about the zsh know-how (and porting the code to zsh) to rig this kind of capture after each run line.

mike3996
  • 1,549
  • It's not really the shell's job to redirect the stream to the user. The applications write their output to the terminal device, the shell is not involved in that. – Stéphane Chazelas Feb 26 '14 at 14:22
  • @StephaneChazelas, but it is the shell's job to, say, redirect streams into files per user's requests. There are also recipes for zsh that color stderr in red to separate it from stdout. I think that indicates the shell's role in the streaming matters. – mike3996 Feb 26 '14 at 14:34
  • Yes, you could do it using screen or script and precmd and preexec hooks. – Stéphane Chazelas Feb 27 '14 at 10:09

5 Answers5

13

There is no feature to capture the output from the screen on most terminal emulators. I seem to recall the author of xterm (the “reference” terminal emulator) stating that it would be difficult to implement. Even if that was possible, the shell would have to keep track of where the last prompt had been.

So you won't escape having to run the command again, unless you use a terminal-specific, manual mechanism such as copy-pasting with the mouse in xterm or with the keyboard in Screen.

It would be highly impractical for the shell to automatically capture the output of commands, because it cannot distinguish between commands that have complex terminal and user interactions from commands that simply output printable characters.

You can rerun the command and capture its output. There are various ways to do each. To rerun the command, you can use:

  • !! history substitution — most convenient to type;
  • fc -e -, which can be used in a function.

To capture the output, you can use command substitution, or a function like the following:

K () {
  lines=("${(f@)$(cat)}")
}
!! |K

This sets the lines array to the output of the command that's piped into it.

  • So be it. I now realize in light of good explanations that a completely automatic approach is too difficult as is, the second best thing is something like that K of yours. – mike3996 Feb 27 '14 at 08:52
5

Here's a first cut of something to put the last line of output in a variable called $lastline.

precmd () {                                                                                                                                                                                                        
    exec 2>&- >&-
    lastline=$(tail -1 ~/.command.out) 
    sleep 0.1   # TODO: synchronize better
    exec > /dev/tty 2>&1
}

preexec() {
    exec > >(tee ~/.command.out&)
}

This uses zsh's preexec hook to run exec with tee to store a copy of the command's stdout, then uses precmd to read the stored output and restore stdout to be just the terminal for showing the prompt.

But it still needs some work. For example, since stdout is no longer a terminal, programs such as vim and less don't work properly.

There's some useful related information in these questions:

Mikel
  • 57,299
  • 15
  • 134
  • 153
  • Yes, perhaps a 100% automatic approach is not going to work. There are many exec calls in the code, is the command running multiple times or am I caught in special semantics? – mike3996 Feb 27 '14 at 08:48
  • 1
    exec without a program name just sets redirections. – Mikel Feb 27 '14 at 15:06
4

I came up with this half-baked solution:

alias -g ___='"$(eval "$(fc -ln -1)" | tail -n 1)"'

This allows you to write ___ at any point in the command line. The previous command will be re-run and the ___ will be replaced with the last line of its output. Example usage:

$ touch foo bar baz
$ ls -1
bar
baz
foo
$ vim ___

The last command will be expanded to vim foo.

This does have some sharp edges!

  • If you include ___ in a command but the previous command also included a ___, the shell will hang in some weird state for a while. You can get out of this state immediately with Ctrl-C.

  • You also can’t press Tab to expand the ___, like you can with !$ and other constructions.

  • Some commands will display different output when they are run “normally” and when they are attached to a pipe. (Compare the output of ls and ls | cat.) If the command triggered by ___ is one of these, you might end up running a different command than you expected.

  • And of course, if you wanted to do something with an output line other than the last one, this won’t help you.

I picked the name ___ because it’s something I’ve never wanted to include as a word in a command line, not even as an argument. You could pick a different name, but take care not to choose something that might be expanded for you inadvertently.

bdesham
  • 1,307
  • 2
  • 13
  • 23
3

You can do this by just piping your results to tee, which just saves the output to a file and displays it at the same time.

So for example, you could do this, which shows your output like normal, but also saves it to the file /tmp/it

locate foobar.mmpz | tee /tmp/it

then cat that file and grep it to select things, e.g.

cat /tmp/it | grep progo | grep lms

then to use it, you could just do this:

vi $(!!)

Brad Parks
  • 1,669
0

A zsh widget for the task:

# ctrl-x ctrl-l: edit last command output in neovim
zmodload -i zsh/parameter
vim-last-command-output() {
    LBUFFER+="vim $(eval $history[$((HISTCMD-1))])"
    zle accept-line
}
zle -N vim-last-command-output
bindkey '^x^n' vim-last-command-output
SergioAraujo
  • 459
  • 6
  • 8