From f614dc3cfb92f550a8ca7d29872999618bc9ab41 Mon Sep 17 00:00:00 2001 From: Dylan Araps Date: Wed, 20 Jun 2018 13:03:53 +1000 Subject: [PATCH] Added introduction. --- README.md | 14 +- manuscript/Book.txt | 1 + manuscript/chapter0.txt | 374 +------------------------------------ manuscript/chapter1.txt | 388 ++++++++++++++++++++++++++++++++------- manuscript/chapter10.txt | 52 ++---- manuscript/chapter11.txt | 41 ++++- manuscript/chapter12.txt | 50 +---- manuscript/chapter13.txt | 114 ++++-------- manuscript/chapter14.txt | 143 +++++++++------ manuscript/chapter15.txt | 152 ++++----------- manuscript/chapter16.txt | 250 +++++++++++-------------- manuscript/chapter17.txt | 193 +++++++++++++++++++ manuscript/chapter2.txt | 163 +++++++++------- manuscript/chapter3.txt | 212 ++++++--------------- manuscript/chapter4.txt | 178 ++++++++++++++++-- manuscript/chapter5.txt | 50 ++++- manuscript/chapter6.txt | 60 +----- manuscript/chapter7.txt | 101 +++++----- manuscript/chapter8.txt | 82 ++++++--- manuscript/chapter9.txt | 47 +++-- 20 files changed, 1344 insertions(+), 1321 deletions(-) create mode 100644 manuscript/chapter17.txt diff --git a/README.md b/README.md index ec50d7f..893833a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ src="https://img.shields.io/badge/license-MIT-blue.svg">
-The goal of this repository is to document known and unknown methods of +The goal of this book is to document known and unknown methods of doing various tasks using only built-in `bash` features. Using the snippets from this bible can help to remove unneeded dependencies from your scripts and in most cases make them that little bit faster. I came across these @@ -45,6 +45,7 @@ scripts and not full blown utilities. +* [Introduction](#introduction) * [Strings](#strings) * [Trim leading and trailing white-space from string](#trim-leading-and-trailing-white-space-from-string) * [Trim all white-space from string and truncate spaces](#trim-all-white-space-from-string-and-truncate-spaces) @@ -156,6 +157,17 @@ scripts and not full blown utilities.
+ +# Introduction + +A collection of pure `bash` alternatives to external processes and programs. The `bash` scripting language is more powerful than people realise and you can accomplish most tasks without the need or dependency of external programs. + +Calling an external process in `bash` is expensive and excessive use will cause a noticeable slowdown. By sticking to built-in methods (*where possible*) your scripts and programs will be faster, require less dependencies and you'll gain a better understanding of the language itself. + +The contents of this book provide a reference for solving the problems encountered when writing programs and scripts in `bash`. The examples are in function format showcasing how to incorporate these solutions into your code. + + + # Strings diff --git a/manuscript/Book.txt b/manuscript/Book.txt index 21c4edb..abb34c6 100644 --- a/manuscript/Book.txt +++ b/manuscript/Book.txt @@ -15,3 +15,4 @@ chapter13.txt chapter14.txt chapter15.txt chapter16.txt +chapter17.txt diff --git a/manuscript/chapter0.txt b/manuscript/chapter0.txt index 37a625f..b7200f3 100644 --- a/manuscript/chapter0.txt +++ b/manuscript/chapter0.txt @@ -1,376 +1,10 @@ -# Strings +# Introduction -## Trim leading and trailing white-space from string +A collection of pure `bash` alternatives to external processes and programs. The `bash` scripting language is more powerful than people realise and you can accomplish most tasks without the need or dependency of external programs. -This is an alternative to `sed`, `awk`, `perl` and other tools. The -function below works by finding all leading and trailing white-space and -removing it from the start and end of the string. The `:` built-in is used in place of a temporary variable. +Calling an external process in `bash` is expensive and excessive use will cause a noticeable slowdown. By sticking to built-in methods (*where possible*) your scripts and programs will be faster, require less dependencies and you'll gain a better understanding of the language itself. -**Example Function:** - -```sh -trim_string() { - # Usage: trim_string " example string " - : "${1#"${1%%[![:space:]]*}"}" - : "${_%"${_##*[![:space:]]}"}" - printf '%s\n' "$_" -} -``` - -**Example Usage:** - -```shell -$ trim_string " Hello, World " -Hello, World - -$ name=" John Black " -$ trim_string "$name" -John Black -``` - - -## Trim all white-space from string and truncate spaces - -This is an alternative to `sed`, `awk`, `perl` and other tools. The -function below works by abusing word splitting to create a new string -without leading/trailing white-space and with truncated spaces. - -**Example Function:** - -```sh -# shellcheck disable=SC2086,SC2048 -trim_all() { - # Usage: trim_all " example string " - set -f - set -- $* - printf '%s\n' "$*" - set +f -} -``` - -**Example Usage:** - -```shell -$ trim_all " Hello, World " -Hello, World - -$ name=" John Black is my name. " -$ trim_all "$name" -John Black is my name. -``` - -## Use regex on a string - -We can use the result of `bash`'s regex matching to replace `sed` for a -large number of use-cases. - -**CAVEAT**: This is one of the few platform dependant `bash` features. -`bash` will use whatever regex engine is installed on the user's system. -Stick to POSIX regex features if aiming for compatibility. - -**CAVEAT**: This example only prints the first matching group. When using -multiple capture groups some modification is needed. - -**Example Function:** - -```sh -regex() { - # Usage: regex "string" "regex" - [[ $1 =~ $2 ]] && printf '%s\n' "${BASH_REMATCH[1]}" -} -``` - -**Example Usage:** - -```shell -$ # Trim leading white-space. -$ regex ' hello' '^\s*(.*)' -hello - -$ # Validate a hex color. -$ regex "#FFFFFF" '^(#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))$' -#FFFFFF - -$ # Validate a hex color (invalid). -$ regex "red" '^(#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))$' -# no output (invalid) -``` - -**Example Usage in script:** - -```shell -is_hex_color() { - if [[ "$1" =~ ^(#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))$ ]]; then - printf '%s\n' "${BASH_REMATCH[1]}" - else - printf '%s\n' "error: $1 is an invalid color." - return 1 - fi -} - -read -r color -is_hex_color "$color" || color="#FFFFFF" - -# Do stuff. -``` - - -## Split a string on a delimiter - -This is an alternative to `cut`, `awk` and other tools. - -**Example Function:** - -```sh -split() { - # Usage: split "string" "delimiter" - IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}" - printf '%s\n' "${arr[@]}" -} -``` - -**Example Usage:** - -```shell -$ split "apples,oranges,pears,grapes" "," -apples -oranges -pears -grapes - -$ split "1, 2, 3, 4, 5" ", " -1 -2 -3 -4 -5 - -# Multi char delimiters work too! -$ split "hello---world---my---name---is---john" "---" -hello -world -my -name -is -john -``` - -## Change a string to lowercase - -**CAVEAT:** Requires `bash` 4+ - -**Example Function:** - -```sh -lower() { - # Usage: lower "string" - printf '%s\n' "${1,,}" -} -``` - -**Example Usage:** - -```shell -$ lower "HELLO" -hello - -$ lower "HeLlO" -hello - -$ lower "hello" -hello -``` - -## Change a string to uppercase - -**CAVEAT:** Requires `bash` 4+ - -**Example Function:** - -```sh -upper() { - # Usage: upper "string" - printf '%s\n' "${1^^}" -} -``` - -**Example Usage:** - -```shell -$ upper "hello" -HELLO - -$ upper "HeLlO" -HELLO - -$ upper "HELLO" -HELLO -``` - -## Trim quotes from a string - -**Example Function:** - -```sh -trim_quotes() { - # Usage: trim_quotes "string" - : "${1//\'}" - printf '%s\n' "${_//\"}" -} -``` - -**Example Usage:** - -```shell -$ var="'Hello', \"World\"" -$ trim_quotes "$var" -Hello, World -``` - -## Strip all instances of pattern from string - -**Example Function:** - -```sh -strip_all() { - # Usage: strip_all "string" "pattern" - printf '%s\n' "${1//$2}" -} -``` - -**Example Usage:** - -```shell -$ strip_all "The Quick Brown Fox" "[aeiou]" -Th Qck Brwn Fx - -$ strip_all "The Quick Brown Fox" "[[:space:]]" -TheQuickBrownFox - -$ strip_all "The Quick Brown Fox" "Quick " -The Brown Fox -``` - -## Strip first occurrence of pattern from string - -**Example Function:** - -```sh -strip() { - # Usage: strip "string" "pattern" - printf '%s\n' "${1/$2}" -} -``` - -**Example Usage:** - -```shell -$ strip "The Quick Brown Fox" "[aeiou]" -Th Quick Brown Fox - -$ strip "The Quick Brown Fox" "[[:space:]]" -TheQuick Brown Fox -``` - -## Strip pattern from start of string - -**Example Function:** - -```sh -lstrip() { - # Usage: lstrip "string" "pattern" - printf '%s\n' "${1##$2}" -} -``` - -**Example Usage:** - -```shell -$ lstrip "The Quick Brown Fox" "The " -Quick Brown Fox -``` - -## Strip pattern from end of string - -**Example Function:** - -```sh -rstrip() { - # Usage: rstrip "string" "pattern" - printf '%s\n' "${1%%$2}" -} -``` - -**Example Usage:** - -```shell -$ rstrip "The Quick Brown Fox" " Fox" -The Quick Brown -``` - -## Check if string contains a sub-string - -**Using a test:** - -```shell -if [[ "$var" == *sub_string* ]]; then - printf '%s\n' "sub_string is in var." -fi - -# Inverse (substring not in string). -if [[ "$var" != *sub_string* ]]; then - printf '%s\n' "sub_string is not in var." -fi - -# This works for arrays too! -if [[ "${arr[*]}" == *sub_string* ]]; then - printf '%s\n' "sub_string is in array." -fi -``` - -**Using a case statement:** - -```shell -case "$var" in - *sub_string*) - # Do stuff - ;; - - *sub_string2*) - # Do more stuff - ;; - - *) - # Else - ;; -esac -``` - -## Check if string starts with sub-string - -```shell -if [[ "$var" == sub_string* ]]; then - printf '%s\n' "var starts with sub_string." -fi - -# Inverse (var doesn't start with sub_string). -if [[ "$var" != sub_string* ]]; then - printf '%s\n' "var does not start with sub_string." -fi -``` - -## Check if string ends with sub-string - -```shell -if [[ "$var" == *sub_string ]]; then - printf '%s\n' "var ends with sub_string." -fi - -# Inverse (var doesn't start with sub_string). -if [[ "$var" != *sub_string ]]; then - printf '%s\n' "var does not end with sub_string." -fi -``` +The contents of this book provide a reference for solving the problems encountered when writing programs and scripts in `bash`. The examples are in function format showcasing how to incorporate these solutions into your code. diff --git a/manuscript/chapter1.txt b/manuscript/chapter1.txt index e677440..37a625f 100644 --- a/manuscript/chapter1.txt +++ b/manuscript/chapter1.txt @@ -1,130 +1,376 @@ -# Arrays +# Strings -## Reverse an array +## Trim leading and trailing white-space from string -Enabling `extdebug` allows access to the `BASH_ARGV` array which stores -the current function’s arguments in reverse. +This is an alternative to `sed`, `awk`, `perl` and other tools. The +function below works by finding all leading and trailing white-space and +removing it from the start and end of the string. The `:` built-in is used in place of a temporary variable. **Example Function:** ```sh -reverse_array() { - # Usage: reverse_array "array" - shopt -s extdebug - f()(printf '%s\n' "${BASH_ARGV[@]}"); f "$@" - shopt -u extdebug +trim_string() { + # Usage: trim_string " example string " + : "${1#"${1%%[![:space:]]*}"}" + : "${_%"${_##*[![:space:]]}"}" + printf '%s\n' "$_" } ``` **Example Usage:** ```shell -$ reverse_array 1 2 3 4 5 -5 -4 -3 -2 -1 +$ trim_string " Hello, World " +Hello, World -$ arr=(red blue green) -$ reverse_array "${arr[@]}" -green -blue -red +$ name=" John Black " +$ trim_string "$name" +John Black ``` -## Remove duplicate array elements -Create a temporary associative array. When setting associative array -values and a duplicate assignment occurs, bash overwrites the key. This -allows us to effectively remove array duplicates. +## Trim all white-space from string and truncate spaces + +This is an alternative to `sed`, `awk`, `perl` and other tools. The +function below works by abusing word splitting to create a new string +without leading/trailing white-space and with truncated spaces. + +**Example Function:** + +```sh +# shellcheck disable=SC2086,SC2048 +trim_all() { + # Usage: trim_all " example string " + set -f + set -- $* + printf '%s\n' "$*" + set +f +} +``` + +**Example Usage:** + +```shell +$ trim_all " Hello, World " +Hello, World + +$ name=" John Black is my name. " +$ trim_all "$name" +John Black is my name. +``` + +## Use regex on a string + +We can use the result of `bash`'s regex matching to replace `sed` for a +large number of use-cases. + +**CAVEAT**: This is one of the few platform dependant `bash` features. +`bash` will use whatever regex engine is installed on the user's system. +Stick to POSIX regex features if aiming for compatibility. + +**CAVEAT**: This example only prints the first matching group. When using +multiple capture groups some modification is needed. + +**Example Function:** + +```sh +regex() { + # Usage: regex "string" "regex" + [[ $1 =~ $2 ]] && printf '%s\n' "${BASH_REMATCH[1]}" +} +``` + +**Example Usage:** + +```shell +$ # Trim leading white-space. +$ regex ' hello' '^\s*(.*)' +hello + +$ # Validate a hex color. +$ regex "#FFFFFF" '^(#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))$' +#FFFFFF + +$ # Validate a hex color (invalid). +$ regex "red" '^(#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))$' +# no output (invalid) +``` + +**Example Usage in script:** + +```shell +is_hex_color() { + if [[ "$1" =~ ^(#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))$ ]]; then + printf '%s\n' "${BASH_REMATCH[1]}" + else + printf '%s\n' "error: $1 is an invalid color." + return 1 + fi +} + +read -r color +is_hex_color "$color" || color="#FFFFFF" + +# Do stuff. +``` + + +## Split a string on a delimiter + +This is an alternative to `cut`, `awk` and other tools. + +**Example Function:** + +```sh +split() { + # Usage: split "string" "delimiter" + IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}" + printf '%s\n' "${arr[@]}" +} +``` + +**Example Usage:** + +```shell +$ split "apples,oranges,pears,grapes" "," +apples +oranges +pears +grapes + +$ split "1, 2, 3, 4, 5" ", " +1 +2 +3 +4 +5 + +# Multi char delimiters work too! +$ split "hello---world---my---name---is---john" "---" +hello +world +my +name +is +john +``` + +## Change a string to lowercase **CAVEAT:** Requires `bash` 4+ **Example Function:** ```sh -remove_array_dups() { - # Usage: remove_array_dups "array" - declare -A tmp_array - - for i in "$@"; do - [[ "$i" ]] && IFS=" " tmp_array["${i:- }"]=1 - done - - printf '%s\n' "${!tmp_array[@]}" +lower() { + # Usage: lower "string" + printf '%s\n' "${1,,}" } ``` **Example Usage:** ```shell -$ remove_array_dups 1 1 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 5 -1 -2 -3 -4 -5 +$ lower "HELLO" +hello -$ arr=(red red green blue blue) -$ remove_array_dups "${arr[@]}" -red -green -blue +$ lower "HeLlO" +hello + +$ lower "hello" +hello ``` -## Random array element +## Change a string to uppercase + +**CAVEAT:** Requires `bash` 4+ **Example Function:** ```sh -random_array_element() { - # Usage: random_array_element "array" - local arr=("$@") - printf '%s\n' "${arr[RANDOM % $#]}" +upper() { + # Usage: upper "string" + printf '%s\n' "${1^^}" } ``` **Example Usage:** ```shell -$ array=(red green blue yellow brown) -$ random_array_element "${array[@]}" -yellow +$ upper "hello" +HELLO -# You can also just pass multiple arguments. -$ random_array_element 1 2 3 4 5 6 7 -3 +$ upper "HeLlO" +HELLO + +$ upper "HELLO" +HELLO ``` -## Cycle through an array +## Trim quotes from a string -Each time the `printf` is called, the next array element is printed. When -the print hits the last array element it starts from the first element -again. +**Example Function:** ```sh -arr=(a b c d) - -cycle() { - printf '%s ' "${arr[${i:=0}]}" - ((i=i>=${#arr[@]}-1?0:++i)) +trim_quotes() { + # Usage: trim_quotes "string" + : "${1//\'}" + printf '%s\n' "${_//\"}" } ``` +**Example Usage:** -## Toggle between two values +```shell +$ var="'Hello', \"World\"" +$ trim_quotes "$var" +Hello, World +``` -This works the same as above, this is just a different use case. +## Strip all instances of pattern from string + +**Example Function:** ```sh -arr=(true false) - -cycle() { - printf '%s ' "${arr[${i:=0}]}" - ((i=i>=${#arr[@]}-1?0:++i)) +strip_all() { + # Usage: strip_all "string" "pattern" + printf '%s\n' "${1//$2}" } ``` +**Example Usage:** + +```shell +$ strip_all "The Quick Brown Fox" "[aeiou]" +Th Qck Brwn Fx + +$ strip_all "The Quick Brown Fox" "[[:space:]]" +TheQuickBrownFox + +$ strip_all "The Quick Brown Fox" "Quick " +The Brown Fox +``` + +## Strip first occurrence of pattern from string + +**Example Function:** + +```sh +strip() { + # Usage: strip "string" "pattern" + printf '%s\n' "${1/$2}" +} +``` + +**Example Usage:** + +```shell +$ strip "The Quick Brown Fox" "[aeiou]" +Th Quick Brown Fox + +$ strip "The Quick Brown Fox" "[[:space:]]" +TheQuick Brown Fox +``` + +## Strip pattern from start of string + +**Example Function:** + +```sh +lstrip() { + # Usage: lstrip "string" "pattern" + printf '%s\n' "${1##$2}" +} +``` + +**Example Usage:** + +```shell +$ lstrip "The Quick Brown Fox" "The " +Quick Brown Fox +``` + +## Strip pattern from end of string + +**Example Function:** + +```sh +rstrip() { + # Usage: rstrip "string" "pattern" + printf '%s\n' "${1%%$2}" +} +``` + +**Example Usage:** + +```shell +$ rstrip "The Quick Brown Fox" " Fox" +The Quick Brown +``` + +## Check if string contains a sub-string + +**Using a test:** + +```shell +if [[ "$var" == *sub_string* ]]; then + printf '%s\n' "sub_string is in var." +fi + +# Inverse (substring not in string). +if [[ "$var" != *sub_string* ]]; then + printf '%s\n' "sub_string is not in var." +fi + +# This works for arrays too! +if [[ "${arr[*]}" == *sub_string* ]]; then + printf '%s\n' "sub_string is in array." +fi +``` + +**Using a case statement:** + +```shell +case "$var" in + *sub_string*) + # Do stuff + ;; + + *sub_string2*) + # Do more stuff + ;; + + *) + # Else + ;; +esac +``` + +## Check if string starts with sub-string + +```shell +if [[ "$var" == sub_string* ]]; then + printf '%s\n' "var starts with sub_string." +fi + +# Inverse (var doesn't start with sub_string). +if [[ "$var" != sub_string* ]]; then + printf '%s\n' "var does not start with sub_string." +fi +``` + +## Check if string ends with sub-string + +```shell +if [[ "$var" == *sub_string ]]; then + printf '%s\n' "var ends with sub_string." +fi + +# Inverse (var doesn't start with sub_string). +if [[ "$var" != *sub_string ]]; then + printf '%s\n' "var does not end with sub_string." +fi +``` + diff --git a/manuscript/chapter10.txt b/manuscript/chapter10.txt index fd3dfd7..5fc7c0c 100644 --- a/manuscript/chapter10.txt +++ b/manuscript/chapter10.txt @@ -1,42 +1,30 @@ -# Traps +# Arithmetic -Traps allow you to execute code on various signals. In `pxltrm` I'm using traps to redraw the user interface on window resize. Another use case is cleaning up temporary files on script exit. - -These `trap` lines should be added near the start of your script so any early errors are also caught. - -**NOTE:** For a full list of signals, see `trap -l`. - - -## Do something on script exit +## Simpler syntax to set variables ```shell -# Clear screen on script exit. -trap 'printf \\e[2J\\e[H\\e[m' EXIT +# Simple math +((var=1+2)) + +# Decrement/Increment variable +((var++)) +((var--)) +((var+=1)) +((var-=1)) + +# Using variables +((var=var2*arr[2])) ``` -## Ignore terminal interrupt (CTRL+C, SIGINT) +## Ternary tests ```shell -trap '' INT -``` - -## React to window resize. - -```shell -# Call a function on window resize. -trap 'code_here' SIGWINCH -``` - -## Do something before every command. - -```shell -trap 'code_here' DEBUG -``` - -## Do something when a shell function or a sourced file finishes executing - -```shell -trap 'code_here' RETURN +# Set the value of var to var2 if var2 is greater than var. +# var: variable to set. +# var2>var: Condition to test. +# ?var2: If the test succeeds. +# :var: If the test fails. +((var=var2>var?var2:var)) ``` diff --git a/manuscript/chapter11.txt b/manuscript/chapter11.txt index 81071a2..fd3dfd7 100644 --- a/manuscript/chapter11.txt +++ b/manuscript/chapter11.txt @@ -1,13 +1,42 @@ -# Performance +# Traps -## Disable Unicode +Traps allow you to execute code on various signals. In `pxltrm` I'm using traps to redraw the user interface on window resize. Another use case is cleaning up temporary files on script exit. -If your script doesn't require unicode, you can disable it for a speed boost. Results may vary but I've seen an improvement in Neofetch and some other smaller programs. +These `trap` lines should be added near the start of your script so any early errors are also caught. + +**NOTE:** For a full list of signals, see `trap -l`. + + +## Do something on script exit ```shell -# Disable unicode. -LC_ALL=C -LANG=C +# Clear screen on script exit. +trap 'printf \\e[2J\\e[H\\e[m' EXIT +``` + +## Ignore terminal interrupt (CTRL+C, SIGINT) + +```shell +trap '' INT +``` + +## React to window resize. + +```shell +# Call a function on window resize. +trap 'code_here' SIGWINCH +``` + +## Do something before every command. + +```shell +trap 'code_here' DEBUG +``` + +## Do something when a shell function or a sourced file finishes executing + +```shell +trap 'code_here' RETURN ``` diff --git a/manuscript/chapter12.txt b/manuscript/chapter12.txt index d3857db..81071a2 100644 --- a/manuscript/chapter12.txt +++ b/manuscript/chapter12.txt @@ -1,51 +1,13 @@ -# Obsolete Syntax +# Performance -## Shebang +## Disable Unicode -Use `#!/usr/bin/env bash` instead of `#!/bin/bash`. - -- The former searches the user's `PATH` to find the `bash` binary. -- The latter assumes it is always installed to `/bin/` which can cause issues. +If your script doesn't require unicode, you can disable it for a speed boost. Results may vary but I've seen an improvement in Neofetch and some other smaller programs. ```shell -# Right: - - #!/usr/bin/env bash - -# Wrong: - - #!/bin/bash -``` - -## Command Substitution - -Use `$()` instead of `` ` ` ``. - -```shell -# Right. -var="$(command)" - -# Wrong. -var=`command` - -# $() can easily be nested whereas `` cannot. -var="$(command "$(command)")" -``` - -## Function Declaration - -Don't use the `function` keyword, it reduces compatibility with older versions of `bash`. - -```shell -# Right. -do_something() { - # ... -} - -# Wrong. -function do_something() { - # ... -} +# Disable unicode. +LC_ALL=C +LANG=C ``` diff --git a/manuscript/chapter13.txt b/manuscript/chapter13.txt index f743554..d3857db 100644 --- a/manuscript/chapter13.txt +++ b/manuscript/chapter13.txt @@ -1,99 +1,51 @@ -# Internal Variables +# Obsolete Syntax -**NOTE**: This list does not include every internal variable (*You can -help by adding a missing entry!*). +## Shebang -For a complete list, see: -http://tldp.org/LDP/abs/html/internalvariables.html +Use `#!/usr/bin/env bash` instead of `#!/bin/bash`. -## Get the location to the `bash` binary +- The former searches the user's `PATH` to find the `bash` binary. +- The latter assumes it is always installed to `/bin/` which can cause issues. ```shell -"$BASH" +# Right: + + #!/usr/bin/env bash + +# Wrong: + + #!/bin/bash ``` -## Get the version of the current running `bash` process +## Command Substitution + +Use `$()` instead of `` ` ` ``. ```shell -# As a string. -"$BASH_VERSION" +# Right. +var="$(command)" -# As an array. -"${BASH_VERSINFO[@]}" +# Wrong. +var=`command` + +# $() can easily be nested whereas `` cannot. +var="$(command "$(command)")" ``` -## Open the user's preferred text editor +## Function Declaration + +Don't use the `function` keyword, it reduces compatibility with older versions of `bash`. ```shell -"$EDITOR" "$file" +# Right. +do_something() { + # ... +} -# NOTE: This variable may be empty, set a fallback value. -"${EDITOR:-vi}" "$file" -``` - -## Get the name of the current function - -```shell -# Current function. -"${FUNCNAME[0]}" - -# Parent function. -"${FUNCNAME[1]}" - -# So on and so forth. -"${FUNCNAME[2]}" -"${FUNCNAME[3]}" - -# All functions including parents. -"${FUNCNAME[@]}" -``` - -## Get the host-name of the system - -```shell -"$HOSTNAME" - -# NOTE: This variable may be empty. -# Optionally set a fallback to the hostname command. -"${HOSTNAME:-$(hostname)}" -``` - -## Get the architecture of the Operating System - -```shell -"$HOSTTYPE" -``` - -## Get the name of the Operating System / Kernel - -This can be used to add conditional support for different Operating -Systems without needing to call `uname`. - -```shell -"$OSTYPE" -``` - -## Get the current working directory - -This is an alternative to the `pwd` built-in. - -```shell -"$PWD" -``` - -## Get the number of seconds the script has been running - -```shell -"$SECONDS" -``` - -## Get a pseudorandom integer - -Each time `$RANDOM` is used, a different integer between `0` and `32767` is returned. This variable should not be used for anything related to security (*this includes encryption keys etc*). - - -```shell -"$RANDOM" +# Wrong. +function do_something() { + # ... +} ``` diff --git a/manuscript/chapter14.txt b/manuscript/chapter14.txt index ca647b9..f743554 100644 --- a/manuscript/chapter14.txt +++ b/manuscript/chapter14.txt @@ -1,78 +1,99 @@ -# Information about the terminal +# Internal Variables -## Get the terminal size in lines and columns (*from a script*) +**NOTE**: This list does not include every internal variable (*You can +help by adding a missing entry!*). -This is handy when writing scripts in pure bash and `stty`/`tput` can’t be -called. +For a complete list, see: +http://tldp.org/LDP/abs/html/internalvariables.html -**Example Function:** - -```sh -get_term_size() { - # Usage: get_term_size - - # (:;:) is a micro sleep to ensure the variables are - # exported immediately. - shopt -s checkwinsize; (:;:) - printf '%s\n' "$LINES $COLUMNS" -} -``` - -**Example Usage:** +## Get the location to the `bash` binary ```shell -# Output: LINES COLUMNS -$ get_term_size -15 55 +"$BASH" ``` -## Get the terminal size in pixels - -**CAVEAT**: This does not work in some terminal emulators. - -**Example Function:** - -```sh -get_window_size() { - # Usage: get_window_size - printf '%b' "${TMUX:+\\ePtmux;\\e}\\e[14t${TMUX:+\\e\\\\}" - IFS=';t' read -d t -t 0.05 -sra term_size - printf '%s\n' "${term_size[1]}x${term_size[2]}" -} -``` - -**Example Usage:** +## Get the version of the current running `bash` process ```shell -# Output: WIDTHxHEIGHT -$ get_window_size -1200x800 +# As a string. +"$BASH_VERSION" -# Output (fail): -$ get_window_size -x +# As an array. +"${BASH_VERSINFO[@]}" ``` -## Get the current cursor position - -This is useful when creating a TUI in pure bash. - -**Example Function:** - -```sh -get_cursor_pos() { - # Usage: get_cursor_pos - IFS='[;' read -p $'\e[6n' -d R -rs _ y x _ - printf '%s\n' "$x $y" -} -``` - -**Example Usage:** +## Open the user's preferred text editor ```shell -# Output: X Y -$ get_cursor_pos -1 8 +"$EDITOR" "$file" + +# NOTE: This variable may be empty, set a fallback value. +"${EDITOR:-vi}" "$file" +``` + +## Get the name of the current function + +```shell +# Current function. +"${FUNCNAME[0]}" + +# Parent function. +"${FUNCNAME[1]}" + +# So on and so forth. +"${FUNCNAME[2]}" +"${FUNCNAME[3]}" + +# All functions including parents. +"${FUNCNAME[@]}" +``` + +## Get the host-name of the system + +```shell +"$HOSTNAME" + +# NOTE: This variable may be empty. +# Optionally set a fallback to the hostname command. +"${HOSTNAME:-$(hostname)}" +``` + +## Get the architecture of the Operating System + +```shell +"$HOSTTYPE" +``` + +## Get the name of the Operating System / Kernel + +This can be used to add conditional support for different Operating +Systems without needing to call `uname`. + +```shell +"$OSTYPE" +``` + +## Get the current working directory + +This is an alternative to the `pwd` built-in. + +```shell +"$PWD" +``` + +## Get the number of seconds the script has been running + +```shell +"$SECONDS" +``` + +## Get a pseudorandom integer + +Each time `$RANDOM` is used, a different integer between `0` and `32767` is returned. This variable should not be used for anything related to security (*this includes encryption keys etc*). + + +```shell +"$RANDOM" ``` diff --git a/manuscript/chapter15.txt b/manuscript/chapter15.txt index 98e8e64..ca647b9 100644 --- a/manuscript/chapter15.txt +++ b/manuscript/chapter15.txt @@ -1,150 +1,78 @@ -# Conversion +# Information about the terminal -## Convert a hex color to RGB +## Get the terminal size in lines and columns (*from a script*) + +This is handy when writing scripts in pure bash and `stty`/`tput` can’t be +called. **Example Function:** ```sh -hex_to_rgb() { - # Usage: hex_to_rgb "#FFFFFF" - ((r=16#${1:1:2})) - ((g=16#${1:3:2})) - ((b=16#${1:5:6})) +get_term_size() { + # Usage: get_term_size - printf '%s\n' "$r $g $b" + # (:;:) is a micro sleep to ensure the variables are + # exported immediately. + shopt -s checkwinsize; (:;:) + printf '%s\n' "$LINES $COLUMNS" } ``` **Example Usage:** ```shell -$ hex_to_rgb "#FFFFFF" -255 255 255 +# Output: LINES COLUMNS +$ get_term_size +15 55 ``` +## Get the terminal size in pixels -## Convert an RGB color to hex +**CAVEAT**: This does not work in some terminal emulators. **Example Function:** ```sh -rgb_to_hex() { - # Usage: rgb_to_hex "r" "g" "b" - printf '#%02x%02x%02x\n' "$1" "$2" "$3" +get_window_size() { + # Usage: get_window_size + printf '%b' "${TMUX:+\\ePtmux;\\e}\\e[14t${TMUX:+\\e\\\\}" + IFS=';t' read -d t -t 0.05 -sra term_size + printf '%s\n' "${term_size[1]}x${term_size[2]}" } ``` **Example Usage:** ```shell -$ rgb_to_hex "255" "255" "255" -#FFFFFF +# Output: WIDTHxHEIGHT +$ get_window_size +1200x800 + +# Output (fail): +$ get_window_size +x ``` +## Get the current cursor position -# Code Golf +This is useful when creating a TUI in pure bash. -## Shorter `for` loop syntax +**Example Function:** -```shell -# Tiny C Style. -for((;i++<10;)){ echo "$i";} - -# Undocumented method. -for i in {1..10};{ echo "$i";} - -# Expansion. -for i in {1..10}; do echo "$i"; done - -# C Style. -for((i=0;i<=10;i++)); do echo "$i"; done -``` - -## Shorter infinite loops - -```shell -# Normal method -while :; do echo hi; done - -# Shorter -for((;;)){ echo hi;} -``` - -## Shorter function declaration - -```shell -# Normal method -f(){ echo hi;} - -# Using a subshell -f()(echo hi) - -# Using arithmetic -# You can use this to assign integer values. -# Example: f a=1 -# f a++ -f()(($1)) - -# Using tests, loops etc. -# NOTE: You can also use ‘while’, ‘until’, ‘case’, ‘(())’, ‘[[]]’. -f()if true; then echo "$1"; fi -f()for i in "$@"; do echo "$i"; done -``` - -## Shorter `if` syntax - -```shell -# One line -# Note: The 3rd statement may run when the 1st is true -[[ "$var" == hello ]] && echo hi || echo bye -[[ "$var" == hello ]] && { echo hi; echo there; } || echo bye - -# Multi line (no else, single statement) -# Note: The exit status may not be the same as with an if statement -[[ "$var" == hello ]] && \ - echo hi - -# Multi line (no else) -[[ "$var" == hello ]] && { - echo hi - # ... +```sh +get_cursor_pos() { + # Usage: get_cursor_pos + IFS='[;' read -p $'\e[6n' -d R -rs _ y x _ + printf '%s\n' "$x $y" } ``` -## Simpler `case` statement to set variable - -We can use the `:` builtin to avoid repeating `variable=` in a case -statement. The `$_` variable stores the last argument of the last -successful command. `:` always succeeds so we can abuse it to store the -variable value. +**Example Usage:** ```shell -# Modified snippet from Neofetch. -case "$OSTYPE" in - "darwin"*) - : "MacOS" - ;; - - "linux"*) - : "Linux" - ;; - - *"bsd"* | "dragonfly" | "bitrig") - : "BSD" - ;; - - "cygwin" | "msys" | "win32") - : "Windows" - ;; - - *) - printf '%s\n' "Unknown OS detected, aborting..." >&2 - exit 1 - ;; -esac - -# Finally, set the variable. -os="$_" +# Output: X Y +$ get_cursor_pos +1 8 ``` diff --git a/manuscript/chapter16.txt b/manuscript/chapter16.txt index 756faa9..98e8e64 100644 --- a/manuscript/chapter16.txt +++ b/manuscript/chapter16.txt @@ -1,192 +1,150 @@ -# Other +# Conversion -## Use `read` as an alternative to the `sleep` command - -I was surprised to find out `sleep` is an external command and isn't a -built-in. - -**CAVEAT:** Requires `bash` 4+ +## Convert a hex color to RGB **Example Function:** ```sh -read_sleep() { - # Usage: sleep 1 - # sleep 0.2 - read -rst "${1:-1}" -N 999 +hex_to_rgb() { + # Usage: hex_to_rgb "#FFFFFF" + ((r=16#${1:1:2})) + ((g=16#${1:3:2})) + ((b=16#${1:5:6})) + + printf '%s\n' "$r $g $b" } ``` **Example Usage:** ```shell -read_sleep 1 -read_sleep 0.1 -read_sleep 30 +$ hex_to_rgb "#FFFFFF" +255 255 255 ``` -## Check if a program is in the user's PATH -```shell -# There are 3 ways to do this and you can use either of -# these in the same way. -type -p executable_name &>/dev/null -hash executable_name &>/dev/null -command -v executable_name &>/dev/null - -# As a test. -if type -p executable_name &>/dev/null; then - # Program is in PATH. -fi - -# Inverse. -if ! type -p executable_name &>/dev/null; then - # Program is not in PATH. -fi - -# Example (Exit early if program isn't installed). -if ! type -p convert &>/dev/null; then - printf '%s\n' "error: convert isn't installed, exiting..." - exit 1 -fi -``` - -## Get the current date using `strftime` - -Bash’s `printf` has a built-in method of getting the date which we can use -in place of the `date` command in a lot of cases. - -**CAVEAT:** Requires `bash` 4+ +## Convert an RGB color to hex **Example Function:** ```sh -date() { - # Usage: date "format" - # See: 'man strftime' for format. - printf "%($1)T\\n" "-1" +rgb_to_hex() { + # Usage: rgb_to_hex "r" "g" "b" + printf '#%02x%02x%02x\n' "$1" "$2" "$3" } ``` **Example Usage:** ```shell -# Using above function. -$ date "%a %d %b - %l:%M %p" -Fri 15 Jun - 10:00 AM - -# Using printf directly. -$ printf '%(%a %d %b - %l:%M %p)T\n' "-1" -Fri 15 Jun - 10:00 AM - -# Assigning a variable using printf. -$ printf -v date '%(%a %d %b - %l:%M %p)T\n' '-1' -$ printf '%s\n' "$date" -Fri 15 Jun - 10:00 AM +$ rgb_to_hex "255" "255" "255" +#FFFFFF ``` -## Generate a UUID V4 -**Example Function:** +# Code Golf -```sh -uuid() { - # Usage: uuid - C="89ab" +## Shorter `for` loop syntax - for ((N=0;N<16;++N)); do - B="$((RANDOM%256))" +```shell +# Tiny C Style. +for((;i++<10;)){ echo "$i";} - case "$N" in - 6) printf '4%x' "$((B%16))" ;; - 8) printf '%c%x' "${C:$RANDOM%${#C}:1}" "$((B%16))" ;; +# Undocumented method. +for i in {1..10};{ echo "$i";} - 3|5|7|9) - printf '%02x-' "$B" - ;; +# Expansion. +for i in {1..10}; do echo "$i"; done - *) - printf '%02x' "$B" - ;; - esac - done +# C Style. +for((i=0;i<=10;i++)); do echo "$i"; done +``` - printf '\n' +## Shorter infinite loops + +```shell +# Normal method +while :; do echo hi; done + +# Shorter +for((;;)){ echo hi;} +``` + +## Shorter function declaration + +```shell +# Normal method +f(){ echo hi;} + +# Using a subshell +f()(echo hi) + +# Using arithmetic +# You can use this to assign integer values. +# Example: f a=1 +# f a++ +f()(($1)) + +# Using tests, loops etc. +# NOTE: You can also use ‘while’, ‘until’, ‘case’, ‘(())’, ‘[[]]’. +f()if true; then echo "$1"; fi +f()for i in "$@"; do echo "$i"; done +``` + +## Shorter `if` syntax + +```shell +# One line +# Note: The 3rd statement may run when the 1st is true +[[ "$var" == hello ]] && echo hi || echo bye +[[ "$var" == hello ]] && { echo hi; echo there; } || echo bye + +# Multi line (no else, single statement) +# Note: The exit status may not be the same as with an if statement +[[ "$var" == hello ]] && \ + echo hi + +# Multi line (no else) +[[ "$var" == hello ]] && { + echo hi + # ... } ``` -**Example Usage:** +## Simpler `case` statement to set variable + +We can use the `:` builtin to avoid repeating `variable=` in a case +statement. The `$_` variable stores the last argument of the last +successful command. `:` always succeeds so we can abuse it to store the +variable value. ```shell -$ uuid -d5b6c731-1310-4c24-9fe3-55d556d44374 -``` +# Modified snippet from Neofetch. +case "$OSTYPE" in + "darwin"*) + : "MacOS" + ;; -## Progress bars + "linux"*) + : "Linux" + ;; -This is a simple way of drawing progress bars without needing a for loop -in the function itself. + *"bsd"* | "dragonfly" | "bitrig") + : "BSD" + ;; -**Example Function:** + "cygwin" | "msys" | "win32") + : "Windows" + ;; -```sh -bar() { - # Usage: bar 1 10 - # ^----- Elapsed Percentage (0-100). - # ^-- Total length in chars. - ((elapsed=$1*$2/100)) + *) + printf '%s\n' "Unknown OS detected, aborting..." >&2 + exit 1 + ;; +esac - # Create the bar with spaces. - printf -v prog "%${elapsed}s" - printf -v total "%$(($2-elapsed))s" - - printf '%s\r' "[${prog// /-}${total}]" -} -``` - -**Example Usage:** - -```shell -for ((i=0;i<=100;i++)); do - # Pure bash micro sleeps (for the example). - (:;:) && (:;:) && (:;:) && (:;:) && (:;:) - - # Print the bar. - bar "$i" "10" -done - -printf '\n' -``` - -## Get the list of functions from your script - -```sh -get_functions() { - # Usage: get_functions - IFS=$'\n' read -d "" -ra functions < <(declare -F) - printf '%s\n' "${functions[@]//declare -f }" -} -``` - -## Bypass shell aliases - -```shell -# alias -ls - -# command -# shellcheck disable=SC1001 -\ls -``` - -## Bypass shell functions - -```shell -# function -ls - -# command -command ls +# Finally, set the variable. +os="$_" ``` diff --git a/manuscript/chapter17.txt b/manuscript/chapter17.txt new file mode 100644 index 0000000..756faa9 --- /dev/null +++ b/manuscript/chapter17.txt @@ -0,0 +1,193 @@ +# Other + +## Use `read` as an alternative to the `sleep` command + +I was surprised to find out `sleep` is an external command and isn't a +built-in. + +**CAVEAT:** Requires `bash` 4+ + +**Example Function:** + +```sh +read_sleep() { + # Usage: sleep 1 + # sleep 0.2 + read -rst "${1:-1}" -N 999 +} +``` + +**Example Usage:** + +```shell +read_sleep 1 +read_sleep 0.1 +read_sleep 30 +``` + +## Check if a program is in the user's PATH + +```shell +# There are 3 ways to do this and you can use either of +# these in the same way. +type -p executable_name &>/dev/null +hash executable_name &>/dev/null +command -v executable_name &>/dev/null + +# As a test. +if type -p executable_name &>/dev/null; then + # Program is in PATH. +fi + +# Inverse. +if ! type -p executable_name &>/dev/null; then + # Program is not in PATH. +fi + +# Example (Exit early if program isn't installed). +if ! type -p convert &>/dev/null; then + printf '%s\n' "error: convert isn't installed, exiting..." + exit 1 +fi +``` + +## Get the current date using `strftime` + +Bash’s `printf` has a built-in method of getting the date which we can use +in place of the `date` command in a lot of cases. + +**CAVEAT:** Requires `bash` 4+ + +**Example Function:** + +```sh +date() { + # Usage: date "format" + # See: 'man strftime' for format. + printf "%($1)T\\n" "-1" +} +``` + +**Example Usage:** + +```shell +# Using above function. +$ date "%a %d %b - %l:%M %p" +Fri 15 Jun - 10:00 AM + +# Using printf directly. +$ printf '%(%a %d %b - %l:%M %p)T\n' "-1" +Fri 15 Jun - 10:00 AM + +# Assigning a variable using printf. +$ printf -v date '%(%a %d %b - %l:%M %p)T\n' '-1' +$ printf '%s\n' "$date" +Fri 15 Jun - 10:00 AM +``` + +## Generate a UUID V4 + +**Example Function:** + +```sh +uuid() { + # Usage: uuid + C="89ab" + + for ((N=0;N<16;++N)); do + B="$((RANDOM%256))" + + case "$N" in + 6) printf '4%x' "$((B%16))" ;; + 8) printf '%c%x' "${C:$RANDOM%${#C}:1}" "$((B%16))" ;; + + 3|5|7|9) + printf '%02x-' "$B" + ;; + + *) + printf '%02x' "$B" + ;; + esac + done + + printf '\n' +} +``` + +**Example Usage:** + +```shell +$ uuid +d5b6c731-1310-4c24-9fe3-55d556d44374 +``` + +## Progress bars + +This is a simple way of drawing progress bars without needing a for loop +in the function itself. + +**Example Function:** + +```sh +bar() { + # Usage: bar 1 10 + # ^----- Elapsed Percentage (0-100). + # ^-- Total length in chars. + ((elapsed=$1*$2/100)) + + # Create the bar with spaces. + printf -v prog "%${elapsed}s" + printf -v total "%$(($2-elapsed))s" + + printf '%s\r' "[${prog// /-}${total}]" +} +``` + +**Example Usage:** + +```shell +for ((i=0;i<=100;i++)); do + # Pure bash micro sleeps (for the example). + (:;:) && (:;:) && (:;:) && (:;:) && (:;:) + + # Print the bar. + bar "$i" "10" +done + +printf '\n' +``` + +## Get the list of functions from your script + +```sh +get_functions() { + # Usage: get_functions + IFS=$'\n' read -d "" -ra functions < <(declare -F) + printf '%s\n' "${functions[@]//declare -f }" +} +``` + +## Bypass shell aliases + +```shell +# alias +ls + +# command +# shellcheck disable=SC1001 +\ls +``` + +## Bypass shell functions + +```shell +# function +ls + +# command +command ls +``` + + + diff --git a/manuscript/chapter2.txt b/manuscript/chapter2.txt index 90fc12f..e677440 100644 --- a/manuscript/chapter2.txt +++ b/manuscript/chapter2.txt @@ -1,94 +1,129 @@ -# Loops +# Arrays -## Loop over a range of numbers +## Reverse an array -Don't use `seq`. +Enabling `extdebug` allows access to the `BASH_ARGV` array which stores +the current function’s arguments in reverse. -```shell -# Loop from 0-100 (no variable support). -for i in {0..100}; do - printf '%s\n' "$i" -done +**Example Function:** + +```sh +reverse_array() { + # Usage: reverse_array "array" + shopt -s extdebug + f()(printf '%s\n' "${BASH_ARGV[@]}"); f "$@" + shopt -u extdebug +} ``` -## Loop over a variable range of numbers - -Don't use `seq`. +**Example Usage:** ```shell -# Loop from 0-VAR. -VAR=50 -for ((i=0;i<=VAR;i++)); do - printf '%s\n' "$i" -done +$ reverse_array 1 2 3 4 5 +5 +4 +3 +2 +1 + +$ arr=(red blue green) +$ reverse_array "${arr[@]}" +green +blue +red ``` -## Loop over an array +## Remove duplicate array elements -```shell -arr=(apples oranges tomatoes) +Create a temporary associative array. When setting associative array +values and a duplicate assignment occurs, bash overwrites the key. This +allows us to effectively remove array duplicates. -# Just elements. -for element in "${arr[@]}"; do - printf '%s\n' "$element" -done +**CAVEAT:** Requires `bash` 4+ + +**Example Function:** + +```sh +remove_array_dups() { + # Usage: remove_array_dups "array" + declare -A tmp_array + + for i in "$@"; do + [[ "$i" ]] && IFS=" " tmp_array["${i:- }"]=1 + done + + printf '%s\n' "${!tmp_array[@]}" +} ``` -## Loop over an array with an index +**Example Usage:** ```shell -arr=(apples oranges tomatoes) +$ remove_array_dups 1 1 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 5 +1 +2 +3 +4 +5 -# Elements and index. -for i in "${!arr[@]}"; do - printf '%s\n' "${arr[$i]}" -done - -# Alternative method. -for ((i=0;i<${#arr[@]};i++)); do - printf '%s\n' "${arr[$i]}" -done +$ arr=(red red green blue blue) +$ remove_array_dups "${arr[@]}" +red +green +blue ``` -## Loop over the contents of a file +## Random array element -```shell -while read -r line; do - printf '%s\n' "$line" -done < "file" +**Example Function:** + +```sh +random_array_element() { + # Usage: random_array_element "array" + local arr=("$@") + printf '%s\n' "${arr[RANDOM % $#]}" +} ``` -## Loop over files and directories - -Don’t use `ls`. +**Example Usage:** ```shell -# Greedy example. -for file in *; do - printf '%s\n' "$file" -done +$ array=(red green blue yellow brown) +$ random_array_element "${array[@]}" +yellow -# PNG files in dir. -for file in ~/Pictures/*.png; do - printf '%s\n' "$file" -done +# You can also just pass multiple arguments. +$ random_array_element 1 2 3 4 5 6 7 +3 +``` -# Iterate over directories. -for dir in ~/Downloads/*/; do - printf '%s\n' "$dir" -done +## Cycle through an array -# Brace Expansion. -for file in /path/to/parentdir/{file1,file2,subdir/file3}; do - printf '%s\n' "$file" -done +Each time the `printf` is called, the next array element is printed. When +the print hits the last array element it starts from the first element +again. -# Iterate recursively. -shopt -s globstar -for file in ~/Pictures/**/*; do - printf '%s\n' "$file" -done -shopt -u globstar +```sh +arr=(a b c d) + +cycle() { + printf '%s ' "${arr[${i:=0}]}" + ((i=i>=${#arr[@]}-1?0:++i)) +} +``` + + +## Toggle between two values + +This works the same as above, this is just a different use case. + +```sh +arr=(true false) + +cycle() { + printf '%s ' "${arr[${i:=0}]}" + ((i=i>=${#arr[@]}-1?0:++i)) +} ``` diff --git a/manuscript/chapter3.txt b/manuscript/chapter3.txt index cdef4a2..90fc12f 100644 --- a/manuscript/chapter3.txt +++ b/manuscript/chapter3.txt @@ -1,188 +1,94 @@ -# File handling +# Loops -**CAVEAT:** `bash` doesn't handle binary data properly in versions `< 4.4`. +## Loop over a range of numbers -## Read a file to a string - -Alternative to the `cat` command. +Don't use `seq`. ```shell -file_data="$(<"file")" +# Loop from 0-100 (no variable support). +for i in {0..100}; do + printf '%s\n' "$i" +done ``` -## Read a file to an array (*by line*) +## Loop over a variable range of numbers -Alternative to the `cat` command. +Don't use `seq`. ```shell -# Bash <4 -IFS=$'\n' read -d "" -ra file_data < "file" - -# Bash 4+ -mapfile -t file_data < "file" +# Loop from 0-VAR. +VAR=50 +for ((i=0;i<=VAR;i++)); do + printf '%s\n' "$i" +done ``` -## Get the first N lines of a file - -Alternative to the `head` command. - -**CAVEAT:** Requires `bash` 4+ - -**Example Function:** - -```sh -head() { - # Usage: head "n" "file" - mapfile -tn "$1" line < "$2" - printf '%s\n' "${line[@]}" -} -``` - -**Example Usage:** +## Loop over an array ```shell -$ head 2 ~/.bashrc -# Prompt -PS1='➜ ' +arr=(apples oranges tomatoes) -$ head 1 ~/.bashrc -# Prompt +# Just elements. +for element in "${arr[@]}"; do + printf '%s\n' "$element" +done ``` -## Get the last N lines of a file - -Alternative to the `tail` command. - -**CAVEAT:** Requires `bash` 4+ - -**Example Function:** - -```sh -tail() { - # Usage: tail "n" "file" - mapfile -tn 0 line < "$2" - printf '%s\n' "${line[@]: -$1}" -} -``` - -**Example Usage:** +## Loop over an array with an index ```shell -$ tail 2 ~/.bashrc -# Enable tmux. -# [[ -z "$TMUX" ]] && exec tmux +arr=(apples oranges tomatoes) -$ tail 1 ~/.bashrc -# [[ -z "$TMUX" ]] && exec tmux +# Elements and index. +for i in "${!arr[@]}"; do + printf '%s\n' "${arr[$i]}" +done + +# Alternative method. +for ((i=0;i<${#arr[@]};i++)); do + printf '%s\n' "${arr[$i]}" +done ``` -## Get the number of lines in a file - -Alternative to `wc -l`. - -**Example Function (bash 4):** - -```sh -lines() { - # Usage: lines "file" - mapfile -tn 0 lines < "$1" - printf '%s\n' "${#lines[@]}" -} -``` - -**Example Function (bash 3):** - -This method uses less memory than the `mapfile` method and it's more -compatible but it's slower for bigger files. - -```sh -lines_loop() { - # Usage: lines_loop "file" - count=0 - while IFS= read -r _; do - ((count++)) - done < "$1" - printf '%s\n' "$count" -} -``` - -**Example Usage:** +## Loop over the contents of a file ```shell -$ lines ~/.bashrc -48 - -$ lines_loop ~/.bashrc -48 +while read -r line; do + printf '%s\n' "$line" +done < "file" ``` -## Count files or directories in directory +## Loop over files and directories -This works by passing the output of the glob as function arguments. We -then count the arguments and print the number. - -**Example Function:** - -```sh -count() { - # Usage: count /path/to/dir/* - # count /path/to/dir/*/ - printf '%s\n' "$#" -} -``` - -**Example Usage:** +Don’t use `ls`. ```shell -# Count all files in dir. -$ count ~/Downloads/* -232 +# Greedy example. +for file in *; do + printf '%s\n' "$file" +done -# Count all dirs in dir. -$ count ~/Downloads/*/ -45 +# PNG files in dir. +for file in ~/Pictures/*.png; do + printf '%s\n' "$file" +done -# Count all jpg files in dir. -$ count ~/Pictures/*.jpg -64 -``` +# Iterate over directories. +for dir in ~/Downloads/*/; do + printf '%s\n' "$dir" +done -## Create an empty file +# Brace Expansion. +for file in /path/to/parentdir/{file1,file2,subdir/file3}; do + printf '%s\n' "$file" +done -Alternative to `touch`. - -```shell -# Shortest. -:> file - -# Longer alternatives: -echo -n > file -printf '' > file -``` - -## Extract lines between two markers - -**Example Function:** - -```sh -extract() { - # Usage: extract file "opening marker" "closing marker" - while IFS=$'\n' read -r line; do - [[ "$extract" && "$line" != "$3" ]] && \ - printf '%s\n' "$line" - - [[ "$line" == "$2" ]] && extract=1 - [[ "$line" == "$3" ]] && extract= - done < "$1" -} -``` - -**Example Usage:** - -```shell -# Extract code blocks from MarkDown file. -$ extract ~/projects/pure-bash/README.md '```sh' '```' -# Output here... +# Iterate recursively. +shopt -s globstar +for file in ~/Pictures/**/*; do + printf '%s\n' "$file" +done +shopt -u globstar ``` diff --git a/manuscript/chapter4.txt b/manuscript/chapter4.txt index 397e4ad..cdef4a2 100644 --- a/manuscript/chapter4.txt +++ b/manuscript/chapter4.txt @@ -1,50 +1,188 @@ -# File Paths +# File handling -## Get the directory name of a file path +**CAVEAT:** `bash` doesn't handle binary data properly in versions `< 4.4`. -Alternative to the `dirname` command. +## Read a file to a string + +Alternative to the `cat` command. + +```shell +file_data="$(<"file")" +``` + +## Read a file to an array (*by line*) + +Alternative to the `cat` command. + +```shell +# Bash <4 +IFS=$'\n' read -d "" -ra file_data < "file" + +# Bash 4+ +mapfile -t file_data < "file" +``` + +## Get the first N lines of a file + +Alternative to the `head` command. + +**CAVEAT:** Requires `bash` 4+ **Example Function:** ```sh -dirname() { - # Usage: dirname "path" - printf '%s\n' "${1%/*}/" +head() { + # Usage: head "n" "file" + mapfile -tn "$1" line < "$2" + printf '%s\n' "${line[@]}" } ``` **Example Usage:** ```shell -$ dirname ~/Pictures/Wallpapers/1.jpg -/home/black/Pictures/Wallpapers/ +$ head 2 ~/.bashrc +# Prompt +PS1='➜ ' -$ dirname ~/Pictures/Downloads/ -/home/black/Pictures/ +$ head 1 ~/.bashrc +# Prompt ``` -## Get the base-name of a file path +## Get the last N lines of a file -Alternative to the `basename` command. +Alternative to the `tail` command. + +**CAVEAT:** Requires `bash` 4+ **Example Function:** ```sh -basename() { - # Usage: basename "path" - : "${1%/}" - printf '%s\n' "${_##*/}" +tail() { + # Usage: tail "n" "file" + mapfile -tn 0 line < "$2" + printf '%s\n' "${line[@]: -$1}" } ``` **Example Usage:** ```shell -$ basename ~/Pictures/Wallpapers/1.jpg -1.jpg +$ tail 2 ~/.bashrc +# Enable tmux. +# [[ -z "$TMUX" ]] && exec tmux -$ basename ~/Pictures/Downloads/ -Downloads +$ tail 1 ~/.bashrc +# [[ -z "$TMUX" ]] && exec tmux +``` + +## Get the number of lines in a file + +Alternative to `wc -l`. + +**Example Function (bash 4):** + +```sh +lines() { + # Usage: lines "file" + mapfile -tn 0 lines < "$1" + printf '%s\n' "${#lines[@]}" +} +``` + +**Example Function (bash 3):** + +This method uses less memory than the `mapfile` method and it's more +compatible but it's slower for bigger files. + +```sh +lines_loop() { + # Usage: lines_loop "file" + count=0 + while IFS= read -r _; do + ((count++)) + done < "$1" + printf '%s\n' "$count" +} +``` + +**Example Usage:** + +```shell +$ lines ~/.bashrc +48 + +$ lines_loop ~/.bashrc +48 +``` + +## Count files or directories in directory + +This works by passing the output of the glob as function arguments. We +then count the arguments and print the number. + +**Example Function:** + +```sh +count() { + # Usage: count /path/to/dir/* + # count /path/to/dir/*/ + printf '%s\n' "$#" +} +``` + +**Example Usage:** + +```shell +# Count all files in dir. +$ count ~/Downloads/* +232 + +# Count all dirs in dir. +$ count ~/Downloads/*/ +45 + +# Count all jpg files in dir. +$ count ~/Pictures/*.jpg +64 +``` + +## Create an empty file + +Alternative to `touch`. + +```shell +# Shortest. +:> file + +# Longer alternatives: +echo -n > file +printf '' > file +``` + +## Extract lines between two markers + +**Example Function:** + +```sh +extract() { + # Usage: extract file "opening marker" "closing marker" + while IFS=$'\n' read -r line; do + [[ "$extract" && "$line" != "$3" ]] && \ + printf '%s\n' "$line" + + [[ "$line" == "$2" ]] && extract=1 + [[ "$line" == "$3" ]] && extract= + done < "$1" +} +``` + +**Example Usage:** + +```shell +# Extract code blocks from MarkDown file. +$ extract ~/projects/pure-bash/README.md '```sh' '```' +# Output here... ``` diff --git a/manuscript/chapter5.txt b/manuscript/chapter5.txt index ef9d0ce..397e4ad 100644 --- a/manuscript/chapter5.txt +++ b/manuscript/chapter5.txt @@ -1,16 +1,50 @@ -# Variables +# File Paths -## Assign and access a variable using a variable +## Get the directory name of a file path + +Alternative to the `dirname` command. + +**Example Function:** + +```sh +dirname() { + # Usage: dirname "path" + printf '%s\n' "${1%/*}/" +} +``` + +**Example Usage:** ```shell -hello_world="test" +$ dirname ~/Pictures/Wallpapers/1.jpg +/home/black/Pictures/Wallpapers/ -# Create the variable name. -var1="world" -var2="hello_${var1}" +$ dirname ~/Pictures/Downloads/ +/home/black/Pictures/ +``` -# Print the value of the variable name stored in 'hello_$var1'. -printf '%s\n' "${!var2}" +## Get the base-name of a file path + +Alternative to the `basename` command. + +**Example Function:** + +```sh +basename() { + # Usage: basename "path" + : "${1%/}" + printf '%s\n' "${_##*/}" +} +``` + +**Example Usage:** + +```shell +$ basename ~/Pictures/Wallpapers/1.jpg +1.jpg + +$ basename ~/Pictures/Downloads/ +Downloads ``` diff --git a/manuscript/chapter6.txt b/manuscript/chapter6.txt index b83c87a..ef9d0ce 100644 --- a/manuscript/chapter6.txt +++ b/manuscript/chapter6.txt @@ -1,57 +1,17 @@ -# Escape Sequences +# Variables -Contrary to popular belief, there's no issue in using raw escape sequences. Using `tput` just abstracts the same ANSI escape sequences. What's worse is that `tput` isn't actually portable, there are a number of different `tput` variants on different Operating Systems each with different commands (*try and run `tput setaf 3` on a FreeBSD system*). The easiest solution ends up being raw ANSI sequences. +## Assign and access a variable using a variable -## Text Colors +```shell +hello_world="test" -**NOTE:** Sequences requiring RGB values only work in True-Color Terminal Emulators. - -| Sequence | What does it do? | Value | -| -------- | ---------------- | ----- | -| `\e[38;5;m` | Set text foreground color. | `0-255` -| `\e[48;5;m` | Set text background color. | `0-255` -| `\e[38;2;;;m` | Set text foreground color to RGB color. | `R`, `G`, `B` -| `\e[48;2;;;m` | Set text background color to RGB color. | `R`, `G`, `B` - -## Text Attributes - -| Sequence | What does it do? | -| -------- | ---------------- | -| `\e[m` | Reset text formatting and colors. -| `\e[1m` | Bold text. | -| `\e[2m` | Faint text. | -| `\e[3m` | Italic text. | -| `\e[4m` | Underline text. | -| `\e[5m` | Slow blink. | -| `\e[7m` | Swap foreground and background colors. | - - -## Cursor Movement - -| Sequence | What does it do? | Value | -| -------- | ---------------- | ----- | -| `\e[;H` | Move cursor to absolute position. | `line`, `column` -| `\e[H` | Move cursor to home position (`0,0`). | -| `\e[A` | Move cursor up N lines. | `num` -| `\e[B` | Move cursor down N lines. | `num` -| `\e[C` | Move cursor right N columns. | `num` -| `\e[D` | Move cursor left N columns. | `num` -| `\e[s` | Save cursor position. | -| `\e[u` | Restore cursor position. | - - -## Erasing Text - -| Sequence | What does it do? | -| -------- | ---------------- | -| `\e[K` | Erase from cursor position to end of line. -| `\e[1K` | Erase from cursor position to start of line. -| `\e[2K` | Erase the entire current line. -| `\e[J` | Erase from the current line to the bottom of the screen. -| `\e[1J` | Erase from the current line to the top of the screen. -| `\e[2J` | Clear the screen. -| `\e[2J\e[H` | Clear the screen and move cursor to `0,0`. +# Create the variable name. +var1="world" +var2="hello_${var1}" +# Print the value of the variable name stored in 'hello_$var1'. +printf '%s\n' "${!var2}" +``` diff --git a/manuscript/chapter7.txt b/manuscript/chapter7.txt index 187315c..b83c87a 100644 --- a/manuscript/chapter7.txt +++ b/manuscript/chapter7.txt @@ -1,67 +1,56 @@ -# Parameter Expansion +# Escape Sequences -## Indirection +Contrary to popular belief, there's no issue in using raw escape sequences. Using `tput` just abstracts the same ANSI escape sequences. What's worse is that `tput` isn't actually portable, there are a number of different `tput` variants on different Operating Systems each with different commands (*try and run `tput setaf 3` on a FreeBSD system*). The easiest solution ends up being raw ANSI sequences. -| Parameter | What does it do? | -| --------- | ---------------- | -| `${!VAR}` | Access a variable based on the value of `VAR`. See: [link](#assign-and-access-a-variable-using-a-variable) -| `${!VAR*}` | Expand to `IFS` separated list of variable names starting with `VAR`. | -| `${!VAR@}` | Expand to `IFS` separated list of variable names starting with `VAR`. | +## Text Colors + +**NOTE:** Sequences requiring RGB values only work in True-Color Terminal Emulators. + +| Sequence | What does it do? | Value | +| -------- | ---------------- | ----- | +| `\e[38;5;m` | Set text foreground color. | `0-255` +| `\e[48;5;m` | Set text background color. | `0-255` +| `\e[38;2;;;m` | Set text foreground color to RGB color. | `R`, `G`, `B` +| `\e[48;2;;;m` | Set text background color to RGB color. | `R`, `G`, `B` + +## Text Attributes + +| Sequence | What does it do? | +| -------- | ---------------- | +| `\e[m` | Reset text formatting and colors. +| `\e[1m` | Bold text. | +| `\e[2m` | Faint text. | +| `\e[3m` | Italic text. | +| `\e[4m` | Underline text. | +| `\e[5m` | Slow blink. | +| `\e[7m` | Swap foreground and background colors. | -## Replacement +## Cursor Movement -| Parameter | What does it do? | -| --------- | ---------------- | -| `${VAR#PATTERN}` | Remove shortest match of pattern from start of string. | -| `${VAR##PATTERN}` | Remove longest match of pattern from start of string. | -| `${VAR%PATTERN}` | Remove shortest match of pattern from end of string. | -| `${VAR%%PATTERN}` | Remove longest match of pattern from end of string. | -| `${VAR/PATTERN/REPLACE}` | Replace first match with string. -| `${VAR//PATTERN/REPLACE}` | Replace all matches with string. -| `${VAR/PATTERN}` | Remove first match. -| `${VAR//PATTERN}` | Remove all matches. - -## Length - -| Parameter | What does it do? | -| --------- | ---------------- | -| `${#VAR}` | Length of var in characters. -| `${#ARR[@]}` | Length of array in elements. - -## Expansion - -| Parameter | What does it do? | -| --------- | ---------------- | -| `${VAR:OFFSET}` | Remove first `N` chars from variable. -| `${VAR:OFFSET:LENGTH}` | Get substring from `N` character to `N` character.
(`${VAR:10:10}`: Get sub-string from char `10` to char `20`) -| `${VAR:: OFFSET}` | Get first `N` chars from variable. -| `${VAR:: -OFFSET}` | Remove last `N` chars from variable. -| `${VAR: -OFFSET}` | Get last `N` chars from variable. -| `${VAR:OFFSET:-OFFSET}` | Cut first `N` chars and last `N` chars. | `bash 4.2+` | - -## Case Modification - -| Parameter | What does it do? | CAVEAT | -| --------- | ---------------- | ------ | -| `${VAR^}` | Uppercase first character. | `bash 4+` | -| `${VAR^^}` | Uppercase all characters. | `bash 4+` | -| `${VAR,}` | Lowercase first character. | `bash 4+` | -| `${VAR,,}` | Lowercase all characters. | `bash 4+` | +| Sequence | What does it do? | Value | +| -------- | ---------------- | ----- | +| `\e[;H` | Move cursor to absolute position. | `line`, `column` +| `\e[H` | Move cursor to home position (`0,0`). | +| `\e[A` | Move cursor up N lines. | `num` +| `\e[B` | Move cursor down N lines. | `num` +| `\e[C` | Move cursor right N columns. | `num` +| `\e[D` | Move cursor left N columns. | `num` +| `\e[s` | Save cursor position. | +| `\e[u` | Restore cursor position. | -## Default Value +## Erasing Text -| Parameter | What does it do? | -| --------- | ---------------- | -| `${VAR:-STRING}` | If `VAR` is empty or unset, use `STRING` as it's value. -| `${VAR-STRING}` | If `VAR` is unset, use `STRING` as it's value. -| `${VAR:=STRING}` | If `VAR` is empty or unset, set the value of `VAR` to `STRING`. -| `${VAR=STRING}` | If `VAR` is unset, set the value of `VAR` to `STRING`. -| `${VAR:+STRING}` | If `VAR` isn't empty, use `STRING` as it's value. -| `${VAR+STRING}` | If `VAR` is set, use `STRING` as it's value. -| `${VAR:?STRING}` | Display an error if empty or unset. -| `${VAR?STRING}` | Display an error if unset. +| Sequence | What does it do? | +| -------- | ---------------- | +| `\e[K` | Erase from cursor position to end of line. +| `\e[1K` | Erase from cursor position to start of line. +| `\e[2K` | Erase the entire current line. +| `\e[J` | Erase from the current line to the bottom of the screen. +| `\e[1J` | Erase from the current line to the top of the screen. +| `\e[2J` | Clear the screen. +| `\e[2J\e[H` | Clear the screen and move cursor to `0,0`. diff --git a/manuscript/chapter8.txt b/manuscript/chapter8.txt index 4a926e1..187315c 100644 --- a/manuscript/chapter8.txt +++ b/manuscript/chapter8.txt @@ -1,42 +1,68 @@ -# Brace Expansion +# Parameter Expansion -## Ranges +## Indirection -```shell -# Syntax: {..} +| Parameter | What does it do? | +| --------- | ---------------- | +| `${!VAR}` | Access a variable based on the value of `VAR`. See: [link](#assign-and-access-a-variable-using-a-variable) +| `${!VAR*}` | Expand to `IFS` separated list of variable names starting with `VAR`. | +| `${!VAR@}` | Expand to `IFS` separated list of variable names starting with `VAR`. | -# Print numbers 1-100. -echo {1..100} -# Print range of floats. -echo 1.{1..9} +## Replacement -# Print chars a-z. -echo {a..z} -echo {A..Z} +| Parameter | What does it do? | +| --------- | ---------------- | +| `${VAR#PATTERN}` | Remove shortest match of pattern from start of string. | +| `${VAR##PATTERN}` | Remove longest match of pattern from start of string. | +| `${VAR%PATTERN}` | Remove shortest match of pattern from end of string. | +| `${VAR%%PATTERN}` | Remove longest match of pattern from end of string. | +| `${VAR/PATTERN/REPLACE}` | Replace first match with string. +| `${VAR//PATTERN/REPLACE}` | Replace all matches with string. +| `${VAR/PATTERN}` | Remove first match. +| `${VAR//PATTERN}` | Remove all matches. -# Nesting. -echo {A..Z}{0..9} +## Length -# Print zero-padded numbers. -# CAVEAT: bash 4+ -echo {01..100} +| Parameter | What does it do? | +| --------- | ---------------- | +| `${#VAR}` | Length of var in characters. +| `${#ARR[@]}` | Length of array in elements. -# Change increment amount. -# Syntax: {....} -# CAVEAT: bash 4+ -echo {1..10..2} # Increment by 2. -``` +## Expansion -## String Lists +| Parameter | What does it do? | +| --------- | ---------------- | +| `${VAR:OFFSET}` | Remove first `N` chars from variable. +| `${VAR:OFFSET:LENGTH}` | Get substring from `N` character to `N` character.
(`${VAR:10:10}`: Get sub-string from char `10` to char `20`) +| `${VAR:: OFFSET}` | Get first `N` chars from variable. +| `${VAR:: -OFFSET}` | Remove last `N` chars from variable. +| `${VAR: -OFFSET}` | Get last `N` chars from variable. +| `${VAR:OFFSET:-OFFSET}` | Cut first `N` chars and last `N` chars. | `bash 4.2+` | -```shell -echo {apples,oranges,pears,grapes} +## Case Modification + +| Parameter | What does it do? | CAVEAT | +| --------- | ---------------- | ------ | +| `${VAR^}` | Uppercase first character. | `bash 4+` | +| `${VAR^^}` | Uppercase all characters. | `bash 4+` | +| `${VAR,}` | Lowercase first character. | `bash 4+` | +| `${VAR,,}` | Lowercase all characters. | `bash 4+` | + + +## Default Value + +| Parameter | What does it do? | +| --------- | ---------------- | +| `${VAR:-STRING}` | If `VAR` is empty or unset, use `STRING` as it's value. +| `${VAR-STRING}` | If `VAR` is unset, use `STRING` as it's value. +| `${VAR:=STRING}` | If `VAR` is empty or unset, set the value of `VAR` to `STRING`. +| `${VAR=STRING}` | If `VAR` is unset, set the value of `VAR` to `STRING`. +| `${VAR:+STRING}` | If `VAR` isn't empty, use `STRING` as it's value. +| `${VAR+STRING}` | If `VAR` is set, use `STRING` as it's value. +| `${VAR:?STRING}` | Display an error if empty or unset. +| `${VAR?STRING}` | Display an error if unset. -# Example Usage: -# Remove dirs Movies, Music and ISOS from ~/Downloads/. -rm -rf ~/Downloads/{Movies,Music,ISOS} -``` diff --git a/manuscript/chapter9.txt b/manuscript/chapter9.txt index 5fc7c0c..4a926e1 100644 --- a/manuscript/chapter9.txt +++ b/manuscript/chapter9.txt @@ -1,30 +1,41 @@ -# Arithmetic +# Brace Expansion -## Simpler syntax to set variables +## Ranges ```shell -# Simple math -((var=1+2)) +# Syntax: {..} -# Decrement/Increment variable -((var++)) -((var--)) -((var+=1)) -((var-=1)) +# Print numbers 1-100. +echo {1..100} -# Using variables -((var=var2*arr[2])) +# Print range of floats. +echo 1.{1..9} + +# Print chars a-z. +echo {a..z} +echo {A..Z} + +# Nesting. +echo {A..Z}{0..9} + +# Print zero-padded numbers. +# CAVEAT: bash 4+ +echo {01..100} + +# Change increment amount. +# Syntax: {....} +# CAVEAT: bash 4+ +echo {1..10..2} # Increment by 2. ``` -## Ternary tests +## String Lists ```shell -# Set the value of var to var2 if var2 is greater than var. -# var: variable to set. -# var2>var: Condition to test. -# ?var2: If the test succeeds. -# :var: If the test fails. -((var=var2>var?var2:var)) +echo {apples,oranges,pears,grapes} + +# Example Usage: +# Remove dirs Movies, Music and ISOS from ~/Downloads/. +rm -rf ~/Downloads/{Movies,Music,ISOS} ```