#!/usr/bin/env bash
#
# cjdscript | Deploy and run the collection of working tools included with cjdns
#
# Version 10.2
#
# Written by Kevin MacMartin
# Licensed under the GPLv3
#

# Name of the script file, for use in help and usage functionality
script_name="${0##*/}"

# VERSION: This is saved @ ~/.cjdscript/.dirver and will display an out-of-date warning when it doesn't match
cjdscript_version='10'

# CJDSCRIPT ROOT: Base directory for cjdscript files to be installed
cjdscript_rootdir="$HOME/.cjdscript"

# CJDNS REPO and BRANCH: The location of the cjdns git repository and the branch to checkout
[[ -z "$cjdns_gitrepo" ]] \
    && cjdns_gitrepo='https://github.com/cjdelisle/cjdns.git'
[[ -z "$cjdns_gitbranch" ]] \
    && cjdns_gitbranch='master'

# INCLUDED SCRIPTS: The scripts supported by cjdscripts, separated by language
bash_scripts=('cjdroid-build.sh' 'ip6tables.sh')
binaries=('cjdroute' 'makekeys' 'privatetopublic' 'publictoip6' 'sybilsim')
nodejs_scripts=('admin-panel' 'cjdnslog.js' 'dumptable.js' 'makesim' 'pathfinderTree.js' 'peerStats.js' 'ping' 'pingAll.js' 'traceroute')
python2_scripts=('cjdnsadminmaker.py' 'cjdnslog' 'drawgraph' 'dumpgraph' 'dumptable' 'dynamicEndpoints.py' 'findnodes' 'graphStats' 'peerStats' 'pingAll.py' 'pktoip6' 'searches' 'sessionStats' 'trashroutes')

# Silences detailed output so only important messages are seen (unset by the verbose flag)
silent_output=">/dev/null 2>&1"

# Colours
[[ -t 1 ]] && {
    c_d=$'\e[1;30m' # DARK GREY
    c_r=$'\e[1;31m' # RED
    c_g=$'\e[1;32m' # GREEN
    c_y=$'\e[1;33m' # YELLOW
    c_b=$'\e[1;34m' # BLUE
    c_m=$'\e[1;35m' # MAGENTA
    c_t=$'\e[1;36m' # TEAL
    c_w=$'\e[1;37m' # WHITE
    c_u=$'\e[4m'    # UNDERLINE
    c_c=$'\e[0m'    # DISABLES COLOUR
}

# Export path variables with cjdscript locations included
export PATH="$cjdscript_rootdir/bin:$PATH"
export PYTHONPATH="$cjdscript_rootdir/lib/python2.7/site-packages/"

# Display a message and exit with an error when the user quits using signals
function force_quit {
    printf '\n%s\n' "${c_w}Quitting...$c_c"
    exit 1
}

# Return true if a file exists in $1 that has a name containing $2
function file_match {
    for each in "$1/"*; do
        [[ "${each/*\/}" =~ $2 ]] \
            && return 0
    done
    return 1
}

# Remove old python site-packages matching the passed argument
function removeold_sitepkgs {
    declare -a old_files=()
    for each in "$cjdscript_rootdir/lib/python2.7/site-packages/"*; do
        [[ "${each/*\/}" =~ ${1/-*} ]] \
            && old_files=( "${old_files[@]}" "$each" )
    done
    [[ -n "${old_files[*]}" ]] && {
        for pyobject in "${old_files[@]}"; do
            rm -rf "$pyobject"
        done
    }
    unset old_files
}

# BASH SETUP: Triggered by setup_scripts to configure supported bash scripts
function setup_bash {
    # Determine the location of the bash executable
    bash_binpath="$(type -P bash)"

    # Run the bash configuration provided its executable was detected
    if [[ -n "$bash_binpath" ]]; then
        printf '%s\n' "$c_b==>$c_c Linking ${c_w}bash$c_c scripts"

        # Setup bash links
        ln -sf "$bash_binpath" "$cjdscript_rootdir/bin/bash"
        ln -sf "$bash_binpath" "$cjdscript_rootdir/bin/sh"
        ln -sf "$bash_binpath" "$cjdscript_rootdir/bin/sh -e"

        # Setup bash scripts
        for bash_scriptname in "${bash_scripts[@]}"; do
            if [ -f "$cjdscript_rootdir/download/cjdns/contrib/bash/$bash_scriptname" ]; then
                ln -sf "$cjdscript_rootdir/download/cjdns/contrib/bash/$bash_scriptname" "$cjdscript_rootdir/bin/$bash_scriptname"
            elif [ -f "$cjdscript_rootdir/download/cjdns/contrib/android/$bash_scriptname" ]; then
                ln -sf "$cjdscript_rootdir/download/cjdns/contrib/android/$bash_scriptname" "$cjdscript_rootdir/bin/$bash_scriptname"
            else
                printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c $bash_scriptname does not exist and won't be installed"
            fi
        done
    else
        # Display an error stating that bash scripts won't be available
        printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c Not including bash scripts (bash is not available)"
    fi
}

# NODE SETUP: Triggered by setup_scripts to configure supported node scripts
function setup_node {
    # Detect the presence of node and determine the location of its executable
    nodejs_binpath="$(type -P node)"
    nodejs_binpath=${nodejs_binpath:="$(type -P nodejs)"}

    # Run the node configuration provided its executable was detected
    if [[ -n "$nodejs_binpath" ]]; then
        printf '%s\n' "$c_b==>$c_c Linking ${c_w}node$c_c scripts"

        # Setup nodejs links
        ln -sf "$nodejs_binpath" "$cjdscript_rootdir/bin/node"
        ln -sf "$nodejs_binpath" "$cjdscript_rootdir/bin/nodejs"

        # Install nodejs libraries
        ln -sf "$cjdscript_rootdir/download/cjdns/tools/lib" "$cjdscript_rootdir/bin/lib"
        ln -sf "$cjdscript_rootdir/download/cjdns/contrib/nodejs/cjdnsadmin" "$cjdscript_rootdir/cjdnsadmin"

        # Setup admin-panel launcher script
        if type -P npm >/dev/null && [[ -d "$cjdscript_rootdir/download/cjdns/contrib/nodejs/admin" ]]; then
            pushd "$cjdscript_rootdir/download/cjdns/contrib/nodejs/admin" >/dev/null
            npm install >/dev/null 2>&1
            popd >/dev/null
            printf '%s\n\n%s\n%s\n' '#!/usr/bin/env bash' 'cd "${0%/*}"/../download/cjdns/contrib/nodejs/admin' 'node admin.js $@' \
                > "$cjdscript_rootdir/bin/admin-panel"
            chmod 755 "$cjdscript_rootdir/bin/admin-panel"
        else
            # Display an error stating that admin-panel won't be available
            printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c Not including admins"
        fi

        # Create a launcher script for makesim and make it executable
        if [[ -f "$cjdscript_rootdir/download/cjdns/contrib/nodejs/makesim.js" ]]; then
            printf '%s\n\n%s\n%s\n' '#!/usr/bin/env bash' 'cd "${0%/*}"/../download/cjdns/contrib/nodejs' 'node makesim.js $@' \
                > "$cjdscript_rootdir/bin/makesim"
            chmod 755 "$cjdscript_rootdir/bin/makesim"
        else
            # Display an error stating that makesim scripts won't be available
            printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c Not including makesim"
        fi

        # Setup nodejs scripts
        for nodejs_scriptname in "${nodejs_scripts[@]}"; do
            if [[ -f "$cjdscript_rootdir/download/cjdns/tools/${nodejs_scriptname/\.js}" ]]; then
                chmod 755 "$cjdscript_rootdir/download/cjdns/tools/${nodejs_scriptname/\.js}"
                ln -sf "$cjdscript_rootdir/download/cjdns/tools/${nodejs_scriptname/\.js}" "$cjdscript_rootdir/bin/$nodejs_scriptname"
            else
                # Announce missing scripts (but skip ones that have been added manually)
                [[ ! -e "$cjdscript_rootdir/bin/$nodejs_scriptname" ]] \
                    && printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c $nodejs_scriptname does not exist and won't be installed"
            fi
        done
    else
        # Display an error stating that node scripts won't be available
        printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c Not including node scripts (node/nodejs is not available)"
    fi
}

# PYTHON2 SETUP: Triggered by setup_scripts to configure supported python2 scripts
function setup_python2 {
    # DOWNLOAD URLs: The locations to grab different external libraries
    pyurl_setuptools='https://pypi.python.org/packages/source/s/setuptools/setuptools-12.2.tar.gz'
    pyurl_networkx='https://pypi.python.org/packages/source/n/networkx/networkx-1.9.1.tar.gz'
    pyurl_pyside='https://pypi.python.org/packages/source/P/PySide/PySide-1.2.2.tar.gz'
    pyurl_numpy='http://downloads.sourceforge.net/project/numpy/NumPy/1.9.1/numpy-1.9.1.tar.gz'
    pyurl_matplotlib='https://downloads.sourceforge.net/project/matplotlib/matplotlib/matplotlib-1.4.3/matplotlib-1.4.3.tar.gz'

    # Name and version for each _PYURL
    pyinfo_setuptools="${pyurl_setuptools/*\/}"
    pyinfo_setuptools="${pyinfo_setuptools/\.[a-z][a-z]*}"
    pyinfo_networkx="${pyurl_networkx/*\/}"
    pyinfo_networkx="${pyinfo_networkx/\.[a-z][a-z]*}"
    pyinfo_pyside="${pyurl_pyside/*\/}"
    pyinfo_pyside="${pyinfo_pyside/\.[a-z][a-z]*}"
    pyinfo_numpy="${pyurl_numpy/*\/}"
    pyinfo_numpy="${pyinfo_numpy/\.[a-z][a-z]*}"
    pyinfo_matplotlib="${pyurl_matplotlib/*\/}"
    pyinfo_matplotlib="${pyinfo_matplotlib/\.[a-z][a-z]*}"

    # Detect the presence of python2 and determine the location of its executable
    python2_binpath="$(type -P python2)"
    python2_binpath=${python2_binpath:="$(type -P python2.7)"}
    python2_binpath=${python2_binpath:="$(type -P python)"}
    [[ $($python2_binpath --version 2>&1) =~ Python\ 3 ]] \
        && unset python2_binpath

    # Run the python2 configuration provided its executable was detected
    if [[ -n "$python2_binpath" ]]; then
        # Setup Python2 links and lib directory
        ln -sf "$python2_binpath" "$cjdscript_rootdir/bin/python"
        ln -sf "$python2_binpath" "$cjdscript_rootdir/bin/python2"
        ln -sf "$python2_binpath" "$cjdscript_rootdir/bin/python2.7"
        install -d "$PYTHONPATH"

        # Install CJDNS Python2 libraries
        rm -f "$PYTHONPATH/cjdnsadmin"
        ln -sf "$cjdscript_rootdir/download/cjdns/contrib/python/cjdnsadmin" "$PYTHONPATH/cjdnsadmin"

        # INSTALL THIRD PARTY LIBRARIES
        cd "$cjdscript_rootdir/download"

        # SETUPTOOLS: Download, build and install unless the current version is installed
        if ! file_match "$cjdscript_rootdir/lib/python2.7/site-packages" "$pyinfo_setuptools"; then
            removeold_sitepkgs "$pyinfo_setuptools"
            printf '%s' "$c_b==>$c_c Installing ${c_w}python2$c_c library: ${c_y}setuptools${c_c}... "
            [[ -d setuptools ]] && rm -rf setuptools
            wget -q -O - $pyurl_setuptools | tar -xz
            mv setuptools* setuptools
            if [[ -d setuptools ]]; then
                cd setuptools
                eval "$python2_binpath" setup.py install --prefix="$cjdscript_rootdir" $silent_output
                cd "$cjdscript_rootdir/download"
                rm -rf setuptools
                if file_match "$cjdscript_rootdir/lib/python2.7/site-packages" "$pyinfo_setuptools"; then
                    printf '%s\n' "${c_g}DONE!$c_c"
                else
                    printf '%s\n' "${c_r}FAILED:$c_c an error occurred while installing" >&2
                fi
            else
                # Display an error stating that networkx didn't install correctly
                printf '%s\n' "${c_r}FAILED:$c_c an error occurred while downloading" >&2
            fi
        fi

        if file_match "$cjdscript_rootdir/lib/python2.7/site-packages" "$pyinfo_setuptools"; then
            # NETWORKX: Download, build and install unless the current version is installed
            if ! file_match "$cjdscript_rootdir/lib/python2.7/site-packages" "$pyinfo_networkx"; then
                removeold_sitepkgs "$pyinfo_networkx"
                printf '%s' "$c_b==>$c_c Installing ${c_w}python2$c_c library: ${c_y}networkx${c_c}... "
                [[ -d networkx ]] && rm -rf networkx
                wget -q -O - $pyurl_networkx | tar -xz
                mv networkx* networkx
                if [[ -d networkx ]]; then
                    cd networkx
                    eval "$python2_binpath" setup.py install --prefix="$cjdscript_rootdir" $silent_output
                    cd "$cjdscript_rootdir/download"
                    rm -rf networkx
                    if file_match "$cjdscript_rootdir/lib/python2.7/site-packages" "$pyinfo_networkx"; then
                        printf '%s\n' "${c_g}DONE!$c_c"
                    else
                        printf '%s\n' "${c_r}FAILED:$c_c an error occurred while installing" >&2
                    fi
                else
                    # Display an error stating that networkx didn't install correctly
                    printf '%s\n' "${c_r}FAILED:$c_c an error occurred while downloading" >&2
                fi
            fi

            # PYSIDE: Download, build and install unless the current version is installed
            if ! file_match "$cjdscript_rootdir/lib/python2.7/site-packages" "$pyinfo_pyside"; then
                removeold_sitepkgs "$pyinfo_pyside"
                printf '%s' "$c_b==>$c_c Installing ${c_w}python2$c_c library: ${c_y}PySide${c_c}... "
                if [[ $(type -P cmake) ]]; then
                    [[ $(type -P qmake-qt4) ]] \
                        && ln -sf "$(type -P qmake-qt4)" "$cjdscript_rootdir/bin/qmake"
                    if [[ $(type -P qmake) ]]; then
                        [[ -d PySide ]] && rm -rf PySide
                        wget -q -O - $pyurl_pyside | tar -xz
                        mv PySide* PySide
                        if [[ -d PySide ]]; then
                            cd PySide
                            eval "$python2_binpath" setup.py install --prefix="$cjdscript_rootdir" $silent_output
                            cd "$cjdscript_rootdir/download"
                            rm -rf PySide
                            if file_match "$cjdscript_rootdir/lib/python2.7/site-packages" "$pyinfo_pyside"; then
                                printf '%s\n' "${c_g}DONE!$c_c"
                            else
                                printf '%s\n' "${c_r}FAILED:$c_c an error occurred while installing" >&2
                            fi
                        else
                            # Display an error stating that PySide didn't install correctly
                            printf '%s\n' "${c_r}FAILED:$c_c an error occurred while downloading" >&2
                        fi
                    else
                        printf '%s\n' "${c_r}FAILED:$c_c ${c_y}qmake$c_c is required to install" >&2
                    fi
                else
                    printf '%s\n' "${c_r}FAILED:$c_c ${c_y}cmake$c_c is required to install" >&2
                fi
            fi
        else
            # Display an error stating that setuptools didn't install correctly and list the libraries this affects
            printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c networkx and PySide require setuptools and won't be installed"
        fi

        # NUMPY: Download, build and install unless the current version is installed
        if ! file_match "$cjdscript_rootdir/lib/python2.7/site-packages" "$pyinfo_numpy"; then
            removeold_sitepkgs "$pyinfo_numpy"
            printf '%s' "$c_b==>$c_c Installing ${c_w}python2$c_c library: ${c_y}numpy${c_c}... "
            [[ -d numpy ]] && rm -rf numpy
            wget -q -O - $pyurl_numpy | tar -xz
            mv numpy* numpy
            if [[ -d numpy ]]; then
                cd numpy
                eval "$python2_binpath" setup.py build $silent_output
                eval "$python2_binpath" setup.py install --prefix="$cjdscript_rootdir" $silent_output
                cd "$cjdscript_rootdir/download"
                rm -rf numpy
                if file_match "$cjdscript_rootdir/lib/python2.7/site-packages" "$pyinfo_numpy"; then
                    printf '%s\n' "${c_g}DONE!$c_c"
                else
                    printf '%s\n' "${c_r}FAILED:$c_c an error occurred while installing" >&2
                fi
            else
                # Display an error stating that numpy didn't install correctly
                printf '%s\n' "${c_r}FAILED:$c_c an error occurred while downloading" >&2
            fi
        fi

        # MATPLOTLIB: Download, build and install unless the current version is installed
        if ! file_match "$cjdscript_rootdir/lib/python2.7/site-packages" "$pyinfo_matplotlib"; then
            removeold_sitepkgs "$pyinfo_matplotlib"
            printf '%s' "$c_b==>$c_c Installing ${c_w}python2$c_c library: ${c_y}matplotlib${c_c}... "
            [[ -d matplotlib ]] && rm -rf matplotlib
            wget -q -O - $pyurl_matplotlib | tar -xz
            mv matplotlib* matplotlib
            if [[ -d matplotlib ]]; then
                cd matplotlib
                eval "$python2_binpath" setup.py build $silent_output
                eval "$python2_binpath" setup.py install --prefix="$cjdscript_rootdir" $silent_output
                cd "$cjdscript_rootdir/download"
                rm -rf matplotlib
                if file_match "$cjdscript_rootdir/lib/python2.7/site-packages" "$pyinfo_matplotlib"; then
                    printf '%s\n' "${c_g}DONE!$c_c"
                else
                    printf '%s\n' "${c_r}FAILED:$c_c an error occurred while installing" >&2
                fi
            else
                # Display an error stating that matplotlib didn't install correctly
                printf '%s\n' "${c_r}FAILED:$c_c an error occurred while downloading" >&2
            fi
        fi

        # INSTALL PYTHON2 SCRIPTS
        printf '%s\n' "$c_b==>$c_c Linking ${c_w}python2$c_c scripts"
        cd "$cjdscript_rootdir/bin"
        for python2_scriptname in "${python2_scripts[@]}"; do
            if [[ -f "$cjdscript_rootdir/download/cjdns/contrib/python/$python2_scriptname" ]]; then
                ln -sf "$cjdscript_rootdir/download/cjdns/contrib/python/$python2_scriptname" "$cjdscript_rootdir/bin/$python2_scriptname"
            else
                printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c $python2_scriptname does not exist and won't be installed"
            fi
        done
    else
        # Display an error stating that python2 scripts won't be available
        printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c Not including python2 scripts (python2 is not available)"
    fi
}

# BINARY SETUP: Triggered by setup_scripts to configure supported binaries
function setup_binaries {
    # Build CJDNS if node and python2 are available
    if [[ -e "$cjdscript_rootdir/bin/python" ]] && [[ -e "$cjdscript_rootdir/bin/node" ]]; then
        printf '%s' "$c_b==>$c_c Building ${c_w}binaries${c_c}... "
        pushd "$cjdscript_rootdir/download/cjdns" >/dev/null
        eval bash "do" $silent_output
        printf '%s\n' "${c_g}DONE!$c_c"
        cd "$cjdscript_rootdir/bin"

        printf '%s\n' "$c_b==>$c_c Linking ${c_w}binaries$c_c"
        for cjdns_binaryname in "${binaries[@]}"; do
            if [[ -f "$cjdscript_rootdir/download/cjdns/$cjdns_binaryname" ]]; then
                ln -sf "$cjdscript_rootdir/download/cjdns/$cjdns_binaryname" "$cjdscript_rootdir/bin/$cjdns_binaryname"
            else
                printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c $cjdns_binaryname does not exist and won't be installed"
            fi
        done
        popd >/dev/null
    else
        # Display an error stating that compiled binaries won't be available
        printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c Not building binaries (python2 and node are required)"
    fi
}

# MAIN SETUP: Configures cjdscript_rootdir then runs each of the setup functions
function setup_scripts {
    # Create the download folder if it doesn't already exist
    printf '%s\n' "$c_b==>$c_c Running setup and configuration for ${c_w}cjdscript$c_c"
    [[ ! -d "$cjdscript_rootdir/download" ]] \
        && install -d "$cjdscript_rootdir/download"

    # Clone the CJDNS repo if it doesn't exist or update it if it does
    if [[ ! -d "$cjdscript_rootdir/download/cjdns" ]]; then
        pushd "$cjdscript_rootdir/download" >/dev/null
        printf '%s' "$c_b==>$c_c Cloning the $c_w$cjdns_gitbranch$c_c branch of $c_y$cjdns_gitrepo${c_c}... "
        eval git clone "$cjdns_gitrepo" -b "$cjdns_gitbranch" $silent_output
        printf '%s\n' "${c_g}DONE!$c_c"
        popd >/dev/null
    else
        pushd "$cjdscript_rootdir/download/cjdns" >/dev/null
        printf '%s' "$c_b==>$c_c Cleaning and updating the cjdns repo @ $c_y${cjdscript_rootdir}/download/cjdns${c_c}... "
        eval git reset --hard $silent_output
        eval git clean -dxf $silent_output
        eval git pull origin $silent_output
        printf '%s\n' "${c_g}DONE!$c_c"

        printf '%s' "$c_b==>$c_c Checking out the $c_r$cjdns_gitbranch$c_c branch... "
        eval git checkout "$cjdns_gitbranch" $silent_output
        printf '%s\n' "${c_g}DONE!$c_c"
        popd >/dev/null
    fi

    # Create the lib folder if it doesn't already exist
    [[ ! -d "$cjdscript_rootdir/lib" ]] \
        && install -d "$cjdscript_rootdir/lib"

    # Delete the bin folder if it exists then create a new one and enter it
    [[ -d "$cjdscript_rootdir/bin" ]] \
        && rm -rf "$cjdscript_rootdir/bin"
    install -d "$cjdscript_rootdir/bin"

    # Run setup for each script type + binaries
    pushd "$cjdscript_rootdir/bin" >/dev/null
    setup_bash
    setup_node
    setup_python2
    setup_binaries
    popd >/dev/null

    # Update directory version
    printf '%s\n' "$cjdscript_version" > "$cjdscript_rootdir/.dirver"

    # Output information, run help and exit
    printf '%s\n' "$c_b==>$c_c Finished configuring ${c_w}cjdscript$c_c root @ $c_y$cjdscript_rootdir$c_c"
    cjdscript_help 0
}

# Takes a title and list of scripts prints them stylishly in a terminal, or lists them in a pipe
function display_scriptlist {
    # Store the title in $list_title then remove it from $@
    list_title="$1"
    shift

    # Build an array of only the available scripts
    local -a script_list
    for script in "$@"; do
        [[ -f "$cjdscript_rootdir/bin/$script" ]] \
            && script_list=( ${script_list[@]} "$script" )
    done
    [[ ${#script_list[*]} = 0 ]] \
        && return 1

    # The tput command is available and cjdscript is not in a pipe if colwidth > 0
    if (( colwidth > 0 )); then
        local titlewidth=$(( ${#list_title} + 6 ))
        local left_gap=$(( ( ( colwidth - titlewidth ) / 2 ) + 1 ))
        local right_gap=$(( ( colwidth - titlewidth ) - left_gap ))
        printf "%${left_gap}s%s%${right_gap}s\n" "" "$c_b$list_title$c_c" ""
        unset titlewidth left_gap right_gap

        counter=0
        printf ' '
        for script in "${script_list[@]}"; do
            local scriptwidth=$(( ${#script} + 6 ))
            local left_gap=$(( ( colwidth - scriptwidth ) / 2 ))
            local right_gap=$(( ( colwidth - scriptwidth ) - left_gap ))
            (( counter > 0 )) && (( counter < scripts_per_line )) \
                && printf '%s' " $c_d|$c_c "
            (( counter >= scripts_per_line )) && {
                counter=0
                printf '\n '
            }
            printf "%${left_gap}s%s%${right_gap}s" "" "$c_y$script$c_c" ""
            unset scriptwidth left_gap right_gap
            (( counter++ ))
        done
        printf '\n\n'
    else
        printf '%s' "$list_title:"
        for script in "${script_list[@]}"; do
            printf '%s' " $script"
        done
        printf '\n'
    fi
}

# LIST SCRIPTS: Output a list of the working scripts
function list_scripts {
    [[ -f "$cjdscript_rootdir/bin/bash" ]] \
        || unset bash_scripts
    [[ -f "$cjdscript_rootdir/bin/node" ]] \
        || unset nodejs_scripts
    [[ -f "$cjdscript_rootdir/bin/python2" ]] \
        || unset python2_scripts
    [[ -f "$cjdscript_rootdir/bin/cjdroute" ]] \
        || unset binaries

    # Calculate the width of each column if tput is available
    colwidth=0
    if [[ -t 1 ]] && type -P tput >/dev/null; then
        # Find the column width (the length of the longest script name + 1 for spacing)
        allscripts=( ${bash_scripts[@]} ${binaries[@]} ${nodejs_scripts[@]} ${python2_scripts[@]} )
        for script in "${allscripts[@]}"; do
            scriptwidth=${#script}
            if (( scriptwidth > colwidth )); then
                colwidth="$scriptwidth"
            fi
        done
        colwidth=$(( colwidth + 3 ))

        # Find the number of scripts that can be squeezed on a single line
        scripts_per_line=1
        termwidth="$(tput cols)"
        while (( ( ( scripts_per_line + 1 ) * colwidth ) < ( termwidth - 2 ) )); do
            (( scripts_per_line++ ))
        done
        colwidth=$(( colwidth + 6 ))
        while (( ( scripts_per_line * ( colwidth + 1 ) ) < ( termwidth - 2 ) )); do
            (( colwidth++ ))
        done
        colwidth=$(( colwidth - 3 ))

        # Add an extra blank line before pretty printing
        printf '\n'
    fi

    # List Bash scripts
    display_scriptlist "BASH SCRIPTS" "${bash_scripts[@]}"

    # List Node scripts
    display_scriptlist "NODE SCRIPTS" "${nodejs_scripts[@]}"

    # List Python2 scripts
    display_scriptlist "PYTHON2 SCRIPTS" "${python2_scripts[@]}"

    # List CJDNS binaries
    display_scriptlist "BINARIES" "${binaries[@]}"

    exit 0
}

# HELP: Output usage information and the list of valid options
function cjdscript_help {
    local leftcol_width=41
    printf '\n%s\n  %s\n  %s\n\n' "${c_b}USAGE${c_c}" "$c_r$script_name$c_c $c_d[${c_g}OPTION$c_d]$c_c" "$c_r$script_name$c_c $c_d[${c_m}SCRIPT$c_d]$c_c $c_d[${c_g}OPTION$c_d]$c_c $c_d[${c_g}OPTION...$c_d]$c_c"
    printf '%s\n' "${c_b}OPTIONS$c_c"
    printf "  %-${leftcol_width}s%s\n" "$c_w-u $c_d| $c_w--update$c_c" 'update/setup symlinks, libs and scripts'
    printf "  %-${leftcol_width}s%s\n" "$c_w-v $c_d| $c_w--verbose$c_c" 'run with the update command for verbose output'
    printf "  %-${leftcol_width}s%s\n" "$c_w-l $c_d| $c_w--list$c_c" 'display the list of working scripts'
    printf "  %-${leftcol_width}s%s\n\n" "$c_w-h $c_d| $c_w--help$c_c" 'show this help and exit'
    exit "$1"
}

# Run the exit function if ctrl-c is received
trap force_quit SIGINT SIGQUIT

# Display a warning if an update is needed provided cjdscript is not in a pipe
unset update_warning
[[ -t 1 ]] && [[ -d "$cjdscript_rootdir" ]] && {
    if [ ! -d "$cjdscript_rootdir/download/cjdns" ] \
        || [ ! -d "$cjdscript_rootdir/bin" ] \
        || [ ! -d "$cjdscript_rootdir/lib" ] \
        || [ ! -f "$cjdscript_rootdir/.dirver" ] \
        || [ ! "$(<"$cjdscript_rootdir/.dirver")" = "$cjdscript_version" ]
    then
        update_warning=1
    fi
}

# Unset config variables then parse any input
unset trigger_list trigger_update

if [ -n "$1" ]; then
    while [ -n "$1" ]; do
        case "$1" in
            -h|--help)
                cjdscript_help 0
                ;;
            -l|--list)
                trigger_list=1
                shift
                ;;
            -u|--update)
                unset update_warning
                trigger_update=1
                shift
                ;;
            -v|--verbose)
                unset silent_output
                shift
                ;;
            *)
                # Exit with an error if the argument isn't a valid script or one of the options above
                [[ -f "$cjdscript_rootdir/bin/$1" ]] || {
                    printf '\n%s\n' "$c_b==>$c_c ${c_r}ERROR:$c_c $1 is not a valid command" >&2
                    cjdscript_help 1
                }
                # Run the script with any arguments and exit with the script's completion status
                "$@"
                exit "$?"
                ;;
        esac
    done
elif [[ -d "$cjdscript_rootdir/bin" ]]; then
    list_scripts
fi

# Warn the user about needing to update if they haven't already attempted to do so
[[ -n "$update_warning" ]] \
    && printf '%s\n' "$c_b==>$c_c ${c_y}WARNING:$c_c The $c_r$script_name$c_c root is out of date! (to update: $c_r$script_name$c_c $c_w-u $c_d| $c_w--update$c_c)"

# List the scripts if $trigger_list is set
[[ -n "$trigger_list" ]] \
    && list_scripts

# Prompt user to ensure an update is intentional then run setup_scripts
if [ -n "$trigger_update" ] || [ ! -d "$cjdscript_rootdir" ]; then
    [[ -d "$cjdscript_rootdir" ]] && {
        printf '%s' "$c_b==>$c_c ${c_y}WARNING:$c_c Update the root filesystem @ $c_w$cjdscript_rootdir$c_c? (${c_b}YES$c_c to confirm): "
        read choice
        [[ "$choice" = 'YES' ]] || {
            printf '%s\n' "$c_b==>$c_c The ${c_w}cjdscript$c_c root will ${c_u}not$c_c be updated"
            exit 0
        }
    }
    setup_scripts
fi

# Exit successfully
exit 0
