BASH
We've all heard of shell programming, but in the end what we write are simple scripts composed of strung-together commands, performing simple operations in a more-or-less linear manner. But the shell can do much more than what we tend to ask of it.
bash is, in my opinion, the most powerful shell on the market that doesn't require you to be a programmer to use it. On the other hand, for those who want to learn to program it, rather than just script it, its sound principles and powerful features offer a wealth of opportunity for writing fast, flexible programs that can often out-perform most other interpreted languages.
This is a very informal crash tutorial in intermediate features of shell programming. It assumes a basic grasp of programming principles and of simple, beginner-level shell scripting.
1. ENVIRONMENT AND SUBSHELLS:
It may seem to some readers that this needn't be stated, but it needs: one cannot, from within one process, affect the environment of another process, and this also applies to subprocesses affecting their respective parent processes.
The reason this needs to be stated is simply this: in shell programming, it's easy to accidentally create a subshell. The most common example is pipelines. Every pipeline is run in a subshell.
bar=woot cat foo | while read do bar="$REPLY" done echo "$bar"
In the above, the echo will output woot, rather than the last line of the file foo.
Likewise, backticks and $(command) constructs (i.e. command substitution), as well as parenthesised simple and compound commands, are run within subshells.
Every binary you execute actually starts out as a subshell: bash is doing your classic fork/exec we all know and love. The only exception to this is when you use the . or source builtin commands, which require an executable file as argument.
Where high efficiency and paucity of resources are a concern, subshells and even executables should be avoided. Fortunately, bash 3 and beyond are so powerful that most of the things you're used to relying on executables for (cat, grep, cut, sed, awk, netcat, expr, perl) can be done natively.
Normally, variables are not passed on into the environments of subprocesses other than subshells. Only variables marked for export are propagated to the environments of subprocesses, and there are several ways to so mark a variable:
- with the
local, declare, or typeset builtin commands, given the -x flag
- with the
export builtin
- by running
set -a, which will mark all subsequently-defined or -redefined functions and variables
Applying any properties to an undeclared variable will also cause it to be declared, but with a null (empty) value.
The marking may be removed by unsetting the variable or by explicitly removing it using the local, declare, or typeset builtins.
You can define variables that only exist in the child process' environment simply by prefixing the command with the list of variable definitions:
foo=bar baz=woot /bin/bash -c 'echo $baz' > woot echo $foo >
2. SIMPLE AND COMPOUND COMMANDS:
Compound commands are essentially just combinations of simple or compound commands.
They can be formed by parenthesising a list of commands:
( exit 1 )
Bracing them:
{ false; }
Invoking arithmetic evaluation with (( or conditional evaluation with [[,
Or by invoking one of the shell's control flow keywords, like:
if for while until case select
The exit value of any compound command is the exit value of the last simple command executed within it.
Simple commands are the kinds of commands you're used to entering as you move about in a shell, like chdir, rm, cat, grep, et cetera. They may be functions, bash builtins, aliases, or executable files.
3. WORD SPLITTING:
You cannot understand shell programming without understanding word splitting. Word splitting is at the heart of all of your problems when you try to shell out from another program, the reason programmers hate spaces in filenames, and much more.
Every simple command line is split into words. The first word is the command, and the remaining words are each an argument to the command.
The special variable IFS contains the characters on which words are split. Normally this is a space, a tab, and a newline. Any unquoted, unescaped occurrence of any of these characters causes the previous word to be terminated and a new word to begin.
Thus the command:
cat foo bar
Is split into three words, the first of which is treated as the command name. The second and third words are given to cat as its arguments. This is basically the same as in other languages doing something like this:
cat('foo', 'bar')
Or, more to the point:
fork || exec('cat', 'foo', 'bar')
But in these:
cat foo\ bar cat 'foo bar' cat "foo bar" cat quot;foo bar"
cat foo bar'
The space has been escaped in the first instance and quoted in the others, so no word splitting occurs on that space, so the command is analogous to:
exec('cat', 'foo bar')
The same is true for:
cat foo' 'bar cat 'foo 'bar cat fo'o b'ar
And so forth.
4. QUOTING
There are several kinds of quoting available in bash, each with its own uses. The most common are double quotes, eminently useful because many sorts of substitution occur within them just as happens at the command line.
foo=bar; echo "woot '$foo'`echo bang`"
This will output:
woot 'bar'bang
Single quotes are easier to predict:
foo=bar; echo 'woot \"$foo"`echo bang`' > woot \"$foo"`echo bang`
But the difficulty in single quotes is illustrated just there: you cannot escape anything inside of single quotes, because everything, including escape sequences, is already escaped.
foo=bar; echo 'woot \'$foo\' 'bang\' > woot \bar' bang\
The only way to get a literal single quote there is to close the single quotes, escape a single quote, and then reopen the single quotes:
foo=bar; echo 'woot '\''$foo'\'' bang' > woot '$foo' bang
That's where dollared single-quotes come in. These allow escape sequences, but no other substitutions occur.
foo=bar; echo woot \'$foo\' bang' > woot '$foo' bang
Fancy. Even fancier:
echo woot\nfoo\n\tbar' > woot > foo > bar
5. VARIABLES (a.k.a. PARAMETERS):
In other languages, you're probably used to citing a variable and thereby treating the variable itself:
cat($foo)
Regardless of the value of $foo, it's one argument.
In bash, things are more complicated. Much more complicated.
In bash, you're either dealing with a variable or its value.
foo=bar cat $foo > cat: bar: No such file or directory
foo="bar baz" cat $foo > cat: bar: No such file or directory > cat: baz: No such file or directory cat foo > cat: foo: No such file or directory cat "$foo" > cat: bar baz: No such file or directory
Parameter expansion (a.k.a. variable substitution) occurs in line with word splitting, so if the variable contains word splitting characters (one of the characters in the IFS special variable's value), it will get split on those characters unless the variable is expanded into already-opened quotes.
foo='"bar baz"' cat $foo > cat: "bar: No such file or directory > cat: baz": No such file or directory
bash provides for two kinds of variables: scalar and array. Both types of variables may be declared and defined using name=value sets (although for arrays there's a little more to it), and/or by the export, local, declare, and typeset builtins.
5.1. SCALAR VARIABLES:
Scalar variables have, as the name implies, one value. These are the variables you commonly see tossed about in most shell scripts:
foo=bar bar=`echo baz`
It might not seem like there's much to these, but there's a lot more than just foo=bar; echo $foo; For starters, consider this:
thing=3 declare -i stuff=thing+2 echo $stuff > 5 stuff=thing+stuff echo $stuff > 8
That's arithmetic evaluation occurring at time of assignation, thanks to that -i mark we gave to the stuff variable. You don't necessarily need a variable for it, but it can be handy.
echo $((256 % 30)) > 16
Now consider this:
you=Jack echo "$you's bean business ($youbean) was booming." > Jack's bean business () was booming.
bash doesn't look very hard for variables. If the first character isn't a number, it looks for the longest possible identifier and uses it, even if it doesn't identify an already-defined variable.
you=Jack echo "$you's bean business (${you}bean) was booming." > Jack's bean business (Jackbean) was booming.
Using braces on a variable name also lets us introduce all sorts of expansion tricks.
thing="to sleep and perchance to dream"
Let's chop off the first to:
echo "${thing#to}" > sleep and perchance to dream
Or everything up through the last to:
echo "${thing##*to }" > dream
Or just the first two words:
echo "${thing#* * }" > and perchance to dream
We can do the same thing for the end of the variable:
echo "${thing%to*}" > to sleep and perchance echo "${thing%%to*}" > echo "${thing% * *}" > to sleep and perchance
What's more, we can also do searches and replaces with the / expansion:
echo "${thing/to/2}" > 2 sleep and perchance to dream echo "${thing//to/2}" > 2 sleep and perchance 2 dream echo "${thing//e?/WOOT}" > to slWOOTp and pWOOTchancWOOTto drWOOTm
Sometimes we want to use a variable if it's defined, but have some other value if it's not, or has a null value:
undef= echo "${undef:-foo}" > foo
We can even assign it at the same time, which can be handy with the : builtin:
echo "${undef:=foo}" > foo echo "${undef:=what}" > foo undef= : "${undef:=foo}" echo "$undef" > foo
We can even bork if it's null or not defined:
undef= echo "${undef:?BORK}" > -bash: undef: BORK echo $? > 1
Substrings:
echo "${thing:13}" > perchance to dream echo "${thing:13:9}" > perchance
Length:
echo "${#thing}" > 31
Weird combinations:
: "${stuff:="${thing:13}"}" echo "${thing:${#stuff}:13}" > ance to dream
And even other vars:
stuff=thing echo "${!stuff}" > to sleep and perchance to dream
Notice that I quoted the "${thing:13}" inside the default assignment to stuff? It's a good idea to always quote your string expansions. Substitutions still occur inside of parameter expansions, even if they mean far less than they otherwise would. Inside of the expansion, quoting and word splitting are 'reset', so to speak. The quotation marks, rather than closing and reopening the outer quotes, actually opened and closed an inner set of quotes. When ${} and $() are expanded, what's inside is evaluated independently of what contains it, with the only exceptions being arrays and the parameter list, which we'll deal with later.
Mostly this precaution is to ease maintainability, as transforming the code later can get pretty hairy. This doesn't stop it from growing hair, it just makes it easier to shave.
Keep a mental taint flag on all your variables you get from outside -- even if they're from a trusted command's output. Was there an error? Are you SURE you got what you expected? Treat these values with caution -- quote them, protect them. Any value might have a space in it, so quote its expansion where such things make an unwanted difference.
5.1.1. POSITIONAL PARAMETERS (a.k.a. ARGUMENTS):
The positional parameters are usually the arguments passed to the current script or function, with exception for $0, which is the program's idea of what path was used to run it.
Notice that? The program's idea of what path was used to run it. It doesn't necessarily mean that that's the right idea. Don't sudo $0, because $0 could very well be a lie. See exec(3). Hell, you can even use the shell's built-in exec to lie to a command about what it is:
( exec -a killer_sub /bin/bash -c 'echo $0' ) > killer_sub
DON'T RUN $0
K?
The other positional parameters are the arguments to the program, and they're much easier and safer to deal with.
Consider this program, which I call yes:
#!/bin/bash s="${1:-y}" while true; do echo "$s"; done
It works exactly like the yes we all know and love, excepting only the --help and --version arguments, which it doesn't support.
You can use the set builtin to set the positional parameters (except $0):
set -- $thing echo $4 > perchance
Unless you use braces, bash will only look at the first number following the $ to determine which argument you want to retrieve, so to get the tenth argument you have to use braces.
set -- one two three four five six seven eight nine ten eleven echo 0 > one0 echo ${10} > ten
When you're done with an argument and want to dispose of it, you can shift it off:
shift echo > two shift 3 echo > five
The positional parameters are subject to all of the same parameter expansions you saw in the previous section, except :=, which just won't work.
set -- echo "${1:=foo}" > -bash: : cannot assign in this way
Now, if you want to access all of the positional parameters, there are two ways to do it, and each has its place. Consider this:
set -- foo 'foo bar' jazz for i in $*; do echo $i; done > foo > foo > bar > jazz
The $* expansion expands all of the positional parameters separated by spaces. Simple enough, but look what happened to $2 -- it used to be foo bar, but it got word-split.
The other way to get all of the positional parameters is with $@, which operates exactly the same way unless you wrap it in quotes.
for i in $*; do echo $i; done > foo > foo > bar > jazz for i in "$*"; do echo $i; done > foo foo bar jazz for i in $@; do echo $i; done > foo > foo > bar > jazz for i in "$@"; do echo $i; done > foo > foo bar > jazz
You'll almost always want to use the quoted $@ expansion. What's happening here is that each positional parameter is being expanded into a separate word. Think about that: word-splitting is suspended upon the first quotation mark, but after the first positional parameter is expanded, the word is split anyway.
for i in "woot$@jam"; do echo $i; done > wootfoo > foo bar > jazzjam
5.2. ARRAY VARIABLES:
bash provides simple, single-dimensional, numerically-indexed arrays.
You can declare an array using the -a flag to the local, declare, or typeset builtins, and wrapping its value in parentheses:
declare -a foo=( bar baz )
The declare -a isn't strictly necessary:
shaz=( era dotty )
Like the overwhelming majority of programming languages, bash arrays are indexed from zero.
echo ${foo[0]} > bar
You can assign to individual buckets within an array using the common bracket subscription:
foo[23]=jazz
You can use the same subscripting assignment when you initialise the array:
foo=( bar baz [23]=jazz )
bash arrays are also sparse, which means that putting something into the 24th slot doesn't cause the previous 23 slots to suddenly exist.
echo ${#foo[*]} > 3
Dealing with arrays can become rather verbose, because if you don't use braces you're only accessing the first element:
echo $foo[23] > bar[23] echo ${foo[23]} > jazz
Iterating through an array's values is pretty easy:
for i in "${foo[@]}"; do echo "$i"; done > bar > baz > jazz
You can also iterate through its defined indices:
echo ${!foo[*]} > 0 1 23 for i in ${!foo[*]}; do echo "${foo[$i]}"; done > bar > baz > jazz
Notice the use of * and @ so far. Just like the positional parameters, @ and * are the same except when used within double quotes. That is, when you use @ within double-quotes, each value expands into its own word, whereas * will not. This doesn't affect ${#array[@]}, which always expands to just a single number, but it does affect ${!array[@]} (defined indices), and more importantly ${array[@]} (values).
Get a count of the values in an array: ${#array[*]}
Get the list of defined indices in an array: ${!array[*]}
Get the list of values in an array: "${array[@]}"
Arrays are particularly useful for protecting arguments you need to pass on to another program. Let's say we're building a specialised program that happens to use rsync in weird and esoteric ways. We have to build up a list of options we'll be passing to rsync, and some of those options take arguments that we're getting from user input.
User input is like a box of chocolates... we need to protect the spaces the user took care to quote on the way into our arguments list, but we also need to protect ourselves because they might be trying to insert options we don't want in there.
A scalar variable would have to be carefully examined and transformed to be well-sanitised and well-protected without getting mangled.
But we could just shove the options into an array and expand each value into its own word using the quoted @:
rsync "${options[@]}"
It's just as easy as that.
5.3. SPECIAL VARIABLES:
bash has many, many special variables. Look them up in the manpage under Special Parameters and Shell Variables. Only a few are shown here:
$?
- the status of the most-recently-executed foreground command
$$
- the process ID of the current shell (not subshell)
$!
- the process ID the most-recently-executed background command
$IFS
- word-splitting characters
$BASH_REMATCH
- the substring matches when using
[['s built-in regular expression matching
$RANDOM
- a random unsigned 32-bit integer, every time
$USER
- don't trust this! it's not read-only.
$UID
- trust this. it is read-only.
$EUID
- also read-only.
Argument processing (see getopts):
$OPTIND
- the index of the next argument to be processed
$OPTARG
- the value of the argument to the last option processed
Helpful debugging info:
$FUNCNAME
$BASH_ARGC
$BASH_ARGV
$BASH_SUBSHELL
$BASH_SOURCE
$BASH_COMMAND
$BASH_LINENO
$SECONDS
Programmable completion:
$COMP_WORDS
$COMP_CWORD
$COMP_LINE
6. CONTROL FLOW:
6.1. CONDITIONALS:
bash provides a few different ways to make decisions, at the basis of which is the conditional expression.
Conditional expressions simply test the validity of an assertion. You can do this using the [ or [[ builtin commands, of which [ is the more portable, but [[ is (ever so slightly) faster and (much) more flexible. I won't tell you about [, because you've probably seen it all over the place anyway.
You can also simply string commands together using the logical operators && (and) and || (or).
false || echo nasty > nasty true && echo good > good true && false || echo huh > huh true && false || echo what || echo huh > what
The && and || operators have equal precedence, so they're simply evaluated from left to right.
But on to [[ - without getting into listing everything you can do with [[, here's a good taste of it:
From testing files:
[[ -d "$thing" ]] || echo "$thing is not a directory." [[ -e "$thing" ]] || echo "$thing does not exist." [[ -t 0 ]] || stdout_is_terminal=no
To comparing files:
[[ "$file1" -nt "$file2" ]] && echo "$file1 is newer" || echo "$file2 is newer" [[ "$file1" -ef "$file2" ]] && echo "$file1 is the same thing as $file2. hardlinks?"
To testing and comparing variables:
[[ "$var1" = "$var2" ]] && echo "var1 and var2 are the same" [[ "$var1" -gt "$var2" ]] && echo "$var1 is a bigger number than $var2" [[ "$var" ]] || echo "var is null or not defined." [[ -n "$var" ]] || echo "var is null or not defined." [[ -z "$var" ]] && echo "var is null or not defined."
Glob pattern matches (don't quote the glob!):
[[ "$var" == f* ]] && echo "$var starts with f"
Regular expression pattern matches (quote the re!):
[[ "$var" =~ '(.).*(.) ]] && echo "$var starts with ${BASH_REMATCH[1]} and ends with ${BASH_REMATCH[2]}"
Combine conditionals:
[[ "$abspath1" -ef "$abspath2" && "$abspath1" != "$abspath2" ]] && echo "$abspath1 and $abspath2 are hardlinks to the same file." [[ -t 0 || "$act_like_tty" ]] || suppress_curses=1
And so forth.
6.2. IF:
if is the simplest control flow keyword. It takes a simple or compound command as the condition and executes the then list if the commands exits 0, or the else list otherwise.
if ! grep -qv '^#' "file.conf"; then echo "file.conf doesn't have any instructions." exit 1 elif [[ -t 0 ]]; then echo "output is not to a terminal." exit 1 else echo "Ready." fi
6.3. WHILE AND UNTIL:
while executes a list of commands until its conditional expression becomes invalid.
while [[ ${c:=0} -lt 5 ]]; do c=$((++c)) echo -n "$c " done
until executes the list until the conditional becomes valid.
until [[ ${c:=0} -eq 4 ]]; do c=$((++c)) echo -n "$c " done
6.4. FOR:
for has two ways of operating. The most common form executes a list of commands for each item in a sequence, a la foreach:
for c in 1 2 3 4; do echo -n "$c " done
The other, less common form mirrors the for construct in most other languages:
for ((c=0; c < 5; c++)); do echo -n "$c " done
Here, the (( ; ; )) construct is three stanzas of arithmetic expressions. The first is evaluated prior to the first iteration over the command list list, and prior to the second stanza; the second is evaluated immediately prior to every iteration, including the first iteration; the third is evaluated immediately after each iteration. If the second stanza evaluates to a positive number (zero is neither positive nor negative, remember), iteration continues; otherwise, iteration stops and the program resumes at the next instruction after the done.
6.5. CASE:
case allows you to compare a value to a series of glob patterns, executing a list of commands upon a match.
case "$var" in file) file="$var" ;; f*) echo "$var starts with f, but isn't a file." ;; o*) options[${#options[*]}]="$var" ;; *) echo "i don't understand '$var'." exit 1 ;; esac
You'll commonly see case statements in init scripts:
# see how we were called case "" in start|stop|status) ;; restart) status &>/dev/null && stop sleep 1 status &>/dev/null || start ;; *) usage ;; esac
And in options processing:
OPTIND=0 while getopts 'ac:lu:' opt; do case "$opt" in a) all=1 ;; c) columns[${#columns[*]}]="$OPTARG" ;; l) : $((++local)) ;; u) user="$OPTARG" ;; esac done shift "$((OPTIND-1))"
6.6. SELECT:
select implements a basic text menu loop system. Given a list of words, a list of numbered menu items is produced on STDERR and the user is prompted (a la $PS3) to select one. When a selection is made, the response is stored in $REPLY, the list of commands is executed, and the prompt is redisplayed.
If the response was null, the menu is redisplayed, followed by the prompt. If an EOF is received, the loop ends and execution is resumed at the next instruction following done.
Who="The butler" What="A lead pipe" Where="The cellar" When="Two hours ago" How="Brutally" hmm=( Who What Where When How ) PS3="Ask: " select item in "${hmm[@]}"; do : "${item:=$REPLY}" echo "${!item}" done
6.7. FUNCTIONS AND ALIASES:
Aliases are just different ways to enter commands.
alias foo="echo what" foo shoot > what shoot
They're nice to have around for those times when all you need to do is change around which command gets run.
[[ -x ssh ]] && alias rsh=ssh rsh $host hostname
But when you need more, but it still doesn't make sense to create a whole other script to do something... functions.
function foo () { echo bar }
The function keyword is optional here, but I always use it so it'll be easier to find in my editors.
Functions are named compound commands (go back and read about compound commands).
function woot () if true; then echo true; else echo false; fi woot > true
They allow you to privately process arguments and define local variables.
function yes () { local s="${1:-y}" x=shoo echo "$s" }
yes no > no echo $s > echo $x > shoo
They can return their own exit status:
function false () { return 1; } type -t false > function false echo $? > 1
And you can make them run in subshells to implicitly protect the parent environment:
x=n function killer_sub () ( x=y echo $x )
killer_sub > y echo $x > n
You can even perform input and output redirections on the entire compound command, and these redirections will occur when the compound command runs:
function night () { cat; } < bump
7. REDIRECTIONS:
You're probably used to seeing some redirections:
echo foo > bar cat /dev/null > logfile script.pl >/dev/null 2>&1
That's all well and good for saving or disposing of output, but it's only the tip of the iceberg.
Consider the example at the very top of this document:
bar=woot cat foo | while read do bar="$REPLY" done echo "$bar"
If you really want the while loop to be defining bar for use outside the loop, you have to eliminate the subshell created by the pipeline.
bar=woot while read; do bar="$REPLY" done < foo echo "$bar"
Now the value of bar is the last line of the file foo.
But what if you need, for instance, the output of a command? Combine redirection with process subsitution and you get:
while read; do bar="$REPLY" done < <(grep -v "^#" foo)
Now bar is the last uncommented line from the file foo.
You can duplicate file descriptors easily enough:
script.pl >/dev/null 2>&1
You just duplicated 2 (STDERR) to 1 (STDOUT), which was already redirected to /dev/null, thus disposing of both. There's shorthand for this:
script.pl &>/dev/null
But if you did:
script.pl 2>&1 >/dev/null
Now you get what would have gone to STDERR coming out of STDOUT, and you're ignoring what otherwise would have gone out of STDOUT.
Let's switch them around:
script.pl 3>&2 2>&1 1>&3
Now errors go out STDOUT and normal output comes through STDERR. First we duplicated 2 and called it 3. Thus, 3 now goes out STDERR. Then we duplicated 1 to 2, so error output goes out STDOUT. Then we duplicated 3 to 1, so that normal output goes out STDERR.
Don't do that. It's hard to follow.
Put redirections before pipes, because pipes redirect STDOUT of the command at left to STDIN of the command at right -- you can't redirect it after that, because parsing of the left command ends at the pipe.
echo foo | grep foo > foo echo foo | grep foo <<<foobar > foobar
There are other interesting redirections:
echo error >/dev/stderr echo error >/dev/2 echo something >/dev/fd/5 echo forgrep > >(grep for >file)
Run a program and send it input whenever we please:
fd=>(grep for >file) echo some text >$fd # do some other stuff, then echo some more text >$fd
Give a variable to a program's STDIN:
grep foo <<<"$thing"
Open a file:
exec 13<inputfile exec 14>outputfile read -u 13 line echo "$line" <&14 echo "that's all she wrote" >/dev/fd/14
Riddle me this:
exec 15<>inoutinout echo foo >&15 cat <&15 cat /dev/fd/15 > foo
What happened? The file descriptor's internal pointer was still set at the end of the file, so telling bash to read from it produced nothing. But the cat /dev/fd/15 isn't telling cat to read file descriptor 15 -- it's telling it to open the file at the given path. New file descriptor = new position, so it read on through.
This behaviour may differ on some systems, where separate reading and writing positions are maintained for each file descriptor.
Where this sort of thing really comes in handy is with sockets. Yes, sockets.
7.1. SIMPLE SOCKETS:
bash supports simple communication over both tcp and udp, using the fake /dev/tcp and /dev/udp paths.
By 'fake' I mean that these paths needn't actually exist, even in bash's world. Instead, bash looks for them when they are used in redirection, but ONLY when they are used in redirection.
Let's find out what our gmond says about the health of our remote host $host.
cat </dev/tcp/$host/8049
Or we could have a brief affair with an HTTP daemon somewhere out in the wild.
exec 16<>/dev/tcp/$host/80 echo "GET / HTTP/1.1 Host: $host Connection: close
" >&16 # we don't want the headers... while read -u 16 && [[ "$line" ]]; do continue; done cat <&16
Crazy, ain't it?
8. OTHER TIPS AND TRICKS:
Don't use a #!/bin/sh shebang when you're writing bash scripts. On many systems, #!/bin/sh could be ksh or actually sh. Only write sh scripts with #!/bin/sh -- bash scripts should be #!/bin/bash, ksh should be #!/bin/ksh, and so forth.
Some of the things discussed here are specific to bash 3+ (regexes, herestrings). You might do well to check the $BASH_VERSION.
Read a file into a variable:
contents="$(< file)"
Avoid nasty escaping in nested backticks by simply avoiding backticks:
var="$(cmd1 "$(cmd2 "arg 1" arg2)")"
Forking is easy thanks to job control:
bg_command & fg_command wait
The =~ comparison operator obviates many needs for grep, but don't expect it to work in bash < 3.
The $(< file) and redirection operators obviate most needs for cat.
Multi-line strings are fine-- just use them, and they work.
Use <<<"herestrings" instead of piping echo.
Use -r when you read from a file.
Use the type builtin to find out what a command really is.
Set vars to readonly when they should be: readonly varname
eval when you need to-- there's nothing wrong with it, but be careful how you use it.exec when it's good for you.
DON'T RUN $0 -- did I mention that already?
Quote as much as you can. It's safer than not, and users will appreciate it when they have to give input with spaces but the script still works.
8.1. DEBUGGING:
Check out the following options to the set builtin:
-uvETxe
Take a look at 5.3: SPECIAL VARIABLES
And the things you can do with the trap builtin and the pseudo-signals DEBUG, ERR, RETURN, and EXIT.
|