Changing a default Zsh completion function - zsh

I noticed that tab completion for the source command in Zsh tries to complete a LOT of files. Maybe everything in $PATH? I tried using a blank .zshrc file to make sure it wasn't anything in there.
ubuntu% source d
zsh: do you wish to see all 109 possibilities (16 lines)?
I did find this file that seems to control that: /usr/share/zsh/functions/Completion/Zsh/_source
#compdef source .
if [[ CURRENT -ge 3 ]]; then
compset -n 2
_normal
else
if [[ -prefix */ && ! -o pathdirs ]]; then
_files
elif [[ $service = . ]]; then
_files -W path
else
_files -W "(. $path)"
fi
fi
If I change the line in that last "else" statement from _files -W "(. $path)" to _files, it works the way I want it to. The tab completion only looks at files & directories in the current dir.
It doesn't seem like altering this file is the best way to go. I'd rather change something in my .zshrc file. But my knowledge of Zsh completions is a bit lacking and the searching I've done thus far hasn't led me to an answer for this.

Maybe everything in $PATH?
Yes, that is correct. It offers those, because source will search your the current dir and your $PATH for any file name you pass it.
To apply your change without modifying the original file, add this to your .zshrc file after calling compinit:
compdef '
if [[ CURRENT -ge 3 ]]; then
compset -n 2
_normal
else
_files
fi
' source
This tells the completion system to use the inline function you specified for the command source (instead of the default function).
Alternatively, to see file completions for the current dir only, you can type
$ source ./<TAB>

Related

ZSH: Behavior on Enter

I realize, when I'm in my terminal, I would expect to press Enter on empty input to make a ls or a git status when I'm on a git repos.
How can I achieve that? I mean, have a custom behavior on Empty input -> Enter in zsh?
EDIT: Thanks for the help. Here's my take with preexec...
precmd() {
echo $0;
if ["${0}" -eq ""]; then
if [ -d .git ]; then
git status
else
ls
fi;
else
$1
fi;
}
On Enter zsh calls the accept-line widget, which causes the buffer to be executed as command.
You can write your own widget in order to implement the behaviour you want and rebind Enter:
my-accept-line () {
# check if the buffer does not contain any words
if [ ${#${(z)BUFFER}} -eq 0 ]; then
# put newline so that the output does not start next
# to the prompt
echo
# check if inside git repository
if git rev-parse --git-dir > /dev/null 2>&1 ; then
# if so, execute `git status'
git status
else
# else run `ls'
ls
fi
fi
# in any case run the `accept-line' widget
zle accept-line
}
# create a widget from `my-accept-line' with the same name
zle -N my-accept-line
# rebind Enter, usually this is `^M'
bindkey '^M' my-accept-line
While it would be sufficient to run zle accept-line only in cases where there actually was a command, zsh would not put a new prompt after the output. And while it is possible to redraw the prompt with zle redisplay, this will probably overwrite the last line(s) of the output if you are using multi-line prompts. (Of course there are workarounds for that, too, but nothing as simple as just using zle accept-line.
Warning: This redfines an (the most?) essential part of your shell. While there is nothing wrong with that per se (else I would not have posted it here), it has the very real chance to make your shell unusable if my-accept-line does not run flawlessly. For example, if zle accept-line were to be missing, you could not use Enter to confirm any command (e.g. to redefine my-accept-line or to start an editor). So please, test it before putting it into your ~/.zshrc.
Also, by default accept-line is bound to Ctrl+J, too. I would recommend to leave it that way, to have an easy way to run the default accept-line.
In my .zshrc I use a combination of precmd and preexec found here:
http://zsh.sourceforge.net/Doc/Release/Functions.html#Hook-Functions
I also find that the git-prompt is super useful:
https://github.com/olivierverdier/zsh-git-prompt

Combining file tests in Zsh

What is the most elegant way in zsh to test, whether a file is either a readable regular file?
I understand that I can do something like
if [[ -r "$name" && -f "$name" ]]
...
But it requires repeating "$name" twice. I know that we can't combine conditions (-rf $name), but maybe some other feature in zsh could be used?
By the way, I considered also something like
if ls ${name}(R.) >/dev/null 2>&1
...
But in this case, the shell would complain "no matches found", when $name does not fulfil the criterium. Setting NULL_GLOB wouldn't help here either, because it would just replace the pattern with an empty string, and the expression would always be true.
In very new versions of zsh (works for 5.0.7, but not 5.0.5) you could do this
setopt EXTENDED_GLOB
if [[ -n $name(#qNR.) ]]
...
$name(#qNR.) matches files with name $name that are readable (R) and regular (.). N enables NULL_GLOB for this match. That is, if no files match the pattern it does not produce an error but is removed from the argument list. -n checks if the match is in fact non-empty. EXTENDED_GLOB is needed to enable the (#q...) type of extended globbing which in turn is needed because parenthesis usually have a different meaning inside conditional expressions ([[ ... ]]).
Still, while it is indeed possible to write something up that uses $name only once, I would advice against it. It is rather more convoluted than the original solution and thus harder to understand (i.e. needs thinking) for the next guy that reads it (your future self counts as "next guy" after at most half a year). And at least this solution will work only on zsh and there only on new versions, while the original would run unaltered on bash.
How about make small(?) shell functions as you mentioned?
tests-raw () {
setopt localoptions no_ksharrays
local then="$1"; shift
local f="${#[-1]}" t=
local -i ret=0
set -- "${#[1,-2]}"
for t in ${#[#]}; do
if test "$t" "$f"; then
ret=$?
"$then"
else
return $?
fi
done
return ret
}
and () tests-raw continue "${#[#]}";
or () tests-raw break "${#[#]}";
# examples
name=/dev/null
if and -r -c "$name"; then
echo 'Ok, it is a readable+character special file.'
fi
#>> Ok, it is...
and -r -f ~/.zshrc ; echo $? #>> 0
or -r -d ~/.zshrc ; echo $? #>> 0
and -r -d ~/.zshrc ; echo $? #>> 1
# It could be `and -rd ~/.zshrc` possible.
I feel this is somewhat overkill though.

ZSH auto_vim (like auto_cd)

zsh has a feature (auto_cd) where just typing the directory name will automatically go to (cd) that directory. I'm curious if there would be a way to configure zsh to do something similar with file names, automatically open files with vim if I type only a file name?
There are three possibilities I can think of. First is suffix aliases which may automatically translate
% *.ps
to
% screen -d -m okular *.ps
after you do
alias -s ps='screen -d -m okular'
. But you need to define this alias for every file suffix. It is also processed before most expansions so if
% *.p?
matches same files as *.ps it won’t open anything.
Second is command_not_found handler:
function command_not_found_handler()
{
emulate -L zsh
for file in $# ; do test -e $file && xdg-open $file:A ; done
}
. But this does not work for absolute or relative paths, only for something that does not contain forward slashes.
Third is a hack overriding accept-line widget:
function xdg-open()
{
emulate -L zsh
for arg in $# ; do
command xdg-open $arg
endfor
}
function _-accept-line()
{
emulate -L zsh
FILE="${(z)BUFFER[1]}"
whence $FILE &>/dev/null || BUFFER="xdg-open $BUFFER"
zle .accept-line
}
zle -N accept-line _-accept-line
. The above alters the history (I can show how to avoid this) and is rather hackish. Good it does not disable suffix aliases (whence '*.ps' returns the value of the alias), I used to think it does. It does disable autocd though. I can avoid this (just || test -d $FILE after whence test), but who knows how many other things are getting corrupt as well. If you are fine with the first and second solutions better to use them.
I guess you can use "fasd_cd" which has an alias v which uses viminfo file to identifi files which you have opened at least once. In my environment it works like a charm.
Fast cd has other amazing stuff you will love!
Don't forget to set this alias on vim to open the last edited file:
alias lvim="vim -c \"normal '0\""

Regarding UNIX Shell Script

When there is no files inside the folder the below script goes inside the for loop. Not sure what i can modify so that it doesn't go inside the for loop. Also when there is no files inside the directory exit status should be success. Wrapper script checks the exit status of the below script
FILESRAW ="/exp/test1/folder" .
for fspec in "$FILESRAW"/* ; do
echo "$fspec"
if [[ -f ${fspec} ]] ; then
..... processing logic
else
... processing logic
fi
done
if using bash,
you can set nullglob
shopt-s nullglob
if you have hidden files,
shopt -s dotglob
with ksh,
#!/bin/ksh
set -o noglob
for file in /path/*
do
....
done
for fspec in `dir $FILESRAW` ; do
To exit if $FILESRAW is empty:
[ $( ls "$FILESRAW" | wc -l ) -eq 0 ] && exit 0
If this test precedes the loop, it will prevent execution from reaching the for loop if $FILESRAW is empty.
When $FILESRAW is empty, "$FILESRAW"/* expands to "/exp/test1/folder/*", as ghostdog74 points out, you can change this behavior by setting nullglob with
shopt -s nullglob
If you want hidden files, set dotglob as well:
shopt -s dotglob
Alternately, you could use ls instead of globing. This has the advantage of working with very full directories (using a pipe, you won't reach the maximum argument limit):
ls "$FILESRAW" | while read file; do
echo "$file"
This becomes messier if you want hidden files, since you'll need to exclude . and .. to emulate globing behavior:
ls -a "$FILESRAW" | egrep -v '^(\.|\.\.)$' | while read file; do
echo "$file"
if you are using ksh,
try putting this in front of for loop so that it won't go inside it.
"set -noglob"
Even I have got the same problem, but I was able to resolve it by doing this.

bind key to complete filename wherever the context is in Zsh

Sometimes I want a filename instead of what zsh guesses for me. For example, I have a PNG file without a proper .png suffix, which makes zsh think it isn't a picture and won't list it when I type Tab to complete arguments of display.
I am wondering if there is a key sequence that completes for a filename wherever the context is, like ^XC for _correct_filename, or how to configure zsh to do the thing?
You can add a key binding to do what you want:
zle -C complete complete-word complete-files
bindkey '^X\t' complete
complete-files () { compadd - $PREFIX* }
Edit: Added $PREFIX
You can add those lines to your ~/.zshrc file.
That makes the completion list all files when you press Ctrl-x Tab at each step instead of Tab. You could choose another keystroke combination that suits you.
Or to make ImageMagick completions always include all files, try editing (make a backup first) the file /usr/share/zsh/functions/Completion/Unix/_imagemagick (or similar) and change this to comment out the existing line that begins with _files and add the new one shown:
if (( $# )); then
# _files "$#" -g "*.(#i)(${~formats//:/|})(-.)"
_files "$#"
return
fi
Dennis' answer didn't expand tilde for me, so I would get stuff like complete-files: no matches found: ~/ma* when I tried to invoke it on foo ~/ma. I did find an alternate in the zsh FAQ that will expand them, though:
zle -C complete-file complete-word _generic
zstyle ':completion:complete-file::::' completer _files
bindkey '^xF' complete-file

Resources