#!/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