diff options
author | Roshless | 2018-07-17 01:46:39 +0200 |
---|---|---|
committer | Roshless | 2018-07-17 01:46:39 +0200 |
commit | 48da19f479c079e62e2c4b3886841866228f6b01 (patch) | |
tree | 2c91342d03441e81ec8c625dde59b463e83e3431 | |
download | aur-48da19f479c079e62e2c4b3886841866228f6b01.tar.gz |
first version
-rw-r--r-- | .SRCINFO | 18 | ||||
-rw-r--r-- | PKGBUILD | 23 | ||||
-rw-r--r-- | etc-update | 895 | ||||
-rw-r--r-- | etc-update.conf | 82 |
4 files changed, 1018 insertions, 0 deletions
diff --git a/.SRCINFO b/.SRCINFO new file mode 100644 index 000000000000..ce300cec3049 --- /dev/null +++ b/.SRCINFO @@ -0,0 +1,18 @@ +pkgbase = etc-update-nogithub + pkgdesc = CLI to interactively merge .pacnew in /etc. I don't trust gentoo's github. + pkgver = 20180717 + pkgrel = 1 + url = https://wiki.gentoo.org/wiki/Handbook:X86/Portage/Tools#etc-update + arch = any + license = GPL + makedepends = git + depends = bash + provides = etc-update + conflicts = etc-update + source = etc-update + source = etc-update.conf + md5sums = 1de63bd8e07fa78779eecf864b6021e4 + md5sums = ca8d58262382a7ead33b69469f661f5e + +pkgname = etc-update-nogithub + diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 000000000000..6b22797686af --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,23 @@ +# Maintainer: Roshless <aur@roshless.com> +_appname_=etc-update +pkgname=${_appname_}-nogithub +pkgdesc="CLI to interactively merge .pacnew in /etc. I don't trust gentoo's github." +pkgver=20180717 +pkgrel=1 +arch=('any') +url="https://wiki.gentoo.org/wiki/Handbook:X86/Portage/Tools#etc-update" +license=('GPL') +depends=('bash') +makedepends=('git') +provides=("${_appname_}") +conflicts=("${_appname_}") +source=("etc-update" + "etc-update.conf") +md5sums=('1de63bd8e07fa78779eecf864b6021e4' + 'ca8d58262382a7ead33b69469f661f5e') + +package() { + install -Dm 0755 "${_appname_}" "$pkgdir/usr/bin/${pkgname}" + install -Dm 0644 "${_appname_}.conf" "$pkgdir/etc/${pkgname}.conf" +} + diff --git a/etc-update b/etc-update new file mode 100644 index 000000000000..850f6a21b88e --- /dev/null +++ b/etc-update @@ -0,0 +1,895 @@ +#!/bin/bash +# Copyright 1999-2012 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +# Author Brandon Low <lostlogic@gentoo.org> +# Mike Frysinger <vapier@gentoo.org> +# +# Previous version (from which I've borrowed a few bits) by: +# Jochem Kossen <j.kossen@home.nl> +# Leo Lipelis <aeoo@gentoo.org> +# Karl Trygve Kalleberg <karltk@gentoo.org> + +cd / + +type -P gsed >/dev/null && sed() { gsed "$@"; } + +get_config() { + # the sed here does: + # - strip off comments + # - match lines that set item in question + # - delete the "item =" part + # - store the actual value into the hold space + # - on the last line, restore the hold space and print it + # If there's more than one of the same configuration item, then + # the store to the hold space clobbers previous value so the last + # setting takes precedence. + local match=$1 + eval $(sed -n -r \ + -e 's:[[:space:]]*#.*$::' \ + -e "/^[[:space:]]*${match}[[:space:]]*=/{s:^([^=]*)=[[:space:]]*([\"']{0,1})(.*)\2:\1=\2\3\2:;H}" \ + -e '${g;p}' \ + "${PORTAGE_CONFIGROOT}"etc/etc-update.conf) +} + +OS_RELEASE_ID=$(cat /etc/os-release 2>/dev/null | grep '^ID=' | cut -d'=' -f2 | sed -e 's/"//g') + +case $OS_RELEASE_ID in + suse|opensuse|opensuse-leap|opensuse-tumbleweed) OS_FAMILY='rpm' ;; + fedora|rhel) OS_FAMILY='rpm' ;; + arch|archarm|manjaro|antergos) OS_FAMILY='arch' NEW_EXT='pacnew';; + *) OS_FAMILY='gentoo' ;; +esac + +if [[ $OS_FAMILY == 'gentoo' ]]; then + get_basename() { + printf -- '%s\n' "${1:10}" + } + get_basename_find_opt() { + echo "._cfg????_${1}" + } + get_scan_regexp() { + echo "s:\(^.*/\)\(\._cfg[0-9]*_\)\(.*$\):\1\2\3$b\1$b\2$b\3:" + } + get_live_file() { + echo "${rpath}/${rfile:10}" + } +elif [[ $OS_FAMILY == 'arch' ]]; then + get_basename() { + printf -- '%s\n' "${1%.${NEW_EXT}}" + } + get_basename_find_opt() { + printf -- '%s\n' "${1}.${NEW_EXT}" + } + get_scan_regexp() { + echo "s:\(^.*/\)\(.*\)\(\.${NEW_EXT}\):\1\2\3$b\1$b\3$b\2:" + } + get_live_file() { + printf -- '%s\n' "${cfg_file%.${NEW_EXT}}" + } +# In rpm we have rpmsave, rpmorig, and rpmnew. +elif [[ $OS_FAMILY == 'rpm' ]]; then + get_basename() { + printf -- '%s\n' "${1}" |sed -e 's/\.rpmsave$//' -e 's/\.rpmnew$//' -e 's/\.rpmorig$//' + } + get_basename_find_opt() { + printf -- '%s\n' "${1}.rpm???*" + } + get_scan_regexp() { + echo "s:\(^.*/\)\(.*\)\(\.\(rpmnew|rpmsave|rpmorig\)\):\1\2\3$b\1$b\3$b\2:" + } + get_live_file() { + printf -- '%s\n' "${cfg_file}" |sed -e 's/\.rpmsave$//' -e 's/\.rpmnew$//' -e 's/\.rpmorig$//' + } +fi + +cmd_var_is_valid() { + # return true if the first whitespace-separated token contained + # in "${1}" is an executable file, false otherwise + [[ -x $(type -P ${1%%[[:space:]]*}) ]] +} + +diff_command() { + local cmd=${diff_command//%file1/$1} + ${cmd//%file2/$2} +} + +# Usage: do_mv_ln [options] <src> <dst> +# Files have to be the last two args, and has to be +# files so we can handle symlinked target sanely. +do_mv_ln() { + local opts=( ${@:1:$(( $# - 2 ))} ) + local src=${@:$(( $# - 1 )):1} + local dst=${@:$(( $# - 0 )):1} + + if [[ ! -L ${src} && -L ${dst} ]] ; then #330221 + local lfile=$(readlink "${dst}") + [[ ${lfile} == /* ]] || lfile="${dst%/*}/${lfile}" + echo " Target is a symlink; replacing ${lfile}" + dst=${lfile} + elif [[ -d ${dst} && ! -L ${dst} ]] ; then + # If ${dst} is a directory, do not move the file + # inside of it if this fails. + rmdir "${dst}" || return + fi + + mv "${opts[@]}" "${src}" "${dst}" +} + +scan() { + ${QUIET} || echo "Scanning Configuration files..." + rm -rf "${TMP}"/files > /dev/null 2>&1 + mkdir "${TMP}"/files || die "Failed mkdir command!" + count=0 + input=0 + local find_opts + local path + + for path in ${SCAN_PATHS} ; do + path="${EROOT%/}${path}" + + if [[ ! -d ${path} ]] ; then + # Protect files that don't exist (bug #523684). If the + # parent directory doesn't exist, we can safely skip it. + path=${path%/} + [[ -d ${path%/*} ]] || continue + local name_opt=$(get_basename_find_opt "${path##*/}") + path="${path%/*}" + find_opts=( -maxdepth 1 ) + else + # Do not traverse hidden directories such as .svn or .git. + local name_opt=$(get_basename_find_opt '*') + find_opts=( -name '.*' -type d -prune -o ) + fi + ${case_insensitive} && \ + find_opts+=( -iname ) || find_opts+=( -name ) + find_opts+=( "$name_opt" ) + find_opts+=( ! -name '.*~' ! -iname '.*.bak' -print ) + + if [[ ! -w ${path} ]] ; then + [[ -e ${path} ]] || continue + die "Need write access to ${path}" + fi + + local file ofile b=$'\001' + local scan_regexp=$(get_scan_regexp) + for file in $(find "${path}"/ "${find_opts[@]}" | + sed \ + -e 's://*:/:g' \ + -e "${scan_regexp}" | + sort -t"$b" -k2,2 -k4,4 -k3,3 | + LC_ALL=C cut -f1 -d"$b") + do + local rpath rfile cfg_file live_file + rpath=${file%/*} + rfile=${file##*/} + cfg_file="${rpath}/${rfile}" + live_file=$(get_live_file) + + local mpath + for mpath in ${CONFIG_PROTECT_MASK}; do + mpath="${EROOT%/}${mpath}" + if [[ "${rpath}" == "${mpath}"* ]] ; then + ${QUIET} || echo "Updating masked file: ${live_file}" + mv "${cfg_file}" "${live_file}" + continue 2 + fi + done + if [[ -L ${file} ]] ; then + if [[ -L ${live_file} && \ + $(readlink "${live_file}") == $(readlink "${file}") ]] + then + rm -f "${file}" + continue + fi + if [[ $(get_basename "${ofile}") != $(get_basename "${rfile}") ]] || + [[ ${opath} != ${rpath} ]] + then + : $(( ++count )) + echo "${live_file}" > "${TMP}"/files/${count} + fi + echo "${cfg_file}" >> "${TMP}"/files/${count} + ofile="${rfile}" + opath="${rpath}" + continue + fi + if [[ ! -f ${file} ]] ; then + ${QUIET} || echo "Skipping non-file ${file} ..." + continue + fi + + if [[ $(get_basename "${ofile}") != $(get_basename "${rfile}") ]] || + [[ ${opath} != ${rpath} ]] + then + MATCHES=0 + if ! [[ -f ${cfg_file} && -f ${live_file} ]] ; then + MATCHES=0 + elif [[ ${eu_automerge} == "yes" ]] ; then + if [[ ! -e ${cfg_file} || ! -e ${live_file} ]] ; then + MATCHES=0 + else + diff -Bbua "${cfg_file}" "${live_file}" | \ + sed -n -r \ + -e '/^[+-]/{/^([+-][\t ]*(#|$)|-{3} |\+{3} )/d;q1}' + : $(( MATCHES = ($? == 0) )) + fi + + else + diff -Nbua "${cfg_file}" "${live_file}" | + sed -n \ + -e '/# .Header:/d' \ + -e '/^[+-][^+-]/q1' + : $(( MATCHES = ($? == 0) )) + fi + + if [[ ${MATCHES} == 1 ]] ; then + ${QUIET} || echo "Automerging trivial changes in: ${live_file}" + do_mv_ln "${cfg_file}" "${live_file}" + continue + else + : $(( ++count )) + echo "${live_file}" > "${TMP}"/files/${count} + echo "${cfg_file}" >> "${TMP}"/files/${count} + ofile="${rfile}" + opath="${rpath}" + continue + fi + fi + + if ! diff -Nbua "${cfg_file}" "${rpath}/${ofile}" | + sed -n \ + -e '/# .Header:/d' \ + -e '/^[+-][^+-]/q1' + then + echo "${cfg_file}" >> "${TMP}"/files/${count} + ofile="${rfile}" + opath="${rpath}" + else + mv "${cfg_file}" "${rpath}/${ofile}" + continue + fi + done + done +} + +parse_automode_flag() { + case $1 in + -9) + local reply + read -p "Are you sure that you want to delete all updates (type YES): " reply + if [[ ${reply} != "YES" ]] ; then + echo "Did not get a 'YES', so ignoring request" + return 1 + else + parse_automode_flag -7 + export rm_opts="" + fi + ;; + -7) + input=0 + export DELETE_ALL="yes" + ;; + -5) + parse_automode_flag -3 + export mv_opts=" ${mv_opts} " + mv_opts="${mv_opts// -i / }" + NONINTERACTIVE_MV=true + ;; + -3) + input=0 + export OVERWRITE_ALL="yes" + ;; + *) + return 1 + ;; + esac + return 0 +} + +sel_file() { + local -i isfirst=0 + until [[ -f ${TMP}/files/${input} ]] || \ + [[ ${input} == -1 ]] || \ + [[ ${input} == -3 ]] + do + local allfiles=( $(cd "${TMP}"/files/ && printf '%s\n' * | sort -n) ) + local isfirst=${allfiles[0]} + + # Optimize: no point in building the whole file list if + # we're not actually going to talk to the user. + if [[ ${OVERWRITE_ALL} == "yes" || ${DELETE_ALL} == "yes" ]] ; then + input=0 + else + local numfiles=${#allfiles[@]} + local numwidth=${#numfiles} + local file fullfile line + for file in "${allfiles[@]}" ; do + fullfile="${TMP}/files/${file}" + line=$(head -n1 "${fullfile}") + printf '%*i%s %s' ${numwidth} ${file} "${PAR}" "${line}" + if [[ ${mode} == 0 ]] ; then + local numupdates=$(( $(wc -l <"${fullfile}") - 1 )) + echo " (${numupdates})" + else + echo + fi + done > "${TMP}"/menuitems + + clear + + if [[ ${mode} == 0 ]] ; then + cat <<-EOF + The following is the list of files which need updating, each + configuration file is followed by a list of possible replacement files. + $(<"${TMP}"/menuitems) + Please select a file to edit by entering the corresponding number. + (don't use -3, -5, -7 or -9 if you're unsure what to do) + (-1 to exit) (${_3_HELP_TEXT}) + (${_5_HELP_TEXT}) + (${_7_HELP_TEXT}) + EOF + printf " (${_9_HELP_TEXT}): " + input=$(read_int) + else + dialog \ + --title "${title}" \ + --menu "Please select a file to update" \ + 0 0 0 $(<"${TMP}"/menuitems) \ + 2> "${TMP}"/input \ + || die "$(<"${TMP}"/input)\n\nUser termination!" 0 + input=$(<"${TMP}"/input) + fi + : ${input:=0} + + if [[ ${input} != 0 ]] ; then + parse_automode_flag ${input} || continue + fi + fi # -3 automerge + if [[ ${input} == 0 ]] ; then + input=${isfirst} + fi + done +} + +user_special() { + local special="${PORTAGE_CONFIGROOT}etc/etc-update.special" + + if [[ -r ${special} ]] ; then + if [[ -z $1 ]] ; then + error "user_special() called without arguments" + return 1 + fi + local pat + while read -r pat ; do + echo "$1" | grep -q "${pat}" && return 0 + done < "${special}" + fi + return 1 +} + +read_int() { + # Read an integer from stdin. Continously loops until a valid integer is + # read. This is a workaround for odd behavior of bash when an attempt is + # made to store a value such as "1y" into an integer-only variable. + local my_input + while : ; do + read my_input + # failed integer conversions will break a loop unless they're enclosed + # in a subshell. + echo "${my_input}" | (declare -i x; read x) 2>/dev/null && break + printf 'Value "%s" is not valid. Please enter an integer value: ' "${my_input}" >&2 + done + echo ${my_input} +} + +do_file() { + interactive_echo() { [[ ${OVERWRITE_ALL} != yes ]] && [[ ${DELETE_ALL} != yes ]] && echo; } + interactive_echo + local -i my_input + local -i linecnt + local fullfile="${TMP}/files/${input}" + local ofile=$(head -n1 "${fullfile}") + + # Walk through all the pending updates for this one file. + linecnt=$(wc -l <"${fullfile}") + while (( linecnt > 1 )) ; do + if (( linecnt == 2 )) ; then + # Only one update ... keeps things simple. + my_input=1 + else + my_input=0 + fi + + # Optimize: no point in scanning the file list when we know + # we're just going to consume all the ones available. + if [[ ${OVERWRITE_ALL} == "yes" || ${DELETE_ALL} == "yes" ]] ; then + my_input=1 + fi + + # Figure out which file they wish to operate on. + while (( my_input <= 0 || my_input >= linecnt )) ; do + local fcount=0 + for line in $(<"${fullfile}"); do + if (( fcount > 0 )); then + printf '%i%s %s\n' ${fcount} "${PAR}" "${line}" + fi + : $(( ++fcount )) + done > "${TMP}"/menuitems + + if [[ ${mode} == 0 ]] ; then + echo "Below are the new config files for ${ofile}:" + cat "${TMP}"/menuitems + echo -n "Please select a file to process (-1 to exit this file): " + my_input=$(read_int) + else + dialog \ + --title "${title}" \ + --menu "Please select a file to process for ${ofile}" \ + 0 0 0 $(<"${TMP}"/menuitems) \ + 2> "${TMP}"/input \ + || die "$(<"${TMP}"/input)\n\nUser termination!" 0 + my_input=$(<"${TMP}"/input) + fi + + if [[ ${my_input} == 0 ]] ; then + # Auto select the first file. + my_input=1 + elif [[ ${my_input} == -1 ]] ; then + input=0 + return + fi + done + + # First line is the old file while the rest are the config files. + : $(( ++my_input )) + local file=$(sed -n -e "${my_input}p" "${fullfile}") + do_cfg "${file}" "${ofile}" + + sed -i -e "${my_input}d" "${fullfile}" + + : $(( --linecnt )) + done + + interactive_echo + rm "${fullfile}" + : $(( --count )) +} + +show_diff() { + clear + local file1=$1 file2=$2 files=("$1" "$2") \ + diff_files=() file i tmpdir + + if [[ -L ${file1} && ! -L ${file2} && + -f ${file1} && -f ${file2} ]] ; then + # If a regular file replaces a symlink to a regular file, then + # show the diff between the regular files (bug #330221). + diff_files=("${file1}" "${file2}") + else + for i in 0 1 ; do + if [[ ! -L ${files[$i]} && -f ${files[$i]} ]] ; then + diff_files[$i]=${files[$i]} + continue + fi + [[ -n ${tmpdir} ]] || \ + tmpdir=$(mktemp -d "${TMP}/symdiff-XXX") + diff_files[$i]=${tmpdir}/${i} + if [[ ! -L ${files[$i]} && ! -e ${files[$i]} ]] ; then + echo "/dev/null" > "${diff_files[$i]}" + elif [[ -L ${files[$i]} ]] ; then + echo "SYM: ${file1} -> $(readlink "${files[$i]}")" > \ + "${diff_files[$i]}" + elif [[ -d ${files[$i]} ]] ; then + echo "DIR: ${file1}" > "${diff_files[$i]}" + elif [[ -p ${files[$i]} ]] ; then + echo "FIF: ${file1}" > "${diff_files[$i]}" + else + echo "DEV: ${file1}" > "${diff_files[$i]}" + fi + done + fi + + if [[ ${using_editor} == 0 ]] ; then + ( + echo "Showing differences between ${file1} and ${file2}" + diff_command "${diff_files[0]}" "${diff_files[1]}" + ) | ${pager} + else + echo "Beginning of differences between ${file1} and ${file2}" + diff_command "${diff_files[0]}" "${diff_files[1]}" + echo "End of differences between ${file1} and ${file2}" + fi + + [[ -n ${tmpdir} ]] && rm -rf "${tmpdir}" +} + +do_cfg() { + local file=$1 + local ofile=$2 + local -i my_input=0 + + until (( my_input == -1 )) || [[ ! -f ${file} && ! -L ${file} ]] ; do + if [[ "${OVERWRITE_ALL}" == "yes" ]] && ! user_special "${ofile}"; then + my_input=1 + elif [[ "${DELETE_ALL}" == "yes" ]] && ! user_special "${ofile}"; then + my_input=2 + else + show_diff "${ofile}" "${file}" + if [[ -L ${file} && ! -L ${ofile} ]] ; then + cat <<-EOF + + ------------------------------------------------------------- + NOTE: File is a symlink to another file. REPLACE recommended. + The original file may simply have moved. Please review. + ------------------------------------------------------------- + + EOF + fi + cat <<-EOF + + File: ${file} + 1) Replace original with update + 2) Delete update, keeping original as is + 3) Interactively merge original with update + 4) Show differences again + 5) Save update as example config + EOF + printf 'Please select from the menu above (-1 to ignore this update): ' + my_input=$(read_int) + fi + + case ${my_input} in + 1) echo "Replacing ${ofile} with ${file}" + do_mv_ln ${mv_opts} "${file}" "${ofile}" + [[ -n ${OVERWRITE_ALL} ]] && my_input=-1 + continue + ;; + 2) echo "Deleting ${file}" + rm ${rm_opts} "${file}" + [[ -n ${DELETE_ALL} ]] && my_input=-1 + continue + ;; + 3) do_merge "${file}" "${ofile}" + my_input=${?} +# [[ ${my_input} == 255 ]] && my_input=-1 + continue + ;; + 4) continue + ;; + 5) do_distconf "${file}" "${ofile}" + ;; + *) continue + ;; + esac + done +} + +do_merge() { + # make sure we keep the merged file in the secure tempdir + # so we dont leak any information contained in said file + # (think of case where the file has 0600 perms; during the + # merging process, the temp file gets umask perms!) + + local file="${1}" + local ofile="${2}" + local mfile="${TMP}/${2#/}.merged" + local -i my_input=0 + + if [[ -L ${file} && -L ${ofile} ]] ; then + echo "Both files are symlinks, so they will not be merged." + return 0 + elif [[ ! -f ${file} ]] ; then + echo "Non-regular file cannot be merged: ${file}" + return 0 + elif [[ ! -f ${ofile} ]] ; then + echo "Non-regular file cannot be merged: ${ofile}" + return 0 + fi + + + echo "${file} ${ofile} ${mfile}" + + if [[ -e ${mfile} ]] ; then + echo "A previous version of the merged file exists, cleaning..." + rm ${rm_opts} "${mfile}" + fi + + # since mfile will be like $TMP/path/to/original-file.merged, we + # need to make sure the full /path/to/ exists ahead of time + mkdir -p "${mfile%/*}" + + until (( my_input == -1 )); do + echo "Merging ${file} and ${ofile}" + $(echo "${merge_command}" | + sed -e "s:%merged:${mfile}:g" \ + -e "s:%orig:${ofile}:g" \ + -e "s:%new:${file}:g") + until (( my_input == -1 )); do + cat <<-EOF + 1) Replace ${ofile} with merged file + 2) Show differences between merged file and original + 3) Remerge original with update + 4) Edit merged file + 5) Return to the previous menu + EOF + printf 'Please select from the menu above (-1 to exit, losing this merge): ' + my_input=$(read_int) + case ${my_input} in + 1) echo "Replacing ${ofile} with ${mfile}" + if [[ ${USERLAND} == BSD ]] ; then + chown "$(stat -f %Su:%Sg "${ofile}")" "${mfile}" + chmod $(stat -f %Mp%Lp "${ofile}") "${mfile}" + else + chown --reference="${ofile}" "${mfile}" + chmod --reference="${ofile}" "${mfile}" + fi + do_mv_ln ${mv_opts} "${mfile}" "${ofile}" + rm ${rm_opts} "${file}" + return 255 + ;; + 2) show_diff "${ofile}" "${mfile}" + continue + ;; + 3) break + ;; + 4) ${EDITOR:-nano -w} "${mfile}" + continue + ;; + 5) rm ${rm_opts} "${mfile}" + return 0 + ;; + *) continue + ;; + esac + done + done + rm ${rm_opts} "${mfile}" + return 255 +} + +do_distconf() { + # search for any previously saved distribution config + # files and number the current one accordingly + + local file=$1 ofile=$2 + local -i count + local suffix + local efile + + for (( count = 0; count <= 9999; ++count )) ; do + suffix=$(printf ".dist_%04i" ${count}) + efile="${ofile}${suffix}" + if [[ ! -f ${efile} && ! -L ${efile} ]] ; then + mv ${mv_opts} "${file}" "${efile}" + break + elif [[ -L ${efile} && -L ${file} ]] ; then + if [[ $(readlink "${efile}") == $(readlink "${file}") ]] ; then + # replace identical copy + mv "${file}" "${efile}" + break + fi + elif [[ -L ${efile} || -L ${file} ]] ; then + # not the same file types + continue + else + local ret= + if [[ ${using_editor} == 0 ]] ; then + diff_command "${file}" "${efile}" &> /dev/null + ret=$? + else + # fall back to plain diff + diff -q "${file}" "${efile}" &> /dev/null + ret=$? + fi + if [[ ${ret} == 0 ]] ; then + # replace identical copy + mv "${file}" "${efile}" + break + fi + fi + done +} + +error() { echo "etc-update: ERROR: $*" 1>&2 ; return 1 ; } +die() { + trap SIGTERM + trap SIGINT + local msg=$1 exitcode=${2:-1} + + if [[ ${exitcode} -eq 0 ]] ; then + ${QUIET} || printf 'Exiting: %b\n' "${msg}" + scan > /dev/null + ! ${QUIET} && [[ ${count} -gt 0 ]] && echo "NOTE: ${count} updates remaining" + else + error "${msg}" + fi + + rm -rf "${TMP}" + exit ${exitcode} +} + +_3_HELP_TEXT="-3 to auto merge all files" +_5_HELP_TEXT="-5 to auto-merge AND not use 'mv -i'" +_7_HELP_TEXT="-7 to discard all updates" +_9_HELP_TEXT="-9 to discard all updates AND not use 'rm -i'" +usage() { + cat <<-EOF + etc-update: Handle configuration file updates + + Usage: etc-update [options] [paths to scan] + + If no paths are specified, then \${CONFIG_PROTECT} will be used. + + Options: + -d, --debug Enable shell debugging + -h, --help Show help and run away + -p, --preen Automerge trivial changes only and quit + -q, --quiet Show only essential output + -v, --verbose Show settings and such along the way + -V, --version Show version and trundle away + + --automode <mode> + ${_3_HELP_TEXT} + ${_5_HELP_TEXT} + ${_7_HELP_TEXT} + ${_9_HELP_TEXT} + EOF + + [[ $# -gt 1 ]] && printf "\nError: %s\n" "${*:2}" 1>&2 + + exit ${1:-0} +} + +# +# Run the script +# + +declare -i count=0 +declare input=0 +declare title="Gentoo's etc-update tool!" + +PREEN=false +SET_X=false +QUIET=false +VERBOSE=false +NONINTERACTIVE_MV=false +while [[ -n $1 ]] ; do + case $1 in + -d|--debug) SET_X=true;; + -h|--help) usage;; + -p|--preen) PREEN=true;; + -q|--quiet) QUIET=true;; + -v|--verbose) VERBOSE=true;; + -V|--version) emerge --version; exit 0;; + --automode) parse_automode_flag $2 && shift || usage 1 "Invalid mode '$2'";; + -*) usage 1 "Invalid option '$1'";; + *) break;; + esac + shift +done +${SET_X} && set -x + +if [[ $OS_FAMILY == 'rpm' ]]; then + PORTAGE_CONFIGROOT='/' + PORTAGE_TMPDIR='/tmp' + CONFIG_PROTECT='/etc /usr/share' + CONFIG_PROTECT_MASK='' + [[ -f /etc/sysconfig/etc-update ]] && . /etc/sysconfig/etc-update +elif [[ $OS_FAMILY == 'arch' ]]; then + PORTAGE_CONFIGROOT='/' + PORTAGE_TMPDIR='/tmp' + CONFIG_PROTECT='/etc /usr/lib /usr/share/config' + CONFIG_PROTECT_MASK='' +fi + +portage_vars=( + CONFIG_PROTECT{,_MASK} + FEATURES + PORTAGE_CONFIGROOT + PORTAGE_INST_{G,U}ID + PORTAGE_TMPDIR + EROOT + USERLAND + NOCOLOR +) + +if type -P portageq > /dev/null; then + eval $(${PORTAGE_PYTHON:+"${PORTAGE_PYTHON}"} "$(type -P portageq)" envvar -v "${portage_vars[@]}") +else + [[ $OS_FAMILY == 'gentoo' ]] && die "missing portageq" +fi + +export PORTAGE_TMPDIR +SCAN_PATHS=${*:-${CONFIG_PROTECT}} +[[ " ${FEATURES} " == *" case-insensitive-fs "* ]] && \ + case_insensitive=true || case_insensitive=false + +TMP="${PORTAGE_TMPDIR}/etc-update-$$" +trap "die terminated" SIGTERM +trap "die interrupted" SIGINT + +rm -rf "${TMP}" 2>/dev/null +mkdir "${TMP}" || die "failed to create temp dir" +# make sure we have a secure directory to work in +chmod 0700 "${TMP}" || die "failed to set perms on temp dir" +chown ${PORTAGE_INST_UID:-0}:${PORTAGE_INST_GID:-0} "${TMP}" || \ + die "failed to set ownership on temp dir" + +# Get all the user settings from etc-update.conf +cfg_vars=( + clear_term + eu_automerge + rm_opts + mv_opts + pager + diff_command + using_editor + merge_command + mode +) +# default them all to "" +eval "${cfg_vars[@]/%/=}" +# then extract them all from the conf in one shot +# (ugly var at end is due to printf appending a '|' to last item) +get_config "($(printf '%s|' "${cfg_vars[@]}")NOVARFOROLDMEN)" + +# finally setup any specific defaults +: ${mode:="0"} +if ! cmd_var_is_valid "${pager}" ; then + pager=${PAGER} + cmd_var_is_valid "${pager}" || pager=cat +fi + +[[ ${clear_term} == "yes" ]] || clear() { :; } + +if [[ ${using_editor} == "0" ]] ; then + # Sanity check to make sure diff exists and works + echo > "${TMP}"/.diff-test-1 + echo > "${TMP}"/.diff-test-2 + + if ! diff_command "${TMP}"/.diff-test-1 "${TMP}"/.diff-test-2 ; then + die "'${diff_command}' does not seem to work, aborting" + fi +else + # NOTE: cmd_var_is_valid doesn't work with diff_command="eval emacs..." + # because it uses type -P. + if ! type ${diff_command%%[[:space:]]*} >/dev/null; then + die "'${diff_command}' does not seem to work, aborting" + fi +fi + +if [[ ${mode} == "0" ]] ; then + PAR=")" +else + PAR="" + if ! type dialog >/dev/null || ! dialog --help >/dev/null ; then + die "mode=1 and 'dialog' not found or not executable, aborting" + fi +fi + +if ${NONINTERACTIVE_MV} ; then + export mv_opts=" ${mv_opts} " + mv_opts="${mv_opts// -i / }" +fi + +if ${VERBOSE} ; then + for v in "${portage_vars[@]}" "${cfg_vars[@]}" TMP SCAN_PATHS ; do + echo "${v}=${!v}" + done +fi + +scan + +${PREEN} && exit 0 + +until (( input == -1 )); do + if (( count == 0 )); then + die "Nothing left to do; exiting. :)" 0 + fi + sel_file + if (( input != -1 )); then + do_file + fi +done + +die "User termination!" 0 diff --git a/etc-update.conf b/etc-update.conf new file mode 100644 index 000000000000..970986251111 --- /dev/null +++ b/etc-update.conf @@ -0,0 +1,82 @@ +# /etc/etc-update.conf: config file for `etc-update` utility +# edit the lines below to your liking + +# mode - 0 for text, 1 for menu (support incomplete) +# note that you need dev-util/dialog installed +mode="0" + +# Whether to clear the term prior to each display +#clear_term="yes" +clear_term="no" + +# Whether trivial/comment changes should be automerged +eu_automerge="yes" + +# arguments used whenever rm is called +rm_opts="-i" + +# arguments used whenever mv is called +mv_opts="-i" + +# arguments used whenever cp is called +cp_opts="-i" + +# set the pager for use with diff commands (this will +# cause the PAGER environment variable to be ignored) +#pager="less" + +# For emacs-users (see NOTE_2) +# diff_command="eval emacs -nw --eval=\'\(ediff\ \"%file1\"\ \"%file2\"\)\'" +#using_editor=1 + +# vim-users: you CAN use vimdiff for diff_command. (see NOTE_1 and NOTE_2) +#diff_command="vim -d %file1 %file2" +#using_editor=1 + +# If using colordiff instead of diff, the less -R option may be required +# for correct display (see 'pager' setting above). +diff_command="diff -uN %file1 %file2" +using_editor=0 + + +# vim-users: don't use vimdiff for merging (see NOTE_1) +merge_command="sdiff -s -o %merged %orig %new" + +# EXPLANATION +# +# pager: +# +# Examples of pager usage: +# pager="cat" # don't use a pager +# pager="less -E" # less +# pager="more" # more +# +# +# diff_command: +# +# Arguments: +# %file1 [REQUIRED] +# %file2 [REQUIRED] +# +# Examples of diff_command: +# diff_command="diff -uN %file1 %file2" # diff +# diff_command="vim -d %file1 %file2" # vimdiff +# +# +# merge_command: +# +# Arguments: +# %orig [REQUIRED] +# %new [REQUIRED] +# %merged [REQUIRED] +# +# Examples of merge_command: +# merge_command="sdiff -s -o %merged %old %new" # sdiff +# + +# NOTE_1: Editors such as vim/vimdiff are not usable for the merge_command +# because it is not known what filenames the produced files have (the user can +# choose while using those programs) + +# NOTE_2: Make sure using_editor is set to "1" when using an editor as +# diff_command! |