4

I'm using the zsh shell and have the following code inside my .zshrc:

fpath=(~/.zsh/completion $fpath)
autoload -Uz _vc

autoload -Uz compinit
compinit

The first line add the path ~/.zsh/completion to the array stored inside the environment variable fpath.

The 2nd line loads a completion function, called _vc, for a custom shell function called vc. The purpose of vc is simply to edit a file inside a particular folder (~/.cheat). _vc is defined in the file ~/.zsh/completion/_vc.

The last 2 lines enable a completion system.

Here's the code for my completion function _vc:

#compdef vc

declare -a cheatsheets
cheatsheets="$(ls ~/.cheat)"
_arguments "1:cheatsheets:(${cheatsheets})" && return 0

I copied it from this address, and adapted it for my needs.

As long as the directory ~/.cheat doesn't have a file whose name contains a single quote, the completion works. But if there's one, such as foo'bar, the completion fails with this error message:

(eval):56: unmatched '
(eval):56: unmatched '
(eval):56: unmatched '
(eval):56: unmatched '

I found a solution, by replacing the double quotes in the line cheatsheets="$(ls ~/.cheat)", with single quotes cheatsheets='$(ls ~/.cheat)'.

Now, when I hit Tab after my vc command, zsh suggests files inside ~/.cheat, including foo\'bar (zsh seems to have escaped the single quote automatically).

However, I don't understand how or why it works. With single quotes, the variable cheatsheets sould contain a literal string. So the $(...) command substitution shouldn't be expanded. For example, if I execute the following commands:

myvar='$(ls)'
echo $myvar    →    outputs `$(ls)` literally

So why is '$(ls ~/.cheat)' expanded inside _arguments "1:cheatsheets:(${cheatsheets})" && return 0? And why does it automatically escape the single quote inside foo'bar?

3

If we insist on doing things The Wrong Way™

#compdef vc

declare -a cheatsheets
cheatsheets=(${(f)"$(ls ~/.cheat/)"})
_arguments '1:cheatsheets:(${cheatsheets})' && return 0

Yuck! This of course will break should a filename contain a newline, as (f) splits on those. Parsing ls is anyways a bad idea; ZSH can glob files directly into an array:

% mkdir ~/.lojban
% touch ~/.lojban/{go\'i,na\ go\'i}
% words=(~/.lojban/*) 
% print -l ${words:t}
go'i
na go'i
% words=(~/.lojban/*(On))
% print -l ${words:t}    
na go'i
go'i
% 

But we probably don't need a local array; _files can complete on a glob:

#compdef vc
_arguments '1:cheatsheets:_files -g "~/.cheat/*"' && return 0

This returns fully qualified file paths; if only the bare filename is required from a search directory we can instead use:

#compdef vc
_arguments '1:cheatsheets:_files -W ~/.cheat' && return 0

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.