The ksh-style (and now specified by POSIX for sh) ${var#pattern}
and ${var%pattern}
(and greedy variants with ##
and %%
) only remove text from the beginning and end respectively of the contents of the variable.
To get 200
out of 100:200:300:400
with those operators, you'd need to apply both ${VAR#*:}
to remove 100:
from the beginning and then ${VAR%%:*}
to remove :300:400
from the end.
That would be with ${${VAR#*:}%%:*}
, but while that would work in zsh, that doesn't work in bash or ksh which don't allow chaining parameter expansion operators.
In zsh
, you'd rather use $VAR[(ws[:])2]
to get the 2
nd:
s
eparated w
ord of $var
¹, or use ${${(s[:])VAR}[2]}
to first split the variable into an array and then get the second element.
In ksh93, you can do ${VAR/*:@(*):*:*/\1}
, but while bash (like zsh²) has copied ksh93's ${param/pattern/replacement}
operator, it hasn't copied the capture and reference part.
In bash (which is still widely used despite all its limitations as it's the GNU shell so pre-installed on virtually all GNU/Linux systems as well as a few non-GNU systems), you can do it in two steps:
tmp=${VAR#*:}; printf '%s\n' "${tmp%%:*}"
bash's only builtin splitting operator (unless you want to consider bash 4.4+'s readarray -d
which is more for reading records from some input stream into an array; and the kind of splitting it does by separating out arguments in its syntax) which is the Bourne-style (extended by Korn) IFS-splitting which is performed upon unquoted expansions (you used it by mistake in echo ${VAR%%:*}
where you forgot the quotes), and by read
.
read
works on one line, so can only be used for variable that don't contain newline characters. Using split+glob (glob being the other side effect of leaving an expansion unquoted) is cumbersome as we need to disable the glob part, and change a global $IFS
parameter.
In bash 4.4+, you can do it like:
nth() { local - # local - is to make the changes to option settings local to # the function like in the Almquist shell. The idea is that it # makes the $- special parameter local to the function. That's # equivalent to the set -o localoptions of zsh. In bash, that # only works for the set of option managed by set, not the ones # managed by shopt. local string="$1" n="$2" IFS="${3- }" set -o noglob # disable the glob part set -- $string'' # apply split+glob with glob disabled printf '%s\n' "${!n}" # dereference the parameter whose name is stored in $n } nth "$VAR" 2 :
Without the ''
, 100::300:
would be split into "100", "", "300" only, so for instance $#
would expand to 3 even though the variable has 4 :
-separated field. Beware though that it means an empty variable is split into 1 empty element instead of none.
For read
(or readarray
), a similar work around would be to add an extra delimiter at the end of the input. Since bash variables (contrary to zsh's) can't contain NUL characters anyway, with read
and arbitrary variable values, you could do:
words=() IFS=: LC_ALL=C read -rd '' -a words < <(printf '%s:\0' "$VAR") && printf '%s\n' "${word[2 - 1]}"
(- 1
because read
starts filling up the array at index 0, not 1; LC_ALL=C
works around some bugs for text not encoded in the user's encoding in bash versions 5.0 to 5.2)
With readarray
(bash 4.4):
records=() readarray -O1 -td : records < <(printf %s: "$VAR") && printf '%s\n' "${records[2]}"
Again, :
added at the end to prevent a trailing empty element being discarded (but again meaning an empty input results in one empty element).
¹ Though note that it splits ::a:b::c:::
into a
, b
and c
only like IFS-splitting did in the Bourne shell (but not in modern Bourne-like shells except when space, tab or newline are used as separators).
² zsh supports it but with a different syntax and that needs the extendedglob
option to be enabled: ${VAR/(#b)*:(*):*:*/$match[1]}
IFS=":" a=( $VAR ); echo "${a[1]}"
read
, thanks!unset IFS
may not be needed afterwards, but it is a precaution that one might want to take.