152 lines
3.7 KiB
Bash
Executable File
152 lines
3.7 KiB
Bash
Executable File
#!/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=
|
|
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"
|
|
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="$ROOT/usr/share/debianutils/shells"
|
|
fi
|
|
|
|
PKG_SHELLS='#'
|
|
LC_COLLATE=C.UTF-8 # glob in reproducible order
|
|
for f in "$PKG_DIR/"*; do
|
|
[ "$f" = "$PKG_DIR/*" ] && break
|
|
while IFS='#' read -r line _; do
|
|
[ -n "$line" ] || continue
|
|
PKG_SHELLS="$PKG_SHELLS$line#"
|
|
realshell=$(dpkg-realpath --root "$ROOT" "$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
|
|
echo "$line" >> "$NEW_ETC_FILE"
|
|
ETC_SHELLS="$ETC_SHELLS$shell#"
|
|
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
|