ZSH Equivalent to Bash `_command` completion - shell

Given an example wrapper shell function:
app() {
setsid "$#"
}
In Bash, you can do complete -F _command app to automatically give app completions for all commands (not just binaries on the path but also the arguments afterwards).
How can you do this in ZSH? I have some leads with _normal and -command-line- but I've been unable to figure out the correct command to put in a .zshrc.
Update:
Just wanted to document something interesting, this produces a SIGSEGV:
autoload -U compinit && compinit
compdef _normal app
# Attempt tab completion of app:
# _normal:1: maximum nested function level reached; increase FUNCNEST?
FUNCNEST=999
# Attempt tab completion of app again
# !!! ZSH crashes (see coredumpctl) !!!

Figured it out:
# Set up autocomplete
_app() {
shift words
(( CURRENT-- ))
_normal
}
autoload -U compinit && compinit
compdef _app app
This was somewhat of a duplicate of this question: How do I dynamically select a completion function in zsh?
If someone knows a shorter solution like what Bash has then please answer as that would be nice.

Related

Override of "select" builtin

Recently I tried to override three common commands:
sleep
wait
select
The first one (sleep) is commonly an external bin (/bin/sleep in my Debian 10).
The second one (wait) is a builtin (You can check it with command -v wait).
The third one (select) is also a builtin.
I will write some examples in order to reproduce what works and what not, please be patient.
There are no problems in overriding sleep and wait, I just added somewhere in my code the following:
sleep() {
echo using custom sleep
}
wait() {
echo using custom wait
}
Things change when I try to override select.
In particular, if I try to create a simple function as above, I get errors as the parser (?) thinks I am trying to use the command rather than create a new function.
You can reproduce the error with the following:
#!/bin/bash
select() {
echo using custom select
}
This is avoidable using the notation function select() {.
If I'm using an interactive shell I can solve the issue with an alias; steps to reproduce:
_select() { echo using custom select;}
alias select='_select'
select
BUT this solution doesn't work if I use files.
Let's try something like:
#!/bin/bash
# this is the actual script
. lib.sh
select
... and:
#!/bin/bash
# this is where I declare the function select
_select() {
echo using custom select
}
alias select='_select'
If I run script which in turn source lib.sh I will get an error when I try to call my alias.
This is the first time, for me, that an alias is a "second choice" after a builtin.
Is there something I am doing wrong or is this an actual "bug"?
Obviusly, a workaround would be rename the function in something else.
From the bash man page in section ALIASES:
Aliases are not expanded when the shell is not interactive, unless the expand_aliases shell option is set
using shopt (see the description of shopt under SHELL BUILTIN COMMANDS below).
Either add
shopt -s expand_aliases
before the first use of select or add -i to your shebang
#!/bin/bash -i
for an interactive shell.

Autodetect possible arguments in shell script for completion

Here's a shell script that does some stuff according to with what parameter it was called:
if [ $1 = "-add" ]
then
...
elif [ $1 = "-remove" ]
...
else
...
fi
A script is an executable one (a link to it was created in the /usr/bin directory). So, I can call it from shell by specifying the link name added in /usr/bin.
What I want, is auto-detecting the possible arguments of script (in my case they are -add, -remove) during it calling. It means that when I'll type a command, related to script calling, then type -re and press a tab button it will suggest that it's -remove and autofill it for me.
How the arguments need to be defined to reach that?
Tried to create aliases in shell config file or few links in /usr/bin directory for all possible inputs and it was working fine, but I don't think it's a best solution for that.
While it does require some configuration outside of your script, adding autocomplete options is fairly easy.
Here's a simple example of a ~/.bash_completion file that adds auto completion of --add and --remove to command yourscript. In a real world case you'd probably want to generate the options by querying the script directly; they're hard coded here for simplicity.
_yourscript_complete()
{
# list of options for your script
local options="--add --remove"
# current word being completed (provided by stock bash completion)
local current_word="${COMP_WORDS[COMP_CWORD]}"
# create list of possible matches and store to ${COMREPLY[#}}
COMPREPLY=($(compgen -W "${options}" -- "$current_word"))
}
complete -F _yourscript_complete yourscript
Note that the ~/.bash_completion is only sourced during login, so you'll need to spawn another login shell to see your changes in action. You may need to enable sourcing of user bash_completion files on your system, too.
The result:
$ yourscript --<tab><tab>
--add --remove
It seems that bash/zsh completion is a powerful tool that can manage you shell input in the way you want.
In one of the other answers some explanations about how it works in bash were presented.
I'm a user of zsh, so, I think, it wouldn't be superfluous to show how I managed my task there:
Configuring .zshrc file:
adding folder for your autocomplete functions:
fpath=(~/.zsh-completions $fpath)
enabling zsh tab-completion system:
autoload -U compinit & compinit
Note: above accented lines must be added to ~/.zshrc file.
Adding a function for your script:
After configuring .zshrc file, restarting zsh and typing scriptname in it, compinit() function will list all files with underscope from $fpath and find the one with first line that matches #compdef scriptname.
So, a new file _scriptname that will hold a function for our script must be added to ~/.zsh-completions directory. To let compinit() find this file on scriptname typing, as mentioned above, its first line must be: #compdef scriptname.
Let the arguments dancing:
For zsh there are a lot of completion functions examples in /usr/share/zsh/functions/Completion directory. By finding the appropriate one there you can configure your shell input according to your tastes. For auto-detecting the attributes (in my case they are -add and -remove) an _attributes() function in the _scriptname
file could be set in the next way:
_arguments -s \
'(-a --add)'{-a,--add}'[adding a new item to the basket]' \
'(-r --remove)'{-r,--remove}'[removing an item from the basket]'
Eventually, after restarting zsh again, auto-detecting for scriptname is working as below:
scriptname -<TAB> => scriptname -
--add -a -- adding a new item to the basket
--remove -r -- removing an item from the basket
scriptname --a<TAB> => scriptname --add
scriptname --r<TAB> => scriptname --remove

Failing to modify .bashrc

My .bashrc file contains:
# mkdir, cd into it
function mkcd ()
{
mkdir -p "$*"
cd "$*"
}
When I type mkcd in shell I get mkcd: Command not found.
when I type source ~/.bashrc I get an error:
Badly placed ()'s.
by the way, my text editor (emacs) is recognising the code as Shell-script[tcsh].
How do I fix this?
If you can accept the restriction that you have to pass the name of the directory to be created as the first argument, it should look like this:
# mkdir, cd into it
function mkcd ()
{
mkdir -p "$#"
cd "$1"
}
You need to run source ~/.bashrc to see it working (or alternatively start a new shell).
Three comments on that function. This will work mostly. To catch some corner cases:
Either use function mkcd { ...; } or mkcd() { ...; }. The first is compatible with ksh, but only if you drop the (). The mkcd() notation is the standard POSIX notation.
Even mkdir -p can fail, so make the cd conditional on mkdir.
Finally, you want exactly one argument to mkdir and cd. Use only one argument, and test that it has a value with the :? modifier in parameter substitution. This will stop the function from sending you $HOME.
Together:
function mkcd
{
mkdir -p "${1:?}" && cd "${1}"
}
Put that in your .bashrc and open a new shell. Type type mkcd. This should respond with:
mkcd is a function, followed by its definition.
I ran your mkcd function on bash 4.2.45 and linux 3.8.0 and it worked as expected. Logging on in a fresh window or running
source ~/.bashrc
in your existing window should define the function for you. If it does not work you'll get an error message like:
mkcd: command not found
While hek2mgl's suggestion is not necessary to make it work, it does make it make more sense since you're only going to cd to one directory.
As commented by Henk Langeveld and hek2mgl: "Wrong shell. Badly placed ()'s is a message produced by tcsh. Switch to bash or ksh."
I thought that opening a terminal on ubuntu entered straight into bash environment. In fact, as commented below, "a Terminal will start a copy of your login shell as defined in /etc/passwd". By typing ps -p $$ in terminal, I realise mine is set to tcsh, the C shell. In that case, one needs to type bash to get into bash environment.
Then source ~/.bashrc compiles the definition for mkcd.

Read string with ZSH completions

I want to read a string from a user (using read builtin or something similar), with all completions enabled for normal shell usage. In other words, I want ZSH to complete all commands but after pressing ENTER I don't want ZSH to execute the command, but pass the string to my script. How can I achieve this?
To read a line with edition and completion, call the vared builtin.
foo='default text'
vared foo
Completion will work as if you were in the value part of a parameter assignment (because that's what vared does). If you want completion like a normal command line, I think you need to fiddle with _complete to make it forget about being inside vared.
zmodload zsh/parameter
autoload +X _complete
functions[_original_complete]=$functions[_complete]
_complete () {
unset 'compstate[vared]'
_original_complete "$#"
}
foo='default text'
vared foo

Is there a hook in Bash to find out when the cwd changes?

I am usually using zsh, which provides the chpwd() hook. That is: If the cwd is changed by the cd builtin, zsh automatically calls the method chpwd() if it exists. This allows to set up variables and aliases which depend on the cwd.
Now I want to port this bit of my .zshrc to bash, but found that chpwd() is not recognized by bash. Is a similar functionality already existing in bash? I'm aware that redefining cd works (see below), yet I'm aiming for a more elegant solution.
function cd()
{
builtin cd $#
chpwd
}
You would have to use a DEBUG trap or PROMPT_COMMAND.
Examples:
trap chpwd DEBUG # calls the function before each command
PROMPT_COMMAND=chpwd # calls the function after each command
Note that the function defined in PROMPT_COMMAND is run before each prompt, though, even empty ones.
A better solution could be defining a custom chpwd hook.
There's not a complete hook system designed in Bash when compared with other modern shells. PROMPT_COMMAND variable is used as a hook function, which is equivalent to precmd hook in ZSH, fish_prompt in Fish. For the time being, ZSH is the only shell I've known that has a chpwd hook builtin.
PROMPT_COMMAND
If set, the value is interpreted as a command to execute before the printing of each primary prompt ($PS1).
https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Bash-Variables
chpwd Hook in Bash
A trick is provided to setup a chpwd equivalent hook in Bash based on PROMPT_COMMAND.
# create a PROPMT_COMMAND equivalent to store chpwd functions
typeset -g CHPWD_COMMAND=""
_chpwd_hook() {
shopt -s nullglob
local f
# run commands in CHPWD_COMMAND variable on dir change
if [[ "$PREVPWD" != "$PWD" ]]; then
local IFS=$';'
for f in $CHPWD_COMMAND; do
"$f"
done
unset IFS
fi
# refresh last working dir record
export PREVPWD="$PWD"
}
# add `;` after _chpwd_hook if PROMPT_COMMAND is not empty
PROMPT_COMMAND="_chpwd_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
Usage
# example 1: `ls` list directory once dir is changed
_ls_on_cwd_change() {
ls
}
# append the command into CHPWD_COMMAND
CHPWD_COMMAND="${CHPWD_COMMAND:+$CHPWD_COMMAND;}_ls_on_cwd_change"
# or just use `ls` directly
CHPWD_COMMAND="${CHPWD_COMMAND:+$CHPWD_COMMAND;}ls"
Source: Create chpwd Equivalent Hook in Bash from my gist.

Resources