No Account Yet?

You are not logged in.

Add to: JBookmarks Add to: Facebook Add to: Windows Live Add to: Digg Add to: Del.icoi.us Add to: Reddit Add to: StumbleUpon Add to: Slashdot Add to: Netscape Add to: Furl Add to: Yahoo Add to: Blogmarks Add to: Technorati Add to: Newsvine Add to: Google Information
Bash Tutorial E-mail
Programming HowTo's - Shell HowTo's
Written by Christopher Hahn   
Sunday, 04 May 2008 18:55

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.

Comments
Search RSS
Only registered users can write comments!

3.22 Copyright (C) 2007 Alain Georgette / Copyright (C) 2006 Frantisek Hliva. All rights reserved."

Last Updated ( Sunday, 18 May 2008 07:45 )