Steven Lu
Steven Lu

Reputation: 43447

How to make ctrl+c cancel a whole command when interacting with a subshell command?

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.

Upvotes: 4

Views: 1320

Answers (2)

Eli Smith
Eli Smith

Reputation: 71

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"'

Upvotes: 0

Gabriel Staples
Gabriel Staples

Reputation: 52817

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

Upvotes: 2

Related Questions