zsh ${var##$pat} parameter expansion with pattern var containing globs - zsh

It seems zsh doesn't honor globs inside variable patterns, in ${var##$pat} parameter expansions:
$ zsh -c 'pat=/*; var=/etc/; echo "$var $pat"; echo "${var##$pat}"'
/etc/ /*
/etc/
# sh result: empty
However, if $pat does not contain *, zsh and sh behave similarly:
$ zsh -c 'pat=/; var=/etc/; echo "$var $pat"; echo "${var##$pat}"'
/etc/ /
etc/
# sh result: same
zsh --emulate sh gives, of course, sh-compatible results. But if I want to stay in zsh emulation, is there any setopt option that changes this behavior? I've looked (briefly) in the docs and I can't really find the reason for this difference.

In zsh, variable contents will only be treated as a pattern if you
ask for that, with a ${~spec} expansion or the (very broad and therefore slightly dangerous) GLOB_SUBST option:
pat=/*t
var=/etc/
print "${var##$pat}"
#=> /etc/
print "${var##$~pat}"
#=> c/
setopt glob_subst
print "${var##$pat}"
#=> c/
This is described in the zshexpn man page, in the section for string substitution expansion ${name/pattern/repl}.

Related

How to set command line args with the space delimited contents of the first command line argument in zsh

I will be getting one command line argument in the script I'm writing which will itself be a space delimited list of the actual command line arguments. I'd like to set the arguments of the current script with these arguments. How might I accomplish that?
I'd like to use set -- but I'm not sure how this would work.
E.g.
Given arguments to my script: -a -b -c
echo $1 # prints "-a -b -c"
You can do this with set -- "${(z)1}". This will split $1 into words, handling quoting the same way the shell itself does:
% cat script.zsh
#!/usr/bin/env zsh
set -- "${(z)1}"
for arg; do
echo "==$arg=="
done
% ./script.zsh "-a -b -c -d'has spaces'"
==-a==
==-b==
==-c==
==-d'has spaces'==
If you also want to remove a level of quotes, use "${(#Q)${(z)1}}" instead.

zsh mystery variable expansion

I found this in prezto source code:
# Set the command name, or in the case of sudo or ssh, the next command.
local cmd="${${2[(wr)^(*=*|sudo|ssh|-*)]}:t}"
I've been reading zsh doc a lot but getting nowhere close to what this is about. In experimentation on the shell itself it seems to indicate the [] is some arithmetic thing, which makes sense, but I don't see the part that explains how (w) is supposed to work. It seems to be some magical operator that applies to the math expression...
slu#ubuntu-sluvm ~/.zprezto ❯❯❯ VAR="one two three four"
slu#ubuntu-sluvm ~/.zprezto ❯❯❯ echo ${VAR[2]}
n
slu#ubuntu-sluvm ~/.zprezto ❯❯❯ echo ${VAR[(w)2]}
two
slu#ubuntu-sluvm ~/.zprezto ❯❯❯ echo ${VAR[(w)]}
zsh: bad math expression: empty string
slu#ubuntu-sluvm ~/.zprezto ❯❯❯
It looks pretty messy at first glance, but once you break it into its parts it's quite simple. This is an example of parameter expansion and extended globbing support in ZSH. If you look higher up in the function from which this code sample is, you'll see they set:
emulate -L zsh
setopt EXTENDED_GLOB
So now let's break apart the line you have there:
${
${
2[ # Expand the 2nd argument
(wr) # Match a word
^(*=*|=|sudo|ssh|-*) # Do not match *=*, =, sudo, ssh, or -*
]
}
:t} # If it is a path, return only the filename
You can test this by creating a sample script like this:
#!/bin/zsh
emulate -L zsh
setopt EXTENDED_GLOB
echo "${$1[(wr)^(*=*|sudo|ssh|-*)]}:t}" # changed 2 to 1, otherwise identical
Here's what it outputs:
$ ./test.sh '/bin/zsh'
zsh
$ ./test.sh 'sudo test'
test
$ ./test.sh 'sudo --flag test'
test
$ ./test.sh 'ssh -o=value test'
test
$ ./test.sh 'test'
test
For more information, see the documentation on expansion and csh-style modifiers.

Combining file tests in Zsh

What is the most elegant way in zsh to test, whether a file is either a readable regular file?
I understand that I can do something like
if [[ -r "$name" && -f "$name" ]]
...
But it requires repeating "$name" twice. I know that we can't combine conditions (-rf $name), but maybe some other feature in zsh could be used?
By the way, I considered also something like
if ls ${name}(R.) >/dev/null 2>&1
...
But in this case, the shell would complain "no matches found", when $name does not fulfil the criterium. Setting NULL_GLOB wouldn't help here either, because it would just replace the pattern with an empty string, and the expression would always be true.
In very new versions of zsh (works for 5.0.7, but not 5.0.5) you could do this
setopt EXTENDED_GLOB
if [[ -n $name(#qNR.) ]]
...
$name(#qNR.) matches files with name $name that are readable (R) and regular (.). N enables NULL_GLOB for this match. That is, if no files match the pattern it does not produce an error but is removed from the argument list. -n checks if the match is in fact non-empty. EXTENDED_GLOB is needed to enable the (#q...) type of extended globbing which in turn is needed because parenthesis usually have a different meaning inside conditional expressions ([[ ... ]]).
Still, while it is indeed possible to write something up that uses $name only once, I would advice against it. It is rather more convoluted than the original solution and thus harder to understand (i.e. needs thinking) for the next guy that reads it (your future self counts as "next guy" after at most half a year). And at least this solution will work only on zsh and there only on new versions, while the original would run unaltered on bash.
How about make small(?) shell functions as you mentioned?
tests-raw () {
setopt localoptions no_ksharrays
local then="$1"; shift
local f="${#[-1]}" t=
local -i ret=0
set -- "${#[1,-2]}"
for t in ${#[#]}; do
if test "$t" "$f"; then
ret=$?
"$then"
else
return $?
fi
done
return ret
}
and () tests-raw continue "${#[#]}";
or () tests-raw break "${#[#]}";
# examples
name=/dev/null
if and -r -c "$name"; then
echo 'Ok, it is a readable+character special file.'
fi
#>> Ok, it is...
and -r -f ~/.zshrc ; echo $? #>> 0
or -r -d ~/.zshrc ; echo $? #>> 0
and -r -d ~/.zshrc ; echo $? #>> 1
# It could be `and -rd ~/.zshrc` possible.
I feel this is somewhat overkill though.

zsh: access last command line argument given to a script

I want to get the last element of $*. The best I've found so far is:
last=`eval "echo \\\$$#"`
But that seems overly opaque.
In zsh, you can either use the P parameter expansion flag or treat # as an array containing the positional parameters:
last=${(P)#}
last=${#[$#]}
A way that works in all Bourne-style shells including zsh is
eval last=\$$#
(You were on the right track, but running echo just to get its output is pointless.)
last=${#[-1]}
should do the trick. More generally,
${#[n]}
will yield the *n*th parameter, while
${#[-n]}
will yield the *n*th to last parameter.
The colon parameter expansion is not in POSIX, but this works in at least zsh, bash, and ksh:
${#:$#}
When there are no arguments, ${#:$#} is treated as $0 in zsh and ksh but as empty in bash:
$ zsh -c 'echo ${#:$#}'
zsh
$ ksh -c 'echo ${#:$#}'
ksh
$ bash -c 'echo ${#:$#}'
$

Unable to get a system variable work for manuals

I have the following system variable in .zshrc
manuals='/usr/share/man/man<1-9>'
I run unsuccessfully
zgrep -c compinit $manuals/zsh*
I get
zsh: no matches found: /usr/share/man/man<1-9>/zsh*
The command should be the same as the following command which works
zgrep -c compinit /usr/share/man/man<1-9>/zsh*
How can you run the above command with a system variable in Zsh?
Try:
$> manuals=/usr/share/man/man<0-9>
$> zgrep -c compinit ${~manuals}/zsh*
The '~' tells zsh to perform expansion of the <0-9> when using the variable. The zsh reference card tells you how to do this and more.
From my investigations, it looks like zsh performs <> substitution before $ substitution. That means when you use the $ variant, it first tries <> substitution (nothing there) then $ substitution (which works), and you're left with the string containing the <> characters.
When you don't use $manuals, it first tries <> substitution and it works. It's a matter of order. The final version below shows how to defer expansion so they happen at the same time:
These can be seen here:
> manuals='/usr/share/man/man<1-9>'
> echo $manuals
/usr/share/man/man<1-9>
> echo /usr/share/man/man<1-9>
/usr/share/man/man1 /usr/share/man/man2 /usr/share/man/man3
/usr/share/man/man4 /usr/share/man/man5 /usr/share/man/man6
/usr/share/man/man7 /usr/share/man/man8
> echo $~manuals
/usr/share/man/man1 /usr/share/man/man2 /usr/share/man/man3
/usr/share/man/man4 /usr/share/man/man5 /usr/share/man/man6
/usr/share/man/man7 /usr/share/man/man8

Resources