Avoidance of external processes

Search and replace

Don't: Waste time and processes using traditional tools such as grep or sed.

I've done this so many times, it's embarrassing.

#!/usr/bin/env bash

for word in "foo" "bar" "baz"
do
    echo $word | grep -q "bar"
    if [[ $? -eq 0 ]]
    then
        echo "$word" | sed 's/bar/BAR/'
    else
        echo "$word"
    fi
done

Do: Use built-in regex matching and/or bash Parameter Expansion.

#!/usr/bin/env bash

for word in "foo" "bar" "baz"
do
    if [[ "$word" =~ bar ]]
    then
        echo "${word//bar/BAZ}"
    else
        echo "$word"
    fi
done

while loop: Variable scope

Don't: Pipe through while loop.

Piping through a while loop creates a subprocess and all variables set within the while loop are forgotten afterwards.

#!/usr/bin/env bash

# Count the lines in some random input

lines=0
dd if=/dev/urandom count=1000 | strings | while read line
do
    let lines+=1
    echo "Read lines: $lines"
done

# Back to outer process: lines is 0 again, oops.
echo "Total lines: $lines"

Do: Use process substition.

<(foo) in places where one would normally read from a file replaces the file with the output from process foo. An additional < for input redirection is still required, just like when reading from a file.

#!/usr/bin/env bash

# Count the lines in some random input

lines=0
while read -r line
do
    let lines+=1
    echo "Read lines: $lines"
done < <(dd if=/dev/urandom count=1000 | strings)

# lines has never been used in a different process scope now.
echo "Total lines: $lines"

looping over results from sed, awk and other streaming editing tools

Don't: Execute awk, sed, tr repeatedly within a loop.

#!/usr/bin/env bash

# Substitute something in some random input
while read -r input
do
    input=$(echo "$input" | sed 's/foo/bar/g')
done < <(dd if=/dev/urandom count=1000)

Do: Avoid repeated execution by making the edit outside the loop.

Execution speed will be significantly higher.

#!/usr/bin/env bash

# Substitute something in some random input
while read -r input
do
    true # nothing
done < <(dd if=/dev/urandom count=1000 | sed 's/foo/bar/g')

See also: Bash native pattern replacement.

#!/usr/bin/env bash

# Native bash pattern replacement

foo="foo"
bar=${foo//foo/bar}

echo $foo $bar

Length of a string

Don't: By all means don't use wc.

If you believe you do have to use wc, don't waste further resources by parsing it's output through awk, but use wc's native option for the byte or character count. I've seen (and DONE!) this countless times and I have no idea if this ever, in any long-forgotten era, was the reasonable thing to do.

#!/usr/bin/env bash

# UTF-8 multibyte characters
string="ÄÖÜ" 
characters=$(echo -n "$string" | wc -m)     # 3 Characters
bytes=$(echo -n "$string" | wc -c)          # 6 Bytes


printf "%s (%s characters, %s bytes)\n" "$string" "$characters" "$bytes"

Do: Use bash Parameter Expansion.

It returns the length in characters, with multi-byte characters counted according to the LANG environment.

#!/usr/bin/env bash

# UTF-8 multibyte characters
string="ÄÖÜ" 
LANG='C.utf8'     characters=${#string}     # 3 Characters
LANG='C'          bytes=${#string}          # 6 Bytes

printf "%s (%s characters, %s bytes)\n" "$string" "$characters" "$bytes"

Execute n times

Don't: Use seq.

#!/usr/bin/env bash

for i in $(seq 1 10)
do
    echo "$i"
done

Do: Use a for loop

...just like you would everywhere else. Don't expect any performance gain, though.

#!/usr/bin/env bash

for (( i=1 ; i<=10 ; i++ ))
do
    echo "$i"
done

bash Loadables

Check out the bash loadables in your OS distribution. Use of these comes at the cost of a loss of portability, though, because the loadables may be installed in differing directories. (Or not at all.)

Without Loadables

#!/usr/bin/env bash

while read -r FILE
do
    BASENAME="$(basename "$FILE")"
    DIRNAME="$(dirname "$FILE")"
    FILESIZE="$(stat --format '%s' "$FILE")"
    printf "%s (%s bytes) in %s\n" "$BASENAME" "$FILESIZE" "$DIRNAME"
done < <(find /usr/lib -type f)

With Loadables

#!/usr/bin/env bash

# Enable loadable bash extensions
BASH_LOADABLES_PATH=/usr/lib/bash:/usr/local/lib/bash
enable -f basename basename
enable -f dirname dirname
enable -f finfo finfo

while read -r FILE
do
    BASENAME="$(basename "$FILE")"
    DIRNAME="$(dirname "$FILE")"
    FILESIZE="$(finfo -s "$FILE")"
    printf "%s (%s bytes) in %s\n" "$BASENAME" "$FILESIZE" "$DIRNAME"
done < <(find /usr/lib -type f)