Skip to content

๐Ÿ–ฅ๏ธ Bash

Aliases

alias cat='bat'

# LS
alias ls='lsd'
alias la='ls -al'
alias ll='ls -alF'
alias lt='ls --tree'

# Quick edit dotfiles
alias bash_aliases="open ~/.bash_aliases"
alias bashrc="open ~/.bashrc"
alias zshrc="open ~/.zshrc"
alias zprofile="open ~/.zprofile"
alias gitconfig="open ~/.gitconfig"
alias gitattributes="open ~/.gitattributes"

Arrays

  • Definitions:

    Persons=('Alice' 'Bob' 'Charlie')
    Persons[0]="Alice"
    Persons[1]="Bob"
    Persons[2]="Charlie"
    

  • Getters:

    echo "${Persons[0]}"     # Element #0
    echo "${Persons[-1]}"    # Last element
    echo "${Persons[@]}"     # All elements, space-separated
    echo "${#Persons[@]}"    # Number of elements
    echo "${#Persons}"       # String length of the 1st element
    echo "${#Persons[3]}"    # String length of the Nth element
    echo "${Persons[@]:3:2}" # Range (from position 3, length 2)
    echo "${!Persons[@]}"    # Keys of all elements, space-separated
    

  • Setters:

    Persons=("${Persons[@]}" "Dave")         # Push
    Persons+=('Dave')                        # Also Push
    Persons=( "${Persons[@]/Al*/}" )         # Remove by regex match
    unset Persons[2]                         # Remove one item
    Persons=("${Persons[@]}")                # Duplicate
    Persons=("${Persons[@]}" "${Others[@]}") # Concatenate
    Persons=(`cat "persons.txt"`)            # Read from file
    

  • Iterations:

    for p in "${Persons[@]}"; do
      echo "$p"
    done
    

Dictionaries

  • Definitions:

    declare -A sounds
    sounds[dog]="bark"
    sounds[cow]="moo"
    sounds[bird]="tweet"
    sounds[wolf]="howl"
    

  • Getters/Setters:

    echo "${sounds[dog]}" # Dog's sound
    echo "${sounds[@]}"   # All values
    echo "${!sounds[@]}"  # All keys
    echo "${#sounds[@]}"  # Number of elements
    unset sounds[dog]     # Delete dog
    

  • Iterations:

    keys
    for key in "${!dictionary[@]}"; do
      echo "$key"
    done
    

values
for value in "${dictionary[@]}"; do
  echo "$value"
done

Directory of script

dir=${0%/*}

Parameter expansions

  • Substitutions:

    # Remove suffix
    ${foo%suffix}
    # Remove prefix
    ${foo#prefix}
    # Remove long suffix
    ${foo%%suffix}
    # Remove long suffix
    ${foo/%suffix}
    # Remove long prefix
    ${foo##prefix}
    # Remove long prefix
    ${foo/#prefix}
    # Replace first match
    ${foo/old/new}
    # Replace all instances
    ${foo//old/new}
    # Replace suffix
    ${foo/%old/new}
    # Replace prefix
    ${foo/#old/new}
    

  • Substrings:

    ${foo:offset:length}
    # offset from right
    ${foo:(-offset):length}
    

  • Default values:

    # $foo, or val if unset (or null)
    ${foo:-val}
    # set $foo to val if unset (or null)
    ${foo:=val}
    # val if $foo is set (and not null)
    ${foo:+val}
    # print error and exit if $foo is unset (or null)
    ${foo:?error} 
    

  • Misc:

    # length of string or array
    ${#foo}
    # lowercase 1st char
    ${foo,}
    # lowercase all chars
    ${foo,,}
    # uppercase 1st char
    ${foo^}
    # uppercase 1st char
    ${foo^^}
    

Check if command exists

command -v foo >/dev/null || error 'foo is required'
if command -v foo >/dev/null; then
    echo "foo is available"
else
    echo "foo is required"
    exit 1
fi

Command options and positional arguments

Quote

A double dash -- is used in most Bash built-in commands and many other commands to signify the end of command options, after which only positional arguments are accepted.
Example use: Let's say you want to grep a file for the string -v.
Normally -v will be considered the option to reverse the matching meaning (only show lines that do not match), but with -- you can grep for the string -v like this:

grep -- -v file

๐Ÿ”—

Debugging

This DEBUG trap pauses the script execution on each line, prints the command that will be executed, and prompts the user to continue.

#!/usr/bin/env bash
trap 'read -p "[$BASH_SOURCE:$LINENO] $BASH_COMMAND"' DEBUG

echo "Foo"
echo "Bar"
echo "Baz"
./script.sh
[./script.sh:4] echo "Foo"
Foo
[./script.sh:5] echo "Bar"
Bar
[./script.sh:6] echo "Baz"
Baz

Delete empty directories

find . -type d -empty -print -delete

Dynamic command arguments

args=()
[[ -n "$INPUT" ]] && args+=( '--input' "$INPUT" )
foo "${args[@]}"

Find and run commands in parallel

find . -type f -name "*" -print0 | xargs -0 --verbose --max-args=1 --max-procs=0 -I % echo "%"

๐Ÿ”—

Heredoc

General syntax
[cmd] <<[-] delimeter [cmd]
    content
delimeter

Notes

  • <<- instead of << to ignore leading (tab-only) indentation
  • || true to suppress the return code (1 by default)
  • 'EOF' to disable command and variable expansion
  • >/dev/null to not echo content to stdout
Multiline content to variable
read -d '' my_var << EOF || true
โ†“โ†“โ†“
$PWD
โ†‘โ†‘โ†‘
EOF
โ†“โ†“โ†“
/home/user
โ†‘โ†‘โ†‘
Multiline content without expansion to variable
read -d '' my_var << 'EOF' || true
โ†“โ†“โ†“
$PWD
โ†‘โ†‘โ†‘
EOF
โ†“โ†“โ†“
$PWD
โ†‘โ†‘โ†‘
Multiline content to file (overwrite)
tee file << EOF >/dev/null || true
โ†“โ†“โ†“
$PWD
โ†‘โ†‘โ†‘
EOF
Multiline content to file (append)
tee file.txt << EOF >/dev/null || true
โ†“โ†“โ†“
$PWD
โ†‘โ†‘โ†‘
EOF

Inline comment on multiline commands

foo `: # This is a comment` |
  bar `: # This is another comment` |
  baz `: # This is the last comment`

This method uses both the buit-in no-op command : together with the comment character # to support shell with interactive_comments disabled.

jq

Basic filters
# Identity:
.

# Object Identifier-Index:
.foo, .foo.bar

# Optional Object Identifier-Index:
.foo?

# Object Index:
.[<string>] 

# Array Index:
.[<number>] 

# Array/String Slice:
.[<number>:<number>]

# Array/Object Value Iterator:
.[]

# Comma:
,

# Pipe:
|
Builtin operators and functions
# Addition:
+

# Subtraction:
- 

# Multiplication, division, modulo:
*, /, % 

# Length (array, object, string, number, etc.):
length

# Keys of an object as an array:
keys, keys_unsorted

# Presence of the given key:
has(key) 

# Presence of input key in the given object:
in(object)

# Apply f to each of the values in the input array or object:
map(f), map_values(f)

# Convert between an object and an array of key-value pairs:
to_entries, from_entries, with_entries(f)

# Filter input with predicate:
select(f)

# Remove a key and its corresponding value from an object:
del(path_expression)

# Add elements of the input array (summed, concatenated, or merged):
add

# Sort the input array
sort, sort_by(path_expression)

# Group elements by expression 
group_by(path_expression)
Map entries
curl -s 'https://api.github.com/repos/github/.github/commits' \
  | jq '.[] | {message: .commit.message, name: .commit.author.name}'
{
  "message": "Merge pull request #340 from trishanu-init/patch-1\n\nchanged the content structure in CODE_OF_CONDUCT.md",
  "name": "Zack Koppert"
}
{/*...*/}
{
  "message": "Create README.md",
  "name": "Ben Balter"
}
Collect into JSON array
curl -s 'https://api.github.com/repos/github/.github/commits' \
  | jq '[.[] | {message: .commit.message, name: .commit.author.name}]'
[
  {
    "message": "Merge pull request #340 from trishanu-init/patch-1\n\nchanged the content structure in CODE_OF_CONDUCT.md",
    "name": "Zack Koppert"
  },
  {/*...*/},
  {
    "message": "Create README.md",
    "name": "Ben Balter"
  }
]
Nested collect and map
curl -s 'https://api.github.com/repos/github/.github/commits' \
  | jq '[.[] | {message: .commit.message, name: .commit.author.name, parents: [.parents[].html_url]}]'
[
  {
    "message": "Merge pull request #340 from trishanu-init/patch-1\n\nchanged the content structure in CODE_OF_CONDUCT.md",
    "name": "Zack Koppert",
    "parents": [
      "https://github.com/github/.github/commit/d9c05dfc6b02cafe78a6342a73e9e9096273c4fd",
      "https://github.com/github/.github/commit/b41159a31d90042b9e612b3eadeb607ac880ce02"
    ]
  },
  {/*...*/},
  {
    "message": "Create README.md",
    "name": "Ben Balter",
    "parents": [
      "https://github.com/github/.github/commit/d707be9dc0c8e9ebf6e198aa925f89f88486c273"
    ]
  }
]
Group and count
curl -s 'https://api.github.com/repos/github/.github/commits' \
  | jq 'group_by(.commit.author.name) | map({(.[0].commit.author.name): length}) | add'
{
  "Alex Webb": 1,
  "Ashley Wolf": 6,
  "Ben Balter": 3,
  "Daniel Adams": 1,
  "Diana Moore": 1,
  "Dinakar": 1,
  "Fayas Noushad": 1,
  "Justin Hutchings": 1,
  "Matthias Wenz": 1,
  "Nihaal Sangha": 1,
  "Paranoรฏd User": 1,
  "Phil Turnbull": 3,
  "Trishanu Nayak": 2,
  "Zack Koppert": 7
}

Multiline comment

<< 'COMMENT'
  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
  Fusce in justo faucibus, venenatis libero vitae, rutrum sem.
  Donec ut aliquam urna. Nulla facilisi.
COMMENT
: '
  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
  Fusce in justo faucibus, venenatis libero vitae, rutrum sem.
  Donec ut aliquam urna. Nulla facilisi.
'

Merge files together

time {
    echo "โ†“โ†“โ†“"
    find . -type f -name "*.txt" -print0 | xargs -0 cat
    echo "โ†‘โ†‘โ†‘"
} > merged.log

Multiline commands output to variable

RESULT=$( \
    echo "โ†“โ†“โ†“" ;\
    echo "Hello, $RANDOM!" ;\
    echo "โ†‘โ†‘โ†‘" ;\
)

And to capture stderr as well:

RESULT=$(( \
    echo "โ†“โ†“โ†“" ;\
    >&2 echo "Hello, $RANDOM!" ;\
    echo "โ†‘โ†‘โ†‘" ;\
) 2>&1 )

Prefered Bash shebang

#!/usr/bin/env bash

๐Ÿ”—

Process substitution

diff <(ls foo/) <(ls bar/) 

Prune find results

find . -path "*/build" -prune -o -name "*.kt" -print

Remove ANSI escape sequences

sed -e 's/\x1b\[[0-9;]*m//g'

๐Ÿ”—

Run command and ignore result

foo &>/dev/null || :

Safer scripts

#!/usr/bin/env bash
set -euo pipefail

๐Ÿ”—

Split string into array

INPUT="a (b)   \"c\""
IFS=" " read -ra array <<< "$INPUT"
printf '%s\n' "${array[@]}"
a
(b)
"c"

Strip XML comments

cat foo.xml |
  sed 's/<!--/\x0<!--/g;s/-->/-->\x0/g' `: # Add null chars before and after delimiters` |
  grep -zv '^<!--' `: # Set null chars as grep delimiter and invert match` |
  tr -d '\0' `: # Remove null chars` |
  xmllint --format - `: # Optional reformat to remove empty lines`

Write variable to file

echo
echo "$var" > out.txt
printf
printf "%s\n" "$var" > out.txt
here string
cat <<< "$var" > out.txt
here doc
cat << EOF > out.txt
$var
EOF

Special parameters

  • $0 Name of the script
  • $# Number of arguments
  • $* Arguments joined as a String
  • $@ Arguments as an array
  • $? Exit status of last command
  • $! PID of last background command
  • $$ PID of current shell
  • $- Set of option flags in current shell
  • $_ Last argument of the last command

Total size of files

find . -type f -name "*.log" -print0 `: # find all .log files` |
  xargs -0 du --total --human-readable `: # compute total space usage` |
  awk 'END{print $1}' `: # extract total value`