Skip to content

Instantly share code, notes, and snippets.

@vladshut
Created May 3, 2021 11:19
Show Gist options
  • Select an option

  • Save vladshut/a80e840064b0813bdf52d1b28e1cfc14 to your computer and use it in GitHub Desktop.

Select an option

Save vladshut/a80e840064b0813bdf52d1b28e1cfc14 to your computer and use it in GitHub Desktop.

Revisions

  1. vladshut created this gist May 3, 2021.
    874 changes: 874 additions & 0 deletions instaborder.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,874 @@
    #!/usr/bin/env bash
    ### ==============================================================================
    ### SO HOW DO YOU PROCEED WITH YOUR SCRIPT?
    ### 1. define the options/parameters and defaults you need in list_options()
    ### 2. define dependencies on other programs/scripts in list_dependencies()
    ### 3. implement the different actions in main() with helper functions
    ### 4. implement helper functions you defined in previous step
    ### ==============================================================================

    ### Created by vs ( vs ) on 2021-05-03
    ### Based on https://github.com/pforret/bashew 1.16.1
    script_version="0.0.1" # if there is a VERSION.md in this script's folder, it will take priority for version number
    readonly script_author="fred.shut.vlad@gmail.com"
    readonly script_created="2021-05-03"
    readonly run_as_root=-1 # run_as_root: 0 = don't check anything / 1 = script MUST run as root / -1 = script MAY NOT run as root

    list_options() {
    ### Change the next lines to reflect which flags/options/parameters you need
    ### flag: switch a flag 'on' / no value specified
    ### flag|<short>|<long>|<description>
    ### e.g. "-v" or "--verbose" for verbose output / default is always 'off'
    ### will be available as $<long> in the script e.g. $verbose
    ### option: set an option / 1 value specified
    ### option|<short>|<long>|<description>|<default>
    ### e.g. "-e <extension>" or "--extension <extension>" for a file extension
    ### will be available a $<long> in the script e.g. $extension
    ### list: add an list/array item / 1 value specified
    ### list|<short>|<long>|<description>| (default is ignored)
    ### e.g. "-u <user1> -u <user2>" or "--user <user1> --user <user2>"
    ### will be available a $<long> array in the script e.g. ${user[@]}
    ### param: comes after the options
    ### param|<type>|<long>|<description>
    ### <type> = 1 for single parameters - e.g. param|1|output expects 1 parameter <output>
    ### <type> = ? for optional parameters - e.g. param|1|output expects 1 parameter <output>
    ### <type> = n for list parameter - e.g. param|n|inputs expects <input1> <input2> ... <input99>
    ### will be available as $<long> in the script after option/param parsing
    echo -n "
    #commented lines will be filtered
    flag|h|help|show usage
    flag|q|quiet|no output
    flag|v|verbose|output more
    flag|f|force|do not ask for confirmation (always yes)
    flag|r|remove|remove original
    option|s|size|size of the output image (default is the smae as original)
    option|bs|bordersize|size of the border|0
    option|c|color|color for the border |white
    option|l|log_dir|folder for log files |$HOME/log/$script_prefix
    option|t|tmp_dir|folder for temp files|/tmp/$script_prefix
    option|o|output|out file template (:ofolder: - original file folder, :oname: - original file name, :oext: - original file extension)|:ofolder:/:oname:_squared.:oext:
    param|1|input|input files (example: test/*.jpg)
    " | grep -v '^#' | grep -v '^\s*$'
    }

    #####################################################################
    ## Put your main script here
    #####################################################################

    main() {
    log_to_file "[$script_basename] $script_version started"

    action="modify_images"
    action=$(lower_case "$action")
    case $action in
    check|env)
    ## leave this default action, it will make it easier to test your script
    #TIP: use «$script_prefix check» to check if this script is ready to execute and what values the options/flags are
    #TIP:> $script_prefix check
    #TIP: use «$script_prefix env» to generate an example .env file
    #TIP:> $script_prefix env > .env
    check_script_settings
    ;;

    update)
    ## leave this default action, it will make it easier to test your script
    #TIP: use «$script_prefix update» to update to the latest version
    #TIP:> $script_prefix check
    update_script_to_latest
    ;;

    *)
    modify_images
    ;;
    esac
    log_to_file "[$script_basename] ended after $SECONDS secs"
    #TIP: >>> bash script created with «pforret/bashew»
    #TIP: >>> for bash development, also check out «pforret/setver» and «pforret/progressbar»
    }

    #####################################################################
    ## Put your helper scripts here
    #####################################################################

    modify_images() {
    log_to_file "modify_images [$input]"
    require_binary "convert"

    folder=$(pwd)
    i=1

    for img in $input ; do
    outputfile=$(outputfile $img)
    squareup $img $outputfile
    add_border $outputfile $outputfile

    echo "${i} ${img} -> ${outputfile}"

    if flag_set "remove"
    then
    echo "Remove original $img"
    rm "$img"
    fi

    i=$(( i+1 ))
    done
    }

    outputfile() {
    local infile=$1

    inputfilename=$(basename "$infile")
    ofolder=$(dirname "$infile")
    oname=${inputfilename%%.*}
    oext=${inputfilename##*.}

    outputfile=${output//":ofolder:"/$ofolder}
    outputfile=${outputfile//":oname:"/$oname}
    outputfile=${outputfile//":oext:"/$oext}

    echo "$outputfile"
    }

    squareup() {
    # setup temporary images and auto delete upon exit
    # use mpc/cache to hold input image temporarily in memory
    local infile=$1
    local outfile=$2

    tmpA="$tmp_dir/squareup_$$.mpc"
    tmpB="$tmp_dir/squareup_$$.cache"
    trap "rm -f $tmpA $tmpB;" 0
    trap "rm -f $tmpA $tmpB; exit 1" 1 2 3 15
    trap "rm -f $tmpA $tmpB; exit 1" ERR


    # test if infile exists and compute dimensions
    if convert -quiet "$infile" +repage "$tmpA"
    then
    width=`identify -format "%w" $tmpA`
    height=`identify -format "%h" $tmpA`
    else
    out "--- FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---"
    fi

    # compute max and min dimensions
    if [ $width -gt $height ]
    then
    max=$width
    min=$height
    else
    max=$height
    min=$width
    fi

    if [ "$size" = "" ]
    then
    dim=$max
    else
    # get % value if specified from size
    factor=`echo "$size" | sed -n 's/\([.0-9]*\)%$/\1/ p'`

    if [ "$factor" = "" ]
    then
    # test if contains a period
    factor2=`echo "$size" | sed -n 's/\([0-9]*[.][0-9]*\)$/\1/ p'`
    [ "$factor2" != "" ] && errMsg "SIZE=$size IS NOT AN INTEGER"
    fi

    # compute pixel equivalent desired size
    if [ "$factor" = "" ]
    then
    dim=$size
    [ $width -gt $height ] && scale="${dim}x" || scale="x${dim}"

    else
    dim=`echo "scale=0; $factor * $max / 100" | bc`
    [ $width -gt $height ] && scale="${dim}x" || scale="x${dim}"
    fi
    fi

    # convert trans to none for bgcolor so that background is transparent
    [ "$color" = "trans" ] && color="none"

    # add resize if size != ""
    [ "$size" = "" ] && resize="" || resize="-filter $filter -resize $scale"

    # process image

    convert \( -size ${dim}x${dim} xc:$color \) \( $tmpA $resize \) -gravity "Center" -composite +repage "$outfile"
    }

    add_border() {
    local infile=$1
    local outfile=$2

    # process image
    convert "$infile" -bordercolor "$color" -border "$bordersize" "$outfile"
    }


    #####################################################################
    ################### DO NOT MODIFY BELOW THIS LINE ###################
    #####################################################################

    # set strict mode - via http://redsymbol.net/articles/unofficial-bash-strict-mode/
    # removed -e because it made basic [[ testing ]] difficult
    set -uo pipefail
    IFS=$'\n\t'
    hash() {
    length=${1:-6}
    if [[ -n $(command -v md5sum) ]]; then
    # regular linux
    md5sum | cut -c1-"$length"
    else
    # macos
    md5 | cut -c1-"$length"
    fi
    }

    force=0
    help=0
    verbose=0
    #to enable verbose even before option parsing
    [[ $# -gt 0 ]] && [[ $1 == "-v" ]] && verbose=1
    quiet=0
    #to enable quiet even before option parsing
    [[ $# -gt 0 ]] && [[ $1 == "-q" ]] && quiet=1

    ### stdout/stderr output
    initialise_output() {
    [[ "${BASH_SOURCE[0]:-}" != "${0}" ]] && sourced=1 || sourced=0
    [[ -t 1 ]] && piped=0 || piped=1 # detect if output is piped
    if [[ $piped -eq 0 ]]; then
    col_reset="\033[0m"
    col_red="\033[1;31m"
    col_grn="\033[1;32m"
    col_ylw="\033[1;33m"
    else
    col_reset=""
    col_red=""
    col_grn=""
    col_ylw=""
    fi

    [[ $(echo -e '\xe2\x82\xac') == '' ]] && unicode=1 || unicode=0 # detect if unicode is supported
    if [[ $unicode -gt 0 ]]; then
    char_succ=""
    char_fail=""
    char_alrt="✴️"
    char_wait=""
    info_icon="🌼"
    config_icon="🌱"
    clean_icon="🧽"
    require_icon="🔌"
    else
    char_succ="OK "
    char_fail="!! "
    char_alrt="?? "
    char_wait="..."
    info_icon="(i)"
    config_icon="[c]"
    clean_icon="[c]"
    require_icon="[r]"
    fi
    error_prefix="${col_red}>${col_reset}"
    }

    out() { ((quiet)) && true || printf '%b\n' "$*"; }
    debug() { if ((verbose)); then out "${col_ylw}# $* ${col_reset}" >&2; else true; fi; }
    die() { out "${col_red}${char_fail} $script_basename${col_reset}: $*" >&2 ; tput bel ; safe_exit ; }
    alert() { out "${col_red}${char_alrt}${col_reset}: $*" >&2 ; }
    success() { out "${col_grn}${char_succ}${col_reset} $*"; }
    announce() { out "${col_grn}${char_wait}${col_reset} $*"; sleep 1 ; }
    progress() {
    ((quiet)) || (
    local screen_width
    screen_width=$(tput cols 2>/dev/null || echo 80)
    local rest_of_line
    rest_of_line=$((screen_width - 5))

    if flag_set ${piped:-0}; then
    out "$*" >&2
    else
    printf "... %-${rest_of_line}b\r" "$* " >&2
    fi
    )
    }

    log_to_file() { [[ -n ${log_file:-} ]] && echo "$(date '+%H:%M:%S') | $*" >>"$log_file"; }

    ### string processing
    lower_case() { echo "$*" | tr '[:upper:]' '[:lower:]'; }
    upper_case() { echo "$*" | tr '[:lower:]' '[:upper:]'; }

    slugify() {
    # slugify <input> <separator>
    # slugify "Jack, Jill & Clémence LTD" => jack-jill-clemence-ltd
    # slugify "Jack, Jill & Clémence LTD" "_" => jack_jill_clemence_ltd
    separator="${2:-}"
    [[ -z "$separator" ]] && separator="-"
    # shellcheck disable=SC2020
    echo "$1" |
    tr '[:upper:]' '[:lower:]' |
    tr 'àáâäæãåāçćčèéêëēėęîïííīįìłñńôöòóœøōõßśšûüùúūÿžźż' 'aaaaaaaaccceeeeeeeiiiiiiilnnoooooooosssuuuuuyzzz' |
    awk '{
    gsub(/[\[\]@#$%^&*;,.:()<>!?\/+=_]/," ",$0);
    gsub(/^ */,"",$0);
    gsub(/ *$/,"",$0);
    gsub(/ */,"-",$0);
    gsub(/[^a-z0-9\-]/,"");
    print;
    }' |
    sed "s/-/$separator/g"
    }

    title_case() {
    # title_case <input> <separator>
    # title_case "Jack, Jill & Clémence LTD" => JackJillClemenceLtd
    # title_case "Jack, Jill & Clémence LTD" "_" => Jack_Jill_Clemence_Ltd
    separator="${2:-}"
    # shellcheck disable=SC2020
    echo "$1" |
    tr '[:upper:]' '[:lower:]' |
    tr 'àáâäæãåāçćčèéêëēėęîïííīįìłñńôöòóœøōõßśšûüùúūÿžźż' 'aaaaaaaaccceeeeeeeiiiiiiilnnoooooooosssuuuuuyzzz' |
    awk '{ gsub(/[\[\]@#$%^&*;,.:()<>!?\/+=_-]/," ",$0); print $0; }' |
    awk '{
    for (i=1; i<=NF; ++i) {
    $i = toupper(substr($i,1,1)) tolower(substr($i,2))
    };
    print $0;
    }' |
    sed "s/ /$separator/g" |
    cut -c1-50
    }

    ### interactive
    confirm() {
    # $1 = question
    flag_set $force && return 0
    read -r -p "$1 [y/N] " -n 1
    echo " "
    [[ $REPLY =~ ^[Yy]$ ]]
    }

    ask() {
    # $1 = variable name
    # $2 = question
    # $3 = default value
    # not using read -i because that doesn't work on MacOS
    local ANSWER
    read -r -p "$2 ($3) > " ANSWER
    if [[ -z "$ANSWER" ]]; then
    eval "$1=\"$3\""
    else
    eval "$1=\"$ANSWER\""
    fi
    }

    trap "die \"ERROR \$? after \$SECONDS seconds \n\
    \${error_prefix} last command : '\$BASH_COMMAND' \" \
    \$(< \$script_install_path awk -v lineno=\$LINENO \
    'NR == lineno {print \"\${error_prefix} from line \" lineno \" : \" \$0}')" INT TERM EXIT
    # cf https://askubuntu.com/questions/513932/what-is-the-bash-command-variable-good-for

    safe_exit() {
    [[ -n "${tmp_file:-}" ]] && [[ -f "$tmp_file" ]] && rm "$tmp_file"
    trap - INT TERM EXIT
    debug "$script_basename finished after $SECONDS seconds"
    exit 0
    }

    flag_set() { [[ "$1" -gt 0 ]]; }

    show_usage() {
    out "Program: ${col_grn}$script_basename $script_version${col_reset} by ${col_ylw}$script_author${col_reset}"
    out "Updated: ${col_grn}$script_modified${col_reset}"
    out "Description: Resizes an image and squares it up by padding and adding border."
    echo -n "Usage: $script_basename"
    list_options |
    awk '
    BEGIN { FS="|"; OFS=" "; oneline="" ; fulltext="Flags, options and parameters:"}
    $1 ~ /flag/ {
    fulltext = fulltext sprintf("\n -%1s|--%-12s: [flag] %s [default: off]",$2,$3,$4) ;
    oneline = oneline " [-" $2 "]"
    }
    $1 ~ /option/ {
    fulltext = fulltext sprintf("\n -%1s|--%-12s: [option] %s",$2,$3 " <?>",$4) ;
    if($5!=""){fulltext = fulltext " [default: " $5 "]"; }
    oneline = oneline " [-" $2 " <" $3 ">]"
    }
    $1 ~ /list/ {
    fulltext = fulltext sprintf("\n -%1s|--%-12s: [list] %s (array)",$2,$3 " <?>",$4) ;
    fulltext = fulltext " [default empty]";
    oneline = oneline " [-" $2 " <" $3 ">]"
    }
    $1 ~ /secret/ {
    fulltext = fulltext sprintf("\n -%1s|--%s <%s>: [secret] %s",$2,$3,"?",$4) ;
    oneline = oneline " [-" $2 " <" $3 ">]"
    }
    $1 ~ /param/ {
    if($2 == "1"){
    fulltext = fulltext sprintf("\n %-17s: [parameter] %s","<"$3">",$4);
    oneline = oneline " <" $3 ">"
    }
    if($2 == "?"){
    fulltext = fulltext sprintf("\n %-17s: [parameter] %s (optional)","<"$3">",$4);
    oneline = oneline " <" $3 "?>"
    }
    if($2 == "n"){
    fulltext = fulltext sprintf("\n %-17s: [parameters] %s (1 or more)","<"$3">",$4);
    oneline = oneline " <" $3 " …>"
    }
    }
    END {print oneline; print fulltext}
    '
    }

    check_last_version(){
    (
    # shellcheck disable=SC2164
    pushd "$script_install_folder" &> /dev/null
    if [[ -d .git ]] ; then
    local remote
    remote="$(git remote -v | grep fetch | awk 'NR == 1 {print $2}')"
    progress "Check for latest version - $remote"
    git remote update &> /dev/null
    if [[ $(git rev-list --count "HEAD...HEAD@{upstream}" 2>/dev/null) -gt 0 ]] ; then
    out "There is a more recent update of this script - run <<$script_prefix update>> to update"
    fi
    fi
    # shellcheck disable=SC2164
    popd &> /dev/null
    )
    }

    update_script_to_latest(){
    # run in background to avoid problems with modifying a running interpreted script
    (
    sleep 1
    cd "$script_install_folder" && git pull
    ) &
    }

    show_tips() {
    ((sourced)) && return 0
    # shellcheck disable=SC2016
    grep <"${BASH_SOURCE[0]}" -v '$0' \
    | awk \
    -v green="$col_grn" \
    -v yellow="$col_ylw" \
    -v reset="$col_reset" \
    '
    /TIP: / {$1=""; gsub(/«/,green); gsub(/»/,reset); print "*" $0}
    /TIP:> / {$1=""; print " " yellow $0 reset}
    ' \
    | awk \
    -v script_basename="$script_basename" \
    -v script_prefix="$script_prefix" \
    '{
    gsub(/\$script_basename/,script_basename);
    gsub(/\$script_prefix/,script_prefix);
    print ;
    }'
    }

    check_script_settings() {
    if [[ -n $(filter_option_type flag) ]]; then
    out "## ${col_grn}boolean flags${col_reset}:"
    filter_option_type flag |
    while read -r name; do
    if ((piped)); then
    eval "echo \"$name=\$${name:-}\""
    else
    eval "echo -n \"$name=\$${name:-} \""
    fi
    done
    out " "
    out " "
    fi

    if [[ -n $(filter_option_type option) ]]; then
    out "## ${col_grn}option defaults${col_reset}:"
    filter_option_type option |
    while read -r name; do
    if ((piped)); then
    eval "echo \"$name=\$${name:-}\""
    else
    eval "echo -n \"$name=\$${name:-} \""
    fi
    done
    out " "
    out " "
    fi

    if [[ -n $(filter_option_type list) ]]; then
    out "## ${col_grn}list options${col_reset}:"
    filter_option_type list |
    while read -r name; do
    if ((piped)); then
    eval "echo \"$name=(\${${name}[@]})\""
    else
    eval "echo -n \"$name=(\${${name}[@]}) \""
    fi
    done
    out " "
    out " "
    fi

    if [[ -n $(filter_option_type param) ]]; then
    if ((piped)); then
    debug "Skip parameters for .env files"
    else
    out "## ${col_grn}parameters${col_reset}:"
    filter_option_type param |
    while read -r name; do
    # shellcheck disable=SC2015
    ((piped)) && eval "echo \"$name=\\\"\${$name:-}\\\"\"" || eval "echo -n \"$name=\\\"\${$name:-}\\\" \""
    done
    echo " "
    fi
    fi
    }

    filter_option_type() {
    list_options | grep "$1|" | cut -d'|' -f3 | sort | grep -v '^\s*$'
    }

    init_options() {
    local init_command
    init_command=$(list_options |
    grep -v "verbose|" |
    awk '
    BEGIN { FS="|"; OFS=" ";}
    $1 ~ /flag/ && $5 == "" {print $3 "=0; "}
    $1 ~ /flag/ && $5 != "" {print $3 "=\"" $5 "\"; "}
    $1 ~ /option/ && $5 == "" {print $3 "=\"\"; "}
    $1 ~ /option/ && $5 != "" {print $3 "=\"" $5 "\"; "}
    $1 ~ /list/ {print $3 "=(); "}
    $1 ~ /secret/ {print $3 "=\"\"; "}
    ')
    if [[ -n "$init_command" ]]; then
    eval "$init_command"
    fi
    }

    expects_single_params() { list_options | grep 'param|1|' >/dev/null; }
    expects_optional_params() { list_options | grep 'param|?|' >/dev/null; }
    expects_multi_param() { list_options | grep 'param|n|' >/dev/null; }

    parse_options() {
    if [[ $# -eq 0 ]]; then
    show_usage >&2
    safe_exit
    fi

    ## first process all the -x --xxxx flags and options
    while true; do
    # flag <flag> is saved as $flag = 0/1
    # option <option> is saved as $option
    if [[ $# -eq 0 ]]; then
    ## all parameters processed
    break
    fi
    if [[ ! $1 == -?* ]]; then
    ## all flags/options processed
    break
    fi
    local save_option
    save_option=$(list_options |
    awk -v opt="$1" '
    BEGIN { FS="|"; OFS=" ";}
    $1 ~ /flag/ && "-"$2 == opt {print $3"=1"}
    $1 ~ /flag/ && "--"$3 == opt {print $3"=1"}
    $1 ~ /option/ && "-"$2 == opt {print $3"=$2; shift"}
    $1 ~ /option/ && "--"$3 == opt {print $3"=$2; shift"}
    $1 ~ /list/ && "-"$2 == opt {print $3"+=($2); shift"}
    $1 ~ /list/ && "--"$3 == opt {print $3"=($2); shift"}
    $1 ~ /secret/ && "-"$2 == opt {print $3"=$2; shift #noshow"}
    $1 ~ /secret/ && "--"$3 == opt {print $3"=$2; shift #noshow"}
    ')
    if [[ -n "$save_option" ]]; then
    if echo "$save_option" | grep shift >>/dev/null; then
    local save_var
    save_var=$(echo "$save_option" | cut -d= -f1)
    debug "$config_icon parameter: ${save_var}=$2"
    else
    debug "$config_icon flag: $save_option"
    fi
    eval "$save_option"
    else
    die "cannot interpret option [$1]"
    fi
    shift
    done

    ((help)) && (
    show_usage
    check_last_version
    out " "
    echo "### TIPS & EXAMPLES"
    show_tips

    ) && safe_exit

    ## then run through the given parameters
    if expects_single_params; then
    single_params=$(list_options | grep 'param|1|' | cut -d'|' -f3)
    list_singles=$(echo "$single_params" | xargs)
    single_count=$(echo "$single_params" | count_words)
    debug "$config_icon Expect : $single_count single parameter(s): $list_singles"
    [[ $# -eq 0 ]] && die "need the parameter(s) [$list_singles]"

    for param in $single_params; do
    [[ $# -eq 0 ]] && die "need parameter [$param]"
    [[ -z "$1" ]] && die "need parameter [$param]"
    debug "$config_icon Assign : $param=$1"
    eval "$param=\"$1\""
    shift
    done
    else
    debug "$config_icon No single params to process"
    single_params=""
    single_count=0
    fi

    if expects_optional_params; then
    optional_params=$(list_options | grep 'param|?|' | cut -d'|' -f3)
    optional_count=$(echo "$optional_params" | count_words)
    debug "$config_icon Expect : $optional_count optional parameter(s): $(echo "$optional_params" | xargs)"

    for param in $optional_params; do
    debug "$config_icon Assign : $param=${1:-}"
    eval "$param=\"${1:-}\""
    shift
    done
    else
    debug "$config_icon No optional params to process"
    optional_params=""
    optional_count=0
    fi

    if expects_multi_param; then
    #debug "Process: multi param"
    multi_count=$(list_options | grep -c 'param|n|')
    multi_param=$(list_options | grep 'param|n|' | cut -d'|' -f3)
    debug "$config_icon Expect : $multi_count multi parameter: $multi_param"
    ((multi_count > 1)) && die "cannot have >1 'multi' parameter: [$multi_param]"
    ((multi_count > 0)) && [[ $# -eq 0 ]] && die "need the (multi) parameter [$multi_param]"
    # save the rest of the params in the multi param
    if [[ -n "$*" ]]; then
    debug "$config_icon Assign : $multi_param=$*"
    eval "$multi_param=( $* )"
    fi
    else
    multi_count=0
    multi_param=""
    # [[ $# -gt 0 ]] && die "cannot interpret extra parameters"
    fi
    }

    require_binary(){
    binary="$1"
    path_binary=$(command -v "$binary" 2>/dev/null)
    [[ -n "$path_binary" ]] && debug "$require_icon required [$binary] -> $path_binary" && return 0
    words=$(echo "${2:-}" | wc -l)
    if ((force)) ; then
    announce "Installing $1 ..."
    case $words in
    0) eval "$install_package $1" ;;
    # require_binary ffmpeg -- binary and package have the same name
    1) eval "$install_package $2" ;;
    # require_binary convert imagemagick -- binary and package have different names
    *) eval "${2:-}"
    # require_binary primitive "go get -u github.com/fogleman/primitive" -- non-standard package manager
    esac
    else
    case $words in
    0) install_instructions="$install_package $1" ;;
    1) install_instructions="$install_package $2" ;;
    *) install_instructions="${2:-}"
    esac
    alert "$script_basename needs [$binary] but it cannot be found"
    alert "1) install package : $install_instructions"
    alert "2) check path : export PATH=\"[path of your binary]:\$PATH\""
    die "Missing program/script [$binary]"
    fi
    }

    folder_prep() {
    if [[ -n "$1" ]]; then
    local folder="$1"
    local max_days=${2:-365}
    if [[ ! -d "$folder" ]]; then
    debug "$clean_icon Create folder : [$folder]"
    mkdir -p "$folder"
    else
    debug "$clean_icon Cleanup folder: [$folder] - delete files older than $max_days day(s)"
    find "$folder" -mtime "+$max_days" -type f -exec rm {} \;
    fi
    fi
    }

    count_words() { wc -w | awk '{ gsub(/ /,""); print}'; }

    recursive_readlink() {
    [[ ! -L "$1" ]] && echo "$1" && return 0
    local file_folder
    local link_folder
    local link_name
    file_folder="$(dirname "$1")"
    # resolve relative to absolute path
    [[ "$file_folder" != /* ]] && link_folder="$(cd -P "$file_folder" &>/dev/null && pwd)"
    local symlink
    symlink=$(readlink "$1")
    link_folder=$(dirname "$symlink")
    link_name=$(basename "$symlink")
    [[ -z "$link_folder" ]] && link_folder="$file_folder"
    [[ "$link_folder" == \.* ]] && link_folder="$(cd -P "$file_folder" && cd -P "$link_folder" &>/dev/null && pwd)"
    debug "$info_icon Symbolic ln: $1 -> [$symlink]"
    recursive_readlink "$link_folder/$link_name"
    }

    lookup_script_data() {
    readonly script_prefix=$(basename "${BASH_SOURCE[0]}" .sh)
    readonly script_basename=$(basename "${BASH_SOURCE[0]}")
    readonly execution_day=$(date "+%Y-%m-%d")
    #readonly execution_year=$(date "+%Y")

    script_install_path="${BASH_SOURCE[0]}"
    debug "$info_icon Script path: $script_install_path"
    script_install_path=$(recursive_readlink "$script_install_path")
    debug "$info_icon Linked path: $script_install_path"
    readonly script_install_folder="$( cd -P "$( dirname "$script_install_path" )" && pwd )"
    debug "$info_icon In folder : $script_install_folder"
    if [[ -f "$script_install_path" ]]; then
    script_hash=$(hash <"$script_install_path" 8)
    script_lines=$(awk <"$script_install_path" 'END {print NR}')
    else
    # can happen when script is sourced by e.g. bash_unit
    script_hash="?"
    script_lines="?"
    fi

    # get shell/operating system/versions
    shell_brand="sh"
    shell_version="?"
    [[ -n "${ZSH_VERSION:-}" ]] && shell_brand="zsh" && shell_version="$ZSH_VERSION"
    [[ -n "${BASH_VERSION:-}" ]] && shell_brand="bash" && shell_version="$BASH_VERSION"
    [[ -n "${FISH_VERSION:-}" ]] && shell_brand="fish" && shell_version="$FISH_VERSION"
    [[ -n "${KSH_VERSION:-}" ]] && shell_brand="ksh" && shell_version="$KSH_VERSION"
    debug "$info_icon Shell type : $shell_brand - version $shell_version"

    readonly os_kernel=$(uname -s)
    os_version=$(uname -r)
    os_machine=$(uname -m)
    install_package=""
    case "$os_kernel" in
    CYGWIN* | MSYS* | MINGW*)
    os_name="Windows"
    ;;
    Darwin)
    os_name=$(sw_vers -productName) # macOS
    os_version=$(sw_vers -productVersion) # 11.1
    install_package="brew install"
    ;;
    Linux | GNU*)
    if [[ $(command -v lsb_release) ]]; then
    # 'normal' Linux distributions
    os_name=$(lsb_release -i) # Ubuntu
    os_version=$(lsb_release -r) # 20.04
    else
    # Synology, QNAP,
    os_name="Linux"
    fi
    [[ -x /bin/apt-cyg ]] && install_package="apt-cyg install" # Cygwin
    [[ -x /bin/dpkg ]] && install_package="dpkg -i" # Synology
    [[ -x /opt/bin/ipkg ]] && install_package="ipkg install" # Synology
    [[ -x /usr/sbin/pkg ]] && install_package="pkg install" # BSD
    [[ -x /usr/bin/pacman ]] && install_package="pacman -S" # Arch Linux
    [[ -x /usr/bin/zypper ]] && install_package="zypper install" # Suse Linux
    [[ -x /usr/bin/emerge ]] && install_package="emerge" # Gentoo
    [[ -x /usr/bin/yum ]] && install_package="yum install" # RedHat RHEL/CentOS/Fedora
    [[ -x /usr/bin/apk ]] && install_package="apk add" # Alpine
    [[ -x /usr/bin/apt-get ]] && install_package="apt-get install" # Debian
    [[ -x /usr/bin/apt ]] && install_package="apt install" # Ubuntu
    ;;

    esac
    debug "$info_icon System OS : $os_name ($os_kernel) $os_version on $os_machine"
    debug "$info_icon Package mgt: $install_package"

    # get last modified date of this script
    script_modified="??"
    [[ "$os_kernel" == "Linux" ]] && script_modified=$(stat -c %y "$script_install_path" 2>/dev/null | cut -c1-16) # generic linux
    [[ "$os_kernel" == "Darwin" ]] && script_modified=$(stat -f "%Sm" "$script_install_path" 2>/dev/null) # for MacOS

    debug "$info_icon Last modif : $script_modified"
    debug "$info_icon Script ID : $script_lines lines / md5: $script_hash"
    debug "$info_icon Creation : $script_created"
    debug "$info_icon Running as : $USER@$HOSTNAME"

    # if run inside a git repo, detect for which remote repo it is
    if git status &>/dev/null; then
    readonly git_repo_remote=$(git remote -v | awk '/(fetch)/ {print $2}')
    debug "$info_icon git remote : $git_repo_remote"
    readonly git_repo_root=$(git rev-parse --show-toplevel)
    debug "$info_icon git folder : $git_repo_root"
    else
    readonly git_repo_root=""
    readonly git_repo_remote=""
    fi

    # get script version from VERSION.md file - which is automatically updated by pforret/setver
    [[ -f "$script_install_folder/VERSION.md" ]] && script_version=$(cat "$script_install_folder/VERSION.md")
    # get script version from git tag file - which is automatically updated by pforret/setver
    [[ -n "$git_repo_root" ]] && [[ -n "$(git tag &>/dev/null)" ]] && script_version=$(git tag --sort=version:refname | tail -1)
    }

    prep_log_and_temp_dir() {
    tmp_file=""
    log_file=""
    if [[ -n "${tmp_dir:-}" ]]; then
    folder_prep "$tmp_dir" 1
    tmp_file=$(mktemp "$tmp_dir/$execution_day.XXXXXX")
    debug "$config_icon tmp_file: $tmp_file"
    # you can use this temporary file in your program
    # it will be deleted automatically if the program ends without problems
    fi
    if [[ -n "${log_dir:-}" ]]; then
    folder_prep "$log_dir" 30
    log_file="$log_dir/$script_prefix.$execution_day.log"
    debug "$config_icon log_file: $log_file"
    fi
    }

    import_env_if_any() {
    env_files=("$script_install_folder/.env" "$script_install_folder/$script_prefix.env" "./.env" "./$script_prefix.env")

    for env_file in "${env_files[@]}"; do
    if [[ -f "$env_file" ]]; then
    debug "$config_icon Read config from [$env_file]"
    # shellcheck disable=SC1090
    source "$env_file"
    fi
    done
    }

    initialise_output # output settings
    lookup_script_data # find installation folder

    [[ $run_as_root == 1 ]] && [[ $UID -ne 0 ]] && die "user is $USER, MUST be root to run [$script_basename]"
    [[ $run_as_root == -1 ]] && [[ $UID -eq 0 ]] && die "user is $USER, CANNOT be root to run [$script_basename]"

    init_options # set default values for flags & options
    import_env_if_any # overwrite with .env if any

    if [[ $sourced -eq 0 ]]; then
    parse_options "$@" # overwrite with specified options if any
    prep_log_and_temp_dir # clean up debug and temp folder
    main # run main program
    safe_exit # exit and clean up
    else
    # just disable the trap, don't execute main
    trap - INT TERM EXIT
    fi