11

My dash script takes a parameter in the form of hostname:port, i.e.:

myhost:1234

Whereas port is optional, i.e.:

myhost

I need to read the host and port into separate variables. In the first case, I can do:

HOST=${1%%:*}
PORT=${1##*:}

But that does not work in second case, when port was omitted; echo ${1##*:} simply returns hostname, instead of an empty string.

In Bash, I could do:

IFS=: read A B <<< asdf:111

But that does not work in dash.

Can I split string on : in dash, without invoking external programs (awk, tr, etc.)?

Peter Mortensen
  • 1,029
  • 1
  • 8
  • 10
Martin Vegter
  • 69
  • 66
  • 195
  • 326
  • 4
    Make sure to split on the last colon if you want to support IPv6, and don't split on colons inside square brackets – Ferrybig Jan 15 '18 at 08:50
  • @Ferrybig `%%` makes it greedy (as opposed to `%`), so it actually does this, at least in part; it would not work with `##`. – jpaugh Jan 15 '18 at 22:30

4 Answers4

18

Just do:

case $1 in
  (*:*) host=${1%:*} port=${1##*:};;
  (*)   host=$1      port=$default_port;;
esac

You may want to change the case $1 to case ${1##*[]]} to account for values of $1 like [::1] (an IPv6 address without port part).

To split, you can use the split+glob operator (leave a parameter expansion unquoted) as that's what it's for after all:

set -o noglob # disable glob part
IFS=:         # split on colon
set -- $1     # split+glob

host=$1 port=${2:-$default_port}

(though that won't allow hostnames that contain a colon (like for that IPv6 address above)).

That split+glob operator gets in the way and causes so much harm the rest of the time that it would seem only fair that it be used whenever it's needed (though, I'll agree it's very cumbersome to use especially considering that POSIX sh has no support for local scope, neither for variables ($IFS here) nor for options (noglob here) (though ash and derivatives like dash are some of the ones that do (together with AT&T implementations of ksh, zsh and bash 4.4 and above)).

Note that IFS=: read A B <<< "$1" has a few issues of its own:

  • you forgot the -r which means backslash will undergo some special processing.
  • it would split [::1]:443 into [ and :1]:443 instead of [ and the empty string (for which you'd need IFS=: read -r A B rest_ignored or [::1] and 443 (for which you can't use that approach)
  • it strips everything past the first occurrence of a newline character, so it can't be used with arbitrary strings (unless you use -d '' in zsh or bash and the data doesn't contain NUL characters, but then note that herestrings (or heredocs) do add an extra newline character!)
  • in zsh (where the syntax comes from) and bash, here strings are implemented using temporary files, so it's generally less efficient than using ${x#y} or split+glob operators.
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
6

Just remove the : in a separate statement; also, remove $host from the input to get the port:

host=${1%:*}
port=${1#"$host"}
port=${port#:}
choroba
  • 45,735
  • 7
  • 84
  • 110
3

Another thought:

host=${1%:*}
port=${1##*:}
[ "$port" = "$1" ] && port=''
glenn jackman
  • 84,176
  • 15
  • 116
  • 168
1

A here string is just a syntactic shortcut for a single-line here document.

$ set myhost:1234
$ IFS=: read A B <<EOF
> $1
> EOF
$ echo "$A"
myhost
$ echo "B"
1234
chepner
  • 7,341
  • 1
  • 26
  • 27