#!/bin/bash # Version: 0.2.0 # Copyright (c) 2017-2026: # Darren 'Tadgy' Austin # Licensed under the terms of the GNU General Public License version 3. # # Automatically or interractively merge tagfiles from two directories into a new set. # # The (space separated) list of package sets. PACKAGE_SETS="${PACKAGE_SETS:-a ap d e f k kde kdei l n t tcl x xap xfce y}" # nullglob is required. shopt -s nullglob # Functions. display_help() { # |--------1---------2---------3---------4---------5---------6---------7---------8 echo "Usage: ${0##*/} [options] " echo "Automatically or interactively merge tagfiles from and" echo ", writing the resultant tagfile set to ." echo echo "Caveats:" echo " * Priorities from sets take presidence over ." echo " * Only the packages listed in the sets are merged with the" echo " priorities from the sets - packages in the " echo " sets not in the set are removed in the sets." echo " * Package descriptions are read from .txt files in either or" echo " , whichever directory contains the file first." # echo " * Only comments from sets are merged into sets." echo echo "Options:" echo " -a, --default-add Do not interactively prompt whether to ADD, REC, SKP or OPT" echo " any new packages - automatically use this default." echo " -r, --default-rec See above." echo " -s, --default-skp See above." echo " -o, --default-opt See above." echo " -n, --no-recopt Only allow the ADD or SKP priorities. In interactive mode" echo " the replacement priority is prompted for, otherwise the" echo " options --default-add or --default-skp determine priority." echo " -N, --no-desc Do not show the package description in interactive mode." echo " -V, --version Display version and copyright information." echo " -h, --help Display this help, obviously." echo " -- Force the end of option processing. Any options following" echo " this are treated as arguments." echo "Option processing ceases with the first non-option or --." echo echo "Arguments (all of which are mandatory):" echo " The top-level directory of the first set of tagfiles." echo " The top-level directory of the second set of tagfiles." echo " Path to the directory in which to write the new tagfiles." return 0 } display_version() { # |--------1---------2---------3---------4---------5---------6---------7---------8 echo "${0##*/} v0.2.0" echo "Copyright (c) 2017-2026 Darren 'Tadgy' Austin " echo "Licensed under the terms of the GNU GPL v3 ." echo "This program is free software: you can modify or redistribute it in accordence" echo "with the GNU GPL. However, it comes with ABSOLUTELY NO WARRANTY; not even the" echo "implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." return 0 } check_directory() { # Arguments: $1 = Directory to check. # Returns: 0 = OK. # 1 = Doesn't exist. # 2 = Not a directory. # 3 = No read permission. # 4 = No write permission. if [[ ! -e "$1" ]]; then echo "${0##*/}: no such directory: $1" >&2 return 1 elif [[ ! -d "$1" ]]; then echo "${0##*/}: not a directory: $1" >&2 return 2 elif [[ ! -r "$1" ]]; then echo "${0##*/}: read permission denied: $1" >&2 return 3 elif [[ ! -w "$1" ]]; then echo "${0##*/}: write permission denied: $1" >&2 return 4 fi return 0 } write_line() { # Arguments: $1 = The package name. # $2 = The packages priority. # $3 = The packages comment. # $4 = File to write to. if [[ ! -z "$3" ]]; then printf "%s:%s\\t\\t%s\\n" "$1" "$2" "$3" >>"$4" || exit 1 else printf "%s:%s\\n" "$1" "$2" >>"$4" || exit 1 fi } prompt_priority() { # Arguments: $1 = Package name being processed. # $2 = Previous priority of package being processed. # $3 = Any comment from the tagfile for the package. # Globsl variables: SET = The tagfile set being processed. # TAGFILES_DIR1 = The first tagfiles directory given on the command line. # TAGFILES_DIR2 = The second tagfiles directory given on the command line. # Returns: The new priority for the package. local ANSWER DESC local -a DESCFILE FLAG_MANUAL_INPUT=1 echo echo echo "Package: $SET/$1:" [[ ! -v FLAG_NO_DESC ]] && { DESCFILE=( "$TAGFILES_DIR1/$SET/$1"-*-*-*.txt "$TAGFILES_DIR2/$SET/$1"-*-*-*.txt ) DESC="$(grep "^$1:" "${DESCFILE[0]}" 2>/dev/null | sed -re 's/^.*:[[:blank:]]*/ /g; s/^[[:blank:]]*$/ ./g')" if [[ -n "$DESC" ]]; then echo " Description from ${DESCFILE[0]}:" echo "$DESC" else echo " Description not found." fi } echo " Current priority: ${2:-(none)}" echo " Comment: ${3:-(none)}" while :; do echo if [[ -v FLAG_NO_RECOPT ]]; then read -r -n 1 -p " Select new priority for package: (A)DD, or (S)KP: " ANSWER else read -r -n 1 -p " Select new priority for package: (A)DD, (R)EC, (S)KP, or (O)PT: " ANSWER fi if [[ "${ANSWER^^}" = "A" ]]; then NEWPRI="ADD" break elif [[ ! -v FLAG_NO_RECOPT ]] && [[ "${ANSWER^^}" = "R" ]]; then NEWPRI="REC" break elif [[ "${ANSWER^^}" = "S" ]]; then NEWPRI="SKP" break elif [[ ! -v FLAG_NO_RECOPT ]] && [[ "${ANSWER^^}" = "O" ]]; then NEWPRI="OPT" break else continue fi done } process_tagfile() { # Arguments: $1 = The tagfiles path to process. # # Globsl variables: DEST_DIR = The destination directory to write the new tagfiles. # SET = The tagfile set being processed. local COMMENT NEWPRI PACKAGE PRIORITY # Clear the destination tagfile. : >"$DEST_DIR/$SET/tagfile" # Open the input tagfile for reading. # Note: We do this with an fd so we can use another 'read' of stdin for prompting. exec {FD}<"$1/$SET/tagfile" # Process the tagfile line by line. while IFS=$'\t :' read -r -u "$FD" PACKAGE PRIORITY COMMENT; do if [[ -v FLAG_NO_RECOPT ]] && [[ ! "${PRIORITY^^}" =~ ^(ADD|SKP)$ ]]; then # Can't use REC or OPT - Get for new priority. if [[ -v DEFAULT_PRIORITY ]]; then NEWPRI="$DEFAULT_PRIORITY" else prompt_priority "$PACKAGE" "${PRIORITY^^}" "$COMMENT" # Sets the NEWPRI variable fi [[ -z "$NEWPRI" ]] && { echo "Abort: failed to get new priority" >&2 exit 1 } write_line "$PACKAGE" "$NEWPRI" "$COMMENT" "$DEST_DIR/$SET/tagfile" elif [[ "${PRIORITY^^}" =~ ^(ADD|SKP)$ ]]; then # ADD or SKP in tagfile - write it out. write_line "$PACKAGE" "${PRIORITY^^}" "$COMMENT" "$DEST_DIR/$SET/tagfile" else echo "** Invalid priority ($PRIORITY) for $PACKAGE in tagfile:" echo "** $1/$2/tagfile" # Invalid priority in tagfile - try to fix. if [[ -v DEFAULT_PRIORITY ]]; then echo "** Correcting using default priority: $DEFAULT_PRIORITY" NEWPRI="$DEFAULT_PRIORITY" else echo "** Prompting for new valid priority..." prompt_priority "$PACKAGE" "${PRIORITY^^}" "$COMMENT" # Sets the NEWPRI variabl fi [[ -z "$NEWPRI" ]] && { echo "Abort: failed to get new priority" >&2 exit 1 } write_line "$PACKAGE" "$NEWPRI" "$COMMENT" "$DEST_DIR/$SET/tagfile" fi done # Close tagfile file descriptor. exec {FD}<&- } merge_tagfiles() { # Globsl variables: DEST_DIR = The destination directory to write the new tagfiles. # SET = The tagfile set being processed. # TAGFILES_DIR1 = The first tagfiles directory given on the command line. # TAGFILES_DIR2 = The second tagfiles directory given on the command line. local COMMENT1 COMMENT2 PACKAGE NEWPRI PRIORITY1 PRIORITY2 # Open the first tagfile for reading. # Note: We do this with an fd so we can use another 'read' of stdin for prompting. exec {FD}<"$TAGFILES_DIR1/$SET/tagfile" while IFS=$'\t :' read -r -u "$FD" PACKAGE PRIORITY1 COMMENT1; do PRIORITY2="$(grep -E "^${PACKAGE//+/\\+}:" "$TAGFILES_DIR2/$SET/tagfile" | sed -re "/^${PACKAGE//+/\\\+}:/ s/[^[:blank:]:]+:[[:blank:]]*([^[:blank:]]+)(\$|[[:blank:]].*\$)/\\1/")" COMMENT2="$(grep -E "^${PACKAGE//+/\\+}:" "$TAGFILES_DIR2/$SET/tagfile" | sed -re "/^${PACKAGE//+/\\\+}:/ s/[^[:blank:]:]+:[[:blank:]]*([^[:blank:]]+)(\$|[[:blank:]](.*)\$)/\\3/")" if [[ -z "$PRIORITY2" ]]; then # Package wasn't in tagfile2. if [[ ! -z "$DEFAULT_PRIORITY" ]]; then # Use default priority. write_line "$PACKAGE" "$DEFAULT_PRIORITY" "$COMMENT1" "$DEST_DIR/$SET/tagfile" else # Prompt for new priority. prompt_priority "$PACKAGE" "$PRIORITY1" "$COMMENT1" # Sets the NEWPRI variabl [[ -z "$NEWPRI" ]] && { echo "Abort: failed to get new priority" >&2 exit 1 } write_line "$PACKAGE" "$NEWPRI" "$COMMENT1" "$DEST_DIR/$SET/tagfile" fi elif [[ ! "${PRIORITY2^^}" =~ ^(ADD|REC|SKP|OPT) ]]; then # Package was listed in tagfile2, but priority is invalid. if [[ ! -z "$DEFAULT_PRIORITY" ]]; then # Use default priority. write_line "$PACKAGE" "$DEFAULT_PRIORITY" "${COMMENT2:-$COMMENT1}" "$DEST_DIR/$SET/tagfile" else # Prompt for new priority. prompt_priority "$PACKAGE" "$PRIORITY2" "${COMMENT2:-$COMMENT1}" # Sets the NEWPRI variabl [[ -z "$NEWPRI" ]] && { echo "Abort: failed to get new priority" >&2 exit 1 } write_line "$PACKAGE" "$NEWPRI" "${COMMENT2:-$COMMENT1}" "$DEST_DIR/$SET/tagfile" fi else # Package was listed in tagfile2 and priority was valid. write_line "$PACKAGE" "${PRIORITY2^^}" "${COMMENT2:-$COMMENT1}" "$DEST_DIR/$SET/tagfile" fi done } # Process command line options. while :; do case "$1" in '-a'|'-default-add'|'--default-add') if [[ "$DEFAULT_PRIORITY" =~ ^(REC|SKP|OPT)$ ]]; then echo "${0##*/}: multiple --default-* options specified" >&2 exit 1 else DEFAULT_PRIORITY="ADD" fi shift ;; '-r'|'-default-rec'|'--default-rec') [[ -v FLAG_NO_RECOPT ]] && { echo "${0##*/}: cannot use --no-recopt with --default-rec" >&2 exit 1 } if [[ "$DEFAULT_PRIORITY" =~ ^(ADD|SKP|OPT)$ ]]; then echo "${0##*/}: multiple --default-* options specified" >&2 exit 1 else DEFAULT_PRIORITY="REC" fi shift ;; '-s'|'-default-skp'|'--default-skp') if [[ "$DEFAULT_PRIORITY" =~ ^(ADD|REC|OPT)$ ]]; then echo "${0##*/}: multiple --default-* options specified" >&2 exit 1 else DEFAULT_PRIORITY="SKP" fi shift ;; '-o'|'-default-opt'|'--default-opt') [[ -v FLAG_NO_RECOPT ]] && { echo "${0##*/}: cannot use --no-recopt with --default-opt" >&2 exit 1 } if [[ "$DEFAULT_PRIORITY" =~ ^(ADD|REC|SKP)$ ]]; then echo "${0##*/}: multiple --default-* options specified" >&2 exit 1 else DEFAULT_PRIORITY="OPT" fi shift ;; '-n'|'-no-recopt'|'--no-recopt') if [[ "$DEFAULT_PRIORITY" = "REC" ]]; then echo "${0##*/}: cannot use --no-recopt with --default-rec" >&2 exit 1 elif [[ "$DEFAULT_PRIORITY" = "OPT" ]]; then echo "${0##*/}: cannot use --no-recopt with --default-opt" >&2 exit 1 else FLAG_NO_RECOPT=1 fi shift ;; '-N'|'-no-desc'|'--no-desc') FLAG_NO_DESC=1 shift ;; '-V'|'-version'|'--version') display_version exit 0 ;; '-h'|'-help'|'--help') display_help exit 0 ;; '--') shift break ;; -*) # This prevents the first non-option argument being an entry in the # current directory which begins with a '-'. Use the '--' option # or ./-foo where this becomes a problem. echo "${0##*/}: invalid option: $1" >&2 echo "Try: ${0##*/} --help" >&2 exit 1 ;; *) break ;; esac done # Process command line arguments. (( $# != 3 )) && { echo "${0##*/}: incorrect number of non-option arguments" >&2 echo "Try: ${0##*/} --help" >&2 exit 1 } for ARG in "$@"; do ERROR_TEXT="$(check_directory "$ARG" 2>&1)" ERROR_CODE=$? if [[ -z "$TAGFILES_DIR1" || -z "$TAGFILES_DIR2" ]]; then # and if (( ERROR_CODE >= 1 && ERROR_CODE <= 3 )); then # Ignore write permission failure. echo "$ERROR_TEXT" >&2 exit 1 else TAGFILES_DIR2="${TAGFILES_DIR1:+$ARG}" TAGFILES_DIR1="${TAGFILES_DIR1:-$ARG}" # Yes, these are correct and in the right order! fi else # if (( ERROR_CODE >= 2 )); then # Non-existant (ERROR_CODE=1) is OK here. echo "$ERROR_TEXT" >&2 exit 1 else DEST_DIR="$ARG" fi fi done [[ -z "$TAGFILES_DIR1" || -z "$TAGFILES_DIR2" || -z "$DEST_DIR" ]] && { echo "Abort: argument processing failure" >&2 exit 1 } # Create destination directory. if [[ -e "$DEST_DIR" ]]; then echo "${0##*/}: directory exists: $DEST_DIR" >&2 exit 1 else # shellcheck disable=SC2174 mkdir -p -m 755 "$DEST_DIR" || exit 1 fi # Main loop. FLAG_MANUAL_INPUT=0 for SET in $PACKAGE_SETS; do mkdir "$DEST_DIR/$SET" || exit 1 if [[ -e "$TAGFILES_DIR1/$SET/tagfile" ]]; then # Found tagfile in dir1. if [[ -e "$TAGFILES_DIR2/$SET/tagfile" ]]; then # Found tagfiles in both dirs - merge the tagfiles into dest dir. merge_tagfiles else # Process tagfile in dir1 and write to dest dir. process_tagfile "$TAGFILES_DIR1" || exit 1 fi else # No tagfile in dir1 - check dir2. if [[ -e "$TAGFILES_DIR2/$SET/tagfile" ]]; then # Found tagfile in dir2 - process it and write to dest dir. process_tagfile "$TAGFILES_DIR2" || exit 1 else # No tagfile in either dir - create a stub file. touch "$DEST_DIR/$SET/tagfile" || exit 1 fi fi done (( FLAG_MANUAL_INPUT == 1 )) && echo