last n elements of an array - zsh

I'm trying to write a function that will print out the last 3 elements of $PWD, with a '...' beforehand if there are more than 3 elements.
e.g.
/home/nornagon/src --> ~/src
/home/nornagon/src/foo/bar/baz --> ...foo/bar/baz
This is my code so far, but $foo[-3,-1] doesn't work if the array has too few elements in it.
function custom_pwd() {
d=${PWD/#$HOME/\~}
d=(${(s:/:)d})
echo $d[-4,-1]
}

The zsh already has some nifty prompt processing available with print's -P option. This should do the trick:
custom_pwd() {
d=$(print -P '%3~')
case $d in
('~'*|/*) echo "$d";;
(*) echo "...$d"
esac
}
See man zshmisc, section "EXPANSION OF PROMPT SEQUENCES" for the gory details.

Here's what I came up with, though it's not terribly elegant:
function custom_pwd() {
local d slash
d=${PWD/#$HOME/\~}
case $d in
/*) slash=/ ;;
*) slash= ;;
esac
d=(${(s:/:)d})
d[1]=$slash$d[1]
num=$#d
ellipsis=
if (( num > 3 )); then num=3; ellipsis='…'; fi
echo $ellipsis${(j./.)d[-$num,-1]}
}

Related

In zsh, is the tag "dynamic-dirs" special?

I’m wanting to do basically what this function does. It comes from the zsh manual page and other zsh documentation. I’m trying to “vaguely understand” without diving into the fine details what this function is doing and how it works.
In this case, I understand everything more or less except the _wanted line and in particular, is the tag "dynamic-dirs" any arbitrary tag or does it need to match what the higher level driving functions are looking for? I read briefly about tags and it seems like they would need to match up but I can’t find dynamic-dirs anywhere in any code that I’ve grep’ed. Nor have a found any type of list of tags that are used and what they means except the example of “files” and “directories” mentioned in the first few paragraphs of zshcompsys(1).
zsh_directory_name() {
emulate -L zsh
setopt extendedglob
local -a match mbegin mend
if [[ $1 = d ]]; then
# turn the directory into a name
if [[ $2 = (#b)(/home/pws/perforce/)([^/]##)* ]]; then
typeset -ga reply
reply=(p:$match[2] $(( ${#match[1]} + ${#match[2]} )) )
else
return 1
fi
elif [[ $1 = n ]]; then
# turn the name into a directory
[[ $2 != (#b)p:(?*) ]] && return 1
typeset -ga reply
reply=(/home/pws/perforce/$match[1])
elif [[ $1 = c ]]; then
# complete names
local expl
local -a dirs
dirs=(/home/pws/perforce/*(/:t))
dirs=(p:${^dirs})
_wanted dynamic-dirs expl 'dynamic directory' compadd -S\] -a dirs
return
else
return 1
fi
return 0
}
The other question is about the ’n’ section. It is going to return a reply even if the directory doesn’t exist. Am I reading the code right?
I guess this would be nice if I was going to do mkdir ~p:foodog ?

Check if "the parameter named by the value of x" is set

I am currently using a piece of code like this:
if (( $( eval "echo \${+$foo}" ) )); then
./my-prog
fi
Is there a more elegant or concise way to express this? Originally I tried
if (( ${+${(P)foo}} )); then
./my-prog
fi
but that raised the "bad substitution" error.
There's also the -v operator, which precludes the need for indirect parameter expansion:
if [[ -v $foo ]]; then
./my-prog
fi
This is documented in man zshmisc, in the CONDITIONAL EXPRESSIONS section.
(P) and + can be used in the same parameter expansion, if you use them in the right order:
if (( ${(P)+foo} )); then
./my-prog
fi

How zsh zle prevent variable CURSOR to be modified arbitrarily in runtime?

As the document says, the zle variable CURSOR can only be in range [0, $#BUFFER].
Test code (put it into .zshrc, the ^[OP is F1):
testCursor() {
echo "\nOriginal C: $CURSOR"
BUFFER="a"
echo "Change Buffer: $CURSOR"
CURSOR=$((CURSOR+10))
echo "Force edit: $CURSOR"
CURSOR=100
echo "Force assign: $CURSOR"
}
zle -N testCursor
bindkey '^[OP' testCursor
The CURSOR satisfied it's range definition in runtime, how did the zsh-zle implements it?
The CURSOR value is handled in Zsh's source code, which is implemented in the C programming language: https://github.com/zsh-users/zsh/blob/3c93497eb701d8f220bc32d38e1f12bfb534c390/Src/Zle/zle_params.c#L266
There is no way for you to declare a similarly constrained variable in Zsh shell code.
However, you can write a math function for it instead:
# Declare a global integer.
typeset -gi TEST=0
# -H makes these hidden, that is, not listed automatically.
typeset -gHi _TEST_MIN=0 _TEST_MAX=10
# Load `min` and `max` functions.
autoload -Uz zmathfunc && zmathfunc
set_test() {
(( TEST = min(max($1,$_TEST_MIN),$_TEST_MAX) ))
}
get_test() {
return $(( min(max($TEST,$_TEST_MIN),$_TEST_MAX) ))
}
# Declare `set_test` as a math function accepting exactly one numeric argument.
functions -M set_test 1
# Declare `get_test` as a math function accepting exactly zero arguments.
functions -M get_test 0
You can then use these in arithmetic statements, with this syntax:
❯ print $(( get_test() ))
0
❯ (( set_test(100) ))
❯ print $(( get_test() ))
10
But also in other contexts, with this syntax:
❯ set_test -1
❯ get_test; print $?
0

Why ever use zsh array-parameters?

list=(1 2 3)
for i in $list; do echo $i; done;
for i in $list[#]; do echo $i; done;
for i in $list[*]; do echo $i; done;
for i in ${list}; do echo $i; done;
for i in ${list[#]}; do echo $i; done;
for i in ${list[*]}; do echo $i; done;
for i in "${list[#]}"; do echo $i; done;
All of these print the same thing:
1
2
3
These
for i in "$list"; do echo $i; done;
for i in "${list}"; do echo $i; done;
for i in "$list[*]"; do echo $i; done;
for i in "${list[*]}"; do echo $i; done;
all print
1 2 3
When should I use brackets vs. no brackets and # vs. *? For example, I know the difference between "${list[#]}" and "${list[*]}", but I have not been able to find a straightforward answer on the difference between $list, ${list}, $list[#], ${list[#]}, $list[*], and ${list[*]}. Why would I ever use array parameters when I could just do $list?
Similarly, why wouldn't I just "$list" when I want all of the elements in the array in one string? In that case I have, insanely, 4 different options: "$list", "${list}", "$list[*]", "${list[*]}".
In general, it is up to us to which to use. But it could be worthwhile to dig into why zsh permits to write codes like these.
From zsh FAQ documents:
1.2: What is it?
Zsh is a UNIX command interpreter (shell) which of the standard shells most resembles the Korn shell (ksh);
--- Chapter 1: Introducing zsh and how to install it, Z-Shell Frequently-Asked Questions
and,
Chapter 2: How does zsh differ from...?
As has already been mentioned, zsh is most similar to ksh, while many of the additions are to please csh users.
...
2.1: Differences from sh and ksh
Most features of ksh (and hence also of sh) are implemented in zsh; problems can arise because the implementation is slightly different.
--- Chapter 2: How does zsh differ from...?, Z-Shell Frequently-Asked Questions
I tell myself it should be noted that zsh has lots of ksh like features and emulation options when I write my own zsh scripts. You could experiment with setopt ksh_arrays to see what is going on, too.
Arrays are (by default) more csh-like than ksh-like: subscripts start at 1, not 0; array[0] refers to array[1]; $array refers to the whole array, not $array[0]; braces are unnecessary: $a[1] == ${a[1]}, etc. Set the KSH_ARRAYS option for compatibility.
--- 2.1: Differences from sh and ksh, Z-Shell Frequently-Asked Questions
It could give some hints/answers by showing some comparisons with ksh.
brackets vs. no brackets
Q22. Why are the braces required with array references, e. g. ${x[1]}?
A22. It would be nice to do $x[1], but the POSIX shell would expand $x and then search for the file pattern resulting by concatenating [1]. ksh is POSIX compatible.
--- III SHELL PROGRAMMING QUESTIONS Q22, KSH-93 - Frequently Asked Questions
ksh treats array parameters like this:
list=(a b c)
echo $list[1]
;# => "a[1]"
;# concatination of the first $list element and "[1]" string
;# rather than below!
echo ${list[1]} ;# => "b"
So, it could be used any of $list, ${list}, $list[#], ${list[#]}, $list[*], and ${list[*]} in the first example; it could be considered as feature of zsh.
You could look at the codes from another angle by reading above ksh document's $x[1] to $list[#] or $list[*].
Note: In zsh, if $list contains empty value(s), "${list[#]}" differs according to "24. Empty argument removal".
24. Empty argument removal
If the substitution does not appear in double quotes, any resulting zero-length argument, whether from a scalar or an element of an array, is elided from the list of arguments inserted into the command line.
--- 24. Empty argument removal, Rules, Expansion, zshparam(1)
# vs. *
Q1. What is the difference between * and #, for example, and ?
A1. When used outside of "", they are equivalent. However, within double quotes, "$#" produces one argument for each positional parameter, and "$*" produces a single argument. Note that "$#" preserves arguments lists, whereas $* may not unless both word splitting and pathname expansion are disabled.
--- III SHELL PROGRAMMING QUESTIONS Q1, KSH-93 - Frequently Asked Questions
This first half is same as zsh as you know. Here is a same reference for zsh document you suggested:
A subscript of the form [*] or [#] evaluates to all elements of an array; there is no difference between the two except when they appear within double quotes.
"$foo[*]" evaluates to "$foo[1] $foo[2] ...", whereas "$foo[#]" evaluates to "$foo[1]" "$foo[2]" ....
...
When an array parameter is referenced as $name (with no subscript) it evaluates to $name[*],
--- Array Subscripts, zshparam(1)
"$list" vs. others
As you can see it would be already clear, zsh gives us 4 different options as its feature. But I think that ksh user could say that:
"$list", "${list}" and "$list[*]" could mean that it would be doing just some operation on the first element of $list (and the result of concatinating "[*]" for the latter) rather than list/array references.
Here is an example code:
list=(1 2 '' 3) # XXX: added an empty entry to check the difference
test-list-dq () {
echo "$1"
local i=
echo '$list:'; for i in $list; do echo $i; done;
echo '$list[#]:'; for i in $list[#]; do echo $i; done;
echo '$list[*]:'; for i in $list[*]; do echo $i; done;
echo '${list}:'; for i in ${list}; do echo $i; done;
echo '${list[#]}:'; for i in ${list[#]}; do echo $i; done;
echo '${list[*]}:'; for i in ${list[*]}; do echo $i; done;
echo '"${list[#]}":'; for i in "${list[#]}"; do echo $i; done;
}
test-list-nq () {
echo "$1"
local i=
for i in "$list"; do echo $i; done
for i in "${list}"; do echo $i; done
for i in "$list[*]"; do echo $i; done
for i in "${list[*]}"; do echo $i; done
}
echo "*double quotes"
test-list-dq "*default"
() {
setopt localoptions ksharrays no_nomatch
test-list-dq "*ksharrays on"
}
echo "*no quotes"
test-list-nq "*default"
() {
setopt localoptions ksharrays no_nomatch
test-list-nq "*ksharrays on"
}
Outputs below:
*double quotes
*default
$list:
1
2
3
$list[#]:
1
2
3
$list[*]:
1
2
3
${list}:
1
2
3
${list[#]}:
1
2
3
${list[*]}:
1
2
3
"${list[#]}":
1
2
3
*ksharrays on
$list:
1
$list[#]:
1[#]
$list[*]:
1[*]
${list}:
1
${list[#]}:
1
2
3
${list[*]}:
1
2
3
"${list[#]}":
1
2
3
*no quotes
*default
1 2 3
1 2 3
1 2 3
1 2 3
*ksharrays on
1
1
1[*]
1 2 3
Try the same with
list=('a b' $'c\nd')
Also, set $IFS to e.g. '|' to see how ${list[*]} works.
list=('a b' $'c\nd')
IFS='|'
printf %s "${list[*]}"

Zsh prompt showing last error code only once

I would like my prompt to show a cross (✘) when the previous command fails. I use the following code:
export PROMPT=$'%(?..✘\n)\n› '
This gives me the following output:
› echo Hello
Hello
› asjdfiasdf
zsh: command not found: asjdfiasdf
✘
›
✘
I would like to modify the prompt so that it does not repeat the cross when the prompt is redrawn after Enter (the third case in the example above).
Is it possible?
I think I got it. Let me know if you find a bug...
preexec() {
preexec_called=1
}
precmd() {
if [ "$?" != 0 ] && [ "$preexec_called" = 1 ]
then echo ✘; unset preexec_called; fi
}
PROMPT=$'\n› '
Result:
› ofaoisfsaoifoisafas
zsh: command not found: ofaoisfsaoifoisafas
✘
›
› echo $? # (not overwritten)
127
I do this in my zsh, though with colors rather than unicode characters. It's the same principle.
First, I set up my colors, ensuring that they are only used when they are supported:
case $TERM in
( rxvt* | vt100* | xterm* | linux | dtterm* | screen )
function PSC() { echo -n "%{\e[${*}m%}"; } # insert color-specifying chars
ERR="%(0?,`PSC '0;32'`,`PSC '1;31'`)" # if last cmd!=err, hash=green, else red
;;
( * )
function PSC() { true; } # no color support? no problem!
ERR=
;;
esac
Next, I set up a magic enter function (thanks to this post about an empty command (ignore the question, see how I adapt it here):
function magic-enter() { # from https://superuser.com/a/625663
if [[ -n $BUFFER ]]
then unset Z_EMPTY_CMD # Enter was pressed on an empty line
else Z_EMPTY_CMD=1 # The line was NOT empty when Enter was pressed
fi
zle accept-line # still perform the standard binding for Enter
}
zle -N magic-enter # define magic-enter as a widget
bindkey "^M" magic-enter # Backup: use ^J
Now it's time to interpret capture the command and use its return code to set the prompt color:
setopt prompt_subst # allow variable substitution
function preexec() { # just after cmd has been read, right before execution
Z_LAST_CMD="$1" # since $_ is unreliable in the prompt
#Z_LAST_CMD="${1[(wr)^(*=*|sudo|-*)]}" # avoid sudo prefix & options
Z_LAST_CMD_START="$(print -Pn '%D{%s.%.}')"
Z_LAST_CMD_START="${Z_LAST_CMD_START%.}" # zsh <= 5.1.1 makes %. a literal dot
Z_LAST_CMD_START="${Z_LAST_CMD_START%[%]}" # zsh <= 4.3.11 makes %. literal
}
function precmd() { # just before the prompt is rendered
local Z_LAST_RETVAL=$? # $? only works on the first line here
Z_PROMPT_EPOCH="$(print -Pn '%D{%s.%.}')" # nanoseconds, like date +%s.%N
Z_PROMPT_EPOCH="${Z_PROMPT_EPOCH%.}" # zsh <= 5.1.1 makes %. a literal dot
Z_PROMPT_EPOCH="${Z_PROMPT_EPOCH%[%]}" # zsh <= 4.3.11 makes %. a literal %.
if [ -n "$Z_LAST_CMD_START" ]; then
Z_LAST_CMD_ELAPSED="$(( $Z_PROMPT_EPOCH - $Z_LAST_CMD_START ))"
Z_LAST_CMD_ELAPSED="$(printf %.3f "$Z_LAST_CMD_ELAPSED")s"
else
Z_LAST_CMD_ELAPSED="unknown time"
fi
# full line for error if we JUST got one (not after hitting <enter>)
if [ -z "$Z_EMPTY_CMD" ] && [ $Z_LAST_RETVAL != 0 ]; then
N=$'\n' # set $N to a literal line break
LERR="$N$(PSC '1;0')[$(PSC '1;31')%D{%Y/%m/%d %T}$(PSC '1;0')]"
LERR="$LERR$(PSC '0;0') code $(PSC '1;31')$Z_LAST_RETVAL"
LERR="$LERR$(PSC '0;0') returned by last command"
LERR="$LERR (run in \$Z_LAST_CMD_ELAPSED):$N"
LERR="$LERR$(PSC '1;31')\$Z_LAST_CMD$(PSC '0;0')$N$N"
print -PR "$LERR"
fi
}
Finally, set the prompt:
PROMPT="$(PSC '0;33')[$(PSC '0;32')%n#%m$(PSC '0;33') %~$PR]$ERR%#$(PSC '0;0') "
Here's how it looks:
A more direct answer to the question, adapted from the above:
function magic-enter() { # from https://superuser.com/a/625663
if [[ -n $BUFFER ]]
then unset Z_EMPTY_CMD # Enter was pressed on an empty line
else Z_EMPTY_CMD=1 # The line was NOT empty when Enter was pressed
fi
zle accept-line # still perform the standard binding for Enter
}
zle -N magic-enter # define magic-enter as a widget
bindkey "^M" magic-enter # Backup: use ^J
function precmd() { # just before the prompt is rendered
local Z_LAST_RETVAL=$? # $? only works on the first line here
# full line for error if we JUST got one (not after hitting <enter>)
if [ -z "$Z_EMPTY_CMD" ] && [ $Z_LAST_RETVAL != 0 ]; then
echo '✘'
fi
}
PROMPT=$'\n› '
With screen shot:
Use the prexec and precmd hooks:
The preexec hook is called before any command executes. It isn't called when no command is executed. For example, if you press enter at an empty prompt, or a prompt that is only whitespace, it won't be called. A call to this hook signals that a command has been run.
The precmd hook is called before the prompt will be displayed to collect the next command. Before printing the prompt you can print out the exit status. In here we can check if a command was just executed, and if there's a status code we want to display.
This is very similar to the solution suggested by #sneep, which is also a great solution. It's worth using the hooks though so that if you've got anything else registering for these hooks they can do so too.
# print exit code once after last command output
function track-exec-command() {
zsh_exec_command=1
}
function print-exit-code() {
local -i code=$?
(( code == 0 )) && return
(( zsh_exec_command != 1 )) && return
unset zsh_exec_command
print -rC1 -- ''${(%):-"%F{160}✘ exit status $code%f"}''
}
autoload -Uz add-zsh-hook
add-zsh-hook preexec track-exec-command
add-zsh-hook precmd print-exit-code
Thanks to everyone for their answers. Four years later, I would like to illustrate a variation on sneep's answer for those looking for the error code and an alert without a symbol. This is a minimalist prompt but when an error occurs it displays the error code and > in red following the top level directory.
preexec() {
preexec_called=1
}
precmd() {
if [ "$?" != 0 ] && [ "$preexec_called" = 1 ]; then
unset preexec_called
PROMPT='%B%F{blue}%1~%f%b%F{red} $? > %F{black}'
else
PROMPT='%B%F{blue}%1~%f%b%F{blue} > %F{black}'
fi
}

Resources