Last active
November 19, 2022 00:08
-
-
Save ChristopherA/06ef69f641dc26f40d36b1456d9930c2 to your computer and use it in GitHub Desktop.
MacOS Keychain for Git, GitHub, SSH, GPG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| #!/usr/bin/env bash | |
| printf "\nTESTING HELP\n\n" | |
| ./silhouette | |
| ./silhouette -h | |
| ./silhouette --help | |
| ./silhouette getsecret -h | |
| ./silhouette getsecret --help | |
| ./silhouette rmsecret -h | |
| ./silhouette rmsecret --help | |
| ./silhouette setsecret -h | |
| ./silhouette setsecret --help | |
| ./silhouette check -h | |
| ./silhouette check --help | |
| printf "\nTESTING MISSING PARAMETERS ERROR MESSAGES\n\n" | |
| ./silhouette getsecret | |
| ./silhouette setsecret | |
| ./silhouette setsecret silhouette.secret.test | |
| printf "\nTESTING INCORRECT PARAMETERS\n\n" | |
| ./silhouette notvalid | |
| printf "\nTESTING FUNCTIONALITY GET/SET SECRET\n\n" | |
| Random_Value=$(openssl rand -hex 12) | |
| printf "\nRandom_Value:\t$Random_Value\n" | |
| printf "\nADD-GENERIC-PASSWORD\n" | |
| # hides console output | |
| { std_err="$( { security add-generic-password -D secret -U -a christophera -s silhouette.secret.test -w $Random_Value login.keychain; } \ | |
| 2>&1 1>&3 3>&- )"; } 3>1 ; result=${PIPESTATUS[@]} ; # hides console output \ | |
| # 3>&2 2>&1 1>&3 3>&- )"; } 3>1 ; result=${PIPESTATUS[@]} ; shows console output MOVE THIS LINE UP | |
| printf "\nresult=\t'$result' \t std_err=\t'$std_err'\n" | |
| printf "\nFIND-GENERIC-PASSWORD\n" | |
| # hides console output | |
| { std_err="$( { security find-generic-password -a christophera -s silhouette.secret.test -w login.keychain; } \ | |
| 2>&1 1>&3 3>&- )"; } 3>1 ; result=${PIPESTATUS[@]} ; # hides console output \ | |
| # 3>&2 2>&1 1>&3 3>&- )"; } 3>1 ; result=${PIPESTATUS[@]} ; shows console output MOVE THIS LINE UP | |
| printf "\nresult=\t'$result' \t std_err=\t'$std_err'\n" | |
| printf "\nFAIL FIND-GENERIC-PASSWORD\n" | |
| # hides console output | |
| { std_err="$( { security find-generic-password -a christophera -s bilbo -w login.keychain; } \ | |
| 2>&1 1>&3 3>&- )"; } 3>1 ; result=${PIPESTATUS[@]} ; # hides console output \ | |
| # 3>&2 2>&1 1>&3 3>&- )"; } 3>1 ; result=${PIPESTATUS[@]} ; shows console output MOVE THIS LINE UP | |
| printf "\nresult=\t'$result' \t std_err=\t'$std_err'\n" | |
| printf "\nDELETE-GENERIC-PASSWORD V1\n" | |
| # hides console output | |
| { std_err="$( { security delete-generic-password -a christophera -s silhouette.secret.test login.keychain; } \ | |
| 2>&1 1>&3 3>&- )"; } 3>1 ; result=${PIPESTATUS[@]} ; # hides console output \ | |
| # 3>&2 2>&1 1>&3 3>&- )"; } 3>1 ; result=${PIPESTATUS[@]} ; shows console output MOVE THIS LINE UP | |
| printf "\nresult=\t'$result' \t std_err=\t'$std_err'\n" | |
| printf "\nFAIL DELETE-GENERIC-PASSWORD V2\n" | |
| # hides console output | |
| { std_err="$( { security delete-generic-password -a christophera -s silhouette.secret.test login.keychain; } \ | |
| 2>&1 1>&3 3>&- )"; } 3>1 ; result=${PIPESTATUS[@]} ; # hides console output \ | |
| # 3>&2 2>&1 1>&3 3>&- )"; } 3>1 ; result=${PIPESTATUS[@]} ; shows console output MOVE THIS LINE UP | |
| printf "\nresult=\t'$result' \t std_err=\t'$std_err'\n" | |
| ./silhouette setsecret silhouette.secret.test $Random_Value | |
| Match_Random_Value=$( ./silhouette getsecret silhouette.secret.test ) | |
| if [ -z "$Random_Value" ] || [ "$Random_Value" != "$Match_Random_Value" ] ; then | |
| printf "Random Value $Random_Value matches!\t✅\n" | |
| else | |
| printf "ERROR: Random Value $Random_Value does't match getsecret silhouette.secret.test $Match_Random_Value.\n" | |
| fi | |
| printf "\nTESTING FUNCTIONALITY RM SECRET\n\n" | |
| ./silhouette rmsecret silhouette.secret.test | |
| ./silhouette getsecret silhouette.secret.test | |
| printf "\nTESTING FUNCTIONALITY SILHOUTTE CHECK \n\n" | |
| ./silhouette check |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Local Silhouette Tool (usually at ~/bin/silhouette.sh) | |
| # Tool for getting and setting local user profile information from | |
| # MacOS keychain, in particular git, GitHub, SSH, and GPG credentials. | |
| # By Christopher Allen @Christopher under MIT License | |
| # Portions derived or inspired by other open source CLI scripts: | |
| # * Command Line Template code from | |
| # * https://github.com/ralish/bash-script-template/ | |
| # * Copyright Samuel D. Leslie @ralish under MIT License | |
| ### EXECUTED FIRST FOR DEBUGGING | |
| # Enable xtrace if the DEBUG environment variable is set | |
| if [[ ${DEBUG-} =~ ^1|yes|true$ ]]; then | |
| set -o xtrace # Trace the execution of the script (debug) | |
| fi | |
| # A better class of script... | |
| set -o errexit # Exit on most errors (see the manual) aka set -e | |
| set -o errtrace # Make sure any error trap is inherited | |
| set -o nounset # Disallow expansion of unset variables aka set -u | |
| set -o pipefail # Use last non-zero exit code in a pipeline | |
| ### FUNCTIONS | |
| # DESC: Main control flow | |
| # ARGS: $@ (optional): Arguments provided to the script | |
| # OUTS: None | |
| function _main () { | |
| trap script_trap_err ERR | |
| trap script_trap_exit EXIT | |
| script_init "$@" | |
| colour_init | |
| if [ -z "${1-}" ] ; then | |
| _script_usage | |
| return 0 | |
| fi | |
| if [[ "${1:-}" =~ ^-h$|^--help$ ]] | |
| then | |
| _script_usage | |
| return 0 | |
| fi | |
| case "$1" in | |
| check) _check "$@" ;; | |
| getsecret) _getsecret "$@" ;; | |
| rmsecret) _rmsecret "$@" ;; | |
| setsecret) _setsecret "$@" ;; | |
| *) script_exit "ERROR: Invalid parameter option '$@' for script '$script_name'. See '$script_name --help'." 1 ;; | |
| esac | |
| } | |
| # DESC: Usage help | |
| # ARGS: None | |
| # OUTS: None | |
| function _script_usage() { | |
| cat <<EOF | |
| Description: | |
| Tool for getting and setting local user profile information from | |
| MacOS keychain, in particular git, GitHub, SSH, and GPG credentials. | |
| Usage: | |
| $script_name check | |
| $script_name getsecret <secret.name> | |
| $script_name rmsecret <secret.name> | |
| $script_name setsecret <secret.name> <value> | |
| Options: | |
| -h | --help Display basic usage information for this command. | |
| <subcommand> -h | --help Display specific usage information for the subcommand | |
| EOF | |
| } | |
| ### SUBCOMMAND FUNCTIONS | |
| # DESC: Displays local profile information and checks details | |
| # ARGS: $@ (optional): Arguments provided to the script | |
| # OUTS: None | |
| function _check() { | |
| #printf "ThisSubCommand:\t$1\n" | |
| #printf "ThisSubCommandParameters:\t$2\n" | |
| Function_Name=$(echo ${FUNCNAME[0]} | cut -c 2-) | |
| if [[ "${2:-}" =~ ^-h$|^--help$ ]] | |
| then | |
| cat <<EOF | |
| Name: check profile | |
| Description: | |
| Retrieves local profile information from the computer, current user, | |
| and current user's MacOS login keychain. | |
| Usage: | |
| $script_name $Function_Name | |
| $script_name $Function_Name -h | --help | |
| Options: | |
| -h --help Display this usage information. | |
| EOF | |
| return 0 | |
| fi | |
| unset PROFILE_SYNC_REQUIRED | |
| _print_profile | |
| } | |
| function _print_profile () { | |
| printf "Local Profile Name:\tValue\t\t\t\tSource\t\tOK?\n" | |
| printf "===================\t=====\t\t\t\t======\t\t===\n" | |
| # Get currently logged in user | |
| Current_User=$( /usr/bin/stat -f "%Su" /dev/console ) | |
| if [ -z "$Current_User" ] ; then | |
| script_exit "ERROR: Unable to get Currently Logged In User!\n" 1 | |
| else | |
| printf "Current Logged In User:\t$Current_User\t\t\tstat\t\t✅\n" | |
| fi | |
| # Get hardware serial number | |
| Device_Serial_Number=$(/usr/sbin/ioreg -l | awk '/IOPlatformSerialNumber/ { split($0, line, "\""); printf("%s\n", line[4]); }') | |
| if [ -z "$Device_Serial_Number" ] ; then | |
| script_exit "ERROR: Unable to get Device Serial Number!\n" 1 | |
| else | |
| printf "Device Serial Number:\t$Device_Serial_Number\t\t\tireg\t\t✅\n" | |
| fi | |
| # Get cpu | |
| Device_CPU=$(/usr/sbin/sysctl -n machdep.cpu.brand_string | sed s/"Apple "// ) | |
| if [ -z "$Device_CPU" ] ; then | |
| script_exit "ERROR: Unable to get Device Serial Number!\n" 1 | |
| else | |
| printf "Device CPU:\t\t$Device_CPU\t\t\t\tireg\t\t✅\n" | |
| fi | |
| # Get desired hostname from login keychain | |
| Desired_Host_Name=$( _getsecret "$Device_Serial_Number.hostname" ) | |
| if [ -z "$Desired_Host_Name" ] ; then | |
| script_exit "ERROR: Unable to lookup Desired HostName from Device Serial Number $Device_Serial_Number!\n" 1 | |
| else | |
| printf "Desired HostName:\t$Desired_Host_Name\t\t\t\t$LOCAL_KEYCHAIN\t✅\n" | |
| fi | |
| # Match Desired_Host_Name against Device Serial Number | |
| Match_Device_Serial_Number=$( _getsecret "$Desired_Host_Name.device.serialnumber" ) | |
| if [ -z "$Match_Device_Serial_Number" ] || [ "$Match_Device_Serial_Number" != "$Device_Serial_Number" ] ; then | |
| export PROFILE_SYNC_REQUIRED=true | |
| pretty_print "SYNC REQUIRED: Device Serial Number $Device_Serial_Number doesn't match keychain lookup $Match_Device_Serial_Number!" "${fg_red-}" | |
| else | |
| printf "Match to Serial:\t$Match_Device_Serial_Number\t\t\tMATCH\t\t✅\n" | |
| fi | |
| # Get Model Name | |
| Device_Model_Name=$(system_profiler SPHardwareDataType | awk -F ': ' '/Model Name/ { print $2 }') | |
| if [ -z "$Device_Model_Name" ] ; then | |
| script_exit "ERROR: Unable to get Device Model Name!\n" 1 | |
| else | |
| printf "Device Model Number:\t$Device_Model_Name\t\t\tsystem_profiler\t✅\n" | |
| fi | |
| OS_Version=$(sw_vers -productVersion) | |
| # string comparison | |
| if [[ "$OS_Version" == 11.* ]]; then | |
| OS_Name="Big Sur" | |
| elif [[ "$OS_Version" == 10.15.* ]]; then | |
| OS_Name="Catalina" | |
| elif [[ "$OS_Version" == 10.14.* ]]; then | |
| OS_Name="Mojave" | |
| elif [[ "$OS_Version" == 10.13.* ]]; then | |
| OS_Name="High Sierra" | |
| elif [[ "$OS_Version" == 10.12.* ]]; then | |
| OS_Name="Sierra" | |
| elif [[ "$OS_Version" == 10.11.* ]]; then | |
| OS_Name="El Capitan" | |
| elif [[ "$OS_Version" == 10.10.* ]]; then | |
| OS_Name="Yosemite" | |
| elif [[ "$OS_Version" == 10.9.* ]]; then | |
| OS_Name="Mavericks" | |
| elif [[ "$OS_Version" == 10.8.* ]]; then | |
| OS_Name="Mountain Lion" | |
| elif [[ "$OS_Version" == 10.7.* ]]; then | |
| OS_Name="Lion" | |
| else | |
| OS_Name="Unknown" | |
| fi | |
| # Get human-friendly Computer Nume from SCUTIL | |
| SCUTIL_Computer_Name=$(scutil --get ComputerName) | |
| if [ -z "$SCUTIL_Computer_Name" ] ; then | |
| script_exit "ERROR: Unable to retrieve SCUTIL Computer Name $SCUTIL_Computer_Name!\n" 1 | |
| else | |
| printf "SCUtil Computer Name:\t$SCUTIL_Computer_Name\tscutil\t\t✅\n" | |
| fi | |
| # Match Desired_Computer_Name against SCUTIL_Computer_Name | |
| Desired_Computer_Name="$(tr '[:lower:]' '[:upper:]' <<< ${Desired_Host_Name:0:1})${Desired_Host_Name:1} $OS_Name $Device_Model_Name" | |
| if [ "$Desired_Computer_Name" != "$SCUTIL_Computer_Name" ] ; then | |
| export PROFILE_SYNC_REQUIRED=true | |
| pretty_print "SYNC REQUIRED: Desired Computer Name '$Desired_Computer_Name' doesn't match SCUTIL Computer Name '$SCUTIL_Computer_Name'!" "${fg_red-}" | |
| else | |
| printf "Match to Composite:\t$SCUTIL_Computer_Name\tMATCH\t\t✅\n" | |
| fi | |
| # Get ssh HostName from SCUTIL | |
| SCUTIL_Host_Name=$(scutil --get HostName) | |
| if [ -z "$SCUTIL_Host_Name" ] ; then | |
| script_exit "ERROR: Unable to retrieve SCUTIL Host Name $SCUTIL_Host_Name!\n" 1 | |
| else | |
| printf "SCUtil Host Name:\t$SCUTIL_Host_Name\t\t\t\tscutil\t\t✅\n" | |
| fi | |
| # Match Desired_Host_Name against SCUTIL_Host_Name | |
| if [ "$Desired_Host_Name" != "$SCUTIL_Host_Name" ] ; then | |
| export PROFILE_SYNC_REQUIRED=true | |
| pretty_print "SYNC REQUIRED: Desired Host Name '$Desired_Host_Name' doesn't match SCUTIL Host Name '$SCUTIL_Host_Name'!" "${fg_red-}" | |
| else | |
| printf "Match to Host Name:\t$SCUTIL_Host_Name\t\t\t\tMATCH\t\t✅\n" | |
| fi | |
| # Get Bonjour Local Host Name from SCUTIL | |
| SCUTIL_Local_Host_Name=$(scutil --get LocalHostName) | |
| if [ -z "$SCUTIL_Local_Host_Name" ] ; then | |
| script_exit "ERROR: Unable to retrieve SCUTIL Bonjour Local Host Name $SCUTIL_Local_Host_Name!\n" 1 | |
| else | |
| printf "SCUtil Bonjour Name:\t$SCUTIL_Local_Host_Name\t\t\t\tscutil\t\t✅\n" | |
| fi | |
| # Match Bonjour Local Host Name against SCUTIL_Host_Name | |
| if [ "$Desired_Host_Name" != "$SCUTIL_Local_Host_Name" ] ; then | |
| export PROFILE_SYNC_REQUIRED=true | |
| pretty_print "SYNC REQUIRED: Desired Host Name '$Desired_Host_Name' doesn't match SCUTIL Host Name '$SCUTIL_Local_Host_Name'!" "${fg_red-}" | |
| else | |
| printf "Match to Bonjour Name:\t$SCUTIL_Local_Host_Name\t\t\t\tMATCH\t\t✅\n" | |
| fi | |
| # Get GitHub token | |
| My_GitHub_Token=$( _getsecret "$Desired_Host_Name.$Current_User.github.token") | |
| if [ -z "$Desired_Host_Name" ] ; then | |
| script_exit "ERROR: Unable to find $My_GitHub_Token for $Desired_Host_Name.$Current_User!\n" 1 | |
| else | |
| printf "My GitHub Token:\t$My_GitHub_Token\t$LOCAL_KEYCHAIN\t✅\n" | |
| fi | |
| # Get GPG Key | |
| My_GPG_Key=$(curl https://github.com/$Current_User.gpg 2>/dev/null) | |
| if [ -z "$My_GPG_Key" ] ; then | |
| script_exit "ERROR: Unable to find $My_GPG_Key from https://github.com/$Current_User.gpg!\n" 1 | |
| else | |
| ## Get GPG Fingerprint from $THIS_GPG_KEY | |
| # ideally for github.com/christophera.gpg | |
| # THIS_GPG_KEY_FINGERPRINT= "FDFE14A54ECB30FC5D2274EFF8D36C91357405ED" | |
| My_GPG_KEY_Fingerprint=`echo $My_GPG_Key 2>/dev/null | gpg --with-colons --import-options show-only --import --fingerprint 2>/dev/null | awk -F: '$1 == "fpr" {print $10}' | head -1` | |
| printf "My GitHub Token:\t$My_GPG_Key_Fingerprint\tRETRIEVED\t✅\n" | |
| fi | |
| } | |
| # DESC: Retrieves secret from keychain | |
| # ARGS: $2 (REQUIRED) secret name to be retrieved from keychain | |
| # OUTS: None | |
| function _getsecret() { | |
| local Function_Name Secret_Name Secret_Value | |
| Function_Name=$(echo ${FUNCNAME[0]} | cut -c 2-) | |
| #printf "\n\tparams: 0: ${0-} 1: ${1-} 2: ${2-} 3: ${3-}\n" | |
| if [ $Function_Name == "${1-}" ]; then #being called from _main | |
| #printf "\tcalled from _main\n" | |
| shift | |
| else # function being called internally | |
| #printf "\tcalled internally\n" | |
| Function_Name="getsecret" | |
| fi | |
| if [ -z "${1-}" ] ; then | |
| script_exit "ERROR: '$script_name $Function_Name' requires a <secret.name> to search. See '$script_name $Function_Name --help'." | |
| fi | |
| if [[ "${1:-}" =~ ^-h$|^--help$ ]] | |
| then | |
| cat <<EOF | |
| Name: getsecret from login keychain | |
| Description: | |
| Searches for retrieves the named secret from the current user's | |
| MacOS login keychain. | |
| Usage: | |
| $script_name $Function_Name <secret.name> | |
| $script_name $Function_Name -h | --help | |
| Options: | |
| -h --help Display this usage information. | |
| Example: | |
| profile getsecret christophera.github.token | |
| EOF | |
| return 0 | |
| fi | |
| Current_User=$( /usr/bin/stat -f "%Su" /dev/console ) | |
| Secret_Value="$(security find-generic-password -a $Current_User -s "$1" -w $LOCAL_KEYCHAIN)" | |
| ## TBD: We need to do some better error handling here. For instance, if result is | |
| ## "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain." | |
| ## then we need to return this error so the function calling check and do the correct thing. | |
| ## in particular, in `profile check` the `Desired_Host_Name=` will be empty on first use as | |
| ## "$Device_Serial_Number.hostname" has not been set yet. | |
| echo $Secret_Value | |
| } | |
| # DESC: Removes secret from keychain | |
| # ARGS: $2 (REQUIRED) secret name on keychain to delete | |
| # OUTS: None | |
| function _rmsecret() { | |
| local Function_Name Secret_Name Secret_Value | |
| Function_Name=$(echo ${FUNCNAME[0]} | cut -c 2-) | |
| if [ $Function_Name == "${1-}" ]; then #being called from _main | |
| shift | |
| else # function being called internally | |
| Function_Name="rmsecret" | |
| fi | |
| if [ -z "${1-}" ] ; then | |
| script_exit "ERROR: '$script_name $Function_Name' requires a <secret.name> to delete. See '$script_name $Function_Name --help'." | |
| fi | |
| if [[ "${1:-}" =~ ^-h$|^--help$ ]] | |
| then | |
| cat <<EOF | |
| Name: rmsecret from login keychain | |
| Description: | |
| Searches for retrieves the named secret from the current user's | |
| MacOS login keychain. | |
| Usage: | |
| $script_name $Function_Name <secret.name> | |
| $script_name $Function_Name -h | --help | |
| Options: | |
| -h --help Display this usage information. | |
| Example: | |
| profile getsecret christophera.github.token | |
| EOF | |
| return 0 | |
| fi | |
| Current_User=$( /usr/bin/stat -f "%Su" /dev/console ) | |
| Result="$(security delete-generic-password -a $Current_User -s "$1" $LOCAL_KEYCHAIN)" | |
| #echo $Result | |
| } | |
| # DESC: Sets secret to local keychain | |
| # ARGS: $2 (REQUIRED) secret name to be stored in local keychain | |
| # ARGS: $3 (REQUIRED) value for secret to be stored in local keychain | |
| # OUTS: None | |
| function _setsecret() { | |
| local Function_Name Secret_Name Secret_Value Result | |
| Function_Name=$(echo ${FUNCNAME[0]} | cut -c 2-) | |
| #printf "\n\tparams: 0: ${0-} 1: ${1-} 2: ${2-} 3: ${3-}\n" | |
| if [ $Function_Name == "${1-}" ]; then #being called from _main | |
| #printf "\tcalled from _main\n" | |
| shift | |
| else # function being called internally | |
| #printf "\tcalled internally\n" | |
| Function_Name="setsecret" | |
| fi | |
| if [[ -z "${0-}" || -z "${1-}" || -z "${2-}" ]] ; then | |
| script_exit "ERROR: '$script_name $Function_Name' requires a <secret.name> & <value> to set. See '$script_name $Function_Name --help'." | |
| fi | |
| if [[ "${1:-}" =~ ^-h$|^--help$ ]] | |
| then | |
| cat <<EOF | |
| Name: setsecret from login keychain | |
| Description: | |
| Sets the named secret in the current user's MacOS login keychain | |
| to the value. | |
| Usage: | |
| $script_name $Function_Name <secret.name> <value> | |
| $script_name $Function_Name -h | --help | |
| Options: | |
| -h --help Display this usage information. | |
| Example: | |
| profile set secret christophera.git.email [email protected] | |
| EOF | |
| return 0 | |
| fi | |
| Current_User=$( /usr/bin/stat -f "%Su" /dev/console ) | |
| Result="$(security add-generic-password -D secret -U -a $Current_User -s "$1" -w "$2" $LOCAL_KEYCHAIN)" | |
| #echo $Result | |
| } | |
| ### UTILITY FUNCTIONS | |
| # DESC: Generic script initialisation | |
| # ARGS: $@ (optional): Arguments provided to the script | |
| # OUTS: $LOCAL_KEYCHAIN: The MacOS keychain used for storage of secrets | |
| # $orig_cwd: The current working directory when the script was run | |
| # $script_path: The full path to the script | |
| # $script_dir: The directory path of the script | |
| # $script_name: The file name of the script | |
| # $script_params: The original parameters provided to the script | |
| # $ta_none: The ANSI control code to reset all text attributes | |
| # NOTE: $script_path only contains the path that was used to call the script | |
| # and will not resolve any symlinks which may be present in the path. | |
| # You can use a tool like realpath to obtain the "true" path. The same | |
| # caveat applies to both the $script_dir and $script_name variables. | |
| # shellcheck disable=SC2034 | |
| function script_init() { | |
| # Exported Variables | |
| export LOCAL_KEYCHAIN="login.keychain" | |
| # Useful paths | |
| readonly orig_cwd="$PWD" | |
| readonly script_path="${BASH_SOURCE[0]}" | |
| readonly script_dir="$(dirname "$script_path")" | |
| readonly script_name="$(basename "$script_path")" | |
| readonly script_params="$*" | |
| # Important to always set as we use it in the exit handler | |
| readonly ta_none="$(tput sgr0 2> /dev/null || true)" | |
| } | |
| # DESC: Handler for unexpected errors | |
| # ARGS: $1 (optional): Exit code (defaults to 1) | |
| # OUTS: None | |
| function script_trap_err() { | |
| local exit_code=1 | |
| # Disable the error trap handler to prevent potential recursion | |
| trap - ERR | |
| # Consider any further errors non-fatal to ensure we run to completion | |
| set +o errexit | |
| set +o pipefail | |
| # Validate any provided exit code | |
| if [[ ${1-} =~ ^[0-9]+$ ]]; then | |
| exit_code="$1" | |
| fi | |
| # Output debug data if in Cron mode | |
| if [[ -n ${cron-} ]]; then | |
| # Restore original file output descriptors | |
| if [[ -n ${script_output-} ]]; then | |
| exec 1>&3 2>&4 | |
| fi | |
| # Print basic debugging information | |
| printf '%b\n' "$ta_none" | |
| printf '***** Abnormal termination of script *****\n' | |
| printf 'Script Path: %s\n' "$script_path" | |
| printf 'Script Parameters: %s\n' "$script_params" | |
| printf 'Script Exit Code: %s\n' "$exit_code" | |
| # Print the script log if we have it. It's possible we may not if we | |
| # failed before we even called cron_init(). This can happen if bad | |
| # parameters were passed to the script so we bailed out very early. | |
| if [[ -n ${script_output-} ]]; then | |
| printf 'Script Output:\n\n%s' "$(cat "$script_output")" | |
| else | |
| printf 'Script Output: None (failed before log init)\n' | |
| fi | |
| fi | |
| # Exit with failure status | |
| exit "$exit_code" | |
| } | |
| # DESC: Handler for exiting the script | |
| # ARGS: None | |
| # OUTS: None | |
| function script_trap_exit() { | |
| cd "$orig_cwd" | |
| # Remove Cron mode script log | |
| if [[ -n ${cron-} && -f ${script_output-} ]]; then | |
| rm "$script_output" | |
| fi | |
| # Remove script execution lock | |
| if [[ -d ${script_lock-} ]]; then | |
| rmdir "$script_lock" | |
| fi | |
| # Restore terminal colours | |
| printf '%b' "$ta_none" | |
| } | |
| # DESC: Exit script with the given message | |
| # ARGS: $1 (required): Message to print on exit | |
| # $2 (optional): Exit code (defaults to 0) | |
| # OUTS: None | |
| # NOTE: The convention used in this script for exit codes is: | |
| # 0: Normal exit | |
| # 1: Abnormal exit due to external error | |
| # 2: Abnormal exit due to script error | |
| function script_exit() { | |
| if [[ $# -eq 1 ]]; then | |
| printf '%s\n' "$1" | |
| exit 0 | |
| fi | |
| if [[ ${2-} =~ ^[0-9]+$ ]]; then | |
| printf '%b\n' "$1" | |
| # If we've been provided a non-zero exit code run the error trap | |
| if [[ $2 -ne 0 ]]; then | |
| script_trap_err "$2" | |
| else | |
| exit 0 | |
| fi | |
| fi | |
| script_exit 'Missing required argument to script_exit()!' 2 | |
| } | |
| # DESC: Initialise colour variables | |
| # ARGS: None | |
| # OUTS: Read-only variables with ANSI control codes | |
| # NOTE: If --no-colour was set the variables will be empty | |
| # shellcheck disable=SC2034 | |
| function colour_init() { | |
| if [[ -z ${no_colour-} ]]; then | |
| # Text attributes | |
| readonly ta_bold="$(tput bold 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly ta_uscore="$(tput smul 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly ta_blink="$(tput blink 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly ta_reverse="$(tput rev 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly ta_conceal="$(tput invis 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| # Foreground codes | |
| readonly fg_black="$(tput setaf 0 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly fg_blue="$(tput setaf 4 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly fg_cyan="$(tput setaf 6 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly fg_green="$(tput setaf 2 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly fg_magenta="$(tput setaf 5 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly fg_red="$(tput setaf 1 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly fg_white="$(tput setaf 7 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly fg_yellow="$(tput setaf 3 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| # Background codes | |
| readonly bg_black="$(tput setab 0 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly bg_blue="$(tput setab 4 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly bg_cyan="$(tput setab 6 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly bg_green="$(tput setab 2 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly bg_magenta="$(tput setab 5 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly bg_red="$(tput setab 1 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly bg_white="$(tput setab 7 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| readonly bg_yellow="$(tput setab 3 2> /dev/null || true)" | |
| printf '%b' "$ta_none" | |
| else | |
| # Text attributes | |
| readonly ta_bold='' | |
| readonly ta_uscore='' | |
| readonly ta_blink='' | |
| readonly ta_reverse='' | |
| readonly ta_conceal='' | |
| # Foreground codes | |
| readonly fg_black='' | |
| readonly fg_blue='' | |
| readonly fg_cyan='' | |
| readonly fg_green='' | |
| readonly fg_magenta='' | |
| readonly fg_red='' | |
| readonly fg_white='' | |
| readonly fg_yellow='' | |
| # Background codes | |
| readonly bg_black='' | |
| readonly bg_blue='' | |
| readonly bg_cyan='' | |
| readonly bg_green='' | |
| readonly bg_magenta='' | |
| readonly bg_red='' | |
| readonly bg_white='' | |
| readonly bg_yellow='' | |
| fi | |
| } | |
| # DESC: Initialise Cron mode | |
| # ARGS: None | |
| # OUTS: $script_output: Path to the file stdout & stderr was redirected to | |
| function cron_init() { | |
| if [[ -n ${cron-} ]]; then | |
| # Redirect all output to a temporary file | |
| readonly script_output="$(mktemp --tmpdir "$script_name".XXXXX)" | |
| exec 3>&1 4>&2 1> "$script_output" 2>&1 | |
| fi | |
| } | |
| # DESC: Acquire script lock | |
| # ARGS: $1 (optional): Scope of script execution lock (system or user) | |
| # OUTS: $script_lock: Path to the directory indicating we have the script lock | |
| # NOTE: This lock implementation is extremely simple but should be reliable | |
| # across all platforms. It does *not* support locking a script with | |
| # symlinks or multiple hardlinks as there's no portable way of doing so. | |
| # If the lock was acquired it's automatically released on script exit. | |
| function lock_init() { | |
| local lock_dir | |
| if [[ $1 = 'system' ]]; then | |
| lock_dir="/tmp/$script_name.lock" | |
| elif [[ $1 = 'user' ]]; then | |
| lock_dir="/tmp/$script_name.$UID.lock" | |
| else | |
| script_exit 'Missing or invalid argument to lock_init()!' 2 | |
| fi | |
| if mkdir "$lock_dir" 2> /dev/null; then | |
| readonly script_lock="$lock_dir" | |
| verbose_print "Acquired script lock: $script_lock" | |
| else | |
| script_exit "Unable to acquire script lock: $lock_dir" 1 | |
| fi | |
| } | |
| # DESC: Pretty print the provided string | |
| # ARGS: $1 (required): Message to print (defaults to a green foreground) | |
| # $2 (optional): Colour to print the message with. This can be an ANSI | |
| # escape code or one of the prepopulated colour variables. | |
| # $3 (optional): Set to any value to not append a new line to the message | |
| # OUTS: None | |
| function pretty_print() { | |
| if [[ $# -lt 1 ]]; then | |
| script_exit 'Missing required argument to pretty_print()!' 2 | |
| fi | |
| if [[ -z ${no_colour-} ]]; then | |
| if [[ -n ${2-} ]]; then | |
| printf '%b' "$2" | |
| else | |
| printf '%b' "$fg_green" | |
| fi | |
| fi | |
| # Print message & reset text attributes | |
| if [[ -n ${3-} ]]; then | |
| printf '%s%b' "$1" "$ta_none" | |
| else | |
| printf '%s%b\n' "$1" "$ta_none" | |
| fi | |
| } | |
| # DESC: Only pretty_print() the provided string if verbose mode is enabled | |
| # ARGS: $@ (required): Passed through to pretty_print() function | |
| # OUTS: None | |
| function verbose_print() { | |
| if [[ -n ${verbose-} ]]; then | |
| pretty_print "$@" | |
| fi | |
| } | |
| # DESC: Combines two path variables and removes any duplicates | |
| # ARGS: $1 (required): Path(s) to join with the second argument | |
| # $2 (optional): Path(s) to join with the first argument | |
| # OUTS: $build_path: The constructed path | |
| # NOTE: Heavily inspired by: https://unix.stackexchange.com/a/40973 | |
| function build_path() { | |
| if [[ $# -lt 1 ]]; then | |
| script_exit 'Missing required argument to build_path()!' 2 | |
| fi | |
| local new_path path_entry temp_path | |
| temp_path="$1:" | |
| if [[ -n ${2-} ]]; then | |
| temp_path="$temp_path$2:" | |
| fi | |
| new_path= | |
| while [[ -n $temp_path ]]; do | |
| path_entry="${temp_path%%:*}" | |
| case "$new_path:" in | |
| *:"$path_entry":*) ;; | |
| *) | |
| new_path="$new_path:$path_entry" | |
| ;; | |
| esac | |
| temp_path="${temp_path#*:}" | |
| done | |
| # shellcheck disable=SC2034 | |
| build_path="${new_path#:}" | |
| } | |
| # DESC: Check a binary exists in the search path | |
| # ARGS: $1 (required): Name of the binary to test for existence | |
| # $2 (optional): Set to any value to treat failure as a fatal error | |
| # OUTS: None | |
| function check_binary() { | |
| if [[ $# -lt 1 ]]; then | |
| script_exit 'Missing required argument to check_binary()!' 2 | |
| fi | |
| if ! command -v "$1" > /dev/null 2>&1; then | |
| if [[ -n ${2-} ]]; then | |
| script_exit "Missing dependency: Couldn't locate $1." 1 | |
| else | |
| verbose_print "Missing dependency: $1" "${fg_red-}" | |
| return 1 | |
| fi | |
| fi | |
| verbose_print "Found dependency: $1" | |
| return 0 | |
| } | |
| # DESC: Validate we have superuser access as root (via sudo if requested) | |
| # ARGS: $1 (optional): Set to any value to not attempt root access via sudo | |
| # OUTS: None | |
| function check_superuser() { | |
| local superuser | |
| if [[ $EUID -eq 0 ]]; then | |
| superuser=true | |
| elif [[ -z ${1-} ]]; then | |
| if check_binary sudo; then | |
| verbose_print 'Sudo: Updating cached credentials ...' | |
| if ! sudo -v; then | |
| verbose_print "Sudo: Couldn't acquire credentials ..." \ | |
| "${fg_red-}" | |
| else | |
| local test_euid | |
| test_euid="$(sudo -H -- "$BASH" -c 'printf "%s" "$EUID"')" | |
| if [[ $test_euid -eq 0 ]]; then | |
| superuser=true | |
| fi | |
| fi | |
| fi | |
| fi | |
| if [[ -z ${superuser-} ]]; then | |
| verbose_print 'Unable to acquire superuser credentials.' "${fg_red-}" | |
| return 1 | |
| fi | |
| verbose_print 'Successfully acquired superuser credentials.' | |
| return 0 | |
| } | |
| # DESC: Run the requested command as root (via sudo if requested) | |
| # ARGS: $1 (optional): Set to zero to not attempt execution via sudo | |
| # $@ (required): Passed through for execution as root user | |
| # OUTS: None | |
| function run_as_root() { | |
| if [[ $# -eq 0 ]]; then | |
| script_exit 'Missing required argument to run_as_root()!' 2 | |
| fi | |
| if [[ ${1-} =~ ^0$ ]]; then | |
| local skip_sudo=true | |
| shift | |
| fi | |
| if [[ $EUID -eq 0 ]]; then | |
| "$@" | |
| elif [[ -z ${skip_sudo-} ]]; then | |
| sudo -H -- "$@" | |
| else | |
| script_exit "Unable to run requested command as root: $*" 1 | |
| fi | |
| } | |
| ### START SCRIPT EXECUTION | |
| ### Now that functions are loaded, execute script | |
| _main "$@" | |
| # script_exit "Script '$script_name $@' execution complete with no errors." 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment