Skip to content

Instantly share code, notes, and snippets.

@the0neWhoKnocks
Last active March 29, 2023 09:50
Show Gist options
  • Save the0neWhoKnocks/8cd5607ecd2329cf819cbce29d1b634f to your computer and use it in GitHub Desktop.
Save the0neWhoKnocks/8cd5607ecd2329cf819cbce29d1b634f to your computer and use it in GitHub Desktop.
Bash Scripting

Bash Scripting


Shebang

#!/usr/bin/env bash

Error handling

Manually handle the error

chmod +x /root/FILE.sh
if [ $? -ne 0 ]; then
  echo "[ERROR] chmod failed"
  exit 1
fi
# with a function
function handleError {
  if [ $1 -ne 0 ]; then
    echo;
    echo " [ERROR] $2"
    echo;
    exit $1
  fi
}

ls /bad/path
handleError $? "Failed to list path"
echo "next step"

Have the script exit automatically when an error occurs (this can have adverse effects in some cases)

set -e

chmod +x /root/FILE.sh

Magic variables

Var Description
$RANDOM It gives you a more or less random integer between 0 and 32767.

Redirection

Type Operator Action Example
stdout Standard Output 1> Overwrite
ls -la ~/ 1> "./out.log"
stderr Standard Error 2> Overwrite
cat /etc/sudoers 2> "err.log"
stdout and stderr &> Overwrite
ls -la ~/ &> "./both.log"
or
cat /etc/sudoers &> "error.log"
stdout 1>> Append
ls -la ~/ 1>> "./out.log" && ls -la ~/ 1>> "./out.log"
stderr 2>> Append
cat /etc/sudoers 2>> "err.log" && cat /etc/sudoers 2>> "err.log"
stdout and stderr &>> Append
ls -la ~/ &>> "./both.log" && cat /etc/sudoers &>> "both.log"

String prefixed with dollar

Type Definition Description
$' ANSI C Quoting Words of the form $'string' are treated specially. The word expands to string, with backslash-escaped characters replaced as specified by the ANSI C standard.
$" Locale translation A double-quoted string preceded by a dollar sign (‘$’) will cause the string to be translated according to the current locale. If the current locale is C or POSIX, the dollar sign is ignored. If the string is translated and replaced, the replacement is double-quoted.

Math

For integers

# Subtraction
expr 1 - 1
# Addition
expr 1 + 1
expr $myvar + 1
# Division
expr $myvar / 3
# Multiplication
expr $myvar \* 3

# via vars
myvar=6
let myvar+=1
let myvar+1
let myvar2=myvar+1

# declare and evaluate multiple items at once
let x=4 y=5 z=x*y u=z/2

Arithmetic Expansion

The $((...)) notation is what is called the Arithmetic Expansion while the ((...)) notation is called a compound command used to evaluate an arithmetic expression in Bash.

The Arithmetic Expansion notation should be the preferred way unless doing an arithmetic evaluation in a Bash if statement, in a Bash for loop, or similar statements.

# one-off
echo $((2 + 2))

# with a var
sum=$((2 + 2))
((sum+=2))
((sum++))

# evaluate multiple items (commas required)
echo $((x=4, y=5, z=x*y, u=z/2))

For floats (just use awk)

# one-off
awk 'BEGIN { print 100/3 }'

# pipes
echo '100' | awk '{ print $1/3 }'

# round up
echo '1.5' | awk '{ printf("%.0f\n", $1) }'

Floating point rounding in GNU awk may not always behave as expected. For example, both 1.5 and 2.5 round to 2, since the FP system rounds halves to the nearest even number, not up.

File existence

# directory exists
if [ -d "<PATH>" ]; then
  # logic
fi
# directory doesn't exist
if [ ! -d "<PATH>" ]; then
  # logic
fi

# file exists 
if [ -f "<PATH>" ]; then
  # logic
fi
# file doesn't exist
if [ ! -f "<PATH>" ]; then
  # logic
fi

Writing to a file

If you have permissions for the file

# overwrite file
echo "text" > "<FILE>"
# append to file
echo "text" >> "<FILE>"

If it requires sudo

# overwrite file
echo "text" | sudo tee "<FILE>" > /dev/null
# append to file
echo "text" | sudo tee -a "<FILE>" > /dev/null

Setting file permissions

# make file executable
chmod +x "<FILE>"

# make read/writable by all
chmod a+rw "<FILE>"
# make read/writable by group
chmod g+rw "<FILE>"
# make read/writable by user (`u` not neccessary since it's the default, just calling out for completeness)
chmod u+rw "<FILE>"

Parsing Arguments

ENABLE_X=false
remainingArgs=()

while [[ $# -gt 0 ]]; do
  case $1 in
    -a1|--arg1) # arg with value
      VAR1="$2"
      shift 2
      ;;
    -a2|--arg2) # arg as a flag
      ENABLE_X=true
      shift
      ;;
    -*|--*)
      echo "Unknown option $1"
      exit 1
      ;;
    *)
      remainingArgs+=("$1")
      shift
      ;;
  esac
done
set -- "${remainingArgs[@]}"

Joining Array Items

envVars=("VAR1=val1" "VAR2=val2")

# construct vars for 'docker run' (notice the use of --, which is needed to tell printf not to interpret the next items as commandline options since the format has '-e' which could be interpreted as a flag)
args=$(printf -- "-e %s " "${baseEnvVars[@]}")

# construct vars for 'docker-compose' (-- not needed for this format)
args=$(printf "export %s; " "${baseEnvVars[@]}")

HereDoc

Use to output multiline formatted file

cat << EOF > file.txt
The current working directory is: $PWD
You are logged in as: $(whoami)
EOF

if true; then
  cat <<- EOF
  Line with a leading tab.
  EOF
fi

Substitution

${parameter:-defaultValue}: Default value for variable

VAR=${parameter:-"some value"}

${parameter:=defaultValue}: Assign default value if one doesn't exist

${USER:=nobody}

${parameter:?"error message"}: Exit with error message if parameter not set

${1:?"ERROR: You didn't provide a required argument"}

${#parameter}: Get length of parameter

[[ ${#var} -ge 9 ]] && { echo "var too long"; exit 1; }

${var#Pattern}: Remove pattern from front

URL="http://domain.com/file.tar.gz"
echo "${URL#*/}"
# result: /domain.com/file.tar.gz

${var##Pattern}: Remove last occurance of pattern, from front

URL="http://domain.com/file.tar.gz"
echo "${URL##*/}"
# result: file.tar.gz

${var%Pattern}: Remove pattern from end

FILE="file.tar.gz"
echo "${FILE%.*}"
# result: file.tar

${var%%Pattern}: Remove last occurance of pattern, from end

FILE="file.tar.gz"
echo "${FILE%%.*}"
# result: file

${parameter/pattern/string}: Replace pattern with String

FILENAME="example.tbn"
echo "${FILENAME/.tbn/-thumb.jpg}"
# result: example-thumb.jpg

${parameter//pattern/string}: Replace all occurances of pattern with String

FILENAME="/some/old/path/with/old/file.jpg"
echo "${FILENAME//old/new}"
# result: /some/new/path/with/new/file.jpg

${parameter:offset:length}: Extract section of parameter

URL="http://domain.com/path"
echo "${parameter:7:10}"
# result: domain.com

${!pattern}: Get all variables starting with pattern

VAR_1="One"
VAR_2="Two"
VAR_3="Three"
echo "${!VAR_*}"

${var^}: Capitalize variable

VAR="name"
echo "${VAR^}"
result: Name

${var^^}: Uppercase variable

VAR="name"
echo "${VAR^^}"
result: NAME

${var,}: Lowercase first letter in variable

VAR="NAME"
echo "${VAR,}"
result: nAME

${var,,}: Lowercase variable

VAR="NAME"
echo "${VAR,,}"
result: name

Examples

Replacing a token with a variable that contains slashes. Just switch from the / delimeter to another character.

echo '"${PWD}/script.sh" -i "${PWD}/temp.log"' | sed "s|\${PWD}|${PWD}|g"

Un/comment a line

# uncomment
sed "/^#zstyle ':omz:update' mode disabled/s/^#//" ~/.zshrc

# comment
sed "/^zstyle ':omz:update' mode disabled/s/^/#/" ~/.zshrc

Replace multiple lines

  • start token: /plugins=(/
  • end token: /)/
sed "/plugins=(/,/)/c\plugins=(\n  NEW_VALUE\n)" ~/.zshrc

List folders and files as a list. Folders first, then files.

# A: all files execpt . and ..
# d: only directory names, not their contents
# 1: one item per line
# p: add a trailing slash to folders
ls -Ad1p --group-directories-first <FOLDER_PATH>

If binary doesn't exist, install it

if ! command -v BINARY_NAME &> /dev/null; then
  # install
fi

Insert text at the top of a file

text='# comment\n'
text+='FU=bar\n\n'
sed -i '1 i\'"${text}" "${HOME}/.zshrc"

Run logic if text not in file

if ! grep -q "$user2" /etc/passwd; then
  echo "User does not exist!!"
fi

Get currently running script path and name

SCRIPT_PATH="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
SCRIPT_NAME=$(basename "$0")

echo "${SCRIPT_PATH}/${SCRIPT_NAME}"

Parse columns of data. They key bit being awk -v col=2 '{print $col}'.

echo 'col1 col2 col3 col4' | awk -v col=2 '{print $col}'
# output: col2

echo 'col1 col2 col3 col4' | awk -v col2=2 -v col4=4 '{print $col2, $col4}'
# output: col2 col4
# Note the comma is neccessary for spacing

# Specify an alternate seperator with '-F'. Note that a multi-character separator may only work in certain versions of awk.
# A multi-character separator would be `-F '[-_]'`, just like a regex. 
echo 'col1|col2|col3|col4' | awk -F '|' -v col=2 '{print $col}'
# output: col2

List folders that don't contain a specific file (in this case thumb.jpg)

find . -mindepth 1 -maxdepth 1 -type d '!' -exec test -e "{}/thumb.jpg" ';' -print | sort

Remove files based on part of it's name. -print0 is used to print out each file on the same line (as an argument), and xargs -0 is used to pass each item from the pipe to rm. The -0 is to handle possible spaces in an arg.

find ./src/ -name "*.test.js" -print0 | xargs -0 rm

Get a list of files withing a directory based on their extension.

find "<DIRECTORY>" -type f \( -name "*.mp4" -o -name "*.mkv" -o -name "*.avi" \)

Handle multiple folders or files within specific directories. You can use multiple nested braces to manipulate any folder or file you want.

# copy a folder and file to another directory
cp -rp ./parent/child/{folder,file.json} ../another_parent/child/

# move folders and files to another directory
mv ./parent/child/{folder/{nested1,nested2},file.json} ../another_parent/child/

Print the full path of a file

find "${PWD}/<FILE>"

Print the name of a file without it's extension

echo "$(basename "<FILE>")" | { read n; echo "${n%.*}"; }

Prompt for a response. Note that ZSH uses the format read "<VAR>?<QUERY>", where as Bash sometimes uses read -r "<QUERY>" <VAR>.

##
# store previous session data
onExit() {
  if [[ "${PWD}" != "${HOME}" ]]; then
    echo "export CUSTOM__PREV_DIR='${PWD}'" > ~/.prev_term
  fi
}
trap onExit EXIT
if [ -f ~/.prev_term ]; then
  source ~/.prev_term
  
  # only prompt to load previous dir if one has been saved, and a new shell was started
  if [[ "${CUSTOM__PREV_DIR}" != "" ]] && [[ "${CUSTOM__PREV_DIR}" != "${HOME}" ]] && [[ "${PWD}" == "${HOME}" ]]; then  
    while true; do
      read "yn?Load '${CUSTOM__PREV_DIR}' (y/n)?: "
      case $yn in
        [Yy]* )
          cd "${CUSTOM__PREV_DIR}"
          break
          ;;
        [Nn]* ) break;;
        * ) echo "Please answer yes or no.";;
      esac
    done
  fi
fi

Sources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment