1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
|
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2021 Helmut Grohne <helmut@subdivi.de>
# A "hashset" is a shell variable containing a sequence of elements separated
# and surrounded by hash (#) characters. None of the elements may contain a
# hash character. The character is thus chosen, because it initiates a comment
# in /etc/shells. All variables ending in _SHELLS in this file are hashsets.
set -e
# Check whether hashset $1 contains element $2.
hashset_contains() {
case "$1" in
*"#$2#"*) return 0 ;;
*) return 1 ;;
esac
}
log() {
if [ "$VERBOSE" = 1 ]; then
echo "$*"
fi
}
ROOT=${DPKG_ROOT:-}
VERBOSE=0
NOACT=0
while [ $# -gt 0 ]; do
case "$1" in
--help)
cat <<EOF
usage: $0 [options]
--no-act Do not move the actual update into place
--verbose Be more verbose
--root DIR Operate on the given chroot, defaults to /
EOF
exit 0
;;
--no-act)
NOACT=1
;;
--root)
shift
if [ "$#" -lt 1 ]; then
echo "missing argument to --root" 1>&2
exit 1
fi
ROOT=$1
;;
--verbose)
VERBOSE=1
;;
*)
echo "unrecognized option $1" 1>&2
exit 1
;;
esac
shift
done
PKG_DIR="$ROOT/usr/share/debianutils/shells.d"
STATE_FILE="$ROOT/var/lib/shells.state"
TEMPLATE_ETC_FILE="$ROOT/usr/share/debianutils/shells"
TARGET_ETC_FILE="$ROOT/etc/shells"
SOURCE_ETC_FILE="$TARGET_ETC_FILE"
NEW_ETC_FILE="$TARGET_ETC_FILE.tmp"
NEW_STATE_FILE="$STATE_FILE.tmp"
if ! test -e "$SOURCE_ETC_FILE"; then
SOURCE_ETC_FILE="$TEMPLATE_ETC_FILE"
fi
PKG_SHELLS='#'
LC_COLLATE=C.UTF-8 # glob in reproducible order
for f in "$TEMPLATE_ETC_FILE" "$PKG_DIR/"*; do
test -f "$f" || continue
while IFS='#' read -r line _; do
[ -n "$line" ] || continue
PKG_SHELLS="$PKG_SHELLS$line#"
realshell=$(dpkg-realpath --root "$ROOT" "$(dirname "$line")")/$(basename "$line")
if [ "$line" != "$realshell" ]; then
PKG_SHELLS="$PKG_SHELLS$realshell#"
fi
done < "$f"
done
STATE_SHELLS='#'
if [ -e "$STATE_FILE" ] ; then
while IFS='#' read -r line _; do
[ -n "$line" ] && STATE_SHELLS="$STATE_SHELLS$line#"
done < "$STATE_FILE"
fi
cleanup() {
rm -f "$NEW_ETC_FILE" "$NEW_STATE_FILE"
}
trap cleanup EXIT
: > "$NEW_ETC_FILE"
ETC_SHELLS='#'
while IFS= read -r line; do
shell=${line%%#*}
# copy all comment lines, packaged shells and local additions
if [ -z "$shell" ] ||
hashset_contains "$PKG_SHELLS" "$shell" ||
! hashset_contains "$STATE_SHELLS" "$shell"; then
if [ -z "$shell" ] || ! hashset_contains "$ETC_SHELLS" "$shell"; then
echo "$line" >> "$NEW_ETC_FILE"
ETC_SHELLS="$ETC_SHELLS$shell#"
fi
else
log "removing shell $shell"
fi
done < "$SOURCE_ETC_FILE"
: > "$NEW_STATE_FILE"
saved_IFS=$IFS
IFS='#'
set -f
# shellcheck disable=SC2086 # word splitting intended, globbing disabled
set -- ${PKG_SHELLS###}
set +f
IFS=$saved_IFS
for shell; do
echo "$shell" >> "$NEW_STATE_FILE"
# add shells that are neither already present nor locally removed
if ! hashset_contains "$ETC_SHELLS" "$shell" &&
! hashset_contains "$STATE_SHELLS" "$shell"; then
echo "$shell" >> "$NEW_ETC_FILE"
log "adding shell $shell"
fi
done
if [ "$NOACT" = 0 ]; then
if [ -e "$STATE_FILE" ]; then
chmod --reference="${STATE_FILE}" "${NEW_STATE_FILE}" || chmod $(stat -c %a "${STATE_FILE}") "${NEW_STATE_FILE}"
chown --reference="${STATE_FILE}" "${NEW_STATE_FILE}" || chown $(stat -c %U "${STATE_FILE}") "${NEW_STATE_FILE}"
else
chmod 0644 "$NEW_STATE_FILE"
fi
chmod --reference="${SOURCE_ETC_FILE}" "${NEW_ETC_FILE}" || chmod $(stat -c %a "${SOURCE_ETC_FILE}") "${NEW_ETC_FILE}"
chown --reference="${SOURCE_ETC_FILE}" "${NEW_ETC_FILE}" || chown $(stat -c %U "${SOURCE_ETC_FILE}") "${NEW_ETC_FILE}"
sync -d "$NEW_ETC_FILE" "$NEW_STATE_FILE"
mv -Z "${NEW_ETC_FILE}" "${TARGET_ETC_FILE}" || mv "${NEW_ETC_FILE}" "${TARGET_ETC_FILE}"
sync "$TARGET_ETC_FILE"
sync "$(dirname "$TARGET_ETC_FILE")"
mv "$NEW_STATE_FILE" "$STATE_FILE"
sync "$STATE_FILE"
sync "$(dirname "$STATE_FILE")"
trap "" EXIT
fi
|