Shell Scripting Notes for Normies#

0. Passing and Parsing Argument#

Tl;dr Use double quotes when passing arguments (unless there is specific reason not to).

The shell parses arguments before passing them on to whatever program is getting invoked.

For example,

Bash#
$ echo "hello          world"
stdout#
hello          world

gives a different output than typing

Bash#
$ echo hello           world
stdout#
hello world

In the first case, shell recognizes one argument. It then strips away the quotes before passing the string as one argument. So the extra whitespaces are treated literally. In the second case, ‘hello’ and ‘world’ are treated as two separate arguments and the whitespace(s) are treated as a delimiter.

A practical example is when passing file or folder names as arguments, and the names contain spaces (e.g. a folder called new proj/. Omitting double quotes will lead shell to think there are two arguments (new and proj) instead of a single path and cause unexpected behaviours or errors.

Here's an example from a shell function I have a function that finds my . files and reports them.
script.sh#
1function repdfile {
2  for dotfile in $HOME/.*
3  do
4    if [[ "$dotfile" == "$HOME/." || "$dotfile" == "$HOME/.." || ! -f "$dotfile" ]]; then
5      continue
6    fi
7    basename $dotfile
8  done
9}

Using ShellCheck, a static analysis tool that checks shell script for common errors, prompts that I should have quoted $HOME to prevent unintentional wordsplitting as a good practice:

stderr#
In .functions line 94:
  for dotfile in $HOME/.*
                 ^---^ SC2231 (info): Quote expansions in this for loop glob to prevent wordsplitting, e.g. "$dir"/*.txt .

and again at the end when echoing

stderr#
In .functions line 101:
  basename $dotfile
           ^------^ SC2086 (info): Double quote to prevent globbing and word splitting.

1. Variables#

Assigning#

Tl;dr There should be no spaces around =.

Assigning and referencing variables is as simple as

Bash#
$ log="log of messages here"
$ echo "$log"
stdout#
log of messages here

But this will not work even though it looks similar

Bash#
$ log = "log of messages here"
$ echo "$log"
stdout#
log: command not found

because the shell will think that log is a command and try to invoke it. Even worse is if the variable shares the name of an existing command which can lead to unexpected behaviour:

Bash#
$ echo = "log of messages here"
stdout#
= log of messages here

Scope#

Variables only exist in the scope of the shell scripts. So if I define a variable in shell

Bash#
$ mypath="./folder/subfolder"

and have a script with mypath,

example.sh#
echo "$mypath"

expected the example.sh script to be able point to it…

Bash#
$ . example.sh
stdout#

I get an empty string. This is because the script is unable to reference mypath. This example also shows that variables do not have to be declared. And the undeclared mypath variable in example.sh is just an empty string.

For example.sh to use mypath, I need to export it so that it becomes available to other subprocesses or scripts launced from the current shell session.

Bash#
$ mypath="./folder/subfolder"
$ export mypath
$ . example.sh
stdout#
./folder/subfolder

2. Control Operators and Structures#

Tl;dr:

  • Control structures are closed by spelling backwards (e.g. if and fi).

  • ; do and ; then on same line as while, for or if.

A quick reference of some built-in operators.

Operator/Structure

Description

&&

AND

||

OR

&

Background operator to chain commands

|

Pipe operator to pipe output from one command to another

;

Command separator

>

Redirects output to file - overwrite

>>

Redirects output to file – append

;

Executes following command regardless of previous exit status (e.g., with if conditions)

ifthen [elif/else] … fi

If condition

forindodone

For loop

whiledodone

While loop

break

Exit from loop but continue from rest of script

continue

Skip to next iteration in loop and continue with rest of script

Formatting for if-else#

then should be on the same line as if. elif and else, if any, should be on their own line. The closing statement fi should be on its own line corresponding with its opening statement (if).

Use this:

shellscript#
1if [[ condition ]]; then
2  ...
3elif [[ condition ]]; then
4  ...
5else 
6  ...
7fi

Instead of this:

shellscript#
1if [[ condition ]]; 
2  then
3  ...
4elif [[ condition ]]; 
5  then
6  ...
7else 
8  ...
9fi

which is also syntatically correct but not the preferred formatting.

3. Testing#

Tl;dr

  • Test operators for testing already exist, so use them. Be explicit.

  • Prefer [[ condition ]].

  • Prefer explicit tests.

  • Prefer == over =.

  • Don’t forget spaces: [[ condition ]] and not [[condition]] (because the [ are also commands that are delimited by whitespaces).

Testing is used to check if certain conditions are true or false before moving ahead to other parts of the script. For example, I may want to (1) test if a file exists before trying to read from it or (2) check if an argument is provided to a function to decide whether to use a default option.

More concretely, to test if a provided path to a file exists before trying to source it, I can do

shellscript#
1file="/path/to/file"
2if [ -f "$file" ]; then
3  ...
4fi

where the -f is a test operator that tests for whether “$file” is indeed a file.

There are many other test operators for common tests, like checking whether an argument is a directory (-d), whether a string is non-empty (-n), or for numeric equality (-eq).

For instance, I can test if no arguments were provided to a function using -eq. If no arguments were provided, then go with some default inputs and behaviour. The $# is a magic variable for the number of positional parameters passed to a script of function. (See here for more magic variables).

shellscript#
1function func() {
2  if [ $# -eq 0 ]; then
3    # go with some default behaviour
4  else
5    # use the inputs
6  fi
7}

An alternative syntax to testing is with something like

shellscript#
1file="/path/to/file"
2if test -f "$file"; then
3  ...
4fi

The [ is actually another form of the test command. Here is an incomplete list of test operators.

Test operator

Description

Example Usage

test

Test

test mytest

[ ]

Test (whitespace required)

[ mytest ]

-eq

Tests whether two values are equal.

if [ 9 -eq 9 ]

-ne

Tests whether two values are not equal.

if [ 9 -ne 9 ]

-lt

Tests whether one value is less than another.

if [ 9 -lt 9 ]

-le

Tests whether one value is less than or equal to another.

if [ 9 -le 9 ]

-gt

Tests whether one value is greater than another.

if [ 9 -gt 9 ]

-ge

Tests whether one value is greater than or equal to another.

if [ 9 -ge 9 ]

-z

Tests whether a string is empty.

if [ -z “$string” ]

-n

Tests whether a string is not empty.

if [ -n “$string” ]

-f

Tests whether a file exists and is a regular file.

if [ -f “$file” ]

-d

Tests whether a file exists and is a directory.

if [ -d /path/to/folder ]

-e

Tests whether a file exists.

if [ -e “$file” ]

-r

Tests whether a file is readable.

if [ -r “$file” ]

-w

Tests whether a file is writable.

if [ -w “$file” ]

-x

Tests whether a file is executable.

if [ -e “$file” ]

-h

Test whether a file is a symbolic link

if [ -h “$file” ]

!

Reverses the condition

if [ ! -h “$file” ]

Extended testing: [[ ]]#

The extended testing syntax uses [[ ... ]] which provides additional support for testing, like the use of control operators && and || for compound expressions:

shellscript#
if [[ $file = "value" && -f "$file" ]]; then
  ...
fi

Tests can also be used in a single line. For example, a test to create a directory if not present:

shellscript#
newdir="path/to/dir"
[ -d "$newdir" ] || mkdir -p "$newdir"

Test, [ ], and [[ ]]#

[[ ]] is preferred (according to the style guide). Using [[ ]] reduces error as no pathname and work splitting occur in [[ ]] (although it would still be good practice to double quote variables in [[ ]]).

As an additional example on the utility of [[ ]] and the no expansion inside of [[ ]] from the guide:

shellscript#
# This matches the exact pattern "f*" (Does not match in this case)
if [[ "filename" == "f*" ]]; then
  echo "Match"
fi
shellscript#
# But this gives a "too many arguments" error as f* is expanded to the
# contents of the current directory
if [ "filename" == f* ]; then
  echo "Match"
fi

Testing strings#

Bash is smart enough to deal with empty strings.

shellscript#
1my_var=""
2if [[ "${my_var}" ]]; then
3  echo "my_var exists"
4else
5  echo "my_var does not exist"
6fi
stdout#
my_var does not exist

This is also ok but not preferred:

shellscript#
1my_var=""
2if [[ "${my_var}" == "" ]]; then
3  echo "my_var exists"
4else
5  echo "my_var does not exist"
6fi
stdout#
my_var does not exist

It’s better to be explicit with -n to avoid confusion.

shellscript#
1my_var=""
2if [[ -n "${my_var}" ]]; then
3  echo "my_var exists"
4else
5  echo "my_var does not exist"
6fi
stdout#
my_var does not exist

== and =#

Use == instead of = for clarity even though both are syntatically correct. Use this:

shellscript#
if [[ "${my_var}" == "some value" ]]; then
  ...
fi

Instead of this:

shellscript#
if [[ "${my_var}" = "some value" ]]; then
  ...
fi

4. Loops#

Tl;dr for loops and while loops:

shellscript#
1for i in 1 2 3 ...
2do
3  echo $i
4done
shellscript#
1while condition
2do
3  # commands to be executed
4done

Basic structure of a for loop#

shellscript#
1for item in item1 item2 item3 ... itemN
2do
3  # Commands to be executed
4  ...
5done
Example: Installing a list of applications using for loop
apt-install.sh#
1apps=(
2  app1
3  app2
4  app3
5  ...
6)
7for app in "${apps[@]}"; do {
8  sudo apt-get install -y "$app"
9}

where [@] is a parameter expansion to expand the apps list into ”app1 app2 app3 …”.

Basic structure of a while loop#

shellscript#
1while [ condition ]
2do
3  # Commands to be executed
4  ...
5done
Example: Installing a list of applications using a file and a while loop

Suppose an applications.txt file enumerates applications for installation.

application.txt#
1app1
2app2
3app3
4...

A while loop can be used to read application.txt and install line by line until the end of file.

shellscript#
1while read app 
2do
3  sudo apt-get install -y "$app"
4done < application.txt

Formatting#

; do should on the same line as for and while (and also for if). Closing statements (e.g., done) should be on their own line vertically aligned with the corresponding opening statement. So the while loop just before should be:

shellscript#
1while read app; do
2  sudo apt-get install -y "$app"
3done < application.txt

The first example should be:

shellscript#
1for i in 1 2 3 ...; do
2  echo $i
3done

Variable names#

Variable names in loops should be named similar to what is being looped over. Not really different from other languages. Name things with common sense. As an example:

This:

shellscript#
for file in "${files[@]}"; do
  something_with "${file}"
done

Instead of this:

shellscript#
for x in "${my_list[@]}"; do
  something_with "${x}"
done

5. Functions#

Executing shell scripts#

Tl;dr:

  • source <script> or . <script> to source.

  • ./<script> to execute.

  • Remember to chmod +x <script> to grant execution permission.

Shell scripts can be implied by .sh but the extension is not necessary. The shell would recognise a file as a shell script using the “shebang” or the “hashbang” in the first line of the file which beings with #!. For eaxmple, to let the shell know to use bash as the shell interpreter, the first line of the file should be

script.sh#
#!/bin/bash

To make the script executable, if it is not already, type the following to add executable to the file permissions:

Bash#
$ chmod +x script.sh

Execute a shell script using

Bash#
$ . script.sh

or

Bash#
$ source script.sh

or

Bash#
$ ./script.sh

The first two sources script.sh while the third one executes it. The first two only requires read permissions. The third one will not work without the execute permission.

This will not work because shell will think that script.sh is a command (it’s a file which may contain a function but it’s not a command itself).

Bash#
$ script.sh

If I want to run the script like a command, I need to export the path to script.sh in bash_profile or bashrc.

Bash#
$ export PATH="/path/to/script.sh:$PATH"

Basic structure of a function#

A function and calling the function looks something like this

script.sh#
#!/bin/bash
function func() {
  # function code
  ...
}

func arg1 arg2 ...

where arg1, arg2, and so on, are positional arguments passed to func. These can be referenced inside the function as $1, $2, an so on.

Positional arguments#

Shell functions only support positional arguments. Named arguments are not supported. If I want to work with names, I need assign them to names in the function.

script.sh#
#!/bin/bash
function func() {
  named_arg1=$1
  named_arg2=$2
  ...
  # function code
  ...
}

func arg1 arg2 ...

where named_arg1, named_arg2, …, can then be used within the function.

Defaults for arguments#

Defaults for the named arguments can be also be set using the following syntax.

script.sh#
#!/bin/bash
function func() {
  named_arg1=${1:-default_value1}
  named_arg2=${2:-default_value2}
  ...
  # function code
  ...
}

func arg1 arg2 ...

Passing arguments from shell to function#

To pass arguments to func from the shell, there are two ways. One way is to source the script which then gives direct access to func.

Bash#
$ source script.sh
$ func arg1 arg2

The other way is to transfer arguments from the script to the function func by using “$1” and “$2” in script.sh.

script.sh#
#!/bin/bash
function func() {
  named_arg1=${1:-default_value1}
  named_arg2=${2:-default_value2}
  ...
  # function code
  ...
}

func "$1" "$2"

So that the shell arguments can be passed in as function arguments.

Bash#
$ ./script.sh arg1 arg2

or

Bash#
$ . script.sh arg1 arg2

Function comments#

Comments should be included for non-trivial functions. The Google style guide suggests that function comments should include:

  • Description of function

  • Globals: Global variables used (and modified)

  • Arguments: Arguments

  • Outputs: Output to STDOUT or STDERR

  • Returns: Returned values other than exit status

Here is an example for a small function that creates a directory and immediately cd into it:

shellscript#
 1#######################################
 2# Create a new directory and enter it.
 3# Arguments:
 4#   directory to create
 5# Returns:
 6#   0 if successful, non-zero on error
 7#######################################
 8function mkdircd() {
 9  if mkdir -p "$@" && cd "$_"; then
10    return 0
11  else
12    echo "Failed to change directory" >&2
13    return 1
14  fi
15}

Here are more examples from the Google style guide:

shellscript#
 1#######################################
 2# Cleanup files from the backup directory.
 3# Globals:
 4#   BACKUP_DIR
 5#   ORACLE_SID
 6# Arguments:
 7#   None
 8#######################################
 9function cleanup() {
10  11}
12
13#######################################
14# Get configuration directory.
15# Globals:
16#   SOMEDIR
17# Arguments:
18#   None
19# Outputs:
20#   Writes location to stdout
21#######################################
22function get_dir() {
23  echo "${SOMEDIR}"
24}

Function names#

Function names should be lower-cased with underscores to separate words. The keyword function before the function name is optional, but should be used consistently.

shellscript#
1my_func() {
2  3}
4
5another_utility() {
6  7}

or

shellscript#
1function my_func() {
2  3}
4
5function another_utility() {
6  7}

The function keyword is not necessary with () after the function name but can be useful for quick identification of functions. (See the example here.)

6. Magic Variables#

Another table of existing magic variables or shell-internal, readonly special variables, that would come in handy.

Special variable

Description

$HOME

path to home dir

$USER

username

$HOSTNAME

hostname

$PWD

current working dir

$PATH

list of directories of executables

$SHELL

current shell executable

$0

Name of shell or script

$@

All positional arguments as separate words

$#

Number of positional paramenters passed to script or function

$?

Return/Exit status of last command

$_

Last argument of previous command

7. Linters#

Tl;dr Use ShellCheck and shfmt:

Bash#
$ shellcheck <path/to/file>
Bash#
$ shfmt -i 2 -w -d <path/to/file>

ShellCheck#

Linters and autoformatters should be used whenever possible to identify common issues and warnings. Apparently, this is no different for shell scripts, large or small.

ShellCheck is a “shell script static analysis tool”. I find it useful to catch syntax errors and ohter common issues. To install,

Bash#
$ sudo apt-get install shellcheck

The basic usage of ShellCheck is just

Bash#
$ shellcheck <path/to/script>

Here’s an example of a short but real script called bin/install.sh that symlinks the ./bin/ folder to $HOME/bin.

Bash#
$ cat bin/install.sh
stdout#
───────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       │ File: bin/install.sh
───────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1   │ #!/bin/bash
   2   │
   3   │ # defaults
   4   │ DOTFILES_MASTER="$HOME/dotfiles"
   5   │ INSTALL_PATH="$HOME"
   6   │
   7   │ cd "$INSTALL_PATH" && ln -s "$DOTFILES_MASTER/bin" .
   8   │ echo "Installed binaries to $HOME/bin"

And using ShellCheck flags out the following.

Bash#
$ shellcheck bin/install.sh
stdout#
 1In bin/install.sh line 5:
 2INSTALL_PATH="$HOME"
 3^----------^ SC2034 (warning): INSTALL_PATH appears unused. Verify use (or export if used externally).
 4
 5
 6In bin/install.sh line 7:
 7cd $HOME && ln -s $DOTFILES_MASTER/bin .
 8   ^---^ SC2086 (info): Double quote to prevent globbing and word splitting.
 9                  ^--------------^ SC2086 (info): Double quote to prevent globbing and word splitting.
10
11Did you mean:
12cd "$HOME" && ln -s "$DOTFILES_MASTER"/bin .
13
14For more information:
15  https://www.shellcheck.net/wiki/SC2034 -- INSTALL_PATH appears unused. Veri...
16  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...

I forgot to double quote the variables. I also forgot that I already set a default option for the installation path just two lines ago. ShellCheck flags those out immediately.

shfmt#

shfmt is another command-line tool that can help with shell scripts. But unlike ShellCheck, shfmt seems more of a formatter and does not catch as many mistakes as ShellCheck. But it’s just as easy to use out of the box. shfmt does not seem to be availabel from APT or Brew, but there is webi. To install

Bash#
$ curl -sS https://webinstall.dev/shfmt | bash
$ source ~/.config/envman/PATH.env

The basic usage of shfmt is

Bash#
$ shfmt -i 2 <path/to/script>

The -i 2 says that indentation occurs with 2 spaces (not tabs). This follows the Google style guide.

Running shfmt on the same bin/install.sh shell script does not catch any errors.

Bash#
$ shfmt -i 2 bin/install.sh
shellscript#
1#!/bin/bash
2
3# defaults
4DOTFILES_MASTER="$HOME/dotfiles"
5INSTALL_PATH="$HOME"
6
7cd "$INSTALL_PATH" && ln -s "$DOTFILES_MASTER/bin" .
8echo "Installed binaries to $HOME/bin"

This file ends before any formatting was made. Showing the diff (the -d) confirms that nothing was flagged for formatting.

Bash#
$ shfmt -i 2 -d bin/install.sh
stdout#
--- bin/install.sh.orig
+++ bin/install.sh

So I’m going to try another file which I know will have flags because there are indentations in loops and functions, and the indentations do not respect the 2 spacing rule. Here is the first 11 lines of a bash/.functions for a function that creates a directory and cd into it immediately.

Bash#
$ head -n 11 bash/.functions | cat
stdout#
-───────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       │ STDIN
───────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1   │ #!/usr/bin/env bash
   2   │
   3   │ # Create a new directory and enter it
   4   │ function mkdircd() {
   5   │     if mkdir -p "$@" && cd "$_"; then
   6   │         return 0
   7   │     else
   8   │         echo "Failed to change directory" >&2
   9   │         return 1
  10   │     fi
  11   │ }

Running shfmt tells me that I was using the wrong indentation.

Bash#
-$ shfmt -i 2 -d bash/.functions
stdout#
 1- # Create a new directory and enter it
 2 function mkdircd() {
 3-    if mkdir -p "$@" && cd "$_"; then
 4-        return 0
 5-    else
 6-        echo "Failed to change directory" >&2
 7-        return 1
 8-    fi
 9+  if mkdir -p "$@" && cd "$_"; then
10+    return 0
11+  else
12+    echo "Failed to change directory" >&2
13+    return 1
14+  fi
15 }
16 ...

By default, shfmt only writes to standard output, as above. Using the -w option tells shfmt to write to the file and format automatically. So showing the diff again after writing to file should show no more formatting errors.

Bash#
$ shfmt -i 2 -w -d  bash/.functions
$ shfmt -i 2 -d  bash/.functions
stdout#

8. Finding and Searching#

find#

Bash#
$ find . -name "*.md" -type f
stdout#
./README.md
./misc/references.md

finds all text files ending with .md. The . indicates the current path and -type f specifies that only regular files are searched (not directories or other types of files). By default, find is recursive, as it also captures references.md in ./misc/. The -maxdepth 1 options limits recursive depth to 1:

Bash#
$ find . -maxdepth 1 -name "*.md"
stdout#
./README.md

find also supports multiple search patterns and case-insensitive search:

Bash#
$ find . -type f  -iname "readme.md" -or -name "*.ps1"
stdout#
./README.md
./win/winget.ps1
./win/chocolatey.ps1

grep#

find only supports seaching of file and folder names. grep can be used to search text inside files. For example, to search which files contain the word “function”,

Bash#
$ grep -r "function" .
stdout#
./bash/.bash_profile:    functions
./bash/.aliases:# Make directory and cd into it (see .functions)
./bash/.aliases:# touch a file and edit (see .functions)
./bash/.aliases:# Open director (see .functions)
./bash/.functions:# Collection of small functions used mainly for aliases and some utitities.
./bash/.functions:function mkdircd() {
./bash/.functions:function touchedit() {
./bash/.functions:function go-up-to-git-root {
... 

But that is not really what I want. What I wanted were to find shell functions, those lines with a “function <func_name>() {...}”. So grep can be combined with regex to handle that:

Bash#
$ grep -r '^function myfunc[a-zA-Z0-9_]*()'
stdout#
bash/.functions:function mkdircd() {
bash/.functions:function touchedit() {
bash/.functions:function fs() {
bash/.functions:function start() {

(See here for more oneliners with grep.)

What the above output also tells me is that I was not using the function syntax with function names consistently. For some functions I used function <func_name> {...} but for others I used <func_name>() {...}. See the note on function names.

9. Additional Notes About Style#

Based on the Google Shell Style Guide.

Shell files#

Extensions#

Executables (files with chmod +x <shellscript>) should preferably have no extension or the .sh extension. It is not necessary to know what the underlying programming language is when executing. (Doing a ls -l will show the permissions of folders and files.) Libraries must have the .sh extension and not have execute permissions.

This means the previous bin/install.sh example, which is executable, should have been (not a pun) named bin/install.

Which shell#

Executables must start with #!/bin/bash. Restricting to Bash gives a consistent shell language that almost all machines have.

Commenting#

Start each shell script file with the #!/bin/bash shebang and a top-level comment on the brief overview of the file contents. Example:

shellscript#
#!/bin/bash
#
# Collection of small functions used mainly for aliases some utitities.
...

Naming#

Lowercase. Separates words can be indicated by underscores. Example: bash_profile and not bash-profile. symlink_tasks.sh or symlinktasks.sh and not symlink-tasks.sh.

Formatting#

Indentation#

2 spaces. No tabs.

Apparently spaces are more consistent. Tabs may be interpreted differently on different editors or platforms. Text editors can be configured to do this by default when it detects shell scripts.

Line length#

80 characters. Max.

Pipes#

A oneliner pipeline should be split over mutliple lines if it gets too long. Here is the example from the style guide:

shellscript#
1# All fits on one line
2command1 | command2
3
4# Long commands
5command1 \
6  | command2 \
7  | command3 \
8  | command4

Same applies to the logical compounds: || and &&.

So the very first example of a function repdfile can be formatted as

shellscript#
 1function repdfile {
 2  for dotfile in "$HOME"/.*; do
 3    if [[ "$dotfile" == "$HOME/." \
 4        || "$dotfile" == "$HOME/.." \
 5        || ! -f "$dotfile" ]]; then
 6      continue
 7    fi
 8    basename "$dotfile"
 9  done
10}

Variable expansion (with braces)#

The style guide suggest using “${var}” over “$var”.

Bash#
# An example of when it makes a difference
$ var="Hello"
$ echo "$var_world"
$ echo "${var}_world"
stdout#
Hello world
Bash#
# Another example
$ var="foo"
$ echo "$varbar"
$ echo "${var}bar"
stdout#
foobar
```{code-block} bash
:caption: Bash
# But in many cases are equivalent
$ var="foo"
$ echo "$var bar"
$ echo "${var} bar"
stdout#
foo bar
foo bar

"${var}" is preferred but "$var" is also ok as long as it is used consistently.

Constants and Environment Variable Names#

Constants should be in caps. Separated by underscores. Top of file.

Variables exported to the environment using export, if any, should also be in caps.

If a constant is only meant to be read, it should be declared as readonly for clarity. So the previous bin/install.sh file should be:

───────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
        File: bin/install.sh
───────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1    #!/bin/bash
   2      3    # defaults
   4    readonly DOTFILES_MASTER="$HOME/dotfiles"
   5    readonly INSTALL_PATH="$HOME"
   6      7    cd "$INSTALL_PATH" && ln -s "$DOTFILES_MASTER/bin" .
   8    echo "Installed binaries to $HOME/bin"

STDOUT vs STDERR#

Error messages should go explicitly to STDERR.

This makes it easier to sift out error messages from other messages.

Using the example from testing strings:

example.sh#
 1err() {
 2  echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2
 3}
 4
 5if [[ "${my_var}" ]]; then
 6  echo "my_var exists"
 7else
 8  err "my_var does not exist"
 9  return 1
10fi
Bash#
$ . example.sh
stdout#
[2024-03-22T14:22:32+0800]: my_var does not exist

To confirm that there was error, check the exit status of the last command:

Bash#
echo $?
stdout#
1

One way to sift out error messages is to write errors to a log.

Bash#
$ . example.sh 2> log.txt
stdout#

Bash#
$ cat log.txt
stdout#
───────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────
       │ File: log.txt
───────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────
   1   │ [2024-03-22T14:24:56+0800]: my_var does not exist
───────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────

Resources#


Home Back to homepage.

Notes See more notes.