Simple Bash framework which provides argument parsing, usage output and text formatting variables
Find a file
2025-01-31 16:29:26 +01:00
examples correct typos 2025-01-31 16:29:26 +01:00
images update logo 2022-03-12 23:27:34 +01:00
LICENSE add license 2022-01-11 15:41:04 +01:00
README.org correct typos 2025-01-31 16:29:26 +01:00
sf correct typos 2025-01-31 16:29:26 +01:00

sf script framework

/denis/sf/media/branch/main/images/logo.png

script framework can be used to simplify and beautify Bash scripts. It provides:

  • Argument parsing
  • Usage output
  • Input functions
  • Output functions
  • Text formatting variables

All just by declaring some variables and sourcing it. Or keep your scripts self-contained and include it as an oneliner.

The usage is pretty self-explanatory once you have seen it. If you're curious and don't want to read through the documentation, head directly to the examples.


Here is the oneliner version of sf which was created with this tool:

  # sf -- script framework (https://github.com/Deleh/sf)
  sftrs=$'\e[0m';sftbf=$'\e[1m';sftdim=$'\e[2m';sftul=$'\e[4m';sftblk=$'\e[5m';sftinv=$'\e[7m';sfthdn=$'\e[8m';sftclr=$'\e[1A\e[K';sftk=$'\e[30m';sftr=$'\e[31m';sftg=$'\e[32m';sfty=$'\e[33m';sftb=$'\e[34m';sftm=$'\e[35m';sftc=$'\e[36m';sftw=$'\e[97m';function sferr { echo -e "${sftbf}${sftr}ERROR${sftrs} $1";[ -z "$2" ]&&exit 1;};function sfwarn { echo -e "${sftbf}${sfty}WARNING${sftrs} $1";};function sfask { if [ -n "$2" ];then echo -ne "$1? [${sftbf}y${sftrs}/${sftbf}N${sftrs}] ";read -r sfin;[[ "$sfin" =~ n|N|^$ ]]&&sfin=false||sfin=true;else echo -ne "$1? [${sftbf}Y${sftrs}/${sftbf}n${sftrs}] ";read -r sfin;[[ "$sfin" =~ y|Y|^$ ]]&&sfin=true||sfin=false;fi;};function sfget { if [ -n "$2" ];then read -r -p "$1 [${sftbf}$2${sftrs}]: " sfin;else read -r -p "$1: " sfin;fi;[ "$sfin" == "" ]&&[ "$2" != "" ]&&sfin="$2";};function _sferr { echo "${sftbf}${sftr}SF PARSE ERROR${sftrs} $1";exit 1;};OLDIFS=$IFS;IFS=";";_sfpargs=();_sfpheads=();_sfpoffset=0;_sfptails=();_sfpusage="";_sfoheads=();_sfooffset=0;_sfotails=();declare -A _sfflags;declare -A _sfargs;sfargs=("${sfargs[@]}" "help;h;Show this help message and exit");for a in "${sfargs[@]}";do _sfsubst=${a//";"};_sfcount="$((${#a} - ${#_sfsubst}))";if [ "$_sfcount" -eq 1 ];then read -r -a _sfparsearr<<<"${a}";[[ " ${_sfpargs[*]} " =~ " ${_sfparsearr[0]} " ]]&&_sferr "${sftbf}${_sfparsearr[0]}${sftrs} is already set: ${sftbf}${a}${sftrs}";_sfpargs+=("${_sfparsearr[0]}");_sfpusage="$_sfpusage ${_sfparsearr[0]}";_sfphead="${_sfparsearr[0]}";[ "${#_sfphead}" -gt "${_sfpoffset}" ]&&_sfpoffset="${#_sfphead}";_sfpheads+=("$_sfphead");_sfptails+=("${_sfparsearr[1]}");elif [ "$_sfcount" -eq 2 ];then read -r -a _sfparsearr<<<"${a}";[ -n "${_sfflags["--${_sfparsearr[0]}"]}" ]&&_sferr "${sftbf}${_sfparsearr[0]}${sftrs} is already set: ${sftbf}${a}${sftrs}";_sfflags["--${_sfparsearr[0]}"]="${_sfparsearr[0]}";[ -n "${_sfflags["-${_sfparsearr[1]}"]}" ]&&_sferr "${sftbf}${_sfparsearr[1]}${sftrs} is already set: ${sftbf}${a}${sftrs}";_sfflags["-${_sfparsearr[1]}"]="${_sfparsearr[0]}";declare "${_sfparsearr[0]//-/_}"=false;_sfohead="-${_sfparsearr[1]}, --${_sfparsearr[0]}";[ "${#_sfohead}" -gt "${_sfooffset}" ]&&_sfooffset="${#_sfohead}";_sfoheads+=("$_sfohead");_sfotails+=("${_sfparsearr[2]}");elif [ "$_sfcount" -eq 4 ];then read -r -a _sfparsearr<<<"${a}";[ -n "${_sfargs["--${_sfparsearr[0]}"]}" ]&&_sferr "${sftbf}${_sfparsearr[0]}${sftrs} is already set: ${sftbf}${a}${sftrs}";_sfargs["--${_sfparsearr[0]}"]="${_sfparsearr[0]}";[ -n "${_sfargs["-${_sfparsearr[1]}"]}" ]&&_sferr "${sftbf}${_sfparsearr[1]}${sftrs} is already set: ${sftbf}${a}${sftrs}";_sfargs["-${_sfparsearr[1]}"]="${_sfparsearr[0]}";declare "${_sfparsearr[0]//-/_}"="${_sfparsearr[3]}";_sfohead="-${_sfparsearr[1]}, --${_sfparsearr[0]} ${_sfparsearr[2]}";[ "${#_sfohead}" -gt "${_sfooffset}" ]&&_sfooffset="${#_sfohead}";_sfoheads+=("$_sfohead");[ "${_sfparsearr[3]}" != "" ]&&_sfotails+=("${_sfparsearr[4]} (default: ${_sfparsearr[3]})")||_sfotails+=("${_sfparsearr[4]}");else _sferr "Wrong argument declaration: ${sftbf}${a}${sftrs}";fi;done;_sfeheads=();_sfetails=();_sfeoffset=0;for e in "${sfexamples[@]}";do _sfsubst=${e//";"};_sfcount="$((${#e} - ${#_sfsubst}))";if [ "$_sfcount" -eq 1 ];then read -r -a _sfparsearr<<<"${e}";_sfehead="${_sfparsearr[0]}";[ "${#_sfehead}" -gt "${_sfeoffset}" ]&&_sfeoffset="${#_sfehead}";_sfeheads+=("$_sfehead");_sfetails+=("${_sfparsearr[1]}");else _sferr "Wrong example declaration: ${sftbf}${e}${sftrs}";fi;done;IFS=$OLDIFS;[ "$sfparr" == true ]&&[ "${#_sfpargs[@]}" == 0 ]&&_sferr "At least one positional argument must be used with ${sftbf}sfparr${sftrs}";[ "$sfparr" == true ]&&_sfpusage="${_sfpusage% *} [${_sfpusage##* } ...]";_sfpoffset=$(("_sfpoffset" + 3));_sfooffset=$(("_sfooffset" + 3));_sfeoffset=$(("_sfeoffset" + 3));_sfwidth=$(stty size|cut -d ' ' -f 2);_sfpdesc="";for i in "${!_sfptails[@]}";do _sfptail="${_sfptails[$i]}";if [ "$((${#_sfptail} + _sfpoffset))" -gt "$_sfwidth" ];then _sftmpwidth="$((_sfwidth - _sfpoffset))";_sftmpwidth=$(echo -e "${_sftmpwidth}\n1"|sort -nr|head -n 1);_sfptail=$(echo "$_sfptail"|fold -s -w "$_sftmpwidth");_sfptail="${_sfptail//$' \n'/$'\n;'}";fi;_sfpdesc="${_sfpdesc}  ${_sfpheads[$i]};${_sfptail}\n";done;_sfodesc="";for i in "${!_sfotails[@]}";do _sfotail="${_sfotails[$i]}";if [ "$((${#_sfotail} + _sfooffset))" -gt "$_sfwidth" ];then _sftmpwidth="$((_sfwidth - _sfooffset))";_sftmpwidth=$(echo -e "${_sftmpwidth}\n1"|sort -nr|head -n 1);_sfotail=$(echo "$_sfotail"|fold -s -w "$_sftmpwidth");_sfotail="${_sfotail//$' \n'/$'\n;'}";fi;_sfodesc="${_sfodesc}  ${_sfoheads[$i]};${_sfotail}\n";done;_sfexamples="";for i in "${!_sfetails[@]}";do _sfetail="${_sfetails[$i]}";if [ "$((${#_sfetail} + _sfeoffset))" -gt "$_sfwidth" ];then _sftmpwidth="$((_sfwidth - _sfeoffset))";_sftmpwidth=$(echo -e "${_sftmpwidth}\n1"|sort -nr|head -n 1);_sfetail=$(echo "$_sfetail"|fold -s -w "$_sftmpwidth");_sfetail="${_sfetail//$' \n'/$'\n;'}";fi;_sfexamples="${_sfexamples}  ${_sfeheads[$i]};${_sfetail}\n";done;function _sfusage { echo -n "Usage: $(basename "$0") [OPTIONS]";echo -ne "$_sfpusage";echo;[ -n "${sfdesc}" ]&&echo -e "\n$sfdesc"|fold -s -w "$_sfwidth";if [ "$_sfpdesc" != "" ];then echo -e "\nPOSITIONAL ARGUMENTS";echo -e "$_sfpdesc"|column -s ";" -t;fi;if [ "$_sfodesc" != "" ];then echo -e "\nOPTIONS";echo -e "$_sfodesc"|column -s ";" -t;fi;if [ "$_sfexamples" != "" ];then echo -e "\nEXAMPLES";echo -e "$_sfexamples"|column -s ";" -t;fi;[ -n "${sfextra}" ]&&echo -e "\n$sfextra"|fold -s -w "$_sfwidth";exit 0;};for a in "$@";do [ "$a" == "-h" ]||[ "$a" == "--help" ]&&_sfusage;done;for d in "${sfdeps[@]}";do if ! command -v "$d"&>/dev/null;then sferr "Command ${sftbf}${d}${sftrs} not found" 0;_sfdeperr=true;fi;done;[ "$_sfdeperr" == true ]&&exit 1;while(("$#"));do if [ -n "${_sfflags["$1"]}" ];then declare "${_sfflags["$1"]//-/_}"=true;elif [ -n "${_sfargs["$1"]}" ];then if [ -n "$2" ]&&[ "${2:0:1}" != "-" ];then declare "${_sfargs["$1"]//-/_}"="$2";shift;else sferr "Argument for ${sftbf}${1}${sftrs} missing";fi;else if [ "${1:0:1}" == "-" ];then sferr "Unsupported argument/flag ${sftbf}${1}${sftrs}";else if [ "${#_sfpargs[@]}" != 0 ];then declare "${_sfpargs[0]//-/_}"="$1";[ "$sfparr" == true ]&&_sfplast="${_sfpargs[0]//-/_}"&&_sfparr=("$1");_sfpargs=("${_sfpargs[@]:1}");elif [ "$sfparr" == true ];then _sfparr+=("$1");else sferr "Too many positional arguments";fi;fi;fi;shift;done;[ "$sfparr" == true ]&&[ "${#_sfparr[@]}" -gt 0 ]&&read -r -a "${_sfplast?}"<<<"${_sfparr[@]}";[ "$sfparr" == true ]&&[ "${#_sfpargs[@]}" -gt 0 ]&&unset '_sfpargs[${#_sfpargs[@]}-1]';if [ "${#_sfpargs[@]}" -gt 0 ];then for p in "${_sfpargs[@]}";do sferr "Positional argument ${sftbf}${p}${sftrs} missing" 0;done;exit 1;fi;unset a d e i OLDIFS _sfargs _sfehead _sfeheads _sfeoffset _sferr _sfetails _sfexamples _sfflags _sfodesc _sfohead _sfoheads _sfooffset _sfotails _sfpargs _sfparr _sfpdesc _sfphead _sfpheads _sfplast _sfpoffset _sfptails _sfpusage _sftmpwidth _sfusage _sfwidth

Requirements

  • At least Bash 4.x

Usage

The general usage for writing a script with sf is:

  1. Declare sf-variables at the top of your script
  2. Include sf
  3. Write your script with already parsed arguments, input functions, output functions and text formatting variables

1. sf-variables

This is the list of variables which can be set before including sf. Everything is optional.

Name Description Example
sfdesc Description of the script sfdesc="This script does nothing"
sfargs Array for declaration of arguments, positional arguments and flags. Look below for more information See below
sfparr Flag which indicates if the last declared positional argument should be treated as array sfparr=true
sfexamples Array for declaration of examples for the usage output. Look below for more information See also below
sfextra Additional usage output sfextra="No copyright"
sfdeps Array for declaration of script dependencies. An error is thrown if one ore more of the set command are not available sfdeps=("ffmpeg" "opusinfo")

Examples which show the usage of all variables can be found below and in the examples directory.

sfargs

This is an array of strings. Every string defines an argument, a flag or a positional argument of the script. The type is defined by the amount of semicolons in the string.

Type Declaration order Example
Positional argument <name>;<description> sfargs+=("FILE;File to read")
Flag <name>;<shorthand>;<description> sfargs+=("verbose;v;Enable verbose output")
Argument <name>;<shorthand>;<value_name>;<default_value>;<description> sfargs+=("text;t;TEXT;done;Print TEXT when finished")

The order of declaration defines the order in the usage output.

sfexamples

This is also an array of strings. Examples are of the form <command>;<description> and can be added to sf like this:

  sfexamples+=("count 8;Count to eight")

2. Include sf

There are three methods of including sf:

  1. Grab the sf file from this repo, place it next to your script and source it:

      source "$(dirname $0)/sf"
  2. Copy and paste the oneliner from the top of this README
  3. Source sf from the web for example with curl:

      source <(curl -s https://raw.githubusercontent.com/Deleh/sf/main/sf)

    Note that this adds an online dependency to your script AND that sourcing from a web resource might be a potential security issue. The main branch should only be used for testing purposes in this method. Replace main in the URL with a commit hash to prevent future changes in sf breaking your script.

3. Write your script

sf deals with missing inputs and handles the parsing of arguments. This means that after sf was included you can be sure that all variables have assigned values. Flags are either false or true, arguments have a provided value or the default value and positional arguments have a provided value.

The values are stored in variables with the name $<name>. If you declared for example a flag like this:

  sfargs+=("verbose;v;Enable verbose output")

Then the variable $verbose exists with a value of either false or true.

Note that dashes in declared sfargs variable names get replaced with underscores.

Input functions

User input can be requested with two functions. After calling a function, the user input is provided in the variable $sfin.

sfask Takes a string as input and asks for yes or no. If an additional argument is provided (doesn't matter what), no will be default. $sfin is either true or false
sfget Takes a string as input and asks for user input. If a second argument is provided, this will be the default if no user input was entered

Note that the functions append a colon/question mark to the given string.

Look at the greet example to see the functions in action.

Output functions

Two output functions are provided which can be used to throw warnings and errors.

sfwarn Takes a string as input and prints a warning
sferr Takes a string as input, prints an error and exits with code 1. If an additional argument is passed (doesn't matter what), it will just throw an error and don't exit
Text formatting variables

The following text formatting variables can be used to modify the output:

sftrs Reset formatting
sftbf Bold
sftdim Dim
sftul Underlined
sftblk Blinking
sftinv Invert foreground/background
sfthdn Hidden
sftclr Clear the previous line
sftk Black
sftr Red
sftg Green
sfty Yellow
sftb Blue
sftm Magenta
sftc Cyan
sftw White

The variables can be used directly in echo, no -e needed. To echo the word "framework" bold and red use the variables for example like this:

  echo "${sftbf}${sftr}framework${sftrs}"

Look at the clear example to see some of them in action.

Examples

All examples can also be found in the examples directory. Play around with the sf-variables and see what happens.

Count

This example script counts from/to a number and shows the general usage of sf-variables:

  #!/usr/bin/env bash

  # ----------------------
  # sf -- script framework
  # ----------------------

  # Declare sf variables
  sfdesc="A simple counter."

  sfargs+=("N;Number to count")
  sfargs+=("reverse;r;Count reverse")
  sfargs+=("text;t;TEXT;done;Print TEXT when finished counting")

  sfexamples+=("count 8;Count to eight")
  sfexamples+=("count -r -t go 3;Count reverse from 3 and print 'go'")

  sfextra="No copyright at all."

  # Include sf, this could be replaced with a long oneliner
  source "$(dirname $0)/sf"

  # ----------------------
  # Actual script
  # ----------------------

  if [ "$N" -gt 10 ]; then                    # Use parsed positional argument
      sferr "I can only count to/from 10"     # Throw an error and exit
  fi

  counter="$N"                                # Use parsed positional argument
  echo -n "$sftbf"                            # Print everything from here bold
  while [ "$counter" -gt 0 ]; do
      if [ "$reverse" == true ]; then         # Use parsed flag
          echo "  $counter"
      else
          echo "  $(expr $N - $counter + 1)"  # Use parsed positional argument
      fi
      counter=$(expr $counter - 1)
      sleep 1
  done
  echo -n "$sftrs"                            # Reset text formatting
  echo "  $text"                              # Use parsed argument

The usage output of the script is:

  Usage: count [OPTIONS] N

  A simple counter.

  POSITIONAL ARGUMENTS
    N  Number to count

  OPTIONS
    -r, --reverse    Count reverse
    -t, --text TEXT  Print TEXT when finished counting (default: done)
    -h, --help       Show this help message and exit

  EXAMPLES
    count 8           Count to eight
    count -r -t go 3  Count reverse from 3 and print 'go'

  No copyright at all.

An example call looks like this:

  $ ./count -r -t go 3
    3
    2
    1
    go

Clear

This script shows the usage of color formatting variables and $sftclr:

  #!/usr/bin/env bash

  # ----------------------
  # sf -- script framework
  # ----------------------

  # Declare sf variables
  sfdesc="Show the usage of color variables and \$sftclr."

  # Include sf, this could be replaced with a long oneliner
  source "$(dirname $0)/sf"

  # ----------------------
  # Actual script
  # ----------------------

  echo -n "${sftbf}"                                                # Output everything from here bold
  echo "${sftr}These"                                               # Red
  sleep 0.5
  echo "${sftm}lines"                                               # Magenta
  sleep 0.5
  echo "${sftb}will"                                                # Blue
  sleep 0.5
  echo "${sftc}delete"                                              # Cyan
  sleep 0.5
  echo "${sftg}themselves"                                          # Green
  sleep 1
  echo "${sfty}now!"                                                # Yellow
  sleep 0.5
  echo -n "${sftclr}${sftclr}${sftclr}${sftclr}${sftclr}${sftclr}"  # Clear six lines
  echo "${sftblk}${sftr}T${sftm}a${sftb}d${sftc}a${sftg}a${sfty}!"  # Blinking colorful
  echo -n "${sftrs}"                                                # Reset text formatting

The produced usage is:

  Usage: clear [OPTIONS]

  Show the usage of color variables and $sftclr.

  OPTIONS
    -h, --help  Show this help message and exit

The execution results in this:

  $ ./clear
  Tadaa!

Add

This script adds numbers and shows the usage of sfparr:

  #!/usr/bin/env bash

  # ----------------------
  # sf -- script framework
  # ----------------------

  # Declare sf variables
  sfdesc="Calculate the sum of multiple numbers."

  sfargs+=("NUMBERS;Numbers which will be added")
  sfargs+=("verbose;v;Enable verbose output")

  sfparr=true  # Treat the last declared positional argument as array

  # Include sf, this could be replaced with a long oneliner
  source "$(dirname $0)/sf"

  # ----------------------
  # Actual script
  # ----------------------

  sum=0

  for n in "${NUMBERS[@]}"; do         # Use parsed positional argument array
      if [ "$verbose" == true ]; then  # Use parsed flag
          echo -n "$sum + $n = "
      fi
      sum="$(expr $sum + $n)"
      if [ "$verbose" == true ]; then  # Use parsed flag
          echo "$sftbf$sum$sftrs"      # Use text formatting variables
      fi
  done

  echo "The sum is: $sftbf$sum$sftrs"  # Use text formatting variables

And here is the produced usage:

  Usage: add [OPTIONS] [NUMBERS ...]

  Calculate the sum of multiple numbers.

  POSITIONAL ARGUMENTS
    NUMBERS  Numbers which will be added

  OPTIONS
    -v, --verbose  Enable verbose output
    -h, --help     Show this help message and exit

An example call looks like this:

  $ ./add -v 1 2 3 4 5
  0 + 1 = 1
  1 + 2 = 3
  3 + 3 = 6
  6 + 4 = 10
  10 + 5 = 15
  The sum is: 15

Greet

This example greets a user and asks for the age. It shows the usage of input functions:

  #!/usr/bin/env bash

  # ----------------------
  # sf -- script framework
  # ----------------------

  # Declare sf variables
  sfdesc="Greet a person."

  sfargs+=("pretty-useless-flag;p;This is a pretty useless flag which is only used to show correct linebreaks of the usage. Change your terminal size and let this print again to see how the output adapts to your window")
  sfargs+=("ask-for-lastname;l;Ask for lastname")

  # Include sf, this could be replaced with a long oneliner
  source "$(dirname $0)/sf"

  # ----------------------
  # Actual script
  # ----------------------

  sfget "Enter your name"                        # Get input
  echo "Hello ${sfin}!"                          # Use input

  if [ "$ask_for_lastname" == true ]; then       # Use variable with underscores instead of dashes
      sfget "Enter your lastname"                # Get input
      echo "Ah I see, your lastname is ${sfin}"  # Use input
  fi

  sfask "Do you want to tell me your age"        # Ask for YES/no
  if [ "$sfin" == true ]; then                   # Use answer
      sfget "Enter your Age" "80"                # Get input with default value
      sfask "Is $sfin really your age" "no"      # Use input and ask for yes/NO
      if [ "$sfin" == true ]; then               # Use answer
          echo "Great!"
      else
          echo "I knew it!"
      fi
  fi

The produced usage:

  Usage: greet [OPTIONS]

  Greet a person.

  OPTIONS
    -p, --pretty-useless-flag  This is a pretty useless flag which is only used to
                               show correct linebreaks of the usage. Change your
                               terminal size and let this print again to see how
                               the output adapts to your window
    -l, --ask-for-lastname     Ask for lastname
    -h, --help                 Show this help message and exit

An example call looks like this:

  $ ./greet
  Enter your name: Jane
  Hello Jane!
  Do you want to tell me your age? [Y/n]
  Enter your Age [80]: 75
  Is 75 really your age? [y/N] y
  Great!

Throw

This example shows the usage of sfdeps:

  #!/usr/bin/env bash

  # ----------------------
  # sf -- script framework
  # ----------------------

  # Declare sf variables
  sfdesc="A script that shows the usage of 'sfdeps'. It should always throw an error."

  sfdeps=("source" "nonexistent" "alsononexistent" "echo")

  # Include sf, this could be replaced with a long oneliner
  source "$(dirname $0)/sf"

  # ----------------------
  # Actual script
  # ----------------------

  echo "If you see this, the commands 'source', 'nonexistent', 'alsononexistent' and 'echo' are available."

The usage output:

  Usage: throw [OPTIONS]

  A script that shows the usage of 'sfdeps'. It should always throw an error.

  OPTIONS
    -h, --help  Show this help message and exit

And the execution:

  $ ./throw
  ERROR Command nonexistent not found
  ERROR Command alsononexistent not found