Skip to content

Commit dde36c2

Browse files
committed
feat: strengthen validation logic and redesign data structure of internal state
- refactor: enforce convention - all side effect variables must be written in UPPERCASE - chore: add custom Git hooks for automatic testing before remote pushing
1 parent d56508b commit dde36c2

15 files changed

Lines changed: 934 additions & 535 deletions

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,5 @@ temp/
3737

3838
# Misc
3939
node_modules
40-
hooks
4140
.memory-bank/
4241
memory-bank/

.gitmodules

Whitespace-only changes.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Licensed under [BSD 3-Clause License](LICENSE).
2222
## Features
2323

2424
- **Zero dependencies:** All code is original and thoroughly audited by the author.
25-
- **Options as the SSOT:** No positional arguments or subcommands—simplicity is prioritized.
25+
- **Options-only design:** No positional arguments or subcommands—options are the single source of truth, prioritizing simplicity.
2626
- **Structured option handling:** Options are defined with clear metadata, allowing reliable registration, parsing, and validation.
2727
- **Multi-level, color-coded logging:** Output messages at five levels (Debug, Info, Warning, Error, Critical) with color support.
2828
- **Reliable locking mechanism:** Prevents concurrent script execution.
@@ -194,7 +194,7 @@ main "$@"
194194

195195
## Design Decisions
196196

197-
- **Option-only approach:** No subcommands or positional arguments. Simplicity takes precedence.
197+
- **Options-only approach:** This template is designed to handle options exclusively—no subcommands or positional arguments. For complex multi-command CLIs, use mature frameworks like Click (Python), Cobra (Go), or similar tools that are battle-tested for subcommand handling.
198198
- **Conservative default syntax** (e.g. `${param:-}`): Use only for **optional arguments** and explicit exceptions. **AVOID overusing on every variable.**
199199
- **Fail-fast philosophy:**
200200
- Enable `set -e` to terminate the script when undefined variables are referenced unexpectedly.

build.sh

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,36 @@
33
# ============================================================================ #
44

55
## FILE : build.sh
6-
## VERSION : v3.0.0
6+
## VERSION : 1.0.0
77
## DESCRIPTION : Merge source and script into template
88
## AUTHOR : silverbullet069
99
## REPOSITORY : https://github.com/Silverbullet069/bash-script-template
1010
## LICENSE : BSD-3-Clause
1111

12-
## TEMREPO : https://github.com/Silverbullet069/bash-script-template
13-
## TEMMODE : lite
14-
## TEMVER : v3.0.0
15-
## TEMUPDATED : 2025-06-21 19:15:03.788041997 +0700
16-
## TEMLIC : BSD-3-Clause
17-
1812
# ============================================================================ #
1913

14+
# DESC: An 'echo' wrapper that redirects standard output to standard error
15+
# ARGS: $@ (required): Message(s) to echo
16+
# OUTS: None
17+
# RETS: None
18+
function log() {
19+
echo "$@" >&2
20+
}
21+
22+
# FUNCTION: check_binary
23+
# DESC: Checks if a given binary/command is available in the system's PATH.
24+
# ARGS: $1 (required): Name of the binary/command to check.
25+
# OUTS: Prints an error message to stderr if the binary is missing.
26+
# RETS: Returns 1 if the binary is missing, 0 otherwise.
27+
function check_binary() {
28+
if ! command -v "$1" >/dev/null 2>&1; then
29+
log "Missing dependency '$1'"
30+
return 1
31+
fi
32+
}
33+
2034
# DESC: Acquire script lock, extracted from script.sh
21-
# ARGS: $1 (optional): Scope of script execution lock (system or user)
35+
# ARGS: $1 (required): Scope of script execution lock (system or user)
2236
# OUTS: None
2337
# RETS: None
2438
# NOTE: This lock implementation is extremely simple but should be reliable
@@ -32,15 +46,15 @@ function lock_init() {
3246
elif [[ "${1}" = "user" ]]; then
3347
lock_dir="/tmp/$(basename "${BASH_SOURCE[0]}").${UID}.lock"
3448
else
35-
echo "Missing or invalid argument to ${FUNCNAME[0]}()!" >&2
49+
log "Missing or invalid argument to ${FUNCNAME[0]}()!"
3650
exit 1
3751
fi
3852

3953
if mkdir "${lock_dir}" 2>/dev/null; then
4054
readonly script_lock="${lock_dir}"
41-
echo "Acquired script lock: ${script_lock}"
55+
log "Acquired script lock: ${script_lock}"
4256
else
43-
echo "Unable to acquire script lock: ${lock_dir}" >&2
57+
log "Unable to acquire script lock: ${lock_dir}"
4458
exit 2
4559
fi
4660
}
@@ -53,7 +67,7 @@ function script_trap_exit() {
5367
# Remove script execution lock
5468
if [[ -d "${script_lock-}" ]]; then
5569
rmdir "${script_lock}"
56-
echo "Clean up script lock: ${script_lock}" >&2
70+
log "Clean up script lock: ${script_lock}"
5771
fi
5872
}
5973

@@ -76,22 +90,22 @@ function build() {
7690
local -r template_path="${3}"
7791

7892
if [[ ! -f "${source_path}" ]]; then
79-
echo "source.sh not found: ${script_path}" >&2
93+
log "source.sh not found: ${script_path}"
8094
exit 1
8195
fi
8296

8397
if [[ ! -r "${source_path}" ]]; then
84-
echo "source.sh is unreadable: ${script_path}" >&2
98+
log "source.sh is unreadable: ${script_path}"
8599
exit 1
86100
fi
87101

88102
if [[ ! -f "${script_path}" ]]; then
89-
echo "script.sh not found: ${script_path}" >&2
103+
log "script.sh not found: ${script_path}"
90104
exit 1
91105
fi
92106

93107
if [[ ! -r "${script_path}" ]]; then
94-
echo "script.sh is unreadable: ${script_path}" >&2
108+
log "script.sh is unreadable: ${script_path}"
95109
exit 1
96110
fi
97111

@@ -100,19 +114,22 @@ function build() {
100114
local -r source_body=$(tail -n +12 "${source_path}")
101115
local -r script_body=$(tail -n +19 "${script_path}" | grep -vE -e '^# shellcheck source=source.sh$' -e '^# shellcheck disable=SC1091$' -e '^source.*source\.sh"$')
102116

117+
# temporily make it writeable
103118
chmod 755 "${template_path}"
104119
{
105-
echo "${script_header}"
106-
echo "${source_body}"
107-
echo "${script_body}"
108-
} >"${template_path}"
120+
log "${script_header}"
121+
log "${source_body}"
122+
log "${script_body}"
123+
} 2>"${template_path}"
109124

125+
# then, make it read-only
110126
chmod 555 "${template_path}"
111-
echo "Build ${template_path} successfully."
127+
128+
log "Build ${template_path} successfully."
112129
}
113130

114131
function cleanup() {
115-
echo "Stopping file monitor..."
132+
log "Stopping file monitor..."
116133
exit 0
117134
}
118135

@@ -138,7 +155,7 @@ function main() {
138155
--event "close_write" \
139156
"${source_path}" "${script_path}" \
140157
| while read -r dir event file; do
141-
echo "Change detected: ${event} on ${dir}${file}"
158+
log "Change detected: ${event} on ${dir}${file}"
142159
# NOTE: add a small delay to allow accumulation of multiple changes
143160
sleep 1
144161
build "${source_path}" "${script_path}" "${template_path}"

clone.sh

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,14 @@
33
# ============================================================================ #
44

55
## FILE : clone.sh
6-
## VERSION : v3.0.0
6+
## VERSION : 1.0.0
77
## DESCRIPTION : Clone specific template into specific output
88
## AUTHOR : silverbullet069
99
## REPOSITORY : https://github.com/Silverbullet069/bash-script-template
1010
## LICENSE : BSD 3-Clause License
1111

1212
## TEMREPO : https://github.com/Silverbullet069/bash-script-template
13-
## TEMMODE : src
14-
## TEMVER : v3.0.0
15-
## TEMUPDATED : 2025-07-09 02:22:09.981519546 +0700
13+
## TEMAUTHOR : Silverbullet069
1614
## TEMLIC : BSD 3-Clause License
1715

1816
# ============================================================================ #
@@ -24,16 +22,30 @@
2422
# OUTS: SCRIPT_PARSED_VALUES populated with parsed parameters
2523
# RETS: 0
2624
function option_init() {
27-
# NOTE: the order: long-name, short-name, default, help, type, required, constraints
28-
register_option "--mode" "-m" "lite" "Template mode" "choice" true "full,lite,legacy,src"
29-
register_option "--output" "-o" "${PWD}/temp.sh" "Output file path" "path"
30-
register_option "--yes" "-y" false "Skip prompting for metadata" "bool"
31-
32-
register_option "--help" "-h" false "Display this help and exit" "bool"
33-
register_option "--log-level" "-l" "INF" "Specify log level" "choice" false "DBG,INF,WRN,ERR"
34-
register_option "--timestamp" "-t" false "Enable timestamp output" "bool"
35-
register_option "--no-color" "-n" false "Disable color output" "bool"
36-
register_option "--quiet" "-q" false "Run silently unless an error is encountered" "bool"
25+
register_builtin_options
26+
27+
register_option \
28+
--long "--mode" \
29+
--short "-m" \
30+
--type "choice" \
31+
--default "lite" \
32+
--required "true" \
33+
--constraints "full,lite,legacy,src"\
34+
--help "Template mode"
35+
36+
register_option \
37+
--long "--output" \
38+
--short "-o" \
39+
--type "path" \
40+
--default "${PWD}/temp.sh" \
41+
--help "Output file path"
42+
43+
register_option \
44+
--long "--yes" \
45+
--short "-y" \
46+
--type "bool" \
47+
--default "false" \
48+
--help "Skip prompting for metadata"
3749
}
3850

3951
function print_help_message() {
@@ -53,7 +65,7 @@ To create a 'full' template with custom output destination:
5365
5466
clone -m full -o path/to/script.bash
5567
56-
By default, the script will prompt for information to be placed inside the
68+
By default, the script will prompt for information to be placed inside the
5769
header. To skip prompting:
5870
5971
clone -y ...
@@ -82,15 +94,15 @@ function main() {
8294
local -r mode="${VALUES["--mode"]}"
8395
local file=
8496
case "${mode}" in
97+
legacy)
98+
file="template_legacy.sh"
99+
;;
85100
full)
86101
file="template.sh"
87102
;;
88103
lite)
89104
file="template_lite.sh"
90105
;;
91-
legacy)
92-
file="template_legacy.sh"
93-
;;
94106
src)
95107
file="script.sh"
96108
;;
@@ -102,7 +114,7 @@ function main() {
102114

103115
# Source path initialization
104116
# shellcheck disable=SC2154
105-
local -r src="${script_dir}/${file}"
117+
local -r src="${SCRIPT_DIR}/${file}"
106118
if [[ ! -f "${src}" ]]; then
107119
script_exit "${src} not found."
108120
fi
@@ -157,7 +169,7 @@ function main() {
157169

158170
# add value to template-related placeholders
159171
local -r updated="$(stat -c "%y" "${src}" 2>/dev/null)"
160-
local -r tag="$(ls -t "${script_dir}/.git/refs/tags" | head -n1)"
172+
local -r tag="$(ls -t "${SCRIPT_DIR}/.git/refs/tags" | head -n1)"
161173

162174
# Replace placeholders in the cloned file
163175
if ! sed -i \
@@ -175,7 +187,7 @@ function main() {
175187
fi
176188

177189
if [[ "${mode}" == "src" ]]; then
178-
local -r path="${script_dir}/source.sh"
190+
local -r path="${SCRIPT_DIR}/source.sh"
179191
if [[ ! -f "${path}" ]]; then
180192
script_exit "${path} not found"
181193
fi

hooks/pre-push

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env bash
2+
./test.sh "tests"
3+
if [[ $? -ne 0 ]]; then
4+
echo "BATS tests failed. Push aborted."
5+
exit 1
6+
fi

script.sh

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,20 @@
1717

1818
# ============================================================================ #
1919

20+
# ============================================================================ #
21+
# CUSTOM LOGIC #
22+
# ============================================================================ #
23+
2024
# DESC: Register the a set of options
2125
# ARGS: None
22-
# OUTS: OPTIONS, ORDERS and VALUES are populated with data
26+
# OUTS: OPTIONS, ORDERS and VALUES data are populated
2327
# RETS: 0
2428
function option_init() {
25-
# NOTE: long-name, short-name, default, help, type, required, constraints
26-
# register_option ...
27-
28-
# CAUTION: --help must be placed as the first option in the built-in options list
29-
# CAUTION: I add a blank link on top of this function inside help message
30-
register_option "--help" "-h" false "Display this help and exit" "bool"
31-
register_option "--log-level" "-l" "INF" "Specify log level" "choice" false "DBG,INF,WRN,ERR"
32-
register_option "--timestamp" "-t" false "Enable timestamp output" "bool"
33-
register_option "--no-color" "-n" false "Disable color output" "bool"
34-
register_option "--quiet" "-q" false "Run silently unless an error is encountered" "bool"
29+
# --help, --log-level, --timestamp, --no-color, --quiet
30+
register_builtin_options
31+
32+
# Custom options
33+
# ...
3534
}
3635

3736
# DESC: Print help message when user declare --help, -h option
@@ -56,6 +55,10 @@ EOF
5655

5756
}
5857

58+
# ============================================================================ #
59+
# MAIN CONTROL FLOW #
60+
# ============================================================================ #
61+
5962
# DESC: Main control flow
6063
# ARGS: $@ (optional): Arguments provided to the script
6164
# OUTS: None
@@ -72,24 +75,35 @@ function main() {
7275
lock_init user
7376

7477
# start here
75-
# shellcheck disable=SC2154
76-
debug "script_params: ${script_params}"
77-
# shellcheck disable=SC2154
78-
debug "script_path: ${script_path}"
79-
# shellcheck disable=SC2154
80-
debug "script_dir: ${script_dir}"
81-
# shellcheck disable=SC2154
82-
debug "script_name: ${script_name}"
78+
# ...
8379

8480
# Logging helper functions
8581
error "This is an error message"
8682
warn "This is a warning message"
8783
info "This is an info message"
8884
debug "This is a debug message"
85+
86+
# Logging internal states
87+
debug "SCRIPT_NAME: ${SCRIPT_NAME}"
88+
debug "SCRIPT_PATH: ${SCRIPT_PATH}"
89+
debug "SCRIPT_DIR: ${SCRIPT_DIR}"
90+
debug "SCRIPT_PARAMS: ${SCRIPT_PARAMS}"
91+
92+
debug "Registered options: ${ORDERS[*]}"
93+
debug "Parsed values:"
94+
for key in "${!VALUES[@]}"; do
95+
debug " ${key} = '${VALUES[$key]}'"
96+
done
97+
debug "OPTION_SHORT:" "${OPTION_SHORT[@]}"
98+
debug "OPTION_DEFAULT:" "${OPTION_DEFAULT[@]}"
99+
debug "OPTION_TYPE:" "${OPTION_TYPE[@]}"
100+
debug "OPTION_REQUIRED:" "${OPTION_REQUIRED[@]}"
101+
debug "OPTION_CONSTRAINTS:" "${OPTION_CONSTRAINTS[@]}"
102+
debug "OPTION_HELP:" "${OPTION_HELP[@]}"
89103
}
90104

91105
# ============================================================================ #
92-
# Helper flags
106+
# SCRIPT INITIALIZATION FLAGS #
93107
# ============================================================================ #
94108

95109
# Enable xtrace if the DEBUG environment variable is set

0 commit comments

Comments
 (0)