7 Tips to Write Better Bash Scripts in 2023

Fullstack CTO
12 min readJan 12, 2023

--

To create efficient and easy-to-maintain shell scripts, follow these tips. Bash is a popular command language used by programmers to automate tasks. It allows for quick execution of processes by entering binary names and parameters, as well as providing language features such as control statements and basic data structures.

When updating or modifying automation scripts, it’s crucial to write clear and manageable code to save time and improve the overall quality of the automation workflow. Here are some strategies to enhance the quality of your Bash scripts and increase your programming productivity.

1. When writing Bash scripts, it’s important to break down your code into multiple functions

This practice, known as the Separation of Concerns (SoC) principle, helps to keep your code organized and easy to understand. Some developers may think that automation scripts don’t require much organization, but this is not the case. Keeping your scripts clean and readable is important for both yourself and others who may work on the project in the future.

In Bash, you can create functions and use an integer return value for error codes, similar to how it’s done in C. For example:

#!/bin/env bash

function piece_of_logic() {
# ...
return $1
}

piece_of_logic 1

echo "Exit code: $?"

In this example, we’ve created a function called “process” that can be reused in other parts of our script. Additionally, Bash functions are command-oriented, so we can use subshells to return strings.

#!/bin/env bash

function get_name() {
echo "Alex"
}

name=$(get_name)

echo "Name: $name"

2. Some tricks and hacks

You don’t have to write the word function if you use parentheses to declare functions, and you don’t have to use parentheses if you write the word function. Both are correct.

#!/bin/env bash

function get_name() {
echo "Alex"
}

function get_name {
echo "Alex"
}

get_name() {
echo "Alex"
}

In my practice, I prefer to use such an opportunity for “typing”. If a function takes no arguments, I do not write brackets, but a function with arguments is always written with brackets. This way it’s easier to read code, as I think.

#!/bin/env bash

function func() {
echo "Arguments: $@"
}

function procedure {
echo "Hello!"
}

Even if you don’t return anything from a function, breaking down your script into smaller functions can still help to keep your code clean. If your script is still complex after this, you can consider splitting it into separate files and merging them with the “source” command.

3. When writing automation scripts, it’s important to use the correct output stream for writing messages

These messages can include information, errors, and warnings, and they are typically displayed in the terminal. However, in some cases, the script may need to provide content for other processes via output streams.

When using the “echo” command to print strings, Bash typically writes data to the standard output stream (STDOUT). However, if you use “echo” to print an error message, it will also go to the standard output stream and not the standard error stream (STDERR). This can cause issues if another process is using the command’s output as input, as the error messages will be included in the output.

To avoid this, it’s important to use the correct output stream when printing data from Bash scripts. For example:

#!/bin/env bash

echo "INFO: This message goes to STDOUT"
echo "ERROR: This message goes to STDERR" >&2

This will write the error message to the standard error stream, ensuring that it is handled correctly.

Repeatedly using >&2 with echo and message prefixes in multiple places in a script can make it difficult to maintain and goes against the DRY (Don’t Repeat Yourself) principle. A better approach is to create reusable functions for these tasks. For example:


function log_error() {
echo "[ERROR] $1" >&2
}

function halt() {
error "$1"
exit 1
}

log_error "An error occurred"
halt "This message goes to STDERR, and script will halt"

4. Use variables consistently

Using variables consistently throughout your script is a key aspect of writing clean and maintainable Bash code. Variables are a powerful tool that allows you to store and manipulate data in your scripts, making them more flexible and easier to understand.

When using variables, it’s important to be consistent in how you name and declare them. It’s recommended to use snake_case for variable names, and to declare them with the keyword “local” for better scoping. Additionally, it’s a good practice to initialize variables with a default value, so you can ensure that they are always set to a known value before being used.

On the other hand, using hard-coded values in your scripts can make them more difficult to understand and maintain. Hard-coded values are values that are directly written into the script, rather than being stored in a variable. These values are often specific to a particular use case, and if they need to be changed, it requires editing the script directly. This can be a time-consuming and error-prone process.

Using variables instead of hard-coded values makes it easy to update values in one place and have it propagate throughout the entire script. Additionally, it also makes it easy to parameterize your scripts, which allows you to run the same script with different inputs. This can be particularly useful when running the same script on different environments or with different data sets.

In summary, using variables consistently in your Bash scripts is a best practice that can help to make your code more readable, flexible, and maintainable. By avoiding hard-coded values and using variables instead, you can make it easier to update your scripts and to share them with others.

Here are some examples of how to use variables consistently in Bash scripts:

  1. Declaring variables with the keyword “local” and initializing them with a default value:
local message="Hello, World!"
local count=0

2. Using variables in place of hard-coded values:

# Using hard-coded values
echo "The current date is: $(date)"

# Using variables
current_date=$(date)
echo "The current date is: $current_date"

3. Parameterizing a script using variables:

#!/bin/bash

local input_file=$1
local output_file=$2

cat $input_file > $output_file
echo "File $input_file has been copied to $output_file"

In this example, the script takes in two arguments, which are stored in the variables “input_file” and “output_file”, respectively. This allows the script to be run on different files without the need to modify the script itself.

4. Using snake_case for variable names:

local number_of_files=5
local user_name="john"

By following these examples, you can use variables consistently in your Bash scripts and make it easier to understand and maintain your code.

5. Keep your script up to date

Automation scripts are often used for long periods of time, so it’s important to keep them up to date. Keep your scripts current by updating them regularly with new features or bug fixes.

Keeping your automation scripts up to date is a critical aspect of maintaining them over time. Automation scripts are often used for long periods of time, so it’s important to keep them current to ensure that they continue to work as expected.

One of the main reasons to keep your scripts up to date is to fix bugs that may have been discovered after the initial release. As you run your script, you may discover that it’s not working as expected or that it’s producing unexpected results. Updating your script to fix these bugs will ensure that your script is running smoothly and that it’s producing accurate results.

Another reason to keep your scripts up to date is to add new features. As your automation workflows evolve, you may find that your script needs to be updated to support new tasks or to better integrate with other systems. Updating your script with new features will ensure that it stays current and that it continues to meet the needs of your organization.

You can also update your scripts to improve the performance, by using more efficient commands, using better data structures and so on.

It’s also a good practice to document any updates that you make to your script, so that others who may need to work with it in the future will know what changes were made. This will make it easier for them to understand how the script works and how it should be used.

In summary, keeping your automation scripts up to date is an essential part of maintaining them over time. Fixing bugs, adding new features, and improving performance will ensure that your scripts continue to work as expected, while documentation of the updates will make it easier for others to understand your code.

Here are some examples of how to keep your Bash scripts up to date:

  1. Fixing bugs:
#!/bin/bash

# Original script
find . -name "*.txt" -exec rm {} \;

# Updated script
find . -name "*.txt" -exec rm {} +

In this example, the original script had a bug that caused it to fail when there were too many files to delete. The updated script uses the + option for the exec command, which allows it to handle large numbers of files.

2. Adding new features:

#!/bin/bash

# Original script
cat file1.txt

# Updated script
cat file1.txt file2.txt file3.txt

In this example, the original script only works on one file, but the updated script can now work on multiple files.

3. Improving performance:

#!/bin/bash

# Original script
grep -r "string" .

# Updated script
grep --exclude-dir={.git,.svn} -r "string" .

In this example, the original script takes a long time to run because it searches through all directories and subdirectories. The updated script uses the — exclude-dir option to skip certain directories like .git and .svn, which improves the performance of the script.

5. Documenting updates:

#!/bin/env bash
#
# Script: example.sh
# Author: John Doe
# Date: 01/01/2022
# Description: This script copies file1.txt to file2.txt
#
# Update: 01/15/2022
# Author: John Doe
# Description: Added support for multiple files
#

cp file1.txt file2.txt

In this example, the original script only support one file but it’s updated to support multiple files and the date and author of the update are also added to the script header, making it easy to understand what was updated and when.

By following these examples, you can keep your Bash scripts up to date and ensure that they continue to work as expected. Regularly updating your scripts with bug fixes, new features, and performance improvements, and keeping documentation on the updates will make it easier for others to understand and maintain your code.

6. Use Command Substitution

Command substitution is a feature that allows you to execute a command and substitute its output in place of the command itself. This can be done by enclosing the command in backticks (`) or by using the $(command) syntax. Here is an example:

#!/bin/env bash
current_date=$(date)
echo "The current date is: $current_date"

In this example, the “date” command is executed and its output is assigned to the variable “current_date”. The variable is then used to print the current date.

Process Substitution: Process substitution is a feature that allows you to use the output of one command as the input of another command. This can be done by using the <(command) or >(command) syntax. Here is an example:

#!/bin/env bash

diff <(ls dir1) <(ls dir2)

In this example, the output of the “ls dir1” command is used as the input of the “diff” command, while the output of the “ls dir2” command is used as the second input of the “diff” command. This allows you to compare the contents of the two directories.

These two tricks are very powerful tools in bash scripting, they allow you to chain commands and make your scripts more efficient and more powerful. Command substitution, for example, allow you to assign the output of a command to a variable and use it later on in the script, while process substitution allows you to use the output of one command as the input of another command. With these two tricks, you can accomplish a wide range of tasks and streamline your workflows.

7. Using multiline strings

understand. A multiline string is a string that spans multiple lines and is often used to store large amounts of text or data. In this post, we will take a closer look at how to use multiline strings in Bash scripting.

  1. Using the single quotes: One way to create a multiline string in Bash is to use single quotes (‘ ‘). Single quotes preserve the line breaks and whitespaces in the string, allowing you to create a string that spans multiple lines. For example:
message='Hello,
World!
'

This creates a multiline string called “message” that contains “Hello,” on the first line and “World!” on the second line.

2. Using the double quotes: Another way to create a multiline string in Bash is to use double quotes (“ “). Double quotes also preserve the line breaks and whitespaces in the string, but they also allow you to use variables within the string. For example:

name="John"
message="Hello,
$name
"

This creates a multiline string called “message” that contains “Hello,” on the first line and the value of the “name” variable on the second line.

3. Using the HEREDOC: Another way to create a multiline string in Bash is to use a HEREDOC. A HEREDOC is a type of redirection that allows you to include a multiline string in a script. HEREDOC, also known as a “here document,” is a feature in Bash scripting that allows you to include a multiline string in a script. It’s a powerful tool that can make your code more readable and easier to understand. In this post, we’ll take a closer look at how to use the HEREDOC in Bash scripting, as well as some tips and tricks to make the most of this feature.

Basic Syntax: To use a HEREDOC, you use the << operator followed by a delimiter, which is a word or phrase that marks the beginning and end of the multiline string. Here’s an example:

cat << EOF
This is a
multiline string
EOF

In this example, the “cat” command is used to display the contents of the HEREDOC, which is a multiline string that contains “This is a” on the first line and “multiline string” on the second line. The delimiter “EOF” marks the beginning and end of the multiline string.

Indentation: When using a HEREDOC, it’s a good practice to indent the multiline string to make it more readable. Indenting the multiline string also helps to distinguish it from the rest of the code. For example:

cat << EOF
This is a
multiline string
EOF

Using variables: You can also use variables within the HEREDOC by enclosing them in double quotes (“ “). This allows you to easily substitute the value of the variable into the string. For example:

name="John"
cat << EOF
Hello, $name
EOF

In this example, the “name” variable is used within the HEREDOC and its value is substituted into the string.

HEREDOC with command substitution: It is also possible to use command substitution within the HEREDOC by using $(command) or command notation. This allows you to include the output of a command within the HEREDOC. For example:

cat << EOF
The current date is: $(date)
EOF

Use a different delimiter: The default delimiter for a HEREDOC is “EOF”, but you can use any word or phrase as a delimiter. It’s a good practice to use a delimiter that is unlikely to appear in the multiline string. For example:

cat << END_MESSAGE
This is a
multiline string
END_MESSAGE

Using a HEREDOC in Bash scripting can make your code more readable and easier to understand. It’s a powerful tool that allows you to include a multiline string in a script and it also allows you to use variables and command substitution within the string.

With the right indentation, a clear delimiter and the use of variables, you can make your HEREDOCs even more powerful.

Using HEREDOC with redirection: Another advantage of using HEREDOC is that it can be used with redirection to write the content of the HEREDOC to a file or to use it as input for a command. For example, the following command will create a file called myfile.txt and writes the content of the HEREDOC to the file:

cat << EOF > myfile.txt
This is a
multiline string
EOF

Using HEREDOC with command input: You can also use the HEREDOC as input for a command. For example, the following command will use the content of the HEREDOC as input for the “grep” command:

grep "string" << EOF
This is a
multiline string
EOF

HEREDOC is a very useful feature in Bash scripting that allows you to include multiline strings in your script and also use variables and command substitution within the string. With the right indentation, clear delimiter, and the use of variables, you can make your HEREDOCs even more powerful. By using redirection and command input, it also allows you to easily write the content of the HEREDOC to a file or use it as input for a command.

By following these tips, you can write better Bash scripts that are easier to understand, maintain and scale. Remember that it’s important to keep your script up-to-date and clean, and don’t hesitate to ask for help or feedback from other developers.

--

--

Fullstack CTO
Fullstack CTO

Written by Fullstack CTO

CTO and co-founder at NEWHR & Geekjob

Responses (3)