#!/usr/bin/env bash :<<'__comments__' Name: remove-python Usage: see show_usage() Synopsis: If python version $1 has been compiled from source and installed at $BASE_PATH by the install-python libexec script then remove the installation; the value of $BASE_PATH in this script and the install-python libexec script must be the same (e.g. /usr/local). Notes: Credit to https://unix.stackexchange.com/questions/190794/ Author: G. D. LaBossiere, Xview Solutions Inc. Release: 1 Date: 2024-03-27 RCS: $Id$ License: GNU GPL v3 __comments__ #------------------------------------------------------------------------------ # bash environment #------------------------------------------------------------------------------ # BASH_VERSINFO[@] # EXEC_NAME # EXEC_PID # FUNCNAME[@] #------------------------------------------------------------------------------ # constants #------------------------------------------------------------------------------ # local installations are rooted here; must be same value as install-python BASE_PATH="/usr/local" # path to local installation binaries; must be same value as install-python BIN_PATH="${BASE_PATH}/bin" # path to local installation libraries; must be same value as install-python LIB_PATH="${BASE_PATH}/lib" # path to local installation sources; must be same value as install-python SRC_PATH="${BASE_PATH}/src" # this script requires these utils to work TOOL_LIST=(id ls rm sudo) #------------------------------------------------------------------------------ # error codes #------------------------------------------------------------------------------ ERROR_10="Requires bash 4.2+" ERROR_15="Missing required program(s)" ERROR_16="Missing required function(s)" ERROR_18="Bad or missing required parameter(s)" ERROR_20="Insufficient user privilege" ERROR_250="Invalid access method" #------------------------------------------------------------------------------ # variables #------------------------------------------------------------------------------ argList=() errFuncName="" exitMessage=() versionChoice="" #------------------------------------------------------------------------------ :<<'__function_calls__' main─┐ ├─exit_handler ├─check_tools ├─check_user_privilege ├─get_user_input ├─remove_version_choice └─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[@], pkgMgr 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: check_user_privilege Synopsis: If EUID is non-zero verify that user is a nopasswd sudoer. Globals: EXEC_NAME, FUNCNAME[@], errFuncName, exitMessage[@] Requires: id, sudo Returns: 0 on success; errFuncName and exit 15 on error; errFuncName, exitMessage[@] and exit 20 if user is not root or a nopasswd sudoer. __comments__ check_user_privilege() { errFuncName="$FUNCNAME" type -p id sudo &>/dev/null || exit 15 [[ $(id -u) -eq 0 ]] && return 0 [[ $(sudo -l -n -U $(id -un)) =~ ALL.*NOPASSWD.*ALL$ ]] && return 0 local msg msg="You must be root or a nopasswd sudoer to run ${EXEC_NAME} ${0##*/}" exitMessage+=("${msg}") exit 20 } #------------------------------------------------------------------------------ :<<'__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_input Synopsis: If argList[0] is: 1) missing return 0; 2) is a major version number check for an installed binary, set versionChoice if found and return 0 else print a message and exit 0; 3) an invalid version number append a message to exitMessage[@] and exit 18; 4) any other value return 0. Globals: BIN_PATH, FUNCNAME[@], argList[@], errFuncName, exitMessage[@], versionChoice Returns: 0 on success; errFuncName, exitMessage[@] and exit 18 on error. __comments__ get_user_input() { [[ ${argList[@]} ]] || return 0 errFuncName="$FUNCNAME" [[ ${argList[0]} =~ ^[2-9]{1}\.[0-9]{1,2}$ ]] && { [[ $(type -p "python${argList[0]}") =~ "$BIN_PATH" ]] && \ versionChoice="${argList[0]}" && return 0 printf '%s\n' "No ${BIN_PATH}/python${argList[0]} was found." && exit 0 } [[ ${argList[0]} =~ ^[0-9]{1,}.*$ ]] && { exitMessage+=("${argList[0]} is not a valid python major version number.") exit 18 } return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: remove_version_choice Synopsis: If versionChoice is set find the files and directories in BASE_PATH installed by the install-python libexec script; user must confirm batch deletion for both installed and source files. Globals: BASE_PATH, BIN_PATH, FUNCNAME[@], LIB_PATH, SRC_PATH, errFuncName, versionChoice Requires: id, ls, rm, sudo Returns: 0 on success; errFuncName and exit 15 on error. __comments__ remove_version_choice() { [[ $versionChoice ]] || return 0 errFuncName="$FUNCNAME" type -p id ls rm sudo &>/dev/null || exit 15 local admin answer item items list # this array excludes the path to the python sources and build directory items=("${BIN_PATH}/2to3-${versionChoice}" "${BIN_PATH}/idle${versionChoice}" "${BIN_PATH}/pip${versionChoice}" "${BIN_PATH}/pydoc${versionChoice}" "${BIN_PATH}/python${versionChoice}*" "${BIN_PATH}/pyvenv-${versionChoice}" "${BASE_PATH}/include/python${versionChoice}*" "${LIB_PATH}/libpython${versionChoice}*" "${LIB_PATH}/pkgconfig/python-${versionChoice}*" "${LIB_PATH}/python${versionChoice}" "${BASE_PATH}/share/man/man1/python${versionChoice}.1") list="$(ls -1d ${items[@]} 2>/dev/null)" printf '%s %s\n\n' "Found the following python ${versionChoice} files and" \ "directories installed:" for item in "${list}"; do ls -1d ${item} done printf '\n%s %s\n%s %s ' "Do you want to completely remove them?" \ "This action cannot be reversed." "Enter 'Yes' or 'YES' to proceed," \ "anything else to quit:" read answer printf '\n' [[ $answer =~ ^Y[e|E][s|S]$ ]] || { printf '%s\n' "All done! No files or directories were removed." return 0 } [[ $(id -u) -eq 0 ]] || admin="sudo" for item in ${list}; do ${admin} rm -fr ${item} 2>/dev/null && printf '%s\n' "Deleted ${item}" || \ printf '%s\n%s\n' "Failed to delete ${item}" \ "You must delete the item manually." done # this array holds the path to the python sources and build directory items=("${SRC_PATH}/[Pp]ython-${versionChoice}*") list="$(ls -1d ${items[@]} 2>/dev/null)" [[ $list ]] && { printf '\n%s %s\n\n' "Found the following python ${versionChoice} source" \ "files and directories:" for item in ${list}; do ls -1d ${item} done printf '\n%s %s\n%s %s ' "Do you want to completely remove them?" \ "This action cannot be reversed." "Enter 'Yes' or 'YES' to proceed," \ "anything else to quit:" read answer printf '\n' [[ $answer =~ ^Y[e|E][s|S]$ ]] && { for item in ${list}; do ${admin} rm -fr ${item} 2>/dev/null && printf '%s\n' \ "Deleted ${item}" || printf '%s\n%s\n' "Failed to delete ${item}" \ "You must delete the item manually." done } || printf '%s\n' "No source files or directories were removed." } || printf '\n%s' "No source files or directories were found at ${SRC_PATH}" printf '\n%s\n' "All done!" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: show_usage Synopsis: If versionChoice is not set display script usage. Globals: BASE_PATH, EXEC_NAME, FUNCNAME[@], errFuncName, versionChoice Returns: 0 in all cases. __comments__ show_usage() { [[ $versionChoice ]] && return 0 errFuncName="$FUNCNAME" printf '%b' " ------------------------------------------------------------------------------- Usage: ${EXEC_NAME} ${0##*/} VERSION VERSION must be a valid Python major version number (e.g. 3.8) else this message will be displayed. This script will search ${BASE_PATH} for python VERSION files installed and optionally remove them. ------------------------------------------------------------------------------- " return 0 } #------------------------------------------------------------------------------ # main #------------------------------------------------------------------------------ main() { trap exit_handler EXIT argList=("$@") 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).") [[ ${exitMessage[@]} ]] && exit 250 declare -F check_tools check_user_privilege exit_handler get_user_input \ remove_version_choice show_usage &>/dev/null || exit 16 check_tools check_user_privilege get_user_input remove_version_choice show_usage exit 0 } #------------------------------------------------------------------------------ # run program #------------------------------------------------------------------------------ main "$@"