I have a variable named descr which can contain a string Blah: -> r1-ae0-2 / [123], -> s7-Gi0-0-1:1-US / Foo, etc. I want to get the -> r1-ae0-2, -> s7-Gi0-0-1:1-US part from the string. At the moment I use descr=$(grep -oP '\->\s*\S+' <<< "$descr" for this. Is there a better way to do this? Is it also possible to do this with parameter expansion?
- 807,993
- 194
- 1,674
- 2,175
- 7,284
- 40
- 125
- 208
3 Answers
ksh93 and zsh have back-reference (or more accurately1, references to capture groups in the replacement) support inside ${var/pattern/replacement}, not bash.
ksh93:
$ var='Blah: -> r1-ae0-2 / [123]'
$ printf '%s\n' "${var/*@(->*([[:space:]])+([^[:space:]]))*/\1}"
-> r1-ae0-2
zsh:
$ var='Blah: -> r1-ae0-2 / [123]'
$ set -o extendedglob
$ printf '%s\n' "${var/(#b)*(->[[:space:]]#[^[:space:]]##)*/$match[1]}"
-> r1-ae0-2
(mksh man page also mentions that future versions will support it with ${KSH_MATCH[1]} for the first capture group. Not available yet as of 2017-04-25).
However, with bash, you can do:
$ [[ $var =~ -\>[[:space:]]*[^[:space:]]+ ]] &&
printf '%s\n' "${BASH_REMATCH[0]}"
-> r1-ae0-2
Which is better as it checks that the pattern is found first.
If your system's regexps support \s/\S, you can also do:
re='->\s*\S+'
[[ $var =~ $re ]]
With zsh, you can get the full power of PCREs with:
$ set -o rematchpcre
$ [[ $var =~ '->\s*\S+' ]] && printf '%s\n' $MATCH
-> r1-ae0-2
With zsh -o extendedglob, see also:
$ printf '%s\n' ${(SM)var##-\>[[:space:]]#[^[:space:]]##}
-> r1-ae0-2
Portably:
$ expr " $var" : '.*\(->[[:space:]]*[^[:space:]]\{1,\}\)'
-> r1-ae0-2
If there are several occurrences of the pattern in the string, the behaviour will vary with all those solutions. However none of them will give you a newline separated list of all matches like in your GNU-grep-based solution.
To do that, you'd need to do the looping by hand. For instance, with bash:
re='(->\s*\S+)(.*)'
while [[ $var =~ $re ]]; do
printf '%s\n' "${BASH_REMATCH[1]}"
var=${BASH_REMATCH[2]}
done
With zsh, you could resort to this kind of trick to store all the matches in an array:
set -o extendedglob
matches=() n=0
: ${var//(#m)->[[:space:]]#[^[:space:]]##/${matches[++n]::=$MATCH}}
printf '%s\n' $matches
1 back-references does more commonly designate a pattern that references what was matched by an earlier group. For instance, the \(.\)\1 basic regular expression matches a single character followed by that same character (it matches on aa, not on ab). That \1 is a back-reference to that \(.\) capture group in the same pattern.
ksh93 does support back-references in its patterns (for instance ls -d -- @(?)\1 will list the file names that consist of two identical characters), not other shells. Standard BREs and PCREs support back-references but not standard ERE, though some ERE implementations support it as an extension. bash's [[ foo =~ re ]] uses EREs.
[[ aa =~ (.)\1 ]]
will not match, but
re='(.)\1'; [[ aa =~ $re ]]
may if the system's EREs support it.
- 522,931
- 91
- 1,010
- 1,501
You want to delete everything up to the first ␣->␣ (not including the "arrow") and after the last ␣/ (including the space and slash).
string="Blah: -> r1-ae0-2 / [123]"
string=${string/*->/->}
string=${string/ \/*}
$string will now be -> r1-ae0-2.
The same two substitutions would turn -> s7-Gi0-0-1:1-US / Foo into -> s7-Gi0-0-1:1-US.
- 320,670
- 36
- 633
- 936
Answering this definitively is impossible without knowing the exact format every message takes. However, as a general approach you can print certain specific fields using cut:
$ cut -d ' ' -f 2 <<< '-> s7-Gi0-0-1:1-US / Foo'
s7-Gi0-0-1:1-US
Or you can print every nth column using awk:
$ awk -F' ' '{ for (i=2;i<=NF;i+=4) print $i }' <<< '-> r1-ae0-2 / [123], -> s7-Gi0-0-1:1-US / Foo'
r1-ae0-2
s7-Gi0-0-1:1-US