#!/usr/bin/env bash :<<'__comments__' Name: setup-python-virtualenv Usage: See show_usage() Synopsis: Search for installed python pip versions and accept user's selection; invoke the selected pip version to install python virtualenv in $HOME/.local/; pip also caches installed files in ~/.cache/pip Notes: See Python Packaging Authority Author: G. D. LaBossiere, Xview Solutions Inc. Release: 1 Date: 2024-04-23 RCS: $Id$ License: GNU GPL v3 __comments__ #------------------------------------------------------------------------------ # bash environment #------------------------------------------------------------------------------ # BASH_VERSINFO[@] # HOME # EXEC_NAME # EXEC_PID # FUNCNAME[@] # PATH #------------------------------------------------------------------------------ # constants #------------------------------------------------------------------------------ # this script requires these utils to work TOOL_LIST=(id ls realpath sort uniq) #------------------------------------------------------------------------------ # error codes #------------------------------------------------------------------------------ ERROR_10="Requires bash 4.2+" ERROR_15="Missing required program(s)" ERROR_16="Missing required function(s)" ERROR_250="Invalid access method" #------------------------------------------------------------------------------ # variables #------------------------------------------------------------------------------ errFuncName="" exitMessage=() pipChoice="" pipChoices=() #------------------------------------------------------------------------------ :<<'__function_calls__' main─┐ ├─exit_handler ✓ ├─check_tools ✓ ├─search_for_pip ✓ ├─get_user_pip_choice ✓ ├─config_virtualenv └─show_usage __function_calls__ #------------------------------------------------------------------------------ # functions #------------------------------------------------------------------------------ <<'__comments__' Name: check_tools Synopsis: For each program name in TOOL_LIST[@] not in the USER's PATH or not installed on this host add program name to missing[@]; return 0 if missing[@] is empty else set exitMessage[@] and exit 15. Globals: FUNCNAME[@], TOOL_LIST[@], errFuncName, exitMessage[@] Returns: 0 on success; errFuncName, exitMessage[@] and exit 15 on error. __comments__ check_tools() { errFuncName="$FUNCNAME" local i missing msg for ((i=0;i<${#TOOL_LIST[@]};i++)); do type -p ${TOOL_LIST[$i]} &>/dev/null || missing+=("${TOOL_LIST[$i]}") done [[ ${missing[@]} ]] || return 0 msg="The following required utilities were not found: ${missing[*]}" exitMessage+=("${msg}") exit 15 } #------------------------------------------------------------------------------ :<<'__comments__' Name: config_virtualenv Synopsis: --- Notes: Credit to https://askubuntu.com/questions/3744 for how to set PATH Globals: FUNCNAME[@], HOME, PATH, errFuncName, pipChoice Requires: global $pipChoice Returns: 0 on success; __comments__ config_virtualenv() { [[ $pipChoice ]] || return 0 errFuncName="$FUNCNAME" local binpath choice cmd color msg1 msg2 msg3 out rc binpath="${HOME}/.local/bin" msg1="The script virtualenv will be installed in ${binpath}" rc="${HOME}/.bash_profile" [[ $PATH =~ $binpath ]] && msg2="which is on your PATH." || { msg2="which is not on your PATH." while true; do printf '\ec\n\n%4s%s\n\n' "" "${EXEC_NAME} ${0##*/}" printf '%4s%s\n%4s%s\n\n%4s%s\n' "" "${msg1} ${msg2}" \ "" "Do you want to add ${binpath} to your PATH?" \ "" "Choose Y for yes, N for no or Q to quit." printf '\n%4s%s' "" "Choice: " read choice [[ $choice =~ ^n|N$ ]] && break 1 [[ $choice =~ ^q|Q$ ]] && exit 0 [[ $choice =~ ^y|Y$ ]] && { msg2="which this script failed to add to your PATH." [[ -w $rc ]] && { [[ $(< ${rc}) =~ ${binpath} ]] || { printf '\n%s\n%s\n' \ "# The script ${0##*/} added ${binpath} to PATH" \ "export PATH=\"${PATH}:${binpath}\"" >> ${rc} [[ $(< ${rc}) =~ ${binpath} ]] && \ msg2="which was added to your PATH." declare -gx PATH="${PATH}:${binpath}" } } [[ $PATH =~ $binpath ]] || declare -gx PATH="${PATH}:${binpath}" msg3="To update your PATH for this login session you must source ${rc}" break 1 } done } while true; do printf '\ec\n\n%4s%s\n\n' "" "${EXEC_NAME} ${0##*/}" printf '%4s%s\n' "" "${msg1} ${msg2}" [[ $msg3 ]] && printf '%4s%s\n' "" "${msg3}" printf '%4s%s\n\n%4s%s\n' \ "" "Do you want to continue and install virtualenv?" \ "" "Choose Y for yes or Q to quit." printf '\n%4s%s' "" "Choice: " read choice [[ $choice =~ ^q|Q$ ]] && exit 0 [[ $choice =~ ^y|Y$ ]] && break 1 done printf '\n%4s%s\n' "" "Please wait... " # install virtualenv in ~/.local/ cmd="${pipChoice} install --user virtualenv" readarray -t out <<< "$( ${cmd} )" printf '\ec\n\n%4s%s\n\n%4s%s\n%4s%s\n' \ "" "${EXEC_NAME} ${0##*/}" \ "" "Executed the command '${cmd}'" \ "" "which generated the following output..." printf '\n%4s' "" printf '%0.s-' {0..76} printf '\n\n' for ((i=0;i<${#out[@]};i++)); do printf '%4s%b\n' "" "${out[$i]}" done printf '\n%4s' "" printf '%0.s-' {0..76} printf '\n\n' printf '%4s%s\n%4s%s\n\n' \ "" "For information on how to create and manage customized and isolated" \ "" "Python environments see https://virtualenv.pypa.io" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: exit_handler Synopsis: If $? is non-zero print error message(s); the first statement to appear in main() must be: trap exit_handler EXIT Globals: errFuncName, exitMessage[@] Returns: 0 on normal exit; $? on error. __comments__ exit_handler() { local errNum="$?" [[ $errNum -eq 0 ]] && return 0 local errMsg errTxt errTxt="ERROR_${errNum}" errMsg="Error ${errNum}: No description available" [[ ${!errTxt} ]] && errMsg="Error ${errNum}: ${!errTxt}" [[ ${errFuncName} ]] && errMsg="${errFuncName}: ${errMsg}" errMsg="${0##*/}: ${errMsg}" [[ ${exitMessage[@]} ]] && printf '%s\n' "${exitMessage[@]}" printf '%s\n' "${errMsg}" return $errNum } #------------------------------------------------------------------------------ :<<'__comments__' Name: get_user_pip_choice Synopsis: Check for installed virtualenv; display elements of pipChoices[@]; accept user input; set pipChoice. Globals: EXEC_NAME, FUNCNAME[@], HOME, errFuncName, pipChoice, pipChoices[@] Returns: exit 0 on user quit; 0 in all other cases. __comments__ get_user_pip_choice() { [[ ${#pipChoices[@]} -eq 0 ]] && return 0 errFuncName="$FUNCNAME" local choice count dir i j msg opt shebang venv ver dir="${HOME}/.local" venv="${dir}/bin/virtualenv" # extract the first line of the file readarray -t shebang <${venv} [[ -x $venv && $shebang =~ 'python' ]] && { ver="${shebang##*\/}" ver="${ver//[^0-9\.]/}" msg[0]="Found an existing Python ${ver} virtualenv script at ${dir}/bin/" # remove pip version choices less than pip${ver} if virtualenv already exists count="${#pipChoices[@]}" for ((i=0;i<${count};i++)); do opt="${pipChoices[$i]##*\/}" opt="${ver//[^0-9\.]/}" [[ ${opt%\.*} -gt ${ver%\.*} ]] && continue 1 [[ ${opt%\.*} -lt ${ver%\.*} ]] && unset pipChoices[$i] [[ ${opt#*\.} -le ${ver#*\.} ]] && unset pipChoices[$i] done [[ ${#pipChoices[@]} -ge 1 ]] && \ msg[1]="To update virtualenv choose a version of pip..." || \ msg[1]="This already works with the highest version of pip installed." } || { msg[0]="This script will install Python virtualenv in ${dir}/" msg[1]="Choose the version of pip you want to use to install virtualenv..." } while true; do printf '\ec\n\n%4s%s\n\n' "" "${EXEC_NAME} ${0##*/}" for ((j=0;j<${#msg[@]};j++)); do printf '%4s%s\n' "" "${msg[$j]}" done for ((i=0;i<${#pipChoices[@]};i++)); do printf '\n%8s%s%2s%s' "" "$((i+1))" "" "${pipChoices[$i]}" done [[ ${#pipChoices[@]} -ne 0 ]] && printf '\n' printf '\n%4s%s\n' "" "Choose H for help or Q to quit." printf '\n%4s%s' "" "Choice: " read choice [[ $choice =~ ^h|H$ ]] && break 1 [[ $choice =~ ^q|Q$ ]] && exit 0 [[ $choice =~ ^[1-9]$ && ${pipChoices[$((choice-1))]} ]] && \ pipChoice="${pipChoices[$((choice-1))]}" && break 1 done return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: search_for_pip Synopsis: Search for python pip scripts installed at /bin, /usr/bin and /usr/local/bin; if found populate pipChoices[@]. Globals: FUNCNAME[@], errFuncName, exitMessage[@], pipChoices[@] Requires: ls, realpath, sort, uniq Returns: 0 on success; exit 0 if no pip script is found; errFuncName, exitMessage[@] and exit 15 on error. __comments__ search_for_pip() { errFuncName="$FUNCNAME" type -p ls realpath sort uniq &>/dev/null || exit 15 local count i list ls /usr/bin/pip* &>/dev/null && \ readarray -t list <<< "$(realpath /usr/bin/pip*)" # if /bin and /usr/bin are not symlinked [[ $(realpath /bin) != $(realpath /usr/bin) ]] && { ls /bin/pip* &>/dev/null && \ readarray -O "${#list[@]}" -t list <<< "$(realpath /bin/pip*)" } ls /usr/local/bin/pip* &>/dev/null && \ readarray -O "${#list[@]}" -t list <<< "$(realpath /usr/local/bin/pip*)" # get and list the installed pip binaries found by version count="${#list[@]}" for ((i=0;i<${count};i++)); do [[ -x ${list[$i]} && ${list[$i]##*\/} =~ ^pip[1-9]\.[1-9]$ ]] && \ list[$i]="${list[$i]##*\ }" || unset list[$i] done readarray -t pipChoices <<< "$(printf '%s\n' "${list[@]}" |sort|uniq)" [[ ${#pipChoices[@]} -eq 0 ]] && printf '\n%4s%s\n\n' \ "" "Python pip is not installed on this host. Cannot continue." return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: show_usage Synopsis: If pipChoice is not set display script usage Globals: FUNCNAME[@], errFuncName, pipChoice Returns: 0 in all cases. __comments__ show_usage() { [[ $pipChoice ]] && return 0 errFuncName="$FUNCNAME" printf '%b' " --------------------------------------------------------------------------- Usage: ${EXEC_NAME} ${0##*/} This script displays the installed versions of Python pip and invokes your selected version to install the virtualenv utility which lets you create customized, isolated Python environments. For more information about virtualenv see https://virtualenv.pypa.io --------------------------------------------------------------------------- " return 0 } #------------------------------------------------------------------------------ # main #------------------------------------------------------------------------------ main() { trap exit_handler EXIT errFuncName="$FUNCNAME" [[ ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -ge 2 ]] || \ [[ ${BASH_VERSINFO[0]} -gt 4 ]] || exit 10 [[ $EXEC_NAME && $EXEC_PID -eq $$ ]] || \ exitMessage+=("${0##*/} may not be executed directly.") [[ -t 1 ]] || exitMessage+=("Script must run interactively (in a terminal).") [[ $(id -u) -eq 0 ]] && \ exitMessage+=("You must run this script as a regular (non-root) user") [[ ${exitMessage[@]} ]] && exit 250 declare -F check_tools config_virtualenv exit_handler get_user_pip_choice \ search_for_pip show_usage &>/dev/null || exit 16 check_tools search_for_pip get_user_pip_choice config_virtualenv show_usage exit 0 } #------------------------------------------------------------------------------ # run program #------------------------------------------------------------------------------ main