diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12624ac..5d264c1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: - name: Install test dependencies run: sudo apt-get update && sudo apt-get install -y bats shellcheck - name: Run shellcheck tests - run: shellcheck alf setup uninstall + run: shellcheck alf setup uninstall test/test_helper.bash - name: Run Bats tests run: bats --recursive --print-output-on-failure test diff --git a/AGENTS.md b/AGENTS.md index 81559b2..65219c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,13 @@ This repository is a Bashly-based CLI. Prefer working from the source partials i - `src/lib/*.sh` - `settings.yml` +## Helper Commands + +- `op.conf` is the repo-local source of truth for common helper commands such as + testing, ShellCheck, formatting, generation, and docs tasks. +- Prefer the `op.conf` entries when looking up or running standard project + workflows. + ## Generated Artifact Policy - Only inspect `./alf` when you need to verify generated output, trace a generation issue, or confirm the final emitted behavior. diff --git a/alf b/alf index 2a0680c..c7e2aad 100755 --- a/alf +++ b/alf @@ -499,8 +499,8 @@ find_config() { # src/lib/generate_completion.sh generate_completions() { - ali1_regex="^([a-z0-9\-]+):" - ali2_regex="^ +([a-z0-9\-]+):" + ali1_regex="^([a-zA-Z0-9\-]+):" + ali2_regex="^ +([a-zA-Z0-9\-]+):" find_config echo '# Completions' @@ -625,7 +625,7 @@ generate_last_cmd() { # src/lib/has_subcommands.sh has_subcommands() { - regex="^ +([a-z0-9\-]+):" + regex="^ +([a-zA-Z0-9\-]+):" find_config while IFS= read -r line || [ -n "$line" ]; do @@ -763,7 +763,14 @@ upload_repo() { pushd "$repo_path" >/dev/null echo "Pushing $repo_path to repository" - git commit -am "automatic push" + git add -A + + if git diff --cached --quiet; then + echo "No local changes to commit" + else + git commit -m "automatic push" + fi + git push popd >/dev/null } @@ -1449,7 +1456,7 @@ alf_info_parse_requirements() { # :command.initialize initialize() { declare -g version="0.6.1" - set -e + set -eo pipefail # src/initialize.sh aliases_file=${ALF_ALIASES_FILE:-~/.bash_aliases} diff --git a/op.conf b/op.conf index e97b8fe..0bce72a 100644 --- a/op.conf +++ b/op.conf @@ -12,7 +12,7 @@ watch: filewatcher --immediate "src/**/*.*" "bashly generate" test: bats --recursive --print-output-on-failure --abort "${1:-test}" #? Run all tests or a given test -shellcheck: shellcheck alf setup uninstall && green "PASS" +shellcheck: shellcheck alf setup uninstall test/test_helper.bash && green "PASS" #? Run shellcheck shfmt: shfmt -ci -i 2 -d alf setup uninstall && green "PASS" diff --git a/settings.yml b/settings.yml index 47f1dd4..8345ec1 100644 --- a/settings.yml +++ b/settings.yml @@ -2,6 +2,7 @@ commands_dir: commands enable_view_markers: always enable_env_var_names_array: never enable_deps_array: never +strict: set -eo pipefail usage_colors: caption: bold diff --git a/src/lib/download_repo.sh b/src/lib/download_repo.sh index 3bdec80..6cdf7c4 100644 --- a/src/lib/download_repo.sh +++ b/src/lib/download_repo.sh @@ -1,5 +1,5 @@ download_repo() { - find_config + find_config || return 1 if [[ ! -f $rc_file ]]; then echo "Cannot find $rc_file" diff --git a/src/lib/find_config.sh b/src/lib/find_config.sh index c16e03a..3f83b35 100644 --- a/src/lib/find_config.sh +++ b/src/lib/find_config.sh @@ -11,6 +11,6 @@ find_config() { echo "You should either:" echo "- Run this command in a folder with 'alf.conf' file, or" echo "- Run 'alf connect' to properly connect to a remote config" - exit 1 + return 1 fi -} \ No newline at end of file +} diff --git a/src/lib/generate_completion.sh b/src/lib/generate_completion.sh index daeb20f..e6e0958 100644 --- a/src/lib/generate_completion.sh +++ b/src/lib/generate_completion.sh @@ -1,7 +1,7 @@ generate_completions() { - ali1_regex="^([a-z0-9\-]+):" - ali2_regex="^ +([a-z0-9\-]+):" - find_config + ali1_regex="^([a-zA-Z0-9\-]+):" + ali2_regex="^ +([a-zA-Z0-9\-]+):" + find_config || return 1 echo '# Completions' echo 'if command -v complete &> /dev/null ; then' @@ -28,4 +28,3 @@ generate_completions() { echo 'fi' } - diff --git a/src/lib/generate_config.sh b/src/lib/generate_config.sh index 3756588..cfe6e7a 100644 --- a/src/lib/generate_config.sh +++ b/src/lib/generate_config.sh @@ -3,7 +3,7 @@ generate_config() { state="simple" lastcmd="" case_open="false" - find_config + find_config || return 1 echo "# This file was automatically generated by alf" echo "# https://github.com/dannyben/alf" @@ -64,4 +64,3 @@ generate_config() { generate_completions fi } - diff --git a/src/lib/has_subcommands.sh b/src/lib/has_subcommands.sh index da57327..3e69726 100644 --- a/src/lib/has_subcommands.sh +++ b/src/lib/has_subcommands.sh @@ -1,7 +1,7 @@ ## Returns true if the config has subcommands has_subcommands() { - regex="^ +([a-z0-9\-]+):" - find_config + regex="^ +([a-zA-Z0-9\-]+):" + find_config || return 1 while IFS= read -r line || [ -n "$line" ]; do if [[ $line =~ $regex ]]; then @@ -10,4 +10,4 @@ has_subcommands() { done <"$config_file" return 1 -} \ No newline at end of file +} diff --git a/src/lib/save_config.sh b/src/lib/save_config.sh index 573f9b8..4da77b9 100644 --- a/src/lib/save_config.sh +++ b/src/lib/save_config.sh @@ -1,5 +1,5 @@ save_config() { - find_config + find_config || return 1 tilde='~' friendly_aliases_file="${aliases_file/#$HOME/$tilde}" diff --git a/src/lib/show_alias_command.sh b/src/lib/show_alias_command.sh index a47c582..4708d61 100644 --- a/src/lib/show_alias_command.sh +++ b/src/lib/show_alias_command.sh @@ -3,7 +3,7 @@ show_alias_command() { local subcode="${2:-}" local regex_exact_code regex_exact_subcode cmd1 cmd2 - find_config + find_config || return 1 regex_exact_code="^($code): *(.+)$" regex_exact_subcode="^( +)($subcode): *(.+)$" cmd1="" diff --git a/src/lib/show_info.sh b/src/lib/show_info.sh index c063329..b5e4eae 100644 --- a/src/lib/show_info.sh +++ b/src/lib/show_info.sh @@ -1,5 +1,5 @@ show_info() { - find_config + find_config || return 1 if [[ -d "$repo_path/.git" ]]; then pushd "$repo_path" >/dev/null diff --git a/src/lib/upload_repo.sh b/src/lib/upload_repo.sh index 9def189..4c045ce 100644 --- a/src/lib/upload_repo.sh +++ b/src/lib/upload_repo.sh @@ -1,5 +1,5 @@ upload_repo() { - find_config + find_config || return 1 if [[ ! -f $rc_file ]]; then echo "Cannot find $rc_file" @@ -9,7 +9,14 @@ upload_repo() { pushd "$repo_path" >/dev/null echo "Pushing $repo_path to repository" - git commit -am "automatic push" + git add -A + + if git diff --cached --quiet; then + echo "No local changes to commit" + else + git commit -m "automatic push" + fi + git push popd >/dev/null } diff --git a/test/fixtures/generate_completion/with-uppercase-subcommands/alf.conf b/test/fixtures/generate_completion/with-uppercase-subcommands/alf.conf new file mode 100644 index 0000000..ea7a638 --- /dev/null +++ b/test/fixtures/generate_completion/with-uppercase-subcommands/alf.conf @@ -0,0 +1,7 @@ +G: git + S: status + L: log +dir: ls +DC: docker-compose + DEPLOY: stack deploy + UPD: up -d diff --git a/test/fixtures/generate_completion/with-uppercase-subcommands/expected.txt b/test/fixtures/generate_completion/with-uppercase-subcommands/expected.txt new file mode 100644 index 0000000..536c258 --- /dev/null +++ b/test/fixtures/generate_completion/with-uppercase-subcommands/expected.txt @@ -0,0 +1,5 @@ +# Completions +if command -v complete &> /dev/null ; then + complete -W "S L" G + complete -W "DEPLOY UPD" DC +fi diff --git a/test/fixtures/generate_config/with-uppercase-subcommands/alf.conf b/test/fixtures/generate_config/with-uppercase-subcommands/alf.conf new file mode 100644 index 0000000..ac4d4a6 --- /dev/null +++ b/test/fixtures/generate_config/with-uppercase-subcommands/alf.conf @@ -0,0 +1,5 @@ +G: git + S: status + DEPLOY: !docker stack deploy +DNS: dig +short + CHECK: !host -t ns diff --git a/test/fixtures/generate_config/with-uppercase-subcommands/expected.sh b/test/fixtures/generate_config/with-uppercase-subcommands/expected.sh new file mode 100644 index 0000000..88c49ea --- /dev/null +++ b/test/fixtures/generate_config/with-uppercase-subcommands/expected.sh @@ -0,0 +1,38 @@ +# This file was automatically generated by alf +# https://github.com/dannyben/alf + +unalias G 1>/dev/null 2>&1 +G() { + case "$1" in + S) + shift + git status "$@" + ;; + DEPLOY) + shift + docker stack deploy "$@" + ;; + *) + git "$@" + ;; + esac +} + +unalias DNS 1>/dev/null 2>&1 +DNS() { + case "$1" in + CHECK) + shift + host -t ns "$@" + ;; + *) + dig +short "$@" + ;; + esac +} + +# Completions +if command -v complete &> /dev/null ; then + complete -W "S DEPLOY" G + complete -W "CHECK" DNS +fi diff --git a/test/fixtures/has_subcommands/with-uppercase-subcommands/alf.conf b/test/fixtures/has_subcommands/with-uppercase-subcommands/alf.conf new file mode 100644 index 0000000..a1603c8 --- /dev/null +++ b/test/fixtures/has_subcommands/with-uppercase-subcommands/alf.conf @@ -0,0 +1,5 @@ +G: git + S: status + L: log +dc: docker-compose + DEPLOY: stack deploy diff --git a/test/lib/generate_completion.bats b/test/lib/generate_completion.bats index 348f0be..f3f46cd 100644 --- a/test/lib/generate_completion.bats +++ b/test/lib/generate_completion.bats @@ -22,6 +22,18 @@ teardown() { [ "$output" = "$(cat "$(fixture_path generate_completion/with-subcommands/expected.txt)")" ] } +@test "generate_completions preserves uppercase aliases and subcommands" { + export ALF_RC_FILE + ALF_RC_FILE="$(fixture_path generate_completion/with-uppercase-subcommands/alfrc)" + cd "$(fixture_path generate_completion/with-uppercase-subcommands)" || exit 1 + source_libs find_config generate_completion + + run generate_completions + + [ "$status" -eq 0 ] + [ "$output" = "$(cat "$(fixture_path generate_completion/with-uppercase-subcommands/expected.txt)")" ] +} + @test "generate_completions emits only the wrapper when there are no subcommands" { export ALF_RC_FILE ALF_RC_FILE="$(fixture_path generate_completion/without-subcommands/alfrc)" diff --git a/test/lib/generate_config.bats b/test/lib/generate_config.bats index 72fda1d..1355e1c 100644 --- a/test/lib/generate_config.bats +++ b/test/lib/generate_config.bats @@ -34,6 +34,18 @@ teardown() { [ "$output" = "$(cat "$(fixture_path generate_config/with-subcommands/expected.sh)")" ] } +@test "generate_config emits uppercase nested alias functions and completions" { + export ALF_RC_FILE + ALF_RC_FILE="$(fixture_path generate_config/with-uppercase-subcommands/alfrc)" + cd "$(fixture_path generate_config/with-uppercase-subcommands)" || exit 1 + source_libs find_config generate_last_cmd has_subcommands generate_completion generate_config + + run generate_config + + [ "$status" -eq 0 ] + [ "$output" = "$(cat "$(fixture_path generate_config/with-uppercase-subcommands/expected.sh)")" ] +} + @test "generate_config fails when no config can be found" { export ALF_RC_FILE ALF_RC_FILE="$(fixture_path generate_config/without-config/alfrc)" diff --git a/test/lib/has_subcommands.bats b/test/lib/has_subcommands.bats index eb06af3..c23bfe3 100644 --- a/test/lib/has_subcommands.bats +++ b/test/lib/has_subcommands.bats @@ -21,6 +21,17 @@ teardown() { [ "$?" -eq 0 ] } +@test "has_subcommands returns success when nested aliases use uppercase codes" { + export ALF_RC_FILE + ALF_RC_FILE="$(fixture_path has_subcommands/with-uppercase-subcommands/alfrc)" + cd "$(fixture_path has_subcommands/with-uppercase-subcommands)" || exit 1 + source_libs find_config has_subcommands + + has_subcommands + + [ "$?" -eq 0 ] +} + @test "has_subcommands returns failure when aliases are flat" { export ALF_RC_FILE ALF_RC_FILE="$(fixture_path has_subcommands/without-subcommands/alfrc)" diff --git a/test/lib/save_config.bats b/test/lib/save_config.bats index 3c9d490..f33e64a 100644 --- a/test/lib/save_config.bats +++ b/test/lib/save_config.bats @@ -62,7 +62,7 @@ teardown() { find_config() { echo "ERROR: Cannot find config file" - exit 1 + return 1 } generate_config() { diff --git a/test/lib/upload_repo.bats b/test/lib/upload_repo.bats index 9f2f41c..46e20ee 100644 --- a/test/lib/upload_repo.bats +++ b/test/lib/upload_repo.bats @@ -25,12 +25,44 @@ teardown() { git() { printf '%s\n' "$1" >>"$TEST_ROOT/git.log" + + if [ "$1" = "diff" ]; then + return 1 + fi } upload_repo - [ "$(sed -n '1p' "$TEST_ROOT/git.log")" = "commit" ] - [ "$(sed -n '2p' "$TEST_ROOT/git.log")" = "push" ] + [ "$(sed -n '1p' "$TEST_ROOT/git.log")" = "add" ] + [ "$(sed -n '2p' "$TEST_ROOT/git.log")" = "diff" ] + [ "$(sed -n '3p' "$TEST_ROOT/git.log")" = "commit" ] + [ "$(sed -n '4p' "$TEST_ROOT/git.log")" = "push" ] +} + +@test "upload_repo pushes even when there is nothing to commit" { + export ALF_RC_FILE="$TEST_ROOT/alfrc" + repo_path="$TEST_ROOT/repo" + mkdir -p "$repo_path" + : >"$ALF_RC_FILE" + cd "$TEST_ROOT" || exit 1 + source_libs upload_repo + + find_config() { + : + } + + git() { + printf '%s\n' "$1" >>"$TEST_ROOT/git.log" + } + + run upload_repo + + [ "$status" -eq 0 ] + assert_output_contains "No local changes to commit" + [ "$(sed -n '1p' "$TEST_ROOT/git.log")" = "add" ] + [ "$(sed -n '2p' "$TEST_ROOT/git.log")" = "diff" ] + [ "$(sed -n '3p' "$TEST_ROOT/git.log")" = "push" ] + [ -z "$(sed -n '4p' "$TEST_ROOT/git.log")" ] } @test "upload_repo fails when no rc file exists" { diff --git a/test/test_helper.bash b/test/test_helper.bash index 08648f3..fc281b3 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -23,7 +23,7 @@ teardown_test_environment() { assert_output_contains() { local expected="$1" - [[ "$output" == *"$expected"* ]] + [[ "${output-}" == *"$expected"* ]] } fixture_path() { @@ -47,13 +47,16 @@ unset_functions() { source_libs() { local lib + # shellcheck disable=SC1091 source "$ALF_REPO_ROOT/src/initialize.sh" for lib in "$@"; do + # shellcheck disable=SC1090 source "$ALF_REPO_ROOT/src/lib/${lib}.sh" done } source_command() { + # shellcheck disable=SC1090 source "$ALF_REPO_ROOT/src/commands/${1}.sh" }