4

I have a simple shell alias

alias fz='vim -p $(fzf -m)'

fzf is an interactive terminal program, which only when it terminates sends a list of filenames on stdout. This then is used to open the files chosen with vim.

Now the trouble is when i decide to cancel and hit ctrl+c, the shell goes right ahead and still opens vim (as if I just ran vim -p). This kind of makes sense.

Now, one resolution to the immediate problem that I have is to simply make my alias more sophisticated so that it does not launch vim if fzf's output is empty. Alternatively, I could likely do something to abort launching vim if fzf's exit code is not zero.

However, I am curious about how I might go about commanding my shell to terminate this fz when I Ctrl + C. Is it simply not possible when I am in the subshell context? Would I be able to do so by setting process group id somehow?

I tried set -m, but it did not change behavior.

Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
Steven Lu
  • 41,389
  • 58
  • 210
  • 364
  • I am not certain since i haven't conclusively tested this yet, but, according to [this answer](https://stackoverflow.com/a/36820679/340947) if I change my alias to a script, it will do what I want. UPDATE: This appears not to work. Even with `set -m`. I can confirm that the pgid is set right with the shell script version, though. – Steven Lu Feb 20 '18 at 18:09
  • A list of filenames delimited how? (That is, how are filenames with spaces represented?) -- in most cases, this alias wouldn't be expected to work with unusual/surprising names. – Charles Duffy Feb 20 '18 at 18:15
  • @CharlesDuffy filenames with spaces likely break this, but that is ok (completely irrelevant for the purpose of the question). Imagine if I'm using `find -print0` and `xargs -0` instead – Steven Lu Feb 20 '18 at 18:16
  • What version of bash, specifically? (Do you have one new enough that process substitutions' PIDs are `wait`able? -- that would allow branching based on its exit status, or whether it died via signal) – Charles Duffy Feb 20 '18 at 18:17
  • scripts will get run by bash 4.3.11 and using zsh 5.3.1 interactively – Steven Lu Feb 20 '18 at 18:18
  • ...so, it doesn't quite answer your question, but without assuming an extremely new bash, I'd probably write this as `fz() { local -a files; readarray -t files < <(fzf -m) && (( ${#files[@]} )) || break; vim "${files[@]}"; }` – Charles Duffy Feb 20 '18 at 18:18
  • **Oh**. If this is a question about zsh, not bash, it should be tagged as such; they're very different (not all that mutually-compatible) shells. My suggestion above is written only for bash, not zsh. – Charles Duffy Feb 20 '18 at 18:18
  • (zsh doesn't *try* to be POSIX-compatible unless it was run under the `sh` name, so I'd argue that the `posix` tag is likewise inappropriate here... unless you *are* running zsh in POSIX mode, in which case it actually does a better job of POSIX compliance than bash does). – Charles Duffy Feb 20 '18 at 18:21
  • Hm, since i use both bash and zsh interactively across multiple machines the solution should work in both... ok, at this rate it seems like the practical thing is to look at (or just go back to using) fzf's provided shell integrations. – Steven Lu Feb 20 '18 at 18:21
  • I used to (significantly over a decade ago, now) mix my shells in interactive use, but my experience was getting accustomed to using zsh led me to relax my habits and write buggy code when targeting bash or POSIX sh (assuming sane behavior, rather than POSIX-mandated behavior, on unquoted expansions for example). Perhaps you're more disciplined. :) – Charles Duffy Feb 20 '18 at 18:23
  • BTW, to see what I was saying about being able to capture process substitutions' exit status in bash 4.4, see the following example: `cat <(printf '%s\n' hello world; exit 3); pid=$!; wait "$pid"; echo "$?"` -- so if you used `readarray` or `mapfile` or `read -a` in place of `cat`, you could get both the output *and* the exit status, thus determining how your `fzf` exited. – Charles Duffy Feb 20 '18 at 18:27
  • `fz() { IFS=$'\n' read -r -d '' -a files < <(fzf -m && printf '\0') && vim "${files[@]}"; }` would also do -- since `read -d ''` will only have a successful exit if its input ends with a NUL, any nonzero exit status from `fzf` will be effectively passed through; and that will work with older bash releases (including the 3.2 build that Apple still ships). No clue about what will/won't work in zsh, though. – Charles Duffy Feb 20 '18 at 18:28
  • Hmm.. well... so far i've kept aliases simple, but this one slightly fancy alias seems to have opened a pandora's box. I have a bit of a library of shell scripts now, and they all have the `#!/bin/bash` in them so that it enforces at least a little bit of discipline. The thing is that I don't want to go without zsh's wonderful tab completions and other niceness. – Steven Lu Feb 20 '18 at 18:32
  • I find process substitutions to work so inconsistently with programs (which variously fail to properly open file descriptor "files") that I've been avoiding them as a rule of thumb. – Steven Lu Feb 20 '18 at 18:33
  • The [!alias factoid in the freenode #bash channel](http://wooledge.org/~greybot/meta/alias) is "If you have to ask, you probably want to use a function instead". I've always considered that sound advice. – Charles Duffy Feb 20 '18 at 18:34
  • If a function is safer than an alias then a standalone script is even safer, right? – Steven Lu Feb 20 '18 at 18:34
  • Process substitutions are reliable so long as you understand where they are and aren't applicable. (If you couldn't use a named pipe, you can't use a process substitution; if a program closes all FDs it inherits other than stdout and stderr, it can't accept a `/dev/fd` link). – Charles Duffy Feb 20 '18 at 18:34
  • 1
    I'm not sure "safer" is the phrase. It's a capability continuum -- there's a wider range of *things you can do* with a function than things you can do with an alias, simply by virtue of how functions work (having their own stack frame, being invoked with a distinct argument list rather than relying on string substitution to throw things after the end). And, for that matter, there's a large array of things you can do with a function that you *can't* do from a standalone script (unless you invoke that script using `source`). – Charles Duffy Feb 20 '18 at 18:35
  • Thanks for these notes, I appreciate it a lot! Also, great point about function vs script. I do have some "scripts" that are intended only for sourcing so I've encountered this difference ;) At this point, where i am stuck is that if i put this `vim $(fzf)` in a shell script, i observe proper pgid and tpgid assignments, i.e. vim gets launched with same tpgid as fzf was, but ctrl+c while fzf is open STILL does not prevent vim from opening! Perplexed. (`set -m` was a red herring, it does the wrong thing, forcing a new pgid) – Steven Lu Feb 20 '18 at 18:37
  • 1
    Perhaps something like this? `alias fz='F=$(fzf) && vim $F` with preview `alias fz='F=$(fzf --preview "bat --style=numbers --color=always --line-range :500 {}") && vim $F'` – Ryan Sep 30 '21 at 16:56
  • @Ryan this worked for me: ctrl-c in FZF did not open Vim. Thanks! – Jerome Beckett Oct 21 '21 at 10:04

2 Answers2

2

Make script calling fzf (or any other command) exit when you exit early with Ctrl + C

1. Simple alias

This works:

# command
files_list=$(fzf -m) && vim -p $files_list

# alias
alias fz='$(fzf -m) && vim -p $files_list'

Explanation:

When fzf exits normally it returns error code 0 (note: you can read the return code after it exits by running echo $?). When fzf is killed by Ctrl + C, however, it returns error code 130. The && part means "only do the part on the right if the part on the left is zero"--meaning that the command on the left completed successfully. So, when you hit Ctrl + C while fzf is running, it returns error code 130, and the part on the right will never execute.

2. More-robust function

If you need to do anything more-complicated, however, use a function (in your ~/.bashrc or ~/.bash_aliases file still if you like) instead of an alias. Here is a more-robust example of the alias above, in function form. The way I've written the alias above, I expect it to fail if you have filenames with whitespace or strange characters in them. However, the below function should work even in those cases I think:

fz() {
    files_list="$(fzf -m)"
    return_code="$?"

    if [ "$return_code" -ne 0 ]; then
        echo "Nothing to do; fzf return_code = $return_code"
        return "$return_code"
    fi

    echo "FILES SELECTED:"
    echo "$files_list"

    # Convert the above list of newline-separated file names into an array
    # - See: https://stackoverflow.com/a/24628676/4561887
    SAVEIFS=$IFS   # Save current IFS (Internal Field Separator)
    IFS=$'\n'      # Change IFS to newline char
    files_array=($files_list) # split this newline-separated string into an array
    IFS=$SAVEIFS   # Restore original IFS

    # Call vim with each member of the array as an argument
    vim -p "${files_array[@]}"
}

Note: if you're not familiar with the [ function (yes, that's a function name!), run man [ or man test for more information. The [ function is also called test. That's why the arguments after that function name require spaces between them--they are arguments to a function! The ] symbol is simply the required closing argument to the [ (test) function.

References:

  1. My array_practice.sh bash script from my eRCaGuy_hello_world repo. This is where I learned and documented a lot of this stuff.
  2. How to pass an array of arguments to a command: How can I create and use a backup copy of all input args ("$@") in bash?
  3. How to parse a newline-separated string into an array of elements: Convert multiline string to array
  4. Use return to exit a bash function early: How to exit a function in bash
Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
0

I took a similar approach inspired by Gabriel's answer, but modified to be simpler and dynamic to the editor that is configured as default. Note that I have adapted it to be used for opening single files and not multiple, but that's easily changed:

fzfedit() {
  local sel="$(fzf)"
  # If the selected file exists, open it with $EDITOR
  [[ -e "$sel" ]] && $EDITOR "$sel"
}

Or even more concisely as an alias:

alias fzfedit='sel=$(fzf) && test -e "$sel" && $EDITOR "$sel"'
Eli Smith
  • 71
  • 1
  • 6