diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml new file mode 100644 index 000000000..7770e465e --- /dev/null +++ b/.github/workflows/smoke-test.yml @@ -0,0 +1,66 @@ +name: Smoke Test - exp-update + +on: + push: + paths: + - 'install.sh' + - 'sdata/step/exp-update.sh' + - 'sdata/lib/options-exp-update.sh' + pull_request: + paths: + - 'install.sh' + - 'sdata/step/exp-update.sh' + - 'sdata/lib/options-exp-update.sh' + workflow_dispatch: + +jobs: + smoke-test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Make install.sh executable + run: chmod +x install.sh + + - name: Run smoke test + run: | + echo "Running: ./install.sh exp-update --non-interactive --skip-notice --dry-run -v" + + # Capture output and exit code + OUTPUT=$(./install.sh exp-update --non-interactive --skip-notice --dry-run -v 2>&1) + EXIT_CODE=$? + + echo "Exit code: $EXIT_CODE" + echo "Output:" + echo "$OUTPUT" + + # Check exit code + if [ $EXIT_CODE -ne 0 ]; then + echo "❌ Smoke test failed: Non-zero exit code" + exit 1 + fi + + # Check for expected strings in output + if ! echo "$OUTPUT" | grep -q "DRY-RUN MODE"; then + echo "❌ Smoke test failed: Missing 'DRY-RUN MODE' in output" + exit 1 + fi + + if ! echo "$OUTPUT" | grep -q "Detecting Repository Structure"; then + echo "❌ Smoke test failed: Missing 'Detecting Repository Structure' in output" + exit 1 + fi + + # Check for non-empty Structure line (should contain detected directories) + STRUCTURE_LINE=$(echo "$OUTPUT" | grep "Structure:" | head -1) + if [[ -z "$STRUCTURE_LINE" ]] || [[ "$STRUCTURE_LINE" == "Structure:" ]]; then + echo "❌ Smoke test failed: Structure line is empty or malformed" + echo "Found: $STRUCTURE_LINE" + exit 1 + fi + + echo "✅ Smoke test passed: All checks successful" + echo "Detected structure: $STRUCTURE_LINE" diff --git a/install.sh b/install.sh index 57b36af9b..a6650e2f9 100755 --- a/install.sh +++ b/install.sh @@ -1,8 +1,6 @@ #!/usr/bin/env bash cd "$(dirname "$0")" -# TODO: Use REPO_ROOT instead of base -# Also, when scripts are sourced they do not need export to inherit vars -export base="$(pwd)" +# Use REPO_ROOT instead of base - when scripts are sourced they do not need export to inherit vars REPO_ROOT="$(pwd)" source ./sdata/lib/environment-variables.sh source ./sdata/lib/functions.sh diff --git a/sdata/lib/functions.sh b/sdata/lib/functions.sh index c9012120d..990c96028 100644 --- a/sdata/lib/functions.sh +++ b/sdata/lib/functions.sh @@ -1,6 +1,8 @@ # This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang. # NOTE that you NOT need to `cd ..' because the `$0' is NOT this file, but the script file which will source this file. +# shellcheck shell=bash + # The script that use this file should have two lines on its top as follows: # cd "$(dirname "$0")" # export base="$(pwd)" @@ -9,7 +11,7 @@ function try { "$@" || sleep 0; } function v(){ echo -e "####################################################" echo -e "${STY_BLUE}[$0]: Next command:${STY_RST}" - echo -e "${STY_GREEN}$@${STY_RST}" + echo -e "${STY_GREEN}$*${STY_RST}" local execute=true if $ask;then while true;do @@ -29,14 +31,14 @@ function v(){ done fi if $execute;then x "$@";else - echo -e "${STY_YELLOW}[$0]: Skipped \"$@\"${STY_RST}" + echo -e "${STY_YELLOW}[$0]: Skipped \"$*\"${STY_RST}" fi } # When use v() for a defined function, use x() INSIDE its definition to catch errors. function x(){ if "$@";then local cmdstatus=0;else local cmdstatus=1;fi # 0=normal; 1=failed; 2=failed but ignored while [ $cmdstatus == 1 ] ;do - echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$@${STY_RED}\" has failed." + echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$*${STY_RED}\" has failed." echo -e "You may need to resolve the problem manually BEFORE repeating this command." echo -e "[Tip] If a certain package is failing to install, try installing it separately in another terminal.${STY_RST}" echo " r = Repeat this command (DEFAULT)" @@ -52,9 +54,9 @@ function x(){ esac done case $cmdstatus in - 0) echo -e "${STY_BLUE}[$0]: Command \"${STY_GREEN}$@${STY_BLUE}\" finished.${STY_RST}";; - 1) echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$@${STY_RED}\" has failed. Exiting...${STY_RST}";exit 1;; - 2) echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$@${STY_RED}\" has failed but ignored by user.${STY_RST}";; + 0) echo -e "${STY_BLUE}[$0]: Command \"${STY_GREEN}$*${STY_BLUE}\" finished.${STY_RST}";; + 1) echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$*${STY_RED}\" has failed. Exiting...${STY_RST}";exit 1;; + 2) echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$*${STY_RED}\" has failed but ignored by user.${STY_RST}";; esac } function showfun(){ diff --git a/sdata/lib/options-exp-update.sh b/sdata/lib/options-exp-update.sh index 93cb4c7f0..d815e9172 100644 --- a/sdata/lib/options-exp-update.sh +++ b/sdata/lib/options-exp-update.sh @@ -1,13 +1,20 @@ # Handle args for subcmd: exp-update +# shellcheck shell=bash showhelp(){ -echo -e "Syntax: $0 exp-update [OPTIONS]... +echo -e "Usage: install.sh exp-update [OPTIONS]... + +Experimental updating without full reinstall. +Updates dotfiles by syncing configuration files to home directory. + Options: -f, --force Force check all files even if no new commits -p, --packages Enable package checking and building -n, --dry-run Show what would be done without making changes -v, --verbose Enable verbose output -h, --help Show this help message + -s, --skip-notice Skip notice about script being untested + --non-interactive Run without prompting for user input This script updates your dotfiles by: 1. Auto-detecting repository structure (dots/ prefix or direct) @@ -15,12 +22,19 @@ This script updates your dotfiles by: 3. Optionally rebuilding packages (if -p flag is used) 4. Syncing configuration files to home directory 5. Updating script permissions + +Ignore file patterns support: + - Exact matches (e.g., 'path/to/file') + - Directory patterns (e.g., 'path/to/dir/') + - Wildcards (e.g., '*.log', 'path/*/file') + - Root-relative patterns (e.g., '/.config') + - Substring matching (prefix with '**', e.g., '**temp' matches any path containing 'temp') " } # `man getopt` to see more para=$(getopt \ -o hfpnv \ - -l help,force,packages,dry-run,verbose,skip-notice \ + -l help,force,packages,dry-run,verbose,skip-notice,non-interactive \ -n "$0" -- "$@") [ $? != 0 ] && echo "$0: Error when getopt, please recheck parameters." && exit 1 ##################################################################################### @@ -42,6 +56,7 @@ CHECK_PACKAGES=false DRY_RUN=false VERBOSE=false SKIP_NOTICE=false +NON_INTERACTIVE=false eval set -- "$para" while true ; do @@ -57,6 +72,8 @@ while true ; do # log_info "Verbose mode enabled" --skip-notice) SKIP_NOTICE=true;shift;; # log_warning "Skipping notice about script being untested" + --non-interactive) NON_INTERACTIVE=true;shift;; + # log_info "Non-interactive mode enabled" ## Ending --) break ;; diff --git a/sdata/lib/options-install.sh b/sdata/lib/options-install.sh index 9ff088132..6f1a4472a 100644 --- a/sdata/lib/options-install.sh +++ b/sdata/lib/options-install.sh @@ -1,4 +1,5 @@ # Handle args for subcmd: install +# shellcheck shell=bash showhelp(){ echo -e "Syntax: $0 [OPTIONS]... diff --git a/sdata/lib/options.sh b/sdata/lib/options.sh index 38982d6cd..8138c5173 100644 --- a/sdata/lib/options.sh +++ b/sdata/lib/options.sh @@ -1,6 +1,8 @@ # This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang. # NOTE that you NOT need to `cd ..' because the `$0' is NOT this file, but the script file which will source this file. +# shellcheck shell=bash + # The script that use this file should have two lines on its top as follows: # cd "$(dirname "$0")" export base="$(pwd)" showhelp_global(){ @@ -35,11 +37,12 @@ Subcommand: Subcommand: exp-update Using experimental update script. Options for exp-update: - -u, --update-force Force check all files even if no new commits (update script) + -f, --force Force check all files even if no new commits (update script) -p, --packages Enable package checking and building (update script) -n, --dry-run Show what would be done without making changes (update script) -v, --verbose Enable verbose output (update script) --skip-notice Skip warning notice (for experimental scripts) + --non-interactive Run without prompting for user input " } diff --git a/sdata/lib/package-installers.sh b/sdata/lib/package-installers.sh index 96067bef4..68c76ced2 100644 --- a/sdata/lib/package-installers.sh +++ b/sdata/lib/package-installers.sh @@ -2,6 +2,8 @@ # This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang. # NOTE that you NOT need to `cd ..' because the `$0' is NOT this file, but the script file which will source this file. +# shellcheck shell=bash + # This file is provided for any distros, mainly non-Arch(based) distros. # The script that use this file should have two lines on its top as follows: @@ -9,8 +11,8 @@ # export base="$(pwd)" install-agsv1(){ - x mkdir -p $base/cache/agsv1 - x cd $base/cache/agsv1 + x mkdir -p $REPO_ROOT/cache/agsv1 + x cd $REPO_ROOT/cache/agsv1 try git init -b main try git remote add origin https://github.com/Aylur/ags.git x git pull origin main && git submodule update --init --recursive @@ -20,12 +22,12 @@ install-agsv1(){ x meson setup build # --reconfigure x meson install -C build x sudo mv /usr/local/bin/ags{,v1} - x cd $base + x cd $REPO_ROOT } install-Rubik(){ - x mkdir -p $base/cache/Rubik - x cd $base/cache/Rubik + x mkdir -p $REPO_ROOT/cache/Rubik + x cd $REPO_ROOT/cache/Rubik try git init -b main try git remote add origin https://github.com/googlefonts/rubik.git x git pull origin main && git submodule update --init --recursive @@ -35,12 +37,12 @@ install-Rubik(){ x sudo cp OFL.txt /usr/local/share/licenses/ttf-rubik/LICENSE x fc-cache -fv x gsettings set org.gnome.desktop.interface font-name 'Rubik 11' - x cd $base + x cd $REPO_ROOT } install-Gabarito(){ - x mkdir -p $base/cache/Gabarito - x cd $base/cache/Gabarito + x mkdir -p $REPO_ROOT/cache/Gabarito + x cd $REPO_ROOT/cache/Gabarito try git init -b main try git remote add origin https://github.com/naipefoundry/gabarito.git x git pull origin main && git submodule update --init --recursive @@ -49,12 +51,12 @@ install-Gabarito(){ x sudo mkdir -p /usr/local/share/licenses/ttf-gabarito/ x sudo cp OFL.txt /usr/local/share/licenses/ttf-gabarito/LICENSE x fc-cache -fv - x cd $base + x cd $REPO_ROOT } install-OneUI(){ - x mkdir -p $base/cache/OneUI4-Icons - x cd $base/cache/OneUI4-Icons + x mkdir -p $REPO_ROOT/cache/OneUI4-Icons + x cd $REPO_ROOT/cache/OneUI4-Icons try git init -b main try git remote add origin https://github.com/end-4/OneUI4-Icons.git # try git remote add origin https://github.com/mjkim0727/OneUI4-Icons.git @@ -63,12 +65,12 @@ install-OneUI(){ x sudo cp -r OneUI /usr/local/share/icons x sudo cp -r OneUI-dark /usr/local/share/icons x sudo cp -r OneUI-light /usr/local/share/icons - x cd $base + x cd $REPO_ROOT } install-bibata(){ - x mkdir -p $base/cache/bibata-cursor - x cd $base/cache/bibata-cursor + x mkdir -p $REPO_ROOT/cache/bibata-cursor + x cd $REPO_ROOT/cache/bibata-cursor name="Bibata-Modern-Classic" file="$name.tar.xz" # Use axel because `curl -O` always downloads a file with 0 byte size, idk why @@ -76,12 +78,12 @@ install-bibata(){ tar -xf $file x sudo mkdir -p /usr/local/share/icons x sudo cp -r $name /usr/local/share/icons - x cd $base + x cd $REPO_ROOT } install-MicroTeX(){ - x mkdir -p $base/cache/MicroTeX - x cd $base/cache/MicroTeX + x mkdir -p $REPO_ROOT/cache/MicroTeX + x cd $REPO_ROOT/cache/MicroTeX try git init -b master try git remote add origin https://github.com/NanoMichael/MicroTeX.git x git pull origin master && git submodule update --init --recursive @@ -92,7 +94,7 @@ install-MicroTeX(){ x sudo mkdir -p /opt/MicroTeX x sudo cp ./LaTeX /opt/MicroTeX/ x sudo cp -r ./res /opt/MicroTeX/ - x cd $base + x cd $REPO_ROOT } install-uv(){ diff --git a/sdata/step/0.install-greeting.sh b/sdata/step/0.install-greeting.sh index 212f9bbfe..413aacf64 100644 --- a/sdata/step/0.install-greeting.sh +++ b/sdata/step/0.install-greeting.sh @@ -1,6 +1,8 @@ # This script is meant to be sourced. # It's not for directly running. +# shellcheck shell=bash + ##################################################################################### printf "${STY_CYAN}[$0]: Hi there! Before we start:${STY_RST}\n" diff --git a/sdata/step/2.install-setups-selector.sh b/sdata/step/2.install-setups-selector.sh index 73d554ec5..92c67d40a 100644 --- a/sdata/step/2.install-setups-selector.sh +++ b/sdata/step/2.install-setups-selector.sh @@ -1,6 +1,8 @@ # This script is meant to be sourced. # It's not for directly running. +# shellcheck shell=bash + #################### # Detect distro # Helpful link(s): diff --git a/sdata/step/3.install-files.sh b/sdata/step/3.install-files.sh index d213e75c6..51c436b1d 100644 --- a/sdata/step/3.install-files.sh +++ b/sdata/step/3.install-files.sh @@ -1,6 +1,8 @@ # This script is meant to be sourced. # It's not for directly running. +# shellcheck shell=bash + # TODO: https://github.com/end-4/dots-hyprland/issues/2137 function warning_rsync(){ @@ -179,7 +181,7 @@ warn_files_tests+=(/usr/local/share/licenses/ttf-gabarito) warn_files_tests+=(/usr/local/share/icons/OneUI{,-dark,-light}) warn_files_tests+=(/usr/local/share/icons/Bibata-Modern-Classic) warn_files_tests+=(/usr/local/bin/{LaTeX,res}) -for i in ${warn_files_tests[@]}; do +for i in "${warn_files_tests[@]}"; do echo $i test -f $i && warn_files+=($i) test -d $i && warn_files+=($i) @@ -223,6 +225,6 @@ if [[ -z "${ILLOGICAL_IMPULSE_VIRTUAL_ENV}" ]]; then printf "\n${STY_RED}[$0]: \!! Important \!! : Please ensure environment variable ${STY_RST} \$ILLOGICAL_IMPULSE_VIRTUAL_ENV ${STY_RED} is set to proper value (by default \"~/.local/state/quickshell/.venv\"), or Quickshell config will not work. We have already provided this configuration in ~/.config/hypr/hyprland/env.conf, but you need to ensure it is included in hyprland.conf, and also a restart is needed for applying it.${STY_RST}\n" fi -if [[ ! -z "${warn_files[@]}" ]]; then +if [[ ${#warn_files[@]} -gt 0 ]]; then printf "\n${STY_RED}[$0]: \!! Important \!! : Please delete ${STY_RST} ${warn_files[*]} ${STY_RED} manually as soon as possible, since we\'re now using AUR package or local PKGBUILD to install them for Arch(based) Linux distros, and they'll take precedence over our installation, or at least take up more space.${STY_RST}\n" fi diff --git a/sdata/step/exp-uninstall.sh b/sdata/step/exp-uninstall.sh index 03e095cd4..44525fd36 100644 --- a/sdata/step/exp-uninstall.sh +++ b/sdata/step/exp-uninstall.sh @@ -1,6 +1,8 @@ # This script is meant to be sourced. # It's not for directly running. +# shellcheck shell=bash + printf 'Hi there!\n' printf 'This script 1. will uninstall [end-4/dots-hyprland > illogical-impulse] dotfiles\n' printf ' 2. will try to revert *mostly everything* installed using install.sh, so it'\''s pretty destructive\n' @@ -40,7 +42,7 @@ starship.toml thorium-flags.conf ) -for i in ${dirs[@]} +for i in "${dirs[@]}" do v rm -rf "$XDG_CONFIG_HOME/$i" done diff --git a/sdata/step/exp-update-tester.sh b/sdata/step/exp-update-tester.sh index d62c04a28..7c111b83e 100755 --- a/sdata/step/exp-update-tester.sh +++ b/sdata/step/exp-update-tester.sh @@ -1,15 +1,13 @@ #!/usr/bin/env bash # -# exp-update-tester.sh - Test suite for update.sh +# exp-update-tester.sh - Test suite for update.sh (sourced subcommand) # set -euo pipefail # Colors RED='\033[0;31m' GREEN='\033[0;32m' -YELLOW='\033[1;33m' BLUE='\033[0;34m' -CYAN='\033[0;36m' NC='\033[0m' TESTS_PASSED=0 @@ -32,22 +30,22 @@ log_fail() { ((TESTS_FAILED++)) } -log_info() { - echo -e "${YELLOW}[INFO]${NC} $1" +log_error() { + echo -e "${RED}[ERROR]${NC} $1" } # Setup test environment setup_test_env() { local temp_dir temp_dir=$(mktemp -d -t dotfiles-test.XXXXXX) - + cd "$temp_dir" || { echo "Failed to cd to test directory"; return 1; } git init -q git config user.email "test@example.com" git config user.name "Test User" - + git commit --allow-empty -m "Initial commit" -q - + echo "$temp_dir" } @@ -63,10 +61,10 @@ cleanup_test_env() { run_test() { local test_name="$1" local test_func="$2" - + # Cleanup before test cleanup_test_env - + # Run the test if $test_func; then echo "✓ $test_name passed" @@ -79,18 +77,18 @@ run_test() { # Test 1: Script exists and is executable test_script_exists() { - log_test "Checking if update.sh exists and is executable" - - if [[ ! -f "update.sh" ]]; then - log_fail "update.sh not found" + log_test "Checking if install.sh exists and is executable" + + if [[ ! -f "install.sh" ]]; then + log_fail "install.sh not found" return 1 fi - - if [[ ! -x "update.sh" ]]; then - log_fail "update.sh is not executable" + + if [[ ! -x "install.sh" ]]; then + log_fail "install.sh is not executable" return 1 fi - + log_pass "Script exists and is executable" return 0 } @@ -98,8 +96,8 @@ test_script_exists() { # Test 2: Script has no syntax errors test_syntax() { log_test "Checking script syntax" - - if bash -n update.sh; then + + if bash -n install.sh; then log_pass "No syntax errors found" return 0 else @@ -111,8 +109,8 @@ test_syntax() { # Test 3: Help option works test_help_option() { log_test "Testing --help option" - - if ./update.sh --help 2>&1 | grep -q "Usage:"; then + + if ./install.sh exp-update --help 2>&1 | grep -qiE "(Usage|Options|exp-update)"; then log_pass "Help option works" return 0 else @@ -124,52 +122,39 @@ test_help_option() { # Test 4: Test repository structure detection (dots/ prefix) test_dots_structure() { log_test "Testing dots/ prefix structure detection" - + local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" - + cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } - + mkdir -p dots/.config/test-app mkdir -p dots/.local/bin echo "test config" > dots/.config/test-app/config.conf - + git add . git commit -m "Add dots structure" -q - - cat > test_detection.sh << 'EOF' + + cat > test_detection.sh << EOF #!/bin/bash +# Mock logging and style functions/variables +log_info() { :; } +log_warning() { :; } +log_error() { :; } +log_success() { :; } +log_header() { :; } +log_die() { echo "ERROR: \$1"; exit 1; } +STY_CYAN="" STY_RST="" STY_YELLOW="" + REPO_ROOT="$1" -detect_repo_structure() { - local found_dirs=() - if [[ -d "${REPO_ROOT}/dots/.config" ]]; then - found_dirs+=("dots/.config") - [[ -d "${REPO_ROOT}/dots/.local/bin" ]] && found_dirs+=("dots/.local/bin") - elif [[ -d "${REPO_ROOT}/.config" ]]; then - found_dirs+=(".config") - [[ -d "${REPO_ROOT}/.local/bin" ]] && found_dirs+=(".local/bin") - else - for candidate in "dots/.config" ".config" "dots/.local/bin" ".local/bin"; do - if [[ -d "${REPO_ROOT}/${candidate}" ]]; then - if [[ ! " ${found_dirs[*]} " =~ " ${candidate} " ]]; then - found_dirs+=("${candidate}") - fi - fi - done - fi - if [[ ${#found_dirs[@]} -eq 0 ]]; then - echo "ERROR" >&2 - return 1 - fi - echo "${found_dirs[@]}" -} +source "$ORIGINAL_DIR/sdata/step/exp-update.sh" detect_repo_structure EOF - + chmod +x test_detection.sh result=$(./test_detection.sh "$test_repo") - + if [[ "$result" == *"dots/.config"* ]]; then log_pass "Dots structure detected correctly" cd "$ORIGINAL_DIR" @@ -184,52 +169,39 @@ EOF # Test 5: Test flat structure detection test_flat_structure() { log_test "Testing flat structure detection" - + local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" - + cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } - + mkdir -p .config/test-app mkdir -p .local/bin echo "test config" > .config/test-app/config.conf - + git add . git commit -m "Add flat structure" -q - - cat > test_detection.sh << 'EOF' + + cat > test_detection.sh << EOF #!/bin/bash +# Mock logging and style functions/variables +log_info() { :; } +log_warning() { :; } +log_error() { :; } +log_success() { :; } +log_header() { :; } +log_die() { echo "ERROR: \$1"; exit 1; } +STY_CYAN="" STY_RST="" STY_YELLOW="" + REPO_ROOT="$1" -detect_repo_structure() { - local found_dirs=() - if [[ -d "${REPO_ROOT}/dots/.config" ]]; then - found_dirs+=("dots/.config") - [[ -d "${REPO_ROOT}/dots/.local/bin" ]] && found_dirs+=("dots/.local/bin") - elif [[ -d "${REPO_ROOT}/.config" ]]; then - found_dirs+=(".config") - [[ -d "${REPO_ROOT}/.local/bin" ]] && found_dirs+=(".local/bin") - else - for candidate in "dots/.config" ".config" "dots/.local/bin" ".local/bin"; do - if [[ -d "${REPO_ROOT}/${candidate}" ]]; then - if [[ ! " ${found_dirs[*]} " =~ " ${candidate} " ]]; then - found_dirs+=("${candidate}") - fi - fi - done - fi - if [[ ${#found_dirs[@]} -eq 0 ]]; then - echo "ERROR" >&2 - return 1 - fi - echo "${found_dirs[@]}" -} +source "$ORIGINAL_DIR/sdata/step/exp-update.sh" detect_repo_structure EOF - + chmod +x test_detection.sh result=$(./test_detection.sh "$test_repo") - + if [[ "$result" == *".config"* ]] && [[ "$result" != *"dots/"* ]]; then log_pass "Flat structure detected correctly" cd "$ORIGINAL_DIR" @@ -283,47 +255,22 @@ EOF mkdir -p .config mkdir -p secrets - cat > test_ignore.sh << 'EOF' + cat > test_ignore.sh << EOF #!/bin/bash +# Mock logging and style functions/variables +log_info() { :; } +log_warning() { :; } +log_error() { :; } +log_success() { :; } +log_header() { :; } +log_die() { echo "ERROR: \$1"; exit 1; } +STY_CYAN="" STY_RST="" STY_YELLOW="" + REPO_ROOT="$1" UPDATE_IGNORE_FILE="${REPO_ROOT}/.updateignore" HOME_UPDATE_IGNORE_FILE="/dev/null" - -should_ignore() { - local file_path="$1" - local relative_path="${file_path#$HOME/}" - local repo_relative="" - if [[ "$file_path" == "$REPO_ROOT"* ]]; then - repo_relative="${file_path#$REPO_ROOT/}" - fi - - for ignore_file in "$UPDATE_IGNORE_FILE" "$HOME_UPDATE_IGNORE_FILE"; do - if [[ -f "$ignore_file" ]]; then - while IFS= read -r pattern || [[ -n "$pattern" ]]; do - [[ -z "$pattern" || "$pattern" =~ ^[[:space:]]*# ]] && continue - pattern=$(echo "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - [[ -z "$pattern" ]] && continue - - if [[ "$relative_path" == "$pattern" ]] || [[ "$repo_relative" == "$pattern" ]]; then - return 0 - fi - if [[ "$relative_path" == $pattern ]] || [[ "$repo_relative" == $pattern ]]; then - return 0 - fi - if [[ "$pattern" == */ ]]; then - local dir_pattern="${pattern%/}" - if [[ "$relative_path" == "$dir_pattern"/* ]] || [[ "$repo_relative" == "$dir_pattern"/* ]]; then - return 0 - fi - fi - if [[ "$file_path" == *"$pattern"* ]] || [[ "$relative_path" == *"$pattern"* ]]; then - return 0 - fi - done <"$ignore_file" - fi - done - return 1 -} +# Source the production script to use the real should_ignore function +source "$ORIGINAL_DIR/sdata/step/exp-update.sh" test_cases=( "$REPO_ROOT/app.log:0" @@ -335,7 +282,7 @@ test_cases=( all_passed=true for test_case in "${test_cases[@]}"; do - IFS=':' read -r file expected <<< "$test_case" + IFS=":" read -r file expected <<< "$test_case" mkdir -p "$(dirname "$file")" touch "$file" @@ -376,16 +323,22 @@ EOF # Test 8: Test safe_read security - COMPLETELY NON-INTERACTIVE test_safe_read_security() { log_test "Testing safe_read uses secure assignment (printf -v)" - - # Check that safe_read uses printf -v and not eval - if grep -A 10 "safe_read()" update.sh | grep -q "printf -v.*varname"; then - log_pass "safe_read uses secure printf -v assignment" - return 0 - elif grep -A 10 "safe_read()" update.sh | grep -q "eval.*varname"; then - log_fail "safe_read uses vulnerable eval assignment" + + local awk_script='/^safe_read() \{/,/^\}/' + local safe_read_function + safe_read_function=$(awk "$awk_script" "$ORIGINAL_DIR/sdata/step/exp-update.sh") + + if [[ -z "$safe_read_function" ]]; then + log_fail "Could not find safe_read function" return 1 + fi + + # Check for secure printf -v assignment and absence of eval + if echo "$safe_read_function" | grep -q "printf -v" && ! echo "$safe_read_function" | grep -q "eval"; then + log_pass "safe_read uses secure printf -v assignment and no eval" + return 0 else - log_fail "Cannot determine safe_read assignment method" + log_fail "safe_read does not use secure assignment or contains eval" return 1 fi } @@ -393,25 +346,25 @@ test_safe_read_security() { # Test 9: Test dry-run mode test_dry_run() { log_test "Testing dry-run mode" - + local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" - + cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } - - mkdir -p dots/.config/test-app - echo "repo config" > dots/.config/test-app/config.conf - + + # Copy necessary files for install.sh to run + cp "$ORIGINAL_DIR/install.sh" . + cp -r "$ORIGINAL_DIR/sdata" . + cp -r "$ORIGINAL_DIR/dots" . + chmod +x install.sh + git add . git commit -m "Add test config" -q - - cp "$ORIGINAL_DIR/update.sh" . - chmod +x update.sh - - # Use printf to pipe responses automatically - printf "y\ny\n" | ./update.sh -n --skip-notice 2>&1 | tee dry_run_output.txt - + + # Use non-interactive mode and check for DRY-RUN marker + ./install.sh exp-update -n --skip-notice --non-interactive 2>&1 | tee dry_run_output.txt + if grep -q "DRY-RUN" dry_run_output.txt; then log_pass "Dry-run mode detected in output" else @@ -419,7 +372,7 @@ test_dry_run() { cd "$ORIGINAL_DIR" return 1 fi - + if [[ ! -f "${HOME}/.config/test-app/config.conf" ]]; then log_pass "No files created in home during dry-run" else @@ -428,7 +381,7 @@ test_dry_run() { cd "$ORIGINAL_DIR" return 1 fi - + cd "$ORIGINAL_DIR" return 0 } @@ -436,20 +389,20 @@ test_dry_run() { # Test 10: Test command-line flags test_flags() { log_test "Testing command-line flags" - + # Only test non-interactive flags local flags=("-h" "--help") local all_passed=true - + for flag in "${flags[@]}"; do - if ./update.sh "$flag" 2>&1 | grep -q -E "(Usage|help)"; then - log_info " ✓ $flag recognized" + if ./install.sh exp-update "$flag" 2>&1 | grep -qiE "(Usage|Options|exp-update)"; then + log_test " ✓ $flag recognized" else - log_info " ✗ $flag not recognized" + log_test " ✗ $flag not recognized" all_passed=false fi done - + if [[ "$all_passed" == true ]]; then log_pass "Help flags recognized correctly" return 0 @@ -464,11 +417,11 @@ test_shellcheck() { log_test "Running shellcheck (if available)" if ! command -v shellcheck &>/dev/null; then - log_info "shellcheck not found, skipping static analysis" + log_test "shellcheck not found, skipping static analysis" return 0 fi - if shellcheck -e SC1090,SC1091,SC2148,SC2034,SC2155,SC2164 update.sh; then + if shellcheck -e SC1090,SC1091,SC2148,SC2034,SC2155,SC2164 install.sh; then log_pass "shellcheck passed" return 0 else @@ -477,45 +430,87 @@ test_shellcheck() { fi } -# Test 12: Test fresh clone scenario -test_fresh_clone() { - log_test "Testing fresh clone scenario" - +# Test 13: Test ** substring ignore patterns +test_substring_ignore_patterns() { + log_test "Testing ** substring ignore pattern matching" + local test_repo test_repo=$(setup_test_env) TEST_DIR="$test_repo" - - cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } - - mkdir -p .config/test-app - echo "config" > .config/test-app/settings.conf - - cat > test_fresh_clone.sh << 'EOF' -#!/bin/bash -has_new_commits() { - if git rev-parse --verify HEAD@{1} &>/dev/null; then - [[ "$(git rev-parse HEAD)" != "$(git rev-parse HEAD@{1})" ]] - else - return 0 - fi -} -if has_new_commits; then + cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; } + + cat > .updateignore << 'EOF' +**temp** +**backup** +**test** +EOF + + mkdir -p .config/test-app + mkdir -p temp-backup-dir + mkdir -p .local/share/test-temp + mkdir -p .config/temp-file + + cat > test_substring_ignore.sh << EOF +#!/bin/bash +# Mock logging and style functions/variables +log_info() { :; } +log_warning() { :; } +log_error() { :; } +log_success() { :; } +log_header() { :; } +log_die() { echo "ERROR: \$1"; exit 1; } +STY_CYAN="" STY_RST="" STY_YELLOW="" + +REPO_ROOT="$1" +UPDATE_IGNORE_FILE="${REPO_ROOT}/.updateignore" +HOME_UPDATE_IGNORE_FILE="/dev/null" +# Source the production script to use the real should_ignore function +source "$ORIGINAL_DIR/sdata/step/exp-update.sh" + +test_cases=( + "$REPO_ROOT/temp-backup-dir/file:0" + "$REPO_ROOT/.config/test-app/temp.conf:0" + "$REPO_ROOT/.local/share/test-temp/data:0" + "$REPO_ROOT/.config/temp-file/config:0" + "$REPO_ROOT/normal-config:1" +) + +all_passed=true +for test_case in "${test_cases[@]}"; do + IFS=":" read -r file expected <<< "$test_case" + mkdir -p "$(dirname "$file")" + touch "$file" + + if should_ignore "$file"; then + result=0 + else + result=1 + fi + + if [[ $result -ne $expected ]]; then + echo "FAIL: $file (expected: $expected, got: $result)" + all_passed=false + fi +done + +if [[ "$all_passed" == true ]]; then echo "PASS" else echo "FAIL" fi EOF - - chmod +x test_fresh_clone.sh - result=$(./test_fresh_clone.sh) - + + chmod +x test_substring_ignore.sh + result=$(./test_substring_ignore.sh "$test_repo") + if [[ "$result" == "PASS" ]]; then - log_pass "Fresh clone scenario handled correctly" + log_pass "** substring ignore patterns work correctly" cd "$ORIGINAL_DIR" return 0 else - log_fail "Fresh clone scenario not handled properly" + log_fail "** substring ignore patterns failed" + echo "$result" cd "$ORIGINAL_DIR" return 1 fi @@ -524,32 +519,32 @@ EOF # Main test runner main() { echo -e "${BLUE}================================${NC}" - echo -e "${BLUE} Update.sh Test Suite${NC}" + echo -e "${BLUE} Update.sh Test Suite (Sourced Subcommand)${NC}" echo -e "${BLUE}================================${NC}\n" - - if [[ ! -f "update.sh" ]]; then - log_error "Please run this test from the directory containing update.sh" + + if [[ ! -f "install.sh" ]]; then + log_error "Please run this test from the directory containing install.sh" exit 1 fi - - chmod +x update.sh 2>/dev/null || true - + + chmod +x install.sh 2>/dev/null || true + # Define tests tests=( "test_script_exists" - "test_syntax" + "test_syntax" "test_help_option" "test_dots_structure" "test_flat_structure" "test_dots_mapping" "test_ignore_patterns" + "test_substring_ignore_patterns" "test_safe_read_security" "test_dry_run" "test_flags" "test_shellcheck" - "test_fresh_clone" ) - + # Run tests for test in "${tests[@]}"; do if $test; then @@ -559,7 +554,7 @@ main() { fi echo done - + # Summary echo -e "${BLUE}================================${NC}" echo -e "${BLUE} Test Summary${NC}" @@ -567,7 +562,7 @@ main() { echo -e "${GREEN}Passed: $TESTS_PASSED${NC}" echo -e "${RED}Failed: $TESTS_FAILED${NC}" echo -e "${BLUE}Total: ${#tests[@]}${NC}\n" - + if [[ $TESTS_FAILED -eq 0 ]]; then echo -e "${GREEN}All tests passed! 🎉${NC}\n" exit 0 @@ -581,7 +576,7 @@ main() { cleanup() { echo "Cleaning up test files..." cleanup_test_env - rm -f test_detection.sh test_ignore.sh test_safe_read.sh test_fresh_clone.sh dry_run_output.txt 2>/dev/null || true + rm -f test_detection.sh test_ignore.sh test_safe_read.sh test_fresh_clone.sh test_substring_ignore.sh dry_run_output.txt 2>/dev/null || true rm -rf "${HOME}/.config/test-app" 2>/dev/null || true } diff --git a/sdata/step/exp-update.sh b/sdata/step/exp-update.sh old mode 100644 new mode 100755 index 6b4cfa45c..fc5cf320c --- a/sdata/step/exp-update.sh +++ b/sdata/step/exp-update.sh @@ -1,6 +1,8 @@ # This script is meant to be sourced. # It's not for directly running. +# shellcheck shell=bash + ##################################################################################### # # update.sh - Enhanced dotfiles update script @@ -10,7 +12,12 @@ # - Pull latest commits from remote # - Rebuild packages if PKGBUILD files changed (user choice) # - Handle config file conflicts with user choices -# - Respect .updateignore file for exclusions +# - Respect .updateignore file for exclusions with flexible pattern matching: +# - Exact matches (e.g., "path/to/file") +# - Directory patterns (e.g., "path/to/dir/") +# - Wildcards (e.g., "*.log", "path/*/file") +# - Root-relative patterns (e.g., "/.config") +# - Substring matching (prefix with "**", e.g., "**temp" matches any path containing "temp") # set -euo pipefail @@ -47,7 +54,7 @@ detect_repo_structure() { [[ -d "${REPO_ROOT}/.local/share" ]] && found_dirs+=(".local/share") else # Manual detection of common directories - for candidate in "dots/.config" ".config" "config" "dots/.local/bin" ".local/bin" "dots/.local/share" ".local/share"; do + for candidate in "dots/.config" ".config" "dots/.local/bin" ".local/bin" "dots/.local/share" ".local/share"; do if [[ -d "${REPO_ROOT}/${candidate}" ]]; then # Avoid duplicates if [[ ! " ${found_dirs[*]} " =~ " ${candidate} " ]]; then @@ -94,8 +101,6 @@ safe_read() { fi fi } - -# Function to check if a file should be ignored should_ignore() { local file_path="$1" local relative_path="${file_path#$HOME/}" @@ -161,9 +166,10 @@ should_ignore() { done fi - # Simple substring matching (for backward compatibility) - if [[ ! "$should_skip" == true ]]; then - if [[ "$file_path" == *"$pattern"* ]] || [[ "$relative_path" == *"$pattern"* ]]; then + # Substring matching (only if pattern starts with '**') + if [[ ! "$should_skip" == true && "$pattern" == \*\** ]]; then + local substring_pattern="${pattern#\*\*}" # Remove the leading '**' + if [[ -n "$substring_pattern" && ("$file_path" == *"$substring_pattern"* || "$relative_path" == *"$substring_pattern"*) ]]; then should_skip=true fi fi @@ -417,7 +423,6 @@ list_packages() { build_packages() { local build_mode="$1" local packages_to_build=() - local rebuilt_packages=0 case "$build_mode" in "changed") @@ -474,7 +479,6 @@ build_packages() { log_info "Package building cancelled by user" return fi - for pkg_name in "${packages_to_build[@]}"; do pkg_dir="${ARCH_PACKAGES_DIR}/${pkg_name}" @@ -490,7 +494,10 @@ build_packages() { continue fi - cd "$pkg_dir" || continue + cd "$pkg_dir" || { + log_error "Failed to change to package directory: $pkg_dir" + continue + } if makepkg -si --noconfirm; then log_success "Successfully built and installed $pkg_name" @@ -655,9 +662,20 @@ rebuilt_packages=0 if [[ "$CHECK_PACKAGES" == true ]]; then log_header "Package Management" - if [[ ! -d "$ARCH_PACKAGES_DIR" ]]; then - log_warning "No packages directory found (tried: dist-arch, arch-packages, sdist/arch). Skipping package management." + # Check if required Arch Linux tools are available + if ! command -v pacman &>/dev/null || ! command -v makepkg &>/dev/null; then + log_warning "Arch Linux package management tools (pacman/makepkg) not found." + log_warning "Skipping package management as this appears to be a non-Arch Linux system." + log_warning "Use -p/--packages flag only on Arch Linux systems." + PKG_TOOLS_AVAILABLE=false else + PKG_TOOLS_AVAILABLE=true + fi + + if [[ "$PKG_TOOLS_AVAILABLE" == true ]]; then + if [[ ! -d "$ARCH_PACKAGES_DIR" ]]; then + log_warning "No packages directory found (tried: dist-arch, arch-packages, sdist/arch). Skipping package management." + else changed_pkgbuilds=() for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do if [[ -f "${pkg_dir}/PKGBUILD" ]]; then @@ -678,7 +696,19 @@ if [[ "$CHECK_PACKAGES" == true ]]; then echo "4) Skip package building" echo - if safe_read "Choose an option (1-4): " pkg_choice "1"; then + if [[ "$NON_INTERACTIVE" == true ]]; then + pkg_choice="1" + log_info "Non-interactive mode: Using default package option: $pkg_choice" + elif safe_read "Choose an option (1-4): " pkg_choice "1"; then + if [[ "$VERBOSE" == true ]]; then + log_info "User selected package option: $pkg_choice" + fi + else + log_warning "Failed to read input. Skipping package building." + pkg_choice="" + fi + + if [[ -n "$pkg_choice" ]]; then case $pkg_choice in 1) build_packages "changed" ;; 2) @@ -689,14 +719,23 @@ if [[ "$CHECK_PACKAGES" == true ]]; then 3) build_packages "all" ;; 4 | *) log_info "Skipping package building" ;; esac - else - log_warning "Failed to read input. Skipping package building." fi else log_info "No PKGBUILDs have changed since last update." echo - if safe_read "Do you want to check and build packages anyway? (y/N): " check_anyway "N"; then - if [[ "$check_anyway" =~ ^[Yy]$ ]]; then + if [[ "$NON_INTERACTIVE" == true ]]; then + check_anyway="N" + log_info "Non-interactive mode: Using default for check packages anyway: $check_anyway" + elif safe_read "Do you want to check and build packages anyway? (y/N): " check_anyway "N"; then + if [[ "$VERBOSE" == true ]]; then + log_info "User chose to check packages anyway: $check_anyway" + fi + else + log_warning "Failed to read input. Skipping package management." + check_anyway="" + fi + + if [[ -n "$check_anyway" && "$check_anyway" =~ ^[Yy]$ ]]; then if list_packages; then echo echo "Package build options:" @@ -717,26 +756,11 @@ if [[ "$CHECK_PACKAGES" == true ]]; then else log_info "Skipping package management" fi - else - log_info "Skipping package management" fi fi - fi -else - log_header "Package Management" - log_info "Package checking disabled. Use -p or --packages flag to enable package management." - - if [[ -d "$ARCH_PACKAGES_DIR" ]]; then - changed_count=0 - for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do - if [[ -f "${pkg_dir}/PKGBUILD" ]] && check_pkgbuild_changed "$pkg_dir"; then - ((changed_count++)) - fi - done - - if [[ $changed_count -gt 0 ]]; then - log_warning "Note: $changed_count package(s) have changed PKGBUILDs. Use -p flag to manage packages." - fi + else + log_header "Package Management" + log_info "Package checking disabled. Use -p or --packages flag to enable package management." fi fi @@ -794,9 +818,16 @@ if [[ "$process_files" == true ]]; then home_file="${home_dir_path}/${rel_path}" if should_ignore "$home_file"; then + if [[ "$VERBOSE" == true ]]; then + log_info "Ignored: $rel_path (matches ignore pattern)" + fi continue fi + if [[ "$VERBOSE" == true ]]; then + log_info "Processing: $rel_path" + fi + ((files_processed++)) if [[ "$DRY_RUN" != true ]]; then