Bash Magic

Bash is great. Here is a list of bash tricks & knowledge points that I found magical

Posted by Rico's Nerd Cluster on June 15, 2023

Data Structures

Array

Bash has 1D indexed array (the common array we normally see), and associative array. Any variable can be used as an array. To declare an array, use declare.

Bash Array

An indexed array is “common”. However, it doesn’t guarantee that items are stored contiguously.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
declare -a array1 array2    # one can declare multiple variables
array1=("apple" "peach")
echo ${array1[1]}   # see apple,
echo ${array1[@]}   # see apple,peach
array2=("pear")
# $array2 is the first element in array2 only, similar to c.
# array1+=$array2 #applepear peach 

# This is also applepear peach
# array1+="${array2[@]}"

array1+=("${array2[@]}")    #see apple, peach, pear ?
echo ${array1[@]}   # see apple,peach

array1+=("pecan")   
echo ${array1[@]}   #see apple, peach, pear, pecan

Associative Array

A bash associative array is similar to a python dictionary. E.g., below is an excerpt of a script that downloads ROS bags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
declare -A bags_link_lookup #?

# This is very dictionary-like
bags_link_lookup["data/rgbd_dataset_freiburg1_xyz.bag"]="https://cvg.cit.tum.de/rgbd/dataset/freiburg1/rgbd_dataset_freiburg1_xyz.bag"
bags_link_lookup["data/rgbd_dataset_freiburg1_rpy.bag"]="https://cvg.cit.tum.de/rgbd/dataset/freiburg1/rgbd_dataset_freiburg1_rpy.bag"

echo "Download bag files if they are missing ..."
for key in "${!bags_link_lookup[@]}"; do
    if [[ -f "${key}" ]]; then 
        echo "No need to download dataset ${key}..."
    else
        echo "Downloading dataset ${key}"
        wget -P data "${bags_link_lookup[$key]}"
    fi
done
echo "Done Downloading"

Commands

Find

  • find regular files and count their numbers: find . -type f | wc -l
    • find . -type f lists all regular files recursively from the current directory (.)
    • wc -l counts the number of lines

Tree

1
- `tree -L 2`: limits the search depth to 2

Grep

  • grep -B 5 "Something": grep the next 5 lines of each instance
  • grep -Rin vs grep -rin: -r doesn’t allow softlink.

Compound Command?

A “compound command” in Bash is any control‐flow construct that groups multiple simple commands into a single logical unit. Examples of compound commands include:

  • Loops: for …; do …; done
  • Conditionals: if …; then …; fi
  • Grouped commands: { …; } or ( … )
  • Case statements: case …; esac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LIST="$(mktemp)"
# Works
for f in "$DIR"/*.mp4; do
  echo "Found: $f" >&2
  [[ -e "$f" ]] || continue
  printf "file '%s'\n" "$f"
done > "$LIST"

# DOESN'T WORK
for f in "$DIR"/*.mp4; do
  echo $f
  # Skip if no matches
  [[ -e "$f" ]] || continue
  # FFmpeg concat demuxer wants paths in single quotes
  printf "file '%s'\n" "$f"
done > "$LIST"

This is because:

1
2
3
for f in "$DIR"/*.mp4; do
    ... 
done > "$LIST"

the> "$LIST" redirection is attached to the entire for …; do …; done block. All standard‐output (stdout) from anything inside that loop — every echo, printf, or other command that writes to stdout—gets sent into "$LIST"

Every write to the stdout will go to file $LIST. You can either redirect it to stderr >&2, or check out the file: cat $LIST

tee

tee writes to standard input and one more file.

Bind

  • Create a custom keyboard shortcut that triggers a command in shell: (in this case, navi)
1
bind -x '"\C-f": navi'

Command

command -v lcov: command -v checks if this is a command is an executable;

1
if command -v lcov > /dev/null  # makes it go to null

Variables

IFS

IFS: IFS is a special built-in shell variable that controls how Bash splits words. Many commands rely on it implicitly, but the most common place you’ll see it used intentionally is with the read builtin. By default, IFS contains:

1
space, tab, newline

That means when you run:

1
read -ra arr <<< "a b c"

Bash splits the input on whitespace and populates the array accordingly.

Why IFS Matters

The read command is hardcoded to use the variable named exactly IFS for field splitting. You cannot rename it. If you want to change how input is split, you must modify IFS. For example, suppose you have a pipe-delimited string:

1
OVERLAPS="foo|bar|baz"

You can split it like this:

1
IFS='|' read -ra OVERLAP_ARRAY <<< "$OVERLAPS"