3

When I run find or locate, the matching files will populate stdout, one file per line. The next step is often that I want to open one of these files. This would be faster and more efficient if I did not have to type out the entire file path, but simply could interact with the search results somehow. Which is the fastest way to open a file displayed in the search results?

Solutions like those given in Open a result of `locate` with `vi`, How to `locate` multiple files and open them in vim?, and How can I act on the results of the "locate" command? require typing out a rather long second command, which is not as fast as I would like.

Instead, is there a way to, for example, automatically assign each file from the search result to a numerical variable name (s1 - sn), so that to open the first result I would simply type vi $s1? Or is a better solution to this problem to use a fuzzy finder like fzf or fasd?

joelostblom
  • 1,931

5 Answers5

6

I've had this in my ~/.screenrc for a while:

bind -c pasteline 1 eval copy 'stuff "-Y"' 'paste .'
bind -c pasteline 2 eval copy 'stuff "2-Y"' 'paste .'
bind -c pasteline 3 eval copy 'stuff "3-Y"' 'paste .'
bind -c pasteline 4 eval copy 'stuff "4-Y"' 'paste .'
bind -c pasteline 5 eval copy 'stuff "5-Y"' 'paste .'
bind -c pasteline 6 eval copy 'stuff "6-Y"' 'paste .'
bind -c pasteline 7 eval copy 'stuff "7-Y"' 'paste .'
bind -c pasteline 8 eval copy 'stuff "8-Y"' 'paste .'
bind -c pasteline 9 eval copy 'stuff "9-Y"' 'paste .'
bindkey ¬ command -c pasteline

Basically, typing ¬1 from within screen, inserts the first line above the cursor, ¬2 the second line and so on.

In my ~/.Xdefaults, I also have:

XTerm.VT100.translations:             #override\
    Meta <KeyPress> /: dabbrev-expand()

Which lets xterm complete (upon Alt+/) on what's on the screen (looking backward from the cursor position).

With zsh, when used within screen, you could do:

copy-screen() {
  screen -X eval copy 'stuff "-$ H\r"' 'writebuf .lastoutput'
  killring=(${(Oaf)"$(<~/.lastoutput)"})
  CUTBUFFER=$killring[1]
  killring[1]=()
}
zle -N copy-screen
bindkey '\ec' copy-screen

to bind Alt+C to that widget that stores the lines above the cursor into the cut buffer and the kill ring (what you paste with Ctrl+Y and cycle through with Alt+Y in emacs mode). (the above assumes screen was started from your home directory).

If the inserted text needs to be quoted (because it contains spaces or other special shell characters for instance), you can type Alt+" for zsh to quote it.

As an example, you've just run:

$ find /usr/local -size +1M
/usr/local/lib/liblzma.a
/usr/local/share/doc/sudo/ChangeLog
/usr/local/share/perl/5.18.2/Unicode/Unihan/Definition.db
/usr/local/share/perl/5.18.2/Unicode/Unihan/RSKangXi.db
/usr/local/share/perl/5.18.2/Unicode/Unihan/IRG_TSource.db
/usr/local/share/perl/5.18.2/Unicode/Unihan/HanYu.db
/usr/local/share/perl/5.18.2/Unicode/Unihan/RSUnicode.db
/usr/local/share/perl/5.18.2/Unicode/Unihan/IRG_GSource.db
/usr/local/share/perl/5.18.2/Unicode/Unihan/IRGKangXi.db
/usr/local/share/perl/5.18.2/Unicode/Unihan/IRGHanyuDaZidian.db    

And you want to open vim on that sudo ChangeLog above. With the first approach you'd type:

vim ¬9

With the second approach:

vim /usAlt+/

And repeat that Alt+/ until you get to the changelog.

With the third approach:

vim Alt+CCtrl+YAlt+Y

And repeat that Alt+Y until you get to the changelog.

The last approach can be used for your $s1 query.

Instead of storing in the killring array, store in an array (like $s) and use $s[1] for the first line, $s[2] for the second...

copy-screen() {
  screen -X eval copy 'stuff "-$ H\r"' 'writebuf .lastoutput'
  s=(${(Oaf)"$(<~/.lastoutput)"})
}
zle -N copy-screen
bindkey '\ec' copy-screen

That Alt+C stores the lines above the cursor in the s array.

In any case, what we're retrieving is what is displayed on the screen, which is not necessarily the same as what the last command output. For instance printf 'a\bc \n' outputs 5 bytes a, BS, c, SPC and LF, but displays just a c.

  • Great! Thank you for this detailed answer! I am not using screen myself, but you inspired my solution using tmux. I also found this tmux plugin that seems to perform a similar function although I prefer the simpler solution I have already. – joelostblom Mar 03 '17 at 04:54
4

With bash:

IFS=$'\n' select fname in $(locate fubar); do
    if [ "$fname" ]; then
        vim "$fname"
        break
    fi
done

With Vim:

!!locate fubar

-- then go to the file you want and press gf. See also :h gf and :h 'isfname'.

Another way with Vim: see :h :find. With recent versions of Vim see :h :filter.

Yet another way with Vim: use the Unite plugin. Or, for grep-like operations, use the CtrlSF plugin. There are, of course, many other ways.

Satō Katsura
  • 13,368
  • 2
  • 31
  • 50
4

If you can bear with the ugliness of the Bash arrays, you could do something like

mapfile res < <(find -name <pattern>)

or

mapfile res < <(locate <pattern>)

This will save your lines into an array res.

Then you can see the matches and iterate through them:

$ echo "${res[@]}" # lists all matches
$ editor ${res[2]} # opens the third match

PS

I just use the mouse to select the line I need, though. Or just do

$ editor `locate <pattern>`

if I know there aren't many results or "fancy" characters.

undercat
  • 1,857
1

I combined ideas Sato Katsura's and Stéphane Chazelas's answers into a script, which is aliased to sr (for "Select Result"). It can be called after a command that outputs paths line by line to stdout. For example

$ locate genomics
/home/user/articles/macaulay_voet_2014_plos_genetics.pdf
/home/user/articles/shmulevich_et_al_2003_comparative_functional_genomics.pdf

$ sr zh
1) /home/user/articles/macaulay_voet_2014_plos_genetics.pdf
2) /home/user/articles/shmulevich_et_al_2003_comparative_functional_genomics.pdf
#? 

The script then waits for me to type a number and press enter, after which it opens that item in the program specified as the first argument (in this case zathura, for which I have defined the shorthand zh in the script). I have yet to test this extensively, but it is doing everything I want at the moment.

My current solution requires tmux, since it can access the output of the last command without rerunning it (by copying from the terminal). Similar solutions exist for screen as shown in Stéphane Chazelas' answer. If using neither of these, one could simply get the last command from history and then evaluate it again in the select statement. Below is the full script.

#!/usr/bin/env bash

# Enumerate stdout from the previous search command line by line.
# Enable opening a file by passing the desired opening program as an
# argument and then select a number from the output.
# Defaults to using `xdg-open` when no argument is passed.
# Does only work for stdout that list the full path to a file or relative
# the directory this script is being called from.

# Aliases are not expanded within bash scripts.
# Create a few aliases instead of importing entire alias file.
case $1 in
    '' )
        opener='xdg-open'
        ;;
    'vi' )
        opener='nvim'
        ;;
    'zh' )
        opener='zathura'
        ;;
    * )
        opener=$1
esac

# The default split delimiter in bash is any whitespace. Change this
# to only split on newline in order to account for filenames with spaces.
old_IFS="$IFS"
IFS=$'\n'

# Use tmux to copy the paragraph above. This specific navigation sequence
# only works if each prompt is prefaced with a newline, such as from having
# `precmd() { print "" }` in `.zshrc`
tmux copy-mode
tmux send-keys 2 { 3 j 0 space } enter
# Results are saved in an array for clarity
search_results+=$(tmux save-buffer -)
select fname in $search_results; do
    $opener "$fname"
    break
done

# Set IFS back to default
IFS="$old_IFS"
joelostblom
  • 1,931
  • The point of if [ "$fname" ] is to test that fname is set and not empty. Historically you had to write that as if [ x"$fname" != x ], but modern shells allow if [ "$fname" ]. However, if [ '"$fname"' ] is wrong (it's always true). The nested if is also better written as a case. :) – Satō Katsura Mar 01 '17 at 22:03
  • @SatoKatsura Thanks for the tips! I updated the answer accordingly. I don't think $fname will ever be empty since I am choosing from a list visually, but correct me if I am wrong there. – joelostblom Mar 03 '17 at 04:52
  • The empty $fname happens when you choose the wrong number, or when you just press Enter. It's pretty much a safety net. – Satō Katsura Mar 03 '17 at 04:59
0

One method comes to mind. You could use the full-screen editor specified by 'EDITOR' in your shell environment. The editor is started by pressing ^X^E (ctrl-x, ctrl-e). From within the editor (such as vim) you could run:

:r!find /

The results of this would be imported, you can then trim the output to what you wish, when you write and exit, the contents would be run as a command, as if you entered it at the shell prompt.

Ed Neville
  • 1,340