Use preexec() to evaluate entered command - zsh

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

Related

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

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!

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?

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

zsh create a command line within a script but do not execute it

I'm wondering if it's possible to write a zsh script that will write a command to the prompt but NOT execute it, i.e. leave it there for me to edit and then execute when I'm ready. I can do something like this with keybindings by leaving off the final '\C-m'. eg:
bindkey -s "\e[1;3C" "howdy!"
... I press Alt+RightArrow and the text "howdy!" is printed at the prompt and just left there.
I can also do something like what I want by writing my command to the history file and then recalling it with the up arrow. I've tried 'echo -n sometext' but it doesn't work.
Can I write a script that would exit leaving (say) " howdy! " on the command line? In actual fact I want the script to build up a complex command based on several things, but I want the script to leave it on the CLI for final editing, so automatic execution must be prevented.
Thanks in advance.
Turns out the answer is simple:
print -z $string-to-print
If you mean a zsh function and not an external script, you can write a zle (short for zsh line editor) widget and bind it to some key.
# define to function to use
hello () {
BUFFER=hello
zle end-of-line
}
# create a zle widget, which will invoke the function.
zle -N hello
# bindkey Alt-a to that widget
bindkey "\ea" hello
You can learn more from A User's Guide to the Z-Shell, Chapter 4.

Resources