refactor(dfm): clean up portability, dead code, and error handling

Add bash 4.0+ version check with macOS Homebrew bootstrap. Remove
unreachable fish shell detection and source_file function. Fix bugs:
remove dead ntfy menu entry, fix msg/msgr case mismatch in tests,
guard shift calls against empty args, quote $width, fix $"..." locale
string, fix exit 0 on apt error. Replace declare -A with indexed
array in section_scripts. Use early-return guards with msgr warn for
unavailable brew/apt. Replace exit with return in section functions.
This commit is contained in:
2026-02-05 20:31:17 +02:00
parent 57b566704e
commit abb6c9f615

View File

@@ -15,38 +15,37 @@
SCRIPT=$(basename "$0") SCRIPT=$(basename "$0")
# Detect the current shell # Require bash 4.0+ for associative arrays and mapfile
CURRENT_SHELL=$(ps -p $$ -ocomm= | awk -F/ '{print $NF}') if ((BASH_VERSINFO[0] < 4)); then
echo "dfm requires bash 4.0+, found ${BASH_VERSION}"
if [[ "$(uname)" == "Darwin" ]]; then
if ! command -v brew &> /dev/null; then
echo "Installing Homebrew..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
echo "Installing modern bash via Homebrew..."
brew install bash
echo "Done. Restart your shell and run dfm again."
else
echo "Install bash 4.0+ and try again."
fi
exit 1
fi
# Function to source files based on the shell # shellcheck disable=SC1091
source_file() source "$DOTFILES/config/shared.sh"
# shellcheck disable=SC1090
source "${DOTFILES}/local/bin/msgr"
# Get description from a script file's @description tag
get_script_description()
{ {
local file=$1 local file="$1"
case "$CURRENT_SHELL" in local desc
fish) desc=$(sed -n '/@description/s/.*@description *\(.*\)/\1/p' "$file" | head -1)
if [[ -f "$file.fish" ]]; then echo "${desc:-No description available}"
# shellcheck disable=SC1090
source "$file.fish"
else
echo "Fish shell file not found: $file.fish"
exit 1
fi
;;
sh | bash | zsh)
# shellcheck disable=SC1090
source "$file"
;;
*)
echo "Unsupported shell: $CURRENT_SHELL"
exit 1
;;
esac
} }
# Modify the source commands to use the new function
source_file "$DOTFILES/config/shared.sh"
source_file "${DOTFILES}/local/bin/msgr"
# Menu builder # Menu builder
menu_builder() menu_builder()
{ {
@@ -54,9 +53,9 @@ menu_builder()
local commands=("${@:2}") local commands=("${@:2}")
local width=60 local width=60
printf "\n%s\n" "$(printf '%.s─' $(seq 1 $width))" printf "\n%s\n" "$(printf '%.s─' $(seq 1 "$width"))"
printf "%-${width}s\n" " $title" printf "%-${width}s\n" " $title"
printf "%s\n" "$(printf '%.s─' $(seq 1 $width))" printf "%s\n" "$(printf '%.s─' $(seq 1 "$width"))"
for cmd in "${commands[@]}"; do for cmd in "${commands[@]}"; do
local name=${cmd%%:*} local name=${cmd%%:*}
@@ -80,7 +79,6 @@ section_install()
"imagick:Install ImageMagick CLI" "imagick:Install ImageMagick CLI"
"macos:Setup nice macOS defaults" "macos:Setup nice macOS defaults"
"npm-packages:Install NPM Packages" "npm-packages:Install NPM Packages"
"ntfy:Install ntfy"
"nvm-latest:Install latest lts node using nvm" "nvm-latest:Install latest lts node using nvm"
"nvm:Install Node Version Manager (nvm)" "nvm:Install Node Version Manager (nvm)"
"z:Install z" "z:Install z"
@@ -100,6 +98,7 @@ section_install()
$0 install npm-packages $0 install npm-packages
$0 install z $0 install z
msgr msg "Reloading configurations again..." msgr msg "Reloading configurations again..."
# shellcheck disable=SC1091
source "$DOTFILES/config/shared.sh" source "$DOTFILES/config/shared.sh"
msgr yay "All done!" msgr yay "All done!"
;; ;;
@@ -208,87 +207,88 @@ section_brew()
"untracked:List untracked brew packages" "untracked:List untracked brew packages"
) )
x-have brew && { if ! x-have brew; then
case "$1" in msgr warn "brew not available, skipping"
install) return 0
brew bundle install --file="$BREWFILE" --force --quiet && msgr yay "Done!" fi
;;
update) case "$1" in
brew update && brew outdated && brew upgrade && brew cleanup install)
msgr yay "Done!" brew bundle install --file="$BREWFILE" --force --quiet && msgr yay "Done!"
;; ;;
updatebundle) update)
# Updates .dotfiles/homebrew/Brewfile with descriptions brew update && brew outdated && brew upgrade && brew cleanup
brew bundle dump \ msgr yay "Done!"
--force \ ;;
--file="$BREWFILE" \
--cleanup \
--tap \
--formula \
--cask \
--describe && msgr yay "Done!"
;;
leaves) updatebundle)
brew leaves --installed-on-request # Updates .dotfiles/homebrew/Brewfile with descriptions
;; brew bundle dump \
--force \
--file="$BREWFILE" \
--cleanup \
--tap \
--formula \
--cask \
--describe && msgr yay "Done!"
;;
untracked) leaves)
declare -a BREW_LIST_ALL brew leaves --installed-on-request
while IFS= read -r line; do ;;
BREW_LIST_ALL+=("$line")
done < <(brew list --formula --installed-on-request -1 --full-name)
while IFS= read -r c; do
BREW_LIST_ALL+=("$c")
done < <(brew list --cask -1 --full-name)
# Remove entries that are installed as dependencies untracked)
declare -a BREW_LIST_DEPENDENCIES declare -a BREW_LIST_ALL
while IFS= read -r l; do while IFS= read -r line; do
BREW_LIST_DEPENDENCIES+=("$l") BREW_LIST_ALL+=("$line")
done < <(brew list -1 --installed-as-dependency) done < <(brew list --formula --installed-on-request -1 --full-name)
while IFS= read -r c; do
BREW_LIST_ALL+=("$c")
done < <(brew list --cask -1 --full-name)
declare -a BREW_LIST_BUNDLED # Remove entries that are installed as dependencies
while IFS= read -r b; do declare -a BREW_LIST_DEPENDENCIES
BREW_LIST_BUNDLED+=("$b") while IFS= read -r l; do
done < <(brew bundle list --all --file="$BREWFILE") BREW_LIST_DEPENDENCIES+=("$l")
done < <(brew list -1 --installed-as-dependency)
declare -a BREW_LIST_TRACKED_WITHOUT_DEPS declare -a BREW_LIST_BUNDLED
for f in "${BREW_LIST_ALL[@]}"; do while IFS= read -r b; do
# shellcheck disable=SC2199 BREW_LIST_BUNDLED+=("$b")
if [[ " ${BREW_LIST_DEPENDENCIES[@]} " != *" ${f} "* ]]; then done < <(brew bundle list --all --file="$BREWFILE")
BREW_LIST_TRACKED_WITHOUT_DEPS+=("$f")
fi
done
array_diff BREW_LIST_UNTRACKED BREW_LIST_TRACKED_WITHOUT_DEPS BREW_LIST_BUNDLED declare -a BREW_LIST_TRACKED_WITHOUT_DEPS
for f in "${BREW_LIST_ALL[@]}"; do
# If there are no untracked packages, exit # shellcheck disable=SC2199
if [ ${#BREW_LIST_UNTRACKED[@]} -eq 0 ]; then if [[ " ${BREW_LIST_DEPENDENCIES[@]} " != *" ${f} "* ]]; then
msgr yay "No untracked packages found!" BREW_LIST_TRACKED_WITHOUT_DEPS+=("$f")
exit 0
fi fi
done
echo "Untracked:" array_diff BREW_LIST_UNTRACKED BREW_LIST_TRACKED_WITHOUT_DEPS BREW_LIST_BUNDLED
for f in "${BREW_LIST_UNTRACKED[@]}"; do
echo " $f"
done
;;
autoupdate) # If there are no untracked packages, return
brew autoupdate delete if [ ${#BREW_LIST_UNTRACKED[@]} -eq 0 ]; then
brew autoupdate start 43200 --upgrade --cleanup --immediate msgr yay "No untracked packages found!"
;; return 0
fi
clean) brew bundle cleanup --file="$BREWFILE" && msgr yay "Done!" ;; echo "Untracked:"
for f in "${BREW_LIST_UNTRACKED[@]}"; do
echo " $f"
done
;;
*) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;; autoupdate)
esac brew autoupdate delete
} brew autoupdate start 43200 --upgrade --cleanup --immediate
;;
! x-have brew && menu_builder "$USAGE_PREFIX" "brew not available on this system" clean) brew bundle cleanup --file="$BREWFILE" && msgr yay "Done!" ;;
*) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;;
esac
} }
section_helpers() section_helpers()
@@ -305,10 +305,10 @@ section_helpers()
"wezterm:Show wezterm keybindings" "wezterm:Show wezterm keybindings"
) )
CMD="$1" CMD="${1:-}"
shift [[ $# -gt 0 ]] && shift
SECTION="$1" SECTION="${1:-}"
shift [[ $# -gt 0 ]] && shift
case "$CMD" in case "$CMD" in
path) path)
@@ -379,55 +379,60 @@ section_apt()
"clean:Clean apt cache" "clean:Clean apt cache"
) )
x-have apt && { if ! x-have apt; then
case "$1" in msgr warn "apt not available, skipping"
upkeep) return 0
sudo apt update \ fi
&& sudo apt upgrade -y \
&& sudo apt autoremove -y \
&& sudo apt clean
;;
install) case "$1" in
# if apt.txt is not found, exit upkeep)
[ ! -f "$DOTFILES/tools/apt.txt" ] && msgr err "apt.txt not found" && exit 0 sudo apt update \
&& sudo apt upgrade -y \
&& sudo apt autoremove -y \
&& sudo apt clean
;;
# Load apt.txt, remove comments (even if trailing comment) and empty lines. install)
# # if apt.txt is not found, return with error
# Ignoring "Quote this to prevent word splitting." if [ ! -f "$DOTFILES/tools/apt.txt" ]; then
msgr err "apt.txt not found"
return 1
fi
# Load apt.txt, remove comments (even if trailing comment) and empty lines.
#
# Ignoring "Quote this to prevent word splitting."
# shellcheck disable=SC2046
sudo apt install \
-y $(
grep -vE '^\s*#' "$DOTFILES/tools/apt.txt" \
| sed -e 's/#.*//' \
| tr '\n' ' '
)
# If there's a apt.txt file under hosts/$hostname/apt.txt,
# run install on those lines too.
HOSTNAME=$(hostname -s)
HOST_APT="$DOTFILES/hosts/$HOSTNAME/apt.txt"
[[ -f $HOST_APT ]] && {
# shellcheck disable=SC2046 # shellcheck disable=SC2046
sudo apt install \ sudo apt install -y $(
-y $( grep -vE '^\s*#' "$HOST_APT" \
grep -vE '^\s*#' "$DOTFILES/tools/apt.txt" \ | sed -e 's/#.*//' \
| sed -e 's/#.*//' \ | tr '\n' ' '
| tr '\n' ' ' )
) }
# If there's a apt.txt file under hosts/$hostname/apt.txt, # Try this for an alternative way to install packages
# run install on those lines too. # xargs -a <(awk '! /^ *(#|$)/' "$packagelist") -r -- sudo apt-get install -y
HOSTNAME=$(hostname -s) ;;
HOST_APT="$DOTFILES/hosts/$HOSTNAME/apt.txt"
[[ -f $HOST_APT ]] && {
# shellcheck disable=SC2046
sudo apt install -y $(
grep -vE '^\s*#' "$HOST_APT" \
| sed -e 's/#.*//' \
| tr '\n' ' '
)
}
# Try this for an alternative way to install packages update) sudo apt update ;;
# xargs -a <(awk '! /^ *(#|$)/' "$packagelist") -r -- sudo apt-get install -y upgrade) sudo apt upgrade -y ;;
;; autoremove) sudo apt autoremove -y ;;
clean) sudo apt clean ;;
update) sudo apt update ;; *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;;
upgrade) sudo apt upgrade -y ;; esac
autoremove) sudo apt autoremove -y ;;
clean) sudo apt clean ;;
*) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;;
esac
}
! x-have apt && menu_builder "$USAGE_PREFIX" "apt not available on this system"
} }
section_docs() section_docs()
@@ -534,13 +539,13 @@ section_check()
case "$1" in case "$1" in
a | arch) a | arch)
[[ $2 == "" ]] && echo "$X_ARCH" && exit 0 [[ $2 == "" ]] && echo "$X_ARCH" && return 0
[[ $X_ARCH == "$2" ]] && exit 0 || exit 1 [[ $X_ARCH == "$2" ]] && return 0 || return 1
;; ;;
h | host | hostname) h | host | hostname)
[[ $2 == "" ]] && echo "$X_HOSTNAME" && exit 0 [[ $2 == "" ]] && echo "$X_HOSTNAME" && return 0
[[ $X_HOSTNAME == "$2" ]] && exit 0 || exit 1 [[ $X_HOSTNAME == "$2" ]] && return 0 || return 1
;; ;;
*) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;; *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;;
@@ -551,33 +556,18 @@ section_scripts()
{ {
USAGE_PREFIX="$SCRIPT scripts <command>" USAGE_PREFIX="$SCRIPT scripts <command>"
# Get description from a file
get_script_description()
{
local file
local desc
file="$1"
desc=$(sed -n '/@description/s/.*@description *\(.*\)/\1/p' "$file" | head -1)
echo "${desc:-No description available}"
}
# Collect scripts and their descriptions # Collect scripts and their descriptions
declare -A SCRIPT_MENU local menu_items=()
for script in "$DOTFILES/scripts/install-"*.sh; do for script in "$DOTFILES/scripts/install-"*.sh; do
if [ -f "$script" ]; then if [ -f "$script" ]; then
name=$(basename "$script" .sh | sed 's/install-//') name=$(basename "$script" .sh | sed 's/install-//')
desc=$(get_script_description "$script") desc=$(get_script_description "$script")
SCRIPT_MENU[$name]="$desc" menu_items+=("$name:$desc")
fi fi
done done
case "$1" in case "$1" in
"") "")
# Show the menu
local menu_items=()
for name in "${!SCRIPT_MENU[@]}"; do
menu_items+=("$name:${SCRIPT_MENU[$name]}")
done
menu_builder "$USAGE_PREFIX" "${menu_items[@]}" menu_builder "$USAGE_PREFIX" "${menu_items[@]}"
;; ;;
*) *)
@@ -609,7 +599,7 @@ section_tests()
echo " $i" echo " $i"
done done
;; ;;
msg) msgr)
# shellcheck disable=SC1010 # shellcheck disable=SC1010
msgr done "msgr done" msgr done "msgr done"
msgr done_suffix "msgr done_suffix" msgr done_suffix "msgr done_suffix"
@@ -633,7 +623,7 @@ usage()
{ {
echo "" echo ""
msgr prompt "Usage: $SCRIPT <section> <command>" msgr prompt "Usage: $SCRIPT <section> <command>"
echo $" Empty <command> prints <section> help." echo " Empty <command> prints <section> help."
echo "" echo ""
section_install section_install
echo "" echo ""
@@ -654,8 +644,8 @@ usage()
main() main()
{ {
SECTION="$1" SECTION="${1:-}"
shift [[ $# -gt 0 ]] && shift
# The main loop. The first keyword after $0 triggers section, or help. # The main loop. The first keyword after $0 triggers section, or help.
case "$SECTION" in case "$SECTION" in
install) section_install "$@" ;; install) section_install "$@" ;;
@@ -667,7 +657,7 @@ main()
docs) section_docs "$@" ;; docs) section_docs "$@" ;;
scripts) section_scripts "$@" ;; scripts) section_scripts "$@" ;;
tests) section_tests "$@" ;; tests) section_tests "$@" ;;
*) usage && exit 0 ;; *) usage && return 0 ;;
esac esac
} }