view sbin/fports @ 804:f406b3b76b62

fports: Implemented also long commandline options
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 04 Nov 2024 11:44:47 +0100
parents 31ff8e5efdd6
children b59054f11029
line wrap: on
line source

#!/bin/sh
# -*- indent-tabs-mode: nil; -*-
#:
#: Check the version status of installed ports and compare them to
#: version in remote repositories and the local ports index.
#:
#: :Author:    Franz Glasner
#: :Copyright: (c) 2017-2024 Franz Glasner.
#:             All rights reserved.
#: :License:   BSD 3-Clause "New" or "Revised" License.
#:             See LICENSE for details.
#:             If you cannot find LICENSE see
#:             <https://opensource.org/licenses/BSD-3-Clause>
#: :ID:        @(#)@@SIMPLEVERSIONTAG@@
#:

: # separator for shellcheck: no module-level directives below

# shellcheck disable=SC2034    # VERSION appears unused
VERSION='@@VERSION@@'

# shellcheck disable=SC2016    # no expansion
USAGE='
USAGE: fports -h|--help
       fports -V|--version
       fports deptree [-l maxlevel|--maxlevel=maxlevel] [-r|--reverse] [-t|--list|--transitive] package...
       fports detail -n|--noauto|--no-auto
       fports detail -m|--mapped
       fports detail package...

GLOBAL OPTIONS:

  -V, --version  Print the program name and version number to stdout and exit.

  -h, --help     Print this help message to stdout and exit.

'


_p_datadir='@@DATADIR@@'
[ "${_p_datadir#@@DATADIR}" = '@@' ] && _p_datadir="$(dirname "$0")"/../share/local-bsdtools
. "${_p_datadir}/common.subr"
. "${_p_datadir}/farray.sh"
. "${_p_datadir}/ports.subr"


#:
#: Configuration directory.
#:
: "${CONFIGDIR:=@@ETCDIR@@}"

#:
#: Mapping configuration: installed package name -> original package name.
#:
#: Note:
#:   This is independent of any repo
#
: "${PACKAGE_MAPPING:=${CONFIGDIR}/package-mapping.conf}"

# shellcheck disable=SC1091   # does not exist -- cannot read
[ -r "${CONFIGDIR}/pkgtools.conf" ] && . "${CONFIGDIR}/pkgtools.conf"


# no unset variables
set -u


#:
#: Implementation of the "deptree" command.
#:
command_deptree() {
    local opt_reversed opt_maxlevel opt_flat
    # $@

    local opt

    opt_maxlevel=0
    opt_reversed=no
    opt_flat=no
    while getopts "l:rt-:" opt; do
        postprocess_getopts_for_long "l:rt-:" opt "maxlevel=" "reverse" "list" "transitive" ""
        case "${opt}" in
            l|maxlevel)
                opt_maxlevel=$(($OPTARG + 0));;
            r|reverse)
                # shellcheck disable=SC2034
                opt_reversed=yes;;
            t|list|transitive)
                opt_flat=yes;;
            \?)
                exit 2;;
            *)
                fatal 2 "option handling failed";;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    if checkyesno opt_reversed; then
        _command_deptree_reversed "${opt_maxlevel}" "${opt_flat}" "$@"
    else
        _command_deptree_normal "${opt_maxlevel}" "${opt_flat}" "$@"
    fi
}


#:
#: Implementation of printing a "normal" dependency tree
#:
_command_deptree_normal() {
    local maxlevel flat     # $@

    local pkgdeps pkgqueue curdeps pkg n v flatdeps

    maxlevel="${1}"
    flat="${2}"
    shift 2

    # shellcheck disable=SC2034    # pkgqueue seems unused
    pkgqueue=''
    farray_create pkgqueue  # queue (array) of packages that are queued for
                            # resolution

    for pkg in "$@"; do
        if ! "${PKG}" query '%n' "${pkg}" 1>/dev/null 2>/dev/null ; then
            farray_release pkgqueue
            fatal "${EX_DATAERR}" "Package not found: ${pkg}"
        fi
        farray_append pkgqueue "${pkg}"
    done
    pkgdeps=''
    falist_create pkgdeps   # alist of packagges with its direct dependencies
    while farray_pop pkg pkgqueue 1; do
        if ! falist_contains pkgdeps "${pkg}"; then
            curdeps=''
            farray_create curdeps
            while IFS=$' \t\n' read -r n v; do
                [ -z "${n}" ] || [ -z "${v}" ] && continue
                farray_append curdeps "${n}=${v}"
                farray_append pkgqueue "${n}"
            done <<EOF_01a8cebe-8659-4e32-87a4-bbce117e386b
$(LC_ALL=C.UTF-8 "${PKG}" query '%dn %dv' "${pkg}")
EOF_01a8cebe-8659-4e32-87a4-bbce117e386b
            falist_set pkgdeps "${pkg}" "${curdeps}"
            farray_release curdeps
            curdeps=''
        fi
    done
    farray_release pkgqueue
    #    falist_debug pkgdeps
    if checkyesno flat; then
        for pkg in "$@"; do
            _flatten_pkgdeps flatdeps "${pkgdeps}" "${pkg}"
            _print_flatdeps '-->' "${pkg}" "$(LC_ALL=C.UTF-8 "${PKG}" query '%v' "${pkg}")" "${flatdeps}"
            falist_release "${flatdeps}"
        done
    else
        for pkg in "$@"; do
            _print_dependency_tree 0 "${maxlevel}" '-->' "${pkg}" "$(LC_ALL=C.UTF-8 "${PKG}" query '%v' "${pkg}")" "${pkgdeps}"
        done
    fi
    falist_release pkgdeps
}


#:
#: Implementation of printing a reversed dependency tree
#:
_command_deptree_reversed() {
    local maxlevel flat  # $@

    local pkgdeps pkgqueue curdeps pkg n v flatdeps

    maxlevel="${1}"
    # shellcheck disable=SC2034    # appears unused
    flat="${2}"
    shift 2

    # shellcheck disable=SC2034    # pkgqueue seems unused
    pkgqueue=''
    farray_create pkgqueue  # queue (array) of packages that are queued for
                            # resolution

    for pkg in "$@"; do
        if ! "${PKG}" query '%n' "${pkg}" 1>/dev/null 2>/dev/null ; then
            farray_release pkgqueue
            fatal "${EX_DATAERR}" "Package not found: ${pkg}"
        fi
        farray_append pkgqueue "${pkg}"
    done
    pkgdeps=''
    falist_create pkgdeps   # alist of packagges with its direct dependencies
    while farray_pop pkg pkgqueue 1; do
        if ! falist_contains pkgdeps "${pkg}"; then
            curdeps=''
            farray_create curdeps
            while IFS=$' \t\n' read -r n v; do
                [ -z "${n}" ] || [ -z "${v}" ] && continue
                farray_append curdeps "${n}=${v}"
                farray_append pkgqueue "${n}"
            done <<EOF_5079e996-c6d2-4e6d-825d-53183a64ab06
$(LC_ALL=C.UTF-8 "${PKG}" query '%rn %rv' "${pkg}")
EOF_5079e996-c6d2-4e6d-825d-53183a64ab06
            falist_set pkgdeps "${pkg}" "${curdeps}"
            farray_release curdeps
            curdeps=''
        fi
    done
    farray_release pkgqueue
    #    falist_debug pkgdeps
    if checkyesno flat; then
        for pkg in "$@"; do
            _flatten_pkgdeps flatdeps "${pkgdeps}" "${pkg}"
            _print_flatdeps '<--' "${pkg}" "$(LC_ALL=C.UTF-8 "${PKG}" query '%v' "${pkg}")" "${flatdeps}"
            falist_release "${flatdeps}"
        done
    else
        for pkg in "$@"; do
            _print_dependency_tree 0 "${maxlevel}" '<--' "${pkg}" "$(LC_ALL=C.UTF-8 "${PKG}" query '%v' "${pkg}")" "${pkgdeps}"
        done
    fi
    falist_release pkgdeps
}


#:
#: Internal helper to print an indented dependency list for a package.
#:
#: Args:
#:   $1 (int): The (indentation) level where a level of `0` is the root level
#:   $2 (int): The maximum level (`$1`)  to print to
#:   $3 (str): The package tag to use to for non-root-levels
#:   $4 (str): The package name
#:   $5 (str): The package version
#:   $6 (alist): The alist of resolved packages and their dependencies
#:
_print_dependency_tree() {
    # $1 $2 $3 $4 $5 $6

    local i pkg ver curdeps

    if [ "${2}" -ge 1 ]; then
        [ "${1}" -gt "${2}" ] && return 0
    fi

    i="${1}"
    while [ "${i}" -gt 1 ]; do
        printf '%s' '    '
        i=$((i - 1))
    done
    [ "${1}" -ne 0 ] && printf '%s ' "${3}"
    printf '%s v%s\n' "${4}" "${5}"
    falist_get curdeps "${6}" "${pkg}"
    i=1
    while farray_tryget pkg "${curdeps}" "${i}"; do
        ver="${pkg#*=}"
        pkg="${pkg%%=*}"
        _print_dependency_tree $(($1 + 1)) "${2}" "${3}" "${pkg}" "${ver}" "${6}"
        i=$((i + 1))
    done
    farray_release curdeps
}


#:
#: Args:
#:   $1 (str): The package tag to use
#:   $2 (str): The root package name
#:   $3 (str): The package version of the root package in `$3`
#:   $4 (alist): The alist of the flattened dependencies
#:
_print_flatdeps() {
    # $1 $2 $3 $4

    local pkgnames i n v

    printf '%s v%s\n' "${2}" "${3}"

    # shellcheck disable=SC2034    # appears unused
    pkgnames=''
    farray_create pkgnames
    falist_keys pkgnames "$4"
    farray_sort pkgnames
    i=1
    while farray_tryget n pkgnames "${i}"; do
        falist_get v "${4}" "${n}"
        printf '%s %s v%s\n' "${1}" "${n}" "${v}"
        i=$((i + 1))
    done

    farray_release pkgnames
}


#:
#: Flatten a package dependency alist.
#:
#: Args:
#:   $1 (str): The variable name where to store the flattened dependencies
#:             into. This object (alist) must be released by the caller.
#:   $2 (str): The alist with all packages and its dependencies
#:   $3 (str): The package for which to flatten its dependencies
#:
_flatten_pkgdeps() {
    local pkgdeps rootpkg     # and $1

    local alldeps queue pkg curdeps i depname depver

    pkgdeps="${2}"
    rootpkg="${3}"

    falist_contains pkgdeps "${rootpkg}" || fatal "${EX_SOFTWARE}" "given package \`${rootpkg}' not in the given package dependency map"

    queue=''
    farray_create queue     # array with package names to be flattened

    farray_append queue "${rootpkg}"

    alldeps=''
    falist_create alldeps   # alist with pkgname -> version
    while farray_pop pkg queue 1; do
        if ! falist_contains alldeps "${pkg}"; then
            curdeps=''
            falist_get curdeps pkgdeps "${pkg}"
            i=1
            while farray_tryget depname curdeps "${i}"; do
                depver="${depname#*=}"
                depname="${depname%%=*}"
                if ! falist_contains alldeps "${depname}"; then
                    falist_set alldeps "${depname}" "${depver}"
                fi
                farray_append queue "${depname}"
                i=$((i + 1))
            done
            farray_release curdeps
        fi
    done

    farray_release "${queue}"

    setvar "${1}" "${alldeps}"
}


#:
#: Implementation of the "detail" command.
#:
command_detail() {
    local opt_noauto opt_mapped
    # $@

    local package \
          repositories packagemapping instver instrepo \
          repo title_printed indexfile _dummy opt acookie \
          pkglabel pkgdescr pkgversion mapped_package

    opt_noauto=no
    opt_mapped=no
    while getopts "nm-:" opt; do
        postprocess_getopts_for_long "nm-:" opt "noauto" "no-auto" "mapped" ""
        case "${opt}" in
            n|noauto|no-auto)
                # shellcheck disable=SC2034     # appears unused
                opt_noauto=yes;;
            m|mapped)
                # shellcheck disable=SC2034     # appears unused
                opt_mapped=yes;;
            \?)
                exit 2;;
            *)
                fatal 2 "option handling failed";;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    if checkyesno opt_noauto || checkyesno opt_mapped; then
        [ $# -gt 0 ] && fatal "${EX_USAGE}" "packages are not allowed for options -n or -m"
        if checkyesno opt_noauto && checkyesno opt_mapped; then
            fatal "${EX_USAGE}" "cannot use -n and -m together"
        fi
    fi

    repositories=''
    get_active_repositories repositories
    packagemapping=''
    init_package_mapping packagemapping
    indexfile="$(get_local_index_file)"

    if checkyesno opt_noauto; then
        for package in $(LC_ALL=C.UTF-8 "${PKG}" query -e '%a = 0' '%n'); do
            _package_max_detail "${package}" "${packagemapping}" "${repositories}" "${indexfile}"
        done
    elif checkyesno opt_mapped; then
        acookie="$(falist_cookie_first packagemapping)"
        while falist_tryget_key_at package "${acookie}"; do
            _package_max_detail "${package}" "${packagemapping}" "${repositories}" "${indexfile}"
            acookie="$(falist_cookie_next "${acookie}")"
        done
    else
        for package in "$@"; do
            _package_max_detail "${package}" "${packagemapping}" "${repositories}" "${indexfile}"
        done
    fi

    falist_release "${packagemapping}"
    farray_release "${repositories}"
}


#:
#: Implementation of printing the most details possible for a package.
#:
#: Implements all the repeating stuff for a package for e.g. `command_detail`.
#:
#: Args:
#:   $1 (str): The name of the package
#:   $2 (alist): The
#:   $3 (array): The array with all the configured/active repositories
#:   $4 (str, null): The local index file if it exists
#:
_package_max_detail() {
    local package packagemapping repositories indexfile

    local instver instrepo repo title_printed _dummy \
          pkglabel pkgdescr pkgversion mapped_package

    package="${1}"
    packagemapping="${2}"
    repositories="${3}"
    indexfile="${4}"

    # shellcheck disable=SC2034    # appears unused
    title_printed=no
    IFS='|' read -r instver instrepo <<EOF_e9bd7819-b4c5-4a86-b984-f5226db58cb1
$(LC_ALL=C.UTF-8 "${PKG}" query '%v|%R' "${package}")
EOF_e9bd7819-b4c5-4a86-b984-f5226db58cb1
    print_title title_printed "${package}" "${instver}" "${instrepo}"
    if [ -n "${indexfile}" ]; then
        read -r _dummy pkglabel pkgdescr <<EOF_b1f225bd-d234-4a23-8a2a-40c2e5b7ff3c
$(LC_ALL=C.UTF-8 "${PKG}" version -U -I -n "${package}" -v "${indexfile}")
EOF_b1f225bd-d234-4a23-8a2a-40c2e5b7ff3c
        pkgversion="$(parse_index_file_for_package_version "${indexfile}" "${package}")"
        print_detail_item "INDEX" "${pkgversion}" "${pkglabel}" "${pkgdescr}"
    fi
    farray_for_each repositories _package_repository_detail "${package}" 0
    mapped_package="$(get_package_mapping "${packagemapping}" "${package}")"
    if [ -n "${mapped_package}" ]; then
        printf '%18s %s\n' "--------------->" "${mapped_package}"
        if [ -n "${indexfile}" ]; then
            pkgversion="$(parse_index_file_for_package_version "${indexfile}" "${mapped_package}")"
            pkglabel="$(LC_ALL=C.UTF-8 "${PKG}" version --test-version "${instver}" "${pkgversion}")"
            print_detail_item "INDEX" "${pkgversion}" "${pkglabel}" '' 19
        fi
        farray_for_each repositories _mapped_package_repository_detail "${mapped_package}" "{instver}" 19
    fi
}


#:
#: Array callback to print package details with regard to a repository.
#:
#: Args:
#:   $1 (str): The repositories array
#:   $2 (int): The current index
#:   $3: The element value (i.e. repository name)
#:   $4 (str): The (master) package name
#:   $5 (int): The extra indent value to forward to called functions
#:
_package_repository_detail() {
    local repositories idx reponame package extraindent

    local _dummy \
          pkglabel pkgdescr pkgversion

    repositories="${1}"
    # shellcheck disable=SC2034    # appears unused (yes, accept it)
    idx="${2}"
    reponame="${3}"
    package="${4}"
    extraindent="${5:-0}"

    read -r _dummy  pkglabel pkgdescr <<EOF_19cf2d80-4eb9-4cda-bd4d-96b04e769206
$(LC_ALL=C.UTF-8 "${PKG}" version -U -R -r "${reponame}" -n "${package}" -v)
EOF_19cf2d80-4eb9-4cda-bd4d-96b04e769206
    pkgversion="$(LC_ALL=C.UTF-8 "${PKG}" rquery -U -r "${reponame}" '%v' "${package}")"
    print_detail_item "${reponame}" "${pkgversion}" "${pkglabel}" "${pkgdescr}" "${extraindent}"
}


#:
#: Array callback to print package details for a mapped package with regard
#: to a repository.
#:
#: Args:
#:   $1 (str): The repositories array
#:   $2 (int): The current index
#:   $3: The element value (i.e. repository name)
#:   $4 (str): The mapped package name
#:   $5 (str): The parent package version
#:   $6 (int): The extra indent value to forward to called functions
#:
_mapped_package_repository_detail() {
    local repositories idx reponame package parent_pkgversion extraindent

    local _dummy \
          pkglabel pkgversion

    repositories="${1}"
    # shellcheck disable=SC2034    # appears unused (yes, accept it)
    idx="${2}"
    reponame="${3}"
    package="${4}"
    parent_pkgversion="${5}"
    extraindent="${6:-0}"

    pkgversion="$(LC_ALL=C.UTF-8 "${PKG}" rquery -U -r "${reponame}" '%v' "${package}")"
    pkglabel="$(LC_ALL=C.UTF-8 "${PKG}" version --test-version "${parent_pkgversion}" "${pkgversion}")"
    print_detail_item "${reponame}" "${pkgversion}" "${pkglabel}" '' "${extraindent}"
}


#:
#: Print the output title line for a package.
#:
#: Args:
#:   $1 (str): The name of the variable where to get or store the flag
#:             whether the title for the package in `$2` has already been
#:             printed.
#:   $2 (str): The package name
#:   $3 (str): The package version
#:   $4 (str): The repository name from which the package has been installed
#:
#: Output (stdout):
#:   The formatted title line
#:
print_title() {
    local varname_title_printed package version repo

    varname_title_printed="${1}"
    package="${2}"
    version="${3}"
    repo="${4}"

    if ! checkyes "${varname_title_printed}"; then
        if [ -n "${version}" ]; then
            # The package is installed
            printf '%-36s %-17s (%s)\n' "${package}" "${version}" "${repo}"
        else
            # The package is not installed
            printf '%-36s NOT INSTALLED\n' "${package}"
        fi
        setvar "${varname_title_printed}" 'yes'
    fi
}


#:
#: Print a detail item to stdout.
#:
#: The description `_descr` will not be printed if the label `_label`
#: is ``?``.
#:
#: Args:
#:   $1 (str): The repository name
#:   $2 (str): The version number to print to
#:   $3 (str): The label (aka comparison character) to print to
#:   $4 (str): The description to print to
#:   $5 (int, optional): The extra indentation to use. (Default 0)
#:
#: Output (stdout):
#:   The formatted detail line
#:
print_detail_item() {
    local repo version label descr indent

    local real_descr

    repo="${1}"
    version="${2}"
    label="${3}"
    descr="${4}"
    indent="${5:-0}"

    if [ "${label}" = '?' ]; then
        real_descr=''
    else
        real_descr="${descr}"
    fi

    printf '%-*s  %-15s: %-17s %s %s\n' $((indent)) '' "${repo}" "${version}" "${label}" "${real_descr}"
}


#
# Global option handling
#
while getopts "Vh-:" _opt ; do
    postprocess_getopts_for_long "Vh-:" _opt "version" "help" ""
    case "${_opt}" in
        V|version)
            printf 'fports %s\n' '@@SIMPLEVERSIONSTR@@'
            exit 0
            ;;
        h|help)
            echo "${USAGE}"
            exit 0
            ;;
        \?)
            exit 2;
            ;;
        *)
            fatal 2 "option handling failed"
            ;;
    esac
done

#
# Reset the Shell's option handling system to prepare for handling
# command-local options.
#
shift $((OPTIND-1))
OPTIND=1

command="${1-}"
shift

case "${command}" in
    '') fatal 2 "no command given";;
    deptree)
        command_deptree "$@";;
    detail)
        command_detail "$@";;
    *)
        fatal 2 "unknown command \`${command}'";;
esac