From 5499123bfc56ecd61a9b820af592532ce63b26f1 Mon Sep 17 00:00:00 2001 From: TekWizely Date: Tue, 9 Nov 2021 12:36:50 -0800 Subject: [PATCH] feat: Adds redhat version (#3) This version adds support for RedHat's custom update-alternatives version. * Adds redhat version + docs * Renames my-alternatives -> my-alternatives-debian * Adds SPDX header to scripts * Adds SUSE info --- README.md | 120 +- my-alternatives => my-alternatives-debian | 444 +++--- my-alternatives-redhat | 1542 +++++++++++++++++++++ 3 files changed, 1892 insertions(+), 214 deletions(-) rename my-alternatives => my-alternatives-debian (87%) create mode 100755 my-alternatives-redhat diff --git a/README.md b/README.md index e769a3a..c0f7577 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,14 @@ # My-Alternatives
(hacking update-alternatives to make local changes) [![MIT license](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/tekwizely/pre-commit-golang/blob/master/LICENSE) -My-Alternatives is a light-weight wrapper over _update-alternatives_, using it to make changes to user-level configurations. +My-Alternatives is a light-weight wrapper over _update-alternatives_, offering user-level customizations. + +Supports _Debian_, _SUSE_*, and _RedHat_ + +* for suse support, use the debian version + +----------------------- +### Easy to Get Started With my-alternatives, configuring custom alternatives is as easy as: @@ -17,7 +24,7 @@ _home configuration example_ Your selections will be saved into your `HOME` configuration, and made active in any login shell that performs the initialization routine. -*NOTE:* You can save the initialization routine in your `.profile` +**NOTE:** You can save the initialization routine in your `.profile` ### Per-Shell Configuration @@ -41,7 +48,6 @@ When the `HOME` configuration is the active configuration, my-alternatives impor When a `TEMP` configuration is active, my-alternatives first tries to import from your `HOME` configuration, falling back onto the system-level configuration if the group is not present. - ### Manual Import If you want to import an alternative group into your active configuration without selecting an alternative, you can use the `import` command: @@ -50,26 +56,36 @@ _import example_ ```shell $ my-alternatives import ``` +----------- +## Commands + +Below are tables of the available commands for each supported OS + +### Debian / SUSE -### My-Alternatives Commands +**NOTE:** _SUSE_ uses a [rebranded](https://build.opensuse.org/package/view_file/openSUSE:Leap:15.2/update-alternatives/update-alternatives-suse.patch?expand=1) version of _Debian's_ udpate-alternatives. -Below is the list of custom commands that my-alternatives implements: +Until such time as the feature set of the two versions diverges, this project will **just** maintain the _Debian_ vesion. + +#### My-Alternatives-Debian Commands + +Below is the list of custom commands that _my-alternatives-debian_ implements: | Command | Description |--------------------|------------ | `init`, `shellenv` | Prepare the current shell session for user-level alternatives. | `init-tmp`, `tmp` | Configure the current shell session for temporary (short-lived) changes. -| `select`, `config` | Select the active alternative for group . This is equivalent to `update-alternatives --config` with the adition of the auto-import logic. -| `import` | Import an alternative group into the current configuration. -| `add` | Add an alternative to the group within the current configuration. This is equivalent to `update-alternatives --install` but has _slightly_ different syntax. see `my-alternatives help add` for details. | `rm-tmp` | Remove the temporary configuration from the current shell session, making the `HOME` configuration active. +| `select`, `config` | Select the active alternative for a group. This is equivalent to `update-alternatives --config` with the adition of the auto-import logic. +| `import` | Import an alternative group into the current configuration. +| `add` | Add an alternative to a group within the current configuration. This is equivalent to `update-alternatives --install` but has _slightly_ different syntax. see `my-alternatives help add` for details. | `version` | Display my-alternatives version number. -*NOTE:* See `my-alternatives help ` to learn about a specific command, including additional options. +**NOTE:** See `my-alternatives help ` to learn about a specific command, including additional options. -### Update-Alternatives Commands +#### Debian Update-Alternatives Commands -Below is the list of commands that are implemented as pass-through to the related update-alternatives command: +Below is the list of commands that are implemented as pass-through to the related _Debian_ update-alternatives command: | My-Alternatives Command | Update-Alternatives Command |-------------------------|---------------------------- @@ -91,7 +107,63 @@ My-Alternatives will set the `--admindir` and `--altdir` options to point to you All additional command-line options are passed-through, unmodified. -*NOTE:* See `update-alternatives --help` or `man update-alternatives` to learn more about the various commands and their options. +**NOTE:** See `update-alternatives --help` or `man update-alternatives` to learn more about the various commands and their options. + +### RedHat + +#### hacking The system + +_RedHat_ has its own implementation of update-alternatives, which is slightly different from the _Debian_ version. + +One **major** difference is that it does NOT support the `--query` option, meaning that there's no means of determining an anternative's full configuraiton using _just_ the tool's public API. + +In order to support this version, we have to use knowledge of the system's _private_ API. Namely: + + * Defaulting the "admin" directory to `/var/lib/alternatives` + * Assuming the name and format of files in the admin directory + * Defaulting the "alt" directory to `/etc/alternatives` + * Assuming the name and nature of files in the alt directory + +#### My-Alternatives-RedHat Commands + +Below is the list of custom commands that _my-alternatives-redhat_ implements: + +| Command | Description +|--------------------|------------ +| `init`, `shellenv` | Prepare the current shell session for user-level alternatives. +| `init-tmp`, `tmp` | Configure the current shell session for temporary (short-lived) changes. +| `rm-tmp` | Remove the temporary configuration from the current shell session, making the `HOME` configuration active. +| `select`, `config` | Select the active alternative for a group. This is equivalent to `update-alternatives --config` with the adition of the auto-import logic. +| `import` | Import an alternative group into the current configuration. +| `add` | Add an alternative to a group within the current configuration. This is equivalent to `update-alternatives --install` but has _slightly_ different syntax. see `my-alternatives help add` for details. +| `add-child` | Add an child to an existing alternative for a group within the current configuration. This is equivalent to `update-alternatives --add-slave` but has _slightly_ different syntax. see `my-alternatives help add-child` for details. +| `version` | Display my-alternatives version number. + +**NOTE:** See `my-alternatives help ` to learn about a specific command, including additional options. + +#### RedHat Update-Alternatives Commands + +Below is the list of commands that are implemented as pass-through to the related _RedHat_ update-alternatives command: + +| My-Alternatives Command | Update-Alternatives Command +|-------------------------|---------------------------- +| `select`, `config` | `--config` +| `add-child` | `--add-slave` +| `rm-alt` | `--remove` +| `rm-group`, `rm-grp` | `--remove-all` +| `rm-child` | `--remove-slave` +| `display` | `--display` +| `list` | `--list` +| `set` | `--set` +| `auto` | `--auto` +| `ua-help` | `--help` +| `ua-version` | `--version` + +My-Alternatives will set the `--admindir` and `--altdir` options to point to your active configuration. + +All additional command-line options are passed-through, unmodified. + +**NOTE:** See `update-alternatives --help` or `man update-alternatives` to learn more about the various commands and their options. ### Invoking Update-Alternatives Directly @@ -106,7 +178,7 @@ My-Alternatives will set the `--admindir` and `--altdir` options to point to you All additional command-line options are passed-through, unmodified. -*NOTE:* See `update-alternatives --help` or `man update-alternatives` to learn more about available commands and their options. +**NOTE:** See `update-alternatives --help` or `man update-alternatives` to learn more about available commands and their options. ------------- ## Installing @@ -121,6 +193,19 @@ See the [Releases](https://github.com/TekWizely/my-alternatives/releases) page f git clone git://github.com/TekWizely/my-alternatives.git ``` +### Renaming the Scripts + +Depending on how you acquire the files, the scripts may be named by their OS flavor, i.e: + * Debian : `my-alternatives-debian` + * RedHat : `my-alternatives-redhat` + +Feel free to rename them as desired. Personally, I rename the script **AND** set up a few convenient aliases: +```shell +$ cp my-alternatives-debian ~/bin/my-alternatives +$ alias ma="my-alternatives" +$ alias mua="my-alternatives ua" +``` + --------------- ## Contributing @@ -147,6 +232,8 @@ The `tekwizely/my-alternatives` project is released under the [MIT](https://open ---------------- ##### Misc Notes +As of `v0.7.0`, my-alternatives has been split (and renamed) into two scripts: `my-alternatives-debian` and `my-alternatives-redhat`. + As of `v0.6.0`, my-alternatives is a complete re-write. As much as possible, commands are implemented as pass-through to _update-alternatives_, pointing to your active configuration. My-Alternatives does not require _root / sudo_ privileges to use, as it creates and maintains user-owned configuration directories. @@ -155,7 +242,8 @@ My-Alternatives does not require _root / sudo_ privileges to use, as it creates - Written in _Bash_ - Requires the following system tools: - `update-alternatives` - - `mktemp` + - `readlink` (redhat version) + - `mktemp` (debian version) - `umask` - `dirname` - `basename` @@ -171,3 +259,7 @@ My-Alternatives does not require _root / sudo_ privileges to use, as it creates - `openSUSE Leap 15.3` - `SUSE update-alternatives version 1.19.0.4.` - `Bash 4.4.23(1)-release` + - CentOS + - `CentOS Linux release 8.4.2105` + - `alternatives version 1.13` + - `GNU bash, version 4.4.19(1)-release` diff --git a/my-alternatives b/my-alternatives-debian similarity index 87% rename from my-alternatives rename to my-alternatives-debian index 242975f..ff0157c 100755 --- a/my-alternatives +++ b/my-alternatives-debian @@ -1,19 +1,27 @@ #!/usr/bin/env bash +####################################################################### +# SPDX-License-Identifier: MIT +# Copyright (c) 2021 TekWizely & co-authors +# +# Use of this source code is governed by the MIT license. +# See the accompanying LICENSE file, if present, or visit: +# https://opensource.org/licenses/MIT +####################################################################### +VERSION="v0.7.0-debian" + set -Eeuo pipefail umask 077 # We use tmp files. Try to protect the user -VERSION="v0.6.0" - function usage() { # Need to escape $ and \ cat <<-USAGE my-alternatives (${VERSION}) - + a wrapper for update-alternatives to enable user-level configuration. also supports temporary (per-shell) changes. - + usage: - + eval "\$( my-alternatives [--shell ] [alt_home] )" prepare the current shell session for user-level alternatives NOTE: place this in your .profile @@ -25,9 +33,9 @@ function usage() { invoke a command my-alternatives help learn more about a specific command - + custom commands: - + import import the alternative group into the current configuration when the default configuration is active, imports from system-level alternatives @@ -40,33 +48,33 @@ function usage() { will first invoke 'import ' if the group is not already available in the current configuration version display my-alternatives version number - + renamed pass-through commands: - + select-all | config-all invoke 'update-alternatives --all' rm-alt | rmalt invoke 'update-alternatives --remove' rm-group | rmgrp invoke 'update-alternatives --remove-all' - + these have been renamed to reduce ambiguity - + same-name pass-through commands: - - query | display | list | set | auto | get-selections | set-selections - + + query | display | list | set | auto | get-selections | set-selections + invoke 'update-alternatives --' - + update-alternatives helper commands: - + ua [option ...] invoke 'update-alternatives [option ...]' pointing to the currently-configured alternatives ua-help invoke 'update-alternatives --help' ua-version invoke 'update-alternatives --version' - + USAGE exit 2 } @@ -238,42 +246,42 @@ function main() { function help_init() { cat <<-'USAGE' usage: eval "$( my-alternatives [--shell ] [alt_home] )" - + prepares the current shell session for user-level alternatives. - + if [alt_home] is provided, it will be configured as MY_ALTS_HOME - + when [alt_home] is NOT provided, the following logic will be used determine MY_ALTS_HOME: - + * check if $MY_ALTS_HOME already defined * check $XDG_CONFIG_HOME. if present, use $XDG_CONFIG_HOME/my-alternatives * finally default to ~/.config/my-alternatives - + when [--shell ] is NOT provided, checks $SHELL. - + NOTE: Currently only 'bash' is supported. - + creates the MY_ALTS_HOME directory and required sub-directories if they don't already exist. - + finally, configures PATH and MANPATH. - + example: - + # prepare shell with default MY_ALTS_HOME logic # NOTE: you can place this in your .profile $ eval "$( command my-alternatives init )" - + # = ~/.config/my-alternatives export MY_ALTS_HOME="" export PATH="/bin${PATH:+:$PATH}" export MANPATH="/man:${MANPATH:-}" - + more info: - + see 'my-alternatives --help' to learn more about configuring your own alternatives. - + see 'update-alternatives --help' or 'man update-alternatives' to learn more about alternatives. - + USAGE exit 2 } @@ -320,14 +328,14 @@ function do_init() { exit 2 fi # - # set MY_ALTS_HOME from specified alt_home or default logic + # set from specified values or default logic # MY_ALTS_HOME="${alt_home:-${MY_ALTS_HOME:-${XDG_CONFIG_HOME:-${HOME:?}/.config}/my-alternatives}}" - # print assignment seperately as I don't want to escape ALL the other $ :) + # print assignments seperately as I don't want to escape ALL the other $ :) # printf '_MY_ALTS_HOME="%s"\n' "${MY_ALTS_HOME:?}" - # path clean up adapted from: https://stackoverflow.com/a/2108540/1822537 - # + # path clean up adapted from: https://stackoverflow.com/a/2108540/1822537 + # cat <<-'INIT' if [ -n "${_MY_ALTS_HOME}:-" ] && command mkdir -p -- "${_MY_ALTS_HOME}"; then command mkdir -p -- "${_MY_ALTS_HOME}/bin" @@ -351,9 +359,9 @@ function do_init() { my-alternatives() { case "${1:-}" in # some commands require eval - init | shellenv | \ - init-tmp | tmp | temp | init-temp | \ - rm-temp | rmtmp | rmtemp | remove-tmp | remove-temp | removetmp | removetemp) + init | shellenv | \ + init-tmp | init-temp | tmp | temp | \ + rm-tmp | rm-temp | rmtmp | rmtemp | remove-tmp | remove-temp | removetmp | removetemp) eval "$( command my-alternatives "$@" )" ;; *) @@ -361,7 +369,7 @@ function do_init() { ;; esac } - # finally, export home variable + # finally, export variables command export MY_ALTS_HOME="${_MY_ALTS_HOME}" fi command unset _MY_ALTS_HOME @@ -371,36 +379,36 @@ function do_init() { function help_init_tmp() { cat <<-'USAGE' usage: my-alternatives [tmp_dir] - + configures the current shell session for temporary (short-lived) changes. - + if [tmp_dir] is provided, it will be configured as MY_ALTS_TMP - + when [tmp_dir] is NOT provided, the following logic will be used determine MY_ALTS_TMP: - + * check if $MY_ALTS_TMP already defined * check XDG_CACHE_HOME. if present, use XDG_CACHE_HOME/my-alternatives/ * finally default to ~/.config/my-alternatives/ - + creates the MY_ALTS_TMP directory and required sub-directories if they don't already exist. - + finally, configures PATH and MANPATH. - + example: - + $ my-alternatives init-tmp - + # = ~/.config/my-alternatives/abcd123 export MY_ALTS_TMP="" export PATH="/bin${PATH:+:$PATH}" export MANPATH="/man:${MANPATH:-}" - + more info: - + see 'my-alternatives --help' to learn more about configuring your own alternatives. - + see 'update-alternatives --help' or 'man update-alternatives' to learn more about alternatives. - + USAGE exit 2 } @@ -468,23 +476,23 @@ function do_init_tmp() { function help_rm_tmp() { cat <<-'USAGE' usage: my-alternatives rm-tmp - + removes the temporary configuration from the current shell session. - + restores the curent session to the default user-level alternatives. - + removes the temporary directory referenced by MY_ALTS_TMP - + removes MY_ALTS_TMP from both PATH and MANPATH - + finall, unsets the MY_ALTS_TMP variable - + more info: - + see 'my-alternatives --help' to learn more about creating a temporary configuration. - + see 'update-alternatives --help' or 'man update-alternatives' to learn more about alternatives. - + USAGE exit 2 } @@ -496,9 +504,9 @@ function do_rm_tmp() { printf "my-alternatives: error: shell session not confgured. see 'my-alternatives --help' for usage\n" >&2 exit 2 fi - # No arguments expected + # No further arguments expected # - if [ -n "${1:-}" ]; then + if [[ $# -gt 0 ]]; then printf "my-alternatives: error: unrecognized option: %s. see 'my-alternatives help rm-tmp' for usage\n" "${1}" >&2 exit 2 fi @@ -527,22 +535,22 @@ function do_rm_tmp() { function help_import() { cat <<-'USAGE' usage: my-alternatives import - + imports an alternative group into the currently-configured alternatives. - + when the default alternatives (ie. MY_ALTS_HOME) are active: * tries to import from the system-level alternatives - + when temporary alternatives (i.e MY_ALTS_TMP) are active: * first tries to import from the default alternatives * finally tries to import from the system-level alternatives - + more info: - + see 'my-alternatives --help' to learn more about configuring your own alternatives. - + see 'update-alternatives --help' or 'man update-alternatives' to learn more about alternatives. - + USAGE exit 2 } @@ -576,48 +584,51 @@ function do_import() { printf "my-alternatives: error: no name provided. see 'my-alternatives help import' for usage\n" >&2 exit 1 fi + local from # for status message + local data_file # Create temp file # - local alt_tmp - if ! alt_tmp="$(command mktemp -q -t "my-alts-${name}-query.XXXXXXXX")"; then + if ! data_file="$(command mktemp -q -t "my-alts-${name}-query.XXXXXXXX")"; then printf "my-alternatives: error: unable to create temp file\n" >&2 exit 2 fi # sanity check - if [ ! -f "${alt_tmp}" ]; then + if [ ! -f "${data_file}" ]; then printf "my-alternatives: error: unable to create temp file\n" >&2 exit 2 fi - local from # for status message # If tmp is active try import from home # if [ "${alt_root}" = "${MY_ALTS_TMP:-}" ]; then - # does NOT exit on error - checks alt_tmp for success + # does NOT exit on error - checks data_file for success command update-alternatives \ --altdir "${MY_ALTS_HOME}/alts" --admindir "${MY_ALTS_HOME}/admin" \ --query "${name}" \ - >"${alt_tmp}" 2>/dev/null || true + >"${data_file}" 2>/dev/null || true - if [ -s "${alt_tmp}" ]; then + if [ -s "${data_file}" ]; then from="MY_ALTS_HOME: ${MY_ALTS_HOME}" # for status message fi fi - if [ ! -s "${alt_tmp}" ]; then + if [ -z "${from:-}" ]; then # final fallback = update-alternatives default # exits on error - command update-alternatives --query "${name}" >"${alt_tmp}" - from="system-level alternative" # for status message + command update-alternatives --query "${name}" >"${data_file}" + + if [ -s "${data_file}" ]; then + from="system-level alternative" # for status message + fi fi # final sanity check # - if [ -z "${alt_tmp}" ]; then + if [ -z "${from:-}" ]; then printf "my-alternatives: error: unable to locate alternative group with name %s\n" "${name}" >&2 - rm "${alt_tmp}" + rm "${data_file}" exit 2 fi - alt_parse_query "${alt_tmp}" || return $? - rm "${alt_tmp}" + alt_parse_query "${data_file}" || return $? + rm "${data_file}" if [ -z "${ALT_GROUP_LINK}" ]; then printf "my-alternatives: error: unable to determine provided location\n" >&2 @@ -686,49 +697,52 @@ function do_import() { function help_add() { cat <<-'USAGE' usage: my-alternatives add [--child ]... - + adds an alternative to a group within the currently-configured alternatives. The group is created if it does not already exist. - + NOTE: This is generally a pass-through to 'update-alternatives --install', BUT the argument syntax is slightly diffrent: - - * the order of and arguments are inverted + + * the order of and arguments are inverted * uses --child (vs --slave) for secondary files - + * arguments should be relative + ex: bin/pager + ex: man/man1/pager.1.gz + options: - + the name of the alternative group. used when referring to the group in other commands. the name must be unique within the currently-configured alternatives. ex: pager ex: pager.1.gz - + path provided by the group. path should be relative. ex: bin/pager ex: man/man1/pager.1.gz - + the path of the alternative being introduced. ex: /usr/bin/less ex: /usr/share/man/man1/less.1.gz - + priority of this alternative. alternatives with larger values have higher priority in automatic mode. - + --child adds a secondary entry. optional, but if given, all arguments are required. - + more info: - + see 'my-alternatives --help' to learn more about configuring your own alternatives. - + see 'update-alternatives --help' or 'man update-alternatives' to learn more about the '--install' command. - + USAGE exit 2 } @@ -793,56 +807,61 @@ function do_add() { fi args+=("${1}") shift - # secondary entries + # children # - while [ -n "${1:-}" ]; do - if [[ "${1}" != "--child" ]]; then - printf "my-alternatives: error: unrecognized option: %s. see 'my-alternatives help add' for usage\n" "${1}" >&2 - exit 2 - fi - args+=("--slave") - shift - # secondary group-name - # - if [ -z "${1:-}" ]; then - printf "my-alternatives: error: secondary group-name not provided. see 'my-alternatives help add' for usage\n" >&2 - exit 1 - fi - providing_name="${1}" - shift - # secondary group-path - # - if [ -z "${1:-}" ]; then - printf "my-alternatives: error: secondary group-path not provided. see 'my-alternatives help add' for usage\n" >&2 - exit 1 - fi - if [[ "${1}" =~ ^/(bin|man)/ ]]; then - providing_path="${alt_root}${1}" - elif [[ "${1}" =~ ^(bin|man)/ ]]; then - providing_path="${alt_root}/${1}" - else - local group_path - group_path="$(get_my_alt_rel_path "${1}")" - if [ "${group_path}" == "${1}" ]; then - printf "my-alternatives: error: unable to determine relative path for group-path: %s\n" "${group_path}" >&2 - exit 2 - fi - providing_path="${alt_root}${group_path}" - fi - shift + while (($#)); do + case "${1}" in + --child) + args+=("--slave") + shift + # secondary group-name + # + if [ -z "${1:-}" ]; then + printf "my-alternatives: error: secondary group-name not provided. see 'my-alternatives help add' for usage\n" >&2 + exit 1 + fi + providing_name="${1}" + shift + # secondary group-path + # + if [ -z "${1:-}" ]; then + printf "my-alternatives: error: secondary group-path not provided. see 'my-alternatives help add' for usage\n" >&2 + exit 1 + fi + if [[ "${1}" =~ ^/(bin|man)/ ]]; then + providing_path="${alt_root}${1}" + elif [[ "${1}" =~ ^(bin|man)/ ]]; then + providing_path="${alt_root}/${1}" + else + local group_path + group_path="$(get_my_alt_rel_path "${1}")" + if [ "${group_path}" == "${1}" ]; then + printf "my-alternatives: error: unable to determine relative path for group-path: %s\n" "${group_path}" >&2 + exit 2 + fi + providing_path="${alt_root}${group_path}" + fi + shift - # add to args (inverted) - args+=("${providing_path}" "${providing_name}") + # add to args (inverted) + args+=("${providing_path}" "${providing_name}") - # secondary alt-path - # - if [ -z "${1:-}" ]; then - printf "my-alternatives: error: secondary alt-path not provided. see 'my-alternatives help add' for usage\n" >&2 - exit 1 - fi - args+=("${1}") - shift + # secondary alt-path + # + if [ -z "${1:-}" ]; then + printf "my-alternatives: error: secondary alt-path not provided. see 'my-alternatives help add' for usage\n" >&2 + exit 1 + fi + args+=("${1}") + shift + ;; + *) + printf "my-alternatives: error: unrecognized option: %s. see 'my-alternatives help add' for usage\n" "${1}" >&2 + exit 2 + ;; + esac done + command update-alternatives \ --altdir "${alt_root}/alts" --admindir "${alt_root}/admin" \ "${args[@]}" \ @@ -852,17 +871,17 @@ function do_add() { function help_select() { cat <<-'USAGE' usage: my-alternatives + + shows alternatives for the group and asks the user to select which one to use. + + calls 'import ' if the group is not present in the currently-configured alternatives. + + more info: + + see 'my-alternatives --help' to learn more about the 'import' command. + + see 'update-alternatives --help' or 'man update-alternatives' to learn more about the '--config' command. + + USAGE + exit 2 +} + +function do_select() { + # Ensure alt_home configured + # + if [ -z "${MY_ALTS_HOME:-}" ]; then + printf "my-alternatives: error: shell session not confgured. see 'my-alternatives --help' for usage\n" >&2 + exit 2 + fi + local alt_root="${MY_ALTS_TMP:-${MY_ALTS_HOME}}" + # No name argument + # + if [[ -z "${1:-}" ]]; then + printf "my-alternatives: error: no name provided. see 'my-alternatives help select' for usage\n" >&2 + exit 1 + fi + local name="${1}" + shift + # No further arguments expected + # + if [[ $# -gt 0 ]]; then + printf "my-alternatives: error: unrecognized option: %s. see 'my-alternatives help select' for usage\n" "${1}" >&2 + exit 2 + fi + # need to import? + # + if ! alt_group_exists "${name}"; then + do_import "${name}" || return $? + fi + command update-alternatives \ + --altdir "${alt_root}/alts" --admindir "${alt_root}/admin" \ + "--config" "${name}" +} + +## +# help_alias +# Generates help text for the various command aliases +# +# $1 = command +# $2 = alias +# +function help_alias() { + # Need to escape $ and \ + cat <<-USAGE + ${2} is an alias for the '${1}' command. + + see 'my-alternatives help ${1}' for usage. + + USAGE + exit 2 +} + +## +# help_passthrough +# Generates help text for the various pass-through commands +# +# $1 = update-alternatives command +# $2 = my-alternatives command (optional) +# +function help_passthrough() { + # Need to escape $ and \ + cat <<-USAGE + usage: my-alternatives ${2:-${1}} [option ...] + + invokes update-alternatives --${1} pointing to the currently-configured alternatives. + + all options are passed through, unmodified. + + this is equivilent to: + + # ="\${MY_ALTS_TMP:-\${MY_ALTS_HOME}}" + update-alternatives --altdir "/alts" --admindir "/admin" --${1} [option ...] + + more info: + + see 'update-alternatives --help' or 'man update-alternatives' to learn more about the '--${1}' command. + + USAGE + exit 2 +} + +function help_update_alternatives_help() { + cat <<-'USAGE' + usage: my-alternatives ua-help + + convenience command for invoking: + + update-alternatives --help + + USAGE + exit 2 +} + +function help_update_alternatives_version() { + cat <<-'USAGE' + usage: my-alternatives ua-version + + convenience command for invoking: + + update-alternatives --version + + USAGE + exit 2 +} + +function help_update_alternatives() { + cat <<-'USAGE' + usage: my-alternatives ua [option ...] + + invokes update-alternatives pointing to the currently-configured alternatives. + + all options are passed through, unmodified. + + this is equivilent to: + + # ="\${MY_ALTS_TMP:-\${MY_ALTS_HOME}}" + update-alternatives --altdir "/alts" --admindir "/admin" [option ...] + + more info: + + see 'my-alternatives --help' to learn more about configuring your own alternatives. + + see 'update-alternatives --help' or 'man update-alternatives' to learn more about alternatives. + + USAGE + exit 2 +} + +function do_update_alternatives() { + # Ensure alt_home configured + # + if [ -z "${MY_ALTS_HOME:-}" ]; then + printf "my-alternatives: error: shell session not confgured. see 'my-alternatives --help' for usage\n" >&2 + exit 2 + fi + local alt_root="${MY_ALTS_TMP:-${MY_ALTS_HOME}}" + # Invoke update-alternatives + # + command update-alternatives \ + --altdir "${alt_root}/alts" \ + --admindir "${alt_root}/admin" \ + "$@" +} + +## +# alt_group_exists +# Checks if a named alt group exists in the configured alt root +# NOTE: Assumes knowledge of the configuration's "admin" directory +# $1 = name +# +# returns 0 if exist 1 otherwise +# +function alt_group_exists() { + local alt_root="${MY_ALTS_TMP:-${MY_ALTS_HOME:?}}" + if [ -f "${alt_root}/admin/${1}" ]; then + return 0 + fi + return 1 +} + +## +# is_dir_in_path +# Checks if the provided entry exists in the provided colon-separated list +# +# $1 = path ( or any colon-sepearated list ) +# $2 = dir ( entry to look for ) +# +# Returns 0 if found 1 otherwise +# +# exmaple: +# +# is_dir_in_path "${PATH}" /usr/local/bin && echo YES || echo NO +# +function is_dir_in_path() { + # 0 = success + # Most likely to be in middle, least likely to match full path + # + [[ "$1" == *":$2:"* || "$1" == "$2:"* || "$1" == *":$2" || "$1" == "$2" ]] && return 0 || return 1 +} + +## +# $1 = file (assumed to represent a file ie last element removed for path checks) +# +function get_my_alt_rel_path() { + local file path + if [ "${1}" == "" ]; then + printf "" + return + else + file="$(basename "$1")" + path="$(dirname "$1")" + if [ "${file}" == "" ] || [ "${path}" == "." ] || [ "${path}" == "/" ]; then + printf "%s" "$1" + return + fi + fi + # Check $PATH + # + if is_dir_in_path "${PATH}" "${path}"; then + printf "/bin/%s" "${file}" + return + fi + # Check manpath + # + if command -v manpath &>/dev/null; then + local _manpath + _manpath="$(command manpath 2>/dev/null)" + while [ "${path}" != "." ] && [ "${path}" != "/" ]; do + if is_dir_in_path "${_manpath}" "${path}"; then + printf "/man/%s" "${file}" + return + fi + file="$(command basename "${path}")/${file}" + path="$(command dirname "${path}")" + done + fi + # Unknown + # + printf "%s" "$1" +} + +# ############################################################################## +# BEGIN alternatives_parse_admin_redhat.bash v1.0.0 +# ############################################################################## + +## +# alt_parse_admin_redhat - Parses RedHat's update-alternatives admin files +# +# adapted from: +# +# https://github.com/fedora-sysv/chkconfig/blob/master/alternatives.c#L264 +# +# $1 = file to read from (can be process sub) +# $2 = debug|trace +# +# example: +# +# alt_parse_admin_redhat <( cat /var/lib/alternatives/pager ) +# +function alt_parse_admin_redhat() { + # Reset global vars + # + ALT_GROUP_LINK= + #ALT_GROUP_NAME= + ALT_GROUP_SLAVE_NAMES=() + ALT_GROUP_SLAVE_LINKS=() + ALT_GROUP_STATUS= # auto | manual + #ALT_GROUP_CURRENT_VALUE= + #ALT_GROUP_CURRENT_INDEX= + ALT_GROUP_BEST_VALUE= + ALT_GROUP_BEST_INDEX= + ALT_GROUP_VALUES=() + ALT_GROUP_PRIORITIES=() + ALT_GROUP_FAMILIES=() + ALT_GROUP_INIT_SCRIPTS=() + # TODO Consider moving to serialized arrays for slave values + #ALT_GROUP_VALUE_${v}_SLAVE_${s}_VALUE + unset _ALT_TMP_SLAVE_INDEX + + _ALT_PARSE_ADMIN_STATE="STATUS" + local IFS line lineno=0 # 1 on first use + while IFS="" read -r line || [ -n "${line}" ]; do + ((lineno += 1)) + # Keeps looping on return code 3, otherwise returns on !0 + # + local return_code + while :; do + if [ "${2:-}" == "trace" ]; then + printf "# line NO : %s\n" "${lineno}" + printf "# line TEXT: '%s'\n" "${line}" + printf "# state : %s\n" "${_ALT_PARSE_ADMIN_STATE}" + printf "# ---------\n" + fi + _alt_parse_admin_state_"${_ALT_PARSE_ADMIN_STATE}" "${line}" + return_code=$? + [[ $return_code -eq 3 ]] || break + done + [[ $return_code -eq 0 ]] || return $return_code + done <"${1}" + # Final state should be one of "eof candidate" states + # + if [ ! "${_ALT_PARSE_ADMIN_STATE}" = "MAYBE_ALT_START" ] && [ ! "${_ALT_PARSE_ADMIN_STATE}" = "MAYBE_ALT_SLAVE_VALUE" ]; then + printf "unexpected parse state: %s\n" "${_ALT_PARSE_ADMIN_STATE}" >&2 + return 2 + fi + if [ "${2:-}" == "trace" ] || [ "${2:-}" == "debug" ]; then + #printf "ALT_GROUP_NAME : %s\n" "${ALT_GROUP_NAME}" + printf "ALT_GROUP_LINK : %s\n" "${ALT_GROUP_LINK}" + printf "ALT_GROUP_SLAVE_NAMES : %s\n" "${ALT_GROUP_SLAVE_NAMES[*]}" + printf "ALT_GROUP_SLAVE_LINKS : %s\n" "${ALT_GROUP_SLAVE_LINKS[*]}" + printf "ALT_GROUP_STATUS : %s\n" "${ALT_GROUP_STATUS}" + #printf "ALT_GROUP_CURRENT_VALUE: %s\n" "${ALT_GROUP_CURRENT_VALUE}" + #printf "ALT_GROUP_CURRENT_INDEX: %s\n" "${ALT_GROUP_CURRENT_INDEX}" + printf "ALT_GROUP_BEST_VALUE : %s\n" "${ALT_GROUP_BEST_VALUE}" + printf "ALT_GROUP_BEST_INDEX : %s\n" "${ALT_GROUP_BEST_INDEX}" + printf "ALT_GROUP_VALUES : %s\n" "${ALT_GROUP_VALUES[*]}" + printf "ALT_GROUP_PRIORITIES : %s\n" "${ALT_GROUP_PRIORITIES[*]}" + printf "ALT_GROUP_FAMILIES : %s\n" "${ALT_GROUP_FAMILIES[*]}" + printf "ALT_GROUP_INIT_SCRIPTS : %s\n" "${ALT_GROUP_INIT_SCRIPTS[*]}" + local v=0 + while [[ $v -lt ${#ALT_GROUP_VALUES[@]} ]]; do + local s=0 + while [[ $s -lt ${#ALT_GROUP_SLAVE_NAMES[@]} ]]; do + local alt_slave_value_ref="ALT_GROUP_VALUE_${v}_SLAVE_${s}_VALUE" + printf "${alt_slave_value_ref} : %s\n" "${!alt_slave_value_ref}" + ((s += 1)) + done + ((v += 1)) + done + fi +} + +function _alt_parse_admin_state_STATUS() { + local regex='^((auto)|(manual))$' + if [[ "${1}" =~ $regex ]]; then + ALT_GROUP_STATUS="${BASH_REMATCH[1]}" + _ALT_PARSE_ADMIN_STATE="LINK" + return 0 + else + printf "unable to parse Status field: %s\n" "${1}" >&2 + return 2 + fi +} + +function _alt_parse_admin_state_LINK() { + local regex="^(/.+)$" + if [[ "${1}" =~ $regex ]]; then + ALT_GROUP_LINK="${BASH_REMATCH[1]}" + _ALT_PARSE_ADMIN_STATE="MAYBE_SLAVE_NAME" + return 0 + else + printf "unable to parse Primary Link field: %s\n" "${1}" >&2 + return 2 + fi +} + +function _alt_parse_admin_state_MAYBE_SLAVE_NAME() { + if [ -n "${1}" ]; then + _ALT_PARSE_ADMIN_STATE="SLAVE_NAME" + return 3 # re-check line + else + # blank line separates names/links from alternatives + # so consume the blank line, expect at least 1 alternative + # + _ALT_PARSE_ADMIN_STATE="ALT_VALUE" + return 0 + fi +} + +function _alt_parse_admin_state_SLAVE_NAME() { + local regex="^([^/].*)$" + if [[ "${1}" =~ $regex ]]; then + ALT_GROUP_SLAVE_NAMES+=("${BASH_REMATCH[1]}") + _ALT_PARSE_ADMIN_STATE="SLAVE_LINK" + return 0 + else + printf "unable to parse Slave Name field: %s\n" "${1}" >&2 + return 2 + fi +} + +function _alt_parse_admin_state_SLAVE_LINK() { + # Official source does not confirm leading / here + local regex="^(.+)$" + if [[ "${1}" =~ $regex ]]; then + ALT_GROUP_SLAVE_LINKS+=("${BASH_REMATCH[1]}") + _ALT_PARSE_ADMIN_STATE="MAYBE_SLAVE_NAME" + return 0 + else + printf "unable to parse Slave Link field: %s\n" "${1}" >&2 + return 2 + fi +} + +# eof candidate +function _alt_parse_admin_state_MAYBE_ALT_START() { + _ALT_PARSE_ADMIN_STATE="ALT_VALUE" + return 3 # re-check line +} + +function _alt_parse_admin_state_ALT_VALUE() { + local regex="^(/.+)$" + if [[ "${1}" =~ $regex ]]; then + ALT_GROUP_VALUES+=("${BASH_REMATCH[1]}") + _ALT_PARSE_ADMIN_STATE="ALT_PRIORITY" + return 0 + else + printf "unable to parse Alternative Value field: %s\n" "${1}" >&2 + return 2 + fi +} + +function _alt_parse_admin_state_ALT_PRIORITY() { + # Official source accepts \s* but likely always receives \s+ + local regex='^(@(.+)@)?([0-9]+)[[:space:]]*(.*)$' + if [[ "${1}" =~ $regex ]]; then + local alt_priority="${BASH_REMATCH[3]}" + local alt_index=${#ALT_GROUP_PRIORITIES[@]} # assign before += for 0-based value + ALT_GROUP_FAMILIES+=("${BASH_REMATCH[2]:-}") + ALT_GROUP_PRIORITIES+=("${alt_priority}") + ALT_GROUP_INIT_SCRIPTS+=("${BASH_REMATCH[4]:-}") + # We have to derive "BEST" manually, based in alternative priorities + # + if [ -z "${ALT_GROUP_BEST_INDEX}" ] || [[ "${ALT_GROUP_PRIORITIES[${ALT_GROUP_BEST_INDEX}]}" -lt "${alt_priority}" ]]; then + ALT_GROUP_BEST_INDEX="${alt_index}" + ALT_GROUP_BEST_VALUE="${ALT_GROUP_VALUES[${alt_index}]}" + fi + _ALT_PARSE_ADMIN_STATE="MAYBE_ALT_SLAVE_VALUE" + return 0 + else + printf "unable to parse Alternative Priority field: %s\n" "${1}" >&2 + return 2 + fi +} + +# eof candidate +function _alt_parse_admin_state_MAYBE_ALT_SLAVE_VALUE() { + : "${_ALT_TMP_SLAVE_INDEX:=0}" # Reset to 0 on first slave of each alternative + if [[ "${_ALT_TMP_SLAVE_INDEX}" -lt ${#ALT_GROUP_SLAVE_LINKS[@]} ]]; then + local regex="^(/.+)$" + if [[ "${1}" =~ $regex ]]; then + # Slave value order matches slave name order - no need to lookup + # + local slave_value="${BASH_REMATCH[1]}" + local alt_value_index=$((${#ALT_GROUP_VALUES[@]} - 1)) + local slave_value_ref="ALT_GROUP_VALUE_${alt_value_index}_SLAVE_${_ALT_TMP_SLAVE_INDEX}_VALUE" + declare -g "${slave_value_ref}"="${slave_value}" + _ALT_TMP_SLAVE_INDEX=$((_ALT_TMP_SLAVE_INDEX + 1)) + # Leave status unchanged + return 0 + else + printf "unable to parse Alternative Slave Value field: %s\n" "${1}" >&2 + return 2 + fi + else + unset _ALT_TMP_SLAVE_INDEX + _ALT_PARSE_ADMIN_STATE="MAYBE_ALT_START" + return 3 # re-check line + fi +} + +# ############################################################################## +# END alternatives_parse_admin_redhat.bash +# ############################################################################## + +# Only process main logic if not being sourced (ie tested) +# +(return 0 2>/dev/null) || main "$@"