Skip to content

Instantly share code, notes, and snippets.

@ChristopherA
Last active November 19, 2022 00:08
Show Gist options
  • Save ChristopherA/06ef69f641dc26f40d36b1456d9930c2 to your computer and use it in GitHub Desktop.
Save ChristopherA/06ef69f641dc26f40d36b1456d9930c2 to your computer and use it in GitHub Desktop.
MacOS Keychain for Git, GitHub, SSH, GPG
#!/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
#!/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