diff --git a/.gitignore b/.gitignore index bdd7f17..00d841b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,20 @@ /.idea/ /.obsidian/ + +# Local administrator configuration and runtime data +/wg-manager.conf +/backups/ +/logs/ +/state/ +/bin/gum + +# Generated client material +*.key +*.pub +*.psk +*-client-*.conf +*.conf.tmp + +# Isolated test output +/tests/tmp/ +/tests/output/ diff --git a/PLANS.md b/PLANS.md index 8388654..6ec1fb3 100644 --- a/PLANS.md +++ b/PLANS.md @@ -260,7 +260,7 @@ Allowed status values: `NOT STARTED`, `IN PROGRESS`, `BLOCKED`, `DONE`. | Milestone | Status | Outcome | |---|---|---| -| M0 | NOT STARTED | Test harness and safe project skeleton | +| M0 | DONE | Test harness and safe project skeleton | | M1 | NOT STARTED | Runtime foundation, CLI shell, and config loading | | M2 | NOT STARTED | WireGuard parser, client model, `status`, and `list` | | M3 | NOT STARTED | Locking, backups, atomic apply, and rollback engine | @@ -1551,6 +1551,20 @@ Use this template: - Next milestone: My ``` +## 2026-10-11 — M0 — DONE + +- Implemented: strict source-safe `wg-manager.sh` help shell, placeholder configuration, runtime ignores, isolated pure-Bash test harness, minimal WireGuard fixture, and deny-by-default logged mocks for all planned external commands. +- Coordinator: `gpt-5.6-sol` (`high`) +- Subagents: `milestone_worker` / `milestone_repairer`, `gpt-5.6-luna` (`medium`) +- Repair cycles: 2 +- Sol review: replaced a tautological host-path check with a recursive pure-Bash guard; made mocks deny empty calls unless explicitly authorized and assert exact invocation logs; removed an unused production path variable, added a narrow ShellCheck source directive, and cleared unintended index staging. +- Verification: `bash -n wg-manager.sh`, `bash -n tests/run.sh`, `bash -n tests/helpers.sh`, `bash -n tests/mocks/bin/*`, `bash tests/run.sh`, `./wg-manager.sh help`, `(cd /tmp && /home/sasa/share/bash/wireguard-scripts/wg-manager.sh help)`, `git diff --check`, `git diff --no-index --check /dev/null `, `git status --short` +- Result: all required checks passed; 13 test assertions passed; unknown commands and extra arguments returned status 64; executable modes and isolated mock behavior were verified. Optional `shellcheck wg-manager.sh tests/run.sh tests/helpers.sh tests/mocks/bin/*` was `SKIP` because ShellCheck is unavailable. +- Decisions: none +- Known limitations: real WireGuard, networking, service, firewall, route, sysctl, journal, and deployment-server checks were not run; M0 verification is fixture- and mock-based as required. +- Suggested commit message: `test: add isolated Bash test harness and project skeleton` +- Next milestone: M1 + # 8. Final completion rule The MVP is complete only when: diff --git a/tests/fixtures/wg0.conf b/tests/fixtures/wg0.conf new file mode 100644 index 0000000..bff2177 --- /dev/null +++ b/tests/fixtures/wg0.conf @@ -0,0 +1,9 @@ +[Interface] +Address = 192.0.2.1/24 +ListenPort = 51820 +PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + +[Peer] +# Non-secret fixture placeholder; not used against a live host. +PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= +AllowedIPs = 192.0.2.2/32 diff --git a/tests/helpers.sh b/tests/helpers.sh new file mode 100755 index 0000000..5ebc672 --- /dev/null +++ b/tests/helpers.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +umask 077 + +readonly HELPERS_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +readonly PROJECT_DIR="$(cd -- "${HELPERS_DIR}/.." && pwd)" +TEST_ROOT='' +TEST_MOCK_LOG='' +TEST_FAILURES=0 +TEST_ASSERTIONS=0 + +setup_test_environment() { + TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/wg-manager-tests.XXXXXX")" + TEST_MOCK_LOG="${TEST_ROOT}/mock-invocations.log" + : >"$TEST_MOCK_LOG" + chmod 700 "$TEST_ROOT" + cp -- "${PROJECT_DIR}/tests/fixtures/wg0.conf" "${TEST_ROOT}/wg0.conf" + export TEST_ROOT TEST_MOCK_LOG + export PATH="${PROJECT_DIR}/tests/mocks/bin:${PATH:-}" + trap cleanup_test_environment EXIT INT TERM +} + +cleanup_test_environment() { + local status=$? + trap - EXIT INT TERM + if [[ -n "$TEST_ROOT" && -d "$TEST_ROOT" ]]; then + rm -rf -- "$TEST_ROOT" + fi + return "$status" +} + +begin_test() { + printf 'TEST %s\n' "$1" +} + +record_failure() { + printf ' FAIL: %s\n' "$1" >&2 + ((TEST_FAILURES += 1)) +} + +assert_equal() { + local expected=$1 actual=$2 message=${3:-значения равны} + ((TEST_ASSERTIONS += 1)) + if [[ "$expected" != "$actual" ]]; then + record_failure "${message}: ожидалось [${expected}], получено [${actual}]" + fi +} + +assert_status() { + local expected=$1 actual=$2 message=${3:-код завершения} + assert_equal "$expected" "$actual" "$message" +} + +assert_contains() { + local needle=$1 haystack=$2 message=${3:-строка содержит ожидаемый текст} + ((TEST_ASSERTIONS += 1)) + if [[ "$haystack" != *"$needle"* ]]; then + record_failure "${message}: не найдено [${needle}]" + fi +} + +assert_file_exists() { + local path=$1 message=${2:-файл существует} + ((TEST_ASSERTIONS += 1)) + if [[ ! -e "$path" ]]; then + record_failure "${message}: отсутствует [${path}]" + fi +} + +assert_file_mode() { + local expected=$1 path=$2 actual message + actual=$(stat -c '%a' -- "$path") + message=${3:-режим файла} + assert_equal "$expected" "$actual" "$message" +} + +assert_success() { + local status=$1 message=${2:-команда успешна} + assert_status 0 "$status" "$message" +} + +assert_failure() { + local status=$1 message=${2:-команда завершилась с ошибкой} + ((TEST_ASSERTIONS += 1)) + if (( status == 0 )); then + record_failure "$message: ожидался ненулевой код" + fi +} + +finish_tests() { + if (( TEST_FAILURES == 0 )); then + printf 'PASS: %d проверок\n' "$TEST_ASSERTIONS" + return 0 + fi + printf 'FAIL: %d ошибок из %d проверок\n' "$TEST_FAILURES" "$TEST_ASSERTIONS" >&2 + return 1 +} diff --git a/tests/mocks/bin/gum b/tests/mocks/bin/gum new file mode 100755 index 0000000..5c9ca66 --- /dev/null +++ b/tests/mocks/bin/gum @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +command_name=${0##*/} +: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}" +allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-} +printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG" +if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then + printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2 + exit 99 +fi +if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then + exit 0 +fi +printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2 +exit 99 diff --git a/tests/mocks/bin/ip b/tests/mocks/bin/ip new file mode 100755 index 0000000..5c9ca66 --- /dev/null +++ b/tests/mocks/bin/ip @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +command_name=${0##*/} +: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}" +allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-} +printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG" +if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then + printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2 + exit 99 +fi +if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then + exit 0 +fi +printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2 +exit 99 diff --git a/tests/mocks/bin/iptables b/tests/mocks/bin/iptables new file mode 100755 index 0000000..5c9ca66 --- /dev/null +++ b/tests/mocks/bin/iptables @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +command_name=${0##*/} +: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}" +allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-} +printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG" +if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then + printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2 + exit 99 +fi +if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then + exit 0 +fi +printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2 +exit 99 diff --git a/tests/mocks/bin/journalctl b/tests/mocks/bin/journalctl new file mode 100755 index 0000000..5c9ca66 --- /dev/null +++ b/tests/mocks/bin/journalctl @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +command_name=${0##*/} +: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}" +allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-} +printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG" +if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then + printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2 + exit 99 +fi +if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then + exit 0 +fi +printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2 +exit 99 diff --git a/tests/mocks/bin/nft b/tests/mocks/bin/nft new file mode 100755 index 0000000..5c9ca66 --- /dev/null +++ b/tests/mocks/bin/nft @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +command_name=${0##*/} +: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}" +allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-} +printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG" +if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then + printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2 + exit 99 +fi +if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then + exit 0 +fi +printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2 +exit 99 diff --git a/tests/mocks/bin/qrencode b/tests/mocks/bin/qrencode new file mode 100755 index 0000000..5c9ca66 --- /dev/null +++ b/tests/mocks/bin/qrencode @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +command_name=${0##*/} +: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}" +allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-} +printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG" +if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then + printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2 + exit 99 +fi +if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then + exit 0 +fi +printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2 +exit 99 diff --git a/tests/mocks/bin/ss b/tests/mocks/bin/ss new file mode 100755 index 0000000..5c9ca66 --- /dev/null +++ b/tests/mocks/bin/ss @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +command_name=${0##*/} +: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}" +allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-} +printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG" +if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then + printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2 + exit 99 +fi +if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then + exit 0 +fi +printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2 +exit 99 diff --git a/tests/mocks/bin/sysctl b/tests/mocks/bin/sysctl new file mode 100755 index 0000000..5c9ca66 --- /dev/null +++ b/tests/mocks/bin/sysctl @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +command_name=${0##*/} +: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}" +allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-} +printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG" +if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then + printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2 + exit 99 +fi +if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then + exit 0 +fi +printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2 +exit 99 diff --git a/tests/mocks/bin/systemctl b/tests/mocks/bin/systemctl new file mode 100755 index 0000000..5c9ca66 --- /dev/null +++ b/tests/mocks/bin/systemctl @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +command_name=${0##*/} +: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}" +allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-} +printf '%q' "$command_name" >>"$TEST_MOCK_LOG"; for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done; printf '\n' >>"$TEST_MOCK_LOG" +if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then + printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2 + exit 99 +fi +if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then + exit 0 +fi +printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2 +exit 99 diff --git a/tests/mocks/bin/wg b/tests/mocks/bin/wg new file mode 100755 index 0000000..8ca9618 --- /dev/null +++ b/tests/mocks/bin/wg @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +command_name=${0##*/} +: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}" +allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-} +printf '%q' "$command_name" >>"$TEST_MOCK_LOG" +for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done +printf '\n' >>"$TEST_MOCK_LOG" +if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then + printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2 + exit 99 +fi +if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then + exit 0 +fi +printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2 +exit 99 diff --git a/tests/mocks/bin/wg-quick b/tests/mocks/bin/wg-quick new file mode 100755 index 0000000..8ca9618 --- /dev/null +++ b/tests/mocks/bin/wg-quick @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +command_name=${0##*/} +: "${TEST_MOCK_LOG:?TEST_MOCK_LOG must identify the isolated test log}" +allow_empty=${MOCK_ALLOW_EMPTY_INVOCATION:-} +printf '%q' "$command_name" >>"$TEST_MOCK_LOG" +for argument in "$@"; do printf ' %q' "$argument" >>"$TEST_MOCK_LOG"; done +printf '\n' >>"$TEST_MOCK_LOG" +if [[ -n "$allow_empty" && "$allow_empty" != "$command_name" ]]; then + printf 'Unexpected mock invocation: %s does not allow empty calls for %s\n' "$command_name" "$allow_empty" >&2 + exit 99 +fi +if [[ $# == 0 && "$allow_empty" == "$command_name" ]]; then + exit 0 +fi +printf 'Unexpected mock invocation: %s received arguments\n' "$command_name" >&2 +exit 99 diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..da65193 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +umask 077 + +readonly TESTS_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=helpers.sh +source "${TESTS_DIR}/helpers.sh" + +setup_test_environment + +begin_test 'sourcing entry point does not execute main' +source_output=$(bash -c 'source "$1"; printf sourced' _ "${PROJECT_DIR}/wg-manager.sh") +assert_equal sourced "$source_output" 'безопасная загрузка' + +begin_test 'help succeeds without root or dependencies' +set +e +help_output=$("${PROJECT_DIR}/wg-manager.sh" help 2>&1) +help_status=$? +set -e +assert_success "$help_status" 'команда help' +assert_contains 'Использование:' "$help_output" 'справка' + +begin_test 'unknown command fails' +set +e +unknown_output=$("${PROJECT_DIR}/wg-manager.sh" unknown 2>&1) +unknown_status=$? +set -e +assert_failure "$unknown_status" 'неизвестная команда' +assert_contains 'неизвестная команда' "$unknown_output" 'сообщение об ошибке' + +begin_test 'help works outside project directory' +set +e +outside_output=$(cd /tmp && "${PROJECT_DIR}/wg-manager.sh" help 2>&1) +outside_status=$? +set -e +assert_success "$outside_status" 'запуск из /tmp' +assert_contains 'Использование:' "$outside_output" 'справка из /tmp' + +begin_test 'mock guard accepts only explicitly supported empty invocation' +set +e +MOCK_ALLOW_EMPTY_INVOCATION=wg wg +expected_mock_status=$? +wg +unapproved_empty_status=$? +wg unexpected +unexpected_mock_status=$? +set -e +assert_success "$expected_mock_status" 'разрешённый вызов мока' +assert_failure "$unapproved_empty_status" 'неразрешённый пустой вызов мока' +assert_failure "$unexpected_mock_status" 'неожиданный вызов мока' +expected_mock_log=$'wg\nwg\nwg unexpected\n' +actual_mock_log=$(<"$TEST_MOCK_LOG") +actual_mock_log+=$'\n' +assert_equal "$expected_mock_log" "$actual_mock_log" 'точный журнал вызовов мока' + +begin_test 'test writes stay below isolated root' +assert_file_exists "${TEST_ROOT}/wg0.conf" 'копия фикстуры' +assert_equal "$TEST_ROOT" "${TEST_MOCK_LOG%/*}" 'журнал находится в тестовом корне' + +scan_test_sources_for_host_path() { + local forbidden_path='/etc' + forbidden_path+='/wireguard' + + scan_path() { + local path line + for path in "$1"/*; do + if [[ -d "$path" ]]; then + scan_path "$path" + elif [[ -f "$path" ]]; then + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" == *"$forbidden_path"* ]]; then + record_failure "тестовый файл содержит запрещённый путь: ${path}" + return + fi + done <"$path" + fi + done + } + + scan_path "$TESTS_DIR" +} + +scan_test_sources_for_host_path + +finish_tests diff --git a/wg-manager.conf.example b/wg-manager.conf.example new file mode 100644 index 0000000..0eb5664 --- /dev/null +++ b/wg-manager.conf.example @@ -0,0 +1,19 @@ +# Safe example configuration. Copy to wg-manager.conf and review before use. +WG_INTERFACE="wg0" +WG_CONF="/etc/wireguard/wg0.conf" +CLIENT_DIR="/etc/wireguard/clients" + +SERVER_ENDPOINT="vpn.example.invalid" +SERVER_PORT="51820" +SERVER_VPN_IP="192.0.2.1" +CLIENT_DNS="192.0.2.53" + +VPN_SUBNET_PREFIX="192.0.2" +START_HOST="2" +END_HOST="254" + +USE_IPV6="0" +IPV6_PREFIX="2001:db8:ffff::" + +DEFAULT_CLIENT_MODE="full-tunnel" +RECENT_HANDSHAKE_SECONDS="180" diff --git a/wg-manager.sh b/wg-manager.sh new file mode 100755 index 0000000..4a180eb --- /dev/null +++ b/wg-manager.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +umask 077 + +print_usage() { + cat <<'USAGE' +Использование: + wg-manager.sh help + +Команды WireGuard Manager будут добавлены в следующих версиях. +USAGE +} + +main() { + if (( $# != 1 )); then + printf 'Ошибка: ожидается одна команда.\n' >&2 + print_usage >&2 + return 64 + fi + + case "$1" in + help) + print_usage + ;; + *) + printf 'Ошибка: неизвестная команда: %s\n' "$1" >&2 + print_usage >&2 + return 64 + ;; + esac +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi