diff --git a/bash.md b/bash.md index bdc26d85..159aab65 100644 --- a/bash.md +++ b/bash.md @@ -1,5 +1,5 @@ --- -title: Bash scripting +title: Bash date: 2020-11-25 18:28:43 icon: icon-bash background: bg-gray-400 @@ -16,57 +16,74 @@ intro: This is a quick reference (cheat sheet) to getting started with Bash scri Getting started {.cols-3} --------------- -### Introduction {.-intro} - -This is a quick reference to getting started with Bash scripting. - -- [Learn bash in y minutes](https://learnxinyminutes.com/docs/bash/) _(learnxinyminutes.com)_ -- [Bash Guide](http://mywiki.wooledge.org/BashGuide) _(mywiki.wooledge.org)_ - -### Example +### hello.sh ```bash -#!/usr/bin/env bash +#!/bin/bash -NAME="John" -echo "Hello $NAME!" +VAR="world" +echo "Hello $VAR!" # => Hello world! ``` +Execute the script +```shell script +$ bash hello.sh +``` + ### Variables ```bash NAME="John" -echo $NAME -echo "$NAME" -echo "${NAME}!" + +echo ${NAME} # => John +echo $NAME # => John +echo "$NAME" # => John +echo '$NAME' # => $NAME +echo "${NAME}!" # => John! + +NAME = "John" # => Error (about space) ``` -### String quotes + + +### Comments ```bash -NAME="John" -echo "Hi $NAME" #=> Hi John -echo 'Hi $NAME' #=> Hi $NAME +# This is an inline Bash comment. ``` -### Shell execution - ```bash -echo "I'm in $(pwd)" -echo "I'm in `pwd`" -# Same +: ' +This is a +very neat comment +in bash +' ``` +Multi-line comments use `:'` to open and `'` to close -See [Command substitution](http://wiki.bash-hackers.org/syntax/expansion/cmdsubst) -### Conditional execution -```bash -git commit && git push -git commit || echo "Commit failed" -``` -### Functions {#functions-example} +### Arguments {.row-span-2} + +| Expression | Description | +| --- | --- | +| `$1` … `$9`| Parameter 1 ... 9 | +| `$0` | Name of the script itself | +| `$1` | First argument | +| `${10}` | Positional parameter 10 | +| `$#` | Number of arguments | +| `$$` | Process id of the shell | +| `$*` | All arguments | +| `$@` | All arguments, starting from first | +| `$-` | Current options | +| `$_` | Last argument of the previous command | + +See: [Special parameters](http://wiki.bash-hackers.org/syntax/shellvars#special_parameters_and_shell_variables). + + + +### Functions ```bash get_name() { @@ -90,20 +107,12 @@ fi See: [Conditionals](#conditionals-2) -### Strict mode - -```bash -set -euo pipefail -IFS=$'\n\t' -``` - -See: [Unofficial bash strict mode](http://redsymbol.net/articles/unofficial-bash-strict-mode/) - ### Brace expansion ```bash echo {A,B}.js ``` +--- | Expression | Description | | ---------- | ------------------- | @@ -113,30 +122,60 @@ echo {A,B}.js See: [Brace expansion](http://wiki.bash-hackers.org/syntax/expansion/brace) +### Shell execution + +```bash +echo "I'm in $(PWD)" +# Same +echo "I'm in `pwd`" +``` + +See: [Command substitution](http://wiki.bash-hackers.org/syntax/expansion/cmdsubst) + + Parameter expansions {.cols-3} -------------------- -### Basics {.row-span-4} -```bash -name="John" -echo ${name} -echo ${name/J/j} #=> "john" (substitution) -echo ${name:0:2} #=> "Jo" (slicing) -echo ${name::2} #=> "Jo" (slicing) -echo ${name::-1} #=> "Joh" (slicing) -echo ${name:(-1)} #=> "n" (slicing from right) -echo ${name:(-2):1} #=> "h" (slicing from right) -echo ${food:-Cake} #=> $food or "Cake" -``` + +### Syntax {.row-span-2} + +| Code | Description | +| ----------------- | ------------------- | +| `${FOO%suffix}` | Remove suffix | +| `${FOO#prefix}` | Remove prefix | +| `${FOO%%suffix}` | Remove long suffix | +| `${FOO##prefix}` | Remove long prefix | +| `${FOO/from/to}` | Replace first match | +| `${FOO//from/to}` | Replace all | +| `${FOO/%from/to}` | Replace suffix | +| `${FOO/#from/to}` | Replace prefix | +#### Substrings +| Expression | Description | +| --------------- | ------------------------------ | +| `${FOO:0:3}` | Substring _(position, length)_ | +| `${FOO:(-3):3}` | Substring from the right | +#### Length +| Expression | Description | +| ---------- | ---------------- | +| `${#FOO}` | Length of `$FOO` | +#### Default values +| Expression | Description | +| ----------------- | -------------------------------------------------------- | +| `${FOO:-val}` | `$FOO`, or `val` if unset | +| `${FOO:=val}` | Set `$FOO` to `val` if unset | +| `${FOO:+val}` | `val` if `$FOO` is set | +| `${FOO:?message}` | Show message and exit if `$FOO` is unset | + + + +### Substitution ```bash -length=2 -echo ${name:0:length} #=> "Jo" +echo ${food:-Cake} #=> $food or "Cake" ``` -See: [Parameter expansion](http://wiki.bash-hackers.org/syntax/pe) ```bash STR="/path/to/foo.cpp" @@ -153,308 +192,230 @@ echo ${STR##*/} # foo.cpp echo ${STR/foo/bar} # /path/to/bar.cpp ``` -```bash -STR="Hello world" -echo ${STR:6:5} # "world" -echo ${STR: -5:5} # "world" -``` -```bash -SRC="/path/to/foo.cpp" -BASE=${SRC##*/} #=> "foo.cpp" (basepath) -DIR=${SRC%$BASE} #=> "/path/to/" (dirpath) -``` +### Slicing -### Substitution {.row-span-3} +```bash +name="John" +echo ${name} # => John +echo ${name:0:2} # => Jo +echo ${name::2} # => Jo +echo ${name::-1} # => Joh +echo ${name:(-1)} # => n +echo ${name:(-2)} # => hn +echo ${name:(-2):2} # => hn -| Code | Description | -| ----------------- | ------------------- | -| `${FOO%suffix}` | Remove suffix | -| `${FOO#prefix}` | Remove prefix | -| `${FOO%%suffix}` | Remove long suffix | -| `${FOO##prefix}` | Remove long prefix | +length=2 +echo ${name:0:length} # => Jo +``` +See: [Parameter expansion](http://wiki.bash-hackers.org/syntax/pe) -| `${FOO/from/to}` | Replace first match | -| `${FOO//from/to}` | Replace all | -| `${FOO/%from/to}` | Replace suffix | -| `${FOO/#from/to}` | Replace prefix | -### Comments +### basepath & dirpath ```bash -# Single line comment +SRC="/path/to/foo.cpp" ``` ```bash -: ' -This is a -multi line -comment -' +BASEPATH=${SRC##*/} +echo $BASEPATH # => "foo.cpp" + + +DIRPATH=${SRC%$BASEPATH} +echo $DIRPATH # => "/path/to/" ``` -### Substrings -| Expression | Description | -| --------------- | ------------------------------ | -| `${FOO:0:3}` | Substring _(position, length)_ | -| `${FOO:(-3):3}` | Substring from the right | -### Length -| Expression | Description | -| ---------- | ---------------- | -| `${#FOO}` | Length of `$FOO` | -### Manipulation + +### Transform ```bash STR="HELLO WORLD!" -echo ${STR,} #=> "hELLO WORLD!" (lowercase 1st letter) -echo ${STR,,} #=> "hello world!" (all lowercase) +echo ${STR,} # => hELLO WORLD! +echo ${STR,,} # => hello world! STR="hello world!" -echo ${STR^} #=> "Hello world!" (uppercase 1st letter) -echo ${STR^^} #=> "HELLO WORLD!" (all uppercase) +echo ${STR^} # => Hello world! +echo ${STR^^} # => HELLO WORLD! + +ARR=(hello World) +echo "${ARR[@],}" # => hello world +echo "${ARR[@]^}" # => Hello World ``` -### Default values -| Expression | Description | -| ----------------- | -------------------------------------------------------- | -| `${FOO:-val}` | `$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:?message}` | Show error message and exit if `$FOO` is unset (or null) | -Omitting the `:` removes the (non)nullity checks, e.g. `${FOO-val}` expands to `val` if unset otherwise `$FOO`. -Loops {.cols-3} ------ +Arrays {.cols-3} +------ -### Basic for loop +### Defining arrays ```bash -for i in /etc/rc.*; do - echo $i -done -``` - -### C-like for loop +Fruits=('Apple' 'Banana' 'Orange') -```bash -for ((i = 0 ; i < 100 ; i++)); do - echo $i -done -``` +Fruits[0]="Apple" +Fruits[1]="Banana" +Fruits[2]="Orange" -### Ranges {.row-span-2} +ARRAY2=(foo{1..2}) # => foo1 foo2 +ARRAY3=({A..D}) # => A B C D -```bash -for i in {1..5}; do - echo "Welcome $i" -done +# declare construct +declare -a Numbers=(1 2 3 4 5 6) ``` -#### With step size -```bash -for i in {5..50..5}; do - echo "Welcome $i" -done -``` +### Indexing +| - | - | +|--------------------|-----------------------| +| `${Fruits[0]}` | First element | +| `${Fruits[-1]}` | Last element | +| `${Fruits[*]}` | All elements | +| `${Fruits[@]}` | All elements | +| `${#Fruits[@]}` | Number of all | +| `${#Fruits}` | Length of 1st | +| `${#Fruits[3]}` | Length of nth | +| `${Fruits[@]:3:2}` | Range | +| `${!Fruits[@]}` | Keys of all | -### Auto increment + +### Iteration ```bash -i=1 -while [[ $i -lt 4 ]]; do - echo "Number: $i" - ((i++)) +Fruits=('Apple' 'Banana' 'Orange') + +for e in "${Fruits[@]}"; do + echo $e done ``` - -### Auto decrement - +#### With index ```bash -i=3 -while [[ $i -gt 0 ]]; do - echo "Number: $i" - ((i--)) +for i in "${!Fruits[@]}"; do + printf "%s\t%s\n" "$i" "${Fruits[$i]}" done -``` - - -### Continue {.col-span-2} -```bash {data=3,5} -for number in $(seq 1 3); do - if [[ $number == 2 ]]; then - continue; # Skip rest of this particular loop iteration. - fi - echo "$number" # This will not print 2 -done ``` -### Break +### Operations {.col-span-2} ```bash -for number in $(seq 1 3); do - if [[ $number == 2 ]]; then - # Skip entire rest of loop. - break; - fi - # This will only print 1 - echo "$number" -done +Fruits=("${Fruits[@]}" "Watermelon") # Push +Fruits+=('Watermelon') # Also Push +Fruits=( ${Fruits[@]/Ap*/} ) # Remove by regex match +unset Fruits[2] # Remove one item +Fruits=("${Fruits[@]}") # Duplicate +Fruits=("${Fruits[@]}" "${Veggies[@]}") # Concatenate +lines=(`cat "logfile"`) # Read from file ``` - - -### Reading lines - +### Arrays as arguments ```bash -cat file.txt | while read line; do - echo $line -done +function extract() +{ + local -n myarray=$1 + local idx=$2 + echo "${myarray[$idx]}" +} +Fruits=('Apple' 'Banana' 'Orange') +extract Fruits 2 # => Orangle ``` -### Forever -```bash -while true; do - # here is some code. -done -``` -or -```bash -while :; do - # here is some code. -done -``` -Functions {.cols-3} ---------- -### Defining functions +Dictionaries {.cols-3} +------------ -```bash -myfunc() { - echo "hello $1" -} -``` +### Defining ```bash -# Same as above (alternate syntax) -function myfunc() { - echo "hello $1" -} +declare -A sounds ``` ```bash -myfunc "John" +sounds[dog]="bark" +sounds[cow]="moo" +sounds[bird]="tweet" +sounds[wolf]="howl" ``` -### Returning values -```bash -myfunc() { - local myresult='some value' - echo $myresult -} -``` +### Working with dictionaries ```bash -result="$(myfunc)" +echo ${sounds[dog]} # Dog's sound +echo ${sounds[@]} # All values +echo ${!sounds[@]} # All keys +echo ${#sounds[@]} # Number of elements +unset sounds[dog] # Delete dog ``` -### Raising errors +### Iteration ```bash -myfunc() { - return 1 -} +for val in "${sounds[@]}"; do + echo $val +done ``` - +--- ```bash -if myfunc; then - echo "success" -else - echo "failure" -fi +for key in "${!sounds[@]}"; do + echo $key +done ``` -### Arguments -| Expression | Description | -| --- | --- | -| `$#` | Number of arguments | -| `$*` | All arguments | -| `$@` | All arguments, starting from first | -| `$1` | First argument | -| `$_` | Last argument of the previous command | -See [Special parameters](http://wiki.bash-hackers.org/syntax/shellvars#special_parameters_and_shell_variables). Conditionals {.cols-3} ------------ -### Conditions - -Note that `[[` is actually a command/program that returns either `0` (true) or `1` (false). Any program that obeys the same logic (like all base utils, such as `grep(1)` or `ping(1)`) can be used as condition, see examples. +### Integer conditions | Condition | Description | | --- | --- | -| `[[ -z STRING ]]` | Empty string | -| `[[ -n STRING ]]` | Not empty string | -| `[[ STRING == STRING ]]` | Equal | -| `[[ STRING != STRING ]]` | Not Equal | - -| `[[ NUM -eq NUM ]]` | Equal | -| `[[ NUM -ne NUM ]]` | Not equal | -| `[[ NUM -lt NUM ]]` | Less than | -| `[[ NUM -le NUM ]]` | Less than or equal | -| `[[ NUM -gt NUM ]]` | Greater than | -| `[[ NUM -ge NUM ]]` | Greater than or equal | +| `[[ NUM -eq NUM ]]` | Equal | +| `[[ NUM -ne NUM ]]` | Not equal | +| `[[ NUM -lt NUM ]]` | Less than | +| `[[ NUM -le NUM ]]` | Less than or equal | +| `[[ NUM -gt NUM ]]` | Greater than | +| `[[ NUM -ge NUM ]]` | Greater than or equal | +| `(( NUM < NUM ))` | Less than | +| `(( NUM <= NUM ))` | Less than or equal | +| `(( NUM > NUM ))` | Greater than | +| `(( NUM >= NUM ))` | Greater than or equal | -| `[[ STRING =~ STRING ]]` | Regexp | -| `(( NUM < NUM ))` | Numeric conditions | +### String conditions -#### More conditions +| Condition | Description | +| --- | --- | +| `[[ -z STR ]]` | Empty string | +| `[[ -n STR ]]` | Not empty string | +| `[[ STR == STR ]]` | Equal | +| `[[ STR = STR ]]` | Equal (Same above) | +| `[[ STR < STR ]]` | Less than _(ASCII)_ | +| `[[ STR > STR ]]` | Greater than _(ASCII)_ | +| `[[ STR != STR ]]` | Not Equal | +| `[[ STR =~ STR ]]` | Regexp | -| Condition | Description | -| -------------------- | ------------------------ | -| `[[ -o noclobber ]]` | If OPTIONNAME is enabled | -| `[[ ! EXPR ]]` | Not | -| `[[ X && Y ]]` | And | -| `[[ X || Y ]]` | Or | -### File conditions -| Condition | Description | -| --- | --- | -| `[[ -e FILE ]]` | Exists | -| `[[ -r FILE ]]` | Readable | -| `[[ -h FILE ]]` | Symlink | -| `[[ -d FILE ]]` | Directory | -| `[[ -w FILE ]]` | Writable | -| `[[ -s FILE ]]` | Size is > 0 bytes | -| `[[ -f FILE ]]` | File | -| `[[ -x FILE ]]` | Executable | -| `[[ FILE1 -nt FILE2 ]]` | 1 is more recent than 2 | -| `[[ FILE1 -ot FILE2 ]]` | 2 is more recent than 1 | -| `[[ FILE1 -ef FILE2 ]]` | Same files | -### Example +### Example {.row-span-3} #### String ```bash @@ -502,126 +463,268 @@ if [[ -e "file.txt" ]]; then fi ``` -Arrays {.cols-2} ------- -### Defining arrays -```bash -Fruits=('Apple' 'Banana' 'Orange') -``` + +### File conditions {.row-span-2} + +| Condition | Description | +| --- | --- | +| `[[ -e FILE ]]` | Exists | +| `[[ -d FILE ]]` | Directory | +| `[[ -f FILE ]]` | File | +| `[[ -h FILE ]]` | Symlink | +| `[[ -s FILE ]]` | Size is > 0 bytes | +| `[[ -r FILE ]]` | Readable | +| `[[ -w FILE ]]` | Writable | +| `[[ -x FILE ]]` | Executable | +| `[[ f1 -nt f2 ]]` | f1 newer than f2 | +| `[[ f1 -ot f2 ]]` | f2 older than f1 | +| `[[ f1 -ef f2 ]]` | Same files | + + +### More conditions + +| Condition | Description | +| -------------------- | ------------------------ | +| `[[ -o noclobber ]]` | If OPTIONNAME is enabled | +| `[[ ! EXPR ]]` | Not | +| `[[ X && Y ]]` | And | +| `[[ X || Y ]]` | Or | + + +### logical and, or ```bash -Fruits[0]="Apple" -Fruits[1]="Banana" -Fruits[2]="Orange" +if [ "$1" = 'y' -a $2 -gt 0 ]; then + echo "yes" +fi + +if [ "$1" = 'n' -o $2 -lt 0 ]; then + echo "no" +fi ``` -### Iteration + +Loops {.cols-3} +----- + +### Basic for loop ```bash -for i in "${arrayName[@]}"; do +for i in /etc/rc.*; do echo $i done ``` +### C-like for loop + +```bash +for ((i = 0 ; i < 100 ; i++)); do + echo $i +done +``` -### Working with arrays +### Ranges {.row-span-2} ```bash -echo ${Fruits[0]} # Element #0 -echo ${Fruits[-1]} # Last element -echo ${Fruits[@]} # All elements, space-separated -echo ${#Fruits[@]} # Number of elements -echo ${#Fruits} # String length of the 1st element -echo ${#Fruits[3]} # String length of the Nth element -echo ${Fruits[@]:3:2} # Range (from position 3, length 2) -echo ${!Fruits[@]} # Keys of all elements, space-separated +for i in {1..5}; do + echo "Welcome $i" +done ``` -### Operations + +#### With step size ```bash -Fruits=("${Fruits[@]}" "Watermelon") # Push -Fruits+=('Watermelon') # Also Push -Fruits=( ${Fruits[@]/Ap*/} ) # Remove by regex match -unset Fruits[2] # Remove one item -Fruits=("${Fruits[@]}") # Duplicate -Fruits=("${Fruits[@]}" "${Veggies[@]}") # Concatenate -lines=(`cat "logfile"`) # Read from file +for i in {5..50..5}; do + echo "Welcome $i" +done ``` -Dictionaries {.cols-3} ------------- -### Defining + +### Auto increment ```bash -declare -A sounds +i=1 +while [[ $i -lt 4 ]]; do + echo "Number: $i" + ((i++)) +done ``` +### Auto decrement + ```bash -sounds[dog]="bark" -sounds[cow]="moo" -sounds[bird]="tweet" -sounds[wolf]="howl" +i=3 +while [[ $i -gt 0 ]]; do + echo "Number: $i" + ((i--)) +done ``` -Declares `sound` as a Dictionary object (aka associative array). -### Working with dictionaries +### Continue + +```bash {data=3,5} +for number in $(seq 1 3); do + if [[ $number == 2 ]]; then + continue; + fi + echo "$number" +done +``` + + +### Break ```bash -echo ${sounds[dog]} # Dog's sound -echo ${sounds[@]} # All values -echo ${!sounds[@]} # All keys -echo ${#sounds[@]} # Number of elements -unset sounds[dog] # Delete dog +for number in $(seq 1 3); do + if [[ $number == 2 ]]; then + # Skip entire rest of loop. + break; + fi + # This will only print 1 + echo "$number" +done +``` + +### Until +```bash +count=0 +until [ $count -gt 10 ]; do + echo "$count" + ((count++)) +done ``` -### Iteration -#### Iterate over values +### Forever ```bash -for val in "${sounds[@]}"; do - echo $val +while true; do + # here is some code. +done +``` + +### Forever (shorthand) +```bash +while :; do + # here is some code. done ``` -#### Iterate over keys + +### Reading lines ```bash -for key in "${!sounds[@]}"; do - echo $key +cat file.txt | while read line; do + echo $line done ``` + + + + +Functions {.cols-3} +--------- + +### Defining functions + +```bash +myfunc() { + echo "hello $1" +} +``` + +```bash +# Same as above (alternate syntax) +function myfunc() { + echo "hello $1" +} +``` + +```bash +myfunc "John" +``` + +### Returning values + +```bash +myfunc() { + local myresult='some value' + echo $myresult +} +``` + +```bash +result="$(myfunc)" +``` + +### Raising errors + +```bash +myfunc() { + return 1 +} +``` + +```bash +if myfunc; then + echo "success" +else + echo "failure" +fi +``` + + + Options {.cols-2} ------- ### Options ```bash -set -o noclobber # Avoid overlay files (echo "hi" > foo) -set -o errexit # Used to exit upon error, avoiding cascading errors -set -o pipefail # Unveils hidden failures -set -o nounset # Exposes unset variables +# Avoid overlay files +# (echo "hi" > foo) +set -o noclobber + +# Used to exit upon error +# avoiding cascading errors +set -o errexit + +# Unveils hidden failures +set -o pipefail + +# Exposes unset variables +set -o nounset ``` ### Glob options ```bash -shopt -s nullglob # Non-matching globs are removed ('*.foo' => '') -shopt -s failglob # Non-matching globs throw errors -shopt -s nocaseglob # Case insensitive globs -shopt -s dotglob # Wildcards match dotfiles ("*.sh" => ".foo.sh") -shopt -s globstar # Allow ** for recursive matches ('lib/**/*.rb' => 'lib/a/b/c.rb') +# Non-matching globs are removed +# ('*.foo' => '') +shopt -s nullglob + +# Non-matching globs throw errors +shopt -s failglob + +# Case insensitive globs +shopt -s nocaseglob + +# Wildcards match dotfiles +# ("*.sh" => ".foo.sh") +shopt -s dotglob + +# Allow ** for recursive matches +# ('lib/**/*.rb' => 'lib/a/b/c.rb') +shopt -s globstar ``` -Set `GLOBIGNORE` as a colon-separated list of patterns to be removed from glob -matches. History {.cols-2} ------- @@ -767,8 +870,8 @@ printf "Hello %s, I'm %s" Sven Olga printf "1 + 1 = %d" 2 #=> "1 + 1 = 2" -printf "This is how you print a float: %f" 2 -#=> "This is how you print a float: 2.000000" +printf "Print a float: %f" 2 +#=> "Print a float: 2.000000" ``` ### Getting options {.col-span-2} @@ -884,6 +987,25 @@ echo $ans read -n 1 ans # Just one character ``` + +### Conditional execution + +```bash +git commit && git push +git commit || echo "Commit failed" +``` + + +### Strict mode + +```bash +set -euo pipefail +IFS=$'\n\t' +``` + +See: [Unofficial bash strict mode](http://redsymbol.net/articles/unofficial-bash-strict-mode/) + + ## Also see {.cols-1} * [Devhints](https://devhints.io/bash) _(devhints.io)_ * [Bash-hackers wiki](http://wiki.bash-hackers.org/) _(bash-hackers.org)_