linuxzsh

Run a command whose absolute path is stored in a variable


I have a script below, which stores the absolute path of FZF in a variable named fzf_cmd. I want to execute FZF using this variable. It works in Bash but not in Zsh.

f1.sh

fun() {
    local fzf_cmd
    [[ -n "${BASH_VERSION}" ]] && fzf_cmd="$(type -P fzf)"
    [[ -n "${ZSH_VERSION}" ]] && fzf_cmd="$(type -p fzf)"
    "${fzf_cmd}" --height 40%
}

fun

I get the following error message in ZSH.

fun:4: no such file or directory: fzf is /home/rishi/commandline-plugins/fzf/bin/fzf

However, when I run fzf explicitly using its absolute path (/home/rishi/commandline-plugins/fzf/bin/fzf), it works as expected in Zsh.

Am I missing anything here? How can I get it to work in both of the shells?

EDIT: The command type -p fzf in Zsh doesn't only print the absolute path of fzf. It prints fzf is /home/rishi/commandline-plugins/fzf/bin/fzf. That was the cause of the issue. I think using whence would be better here!


Solution

  • You got the zsh variant wrong. This should work:

    fun() {
      local fzf_cmd
      if [[ -n "${BASH_VERSION}" ]]; then
        fzf_cmd="$(type -P fzf)"
      elif [[ -n "${ZSH_VERSION}" ]]; then
        fzf_cmd==fzf
      fi
      "${fzf_cmd}" --height 40%
    }
    

    type -p fzf provides too much noise in zsh. While you could use ksh-style whence -p fzf, using the builtin =cmd filename expansion mechanism makes it shorter and more efficient as it avoids forking a process to get the output of a command through a pipe.

    UPDATE

    Detailed explanation, as requested by the OP:

    In the original approach, the use of type -p fzf would have worked, if this command would output only the absolute path of fzf and nothing else, which it unfortunately does not.

    Therefore I used the trick with filename expansion: For a word w, the term =w expands to the first absolute path of w, found in the variable path (respectively $PATH).

    BTW, we could also write it without testing the shell version, using the command which:

    fun() {
      "$(/usr/bin/which fzf)" --height 40%
    }
    

    This ensures that always an external program named fzf is invoked, bypassing a possible function (or alias) of the same name. The disadvantage compared to the approach using =-expansion is, that with which we always create a child process to calculate the path to fzf, while =fzf runs inside the parent shell; no forking needed.

    UPDATE

    It just occurred to me that an even simpler solution would be to use the command command, which works in bash and in zsh:

    fun() {
      /usr/bin/command fzf --height 40%
    }
    

    This would also bypass a function or alias named fzf and ensure that only an executable (external command) named fzf gets invoked.

    UPDATE

    Using absolute path to which and command for compatibility with bash and zsh.