zsh preexec command modification - zsh

Is there a way to modify the command that is about to execute?
I would like to redirect the output to a file, as well as print it on the terminal.
I found that ls > file.txt | cat does the job, so I would like to add that > file.txt | cat to any command that is about to execute.
Is there a better way to redirect to file and print to terminal? I am trying to make a logger.

You can change the action that is performed when you execute a line to alter the command that will be executed. This can be done by defining a function which you then bind to the enter key.
Lets first define a function that can add the '> file.txt | cat' ending to any command:
function log_and_accept {
BUFFER="$BUFFER > file.txt | cat"
zle accept-line
}
The next part is to actually replace the default enter key behaviour with your new function. The default behaviour we are replacing is the accept-line function, and if we look at the zle documentation, you will see that accept-line is bound to ^J and ^M.
To bind this function to those letters you first need to turn it into a widget:
zle -N log_and_accept_widget log_and_accept
Then you can bind it, replacing the old behaviour:
bindkey '^J' log_and_accept_widget
bindkey '^M' log_and_accept_widget
Now you will be expanding that command for every single command you do. Every cd, ls, vim etc etc. As such I recommend that you define a couple more functions that actually turn this on and off:
function turn_on_logging {
bindkey '^J' log_and_accept_widget
bindkey '^M' log_and_accept_widget
}
function turn_off_logging {
bindkey '^J' accept-line
bindkey '^M' accept-line
}
zle -N turn_on_logging_widget turn_on_logging
zle -N turn_off_logging_widget turn_off_logging
bindkey '^P' turn_on_logging_widget
bindkey '^O' turn_off_logging_widget
I think you should be careful with this. After testing it a little, I quickly grew to dislike it.

There are several ways to do that, the 1 I like the most is this block I found here http://git.grml.org/?p=grml-etc-core.git;a=blob_plain;f=etc/zsh/zshrc;hb=HEAD
abk=(
'...' '../..'
'....' '../../..'
'BG' '& exit'
'C' '| wc -l'
'G' '|& grep '${grep_options:+"${grep_options[*]}"}
'H' '| head'
'Hl' ' --help |& less -r' #d (Display help in pager)
'L' '| less'
'LL' '|& less -r'
'M' '| most'
'N' '&>/dev/null' #d (No Output)
'R' '| tr A-z N-za-m' #d (ROT13)
'SL' '| sort | less'
'S' '| sort -u'
'T' '| tail'
'V' '|& vim -'
'co' './configure && make && sudo make install'
'fc' '> file.txt | cat'
)
zleiab() {
emulate -L zsh
setopt extendedglob
local MATCH
if (( NOABBREVIATION > 0 )) ; then
LBUFFER="${LBUFFER},."
return 0
fi
matched_chars='[.-|_a-zA-Z0-9]#'
LBUFFER=${LBUFFER%%(#m)[.-|_a-zA-Z0-9]#}
LBUFFER+=${abk[$MATCH]:-$MATCH}
}
zle -N zleiab && bindkey ",." zleiab
Also notice that I added 'fc' '> file.txt | cat' to the list abk
What this does in that that you type fc after the command and then you hit ,. (comma and period) in rapid succession and zsh will replace fc for > file.txt | cat

Related

encapsulate sourced script in zsh

I'm trying to control what variables get defined when sourcing a script in zsh. I'm imagining something that corresponds to this code:
(
source variable_definitions
somehow_export variable1=$variable_defined_in_script1
)
echo $variable1
as a result I want variable1 to be defined in the external scope and not variable_defined_in_script or any other variables in the sourced script.
(somehow_export is some magical placeholder in this example that allows exporting variable definitions to a parent shell. I believe that's not possible, so I'm looking for other solutions)
Something like this?
(
var_in_script1='Will this work?'
print variable1=$var_in_script1
) | while read line
do
[[ $line == *=* ]] && typeset "$line"
done
print $variable1
#=> Will this work?
print $var_in_script1
#=>
# empty; variable is only defined in the child shell
This uses stdout to send information to the parent shell. Depending on your requirements, you can add text to the print statement to filter for just the variables you want (this just looks for an '=').
If you need to handle more complex variables such as arrays, typeset -p
is a great option in zsh that can help. It's also useful for simply printing
the contents and types of variables.
(
var_local='this is only in the child process'
var_str='this is a string'
integer var_int=4
readonly var_ro='cannot be changed'
typeset -a var_ary
var_ary[1]='idx1'
var_ary[2]='idx2'
var_ary[5]='idx5'
typeset -A var_asc
var_asc[lblA]='label A'
var_asc[lblB]='label B'
# generate 'typeset' commands for the variables
# that will be sent to the parent shell:
typeset -p var_str var_int var_ro var_ary var_asc
) | while read line
do
[[ $line == typeset\ * ]] && eval "$line"
done
print 'In parent:'
typeset -p var_str var_int var_ro var_ary var_asc
print
print 'Not in parent:'
typeset -p var_local
Output:
In parent:
typeset var_str='this is a string'
typeset -i var_int=4
typeset -r var_ro='cannot be changed'
typeset -a var_ary=( idx1 idx2 '' '' idx5 )
typeset -A var_asc=( [lblA]='label A' [lblB]='label B' )
Not in parent:
./tst05:typeset:33: no such variable: var_local

Make zsh zle restore characters on backspace in vi overwrite-mode

I am using Zsh in Vi-mode.
When $KEYMAP == vicmd (i.e. command-mode), I want hitting backspace to move the cursor to the left by one character, without deleting anything. [working]
When $KEYMAP == viins && $ZLE_STATE == *insert* (i.e. insert-mode), I want hitting backspace to move the cursor to the left by one character, deleting the immediately preceding character on the line. [working]
When $KEYMAP == viins && $ZLE_STATE == *overwrite* (i.e. overwrite-mode / replace-mode), I want hitting backspace to move the cursor to the left by one character, restoring the immediately preceding character on the line with the one that had originally been there prior to entering into overwrite-mode. [NOT working]
Here is an example:
# [COMMAND MODE] We start with the following string on the command line:
$ Hello, world!
^
cursor position
# [REPLACE MODE] Now, I hit "R" to enter replace-mode and I type "stuff".
$ Helstufforld!
^
cursor position
# [REPLACE MODE] Finally, I hit backspace 3 times.
$ Helst, world!
^
cursor position
The above example shows what I want to happen when I hit backspace while in overwrite-mode; however, what really happens is the following:
# [COMMAND MODE] We start with the following string on the command line:
$ Hello, world!
^
cursor position
# [REPLACE MODE] Now, I hit "R" to enter replace-mode and I type "stuff".
$ Helstufforld!
^
cursor position
# [REPLACE MODE] Finally, I hit backspace 3 times.
$ Helstworld!
^
cursor position
Notice how, when hitting backspace in the second example, rather than restoring the original 3 characters that were just overwritten (i.e. ", w"), instead the last 3 characters that replaced these characters (i.e. "uff") were deleted, and the characters to the right of the cursor were shifted to the left.
How do I get the behavior that I want?
Okay, so I ended up hacking together a solution to the problem I was having, which I will post here in case anyone else encounters the same issue.
Solution
Put this in your .zshrc:
readonly ZLE_VI_MODE_CMD=0
readonly ZLE_VI_MODE_INS=1
readonly ZLE_VI_MODE_REP=2
readonly ZLE_VI_MODE_OTH=3
function zle-vi-mode {
if [[ $KEYMAP == vicmd ]]; then
echo -n $ZLE_VI_MODE_CMD
elif [[ $KEYMAP == (viins|main) ]] && [[ $ZLE_STATE == *insert* ]]; then
echo -n $ZLE_VI_MODE_INS
elif [[ $KEYMAP == (viins|main) ]] && [[ $ZLE_STATE == *overwrite* ]]; then
echo -n $ZLE_VI_MODE_REP
else
echo -n $ZLE_VI_MODE_OTH
fi
}
function zle-backward-delete-char-fix {
case "$(zle-vi-mode)" in
$ZLE_VI_MODE_REP)
if [[ $CURSOR -le $MARK ]]; then
CURSOR=$(( $(($CURSOR-1)) > 0 ? $(($CURSOR-1)) : 0 ))
MARK=$CURSOR
else
zle undo
fi
;;
*)
zle backward-delete-char
;;
esac
}
zle -N zle-backward-delete-char-fix
## Change cursor shape according to the current Vi-mode.
function zle-line-init zle-keymap-select {
case "$(zle-vi-mode)" in
$ZLE_VI_MODE_CMD) echo -ne '\e[2 q' ;; # cursor -> block
$ZLE_VI_MODE_INS) echo -ne '\e[6 q' ;; # cursor -> vertical bar
$ZLE_VI_MODE_REP)
echo -ne '\e[4 q' # cursor -> underline
MARK=$CURSOR
;;
*)
;;
esac
}
zle -N zle-line-init
zle -N zle-keymap-select
bindkey -v
bindkey '^?' zle-backward-delete-char-fix
bindkey '^h' zle-backward-delete-char-fix
The above code will, additionally, cause your cursor shape to change depending on what vi-mode you are currently in (i.e. since this is a copy/paste from my .zshrc, and that's what I like). If you don't want this, and just want the plain fix, replace the zle-init-line / zle-keymap-select function with the following:
function zle-line-init zle-keymap-select {
case "$(zle-vi-mode)" in
$ZLE_VI_MODE_REP)
MARK=$CURSOR
;;
*)
;;
esac
}

Enter a command for a user

I have written a ZSH function whose output is a command line which runs a program I need the user to be able to interact with.
At the moment I just echo the command line and instruct the user to copy-paste it so that they have the necessary access to its pipes, however is there a way I can just have the function finish by entering the command for the user as if they had copied and pasted it themselves?
I have looked into using zle but that seems to require a key binding, whereas I just want the user to be able to run: myzshfunction arg1 and the ultimate result to be their terminal attached to the program launched as a result of some processing of their arg1.
$ myzshfunction arg2*2
Run this command! foobar baz4
$ foobar baz4
...
The function looks something like this:
myzshfunction() {
if [[ $# = 0 ]]
then
echo "usage: myzshfunction 1.2.3.4"
return
fi
local creds=`curl "https://xxx/$1/latest" | jq -r 'x'`
local cred_arr=("${(#s|/|)creds}")
local pwd_pipe=$(mktemp -u)
mkfifo $pwd_pipe
exec 3<>$pwd_pipe
rm $pwd_pipe
echo $cred_arr[2] >&3
echo "Run this: sshpass -d3 ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$cred_arr[1]#$1"
exec 3>&-
}
TIA
Use print -z to add text to the buffer. From the documentation:
-z Push the arguments onto the editing buffer stack, separated by spaces.
Calling foo, defined below, will result in hi being placed on the command line as if the user had typed it. For example,
% foo () { print -z hi; }
% foo
% hi
The best solution I could come up with was to use zle - the Zsh Line Editor.
This lets you update the command the user is currently editing, which to me feels a bit hacky. I would prefer a solution that lets you call a function, hit return, and then cleanly run a function with STDIO attached to your terminal as if you had run the resulting command line.
Perhaps you could emulate this by bindkey'ing the return key and doing some sort of decision/routing from there to see if the user wants to call myfunc. For now, my solution requires the Esc+i sequence is entered after typing a target for $host.
zle-myfunc() {
apikey=$(keychain-environment-variable api-key)
if [ $? -ne 0 ]; then
echo "Add your api-key to the keychain: "
BUFFER='security add-generic-password -U -a ${USER} -D "environment variable" -s "api-key" -w'
zle accept-line
return 1
fi
local host=$BUFFER
zle kill-buffer
local creds=`curl ..." | jq -r ...`
if [ -z creds ]; then
echo "Couldn't get creds, check your network"
return 1
fi
local creds_arr=("${(#s|/|)creds}")
local pwd_pipe=$(mktemp -u)
mkfifo $pwd_pipe
exec 3<>$pwd_pipe
# anonymise the pipe
rm $pwd_pipe
echo "$creds_arr[2]" >&3
BUFFER="sshpass -d3 ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $creds_arr[1]#$host"
zle accept-line
# exec 3>&-
}
zle -N zle-myfunc
bindkey '\ei' zle-myfunc

Zsh completion case insensitive _multi_parts function

I have a script that takes file like arguments (multi part arguments), I am fetching the possible values and putting them in an array called raw and then using
_multi_parts / "(${raw[#]})"
to autocomplete. The problem is that this is case sensitive, how can I make it so that it I type mycommand get fo and press Tab it will autocomplete to mycommand get Foo/ if Foo is one of the things in raw.
The full completion is here for reference:
_mycommand() {
local curcontext="$curcontext" state line
_arguments "1: :->command" "*: :->label"
case $state in
command)
_arguments "1: :(add get ls rm)"
;;
*)
case $words[2] in
add|get|ls|rm)
if [ -f ~/.pts ]; then
IFS=$'\n' read -d '' -r raw <<< "$(mycommand ls)"
_multi_parts / "(${raw[#]})"
fi
;;
*)
_files
;;
esac
esac
}
_mycommand "$#"
mycommand ls outputs path like names like the following:
Foo/Bar/x
Foo/Bar/y
Derp/z
Placeholder/z
Okay I figured it out:
Change lines
IFS=$'\n' read -d '' -r raw <<< "$(mycommand ls)"
_multi_parts / "(${raw[#]})"
To
IFS=$'\n' raw=($(mycommand ls))
_multi_parts -M "m:{[:lower:][:upper:]}={[:upper:][:lower:]}" / raw

zsh alias expansion

Is it possible to configure zsh to expand global aliases during tab completion? For example, I have the common aliases:
alias -g '...'='../..'
alias -g '....'='../../..'
but when I type, for example, cd .../some<tab> it won't expand to cd .../something or cd ../../something. Consequently, I frequently won't use these handy aliases because they are incompatible with tab completion.
I'm a user of Mikael Magnusson's rationalise-dot. From my zshrc:
# This was written entirely by Mikael Magnusson (Mikachu)
# Basically type '...' to get '../..' with successive .'s adding /..
function rationalise-dot {
local MATCH # keep the regex match from leaking to the environment
if [[ $LBUFFER =~ '(^|/| | |'$'\n''|\||;|&)\.\.$' ]]; then
LBUFFER+=/
zle self-insert
zle self-insert
else
zle self-insert
fi
}
zle -N rationalise-dot
bindkey . rationalise-dot
# without this, typing a . aborts incremental history search
bindkey -M isearch . self-insert
Try looking up zsh abbreviations. They allow you to enter an "abbreviation" which automatically gets replaced with its full form when you hit a magic key such as space. So you can create one which changes ...<SPACE> to ../...
For example, this is what you need in your profile:
typeset -A abbrevs
abbrevs=(
"..." "../.."
"...." "../../.."
)
#create aliases for the abbrevs too
for abbr in ${(k)abbrevs}; do
alias -g $abbr="${abbrevs[$abbr]}"
done
my-expand-abbrev() {
local MATCH
LBUFFER=${LBUFFER%%(#m)[_a-zA-Z0-9]#}
LBUFFER+=${abbrevs[$MATCH]:-$MATCH}
zle self-insert
}
bindkey " " my-expand-abbrev

Resources