2 # Copyright 1999-2012 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
5 # Author Brandon Low <lostlogic@gentoo.org>
6 # Mike Frysinger <vapier@gentoo.org>
8 # Previous version (from which I've borrowed a few bits) by:
9 # Jochem Kossen <j.kossen@home.nl>
10 # Leo Lipelis <aeoo@gentoo.org>
11 # Karl Trygve Kalleberg <karltk@gentoo.org>
15 type -P gsed >/dev/null && sed() { gsed "$@"; }
19 # - strip off comments
20 # - match lines that set item in question
21 # - delete the "item =" part
22 # - store the actual value into the hold space
23 # - on the last line, restore the hold space and print it
24 # If there's more than one of the same configuration item, then
25 # the store to the hold space clobbers previous value so the last
26 # setting takes precedence.
29 -e 's:[[:space:]]*#.*$::' \
30 -e "/^[[:space:]]*${match}[[:space:]]*=/{s:^([^=]*)=[[:space:]]*([\"']{0,1})(.*)\2:\1=\2\3\2:;H}" \
32 "${PORTAGE_CONFIGROOT}"etc/etc-update.conf)
36 # return true if the first whitespace-separated token contained
37 # in "${1}" is an executable file, false otherwise
38 [[ -x $(type -P ${1%%[[:space:]]*}) ]]
42 local cmd=${diff_command//%file1/$1}
46 # Usage: do_mv_ln [options] <src> <dst>
47 # Files have to be the last two args, and has to be
48 # files so we can handle symlinked target sanely.
50 local opts=( ${@:1:$(( $# - 2 ))} )
51 local src=${@:$(( $# - 1 )):1}
52 local dst=${@:$(( $# - 0 )):1}
54 if [[ -L ${dst} ]] ; then #330221
55 local lfile=$(readlink "${dst}")
56 [[ ${lfile} == /* ]] || lfile="${dst%/*}/${lfile}"
57 echo " Target is a symlink; replacing ${lfile}"
61 mv "${opts[@]}" "${src}" "${dst}"
65 ${QUIET} || echo "Scanning Configuration files..."
66 rm -rf "${TMP}"/files > /dev/null 2>&1
67 mkdir "${TMP}"/files || die "Failed mkdir command!"
73 for path in ${SCAN_PATHS} ; do
74 path="${EROOT%/}${path}"
76 if [[ ! -d ${path} ]] ; then
77 [[ ! -f ${path} ]] && continue
78 local my_basename="${path##*/}"
80 find_opts=( -maxdepth 1 -name "._cfg????_${my_basename}" )
82 # Do not traverse hidden directories such as .svn or .git.
83 find_opts=( -name '.*' -type d -prune -o -name '._cfg????_*' )
85 find_opts+=( ! -name '.*~' ! -iname '.*.bak' -print )
87 if [ ! -w "${path}" ] ; then
88 [ -e "${path}" ] || continue
89 die "Need write access to ${path}"
92 local file ofile b=$'\001'
93 for file in $(find "${path}"/ "${find_opts[@]}" |
96 -e "s:\(^.*/\)\(\._cfg[0-9]*_\)\(.*$\):\1\2\3$b\1$b\2$b\3:" |
97 sort -t"$b" -k2,2 -k4,4 -k3,3 |
98 LC_ALL=C cut -f1 -d"$b")
100 local rpath rfile cfg_file live_file
103 cfg_file="${rpath}/${rfile}"
104 live_file="${rpath}/${rfile:10}"
107 for mpath in ${CONFIG_PROTECT_MASK}; do
108 mpath="${EROOT%/}${mpath}"
109 if [[ "${rpath}" == "${mpath}"* ]] ; then
110 ${QUIET} || echo "Updating masked file: ${live_file}"
111 mv "${cfg_file}" "${live_file}"
115 if [[ ! -f ${file} ]] ; then
116 ${QUIET} || echo "Skipping non-file ${file} ..."
120 if [[ "${ofile:10}" != "${rfile:10}" ]] ||
121 [[ ${opath} != ${rpath} ]]
124 if [[ ${eu_automerge} == "yes" ]] ; then
125 if [[ ! -e ${cfg_file} || ! -e ${live_file} ]] ; then
128 diff -Bbua "${cfg_file}" "${live_file}" | \
130 -e '/^[+-]/{/^([+-][\t ]*(#|$)|-{3} |\+{3} )/d;q1}'
131 : $(( MATCHES = ($? == 0) ))
135 diff -Nbua "${cfg_file}" "${live_file}" |
139 : $(( MATCHES = ($? == 0) ))
142 if [[ ${MATCHES} == 1 ]] ; then
143 ${QUIET} || echo "Automerging trivial changes in: ${live_file}"
144 do_mv_ln "${cfg_file}" "${live_file}"
148 echo "${live_file}" > "${TMP}"/files/${count}
149 echo "${cfg_file}" >> "${TMP}"/files/${count}
156 if ! diff -Nbua "${cfg_file}" "${rpath}/${ofile}" |
161 echo "${cfg_file}" >> "${TMP}"/files/${count}
165 mv "${cfg_file}" "${rpath}/${ofile}"
172 parse_automode_flag() {
176 read -p "Are you sure that you want to delete all updates (type YES): " reply
177 if [[ ${reply} != "YES" ]] ; then
178 echo "Did not get a 'YES', so ignoring request"
181 parse_automode_flag -7
187 export DELETE_ALL="yes"
190 parse_automode_flag -3
191 export mv_opts=" ${mv_opts} "
192 mv_opts="${mv_opts// -i / }"
193 NONINTERACTIVE_MV=true
197 export OVERWRITE_ALL="yes"
208 until [[ -f ${TMP}/files/${input} ]] || \
209 [[ ${input} == -1 ]] || \
212 local allfiles=( $(cd "${TMP}"/files/ && printf '%s\n' * | sort -n) )
213 local isfirst=${allfiles[0]}
215 # Optimize: no point in building the whole file list if
216 # we're not actually going to talk to the user.
217 if [[ ${OVERWRITE_ALL} == "yes" || ${DELETE_ALL} == "yes" ]] ; then
220 local numfiles=${#allfiles[@]}
221 local numwidth=${#numfiles}
222 local file fullfile line
223 for file in "${allfiles[@]}" ; do
224 fullfile="${TMP}/files/${file}"
225 line=$(head -n1 "${fullfile}")
226 printf '%*i%s %s' ${numwidth} ${file} "${PAR}" "${line}"
227 if [[ ${mode} == 0 ]] ; then
228 local numupdates=$(( $(wc -l <"${fullfile}") - 1 ))
229 echo " (${numupdates})"
233 done > "${TMP}"/menuitems
237 if [[ ${mode} == 0 ]] ; then
239 The following is the list of files which need updating, each
240 configuration file is followed by a list of possible replacement files.
241 $(<"${TMP}"/menuitems)
242 Please select a file to edit by entering the corresponding number.
243 (don't use -3, -5, -7 or -9 if you're unsure what to do)
244 (-1 to exit) (${_3_HELP_TEXT})
248 printf " (${_9_HELP_TEXT}): "
253 --menu "Please select a file to update" \
254 0 0 0 $(<"${TMP}"/menuitems) \
256 || die "$(<"${TMP}"/input)\n\nUser termination!" 0
257 input=$(<"${TMP}"/input)
261 if [[ ${input} != 0 ]] ; then
262 parse_automode_flag ${input} || continue
265 if [[ ${input} == 0 ]] ; then
272 local special="${PORTAGE_CONFIGROOT}etc/etc-update.special"
274 if [[ -r ${special} ]] ; then
275 if [[ -z $1 ]] ; then
276 error "user_special() called without arguments"
280 while read -r pat ; do
281 echo "$1" | grep -q "${pat}" && return 0
288 # Read an integer from stdin. Continously loops until a valid integer is
289 # read. This is a workaround for odd behavior of bash when an attempt is
290 # made to store a value such as "1y" into an integer-only variable.
294 # failed integer conversions will break a loop unless they're enclosed
296 echo "${my_input}" | (declare -i x; read x) 2>/dev/null && break
297 printf 'Value "%s" is not valid. Please enter an integer value: ' "${my_input}" >&2
303 interactive_echo() { [ "${OVERWRITE_ALL}" != "yes" ] && [ "${DELETE_ALL}" != "yes" ] && echo; }
307 local fullfile="${TMP}/files/${input}"
308 local ofile=$(head -n1 "${fullfile}")
310 # Walk through all the pending updates for this one file.
311 linecnt=$(wc -l <"${fullfile}")
312 while (( linecnt > 1 )) ; do
313 if (( linecnt == 2 )) ; then
314 # Only one update ... keeps things simple.
320 # Optimize: no point in scanning the file list when we know
321 # we're just going to consume all the ones available.
322 if [[ ${OVERWRITE_ALL} == "yes" || ${DELETE_ALL} == "yes" ]] ; then
326 # Figure out which file they wish to operate on.
327 while (( my_input <= 0 || my_input >= linecnt )) ; do
329 for line in $(<"${fullfile}"); do
330 if (( fcount > 0 )); then
331 printf '%i%s %s\n' ${fcount} "${PAR}" "${line}"
334 done > "${TMP}"/menuitems
336 if [[ ${mode} == 0 ]] ; then
337 echo "Below are the new config files for ${ofile}:"
338 cat "${TMP}"/menuitems
339 echo -n "Please select a file to process (-1 to exit this file): "
344 --menu "Please select a file to process for ${ofile}" \
345 0 0 0 $(<"${TMP}"/menuitems) \
347 || die "$(<"${TMP}"/input)\n\nUser termination!" 0
348 my_input=$(<"${TMP}"/input)
351 if [[ ${my_input} == 0 ]] ; then
352 # Auto select the first file.
354 elif [[ ${my_input} == -1 ]] ; then
360 # First line is the old file while the rest are the config files.
362 local file=$(sed -n -e "${my_input}p" "${fullfile}")
363 do_cfg "${file}" "${ofile}"
365 sed -i -e "${my_input}d" "${fullfile}"
377 local file1=$1 file2=$2
378 if [[ ${using_editor} == 0 ]] ; then
380 echo "Showing differences between ${file1} and ${file2}"
381 diff_command "${file1}" "${file2}"
384 echo "Beginning of differences between ${file1} and ${file2}"
385 diff_command "${file1}" "${file2}"
386 echo "End of differences between ${file1} and ${file2}"
395 until (( my_input == -1 )) || [ ! -f "${file}" ] ; do
396 if [[ "${OVERWRITE_ALL}" == "yes" ]] && ! user_special "${ofile}"; then
398 elif [[ "${DELETE_ALL}" == "yes" ]] && ! user_special "${ofile}"; then
401 show_diff "${ofile}" "${file}"
402 if [[ -L ${file} ]] ; then
405 -------------------------------------------------------------
406 NOTE: File is a symlink to another file. REPLACE recommended.
407 The original file may simply have moved. Please review.
408 -------------------------------------------------------------
415 1) Replace original with update
416 2) Delete update, keeping original as is
417 3) Interactively merge original with update
418 4) Show differences again
419 5) Save update as example config
421 printf 'Please select from the menu above (-1 to ignore this update): '
426 1) echo "Replacing ${ofile} with ${file}"
427 do_mv_ln ${mv_opts} "${file}" "${ofile}"
428 [ -n "${OVERWRITE_ALL}" ] && my_input=-1
431 2) echo "Deleting ${file}"
432 rm ${rm_opts} "${file}"
433 [ -n "${DELETE_ALL}" ] && my_input=-1
436 3) do_merge "${file}" "${ofile}"
438 # [ ${my_input} == 255 ] && my_input=-1
443 5) do_distconf "${file}" "${ofile}"
452 # make sure we keep the merged file in the secure tempdir
453 # so we dont leak any information contained in said file
454 # (think of case where the file has 0600 perms; during the
455 # merging process, the temp file gets umask perms!)
459 local mfile="${TMP}/${2}.merged"
461 echo "${file} ${ofile} ${mfile}"
463 if [[ -e ${mfile} ]] ; then
464 echo "A previous version of the merged file exists, cleaning..."
465 rm ${rm_opts} "${mfile}"
468 # since mfile will be like $TMP/path/to/original-file.merged, we
469 # need to make sure the full /path/to/ exists ahead of time
470 mkdir -p "${mfile%/*}"
472 until (( my_input == -1 )); do
473 echo "Merging ${file} and ${ofile}"
474 $(echo "${merge_command}" |
475 sed -e "s:%merged:${mfile}:g" \
476 -e "s:%orig:${ofile}:g" \
477 -e "s:%new:${file}:g")
478 until (( my_input == -1 )); do
480 1) Replace ${ofile} with merged file
481 2) Show differences between merged file and original
482 3) Remerge original with update
484 5) Return to the previous menu
486 printf 'Please select from the menu above (-1 to exit, losing this merge): '
489 1) echo "Replacing ${ofile} with ${mfile}"
490 if [[ ${USERLAND} == BSD ]] ; then
491 chown "$(stat -f %Su:%Sg "${ofile}")" "${mfile}"
492 chmod $(stat -f %Mp%Lp "${ofile}") "${mfile}"
494 chown --reference="${ofile}" "${mfile}"
495 chmod --reference="${ofile}" "${mfile}"
497 do_mv_ln ${mv_opts} "${mfile}" "${ofile}"
498 rm ${rm_opts} "${file}"
501 2) show_diff "${ofile}" "${mfile}"
506 4) ${EDITOR:-nano -w} "${mfile}"
509 5) rm ${rm_opts} "${mfile}"
517 rm ${rm_opts} "${mfile}"
522 # search for any previously saved distribution config
523 # files and number the current one accordingly
525 local file=$1 ofile=$2
530 for (( count = 0; count <= 9999; ++count )) ; do
531 suffix=$(printf ".dist_%04i" ${count})
532 efile="${ofile}${suffix}"
533 if [[ ! -f ${efile} ]] ; then
534 mv ${mv_opts} "${file}" "${efile}"
536 elif diff_command "${file}" "${efile}" &> /dev/null; then
537 # replace identical copy
538 mv "${file}" "${efile}"
544 error() { echo "etc-update: ERROR: $*" 1>&2 ; return 1 ; }
548 local msg=$1 exitcode=${2:-1}
550 if [ ${exitcode} -eq 0 ] ; then
551 ${QUIET} || printf 'Exiting: %b\n' "${msg}"
553 ! ${QUIET} && [ ${count} -gt 0 ] && echo "NOTE: ${count} updates remaining"
562 _3_HELP_TEXT="-3 to auto merge all files"
563 _5_HELP_TEXT="-5 to auto-merge AND not use 'mv -i'"
564 _7_HELP_TEXT="-7 to discard all updates"
565 _9_HELP_TEXT="-9 to discard all updates AND not use 'rm -i'"
568 etc-update: Handle configuration file updates
570 Usage: etc-update [options] [paths to scan]
572 If no paths are specified, then \${CONFIG_PROTECT} will be used.
575 -d, --debug Enable shell debugging
576 -h, --help Show help and run away
577 -p, --preen Automerge trivial changes only and quit
578 -q, --quiet Show only essential output
579 -v, --verbose Show settings and such along the way
580 -V, --version Show version and trundle away
589 [[ $# -gt 1 ]] && printf "\nError: %s\n" "${*:2}" 1>&2
600 declare title="Gentoo's etc-update tool!"
606 NONINTERACTIVE_MV=false
607 while [[ -n $1 ]] ; do
609 -d|--debug) SET_X=true;;
611 -p|--preen) PREEN=true;;
612 -q|--quiet) QUIET=true;;
613 -v|--verbose) VERBOSE=true;;
614 -V|--version) emerge --version; exit 0;;
615 --automode) parse_automode_flag $2 && shift || usage 1 "Invalid mode '$2'";;
616 -*) usage 1 "Invalid option '$1'";;
623 type -P portageq >/dev/null || die "missing portageq"
625 CONFIG_PROTECT{,_MASK}
633 eval $(${PORTAGE_PYTHON:+"${PORTAGE_PYTHON}"} "$(type -P portageq)" envvar -v ${portage_vars[@]})
634 export PORTAGE_TMPDIR
635 SCAN_PATHS=${*:-${CONFIG_PROTECT}}
637 TMP="${PORTAGE_TMPDIR}/etc-update-$$"
638 trap "die terminated" SIGTERM
639 trap "die interrupted" SIGINT
641 rm -rf "${TMP}" 2>/dev/null
642 mkdir "${TMP}" || die "failed to create temp dir"
643 # make sure we have a secure directory to work in
644 chmod 0700 "${TMP}" || die "failed to set perms on temp dir"
645 chown ${PORTAGE_INST_UID:-0}:${PORTAGE_INST_GID:-0} "${TMP}" || \
646 die "failed to set ownership on temp dir"
648 # Get all the user settings from etc-update.conf
660 # default them all to ""
661 eval ${cfg_vars[@]/%/=}
662 # then extract them all from the conf in one shot
663 # (ugly var at end is due to printf appending a '|' to last item)
664 get_config "($(printf '%s|' "${cfg_vars[@]}")NOVARFOROLDMEN)"
666 # finally setup any specific defaults
668 if ! cmd_var_is_valid "${pager}" ; then
670 cmd_var_is_valid "${pager}" || pager=cat
673 [[ ${clear_term} == "yes" ]] || clear() { :; }
675 if [[ ${using_editor} == "0" ]] ; then
676 # Sanity check to make sure diff exists and works
677 echo > "${TMP}"/.diff-test-1
678 echo > "${TMP}"/.diff-test-2
680 if ! diff_command "${TMP}"/.diff-test-1 "${TMP}"/.diff-test-2 ; then
681 die "'${diff_command}' does not seem to work, aborting"
684 # NOTE: cmd_var_is_valid doesn't work with diff_command="eval emacs..."
685 # because it uses type -P.
686 if ! type ${diff_command%%[[:space:]]*} >/dev/null; then
687 die "'${diff_command}' does not seem to work, aborting"
691 if [[ ${mode} == "0" ]] ; then
695 if ! type dialog >/dev/null || ! dialog --help >/dev/null ; then
696 die "mode=1 and 'dialog' not found or not executable, aborting"
700 if ${NONINTERACTIVE_MV} ; then
701 export mv_opts=" ${mv_opts} "
702 mv_opts="${mv_opts// -i / }"
706 for v in ${portage_vars[@]} ${cfg_vars[@]} TMP SCAN_PATHS ; do
715 until (( input == -1 )); do
716 if (( count == 0 )); then
717 die "Nothing left to do; exiting. :)" 0
720 if (( input != -1 )); then
725 die "User termination!" 0