#!/usr/bin/env bash # shellcheck disable=SC2155,SC2223 set -o nounset set -o errexit set -o shwordsplit 2>/dev/null || true # Make zsh behave like bash USER=${USER:-$(id -un)} HOME=${HOME:-$(cd ~ && pwd)} case $(uname) in Linux) FINDTYPE="-xtype" ;; Darwin|*BSD) FINDTYPE="-type" ;; *) echo "Unknown OS: $(uname), guessing no GNU utils." FINDTYPE="-type" ;; esac is_comment() { if [ "$(echo "${1}" | cut -c1-1)" = '#' ] ; then true else false fi } have_command() { command -v "${1}" >/dev/null 2>&1 } prerequisites() { if have_command zsh ; then case $- in *i*) case "$(getent passwd "${USER}" | cut -d: -f7)" in */zsh) ;; *) if [ "$(id)" -ne 0 ] ; then echo 'Enter password to change shell.' >&2 fi chsh -s "$(command -v zsh)" ;; esac ;; esac else echo "ZSH not found!" >&2 fi } install_dotfile_dir() { local SRCDIR="${1}" local dotfile local submodule_prune="$(git -C "${BASEDIR}" submodule status -- "${SRCDIR}" 2>/dev/null | \ awk '{print $2}' | \ while read -r submod ; do echo -n " -o -path ${BASEDIR}/${submod}" done)" # shellcheck disable=SC2086 find "${SRCDIR}" \( -name .git -o \ -path "${SRCDIR}/private_dotfiles" -o \ -name install.sh -o \ -name README.md -o \ -name .gitignore \ ${submodule_prune} \) \ -prune -o ${FINDTYPE} f -print | \ while read -r dotfile ; do local TARGET="${HOME}/.${dotfile#"${SRCDIR}"/}" mkdir -p "$(dirname "${TARGET}")" ln -s -f "${dotfile}" "${TARGET}" done git -C "${BASEDIR}" submodule status -- "${SRCDIR}" 2>/dev/null | \ awk '{print $2}' | \ while read -r submodule ; do local FULLNAME="${BASEDIR}/${submodule}" local TARGET="${HOME}/.${FULLNAME#"${SRCDIR}"/}" mkdir -p "$(dirname "${TARGET}")" if test -L "${TARGET}" ; then if [ "$(readlink "${TARGET}")" != "${FULLNAME}" ] ; then echo "${TARGET} points to $(readlink "${TARGET}") not ${FULLNAME}!" >/dev/stderr fi elif test -d "${TARGET}" ; then echo "rm -rf ${TARGET}" >/dev/stderr else ln -s -f "${FULLNAME}" "${TARGET}" fi done } install_basic_dir() { local SRCDIR="${1}" local DESTDIR="${2}" local file find "${SRCDIR}" ${FINDTYPE} f -print | \ while read -r file ; do local TARGET="${2}/${file#"${SRCDIR}"/}" mkdir -p "$(dirname "${TARGET}")" ln -s -f "${file}" "${TARGET}" done } install_git() { # Install or update a git repository if ! have_command git ; then return 1 fi local REPO="${*: -2:1}" local DESTDIR="${*: -1:1}" set -- "${@:1:$(($#-2))}" if [ -d "${DESTDIR}/.git" ] ; then ( cd "${DESTDIR}" ; git pull -q ) else if [ "${MINIMAL}" -eq 1 ] ; then git clone --depth 1 "$@" "${REPO}" "${DESTDIR}" else git clone "$@" "${REPO}" "${DESTDIR}" fi fi } add_bin_symlink() { local LINKNAME="${HOME}/bin/${2:-$(basename "$1")}" if [ -e "${LINKNAME}" ] && ! [ -h "${LINKNAME}" ] ; then echo "Refusing to overwrite ${LINKNAME}" >&2 return 1 fi ln -sf "${1}" "${LINKNAME}" } postinstall() { true } ssh_key_already_installed() { # Return 1 if the key isn't already installed, 0 if it is local AK="${HOME}/.ssh/authorized_keys" if [ ! -f "$AK" ] ; then return 1 fi local KEYFP="$(ssh-keygen -l -f "$1" 2>/dev/null | awk '{print $2}')" local TMPF="$(mktemp)" local key while read -r key ; do if is_comment "${key}" ; then continue fi echo "$key" > "$TMPF" local EFP="$(ssh-keygen -l -f "${TMPF}" 2>/dev/null | awk '{print $2}')" if [ "$EFP" = "$KEYFP" ] ; then rm "$TMPF" 2>/dev/null return 0 fi done < "${AK}" rm "$TMPF" 2>/dev/null return 1 } install_ssh_keys() { # Install SSH keys verbose 'Installing SSH keys...' local AK="${HOME}/.ssh/authorized_keys" local key local keydir if test "${TRUST_ALL_KEYS}" = 1 ; then keydir="${BASEDIR}/keys/ssh" else keydir="${BASEDIR}/keys/ssh/trusted" fi for key in "${keydir}"/* ; do if [ ! -f "${key}" ] ; then continue fi if ssh_key_already_installed "${key}" ; then verbose "Key $(basename "${key}") already installed..." continue fi echo "# $(basename "${key}") added from skel on $(date +%Y-%m-%d)" >> "${AK}" cat "${key}" >> "${AK}" done } install_gpg_keys() { have_command gpg || \ return 0 local key for key in "${BASEDIR}"/keys/gpg/* ; do gpg --import < "${key}" >/dev/null 2>&1 done } install_known_hosts() { verbose 'Installing known hosts...' >&2 if [ ! -f "${BASEDIR}/keys/known_hosts" ] ; then return 0 fi mkdir -p "${HOME}/.ssh" if [ -f "${HOME}/.ssh/known_hosts" ] ; then local tmpf="$(mktemp)" cat "${BASEDIR}"/keys/known_hosts "${HOME}"/.ssh/known_hosts \ | sort -u > "$tmpf" mv "$tmpf" "${HOME}"/.ssh/known_hosts else cp "${BASEDIR}"/keys/known_hosts "${HOME}"/.ssh/known_hosts fi } install_keys() { install_ssh_keys install_gpg_keys install_known_hosts } is_deb_system() { test -f /usr/bin/apt-get } run_as_root() { # Attempt to run as root if [ "${USER}" = "root" ] ; then "$@" return $? elif test -x "$(command -v sudo 2>/dev/null)" ; then verbose "Using sudo to run ${1}..." sudo "$@" return $? fi return 1 } install_pkg_set() { local pkg_file=${BASEDIR}/packages/${1} local pkg_list="" if [ ! -f "${pkg_file}" ] ; then echo "Package set $(basename "${pkg_file}") does not exist." 1>&2 return 1 fi while read -r line ; do if is_comment "${line}" ; then continue fi if [ -z "${line}" ] ; then continue fi if [ "$(apt-cache -q show "${line}" 2>/dev/null)" != "" ] ; then pkg_list="${pkg_list} ${line}" else echo "Warning: package ${line} not found." >&2 fi done < "${pkg_file}" if [ -n "${pkg_list}" ] ; then verbose "Installing ${pkg_list}" # shellcheck disable=SC2086 run_as_root apt-get install -qqy ${pkg_list} fi } setup_git_email() { local gc_local="${HOME}/.gitconfig.local" if test -f "${gc_local}" ; then return 0 fi if [ "${USER:0:5}" != "david" ] ; then return 0 fi local domain="$(hostname -f | grep -E -o '[a-z0-9-]+\.[a-z0-9-]+$')" case "$(echo "${domain}" | md5sum | awk '{print $1}')" in b21a24d528346ef7d3932306ed96ede5|a5ed434a3f5089b489576cceab824f25) ;; *) return 0 ;; esac echo -e "[user]\n email=${USER}@${domain}" > "${gc_local}" } read_saved_prefs() { # Can't use basedir here as we don't have it yet local old_pref_file="$(dirname "$0")/installed-prefs" local pref_file="$(dirname "$0")/.installed-prefs" if [ -f "${old_pref_file}" ] && ! [ -f "${pref_file}" ] ; then mv "${old_pref_file}" "${pref_file}" fi if [ -f "${pref_file}" ] ; then verbose "Loading saved skel preferences from ${pref_file}" # source is a bashism # shellcheck disable=SC1090 . "${pref_file}" fi } save_prefs() { test "$SAVE" = 1 || return 0 local pref_file=${BASEDIR}/.installed-prefs (echo_pref BASEDIR echo_pref MINIMAL echo_pref INSTALL_KEYS echo_pref TRUST_ALL_KEYS echo_pref VERBOSE) > "$pref_file" } echo_pref() { eval "local val=\${$1}" # shellcheck disable=SC2154 echo ": \${$1:=${val}}" } cleanup() { # Needs zsh if ! have_command zsh ; then return 0 fi zsh >/dev/null 2>&1 <&2 || return 0 } # Operations install_dotfiles() { install_dotfile_dir "${BASEDIR}/dotfiles" if test -d "${BASEDIR}/private_dotfiles" && \ test -d "${BASEDIR}/.git/git-crypt" ; then install_dotfile_dir "${BASEDIR}/private_dotfiles" fi if test -d "${BASEDIR}/local_dotfiles" ; then install_dotfile_dir "${BASEDIR}/local_dotfiles" fi if test -d "${BASEDIR}/dotfile_overlays" ; then for dotfiledir in "${BASEDIR}/dotfile_overlays/"* ; do if test -d "${dotfiledir}" ; then install_dotfile_dir "${dotfiledir}" fi done fi } install_main() { if test -d "${BASEDIR}/.git" ; then have_command git && \ git -C "${BASEDIR}" pull --ff-only test "$MINIMAL" = 1 || ( have_command git && \ git -C "${BASEDIR}" submodule update --init --recursive --depth 1 ) fi test "$MINIMAL" = 1 || { prerequisites # try to update dotfile overlays if test -d "${BASEDIR}/dotfile_overlays" ; then for dotfiledir in "${BASEDIR}/dotfile_overlays/"* ; do if test -d "${dotfiledir}/.git" ; then git -C "${dotfiledir}" pull --ff-only || true git -C "${dotfiledir}" submodule update --init --recursive --depth 1 || true fi done fi } install_dotfiles install_basic_dir "${BASEDIR}/bin" "${HOME}/bin" test "$MINIMAL" = 1 || postinstall test "$INSTALL_KEYS" = 1 && install_keys save_prefs setup_git_email cleanup } install_dconf() { have_command dconf || return 1 find "${BASEDIR}/dconf" -type f -printf '/%P\n' | while read -r dcpath ; do dconf load "${dcpath}/" < "${BASEDIR}/dconf/${dcpath}" done } install_vim_extra() { local DEST="${HOME}/.vim/pack/matir-extra" local REPO="https://github.com/Matir/vim-extra.git" if test -d "${DEST}" ; then if test -d "${DEST}/.git" ; then # do update git -C "${DEST}" pull --ff-only git -C "${DEST}" submodule update --init else echo "${DEST} exists but does not appear to be a git repo." >&2 return 1 fi else # do clone git clone --recurse-submodules "${REPO}" "${DEST}" fi } # Setup variables read_saved_prefs # Defaults if not passed in or saved. # TODO: use flags instead of environment variables. : ${BASEDIR:=$HOME/.skel} : ${MINIMAL:=0} : ${INSTALL_KEYS:=1} : ${TRUST_ALL_KEYS:=0} : ${VERBOSE:=0} : ${SAVE:=1} # Check prerequisites if [ ! -d "$BASEDIR" ] ; then echo "Please install to $BASEDIR!" 1>&2 exit 1 fi if have_command dpkg-query ; then HAVE_X=$(dpkg-query -s xserver-xorg 2>/dev/null | \ grep -c 'Status.*installed' \ || true) else HAVE_X=0 fi IS_KALI=$(grep -ci kali /etc/os-release 2>/dev/null || true) ARCH=$(uname -m) OPERATION=${1:-install} case $OPERATION in install) install_main ;; dotfiles) install_dotfiles ;; package*) PKG_SET=${2:-minimal} install_pkg_set "${PKG_SET}" ;; test) # Do nothing, just sourcing set +o errexit ;; dconf) # Load dconf install_dconf ;; vim-extra) # Install/update extra vim modules install_vim_extra ;; *) echo "Unknown operation $OPERATION." >&2 exit 1 ;; esac echo "OK"