#!/usr/bin/env bash # Get the curent directory, the script name # and the script name with "py" substituted for the extension. args=( "$@" ) dir="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" script="${0##*/}" target="${script%.*}.py" # use_py3: # TRUE = Use if found, use py2 otherwise # FALSE = Use py2 # FORCE = Use py3 use_py3="TRUE" # We'll parse if the first argument passed is # --install-python and if so, we'll just install just_installing="FALSE" tempdir="" compare_to_version () { # Compares our OS version to the passed OS version, and # return a 1 if we match the passed compare type, or a 0 if we don't. # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) # $2 = OS version to compare ours to if [ -z "$1" ] || [ -z "$2" ]; then # Missing info - bail. return fi local current_os= comp= current_os="$(sw_vers -productVersion)" comp="$(vercomp "$current_os" "$2")" # Check gequal and lequal first if [[ "$1" == "3" && ("$comp" == "1" || "$comp" == "0") ]] || [[ "$1" == "4" && ("$comp" == "2" || "$comp" == "0") ]] || [[ "$comp" == "$1" ]]; then # Matched echo "1" else # No match echo "0" fi } set_use_py3_if () { # Auto sets the "use_py3" variable based on # conditions passed # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) # $2 = OS version to compare # $3 = TRUE/FALSE/FORCE in case of match if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then # Missing vars - bail with no changes. return fi if [ "$(compare_to_version "$1" "$2")" == "1" ]; then use_py3="$3" fi } get_remote_py_version () { local pyurl= py_html= py_vers= py_num="3" pyurl="https://www.python.org/downloads/macos/" py_html="$(curl -L $pyurl --compressed 2>&1)" if [ -z "$use_py3" ]; then use_py3="TRUE" fi if [ "$use_py3" == "FALSE" ]; then py_num="2" fi py_vers="$(echo "$py_html" | grep -i "Latest Python $py_num Release" | awk '{print $8}' | cut -d'<' -f1)" echo "$py_vers" } download_py () { local vers="$1" url= clear echo " ### ###" echo " # Downloading Python #" echo "### ###" echo if [ -z "$vers" ]; then echo "Gathering latest version..." vers="$(get_remote_py_version)" fi if [ -z "$vers" ]; then # Didn't get it still - bail print_error fi echo "Located Version: $vers" echo echo "Building download url..." url="$(curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | grep -iE "python-$vers-macos.*.pkg\"" | awk -F'"' '{ print $2 }')" if [ -z "$url" ]; then # Couldn't get the URL - bail print_error fi echo " - $url" echo echo "Downloading..." echo # Create a temp dir and download to it tempdir="$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')" curl "$url" -o "$tempdir/python.pkg" if [ "$?" != "0" ]; then echo echo " - Failed to download python installer!" echo exit $? fi echo echo "Running python install package..." echo sudo installer -pkg "$tempdir/python.pkg" -target / if [ "$?" != "0" ]; then echo echo " - Failed to install python!" echo exit $? fi # Now we expand the package and look for a shell update script pkgutil --expand "$tempdir/python.pkg" "$tempdir/python" if [ -e "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" ]; then # Run the script echo echo "Updating PATH..." echo "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" fi vers_folder="Python $(echo "$vers" | cut -d'.' -f1 -f2)" if [ -f "/Applications/$vers_folder/Install Certificates.command" ]; then # Certs script exists - let's execute that to make sure our certificates are updated echo echo "Updating Certificates..." echo "/Applications/$vers_folder/Install Certificates.command" fi echo echo "Cleaning up..." cleanup echo if [ "$just_installing" == "TRUE" ]; then echo "Done." else # Now we check for py again echo "Rechecking py..." downloaded="TRUE" clear main fi } cleanup () { if [ -d "$tempdir" ]; then rm -Rf "$tempdir" fi } print_error() { clear cleanup echo " ### ###" echo " # Python Not Found #" echo "### ###" echo echo "Python is not installed or not found in your PATH var." echo if [ "$kernel" == "Darwin" ]; then echo "Please go to https://www.python.org/downloads/macos/ to" echo "download and install the latest version, then try again." else echo "Please install python through your package manager and" echo "try again." fi echo exit 1 } print_target_missing() { clear cleanup echo " ### ###" echo " # Target Not Found #" echo "### ###" echo echo "Could not locate $target!" echo exit 1 } format_version () { local vers="$1" echo "$(echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }')" } vercomp () { # Modified from: https://apple.stackexchange.com/a/123408/11374 local ver1="$(format_version "$1")" ver2="$(format_version "$2")" if [ $ver1 -gt $ver2 ]; then echo "1" elif [ $ver1 -lt $ver2 ]; then echo "2" else echo "0" fi } get_local_python_version() { # $1 = Python bin name (defaults to python3) # Echoes the path to the highest version of the passed python bin if any local py_name="$1" max_version= python= python_version= python_path= if [ -z "$py_name" ]; then py_name="python3" fi py_list="$(which -a "$py_name" 2>/dev/null)" # Walk that newline separated list while read python; do if [ -z "$python" ]; then # Got a blank line - skip continue fi if [ "$check_py3_stub" == "1" ] && [ "$python" == "/usr/bin/python3" ]; then # See if we have a valid developer path xcode-select -p > /dev/null 2>&1 if [ "$?" != "0" ]; then # /usr/bin/python3 path - but no valid developer dir continue fi fi python_version="$(get_python_version $python)" if [ -z "$python_version" ]; then # Didn't find a py version - skip continue fi # Got the py version - compare to our max if [ -z "$max_version" ] || [ "$(vercomp "$python_version" "$max_version")" == "1" ]; then # Max not set, or less than the current - update it max_version="$python_version" python_path="$python" fi done <<< "$py_list" echo "$python_path" } get_python_version() { local py_path="$1" py_version= # Get the python version by piping stderr into stdout (for py2), then grepping the output for # the word "python", getting the second element, and grepping for an alphanumeric version number py_version="$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E "[A-Za-z\d\.]+")" if [ ! -z "$py_version" ]; then echo "$py_version" fi } prompt_and_download() { if [ "$downloaded" != "FALSE" ] || [ "$kernel" != "Darwin" ]; then # We already tried to download, or we're not on macOS - just bail print_error fi clear echo " ### ###" echo " # Python Not Found #" echo "### ###" echo target_py="Python 3" printed_py="Python 2 or 3" if [ "$use_py3" == "FORCE" ]; then printed_py="Python 3" elif [ "$use_py3" == "FALSE" ]; then target_py="Python 2" printed_py="Python 2" fi echo "Could not locate $printed_py!" echo echo "This script requires $printed_py to run." echo while true; do read -p "Would you like to install the latest $target_py now? (y/n): " yn case $yn in [Yy]* ) download_py;break;; [Nn]* ) print_error;; esac done } main() { local python= version= # Verify our target exists if [ ! -f "$dir/$target" ]; then # Doesn't exist print_target_missing fi if [ -z "$use_py3" ]; then use_py3="TRUE" fi if [ "$use_py3" != "FALSE" ]; then # Check for py3 first python="$(get_local_python_version python3)" fi if [ "$use_py3" != "FORCE" ] && [ -z "$python" ]; then # We aren't using py3 explicitly, and we don't already have a path python="$(get_local_python_version python2)" if [ -z "$python" ]; then # Try just looking for "python" python="$(get_local_python_version python)" fi fi if [ -z "$python" ]; then # Didn't ever find it - prompt prompt_and_download return 1 fi # Found it - start our script and pass all args "$python" "$dir/$target" "${args[@]}" } # Keep track of whether or not we're on macOS to determine if # we can download and install python for the user as needed. kernel="$(uname -s)" # Check to see if we need to force based on # macOS version. 10.15 has a dummy python3 version # that can trip up some py3 detection in other scripts. # set_use_py3_if "3" "10.15" "FORCE" downloaded="FALSE" # Check for the aforementioned /usr/bin/python3 stub if # our OS version is 10.15 or greater. check_py3_stub="$(compare_to_version "3" "10.15")" trap cleanup EXIT if [ "$1" == "--install-python" ] && [ "$kernel" == "Darwin" ]; then just_installing="TRUE" download_py else main fi