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
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!