2

I have a little ugly bash script on my Ubuntu machine that contains the lines:

search_command="find -L $(printf "%q" "$search_folder") \( ! -regex '.*/\..*/..*' \) -mindepth 1 2> /dev/null"
for i in "${IGNOREENDINGS[@]}"
do
    search_command="$search_command -not -name \"*$i\""
done
search_command="$search_command | sed 's|^${search_folder}/\?||'"
choice=$(eval "$search_command"|fzf -q "$file_query" -1 --preview "preview $search_folder {}")

The script lets me choose a file, using fzf among the matches of a GNU find command.

It has the following problem: Once I choose a file in the interface of fzf the script closes the fzf interface, so that seems to be done, but then I still have to wait for the find command to complete (verified with top), which somehow takes very long. I'm not really sure why; the files that I want always appear almost instantly.

I included a few extra lines above to avoid an X Y problem. I am happy with anything with the same functionality and quicker execution.

Bananach
  • 455
  • Is this on Linux? Or, at least, is this GNU find? Does this work for you: How to stop the find command after first match? – terdon Feb 02 '19 at 17:35
  • @terdon it's Gnu find in bash on Ubuntu 16. Strange, from the link is expect it should quit. Adding "quit" flag is not a viable solution, because I do want to get all output piped to fzf until fzf quits. By the way, I do not get this behavior on another computer of mine, that runs Ubuntu 18 – Bananach Feb 02 '19 at 17:41
  • But didn't you say that the problem is waiting for find? If you need all files, you need to wait for it. While the files you want may appear instantly, it still needs to look at every single file in the directory you have given it to know if there are any more matches. So if you need it to go through all the files, you have to wait for it. It might help if you explained what the script is trying to do since the code isn't very clear. – terdon Feb 02 '19 at 17:50
  • @terdon I need the files as they arrive. As soon as the one arrives that I want, I want it to exit. I edited the question, I hope it's clearer now – Bananach Feb 02 '19 at 17:55
  • So you do want to exit as soon as the first one is found? Please explain what your script is attempting. We won't be able to help much otherwise. – terdon Feb 02 '19 at 17:58
  • @terdon the file that I want is reported by find as one among many. In other words, the filters i use for find are not strong enough to uniquely identify the file I want, hence I pass all search results to fzf. Fzf allows me to select one of the files from a list that is continuously updated from the outputs of find. When the file that I want appears, I select it and don't need find to continue providing more matches – Bananach Feb 02 '19 at 19:05

2 Answers2

3

There are two possibilities here: either fzf is not actually exiting when you select a file, or find is not exiting when fzf does. If it's the latter one, you can write a script to close find manually when fzf exits.

The way that pipes work in Linux, find does not know that the pipe it is writing to has nothing reading from that pipe until it tries to write to that pipe and fails. As a consequence, if you pick the file after find has already found everything it's going to find, find is no longer writing to the pipe, and so will iterate over the whole file system before exiting.

As an illustration of this, if you make a random file in / and then run find / -name $random_file_name | head -n 1, you will very quickly get all the output you are going to get, but the program will continue to run for a long time.

One way to get around this is by killing the process yourself when it's done. Probably the easiest way to do it in your specific case is a named pipe:

tmp_fifo=`mktemp -u`
mkfifo "$tmp_fifo"
eval "$search_command" > "$tmp_fifo" &
choice="$(fzf -q "$file_query" -1 --preview "preview $search_folder {}" < "$tmp_fifo")" ; kill $!
rm "$tmp_fifo"

This creates a temporary named pipe, find writes to it, and fzf reads from it. But when fzf exits, kill $! is run, where $! stands for the last background process to have been started, in this case find.

Chris
  • 1,539
  • This sounds exactly like what I was looking for. Also the explanation makes perfect sense. Will try once back at the computer – Bananach Feb 02 '19 at 19:09
  • this will only work if $search_command is a single process; in the case of { ... commands ...; } > fifo &, a kill $! will only kill the outer subshell, not the ... commands .. run by it (in some shells, the last command from a { ...; } will be exec'ed through instead of waited for, but that's not something you can rely on). –  Feb 04 '19 at 09:11
  • @mosvy $search_command is find, so it is a single process. If it isn't, you can do kill -- -$! to kill the subshell and its children. – Chris Feb 04 '19 at 10:51
  • Yes, it works in this special case, I'm not disputing that 2. no, when used in a subshell or script there will be no job control and no separate process group for ... & so kill -- -$! won't kill anything at all.
  • –  Feb 04 '19 at 10:56
  • @mosvy Try it yourself. {sleep 1000 ; foo ; } & sleep 1 ; kill -- -$! – Chris Feb 04 '19 at 11:17
  • Right. Try it. sh -c '{ sleep 1000 ; foo ; } & sleep 1 ; kill -- -$!' => sh: 1: kill: No such process. Then pgrep -a sleep => 16653 sleep 1000. –  Feb 04 '19 at 11:28
  • @mosvy My bad, I misunderstood what you were saying. Anyway, you can get around this by using exec explicitly for the last process in the subshell. – Chris Feb 04 '19 at 11:45
  • @mosvy Or turn on job control with set -m first. – Chris Feb 04 '19 at 12:02
  • In that case you can do it much simpler with (set -m; find ... | { fzr ...; pkill -g0; }) ;-) –  Feb 04 '19 at 12:03
  • Update: this worked for me, but I had to make a little mess around it to silence absolute every output of everything (e.g. kill complains when the job was closed before it is called, and bash gives notice of background jobs terminating etc.) – Bananach Feb 04 '19 at 17:50
  • @Bananach You can silence the notice bash gives for background jobs terminating by changing kill to kill -13 (sending SIGPIPE instead of SIGTERM). – Chris Feb 04 '19 at 18:02