Make zsh complete arguments from a file - zsh

zsh is great but its completion system is very diverse. And the documentation lacks good examples. Is there a template for completing for a specific application. The completion would get its match data from a file, separated by newlines?
I tried modifying an older example of mine that takes match data "live":
~ % cat .zsh/completers/_jazzup
#compdef jazz_up
_arguments "2: :(`mpc lsplaylists|sed -e 's# #\\\\ #g'`)"
I could supply cat my_file there instead of mpc invocation and so on but would there be a more elegant way to do this simple task? And that completion there is placement-specific: can you provide an example where zsh would attempt to complete at any point after the program name is recognized?
The match data will have whitespaces and so on, the completion should escape the WS. Example of that:
Foo bar
Barbaric
Get it (42)
Now if that completion would be configured for a command Say, we should get this kind of behaviour out of zsh:
$ Say Fo<TAB>
$ Say Foo\ bar
$ Say Ge<TAB>
$ Say Get\ it\ \(42\)

Simple completion needs are better addressed with _describe, it pairs an array holding completion options and a description for them (you can use multiple array/description pairs, check the manual).
(_arguments is great but too complex.)
[...]
First create a file
echo "foo\nbar\nbaz\nwith spac e s\noh:noes\noh\:yes" >! ~/simple-complete
Then create a file _simple somewhere in your $fpath:
#compdef simple
# you may wish to modify the expansion options here
# PS: 'f' is the flag making one entry per line
cmds=( ${(uf)"$(< ~/simple-complete)"} )
# main advantage here is that it is easy to understand, see alternative below
_describe 'a description of the completion options' cmds
# this is the equivalent _arguments command... too complex for what it does
## _arguments '*:foo:(${cmds})'
then
function simple() { echo $* }
autoload _simple # do not forget BEFORE the next cmd!
compdef _simple simple # binds the completion function to a command
simple [TAB]
it works. Just make sure the completion file _simple is placed somewhere in your fpath.
Notice that : in the option list is supposed to be used for separating an option from their (individual) description (oh:noes). So that won't work with _describe unless you quote it (oh\:yes). The commented out _arguments example will not use the : as a separator.

Without changing anything further in .zshrc (I already have autoload -Uz compinit
compinit) I added the following as /usr/local/share/zsh/site-functions/_drush
#compdef drush
_arguments "1: :($(/usr/local/bin/aliases-drush.php))"
Where /usr/local/bin/aliases-drush.php just prints a list of strings, each string being a potential first argument for the command drush. You could use ($(< filename)) to complete from filename.
I based this on https://unix.stackexchange.com/a/458850/9452 -- it's surprising how simple this is at the end of the day.

Related

history expansion in zsh

according to the doc, !! should expand to the last command when hit <tab> after it. However, this is not the case for my setup (I used default oh-my-zsh). Here is an example:
$ echo 1
1
$ echo 2
2
$ !!<tab>
$ echo 1
Moreover, !# does not expand to what are input in the current line. Instead, it expands to the last command
$ echo 1
1
$ echo 2
2
$ echo 3 !#<tab>
$ echo 3 echo 2
Is there any option controls this?
I had a look at the default oh-my-zsh completion settings on github and it look like parameter expansion is not enabled out of the box. According to oh-my-zsh documentation any overrides should go into custom/ directory, in files ending with *.zsh. Getting parameter expansion to work should be as simple as dropping there a file with something like this:
zstyle ':completion:*' completer _expand _complete
The completer function you're looking for is called _expand, here is what man zshcompsys has to say about it:
_expand
This completer function does not really perform completion, but instead
checks if the word on the command line is eligible for
expansion and, if it is, gives detailed control over how this
expansion is done. For this to happen, the completion system needs to
be invoked with complete-word, not expand-or-complete (the default
binding for TAB), as otherwise the string will be expanded by the
shell's internal mechanism before the completion system is started.
Note also this completer should be called before the _complete
completer function.
The tags used when generating expansions are all-expansions for the
string containing all possible expansions, expansions when adding the
possible expansions as single matches and original when adding the
original string from the line. The order in which these strings are
generated, if at all, can be controlled by the group-order and
tag-order styles, as usual.
The format string for all-expansions and for expansions may contain
the sequence '%o' which will be replaced by the original string from
the line.
The kind of expansion to be tried is controlled by the substitute,
glob and subst-globs-only styles.
It is also possible to call _expand as a function, in which case the
different modes may be selected with options: -s for substitute, -g
for glob and -o for subst-globs-only.

Zsh completions with multiple repeated options

I am attempting to bend zsh, my shell of choice, to my will, and am completely at a loss on the syntax and operation of completions.
My use case is this: I wish to have completions for 'ansible-playbook' under the '-e' option support three variations:
Normal file completion: ansible-playbook -e vars/file_name.yml
Prepended file completion: ansible-playbook -e #vars/file_name.yml
Arbitrary strings: ansible-playbook -e key=value
I started out with https://github.com/zsh-users/zsh-completions/blob/master/src/_ansible-playbook which worked decently, but required modifications to support the prefixed file pathing. To achieve this I altered the following lines (the -e line):
...
"(-D --diff)"{-D,--diff}"[when changing (small files and templates, show the diff in those. Works great with --check)]"\
"(-e --extra-vars)"{-e,--extra-vars}"[EXTRA_VARS set additional variables as key=value or YAML/JSON]:extra vars:(EXTRA_VARS)"\
'--flush-cache[clear the fact cache]'\
to this:
...
"(-D --diff)"{-D,--diff}"[when changing (small files and templates, show the diff in those. Works great with --check)]"\
"(-e --extra-vars)"{-e,--extra-vars}"[EXTRA_VARS set additional variables as key=value or YAML/JSON]:extra vars:__at_files"\
'--flush-cache[clear the fact cache]'\
and added the '__at_files' function:
__at_files () {
compset -P #; _files
}
This may be very noobish, but for someone that has never encountered this before, I was pleased that this solved my problem, or so I thought.
This fails me if I have multiple '-e' parameters, which is totally a supported model (similar to how docker allows multiple -v or -p arguments). What this means is that the first '-e' parameter will have my prefixed completion work, but any '-e' parameters after that point become 'dumb' and only allow for normal '_files' completion from what I can tell. So the following will not complete properly:
ansible-playbook -e key=value -e #vars/file
but this would complete for the file itself:
ansible-playbook -e key=value -e vars/file
Did I mess up? I see the same type of behavior for this particular completion plugin's '-M' option (it also becomes 'dumb' and does basic file completion). I may have simply not searched for the correct terminology or combination of terms, or perhaps in the rather complicated documentation missed what covers this, but again, with only a few days experience digging into this, I'm lost.
If multiple -e options are valid, the _arguments specification should start with * so instead of:
"(-e --extra-vars)"{-e,--extra-vars}"[EXTR ....
use:
\*{-e,--extra-vars}"[EXTR ...
The (-e --extra-vars) part indicates a list of options that can not follow the one being specified. So that isn't needed anymore because it is presumably valid to do, e.g.:
ansible-playbook -e key-value --extra-vars #vars/file

Renaming multiple files using parameter unix

I have a rename script below rename.sh. I want to introduce a variable such that I can pass a date argument when executing the script like./rename.sh 20151103 such that 20151103 replaces 20140306 in the script.
for f in *.CDR*; do
echo mv "$f" "${f/-20140306/-0-20140306}"
done
Thinking of automating this as I don't want to manually edit the script each time i'm doing a rename. Any other method will be highly welcomed.
#!/bin/bash
pattern="$1"
for f in *.CDR*; do
echo mv "$f" "${f/-${pattern}/-0-${pattern}}"
done
Explanation:
The #!-line says we're running this as a bash script.
The script will populate the variables $1, $2 etc. with the arguments handed to it on the command line. These are called the positional parameters ($0 usually holds the name of the script).
We take $1, because we know that should contain the pattern we're replacing, and assign it to the variable $pattern. In much more complex scripts, here is where we would handle command line switches (with getopts, but that's an answer for another day).
We quote $1, just because. (It's good practice to quote user input, just to be sure no shell-globbing characters, such as * gets expanded).
The rest is the script like you had from before, but with the string 20140306 replaced by ${pattern}. I'm using ${pattern} rather than $pattern here for readability only. In general, you need to use ${a} rather than $a if you, for example, interpolate a string like "${a}nospaceafter".
Then it should just be a matter of making the script executable before testing it:
$ chmod +x rename.sh
This is the one of the method you can consider:
#!/bin/bash
input=$1
for f in *.CDR*; do
echo mv "$f" "${f/-$input/-0-$input}"
done

Zsh: How to force file completion everywhere following a set of characters?

I'm trying to figure out how to get file completion to work at any word position on the command line after a set of characters. As listed in a shell these characters would be [ =+-\'\"()] (the whitespace is tab and space). Zsh will do this, but only after the backtick character, '`', or $(. mksh does this except not after the characters [+-].
By word position on the command line, I'm talking about each set of characters you type out which are delimited by space and a few other characters. For example,
print Hello World,
has three words at positions 1-3. At position 1, when you're first typing stuff in, completion is pretty much perfect. File completion works after all of the characters I mentioned. After the first word, the completion system gets more limited since it's smart. This is useful for commands, but limiting where you can do file completion isn't particularly helpful.
Here are some examples of where file completion doesn't work for me but should in my opinion:
: ${a:=/...}
echo "${a:-/...}"
make LDFLAGS+='-nostdlib /.../crt1.o /.../crti.o ...'
env a=/... b=/... ...
I've looked at rebinding '^I' (tab) with the handful of different completion widgets Zsh comes with and changing my zstyle ':completion:*' lines. Nothing has worked so far to change this default Zsh behaviour. I'm thinking I need to create a completion function that I can add to the end of my zstyle ':completion:*' completer ... line as a last resort completion.
In the completion function, one route would be to cut out the current word I want to complete, complete it, and then re-insert the completion back into the line if that's possible. It could also be more like _precommand which shifts the second word to the first word so that normal command completion works.
I was able to modify _precommand so that you can complete commands at any word position. This is the new file, I named it _commando and added its directory to my fpath:
#compdef -
# precommands is made local in _main_complete
precommands+=($words[1,$(( CURRENT -1 ))])
shift words
CURRENT=1
_normal
To use it I added it to the end of my ':completion:*' completer ... line in my zshrc so it works with every program in $path. Basically whatever word you're typing in is considered the first word, so command completion works at every word position on the command line.
I'm trying to figure out a way to do the same thing for file completion, but it looks a little more complicated at first glace. I'm not really sure where to go with this, so I'm looking to get some help on this.
I took a closer look at some of Zsh's builtin functions and noticed a few that have special completion behaviour. They belong to the typeset group, which has a function _typeset in the default fpath. I only needed to extract a few lines for what I wanted to do. These are the lines I extracted:
...
elif [[ "$PREFIX" = *\=* ]]; then
compstate[parameter]="${PREFIX%%\=*}"
compset -P 1 '*='
_value
...
These few lines allow typeset completion after each slash in a command like this:
typeset file1=/... file2=~/... file3=/...
I extrapolated from this to create the following function. You can modify it to put in your fpath. I just defined it in my zshrc like this:
_reallyforcefilecompletion() {
local prefix_char
for prefix_char in ' ' $'\t' '=' '+' '-' "'" '"' ')' ':'; do
if [[ "$PREFIX" = *${prefix_char}* ]]; then
if [[ "$PREFIX" = *[\'\"]* ]]; then
compset -q -P "*${prefix_char}"
else
compset -P "*${prefix_char}"
fi
_value
break
fi
done
}
You can use this by adding it to a zstyle line like this:
zstyle ':completion:*' completer _complete _reallyforcefilecompletion
This way, it's only used as a last resort so that smarter completions can try before it. Here's a little explanation of the function starting with the few variables and the command involved:
prefix_char: This gets set to each prefix character we want to complete after. For example, env a=123 has the prefix character =.
PREFIX: Initially this will be set to the part of the current word from the beginning of the word up to the position of the cursor; it may be altered to give a common prefix for all matches.
IPREFIX (not shown in code): compset moves string matches from PREFIX to IPREFIX so that the rest of PREFIX can be completed.
compset: This command simplifies modification of the special parameters, while its return status allows tests on them to be carried out.
_value: Not really sure about this one. The documentation states it plays some sort of role in completion.
Documentation for the completion system
The function: In the second line, we declare prefix_char local to avoid variable pollution. In line three, we start a for loop selecting each prefix_char we want to complete after. In the next if block, we check if the variable PREFIX ends with one of the prefix_chars we want to complete after and if PREFIX contains any quotes. Since PREFIX contains quotes, we use compset -q to basically allow quotes to be ignored so we can complete in them. compset -P strips PREFIX and moves it to IPREFIX, basically so it gets ignored and completion can work.
The next elif statement is for a PREFIX ending with prefix_char but not containing quotes, so we only use compset -P. I added the return 0 to break the loop. A more correct way to make this function would be in a case statement, but we're not using the compset return value, so this works. You don't see anything about file completion besides _value. For the most part we just told the system to ignore part of the word.
Basically this is what the function does. We have a line that looks like:
env TERM=linux PATH=/<---cursor here
The cursor is at the end of that slash. This function allows PREFIX, which is PATH=, to be ignored, so we have:
env TERM=linux /<---cursor here
You can complete a file there with PATH= removed. The function doesn't actually remove the PATH= though, it just recategorizes it as something to ignore.
With this function, you can now complete in all of the examples I listed in the question and a lot more.
One last thing to mention, adding this force-list line in your zshrc cripples this function somehow. It still works but seems to choke. This new force-list function is way better anyway.
zstyle ':completion:*' force-list always
EDIT: There were a couple lines I forgot to copy into the function. Probably should have checked before posting. I think it's good now.

Commenting out a set of lines in a shell script

I was wondering if there is a way to comment out a set of lines in a shell script.
How could I do that? We can use /* */ in other programming languages.
This is most useful when I am converting/using/modifying another script
and I want to keep the original lines instead of deleting.
It seems a cumbersome job to find and prefix # for all the lines which are not used.
Lets say there are 100 lines in the script in consequent lines which are not to used.
I want to comment them all out in one go. Is that possible?
The most versatile and safe method is putting the comment into a void quoted
here-document, like this:
<<"COMMENT"
This long comment text includes ${parameter:=expansion}
`command substitution` and $((arithmetic++ + --expansion)).
COMMENT
Quoting the COMMENT delimiter above is necessary to prevent parameter
expansion, command substitution and arithmetic expansion, which would happen
otherwise, as Bash manual states and POSIX shell standard specifies.
In the case above, not quoting COMMENT would result in variable parameter
being assigned text expansion, if it was empty or unset, executing command
command substitution, incrementing variable arithmetic and decrementing
variable expansion.
Comparing other solutions to this:
Using if false; then comment text fi requires the comment text to be
syntactically correct Bash code whereas natural comments are often not, if
only for possible unbalanced apostrophes. The same goes for : || { comment text }
construct.
Putting comments into a single-quoted void command argument, as in :'comment
text', has the drawback of inability to include apostrophes. Double-quoted
arguments, as in :"comment text", are still subject to parameter expansion,
command substitution and arithmetic expansion, the same as unquoted
here-document contents and can lead to the side-effects described above.
Using scripts and editor facilities to automatically prefix each line in a
block with '#' has some merit, but doesn't exactly answer the question.
if false
then
...code...
fi
false always returns false so this will always skip the code.
You can also put multi-line comments using:
: '
comment1comment1
comment2comment2
comment3comment3
comment4comment4
'
As per the Bash Reference for Bourne Shell builtins
: (a colon)
: [arguments]
Do nothing beyond expanding arguments and performing redirections. The return status is zero.
Thanks to Ikram for pointing this out in the post Shell script put multiple line comment
You can use a 'here' document with no command to send it to.
#!/bin/bash
echo "Say Something"
<<COMMENT1
your comment 1
comment 2
blah
COMMENT1
echo "Do something else"
Wikipedia Reference
Text editors have an amazing feature called search and replace. You don't say what editor you use, but since shell scripts tend to be *nix, and I use VI, here's the command to comment lines 20 to 50 of some shell script:
:20,50s/^/#/
: || {
your code here
your code here
your code here
your code here
}
What if you just wrap your code into function?
So this:
cd ~/documents
mkdir test
echo "useless script" > about.txt
Becomes this:
CommentedOutBlock() {
cd ~/documents
mkdir test
echo "useless script" > about.txt
}
As per this site:
#!/bin/bash
foo=bar
: '
This is a test comment
Author foo bar
Released under GNU
'
echo "Init..."
# rest of script
Depending of the editor that you're using there are some shortcuts to comment a block of lines.
Another workaround would be to put your code in an "if (0)" conditional block ;)
This Perl one-liner comments out lines 1 to 3 of the file orig.sh inclusive (where the first line is numbered 0), and writes the commented version to cmt.sh.
perl -n -e '$s=1;$e=3; $_="#$_" if $i>=$s&&$i<=$e;print;$i++' orig.sh > cmt.sh
Obviously you can change the boundary numbers as required.
If you want to edit the file in place, it's even shorter:
perl -in -e '$s=1;$e=3; $_="#$_" if $i>=$s&&$i<=$e;print;$i++' orig.sh
Demo
$ cat orig.sh
a
b
c
d
e
f
$ perl -n -e '$s=1;$e=3; $_="#$_" if $i>=$s&&$i<=$e;print;$i++' orig.sh > cmt.sh
$ cat cmt.sh
a
#b
#c
#d
e
f

Resources