str84word avatar

CodeWithStyle - Better bash scripts

str84word

Published: 12 Sept 2018 › Updated: 12 Sept 2018CodeWithStyle - Better bash scripts

CodeWithStyle - Better bash scripts

ProgrammingWithStyle.png

After all it is such a handy language we use every day, often do not realize the power on our fingertips. I list here five tips which may save you a lot of code and improve it's quality and sexiness.
by str84word@str84word

Handle the last slash in path

Sometimes we ask the user to enter a path for the base directory on which we either build some path tree or look for executable. In this situation we can never know either user enters slash at the end of the path or not. Either way, this is really common problem, and often you can see different workarounds. Bash lets you handle this problem with merely one line of code.

E_NOARGS=86
E_BADARG=87
USR_PATH=$1                 # => '~/go/'
USR_PATH="${USR_PATH%/}/"   # => '~/go/' (same as if the input was '~/go')
if [ -d "${USR_PATH}" ]; then
    SRC=${USR_PATH}src/     # => '~/go/src/'
    BIN=${USR_PATH}bin/     # => '~/go/bin/'
    PKG=${USR_PATH}pkg/     # => '~/go/pkg/'
    if [ -d ${SRC} ] && [ -d ${BIN} ] && [ -d ${PKG} ]; then
        echo "${USR_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 send data to /dev/null when the output 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

Variable 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 is also: -x for export, -r (read-only) for constants, -a for array, -i for integers (behaves similar to let but with further scope restriction, see the example).

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 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 wide used syntax for creating an 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=$(itoa 77)       # 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/1041904/
                        # Then compile it to the directory where your
                        # script lives:
                        # gcc -std=c99 itoa.c -o itoa

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

Notice 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=$(<itoa) << do not dare!)

Conclusion

Order is crucial to avoid needless complexity. I hope you now say to yourself, "Wow this dude have style" and then you stick with this thought any time you write bash again but where the 'dude' will be you this time :).

Leave CodeWithStyle - Better bash scripts to:

Written by

Designer, Developer n' Huge Dork.

Read more #bash posts


Best Posts From str84word

We have not curated any of str84word's posts yet. But you can encourage our curation team to review posts by visiting them regularly and by referring other readers. Because we give priority to frequently read content.

More Posts From str84word