Better Bash Scripts: Code With Style EP1

in bash •  4 years ago  (edited)

header-image.png

Let's start with the black space where we all have been, the shell. Unix systems have revolutionized how we think about computing those days. Monolithic machines made for single purpose computing have been replaced with reliable small layers [fig1], one of witch is the shell. It consists of small programs, usually solving single task. The most popular shell those days is bash, so this is what we gonna play with today.
by @lordrubish

fig_1-UnixLayeredModel.png
fig1: Simplified model of Unix layers

Handle trailing slash

When asking the user to enter a path we can never know if user enters slash at the end of the path or not. Path validation is a very common problem in shell, and so you can find many workarounds. However, Bash lets you handle this problem with merely one line of code.

path="${1%/}/" => 'user-path/'


You may wander now, "What just happened? Is not % the modulo operator for getting the reminder of division?". Yes, but only when given an integer. $1 is a string retrieved from the first argument and ${..} is a syntax for manipulating strings in Bash. Finally % is the string operator which delete the part that match the pattern after it.

So if slash is present in the given string, it's gonna be removed by the % operator and replaced with the slash at the end of the string quote.

For example usage, let's write a simple script checking out if user defined path is a GO directory.

E_NOARGS=86
E_BADARG=87

path=$1                       # => '~/go/'
path="${path%/}/"             # => '~/go/' (same, as if the input was '~/go')
if [ -d "${path}" ]; then
    SRC=${path}src/           # => '~/go/src/'
    BIN=${path}bin/           # => '~/go/bin/'
    PKG=${path}pkg/           # => '~/go/pkg/'
    if [ -d ${SRC} ] && [ -d ${BIN} ] && [ -d ${PKG} ]; then
        echo "${path} is a proper GOPATH repository."
    else
        exit $E_BADARG
    fi
else
    exit $E_NOARGS
fi
# ...

Clear the file content

You probably know that one can redirect a program output to '/dev/null' when it is not significant .

mv * ./src 2> /dev/null

But did you know that the reverse (sending '/dev/null' output to the file) works as well? In result we get an easy mechanism to clear logs and releasing some computation data. There is however shorter syntax for doing just that.

cat /dev/null > computation.log
: > computation.log           # Same as above
> computation.log             # Still same behavior

Check if a program exists on the $PATH

This one is fairly simple solution, yet somebody may try to reinvent the wheel and manually check the $PATH. Luckily the type command with -p argument is returning either the path string (which evaluates to true in conditional expression) or nothing (which evaluates to false).

dep=java
if ! `type -p ${dep} > /dev/null`; then
    echo "Sorry, you need ${dep} to run my app."
    exit 1;
fi

Variables type and closure

The way to extract a variable to a child process, that most programmers stick to, is the export method. Local scope is often handled by the local keyword which delimiters a variable only to the local process. Also you can merely see bash scripts which are using constants.

Well, there is only one keyword you need to remember, giving a solution to all of above problems and more, it is quite like panacea for bash variable manipulation. Creating a variable simply using the declare is restricting it to the given scope, causing closure effect. Useful to remember are also: -x for export, -r (read-only) for constants, -a for array, -i for integers. declare behaves similar to let but with further scope restriction, see the example bellow.

foo () { 
    local let x=10/2
    declare -i -r y=10/2      # => 5
}

access () { 
    foo
    echo $x                   # => 5
    echo $y                   # => (nothing)
}

Command substitution

This is a powerful concept that often seems to be misunderstood. Those are kind-of anonymous blocks of code. The block evaluation output can be plugged in to a command. Command substitutions can be also assigned to a variable for later use. For an simple example, sending ls -ltr trough the command substitution will result in a nice formatted ls output echoed to stdin.

echo "$(ls -ltr)"             # You need to escape the command substitution
                              # with quote, otherwise echo will eat newlines.

The widely used syntax for creating a command substitution is trough the backticks `...`. Hover the $(...) has superseded the backticks syntax as it comes with few benefits, one of which is nesting command substitutions. Let's try actually something fun, like signing a loop to a variable.

names=('Alice' 'Bob' 'Charlie')

listNames=$(for i in $(seq 0 $(expr ${#names[@]} - 1)); do
    declare -i index=${i}+1
    echo -n "${index}.${names[${i}]} "
done)

names=( 'Dave' 'Erin' 'Frank')

echo "Participants: ${listNames}" # => Participants: 1.Alice 2.Bob 3.Charlie 

Command substitution is ideal tool for expending bash tool-set. You can plug outside scripts, programs written in any language, or assign a code from a file very easily.

fork_c=$(itosym 3)            # Now $fork_c will execute the C program
                              # from ./ directory. You can test it by
                              # downloading a C code snippet from:
                              # https://paste.debian.net/1189532/. Then
                              # move the downloaded file to the
                              # directory where your script lives and
                              # compile it using:
                              # gcc -std=c99 itosym.c -o itosym

file_definition=$(<itosym.c)  # Get content of the 'itosym' source code
self_definition=$(<$0)        # Save the content of the scripts itself

Note from ABS Guide:

Do not set a variable to the contents of a long text file unless you have a very good reason for doing so. Do not set a variable to the contents of a binary file, even as a joke.

bit=$(<itosym) <= Don't dare!

Conclusion

Order is crucial to avoid needless complexity. Bash have all the tools to write code with style and order, there is no need for sacrificing local scope or constant correctness in favor of shell power. At the same time we can write our scripts short and readable by using common techniques.

I hope this article have helped you to make your bash scripts look prettier. Happy hacking!




Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!