2

I have custom script that takes:

  1. optional arguments in the short/long format
  2. one required command line argument

the short/long command line options are for example:

-r, --readonly
-m, --mount

for the one required arguments, these are actually specified in the script as case statements, ie foo and bar in this example:

case $1 in
  foo )
  :
  ;;

  bar )
  :
  ;;

How can I create zsh completion for my script, so that optional arguments are completed when argument starts with -, and required arguments are completed taken from my script case statement?

UPDATE:

this is what I have so far, inspired by the answer from @Marlon Richert.

Suppose my custom script is called myscript.sh and the completion rule I have created is in /usr/share/zsh/functions/Completion/Unix/_myscript.sh:

#compdef myscript.sh

_myscript () {
  local -a args

  args+=(
    {-r,--readonly}'[description for "readonly"]' 
    {-m,--mount}'[description for "mount"]' 
  )

  _arguments $args && return
}

_myscript "$@"

My script myscript.sh itself is located in /usr/local/bin/myscript.sh.

So now when i have the optional arguments -r and -m taken care of, I need to modify my completion rules, so that for the required command line argument of my script, the items from the case statement from /usr/local/bin/myscript.sh are offered as completions.

Also, I am not sure if the syntax of the block starting on line 6 with args+=( in my completion script is correct. Where do I have to put the single quotes?

1 Answer 1

6

Let's suppose the function for which you want to define completions is called myfunc.

First, let's set up the actual function:

  1. Put your function in a file called myfunc.
    • Note that this does not end in .zsh or .sh.
  2. Make sure that the dir containing the file myfunc is in your $fpath. For example, if the file myfunc is located in ~/func, add this to your ~/.zshrc file:
    fpath+=( ~/func )
    
  3. Finally, autoload myfunc in your ~/.zshrc file:
    # We pass a couple of options that make the code less likely to break:
    # -U suppresses alias expansion
    # -z marks the function for zsh-style autoloading == `unsetopt KSH_AUTOLOAD`
    autoload -Uz myfunc
    

You should now be able to use myfunc on the command line (but without any completions yet).

Next, let's create the completion function:

  1. Create a file called _myfunc.
  2. Put the following in this file:
    #compdef myfunc
    
    # The line above means "This function defines completions for myfunc."
    # The combination of that line, plus the file name starting with an 
    # `_`, plus having this file's parent dir in your `$fpath`, ensures 
    # this file will get autoloaded when you call `compinit`.
    
    # `+X` makes sure `myfunc`'s definition will get loaded immediately, 
    # even if you have not called this function yet.
    autoload +X -Uz myfunc
    
    # Get the definition of `myfunc` in string form.
    local funcdef=$(type -f myfunc)
    
    # Get the part that matches `case*esac`, then split it on whitespace 
    # and put the resulting words in an array.
    local -a words=( ${=funcdef[(r)case,(r)esac]} )
    
    # Keep only the words that start with `(` and end with `)`.
    # Even if you used the `case` syntax with only the closing `)`s,
    # in `type -f`, your cases will appear with both `(` and `)`.
    local -a required=( ${(M)words:#'('*')'} )
    
    # `-s`: Allow options to `myfunc ` to be stacked, that is, you are 
    #       allowed to specify `myfunc -rm`. If not, remove the `-s` 
    #       option.
    # `*:`: Let this argument be completed in any position.
    _arguments -s \
      {-r,--readonly}'[description for "readonly"]' \
      {-m,--mount}'[description for "mount"]' \
      "*:required argument:( ${required//[()]/}  )"
    
    replacing required argument with whatever you want to call your argument and filling out descriptions for the options.
    • Note: When you put a function in an autoloadable file, you do not need the funcname() {…} boilerplate.
  3. Again, make sure the directory in which this file is located is in your $fpath.
  4. Make sure you do autoload -Uz compinit; compinit in your .zshrc file and make sure it runs after the dir above has been added to your $fpath.
  5. Restart your shell with exec zsh or close your terminal window and open a new one.

You now should be able to get completion for myfunc.


If readonly and mount are mutually exclusive, you'll need to rewrite the last line of the completion function as follows:

_arguments \
    (-m --mount){-r,--readonly}'[description for "readonly"]' \
    (-r --readonly){-m,--mount}'[description for "mount"]' \
    "*:required argument:( ${required//[()]/}  )"
12
  • the foo and bar were used as examples. These should not be hardcoded in the completion script. These should be extracted from the script that we are creating completion rules for. The script has case statements, and valid arguments are those case items. Aug 6, 2020 at 13:38
  • @400theCat I updated my answer. Aug 9, 2020 at 9:51
  • thank you for you effort, but it does not work. Could you please paste the full completion file, instead of just snippets ? Aug 9, 2020 at 13:09
  • also, if that is not too much to ask: if you could take my template above (in the update) a starting point, that would be fantastic. I am having difficulties to orient myself in your code. Aug 9, 2020 at 13:11
  • Ah, it seems I forgot to add autoload +X -Uz myfunc. I also added more comments. I hope it's more clear now. Aug 9, 2020 at 20:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.