Remove redundant eprefix in config constructors.
[portage.git] / bin / etc-update
1 #!/bin/bash
2 # Copyright 1999-2011 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5 # Author Brandon Low <lostlogic@gentoo.org>
6 #
7 # Previous version (from which I've borrowed a few bits) by:
8 # Jochem Kossen <j.kossen@home.nl>
9 # Leo Lipelis <aeoo@gentoo.org>
10 # Karl Trygve Kalleberg <karltk@gentoo.org>
11
12 cd /
13
14 if type -P gsed >/dev/null ; then
15         sed() { gsed "$@"; }
16 fi
17
18 get_config() {
19         # the sed here does:
20         #  - strip off comments
21         #  - match lines that set item in question
22         #    - delete the "item =" part
23         #    - store the actual value into the hold space
24         #  - on the last line, restore the hold space and print it
25         # If there's more than one of the same configuration item, then
26         # the store to the hold space clobbers previous value so the last
27         # setting takes precedence.
28         local item=$1
29         eval echo $(sed -n \
30                 -e 's:[[:space:]]*#.*$::' \
31                 -e "/^[[:space:]]*$item[[:space:]]*=/{s:[^=]*=[[:space:]]*\([\"']\{0,1\}\)\(.*\)\1:\2:;h}" \
32                 -e '${g;p}' \
33                 "${PORTAGE_CONFIGROOT}"etc/etc-update.conf)
34 }
35
36 cmd_var_is_valid() {
37         # return true if the first whitespace-separated token contained
38         # in "${1}" is an executable file, false otherwise
39         [[ -x $(type -P ${1%%[[:space:]]*}) ]]
40 }
41
42 diff_command() {
43         local cmd=${diff_command//%file1/$1}
44         ${cmd//%file2/$2}
45 }
46
47 scan() {
48         echo "Scanning Configuration files..."
49         rm -rf ${TMP}/files > /dev/null 2>&1
50         mkdir ${TMP}/files || die "Failed mkdir command!" 1
51         count=0
52         input=0
53         local find_opts
54         local my_basename
55
56         for path in ${CONFIG_PROTECT} ; do
57                 path="${EROOT}${path}"
58                 # Do not traverse hidden directories such as .svn or .git.
59                 find_opts="-name .* -type d -prune -o -name ._cfg????_*"
60                 if [ ! -d "${path}" ]; then
61                         [ ! -f "${path}" ] && continue
62                         my_basename="${path##*/}"
63                         path="${path%/*}"
64                         find_opts="-maxdepth 1 -name ._cfg????_${my_basename}"
65                 fi
66
67                 ofile=""
68                 # The below set -f turns off file name globbing in the ${find_opts} expansion.
69                 for file in $(set -f ; find ${path}/ ${find_opts} \
70                        ! -name '.*~' ! -iname '.*.bak' -print |
71                            sed -e "s:\(^.*/\)\(\._cfg[0-9]*_\)\(.*$\):\1\2\3\%\1%\2\%\3:" |
72                            sort -t'%' -k2,2 -k4,4 -k3,3 | LANG=POSIX LC_ALL=POSIX cut -f1 -d'%'); do
73
74                         rpath=$(echo "${file/\/\///}" | sed -e "s:/[^/]*$::")
75                         rfile=$(echo "${file/\/\///}" | sed -e "s:^.*/::")
76                         for mpath in ${CONFIG_PROTECT_MASK}; do
77                                 mpath="${EROOT}${mpath}"
78                                 mpath=$(echo "${mpath/\/\///}")
79                                 if [[ "${rpath}" == "${mpath}"* ]]; then
80                                         mv ${rpath}/${rfile} ${rpath}/${rfile:10}
81                                         break
82                                 fi
83                         done
84                         if [[ ! -f ${file} ]] ; then
85                                 echo "Skipping non-file ${file} ..."
86                                 continue
87                         fi
88
89                         if [[ "${ofile:10}" != "${rfile:10}" ]] ||
90                            [[ ${opath} != ${rpath} ]]; then
91                                 MATCHES=0
92                                 if [[ "${EU_AUTOMERGE}" == "yes" ]]; then
93                                         if [ ! -e "${rpath}/${rfile}" ] || [ ! -e "${rpath}/${rfile:10}" ]; then
94                                                 MATCHES=0
95                                         else
96                                                 diff -Bbua ${rpath}/${rfile} ${rpath}/${rfile:10} | egrep '^[+-]' | egrep -v '^[+-][\t ]*#|^--- |^\+\+\+ ' | egrep -qv '^[-+][\t ]*$'
97                                                 MATCHES=$?
98                                         fi
99                                 elif [[ -z $(diff -Nua ${rpath}/${rfile} ${rpath}/${rfile:10}|
100                                                           grep "^[+-][^+-]"|grep -v '# .Header:.*') ]]; then
101                                         MATCHES=1
102                                 fi
103                                 if [[ "${MATCHES}" == "1" ]]; then
104                                         echo "Automerging trivial changes in: ${rpath}/${rfile:10}"
105                                         mv ${rpath}/${rfile} ${rpath}/${rfile:10}
106                                         continue
107                                 else
108                                         count=${count}+1
109                                         echo "${rpath}/${rfile:10}" > ${TMP}/files/${count}
110                                         echo "${rpath}/${rfile}" >> ${TMP}/files/${count}
111                                         ofile="${rfile}"
112                                         opath="${rpath}"
113                                         continue
114                                 fi
115                         fi
116
117                         if [[ -z $(diff -Nua ${rpath}/${rfile} ${rpath}/${ofile}|
118                                           grep "^[+-][^+-]"|grep -v '# .Header:.*') ]]; then
119                                 mv ${rpath}/${rfile} ${rpath}/${ofile}
120                                 continue
121                         else
122                                 echo "${rpath}/${rfile}" >> ${TMP}/files/${count}
123                                 ofile="${rfile}"
124                                 opath="${rpath}"
125                         fi
126                 done
127         done
128
129 }
130
131 sel_file() {
132         local -i isfirst=0
133         until [[ -f ${TMP}/files/${input} ]] || \
134               [[ ${input} == -1 ]] || \
135               [[ ${input} == -3 ]]
136         do
137                 local numfiles=$(ls ${TMP}/files|wc -l)
138                 local numwidth=${#numfiles}
139                 for file in $(ls ${TMP}/files|sort -n); do
140                         if [[ ${isfirst} == 0 ]] ; then
141                                 isfirst=${file}
142                         fi
143                         numshow=$(printf "%${numwidth}i${PAR} " ${file})
144                         numupdates=$(( $(wc -l <${TMP}/files/${file}) - 1 ))
145                         echo -n "${numshow}"
146                         if [[ ${mode} == 0 ]] ; then
147                                 echo "$(head -n1 ${TMP}/files/${file}) (${numupdates})"
148                         else
149                                 head -n1 ${TMP}/files/${file}
150                         fi
151                 done > ${TMP}/menuitems
152
153                 if [ "${OVERWRITE_ALL}" == "yes" ]; then
154                         input=0
155                 elif [ "${DELETE_ALL}" == "yes" ]; then
156                         input=0
157                 else
158                         [[ $CLEAR_TERM == yes ]] && clear
159                         if [[ ${mode} == 0 ]] ; then
160                                 echo "The following is the list of files which need updating, each
161 configuration file is followed by a list of possible replacement files."
162                         else
163                                 local my_title="Please select a file to update"
164                         fi
165
166                         if [[ ${mode} == 0 ]] ; then
167                                 cat ${TMP}/menuitems
168                                 echo    "Please select a file to edit by entering the corresponding number."
169                                 echo    "              (don't use -3, -5, -7 or -9 if you're unsure what to do)"
170                                 echo    "              (-1 to exit) (-3 to auto merge all remaining files)"
171                                 echo    "                           (-5 to auto-merge AND not use 'mv -i')"
172                                 echo    "                           (-7 to discard all updates)"
173                                 echo -n "                           (-9 to discard all updates AND not use 'rm -i'): "
174                                 input=$(read_int)
175                         else
176                                 dialog --title "${title}" --menu "${my_title}" \
177                                         0 0 0 $(echo -e "-1 Exit\n$(<${TMP}/menuitems)") \
178                                         2> ${TMP}/input || die "User termination!" 0
179                                 input=$(<${TMP}/input)
180                         fi
181                         if [[ ${input} == -9 ]]; then
182                                 read -p "Are you sure that you want to delete all updates (type YES):" reply
183                                 if [[ ${reply} != "YES" ]]; then
184                                         continue
185                                 else
186                                         input=-7
187                                         export rm_opts=""
188                                 fi
189                         fi
190                         if [[ ${input} == -7 ]]; then
191                                 input=0
192                                 export DELETE_ALL="yes"
193                         fi
194                         if [[ ${input} == -5 ]] ; then
195                                 input=-3
196                                 export mv_opts=" ${mv_opts} "
197                                 mv_opts="${mv_opts// -i / }"
198                         fi
199                         if [[ ${input} == -3 ]] ; then
200                                 input=0
201                                 export OVERWRITE_ALL="yes"
202                         fi
203                 fi # -3 automerge
204                 if [[ -z ${input} ]] || [[ ${input} == 0 ]] ; then
205                         input=${isfirst}
206                 fi
207         done
208 }
209
210 user_special() {
211         if [ -r ${PORTAGE_CONFIGROOT}etc/etc-update.special ]; then
212                 if [ -z "$1" ]; then
213                         echo "ERROR: user_special() called without arguments"
214                         return 1
215                 fi
216                 while read -r pat; do
217                         echo ${1} | grep "${pat}" > /dev/null && return 0
218                 done < ${PORTAGE_CONFIGROOT}etc/etc-update.special
219         fi
220         return 1
221 }
222
223 read_int() {
224         # Read an integer from stdin.  Continously loops until a valid integer is
225         # read.  This is a workaround for odd behavior of bash when an attempt is
226         # made to store a value such as "1y" into an integer-only variable.
227         local my_input
228         while true; do
229                 read my_input
230                 # failed integer conversions will break a loop unless they're enclosed
231                 # in a subshell.
232                 echo "${my_input}" | ( declare -i x; read x) 2>/dev/null && break
233                 echo -n "Value '$my_input' is not valid. Please enter an integer value:" >&2
234         done
235         echo ${my_input}
236 }
237
238 do_file() {
239         interactive_echo() { [ "${OVERWRITE_ALL}" != "yes" ] && [ "${DELETE_ALL}" != "yes" ] && echo; }
240         interactive_echo
241         local -i my_input
242         local -i fcount=0
243         until (( $(wc -l < ${TMP}/files/${input}) < 2 )); do
244                 my_input=0
245                 if (( $(wc -l < ${TMP}/files/${input}) == 2 )); then
246                         my_input=1
247                 fi
248                 until (( ${my_input} > 0 )) && (( ${my_input} < $(wc -l < ${TMP}/files/${input}) )); do
249                         fcount=0
250
251                         if [ "${OVERWRITE_ALL}" == "yes" ]; then
252                                 my_input=0
253                         elif [ "${DELETE_ALL}" == "yes" ]; then
254                                 my_input=0
255                         else
256                                 for line in $(<${TMP}/files/${input}); do
257                                         if (( ${fcount} > 0 )); then
258                                                 echo -n "${fcount}${PAR} "
259                                                 echo "${line}"
260                                         else
261                                                 if [[ ${mode} == 0 ]] ; then
262                                                         echo "Below are the new config files for ${line}:"
263                                                 else
264                                                         local my_title="Please select a file to process for ${line}"
265                                                 fi
266                                         fi
267                                         fcount=${fcount}+1
268                                 done > ${TMP}/menuitems
269
270                                 if [[ ${mode} == 0 ]] ; then
271                                         cat ${TMP}/menuitems
272                                         echo -n "Please select a file to process (-1 to exit this file): "
273                                         my_input=$(read_int)
274                                 else
275                                         dialog --title "${title}" --menu "${my_title}" \
276                                                 0 0 0 $(echo -e "$(<${TMP}/menuitems)\n${fcount} Exit") \
277                                                 2> ${TMP}/input || die "User termination!" 0
278                                         my_input=$(<${TMP}/input)
279                                 fi
280                         fi # OVERWRITE_ALL
281
282                         if [[ ${my_input} == 0 ]] ; then
283                                 my_input=1
284                         elif [[ ${my_input} == -1 ]] ; then
285                                 input=0
286                                 return
287                         elif [[ ${my_input} == ${fcount} ]] ; then
288                                 break
289                         fi
290                 done
291                 if [[ ${my_input} == ${fcount} ]] ; then
292                         break
293                 fi
294
295                 fcount=${my_input}+1
296
297                 file=$(sed -e "${fcount}p;d" ${TMP}/files/${input})
298                 ofile=$(head -n1 ${TMP}/files/${input})
299
300                 do_cfg "${file}" "${ofile}"
301
302                 sed -e "${fcount}!p;d" ${TMP}/files/${input} > ${TMP}/files/sed
303                 mv ${TMP}/files/sed ${TMP}/files/${input}
304
305                 if [[ ${my_input} == -1 ]] ; then
306                         break
307                 fi
308         done
309         interactive_echo
310         rm ${TMP}/files/${input}
311         count=${count}-1
312 }
313
314 do_cfg() {
315
316         local file="${1}"
317         local ofile="${2}"
318         local -i my_input=0
319
320         until (( ${my_input} == -1 )) || [ ! -f ${file} ]; do
321                 if [[ "${OVERWRITE_ALL}" == "yes" ]] && ! user_special "${ofile}"; then
322                         my_input=1
323                 elif [[ "${DELETE_ALL}" == "yes" ]] && ! user_special "${ofile}"; then
324                         my_input=2
325                 else
326                         [[ $CLEAR_TERM == yes ]] && clear
327                         if [ "${using_editor}" == 0 ]; then
328                                 (
329                                         echo "Showing differences between ${ofile} and ${file}"
330                                         diff_command "${ofile}" "${file}"
331                                 ) | ${pager}
332                         else
333                                 echo "Beginning of differences between ${ofile} and ${file}"
334                                 diff_command "${ofile}" "${file}"
335                                 echo "End of differences between ${ofile} and ${file}"
336                         fi
337                         if [ -L "${file}" ]; then
338                                 echo
339                                 echo "-------------------------------------------------------------"
340                                 echo "NOTE: File is a symlink to another file. REPLACE recommended."
341                                 echo "      The original file may simply have moved. Please review."
342                                 echo "-------------------------------------------------------------"
343                                 echo
344                         fi
345                         echo -n "File: ${file}
346 1) Replace original with update
347 2) Delete update, keeping original as is
348 3) Interactively merge original with update
349 4) Show differences again
350 5) Save update as example config
351 Please select from the menu above (-1 to ignore this update): "
352                         my_input=$(read_int)
353                 fi
354
355                 case ${my_input} in
356                         1) echo "Replacing ${ofile} with ${file}"
357                            mv ${mv_opts} ${file} ${ofile}
358                            [ -n "${OVERWRITE_ALL}" ] && my_input=-1
359                            continue
360                            ;;
361                         2) echo "Deleting ${file}"
362                            rm ${rm_opts} ${file}
363                            [ -n "${DELETE_ALL}" ] && my_input=-1
364                            continue
365                            ;;
366                         3) do_merge "${file}" "${ofile}"
367                            my_input=${?}
368 #                          [ ${my_input} == 255 ] && my_input=-1
369                            continue
370                            ;;
371                         4) continue
372                            ;;
373                         5) do_distconf "${file}" "${ofile}"
374                            ;;
375                         *) continue
376                            ;;
377                 esac
378         done
379 }
380
381 do_merge() {
382         # make sure we keep the merged file in the secure tempdir
383         # so we dont leak any information contained in said file
384         # (think of case where the file has 0600 perms; during the
385         # merging process, the temp file gets umask perms!)
386
387         local file="${1}"
388         local ofile="${2}"
389         local mfile="${TMP}/${2}.merged"
390         local -i my_input=0
391         echo "${file} ${ofile} ${mfile}"
392
393         if [[ -e ${mfile} ]] ; then
394                 echo "A previous version of the merged file exists, cleaning..."
395                 rm ${rm_opts} "${mfile}"
396         fi
397
398         # since mfile will be like $TMP/path/to/original-file.merged, we
399         # need to make sure the full /path/to/ exists ahead of time
400         mkdir -p "${mfile%/*}"
401
402         until (( ${my_input} == -1 )); do
403                 echo "Merging ${file} and ${ofile}"
404                 $(echo "${merge_command}" |
405                  sed -e "s:%merged:${mfile}:g" \
406                          -e "s:%orig:${ofile}:g" \
407                          -e "s:%new:${file}:g")
408                 until (( ${my_input} == -1 )); do
409                         echo -n "1) Replace ${ofile} with merged file
410 2) Show differences between merged file and original
411 3) Remerge original with update
412 4) Edit merged file
413 5) Return to the previous menu
414 Please select from the menu above (-1 to exit, losing this merge): "
415                         my_input=$(read_int)
416                         case ${my_input} in
417                                 1) echo "Replacing ${ofile} with ${mfile}"
418                                    if  [[ ${USERLAND} == BSD ]] ; then
419                                        chown "$(stat -f %Su:%Sg "${ofile}")" "${mfile}"
420                                        chmod $(stat -f %Mp%Lp "${ofile}") "${mfile}"
421                                    else
422                                        chown --reference="${ofile}" "${mfile}"
423                                        chmod --reference="${ofile}" "${mfile}"
424                                    fi
425                                    mv ${mv_opts} "${mfile}" "${ofile}"
426                                    rm ${rm_opts} "${file}"
427                                    return 255
428                                    ;;
429                                 2)
430                                         [[ $CLEAR_TERM == yes ]] && clear
431                                         if [ "${using_editor}" == 0 ]; then
432                                                 (
433                                                         echo "Showing differences between ${ofile} and ${mfile}"
434                                                         diff_command "${ofile}" "${mfile}"
435                                                 ) | ${pager}
436                                         else
437                                                 echo "Beginning of differences between ${ofile} and ${mfile}"
438                                                 diff_command "${ofile}" "${mfile}"
439                                                 echo "End of differences between ${ofile} and ${mfile}"
440                                         fi
441                                    continue
442                                    ;;
443                                 3) break
444                                    ;;
445                                 4) ${EDITOR:-nano -w} "${mfile}"
446                                    continue
447                                          ;;
448                                 5) rm ${rm_opts} "${mfile}"
449                                    return 0
450                                    ;;
451                                 *) continue
452                                    ;;
453                         esac
454                 done
455         done
456         rm ${rm_opts} "${mfile}"
457         return 255
458 }
459
460 do_distconf() {
461         # search for any previously saved distribution config
462         # files and number the current one accordingly
463
464         local file="${1}"
465         local ofile="${2}"
466         local -i count
467         local -i fill
468         local suffix
469         local efile
470
471         for ((count = 0; count <= 9999; count++)); do
472                 suffix=".dist_"
473                 for ((fill = 4 - ${#count}; fill > 0; fill--)); do
474                         suffix+="0"
475                 done
476                 suffix+="${count}"
477                 efile="${ofile}${suffix}"
478                 if [[ ! -f ${efile} ]]; then
479                         mv ${mv_opts} "${file}" "${efile}"
480                         break
481                 elif diff_command "${file}" "${efile}" &> /dev/null; then
482                         # replace identical copy
483                         mv "${file}" "${efile}"
484                         break
485                 fi
486         done
487 }
488
489 die() {
490         trap SIGTERM
491         trap SIGINT
492
493         if [ "$2" -eq 0 ]; then
494                 echo "Exiting: ${1}"
495                 scan > /dev/null
496                 [ ${count} -gt 0 ] && echo "NOTE: ${count} updates remaining"
497         else
498                 echo "ERROR: ${1}"
499         fi
500
501         rm -rf "${TMP}"
502         exit ${2}
503 }
504
505 usage() {
506         cat <<-EOF
507         etc-update: Handle configuration file updates
508
509         Usage: etc-update [options]
510
511         Options:
512           -d, --debug    Enable shell debugging
513           -h, --help     Show help and run away
514           -V, --version  Show version and trundle away
515         EOF
516
517         [[ -n ${*:2} ]] && printf "\nError: %s\n" "${*:2}" 1>&2
518
519         exit ${1:-0}
520 }
521
522 #
523 # Run the script
524 #
525
526 SET_X=false
527 while [[ -n $1 ]] ; do
528         case $1 in
529                 -d|--debug)   SET_X=true;;
530                 -h|--help)    usage;;
531                 -V|--version) emerge --version ; exit 0;;
532                 *)            usage 1 "Invalid option '$1'";;
533         esac
534         shift
535 done
536 ${SET_X} && set -x
537
538 type portageq > /dev/null || exit $?
539 eval $(portageq envvar -v CONFIG_PROTECT \
540         CONFIG_PROTECT_MASK PORTAGE_CONFIGROOT PORTAGE_INST_GID PORTAGE_INST_UID \
541         PORTAGE_TMPDIR EROOT USERLAND)
542 export PORTAGE_TMPDIR
543
544 TMP="${PORTAGE_TMPDIR}/etc-update-$$"
545 trap "die terminated 1" SIGTERM
546 trap "die interrupted 1" SIGINT
547
548 [ -w ${PORTAGE_CONFIGROOT}etc ] || die "Need write access to ${PORTAGE_CONFIGROOT}etc" 1
549 #echo $PORTAGE_TMPDIR
550 #echo $CONFIG_PROTECT
551 #echo $CONFIG_PROTECT_MASK
552 #export PORTAGE_TMPDIR=$(/usr/lib/portage/bin/portageq envvar PORTAGE_TMPDIR)
553
554 rm -rf "${TMP}" 2> /dev/null
555 mkdir "${TMP}" || die "failed to create temp dir" 1
556 # make sure we have a secure directory to work in
557 chmod 0700 "${TMP}" || die "failed to set perms on temp dir" 1
558 chown ${PORTAGE_INST_UID:-0}:${PORTAGE_INST_GID:-0} "${TMP}" || \
559         die "failed to set ownership on temp dir" 1
560
561 # I need the CONFIG_PROTECT value
562 #CONFIG_PROTECT=$(/usr/lib/portage/bin/portageq envvar CONFIG_PROTECT)
563 #CONFIG_PROTECT_MASK=$(/usr/lib/portage/bin/portageq envvar CONFIG_PROTECT_MASK)
564
565 # load etc-config's configuration
566 CLEAR_TERM=$(get_config clear_term)
567 EU_AUTOMERGE=$(get_config eu_automerge)
568 rm_opts=$(get_config rm_opts)
569 mv_opts=$(get_config mv_opts)
570 cp_opts=$(get_config cp_opts)
571 pager=$(get_config pager)
572 diff_command=$(get_config diff_command)
573 using_editor=$(get_config using_editor)
574 merge_command=$(get_config merge_command)
575 declare -i mode=$(get_config mode)
576 [[ -z ${mode} ]] && mode=0
577 if ! cmd_var_is_valid "${pager}" ; then
578         pager=${PAGER}
579         cmd_var_is_valid "${pager}" || pager=cat
580 fi
581
582 if [ "${using_editor}" == 0 ]; then
583         # Sanity check to make sure diff exists and works
584         echo > "${TMP}"/.diff-test-1
585         echo > "${TMP}"/.diff-test-2
586         
587         if ! diff_command "${TMP}"/.diff-test-1 "${TMP}"/.diff-test-2 ; then
588                 die "'${diff_command}' does not seem to work, aborting" 1
589         fi
590 else
591         if ! type ${diff_command%% *} >/dev/null; then
592                 die "'${diff_command}' does not seem to work, aborting" 1
593         fi
594 fi
595
596 if [[ ${mode} == "1" ]] ; then
597         if ! type dialog >/dev/null || ! dialog --help >/dev/null ; then
598                 die "mode=1 and 'dialog' not found or not executable, aborting" 1
599         fi
600 fi
601
602 #echo "rm_opts: $rm_opts, mv_opts: $mv_opts, cp_opts: $cp_opts"
603 #echo "pager: $pager, diff_command: $diff_command, merge_command: $merge_command"
604
605 if (( ${mode} == 0 )); then
606         PAR=")"
607 else
608         PAR=""
609 fi
610
611 declare -i count=0
612 declare input=0
613 declare title="Gentoo's etc-update tool!"
614
615 scan
616
617 until (( ${input} == -1 )); do
618         if (( ${count} == 0 )); then
619                 die "Nothing left to do; exiting. :)" 0
620         fi
621         sel_file
622         if (( ${input} != -1 )); then
623                 do_file
624         fi
625 done
626
627 die "User termination!" 0