zsh-autoquoter is a zle
widget ("zsh plugin") that will automatically put quotes around arguments to certain commands. So instead of having to decide which type of quotes to suffer through:
$ git commit -m 'we haven'\''t seen the last of this "feature"'
$ git commit -m "we haven't seen the last of this \"feature\""
You can just write English:
$ git commit -m we haven't seen the last of this "feature"
And let zsh-autoquoter do the rest.
And if you use zsh-syntax-highlighting, it'll even highlight these "autoquoted strings" distinctly, so you'll never be surprised when zsh-autoquoter fires.
Check out this blog post for a fancy asciinema demo of zsh-autquoter in action.
Configure command prefixes that you want to be autoquoted by setting the ZAQ_PREFIXES
or ZAQ_PREFIXES_GREEDY
arrays in your ~/.zshrc
:
ZAQ_PREFIXES=('git commit -m' 'ssh *')
ZAQ_PREFIXES_GREEDY=()
By default these arrays are empty. You need to opt into autoquote behavior.
ZAQ_PREFIXES
and ZAQ_PREFIXES_GREEDY
are arrays of shell patterns that will be matched against your input.
Shell patterns are kind of like goofy regular expressions, where *
means .*
and #
means *
and ##
means +
. So while git commit -m
would work as a pattern to match a literal prefix, I would suggest this more flexible alternative:
ZAQ_PREFIXES+=('git commit( [^ ]##)# -[^ -]#m')
git commit -a -m hi hello
^^^^^^^^
git commit -am hi hello
^^^^^^^^
git commit -m hi hello
^^^^^^^^
And while ssh *
will "skip one argument" and then quote the rest, you should probably use a pattern that allows flags and triggers after the first positional argument:
ZAQ_PREFIXES+=('ssh( -[^ ]##)# [^ -][^ ]#')
ssh -Y user@host some 'complex' * | remote <"command"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ZAQ_PREFIXES_GREEDY
patterns will match greedily. Since everything after the matching pattern will be autoquoted, ZAQ_PREFIXES_GREEDY
will cause less of your prompt to be autoquoted. You'll probably want to stick with ZAQ_PREFIXES
, but greedy patterns can be useful for matching a variable number of flags. For example:
ZAQ_PREFIXES_GREEDY=('foo( -[^ ]##)#')
foo -bar -baz hello world
^^^^^^^^^^^
Since #
means "zero or more," a non-greedy pattern would always match zero repetitions.
Wow those are complicated, huh? Yeah. And they're not even very complete: for example, you can't type:
ssh 'some hostname with a space in it' something
Because the pattern doesn't recognize that 'some hostname with a space in it'
is a single positional argument. Patterns operate on characters, even though we think about command arguments in terms of words. But parsing those out would be a whole thing.
Note that [^ -][^ ]#
at the end of a pattern matches exactly one positional argument because zsh-autoquoter always adds an implicit space to the end of your patterns.
You can debug a pattern using the following expression:
setopt EXTENDED_GLOB
input='ssh user@host some command'
prefix='ssh( -[^ ]##)# [^ -][^ ]#'
print ${input#$~prefix }
Or, for testing greedy patterns:
print ${input##$~prefix }
Which is exactly how zsh-autoquoter is implemented. If that prints the entire input
string, your pattern didn't work. If it only prints a subset of it, then that is the subset that will be autoquoted.
See section 14.8.1 Glob Operators for a more thorough description of pattern syntax.
zsh-autoquoter runs before your shell has a chance to expand or glob or parse the command you typed, so it works to quote any shell syntax:
$ ssh user@host * globs and | pipes and > redirects and # comments oh my
There is one exception:
zsh-autoquoter won't add quotes if there already are quotes, so you can still type:
$ git commit -m 'i forgot that i installed zaq'
And it will not double-escape anything.
When doing this check, zsh-autoquoter will look for a fully quoted argument string. So you can still write:
$ git commit -m 'twas the night before christmas
$ git commit -m "fix" the latest "bug"
And it will be autoquoted correctly.
I highly recommend enabling syntax highlighting (see below) to make it obvious when zsh-autoquoter is going to fire and when it is not.
Commands will be rewritten before they're added to history, so your ~/.zsh_history
will reflect an accurate list of commands you ran, not commands you typed. But because zsh-autoquoter ignores already-quoted entries, you can always up-enter and re-run the same command.
Yes, you could imagine wanting it the other way, but I just cannot figure out how to make that work with zsh.
Download zsh-autoquoter.zsh
and source it from your ~/.zshrc
file. Then make sure that you add some prefixes:
source ~/src/zsh-autoquoter/zsh-autoquoter.zsh
ZAQ_PREFIXES=('git commit( [^ ]##)# -[^ -]#m' 'ssh( -[^ ]##)# [^ -][^ ]#')
# If using zsh-syntax-highlighting:
# ZSH_HIGHLIGHT_HIGHLIGHTERS+=(zaq)
$ antigen bundle ianthehenry/zsh-autoquoter
Clone this repo into your custom plugins directory:
$ git clone https://github.com/ianthehenry/zsh-autoquoter.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autoquoter
And then add it to the plugins list in your ~/.zshrc
before you source oh-my-zsh
:
plugins+=(zsh-autoquoter)
source "$ZSH/oh-my-zsh.sh"
If you use the excellent zsh-syntax-highlighting package, you can enable the zaq
highlighter by adding the following line to your ~/.zshrc
:
ZSH_HIGHLIGHT_HIGHLIGHTERS+=(zaq)
You can customize how it looks by setting the zaq:string
key:
ZSH_HIGHLIGHT_STYLES[zaq:string]="fg=green,underline"
If you don't use zsh-syntax-highlighting, you should check it out. It's great. You'll never typo a command again.
Okay yeah look I'm using git commit -m
as an example a lot, but I actually wrote zsh-autoquoter so that I could add notes and todo list items from the command line more easily.
Not to say that git commit -m
is bad -- it's great if you're using a patch queue workflow. But zsh-autoquoter does not condone vague one-line commit messages.