zsh: Complete `command1` the same as `command2 ARG` - zsh

Given two commands:
command1 - which has completions already set up for it
commmand2- which is a wrapper function, that in the end calls command1 ARG
How do I make command2 complete in the same manner as command1 ARG1 would without writing a custom completion for command1?
Here's an example:
alias command1="git ls-files"
command2() {
echo "I'm a wrapper for git ls-files" >&2
git ls-files $#
}
One can do compdef command2=command1 - but that will make command2 complete the same way as git would and not like git ls-files.
Edit: I'm looking for a broad and general solution that'd also work with commands that do not define separate completion functions such as git has.
For these you can do what Marlon Richert suggested below.
Here's a better example:
alias command1="kubectl get pod"
command2() {
echo "I'm a wrapper for kubectl get pod" >&2
kubectl get pod $#
}

Do this to find out the name of the function that needs to be called:
% git ls-files ^Xh # That is, press Ctrl-X, then H.
tags in context :completion::complete:git-ls-files::
argument-rest options (_arguments _git-ls-files _git)
tags in context :completion::complete:git-ls-files:argument-rest:
globbed-files (_files _arguments _git-ls-files _git)
tags in context :completion::complete:git::
argument-rest (_arguments _git)
As you can see, it's _git-ls-files.
Then, discard the leading _ and use the remainder as the $service argument to compdef:
compdef _git command2=git-ls-files
Now it works correctly:
% command2 ^Xh
tags in context :completion::complete:command2::
argument-rest options (_arguments _git-ls-files _git)
tags in context :completion::complete:command2:argument-rest:
globbed-files (_files _arguments _git-ls-files _git)
Update
For your kubectl example, things are slightly less easy, because its completion is not Zsh-native. Instead, it's just a thin Zsh wrapper around a Bash completion function. In this case, you will have to write your own completion function, but thankfully, it's going to be just a mercifully short one:
_command2 () {
# Fake having `kubectl get pod` on the line instead of `command2`.
local -a words=( kubectl get pod $words[2,-1] )
local -i CURRENT=$(( CURRENT + 2 ))
# Restart completion with our new, simulated command line.
_normal
}
compdef _command2 command2
Done!

Related

Unit testing Zsh completion script

I'm trying to write a completion script for Zsh. I'd like to unit test the completion script. For example, I'd like to test that completions for my-command --h include --help.
For Fish, I can use complete -C 'my-command --h', which would then output --help and any other valid completions.
I can't seem to find an equivalent command for Zsh. Does one exist? I've tried things like _main_complete, _complete and _normal, but either they don't support this or I'm not invoking them in the correct way (I get a lot of can only be called from completion function errors).
I get a lot of can only be called from completion function errors
This is because Zsh's completion commands can run only from inside a completion widget, which in turn can only be called while the Zsh Line Editor is active. We can work around this by activating a completion widget on an active command line inside a so-called pseudo terminal:
# Set up your completions as you would normally.
compdef _my-command my-command
_my-command () {
_arguments '--help[display help text]' # Just an example.
}
# Define our test function.
comptest () {
# Gather all matching completions in this array.
# -U discards duplicates.
typeset -aU completions=()
# Override the builtin compadd command.
compadd () {
# Gather all matching completions for this call in $reply.
# Note that this call overwrites the specified array.
# Therefore we cannot use $completions directly.
builtin compadd -O reply "$#"
completions+=("$reply[#]") # Collect them.
builtin compadd "$#" # Run the actual command.
}
# Bind a custom widget to TAB.
bindkey "^I" complete-word
zle -C {,,}complete-word
complete-word () {
# Make the completion system believe we're on a normal
# command line, not in vared.
unset 'compstate[vared]'
_main_complete "$#" # Generate completions.
# Print out our completions.
# Use of ^B and ^C as delimiters here is arbitrary.
# Just use something that won't normally be printed.
print -n $'\C-B'
print -nlr -- "$completions[#]" # Print one per line.
print -n $'\C-C'
exit
}
vared -c tmp
}
zmodload zsh/zpty # Load the pseudo terminal module.
zpty {,}comptest # Create a new pty and run our function in it.
# Simulate a command being typed, ending with TAB to get completions.
zpty -w comptest $'my-command --h\t'
# Read up to the first delimiter. Discard all of this.
zpty -r comptest REPLY $'*\C-B'
zpty -r comptest REPLY $'*\C-C' # Read up to the second delimiter.
# Print out the results.
print -r -- "${REPLY%$'\C-C'}" # Trim off the ^C, just in case.
zpty -d comptest # Delete the pty.
Running the example above will print out:
--help
If you want to test the entire completion output and not just the strings that would be inserted on the command line, then see https://unix.stackexchange.com/questions/668618/how-to-write-automated-tests-for-zsh-completion/668827#668827

ZSH ZLE Widget remove accept-line command from history

I would like to add some additional syntax to zsh. I would like //some/path/to/file to be relative to the root of my source code tree (/src/repositories/projects). So running ls //some/path/to/directory should list files in /src/repositories/projects/some/path/to/directory.
The best way I've found to do this is with a ZLE accept-line widget which rewrites //... paths with the full UNIX path /src/repositories/projects/... and then invokes the command. This works quite well, but it expands the //... syntax inline and stores the expansion in history, which can make searches a bit more complicated. I would like to store the original //... syntax in the history while keep the /src/repositories/projects/... syntax out.
I am able to use print -s to add the original //... syntax to history, but I am having trouble keeping the expanded syntax out of history. Currently my code looks like this:
function accept-line-override() {
# Add untranslated command to history
print -s "$BUFFER"
# Translate //... paths to /src/repositories/projects/...
BUFFER=$(fix-paths "$BUFFER")
# Invoke command with new paths
zle .accept-line
}
zle -N accept-line accept-line-override
This gives me two lines of history for every invoked command:
$ echo //test
/src/repositories/projects/test
$ history | tail -n 2
10020 echo //test
10021 echo /src/repositories/projects/test
Is there a way I can prevent zle .accept-line from adding its command to history? I've seen methods involving history -d but those only seem to work for bash. I also saw somewhere to do:
function accept-line-override() {
# Add untranslated command to history
print -s "$BUFFER"
# Translate //... paths to /src/repositories/projects/...
BUFFER=$(fix-paths "$BUFFER")
# Manually invoke line so it is not added to history
echo
eval $BUFFER
echo
# Reset prompt
BUFFER=""
zle .reset-prompt
}
But this leads to a lot of undesired side-effects, from Emacs refusing to open to auto-completions not clearing before the command is executed. I'd very much like to avoid that complexity if at all possible. Any idea on getting around this?

Use preexec() to evaluate entered command

I want to use preexec() to modify certain commands before they are run but I need to be able to evaluate the current entered command. Is there a variable that contains the entire command before it is executed? I know !! is the last command but I need the current line before it's saved to history.
An example of what I want to do would probably help
ls -l /root please
And then I want preexec to see I wrote "please" at the end and replace it with
sudo ls -l /root
I think something like
preexec() {
if [[ $CURRENT_LINE =~ please$ ]]; then
$CURRENT_LINE="sudo ${CURRENT_LINE% please}"
fi
Would work but I can't find a variable in zsh that gives me the correct $CURRENT_LINE
For bonus points I also want to be able to enter please on a line by itself and have it run sudo !! but I could probably do that with some form of alias.
I think it might be better to make a please function that I can pipe a command to but I don't think that'll work as well because the command will run and fail (before piping) before it is run again with sudo.
As far as I know that the preexec is not for the right place to modify the command to be executed though. We can not change the commands to be executed from inside of the preexec function…
Although the actual command to be executed are passed as $1, $2 and $3.
preexec
Executed just after a command has been read and is about to be executed. If the history mechanism is active (and the line was not discarded from the history buffer), the string that the user typed is passed as the first argument, otherwise it is an empty string. The actual command that will be executed (including expanded aliases) is passed in two different forms: the second argument is a single-line, size-limited version of the command (with things like function bodies elided); the third argument contains the full text that is being executed.
-- zshmisc(1) 9.3.1 Hook Functions
For example:
alias ls='ls -sF --color=auto'
preexec () {
print ">>>preexec<<<"
print -l ${(qqq)#}
}
If I have above in ~/.zshrc then I will get follows:
% echo test preexec<Esc-Return>
ls<Return>
;# outputs below
>>>preexec<<<
"echo test preexec
ls"
"echo test preexec; ls -sF --color=auto"
"echo test preexec
ls -sF --color=auto"
test preexec
total 1692
...
You could add your own zle widget functions to the zsh line editor for manipulating the line editor buffer. (zshzle(1))
You could add the zle widget function to change the behavior for hitting Enter.
my-accept-line () {
if [[ "$BUFFER" == *" please" ]]; then
BUFFER="sudo ${BUFFER% please}"
fi
zle .accept-line
}
zle -N accept-line my-accept-line
The above snippets changes the functionality for accept-line from the built-in behavior to my-accept-line defined here.
Adding the abbreviations also could help which is described below:
Cloning vim's abbreviation feature
-- “examples:zleiab [ZshWiki]” - http://zshwiki.org/home/examples/zleiab

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\""

'tee' and exit status

Is there an alternative to tee which captures standard output and standard error of the command being executed and exits with the same exit status as the processed command?
Something like the following:
eet -a some.log -- mycommand --foo --bar
Where "eet" is an imaginary alternative to "tee" :) (-a means append and -- separates the captured command). It shouldn't be hard to hack such a command, but maybe it already exists and I'm not aware of it?
This works with Bash:
(
set -o pipefail
mycommand --foo --bar | tee some.log
)
The parentheses are there to limit the effect of pipefail to just the one command.
From the bash(1) man page:
The return status of a pipeline is the exit status of the last command, unless the pipefail option is enabled. If pipefail is enabled, the pipeline's return status is the value of the last (rightmost) command to exit with a non-zero status, or zero if all commands exit successfully.
I stumbled upon a couple of interesting solutions at Capture Exit Code Using Pipe & Tee.
There is the $PIPESTATUS variable available in Bash:
false | tee /dev/null
[ $PIPESTATUS -eq 0 ] || exit $PIPESTATUS
And the simplest prototype of "eet" in Perl may look as follows:
open MAKE, "command 2>&1 |" or die;
open (LOGFILE, ">>some.log") or die;
while (<MAKE>) {
print LOGFILE $_;
print
}
close MAKE; # To get $?
my $exit = $? >> 8;
close LOGFILE;
Here's an eet. Works with every Bash I can get my hands on, from 2.05b to 4.0.
#!/bin/bash
tee_args=()
while [[ $# > 0 && $1 != -- ]]; do
tee_args=("${tee_args[#]}" "$1")
shift
done
shift
# now ${tee_args[*]} has the arguments before --,
# and $* has the arguments after --
# redirect standard out through a pipe to tee
exec | tee "${tee_args[#]}"
# do the *real* exec of the desired program
exec "$#"
(pipefail and $PIPESTATUS are nice, but I recall them being introduced in 3.1 or thereabouts.)
This is what I consider to be the best pure-Bourne-shell solution to use as the base upon which you could build your "eet":
# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; echo $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.
I think this is best explained from the inside out – command1 will execute and print its regular output on stdout (file descriptor 1), then once it's done, echo will execute and print command1's exit code on its stdout, but that stdout is redirected to file descriptor three.
While command1 is running, its stdout is being piped to command2 (echo's output never makes it to command2 because we send it to file descriptor 3 instead of 1, which is what the pipe reads). Then we redirect command2's output to file descriptor 4, so that it also stays out of file descriptor one – because we want file descriptor one clear for when we bring the echo output on file descriptor three back down into file descriptor one so that the command substitution (the backticks) can capture it.
The final bit of magic is that first exec 4>&1 we did as a separate command – it opens file descriptor four as a copy of the external shell's stdout. Command substitution will capture whatever is written on standard out from the perspective of the commands inside it – but, since command2's output is going to file descriptor four as far as the command substitution is concerned, the command substitution doesn't capture it – however, once it gets "out" of the command substitution, it is effectively still going to the script's overall file descriptor one.
(The exec 4>&1 has to be a separate command to work with many common shells. In some shells it works if you just put it on the same line as the variable assignment, after the closing backtick of the substitution.)
(I use compound commands ({ ... }) in my example, but subshells (( ... )) would also work. The subshell will just cause a redundant forking and awaiting of a child process, since each side of a pipe and the inside of a command substitution already normally implies a fork and await of a child process, and I don't know of any shell being coded to recognize that it can skip one of those forks because it's already done or is about to do the other.)
You can look at it in a less technical and more playful way, as if the outputs of the commands are leapfrogging each other: command1 pipes to command2, then the echo's output jumps over command2 so that command2 doesn't catch it, and then command2's output jumps over and out of the command substitution just as echo lands just in time to get captured by the substitution so that it ends up in the variable, and command2's output goes on its way to the standard output, just as in a normal pipe.
Also, as I understand it, at the end of this command, $? will still contain the return code of the second command in the pipe, because variable assignments, command substitutions, and compound commands are all effectively transparent to the return code of the command inside them, so the return status of command2 should get propagated out.
A caveat is that it is possible that command1 will at some point end up using file descriptors three or four, or that command2 or any of the later commands will use file descriptor four, so to be more hygienic, we would do:
exec 4>&1
exitstatus=`{ { command1 3>&-; echo $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-
Commands inherit file descriptors from the process that launches them, so the entire second line will inherit file descriptor four, and the compound command followed by 3>&1 will inherit the file descriptor three. So the 4>&- makes sure that the inner compound command will not inherit file descriptor four, and the 3>&- makes sure that command1 will not inherit file descriptor three, so command1 gets a 'cleaner', more standard environment. You could also move the inner 4>&- next to the 3>&-, but I figure why not just limit its scope as much as possible.
Almost no programs uses pre-opened file descriptor three and four directly, so you almost never have to worry about it, but the latter is probably best to keep in mind and use for general-purpose cases.
{ mycommand --foo --bar 2>&1; ret=$?; } | tee -a some.log; (exit $ret)
KornShell, all in one line:
foo; RET_VAL=$?; if test ${RET_VAL} != 0;then echo $RET_VAL; echo Error occurred!>/tmp/out.err;exit 2;fi |tee >>/tmp/out.err ; if test ${RET_VAL} != 0;then exit $RET_VAL;fi
#!/bin/sh
logfile="$1"
shift
exec 2>&1
exec "$#" | tee "$logfile"
Hopefully this works for you.
Assuming Bash or Z shell (zsh),
my_command >>my_log 2>&1
N.B. The sequence of redirection and duplication of standard error onto standard output is significant!
I didn't realise you wanted to see the output on screen as well. This will of course direct all output to the file my_log.

Resources