How to append lists

Today we'll see how to append lists. It's simple, really.

"Well, that's a solved problem, and straightforward with—"

Straightforward, no doubt. Here're three lists:

(1 2 3)
(4 (5) 6)
((7 8) 9)

By appending them, we get this:

(1 2 3 4 (5) 6 (7 8) 9)

"Right, you just—"

So the command we want to run looks like this:

append '(1 2 3)' '(4 (5) 6)' '((7 8) 9)'

"You forgot to wrap it in parentheses, and—"

Wrap in parentheses? Nah, it's faster like this.

"Faster? And you're gonna get 'invalid-read-syntax. Look at the quotes. The first—"

I know. The first thing is: how to reduce a pair? We see it right away:

"That's how it looks — but why are you reducing—"

So our accumulator would be something like this:

acc="${acc%\)}${list/#\(/ }"

"Wait wait wait — what's this noisy thing? Are you—"

Yeah, it looks a bit noisy. But now we just need a while loop and we're done!

"A while loop?"

You don't like it?
Well, I think you're right: we could use while (("$#">1)) and shift, but a for loop is a bit simpler.
So let's go with your idea.

"My idea?!"

There, done! It works:

append() { local acc="$1"
           for list in "${@:2}"
           do acc="${acc%\)}${list/#\(/ }"
           done; echo "$acc" ;}

append '(1 2 3)' '(4 (5) 6)' '((7 8) 9)'
(1 2 3 4 (5) 6 (7 8) 9)

"Is this—"

BUT!

I'm thinking about your comment, and I think we can— yeah, we can definitively remove more noise if we start with an empty accumulator.

Then the loop would start with "$1" instead, right?
So we'd have in "$@" there, which can always be omitted — less noise.
Then in the end we replace the leading space with ( — and done:

append() { local acc
           for list
           do acc="${acc%\)}${list/#\(/ }"
           done; echo "${acc/# /(}" ;}

append '(1 2 3)' '(4 (5) 6)' '((7 8) 9)'
(1 2 3 4 (5) 6 (7 8) 9)

"Is this Bash?"

OR!

We could make the acc start with an empty list — this actually makes more sense, doesn't it?

append() { local acc='()'
           for list
           do acc="${acc%\)}${list/#\(/ }"
           done; echo "${acc/#( /(}" ;}

append '(1 2 3)' '(4 (5) 6)' '((7 8) 9)'
(1 2 3 4 (5) 6 (7 8) 9)

Looks good to me, what do you think?

"Is this Bash? Is this really Bash?
Are you appending Lisp lists in Bash?!"

Cool, isn't it?

"No! This is not normal!"

Hmmm. You have a point.

We've been assuming that the lists would have no extra whitespace — but what if they do? Huh?
What if they span more than one line?
What if there's a bunch of whitespace all over the place?
What then?

So you're right — we must normalize the input lists. Then our input can be as arbitrarily spaced as it wants.

normal() { shopt -s extglob               # FROM            TO   WHERE?
           :    "${1//+([[:space:]])/ }"  # whitespace+  →  ' '  anywhere
           :    "${_// \)/)}"             # ' )'         →  ')'  anywhere
           :    "${_/# \(/(}"             # ' ('         →  '('  beginning
           :    "${_//\( /(}"             # '( '         →  '('  anywhere
           echo "${_/%\) /)}"             # ') '         →  ')'  end
           shopt -u extglob ;}

append() { local acc='()'
           for list
           do : "$(normal "$list")"
              acc="${acc%\)}${_/#\(/ }"
           done; normal "$acc" ;}

# Let's test it!
append '( )'
append '()' '( )' '(  )'   '  (    )  '
append ' ( (1  2  3  4  5 ) ) '
append '( ( 1))' ' ( ( ( 2 3)    )  ) ' '( ((4) ))' '   (5 6)'
append '(1 2 3)' '(4 (5) 6)' '((7 8) 9)'
append \
    '  (  1
          2
          3  )  ' \
              ' (  4 ( 5 )
                            6 ) ' \
                                ' ( ( 7
                                         8 )
                                          9     )    '
()
()
((1 2 3 4 5))
((1) ((2 3)) ((4)) 5 6)
(1 2 3 4 (5) 6 (7 8) 9)
(1 2 3 4 (5) 6 (7 8) 9)

There we have it!
Cool, don't you think?
Hey, let's write map-reduce()!


🤦🏻 ← Lisper facepalming
(mumble mumble strings! mumble strings! something something right-tool-for-the-job something mumble)
📆 2025-11-23