Correct usage of ~ parameter expansion flag? - zsh

According to man zshexpn (5.0.2):
~ Force string arguments to any of the flags below that follow within the parentheses to be treated as
patterns.
For example, using the s flag to perform field splitting requires a string argument:
% print -l ${(s:9:):-"foo893bar923baz"}
foo8
3bar
23baz
My reading of the ~ flag suggests that I should be able to specify a pattern in place of a literal string to split on, so that the following
% print -l ${(~s:<->:):-"foo893bar923baz"}
should produce
foo
bar
baz
Instead, it behaves the same as if I omit the ~, performing no splitting at all.
% print -l ${(s:<->:):-"foo893bar923baz"}
foo893bar923baz
% print -l ${(~s:<->:):-"foo893bar923baz"}
foo893bar923baz

Ok, rereading the question, it's the difference between this:
$ val="foo???bar???baz"
$ print -l ${(s.?.)val}
foo
bar
baz
And this:
$ val="foo???bar???baz"
$ print -l ${(~s.?.)val}
foo???bar???baz
It operates on the variable, i.e. the "argument" to the split (from your documentation quote). In the first example, we substitute literal ?, and in the second, we treat the variable as a glob, and there are no literal ?, so nothing gets substituted.
Still, though, split works on characters and not globs in the substution itself, from the documentation:
s:string:
Force field splitting (see the option SH_WORD_SPLIT) at the separator string.
So, it doesn't look like you can split on a pattern. The ~ character modifies the interpretation of the string to be split.
Also, from the same pattern expansion documentation you reference, it continutes:
Compare with a ~ outside parentheses, which forces the entire
substituted string to be treated as a pattern. [[ "?" = ${(~j.|.)array} ]]
with the EXTENDED_GLOB option set succeeds if and
only if $array contains the string ‘?’ as an element. The argument may
be repeated to toggle the behaviour; its effect only lasts to the end
of the parenthesised group.
The difference between ${(~j.|.)array} and ${(j.|.)~array} is that the former treats the values inarray as global, and the latter treats the result as a glob.
See also:
${~spec} Turn on the GLOB_SUBST option for the evaluation of spec; if
the ‘~’ is doubled, turn it off. When this option is set, the string
resulting from the expansion will be interpreted as a pattern anywhere
that is possible, such as in filename expansion and filename
generation and pattern-matching contexts like the right hand side of
the ‘=’ and ‘!=’ operators in conditions.
Here is a demo that shows the differences:
$ array=("foo???bar???baz" "foo???bar???buz")
$ [[ "foo___bar___baz" = ${(~j.|.)array} ]] && echo true || echo false
false
$ [[ "foo___bar___baz" = ${(j.|.)~array} ]] && echo true || echo false
true
And for completeness:
$ [[ "foo___bar___baz" = ${(~j.|.)~array} ]] && echo true || echo false
true

Related

How to initialize array from array in zsh?

My script test.zsh:
args=$#
argss=($#)
echo ${#:2}
echo ${args:2}
echo ${argss:2}
The output:
$ ./test.zsh foo bar foobar
bar foobar
o bar foobar
o
It looks like args is being initialized as the string of $# instead of as an array. How do I initialize args as an array? ($#) does not seem to work either
You need to put parentheses around $# to make args an array:
args=($#)
In other shells, you should also put quotes around it (args=("$#")) to avoid word splitting, but this is disabled by default in zsh (see the option SH_WORD_SPLIT).
Note that ${#:2} will give you $1 $2 $3 ..., while ${args:2} will give $2 $3 ..., because zsh prepends $0 to $# when you use that form of parameter subscripting for compatibility with other shells.
The preferred zsh way to subscript arrays is ${arr[start,end]}, where end is inclusive and may be negative.
${args[1,-1]} and ${args[#]} will expand to the same thing.

KSH script checks for alphanumeric in case statement

Below is a simplified model of what I'm trying to achieve:
#!bin/ksh
string=AUS00
case $string in
[[:alnum:]] ) echo "alphanumeric" ;;
*) echo "nope" ;;
esac
I'm unable to validate alphanumeric code.
Constraints:
The validation need to happen inside the case statement
alnum function is not supported
Positive check only. Can't check for the absence of alphanumeric.
Thank you very much
The pattern [[:alnum:]] will match a single alphanumeric character. Your string is longer than one character, so it won't match.
If you want to check that your string contains an alnum character, you want *[[:alnum:]]*
If you want to check that your string only contains alnum characters, I'd flip the check to see if the string contains a non-alnum character:
for string in alnumOnly 'not all alnum'; do
case "$string" in
*[^[:alnum:]]*) echo "$string -> nope" ;;
*) echo "$string -> alphanumeric" ;;
esac
done
alnumOnly -> alphanumeric
not all alnum -> nope
I realized that ksh (even ksh88) implements what bash describes as "extended patterns":
A pattern-list is a list of one or more patterns separated
from each other with a |. Composite patterns can be formed
with one or more of the following:
?(pattern-list)
Optionally matches any one of the given patterns.
*(pattern-list)
Matches zero or more occurrences of the given
patterns.
+(pattern-list)
Matches one or more occurrences of the given patterns.
#(pattern-list)
Matches exactly one of the given patterns.
!(pattern-list)
Matches anything, except one of the given patterns.
So we can do:
case "$string" in
+([[:alnum:]]) ) echo "$string -> alphanumeric" ;;
* ) echo "string -> nope" ;;
esac

in zsh, how do you test if an associative array (aka hash table) has a certain property?

I want to write a function that returns 0 if the property exists and 1 if it does not.
for example:
typeset -A hashtable
hashtable[a]='this is a valid element'
testprop hashtable[a] # returns 0
testprop hashtable[b] # returns 1
is that possible?
The parameter expansion ${+name} almost does, what you want. If name is a set parameter 1 is substituted, otherwise 0 is substitued.
To get the desired interface, this can be wrapped into a function:
function testprop {
case ${(P)+${1}} in
0) return 1;;
1) return 0;;
esac
}
alias testprop='noglob testprop'
Explanation:
The parameter expansion flag P tells zsh to interprete the value ${1} as further parameter name.
${+name} is substituted by 1 if name is set, 0 otherwise.
The alias is necessary so that the parameter to testprob does not need to be quoted. Otherwise the square brackets around the index would be interpreted as globbing operator.

Duplicating an array 1:1 in zsh

Although it seems fairly simple, there is a small nuance to the obvious solution.
The following code would cover most situations:
arr_1=(1 2 3 4)
arr_2=($arr_1)
However, empty strings do not copy over. The following code:
arr_1=('' '' 3 4)
arr_2=($arr_1)
print -l \
"Array 1 size: $#arr_1" \
"Array 2 size: $#arr_2"
will yield:
Array 1 size: 4
Array 2 size: 2
How would I go about getting a true copy of an array?
It would be an "Array Subscripts" issue, so you could properly specify an array subscript form to select all elements of an array ($arr_1 for instance) within double quotes:
arr_1=('' '' 3 4)
arr_2=("${arr_1[#]}")
#=> arr_2=("" "" "3" "4")
Each elements of $arr_1 would be properly surrounded with double quotes even if it is empty.
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)
And empty elements of arrays will be removed according to "Empty argument removal", so
arr_2=($arr_1)
#=> arr_2=(${arr_1[*]})
#=> arr_2=(3 4)
Above behavior is not good in this case.
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.
-- Empty argument removal, Rules Expansion zshexpn(1)

side effect of zsh echo?

For some reason this script will work with all of the 'echo's at the end, but without them $wall is an empty string. This seems like really odd behaviour.
#!/bin/zsh
if [ ! -n "$1" ] ; then
files=(~/pictures/backgrounds/*jpg)
else
while [ $1 ] ; do
files+=(`echo $1/*jpg`)
shift
done
fi
echo $files
N=${#files}
echo $N
((N=RANDOM%N))
echo $N
wall=${files[$N]}
echo $wall
cp $wall ~/wall.jpg
This code will sometimes fail because RANDOM%N can result in zero and zsh array indexes start with 1. You should use RANDOM%N+1 instead.
You can:
setopt ksharrays
to enable zero-based indexing.
From man zshoptions:
Emulate ksh array handling as closely as possible. If this
option is set, array elements are numbered from zero, an array
parameter without subscript refers to the first element instead
of the whole array, and braces are required to delimit a sub‐
script (${path[2]}' rather than just$path[2]').

Resources