19

Is there a portable unix shellscripting way of joining a number of strings together with a given separator, like so:

$ strjoin --- foo bar baz quux
foo---bar---baz---quux

Sure I could use a $scripting_language one liner or an ugly explicit loop in a shellscript function, but the unix hackers of old probably had some need for this as well, so someone has made a standard command like this that I don't know about somewhere in the past, right?

edit

The sed method is certainly the easiest one in many situations, but it doesn't work if the strings can contain spaces. And many of the other answers also don't handle that. Are there any solutions other than the $IFS trick that handle spaces (and all possible characters in general) and do not require writing a full loop?

JanKanis
  • 513
  • 1
  • 5
  • 11

8 Answers8

18

For multi-character long separator, you can use:

  • sed (as already pointed by @Mark)

      $ echo foo bar baz quux | sed "s/ /---/g"
    
  • ex

      $ echo foo bar baz quux | ex +"s/ /---/gp" -cq! /dev/stdin
      $ ex +"s/ /---/gp" -scq! <(echo foo bar baz quux)
    
  • printf (but it will show the extra ending separator)

      $ printf "%s---" foo bar baz quux
    

For one-character long separators, you can:

  • use the following shell function (as per this SO post):

      join_by { local IFS="$1"; shift; echo "$*"; }
    

    Usage:

      $ join_by '-' foo bar baz quux
    
  • use tr

      echo foo bar baz quux | tr ' ' '-'
    
kenorb
  • 6,499
  • 2
  • 46
  • 54
  • 5
    The IFS solution doesn't actually work for a multiple character delimiter, just takes the first character as delimiter and ignores the rest: `join_by '---' foo bar baz quux` → `foo-bar-baz-quuz` – Mu Mind Oct 19 '18 at 08:59
  • Also argument lists are separated by blank/non-blank transitions. These don't account for multiple whitespace between arguments, nor quoted entities. – symcbean Jun 30 '23 at 14:48
1

Perl is not that complex for simple operations:

$ perl -e 's/ /---/g'
Paul
  • 152
  • 2
  • 5
    Considering OP wants to join parameters, that'd be `perl -E 'say join(shift, @ARGV)' -- delim str1 str2 str3 ...` The perl one-liner you've posted doesn't actually do anything (well, it changes $_, but that isn't passed into or out of the one liner). You probably wanted to pass `-p` as well. – derobert Sep 15 '11 at 20:09
1

lam

Here is the example using lam command:

$ SEP="---"; lam <(echo foo) -s$SEP <(echo bar) -s$SEP <(echo baz) -s$SEP <(echo quux)
foo---bar---baz---quux

paste

If the separator is one character long, then paste command can be used:

$ printf "%s\n" foo bar baz quux | paste -sd-
foo-bar-baz-quux
kenorb
  • 6,499
  • 2
  • 46
  • 54
  • 2
    This seems to work with GNU `paste` on Linux; on macOS, either install GNU tools (e.g. using `brew ls coreutils`) and use as `gpaste` or work with BSD `paste` and append an additional `-` to explicitly specify standard input as input file: `printf "%s\n" foo bar baz quux | paste -sd- -` – ssc Oct 24 '20 at 12:22
1

The best method I've found is the ugly explicit loop you mentioned.

join(){
    # If no arguments, do nothing.
    # This avoids confusing errors in some shells.
    if [ $# -eq 0 ]; then
        return
    fi

    local joiner="$1"
    shift

    while [ $# -gt 1 ]; do
        printf "%s%s" "$1" "$joiner"
        shift
    done

    printf '%s\n' "$1"
}

Usage:

$ join --- foo bar baz quux
foo---bar---baz---quux

Tested with Bash, Dash, and Zsh on Ubuntu, and should work in other Bourne-based shells.

wjandrea
  • 135
  • 7
0
python -c 'import sys; print "__".join(sys.argv[1:])' a b c
    
function join_by() {
    local L_IFS=$1
    shift
    python -c "import sys; print(\"$L_IFS\".join(sys.argv[1:]))" "$@"
}
Andrew Schulman
  • 8,811
  • 21
  • 32
  • 47
0

Not sure how portable this is, but if:
(1) the strings are in an array, and
(2) the array has at least two elements,
then I would output the first string, and concatenate it with a sequence of the remaining strings prefixed by the separator; the latter can be produced with the 'printf' command.
This is what I came up with:

SEP='---'
STRINGS=( 'foo' 'bar' 'baz' 'quux' )
echo "${STRINGS[0]}$(printf -- "${SEP//%/%%}"'%s' "${STRINGS[@]:1}")"

It works at least in bash, and I think it covers all cases (assuming that the 'STRINGS' array has at least two elements), including a separator string that begins with a hyphen, or a separator string that contains one or more percentage signs.

luvr
  • 71
  • 1
  • 2
0

In addition to @embobo's comment (which will hopefully make it into an answer soon), perl can be used to split and join arbitrary strings. This is more complex than using sed and based on the example above would be major overkill.

voretaq7
  • 79,879
  • 17
  • 130
  • 214
0

awk version:

function join(a, start, end, sep, result, i) {
    sep = sep ? sep : " "
    start = start ? start : 1
    end = end ? end : sizeof(a)
    if (sep == SUBSEP) # magic value
       sep = ""
    result = a[start]
    for (i = start + 1; i <= end; i++)
        result = result sep a[i]
    return result
}

Call it with gawk with --source is your strings:

$ gawk -f join.awk --source 'BEGIN { split("foo bar quux",a); print join(a,1,3,"---") }'
foo---bar---quux

Shell script version:

function join() {
    for i in "$@"; do
        echo -n "$i""---"
    done
    echo
}

join foo bar baz quux 

Call it and trim the last separator:

$ ./join.sh | sed 's/\-\-\-$//'
foo---bar---baz---quux
quanta
  • 51,413
  • 19
  • 159
  • 217