@thalesmello Normally, BTT users manually populate this information when creating bug reports on this forum. However, I decided to write a bash script CLI to automatically generate this information.
Note: I've been using this script for several months and it works well, but please let me know if you find any bugs.
Here's how I use it – I have a Key Sequences / Typed Words Trigger configured in BTT such that when I typed /BTTD
, BTT will run the below bash script and replace the /BTTD
text with my device info like what you saw in my previous post.
Here is the bash script:
#!/bin/bash
###############################################################################
# BetterTouchTool Version Checker CLI - With Improved Structured Logging
###############################################################################
# Purpose: This script checks your installed BetterTouchTool version and compares
# it with the latest available alpha and regular versions. It provides formatted
# output for easy sharing of your BTT environment information.
#
# INSTALLATION & USAGE INSTRUCTIONS:
# 1. Save this file as "get_BTT_info.sh" in a location of your choice
# 2. Make the script executable by running this command in Terminal:
# chmod +x /path/to/get_BTT_info.sh
# 3. Run the script using one of these methods:
# a) ./get_BTT_info.sh (if you're in the same directory)
# b) /path/to/get_BTT_info.sh (using the full or relative path)
# c) bash /path/to/get_BTT_info.sh (if you didn't make it executable)
#
# By default, output is copied to clipboard in markdown format for easy forum posting
# Use -h or --help to see all available options
###############################################################################
# Default configurations
log_level="NONE" # Options: NONE, ERROR, WARNING, INFO, DEBUG
output_format="markdown" # Options: markdown, plain, json
copy_to_clipboard=true # Set to false with '-n' or '--no-copy'
colorize_logs=false # Enable colored logging with '--color'
# Initialize variables for installed version and build
installed_version=""
installed_build=""
# Track if we had to increment the build
incremented_build=false
original_installed_build=""
# Required utilities that must be available on the system
required_utilities=(
"curl" # For fetching online version information
"grep" # For text pattern matching
"awk" # For text processing
"sed" # For string manipulation
"ioreg" # For hardware information
"defaults" # For reading macOS property lists
"/usr/libexec/PlistBuddy" # For advanced property list manipulation
"sw_vers" # For macOS version information
"pbcopy" # For copying output to clipboard
)
# ANSI Color Codes (if --color is enabled)
RED="\033[0;31m"
GREEN="\033[0;32m"
BLUE="\033[0;34m"
YELLOW="\033[0;33m"
WHITE="\033[0;37m"
RESET="\033[0m"
# Display help and usage information
usage() {
cat << EOF
Usage: $0 [options]
Basic Options:
-l, --log-level LEVEL Set the logging level (NONE, ERROR, WARNING, INFO, DEBUG). Default: NONE
-o, --output-format FORMAT Set output format (markdown, plain, json). Default: markdown
-c, --copy Copy output to clipboard (enabled by default)
-n, --no-copy Do not copy output to clipboard
--color Enable colored logs
-h, --help Display this help message and exit
Advanced/Testing Options:
-v, --installed-version VERSION Manually specify BTT version (bypasses auto-detection)
-b, --installed-build BUILD Manually specify BTT build (bypasses auto-detection)
Examples:
$0 # Auto-detect installed BTT version
$0 -l DEBUG -o plain # Set log level to DEBUG and output format to plain text
$0 -o json -n # Set output format to JSON and do not copy to clipboard
$0 --color # Enable colored logs
$0 -v 4.702 -b 2024091705 # Testing: Simulate a specific BTT version/build
EOF
exit 1
}
# Convert log level string to numeric value for comparison
log_level_num() {
case "$1" in
"NONE") echo -1 ;;
"ERROR") echo 0 ;;
"WARNING") echo 1 ;;
"INFO") echo 2 ;;
"DEBUG") echo 3 ;;
*) echo -1 ;; # Default to NONE
esac
}
# Logging function with configurable levels and formatting
log() {
# Field widths for alignment
local level_width=20
local func_width=25
local caller_function="${FUNCNAME[1]}"
[ -z "$caller_function" ] && caller_function="main"
local level=$1
shift
local message="$*"
local current_level
current_level=$(log_level_num "$log_level")
local message_level
message_level=$(log_level_num "$level")
# Only output if message level is less than or equal to current log level
if [ "$current_level" -ge "$message_level" ]; then
local color=""
if [ "$colorize_logs" = true ]; then
case "$level" in
"ERROR") color="$RED" ;;
"WARNING") color="$YELLOW" ;;
"INFO") color="$GREEN" ;;
"DEBUG") color="$BLUE" ;;
*) color="$WHITE" ;;
esac
printf "%-${level_width}b %-${func_width}b %b\n" \
"[$color$level$RESET]" \
"[$caller_function]" \
"${WHITE}$message$RESET" >&2
else
printf "%-${level_width}s %-${func_width}s %s\n" \
"[$level]" \
"[$caller_function]" \
"$message" >&2
fi
fi
}
# Check if all required utilities are available on the system
check_requirements() {
local missing_utilities=()
for util in "${required_utilities[@]}"; do
util_path=$(command -v "$util")
if [ -x "$util_path" ]; then
log "DEBUG" "Found utility '$util' at '$util_path'"
else
log "ERROR" "Required utility '$util' not found in PATH."
missing_utilities+=("$util")
fi
done
# Exit if any required utilities are missing
if [ "${#missing_utilities[@]}" -ne 0 ]; then
log "ERROR" "Missing utilities: ${missing_utilities[*]}"
exit 1
fi
}
# Compare two version numbers (e.g., 4.702 vs 4.705)
# Returns 1 if ver1 > ver2, 0 if equal, -1 if ver1 < ver2
version_compare() {
local ver1="$1"
local ver2="$2"
IFS='.' read -ra ver1_parts <<< "$ver1"
IFS='.' read -ra ver2_parts <<< "$ver2"
local max_index=$(( ${#ver1_parts[@]} > ${#ver2_parts[@]} ? ${#ver1_parts[@]} : ${#ver2_parts[@]} ))
# Compare each part of the version number
for ((i=0; i<max_index; i++)); do
local part1=${ver1_parts[i]:-0}
local part2=${ver2_parts[i]:-0}
if ((10#$part1 > 10#$part2)); then
log "DEBUG" "$ver1 > $ver2"
echo 1
return
elif ((10#$part1 < 10#$part2)); then
log "DEBUG" "$ver1 < $ver2"
echo -1
return
fi
done
log "DEBUG" "$ver1 == $ver2"
echo 0
}
# Get installed BTT version from the application bundle
get_installed_version() {
if [ -z "$installed_version" ] || [ -z "$installed_build" ]; then
# Read version and build from BTT's Info.plist
installed_version=$(defaults read "/Applications/BetterTouchTool.app/Contents/Info.plist" CFBundleShortVersionString 2>/dev/null)
installed_build=$(defaults read "/Applications/BetterTouchTool.app/Contents/Info.plist" CFBundleVersion 2>/dev/null)
fi
# Exit if BTT is not installed or inaccessible
if [ -z "$installed_version" ] || [ -z "$installed_build" ]; then
log "ERROR" "BetterTouchTool not installed or Info.plist inaccessible."
exit 1
fi
log "DEBUG" "installed_version: $installed_version"
log "DEBUG" "installed_build: $installed_build"
original_installed_build="$installed_build"
installed_version_full="$installed_version ($installed_build)"
log "DEBUG" "installed_version_full: $installed_version_full"
}
# Fetch available alpha versions from the BTT website
fetch_alpha_versions() {
# Parse the releases page to extract alpha version information
available_versions=$(curl -s https://folivora.ai/releases/ | grep -o '"btt[0-9]\+\.[0-9]\+-[0-9]\+\.zip"')
formatted_versions=$(echo "$available_versions" | sed -E 's/"btt([0-9]+\.[0-9]+)-([0-9]+)\.zip"/\1 (\2)/')
# Get the latest alpha version (first in the list)
latest_alpha_version=$(echo "$formatted_versions" | head -n 1)
# Log available alpha versions for debugging
number_of_alpha_versions_to_log=10
log "DEBUG" "Available alpha versions (displaying $number_of_alpha_versions_to_log most recent): $(echo "$formatted_versions" | head -n $number_of_alpha_versions_to_log | tr '\n' ' ')"
}
# Determine status relative to alpha versions
determine_alpha_status() {
# Find the position of installed version in the available alpha versions list
installed_index=$(echo "$formatted_versions" | awk -v installed="$installed_version_full" '
{
if ($0 == installed) {
print NR;
exit
}
}')
# Set the message based on whether the installed version was found
if [ -z "$installed_index" ]; then
log "WARNING" "Installed version not found in available alpha versions."
alpha_version_message="Installed alpha version not found."
else
log "DEBUG" "Installed version found at index $installed_index"
alpha_updates_behind=$((installed_index - 1))
alpha_version_message="$alpha_updates_behind update(s) behind latest Alpha version $latest_alpha_version."
fi
}
# Fetch the latest regular version from the BTT website
fetch_regular_version() {
log "INFO" "Fetching latest regular version..."
# Extract version numbers from the release notes page
regular_versions=$(curl -s https://updates.folivora.ai/bettertouchtool_release_notes.html | grep -Eo 'BetterTouchTool >= [0-9]+\.[0-9]+' | sed -E 's/BetterTouchTool >= //g' | sort -V)
latest_regular_version=$(echo "$regular_versions" | tail -n 1)
log "INFO" "Latest regular version: $latest_regular_version"
}
# Compare installed version with the latest regular version
determine_regular_status() {
# Compare versions using the version_compare function
comparison_result=$(version_compare "$installed_version" "$latest_regular_version")
# Set message based on comparison result
if [ "$comparison_result" -eq 0 ]; then
regular_version_message="Up to date with latest Regular version $latest_regular_version."
elif [ "$comparison_result" -eq 1 ]; then
regular_version_message="Installed version is ahead of latest Regular version $latest_regular_version."
else
regular_version_message="Installed version is behind latest Regular version $latest_regular_version."
fi
log "INFO" "$regular_version_message"
}
# Get system information (device model and macOS version)
get_system_info() {
# Get device model information
device_info="$(/usr/libexec/PlistBuddy -c 'Print :0:product-name' /dev/stdin <<< "$(ioreg -arc IOPlatformDevice -k product-name)")"
# Get macOS version information
macos_build="$(sw_vers -buildVersion)"
macos_product_version="$(sw_vers -productVersion)"
# Determine if running a beta version of macOS
if [[ "$macos_build" =~ [a-zA-Z]$ ]]; then
macos_version="$macos_product_version Beta ($macos_build)"
else
macos_version="$macos_product_version ($macos_build)"
fi
log "DEBUG" "device_info: $device_info"
log "DEBUG" "macos_version: $macos_version"
}
# Generate the final output in the specified format
generate_output() {
# Handle special case where build number was incremented
if [ "$incremented_build" = true ]; then
installed_version_full="$installed_version ($original_installed_build)"
alpha_version_message="1 uninstallable update behind latest Alpha version $latest_alpha_version"
fi
# Generate output based on the selected format
if [ "$output_format" == "markdown" ]; then
output_content=$(cat << EOF
- **Device**: $device_info
- **macOS**: $macos_version
- **BTT Version**: $installed_version_full
- **Alpha**: $alpha_version_message
- **Regular**: $regular_version_message
EOF
)
elif [ "$output_format" == "plain" ]; then
output_content=$(cat << EOF
Device: $device_info
macOS: $macos_version
BTT Version: $installed_version_full
Alpha: $alpha_version_message
Regular: $regular_version_message
EOF
)
elif [ "$output_format" == "json" ]; then
output_content=$(printf "{\n \"Device\": \"%s\",\n \"macOS\": \"%s\",\n \"BTT_Version\": \"%s\",\n \"Alpha\": \"%s\",\n \"Regular\": \"%s\"\n}" \
"$device_info" \
"$macos_version" \
"$installed_version_full" \
"$alpha_version_message" \
"$regular_version_message")
else
log "ERROR" "Unsupported output format: $output_format"
exit 1
fi
# Print the output to standard output
echo "$output_content"
# Copy to clipboard if enabled
if [ "$copy_to_clipboard" = true ]; then
echo "$output_content" | pbcopy
log "INFO" "Output copied to clipboard."
fi
}
# Main function to orchestrate the script execution
main() {
# Parse command-line arguments
while [[ "$#" -gt 0 ]]; do
case $1 in
-v|--installed-version)
installed_version="$2"
shift 2
;;
-b|--installed-build)
installed_build="$2"
shift 2
;;
-l|--log-level)
log_level="$2"
shift 2
;;
-o|--output-format)
output_format="$2"
shift 2
;;
-c|--copy)
copy_to_clipboard=true
shift
;;
-n|--no-copy)
copy_to_clipboard=false
shift
;;
--color)
colorize_logs=true
shift
;;
-h|--help)
usage
;;
*)
log "ERROR" "Unknown parameter passed: $1"
usage
;;
esac
done
# Validate log level
case "$log_level" in
NONE|ERROR|WARNING|INFO|DEBUG)
;;
*)
log "ERROR" "Invalid log level: $log_level"
usage
;;
esac
# Validate output format
case "$output_format" in
markdown|plain|json)
;;
*)
log "ERROR" "Invalid output format: $output_format"
usage
;;
esac
# Log the configuration
log "INFO" "Configuration: log_level=$log_level, output_format=$output_format, copy_to_clipboard=$copy_to_clipboard, installed_version=$installed_version, installed_build=$installed_build, colorize_logs=$colorize_logs"
# Execute the main workflow
check_requirements
get_installed_version
fetch_alpha_versions
determine_alpha_status
# Handle the case where installed version wasn't found in alpha versions
if [[ "$alpha_version_message" == "Installed alpha version not found." ]]; then
log "DEBUG" "Installed version not found. Trying with incremented build number..."
installed_build=$((installed_build + 1))
installed_version_full="$installed_version ($installed_build)"
incremented_build=true
log "DEBUG" "Retesting with incremented installed_build: $installed_version_full"
determine_alpha_status
fi
fetch_regular_version
determine_regular_status
get_system_info
generate_output
log "INFO" "Script completed successfully."
}
# Start the script
main "$@"
To replicate my exact setup as I described above (i.e. Key Sequences / Typed Words Trigger whenever I type /BTTD
), you can copy the JSON below and then paste it into the GUI of BTT.
[
{
"BTTActionCategory" : 0,
"BTTLastUpdatedAt" : 1741310204.9550328,
"BTTTriggerType" : 624,
"BTTTriggerTypeDescriptionReadOnly" : "Please Select a Trigger ",
"BTTTriggerClass" : "BTTTriggerTypeKeySequence",
"BTTUUID" : "61A1AE2F-C6E0-4FEC-925C-FF65E0D338F9",
"BTTPredefinedActionType" : 366,
"BTTPredefinedActionName" : "Empty Placeholder",
"BTTEnabled" : 1,
"BTTEnabled2" : 1,
"BTTOrder" : 0,
"BTTAdditionalActions" : [
{
"BTTActionCategory" : 0,
"BTTLastUpdatedAt" : 1741311265.283988,
"BTTTriggerParentUUID" : "61A1AE2F-C6E0-4FEC-925C-FF65E0D338F9",
"BTTIsPureAction" : true,
"BTTTriggerClass" : "BTTTriggerTypeKeySequence",
"BTTUUID" : "72868EBC-85DD-442E-A7D0-94EE24F92F4C",
"BTTPredefinedActionType" : 206,
"BTTPredefinedActionName" : "Execute Shell Script or Task",
"BTTShellTaskActionScript" : "$HOME\/projects\/BTT\/get_BTT_info.sh",
"BTTShellTaskActionConfig" : "\/bin\/bash:::-c:::-:::",
"BTTEnabled" : 1,
"BTTEnabled2" : 1,
"BTTOrder" : 730
},
{
"BTTActionCategory" : 0,
"BTTLastUpdatedAt" : 1741310097.5546122,
"BTTTriggerParentUUID" : "61A1AE2F-C6E0-4FEC-925C-FF65E0D338F9",
"BTTIsPureAction" : true,
"BTTTriggerClass" : "BTTTriggerTypeKeySequence",
"BTTUUID" : "3784EC61-68EF-4F45-A0D9-245D73240A39",
"BTTLayoutIndependentActionChar" : "v",
"BTTShortcutToSend" : "55,9",
"BTTEnabled" : 1,
"BTTEnabled2" : 1,
"BTTOrder" : 731
}
],
"BTTKeySequence" : {
"BTTPauseBetween" : 0.6015528549382716,
"BTTCharactersToDeleteAfterwards" : 5,
"BTTKeyCount" : 11,
"BTTKeySequenceDownKeys" : [
{
"BTTKEYCharacter" : " \/",
"BTTKEYCode" : 44,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : "r⇧ ",
"BTTKEYCode" : 60,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " B",
"BTTKEYCode" : 11,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " T",
"BTTKEYCode" : 17,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " T",
"BTTKEYCode" : 17,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " D",
"BTTKEYCode" : 2,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
}
],
"BTTKeySequenceMixedKeys" : [
{
"BTTKEYCharacter" : " \/",
"BTTKEYCode" : 44,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " \/",
"BTTKEYCode" : 44,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : "r⇧ ",
"BTTKEYCode" : 60,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " B",
"BTTKEYCode" : 11,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " B",
"BTTKEYCode" : 11,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " T",
"BTTKEYCode" : 17,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " T",
"BTTKEYCode" : 17,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " T",
"BTTKEYCode" : 17,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " T",
"BTTKEYCode" : 17,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " D",
"BTTKEYCode" : 2,
"BTTKEYDown" : 1,
"BTTKEYOrderRelevant" : 1,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " D",
"BTTKEYCode" : 2,
"BTTKEYRequired" : 1
}
],
"BTTKeySequenceUpKeys" : [
{
"BTTKEYCharacter" : " \/",
"BTTKEYCode" : 44,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " B",
"BTTKEYCode" : 11,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " T",
"BTTKEYCode" : 17,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " T",
"BTTKEYCode" : 17,
"BTTKEYRequired" : 1
},
{
"BTTKEYCharacter" : " D",
"BTTKEYCode" : 2,
"BTTKEYRequired" : 1
}
]
},
"BTTTriggerConfig" : {
"BTTHUDText" : "Triggered",
"BTTShowHUD" : 1
}
}
]
I hope this helps!