DEV Community

Cover image for Writing Bash Scripts Like A Pro - Part 1 - Styling Guide

Writing Bash Scripts Like A Pro - Part 1 - Styling Guide

Meir Gabay on October 24, 2021

Writing Bash scripts can be challenging if you don't know the quirks and perks. In my mother tongue, we use the Yiddish word for quirks and perks; ...
Collapse
 
netikras profile image
Darius Juodokas

About the ${} and lonely variables... I disagree with this exception.

Three reasons.

  1. CONSISTENCY!!! To write clean and easy to read scripts one must write consistent code. Consistent style, consistent patterns, consistent tooling. This way a maintainer/reader won't have to switch to "oh, that's a different variable expression. wait, is that dollar sign a part of the thing, or is it to be expanded..? oh, it's expanded. Good. So it's a variable" mode.

  2. READABILITY. Tightly coupled to #1. Moreover, it's A LOT easier to read script and know which parts are dynamic and which aren't when the dynamic ones have clearer visual contrast. The ${} expression introduces 3 characters that make a word look like a variable. $ only has 1 char. It's easier to spot that large clunky ${myvar} than $myvar in the code. Regardless whether the value is lonely, at the EOL/SOL, on the left or spelled in reverse (consistency again!).

  3. SAFETY. Writing local age=$1 or even local age=${1} is not the best idea. What if the function was called w/o the parameter? The fn logic will most likely break. I've seen oh too many mistakes like that ending up in production environments going down or even worse -- getting completely destroyed and requiring a full rebuild (means days-weeks of work). Don't do that! Whenever you're accepting and reassigning function parameters, verify whether you've been passed any. One way to do that is [ -n "${1}" ] || return 1, but that's just tedious and inelegant to write the same block of code in every function. Bash expansion provides a very handy safety switch: local age=${1:?Age not provided}. Or a fallback to default value: local age=${1:-30}. This way you can easily control what your variables are. If the value wasn't provided, in first case the function will return 1 and print the error message to stderr "Age not provided". In the second case the fn will continue execution after auto-assigning 30 to the variable age (if ${1} is empty).

How does that relate to safety? By writing ${myVar} you will get the habbit of always thinking "do I need to use bash's variable expansion here? Perhaps default values? Or failfast safety switches? Substitution perhaps..?". Back in the days I was using $ I used to forget to check what's inside the $1 and write failfast mechanisms. By using ${1} now my hand automatically adds the :? or :- and makes me think twice. Even if IDC if the value is empty - I always leave it as local age=${1:-} just to make it absolutely clear that this function can tollerate missing parameter(s).

I think I should write my own post series about shell/bash scripting.....

Collapse
 
unfor19 profile image
Meir Gabay • Edited

I would reply to this comment properly, if you hadn't added the last part

I think I should write my own post series about shell/bash scripting

Go for it.

Collapse
 
netikras profile image
Darius Juodokas

If you have a different opinion, please, do share :) I'd like to know your opinion and motivation behind "lonely variables"

Collapse
 
wrp profile image
William Pursell

Unless newer versions of bash are playing fast and loose with the language, '
if [ "$USER_NAME" = "Julia" || ..' is an error. You could write if test "$USER_NAME" = Julia || ..., but the [ command requires that its final argument be ]. The || is not an argument.

Collapse
 
unfor19 profile image
Meir Gabay • Edited

Superb comment, I never use single [ ] brackets, so I'm less familiar with how it all works. I'll fix my answer to fit the proper syntax, thanks!

Collapse
 
baggiponte profile image
baggiponte

So which naming convention shall I use inside a loop? for _VAR in? Thank you for the guide!

Collapse
 
unfor19 profile image
Meir Gabay

The way I name it

declare -a _ITEMS=("first" "second" "third")
for item in "${_ITEMS[@]}"; do
    echo "Item name is ${item}"
done
Enter fullscreen mode Exit fullscreen mode

Output

Item name is first
Item name is second
Item name is third
Enter fullscreen mode Exit fullscreen mode

I usually name my iterator as item, even if it's a LIST_OF_ROWS or ARRAY_OF_THINGS I usually pick item as my iterator (in any lang).

So, as you can see, there's no clear preference, just go with what suits you, and if you find item to be good for you, go for it

Collapse
 
baggiponte profile image
baggiponte

Thank you for the reply! Guess a guide on arrays would also be super useful - the example you made right here was more clarifying than many web articles I have read...

Thread Thread
 
unfor19 profile image
Meir Gabay

I'll definitely cover arrays in my next blog post, thank you for the feedback, much appreciated!

Thread Thread
 
baggiponte profile image
baggiponte

Also, off the top of my mind: getopt for flags and parallel to avoid writing loops in scripts (I’m just being creative)

Thread Thread
 
unfor19 profile image
Meir Gabay • Edited

Would you believe me if I told you that I've never, ever, used getopts or paralllel ? 🙈 I Never had the need to ... I found better alternatives that were easier to "memorize".

getopts - See bargs; A framework for creating a Bash CLI. You would expect me to use getops for the "Usage" menu ... But I haven't, I used other tricks to make it work

parallel - I've never processed big files in Bash, if I need to go down this road, I'll probably use Go, where it's more inclined towards parallelism and threading. I do use background jobs, but mainly for short processes which don't require "blocks of data". Instead, I use background jobs, which is simply adding & to a command and wait in the bottom of the file to wait for it to finish. For example "downloading 5 files in parallel" or "encrypting 10 files in parallel".

curl -o file1 https://example.com/file1 &
curl -o file2 https://example.com/file2 &
curl -o file3 https://example.com/file3 &
curl -o file4 https://example.com/file4 &
curl -o file5 https://example.com/file5 &

wait # for all jobs to finish ...
Enter fullscreen mode Exit fullscreen mode

It's also a matter of time/knowledge; if you know getopts and parallel then use them. If you don't, feel free to pass it, as I haven't found it "mandatory", but that's me.

Thread Thread
 
baggiponte profile image
baggiponte

I didn’t know about bargs! That’s precious advice, thank you!

Collapse
 
xiuzhi_ profile image
Ting Smith

this blog is impressive, makes me learn a lot

Collapse
 
shrihankp profile image
Shrihan

sudo comment "Great stuff! Will wait for the next parts!"

Collapse
 
psvpl profile image
Piotr Szeptynski

You don't need sudo for this. 😉

Collapse
 
unfor19 profile image
Meir Gabay

Thanks! Much appericiated!

Collapse
 
morphzg profile image
MorphZG

Thanks for sharing. Will follow and wait for more.

Collapse
 
unfor19 profile image
Meir Gabay

Kudos, will post the next part soon :)

Collapse
 
indreias profile image
Ioan Indreias

I'm usually set environment variables for the Bash scripts (or any other executable) like:
$ USER_NAME="Julia" USER_AGE="36" good_vibes.sh

Collapse
 
unfor19 profile image
Meir Gabay

Depends on the context ...
For local usage, sure, that works great.

For CI/CD processes, usually, values/secrets are injected with env vars ...

My mindset - fetch default values from env vars. A matter of approach/opinion I guess

Collapse
 
psvpl profile image
Piotr Szeptynski

Me too. That's better than exporting them to the env.

Collapse
 
bence42 profile image
Bence Szabo

Nice! I'm happy to see that somebody agrees that bash is still relevant and is worth to learn.
Will you cover the readonly "qualifier"?

Collapse
 
unfor19 profile image
Meir Gabay

I'm using Bash across most of my projects, so for me it's definitely here to stay :)

Truth be told, I haven't used readonly in any script, ever. I guess it's best practice to create immutable variables with:

declare -r VAR_NAME="CONSTANT_VALUE"
VAR_NAME="trying-to-change-value"
# Output
# bash: VAR_NAME: readonly variable
Enter fullscreen mode Exit fullscreen mode

Looks cool and easy to implement, I might adopt it in my future scripts.

Collapse
 
tatianacodes profile image
Tatiana

Great timing! I was just looking into creating a couple scripts today.

Collapse
 
unfor19 profile image
Meir Gabay

Glad I could help!

Collapse
 
zoulja profile image
zoulja

Really nice article, but some explanation is needed.

Oh, and make sure you don't use $1 or any other argument directly; always use local var_name="$1".

Why?

Collapse
 
unfor19 profile image
Meir Gabay

@zoulja Good point!

I prefer using named variables instead of positional arguments. So the logic of my code is based on names instead of indexes, such as $1, $2 and so on. This way, even if the order of given arguments is changed, I still maintain my function's logic.

And of course, let's learn by example; assuming we create the greet() function

greet(){
  local name="$1"
  local age="$2"

  echo "Hello ${name}, you're ${age} years old."
}

# Usage
greet "Willy" "33"
Enter fullscreen mode Exit fullscreen mode

Now, I want to change the positional arguments, so the function consumes age and then name

greet(){
  # I switched between $1 and $2
  # Also changed the order of variables so it makes sense
  local age="$1"
  local name="$2"

  # Didn't touch the logic
  echo "Hello ${name}, you're ${age} years old."
}

# Usage
greet "33" "Willy"
Enter fullscreen mode Exit fullscreen mode

I hope that explains it

Collapse
 
hellorge profile image
King o' Hell

Can I have your editor's color profile?

Collapse
 
unfor19 profile image
Meir Gabay • Edited

Well .. As you can those are code blocks, not screenshots, so the coloring comes from dev.to Markdown CSS.

To make code blocks beautiful in Markdown, simply add the relevant lang at the beginning of the code block, like:

echo "awesome"
Enter fullscreen mode Exit fullscreen mode

I added bash right after the first 3 backticks `