6

An ancient version of ipconfig (inside initramfs) requires its user input to supply only up to 7 colon separated elements, like:

ip=client-ip:server-ip:gw-ip:netmask:hostname:device:autoconf

result in an ipconfig error when users do supply more than 7 elements.

Therefore the extra (2 DNS resolvers) should be chopped off.

That can be done inside a subshell with cut, like:

validated_input=$(echo ${user_input} | cut -f1,2,3,4,5,6,7 -d:)

How can such cut be written using (b)ash parameter expansion/substitution?

Without:

  • launching subshell(s)/subprocess(es) (piping)
  • IFS-wrangling/mangling

Because of (1) speed, see Using bash variable substitution instead of cut/awk, and (2) learning.


In other words: How to do a lookup for n-th (7-th) character occurrence and remove/trim everything from there until the end of the string?

  • IFS mangling can be done locally if not mangling with the original ? – Inian Dec 25 '17 at 12:51
  • 2
    I know it does not solve your question per se, but you can save one process with this: validated_input=$(cut -f1-7 -d: <<< "$user_input"). – PesaThe Dec 25 '17 at 12:56
4

This uses only parameter expansion:

${var%:"${var#*:*:*:*:*:*:*:}"}

Example:

$ var=client-ip:server-ip:gw-ip:netmask:hostname:device:autoconf:morefields:another:youwantanother:haveanother:
$ echo "${var%:"${var#*:*:*:*:*:*:*:}"}"
client-ip:server-ip:gw-ip:netmask:hostname:device:autoconf

Thanks ilkkachu for coming up with a fix to the trailing :!


${parameter#word}
${parameter##word}

The word is expanded to produce a pattern just as in filename expansion (see Filename Expansion). If the pattern matches the beginning of the expanded value of parameter, then the result of the expansion is the expanded value of parameter with the shortest matching pattern (the ‘#’ case) or the longest matching pattern (the ‘##’ case) deleted. If parameter is ‘@’ or ‘’, the pattern removal operation is applied to each positional parameter in turn, and the expansion is the resultant list. If parameter is an array variable subscripted with ‘@’ or ‘’, the pattern removal operation is applied to each member of the array in turn, and the expansion is the resultant list.

This will attempt to match the beginning of your parameter, and if it does it will strip it.

Example:

$ var=a:b:c:d:e:f:g:h:i
$ echo "${var#a}"
:b:c:d:e:f:g:h:i
$ echo "${var#a:b:}"
c:d:e:f:g:h:i
$ echo "${var#*:*:}"
c:d:e:f:g:h:i
$ echo "${var##*:}"    # Two hashes make it greedy
i

${parameter%word}
${parameter%%word}

The word is expanded to produce a pattern just as in filename expansion. If the pattern matches a trailing portion of the expanded value of parameter, then the result of the expansion is the value of parameter with the shortest matching pattern (the ‘%’ case) or the longest matching pattern (the ‘%%’ case) deleted. If parameter is ‘@’ or ‘’, the pattern removal operation is applied to each positional parameter in turn, and the expansion is the resultant list. If parameter is an array variable subscripted with ‘@’ or ‘’, the pattern removal operation is applied to each member of the array in turn, and the expansion is the resultant list.

This will attempt to match the end of your parameter, and if it does it will strip it.

Example:

$ var=a:b:c:d:e:f:g:h:i
$ echo "${var%i}"
a:b:c:d:e:f:g:h:
$ echo "${var%:h:i}"
a:b:c:d:e:f:g
$ echo "${var%:*:*}"
a:b:c:d:e:f:g
$ echo "${var%%:*}"    # Two %s make it greedy
a

So in the answer:

${var%:"${var#*:*:*:*:*:*:*:}"}

(note the quotes around ${var#...} so that it is treated as a literal string (not a pattern) to be stripped off the end of $var).

When applied to:

var=client-ip:server-ip:gw-ip:netmask:hostname:device:autoconf:morefields:another:youwantanother:haveanother:

${var#*:*:*:*:*:*:*:} = morefields:another:youwantanother:haveanother:

That is expanded inside ${var%: ... } like so:

${var%:morefields:another:youwantanother:haveanother:}

So you are saying give me:

client-ip:server-ip:gw-ip:netmask:hostname:device:autoconf:morefields:another:youwantanother:haveanother:

But trim :morefields:another:youwantanother:haveanother: off the end.

The Bash Reference Manual (3.5.3)

  • 2
    echo "${var%:${var#*:*:*:*:*:*:*:}}", i.e. add the colon in an appropriate place in the first one? (expansions like that always make feel like something might break if the inner expansion produces a wildcard, but I think it works anyway.) – ilkkachu Dec 25 '17 at 16:24
  • @Jesse_b Shouldn't the "Possible solution 2" be a separate answer? It complicates answer #1 for a reason I can't find. The Bash reference manual manual doesn't help me either. It explains all constructs but doesn't help me to find a solution for my question. Partly because my thinking is limited to 1 step solutions, like cut there is 1 command to get the job done, here it requires 1. identifying the excess part var# and 2 remove it. After reading the manual I don't understand why var=1:2:3:4:5:6:;echo "${var%:${var#*:*:*:*:*:*:*:}}" output 1:2:3:4:5:6: doesn't remove the last colon... – Pro Backup Dec 26 '17 at 12:20
  • @Jesse_b I've got a hard time figuring out why it works. var=1:2:3:4:5:6:;echo ${var%:1:2:3:4:5:6:} doesn't trim the last colon: 1:2:3:4:5:6:. Where var=1:2:3:4:5:6:;echo "${var%:}" does trim the last colon 1:2:3:4:5:6. I am trying to explain in your answer. Undo or improve my future explain edit. – Pro Backup Dec 26 '17 at 13:42
  • @ProBackup Updated the answer. – Jesse_b Dec 26 '17 at 14:00
  • 1
    @PesaThe I just did and both my solution and his regex solution run in 0.000s consistently. His script runs between 0.007 and 0.012s. So honestly speed shouldn't be an issue with any of the solutions. – Jesse_b Dec 26 '17 at 18:51
5

With a regex:

$ var=a:b:c:d:e:f:g:h:i:j:k:l
$ [[ $var =~ ([^:]*:){6}([^:]*) ]] && echo "${BASH_REMATCH[0]}"
a:b:c:d:e:f:g

This should work in standard shell:

#!/bin/sh
var=a:b:c:d:e:f:g:h:i:j:k:l
while true; do 
    case "$var" in
        *:*:*:*:*:*:*:*) var=${var%:*} ;;
        *) break ;;
    esac
done
echo "$var"

Or if we do allow setting IFS for the duration of read:

$ IFS=: read -a arr <<< "$var"
$ arr=("${arr[@]:0:7}")
$ echo "${arr[@]}"
a b c d e f g
$ printf "%s:%s:%s:%s:%s:%s:%s\n" "${arr[0]}" "${arr[1]}" "${arr[2]}" "${arr[3]}" "${arr[4]}" "${arr[5]}"  "${arr[6]}" 
a:b:c:d:e:f:g
  • The regex outputs a blank line with bash 3.2; the case (2nd) works correctly with bash 3.2 – Pro Backup Dec 25 '17 at 13:55
  • @ProBackup, hmm, it works for me (GNU bash, version 3.2.57(1)-release). regex support was added in 3.0, according to wiki.bash-hackers.org/scripting/bashchanges – ilkkachu Dec 25 '17 at 13:59
  • I tested with GNU bash, version 3.2.53(1)-release. On GNU bash, version 4.4.12(1)-release the regex solution works as expected. – Pro Backup Dec 26 '17 at 12:25
0

In zsh:

$ ip=client-ip:server-ip:gw-ip:netmask:hostname:device:autoconf:x:y:z
$ echo ${(j(:))${"${(@s(:))ip}"[1,7]}}
client-ip:server-ip:gw-ip:netmask:hostname:device:autoconf
  • ${(s(:))var} split on :
  • "${(@)...}": make sure to preserve empty elements (like in "$@")
  • ${var[1,7]} elements 1 to 7
  • ${(j(:))var} join elements on :

Or you can do:

$ set -o extendedglob
$ echo ${(M)ip##([^:]#:)(#c0,6)[^:]#}
client-ip:server-ip:gw-ip:netmask:hostname:device:autoconf
  • ${var##pattern} like in ksh, strip the longest leading portion matching the pattern.
  • (M): expanded to the Matched portion instead of stripping it
  • x#: 0 or more xs (like regexp *)
  • (#c0,6) from 0 to 6 of the preceding atom (like ksh's {x,y}(...))
  • These question includes "in bash parameter expansion". The environment is initramfs ash (which I have now included in the question), so there is no zsh or ksh or even full bash available. – Pro Backup Jan 15 '18 at 19:50
  • @ProBackup, OK. The answer can still be useful to others. – Stéphane Chazelas Jan 15 '18 at 20:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.