2019-02-15
Accumulating a separated list in the Bourne shell
One of the things that comes up over and over again when formatting
output is that you want to output a list of things with some separator
between them but you don't want this separator to appear at the
start or the end, or if there is only one item in the list. For
instance, suppose that you are formatting URL parameters in a tiny
little shell script and you may have one or more parameters. If you
have more than one parameter, you need to separate them with '&
';
if you have only one parameter, the web server may well be unhappy
if you stick an '&
' before or after it.
(Or not. Web servers are often very accepting of crazy things in URLs and URL parameters, but one shouldn't count on it. And it just looks irritating.)
The very brute force approach to this general problem in Bourne shells goes like this:
tot="" for i in "$@"; do .... v="var-thing=$i" if [ -z "$tot" ]; then tot="$v" else tot="$tot&$v" fi done
But this is five or six lines and involves some amount of repetition. It would be nice to do better, so when I had to deal with this recently I looked into the Dash manpage to see if it's possible to do better with shell substitutions or something else clever. With shell substitutions we can condense this a lot, but we can't get rid of all of the repetition:
tot="${tot:+$tot&}var-thing=$i"
It annoys me that tot
is repeated in this. However, this is
probably the best all-around option in normal Bourne shell.
Bash has arrays, but the manpage's documentation of them makes my head hurt and this results in Bash-specific scripts (or at least scripts specific to any shell with support for arrays). I'm also not sure if there's any simple way of doing a 'join' operation to generate the array elements together with a separator between them, which is the whole point of the exercise.
(But now I've read various web pages on Bash arrays so I feel like I know a little bit more about them. Also, on joining, see this Stackoverflow Q&A; it looks like there's no built-in support for it.)
In the process of writing this entry, I realized that there is an
option that exploits POSIX pattern substitution after generating our '$tot
' to
remove any unwanted prefix or suffix. Let me show you what I mean:
tot="" for i in "$@"; do ... tot="$tot&var-thing=$i" done # remove leading '&': tot="${tot#&}"
This feels a little bit unclean, since we're adding on a separator
that we don't want and then removing it later. Among other things,
that seems like it could invite accidents where at some point we
forget to remove that leading separator. As a result, I think that
the version using '${var:+word}
' substitution is the best option,
and it's what I'm going to stick with.