Bash and shell expansions: lazy list-making

It’s that time of year again! When stores start putting up colourful sparkly lit-up plastic bits, we all begin to feel a little festive, and by festive I mean let’s go shopping. Specifically, holiday gift shopping! (Gifts for yourself are still gifts, technically.)

Just so this doesn’t all go completely madcap, you ought to make some gift lists. Bash can help.

Brace expansion

These are not braces: ()

Neither are these: []

These are braces: {}

Braces tell Bash to do something with the arbitrary string or strings it finds between them. Multiple strings are comma-separated: {a,b,c}. You can also add an optional preamble and postscript to be attached to each expanded result. Mostly, this can save some typing, such as with common file paths and extensions.

Let’s make some lists for each person we want to give stuff to. The following commands are equivalent:

touch /home/me/gift-lists/Amy.txt /home/me/gift-lists/Bryan.txt /home/me/gift-lists/Charlie.txt
touch /home/me/gift-lists/{Amy,Bryan,Charlie}.txt
tree gift-lists

/home/me/gift-lists
├── Amy.txt
├── Bryan.txt
└── Charlie.txt

Oh darn, “Bryan” spells his name with an “i.” I can fix that.

mv /home/me/gift-lists/{Bryan,Brian}.txt

renamed '/home/me/gift-lists/Bryan.txt' -> '/home/me/gift-lists/Brian.txt'

Shell parameter expansions

Shell parameter expansion allows us to make all sorts of changes to parameters enclosed in braces, like manipulate and substitute text.

There are a few stocking stuffers that all our giftees deserve. Let’s make that a variable:

STUFF=$'socks\nlump of coal\nwhite chocolate'

echo "$STUFF"
socks
lump of coal
white chocolate

Now to add these items to each of our lists with some help from the tee command to get echo and expansions to play nice.

echo "$STUFF" | tee {Amy,Brian,Charlie}.txt

cat {Amy,Brian,Charlie}.txt

socks
lump of coal
white chocolate
socks
lump of coal
white chocolate
socks
lump of coal
white chocolate

Pattern match substitution

On second thought, maybe the lump of coal isn’t such a nice gift. You can replace it with something better using a pattern match substitution in the form of ${parameter/pattern/string}:

echo "${STUFF/lump of coal/candy cane}" | tee {Amy,Brian,Charlie}.txt

cat {Amy,Brian,Charlie}.txt

socks
candy cane
white chocolate
socks
candy cane
white chocolate
socks
candy cane
white chocolate

This replaces the first instance of “lump of coal” with “candy cane.” To replace all instances (if there were multiple), use ${parameter//pattern/string}. This doesn’t change our $STUFF variable, so we can still reuse the original list for someone naughty later.

Substrings

While we’re improving things, our giftees may not all like white chocolate. We’d better add some regular chocolate to our lists just in case. Since I’m super lazy, I’m just going to hit the up arrow and modify a previous Bash command. Luckily, the last word in the $STUFF variable is “chocolate,” which is nine characters long, so I’ll tell Bash to keep just that part using ${parameter:offset}. I’ll use tee’s -a flag to append to my existing lists:

echo "${STUFF: -9}" | tee -a {Amy,Brian,Charlie}.txt

cat {Amy,Brian,Charlie}.txt

socks
candy cane
white chocolate
chocolate
socks
candy cane
white chocolate
chocolate
socks
candy cane
white chocolate
chocolate

You can also:

Do this With this
Get substring from n characters onwards ${parameter:n}
Get substring for x characters starting at n ${parameter:n:x}

There! Now our base lists are finished. Let’s have some eggnog.

Testing variables

You know, it may be the eggnog, but I think I started a list for Amy yesterday and stored it in a variable that I might have called amy. Let’s see if I did. I’ll use the ${parameter:?word} expansion. It’ll write word to standard error and exit if there’s no amy parameter.

echo "${amy:?no such}"

bash: amy: no such

I guess not. Maybe it was Brian instead?

echo "${brian:?no such}"

Lederhosen

You can also:

Do this With this
Substitute word if parameter is unset or null ${parameter:-word}
Substitute word if parameter is not unset or null ${parameter:+word}
Assign word to parameter if parameter is unset or null ${parameter:=word}

Changing case

That’s right! Brian said he wanted some lederhosen and so I made myself a note. This is pretty important, so I’ll add it to Brian’s list in capital letters with the ${parameter^^pattern} expansion. The pattern part is optional. We’re only writing to Brian’s list, so I’ll just use >> instead of tee -a.

echo "${brian^^}" >> Brian.txt

cat Brian.txt

socks
candy cane
white chocolate
chocolate
LEDERHOSEN

You can also:

Do this With this
Capitalize the first letter ${parameter^pattern}
Lowercase the first letter ${parameter,pattern}
Lowercase all letters ${parameter,,pattern}

Expanding arrays

You know what, all this gift-listing business is a lot of work. I’m just going to make an array of things I saw at the store:

gifts=(sweater gameboy wagon pillows chestnuts hairbrush)

I can use substring expansion in the form of ${parameter:offset:length} to make this simple. I’ll add the first two to Amy’s list, the middle two to Brian’s, and the last two to Charlie’s. I’ll use printf to help with newlines.

printf '%s\n' "${gifts[@]:0:2}" >> Amy.txt
printf '%s\n' "${gifts[@]:2:2}" >> Brian.txt
printf '%s\n' "${gifts[@]: -2}" >> Charlie.txt
cat Amy.txt

socks
candy cane
white chocolate
chocolate
sweater
gameboy

cat Brian.txt

socks
candy cane
white chocolate
chocolate
LEDERHOSEN
wagon
pillows

cat Charlie.txt

socks
candy cane
white chocolate
chocolate
chestnuts
hairbrush

There! Now we’ve got a comprehensive set of super personalized gift lists. Thanks Bash! Too bad it can’t do the shopping for us, too.

terminal   linux