Skip to content

Commit 96e50fd

Browse files
feat: achieve zero dependencies and production-ready
- refactor: span across all script files - test: remove package.json and git submodules, run Bats and its libraries inside Docker - test: redesign test suite structure - ci: remove local git hooks - ci: change test.yml to test inside Docker. Testing environment closely resembles production environment - docs: redesign the header structure. Stripped version control out of script files
1 parent fb59580 commit 96e50fd

24 files changed

+1393
-1027
lines changed

.editorconfig

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ trim_trailing_whitespace = false
2222
max_line_length = 80
2323

2424
binary_next_line = true # shfmt: -bn
25-
keep_padding = true # shfmt: -kp
2625
switch_case_indent = true # shfmt: -ci
27-
space_redirects = true # shfmt: -sr
28-
keep_padding = true # shfmt: -kp
26+
# keep_padding = true # shfmt: -kp
27+
# space_redirects = true # shfmt: -sr
2928

3029
# YAML
3130
[*.yml]

.gitconfig

Lines changed: 0 additions & 2 deletions
This file was deleted.

.github/prompts/test.prompt.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
mode: "edit"
33
---
44

5-
Your goal is to set up test cases for the Bash script that's also a command-line application (now refers to as "target script")
5+
Your goal is to set up test cases for the Bash script that's also a command-line application (now referred to as "target script").
66

77
The chosen testing framework is Bash Automated Testing System or Bats, [bats-core](https://github.com/bats-core/bats-core).
88

@@ -13,27 +13,28 @@ The chosen testing auxiliary libraries are:
1313

1414
Requirements:
1515

16-
- `$BATS_SUITE_TMPDIR` is a temporary directory common to all tests of a suite. You MUST use `$BATS_SUITE_TMPDIR` to create files required by multiple tests.
17-
- `$BATS_FILE_TMPDIR` is a temporary directory common to all tests of a test file. You MUST use `$BATS_FILE_TMPDIR` to create files required by multiple tests in the same test file.
18-
- `$BATS_TEST_TMPDIR` is a temporary directory unique for each test. You MUST use `$BATS_TEST_TMPDIR` to create files required only for specific tests.
19-
- You MUST clone the target script and create all mock dependencies in `$BATS_FILE_TMPDIR`, then you MUST change working directory to `$BATS_TEST_TMPDIR` inside `setup()` function then execute the target script with current working directory set to `$BATS_TEST_TMPDIR`. This is to ensure the state of the project directory remains intact after test execution.
20-
- You MUST pay attention to not creating duplicate files and directories when provisioning in `$BATS_FILE_TMPDIR`.
21-
- You MUST place the generated script inside `tests/` directory.
22-
- You SHOULD NOT source the target script, unless the test case required internal function direct invocation.
23-
- You MUST NOT use destructive operations (e.g. `mv -f`, `cp -f`, `ln -f`, ...).
24-
- You MUST ignore backup original working directory at `setup()` function.
25-
-
26-
- You MUST leave `teardown()` function blank:
16+
- If you're generating a new test script, you MUST place the script inside the `tests/` directory.
17+
- You MUST use `$BATS_SUITE_TMPDIR` - a temporary directory common to all tests of a suite - to create files required by multiple tests.
18+
- You MUST use `$BATS_FILE_TMPDIR` - a temporary directory common to all tests of a test file - to create files required by multiple tests in the same test file.
19+
- You MUST use `$BATS_TEST_TMPDIR` - a temporary directory unique for each test - to create files required only for specific tests.
20+
- You MUST clone the target script and create all mock dependencies in `$BATS_FILE_TMPDIR`. This is to ensure the state of the project directory remains intact after test execution.
21+
- You MUST pay attention to not creating duplicate files and directories while doing provision to `$BATS_FILE_TMPDIR`.
22+
- You MUST leave the `teardown()` function blank:
2723
```bash
2824
teardown() {
2925
:
3026
}
3127
```
32-
- You MUST avoid test case duplication (i.e. 2 test cases testing the same logic) at all cost.
28+
- You MUST avoid sourcing the target script, unless the test case requires internal function direct invocation.
29+
- You MUST avoid duplicating test case (i.e. 2 test cases testing the same logic).
30+
- You MUST avoid using destructive operations (e.g. `mv -f`, `cp -f`, `ln -f`, ...).
31+
- You MUST avoid backing up the original working directory in the `setup()` function.
32+
- You MUST avoid changing current working directory to `$BATS_TEST_TMPDIR`, instead, every file operation (copying, moving, changing owner, changing permissions, executing, etc.) MUST construct its own path using `$BATS_TEST_TMPDIR`.
33+
3334

3435
Example:
3536

36-
```bash
37+
```bats
3738
#!/usr/bin/env bats
3839
3940
setup() {

.github/workflows/test.yml

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,48 +23,19 @@ jobs:
2323
- name: Checkout
2424
uses: actions/checkout@v4
2525

26-
- name: Cache Bats dependencies
27-
id: cache-bats-libs
28-
uses: actions/cache@v4
29-
env:
30-
cache-name: cache-bats-libs
31-
with:
32-
path: "${{ github.workspace }}/tests/test_helper"
33-
key: ${{ runner.os }}-tests-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} # not every submodules have package-lock.json? Weird
34-
# a new cache is created when the content of `package.json` change, or when the runner's operating system changes
35-
restore-keys: |
36-
${{ runner.os }}-tests-${{ env.cache-name }}
37-
${{ runner.os }}-tests-
38-
${{ runner.os }}-
26+
- name: Pull Bats Docker image
27+
run: docker pull bats/bats:latest
3928

40-
- name: Setup Bats and bats libs
41-
# provide subsequent steps with output from the step
42-
id: setup-bats
43-
uses: bats-core/bats-action@3.0.0
44-
timeout-minutes: 10
45-
# inputs
46-
with:
47-
# by default, Bats libs are installed in default location
48-
# the inputs are set to `/usr/lib/bats-LIB_NAME`
49-
# to enable caching Bats libs, installed them inside HOME directory
50-
# NOTE: works for linux/win/mac
51-
support-path: "${{ github.workspace }}/tests/test_helper/bats-support"
52-
assert-path: "${{ github.workspace }}/tests/test_helper/bats-assert"
53-
file-path: "${{ github.workspace }}/tests/test_helper/bats-file"
54-
- name: Test script.sh
55-
shell: bash
56-
env:
57-
TERM: xterm
58-
run: bats tests/test.sh
59-
- name: Test template.sh
60-
shell: bash
61-
env:
62-
SCRIPT_NAME: template.sh
63-
TERM: xterm
64-
run: bats tests/test.sh
6529
- name: Test clone_bash_template.sh
6630
shell: bash
6731
env:
68-
SCRIPT_NAME: template.sh
6932
TERM: xterm
70-
run: bats tests/test_clone.sh
33+
run: |
34+
docker run \
35+
--rm \
36+
--user="$(id -u):$(id -g)" \
37+
--network none \
38+
--security-opt=no-new-privileges:true \
39+
--volume="$PWD:/code:ro" \
40+
bats/bats:latest \
41+
/code/tests

.gitignore

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ temp/
3535
[._]ss[a-gi-z]
3636
[._]sw[a-p]
3737

38-
# Node
39-
node_modules
40-
4138
# Misc
42-
template-symlink
43-
script-symlink
39+
node_modules
40+
hooks

.gitmodules

Lines changed: 0 additions & 10 deletions
This file was deleted.

build.sh

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,60 @@
11
#!/usr/bin/env bash
22

3-
## FILE : build.sh
4-
## VERSION : v2.3.1
5-
## DESCRIPTION : Merge source.sh and script.sh to template.sh
6-
## AUTHOR : Silverbullet069
7-
## REPOSITORY : https://github.com/Silverbullet069/bash-script-template
8-
## LICENSE : MIT License
3+
# ============================================================================ #
4+
5+
## FILE : build.sh
6+
## VERSION : v0.0.1
7+
## DESCRIPTION : Merge source.sh and script.sh to template.sh
8+
## AUTHOR : silverbullet069
9+
## REPOSITORY : https://github.com/Silverbullet069/bash-script-template
10+
## LICENSE : MIT License
11+
12+
## TEMREPO : https://github.com/Silverbullet069/bash-script-template
13+
## TEMMODE : lite
14+
## TEMUPDATED : 2025-06-21 19:15:03.788041997 +0700
15+
## TEMLIC : MIT License
16+
17+
# ============================================================================ #
18+
19+
# DESC: Acquire script lock, extracted from script.sh
20+
# ARGS: $1 (optional): Scope of script execution lock (system or user)
21+
# OUTS: None
22+
# RETS: None
23+
# NOTE: This lock implementation is extremely simple but should be reliable
24+
# across all platforms. It does *not* support locking a script with
25+
# symlinks or multiple hardlinks as there's no portable way of doing so.
26+
# If the lock was acquired it's automatically released on script exit.
27+
function lock_init() {
28+
local lock_dir
29+
if [[ "${1}" = "system" ]]; then
30+
lock_dir="/tmp/$(basename "${BASH_SOURCE[0]}").lock"
31+
elif [[ "${1}" = "user" ]]; then
32+
lock_dir="/tmp/$(basename "${BASH_SOURCE[0]}").${UID}.lock"
33+
else
34+
echo "Missing or invalid argument to ${FUNCNAME[0]}()!" >&2
35+
exit 1
36+
fi
37+
38+
if mkdir "${lock_dir}" 2>/dev/null; then
39+
readonly script_lock="${lock_dir}"
40+
echo "Acquired script lock: ${script_lock}"
41+
else
42+
echo "Unable to acquire script lock: ${lock_dir}" >&2
43+
exit 2
44+
fi
45+
}
46+
47+
# DESC: Handler for exiting the script
48+
# ARGS: None
49+
# OUTS: None
50+
# RETS: None
51+
function script_trap_exit() {
52+
# Remove script execution lock
53+
if [[ -d "${script_lock-}" ]]; then
54+
rmdir "${script_lock}"
55+
echo "Clean up script lock: ${script_lock}" >&2
56+
fi
57+
}
958

1059
# ============================================================================ #
1160

@@ -22,26 +71,44 @@ set -o pipefail # Use last non-zero exit code in a pipeline
2271

2372
# Main control flow
2473
function main() {
74+
trap script_trap_exit EXIT
75+
lock_init "user"
76+
77+
local -r script_dir="$(dirname "${BASH_SOURCE[0]}")"
2578

26-
local -r source_body=$(sed -n '/# =\+/,$p' source.sh | tail -n +2) || true
79+
# Check if required files exist
80+
if [[ ! -f "${script_dir}/script.sh" ]]; then
81+
echo "Error: script.sh not found in ${script_dir}" >&2
82+
exit 1
83+
fi
2784

28-
# NOTE: leave the comment divider
29-
local -r script_header=$(sed -n '1,/# =\+/p' script.sh) || true
85+
if [[ ! -f "${script_dir}/source.sh" ]]; then
86+
echo "Error: source.sh not found in ${script_dir}" >&2
87+
exit 1
88+
fi
3089

31-
# Extract + remove shellcheck source lines
32-
local -r script_body=$(sed -n '/# =\+/,$p' script.sh | tail -n +2 | grep -v -e "# shellcheck source=source.sh" -e "# shellcheck disable=SC1091" -e '^source.*source\.sh"') || true
90+
# Arbitrary values
91+
# shellcheck disable=SC2312
92+
local -r script_header=$(head -n 17 "${script_dir}/script.sh" || exit 1)
93+
# shellcheck disable=SC2312
94+
local -r source_body=$(tail -n +12 "${script_dir}/source.sh" || exit 1)
95+
# shellcheck disable=SC2312
96+
local -r script_body=$(tail -n +18 "${script_dir}/script.sh" | grep -vE -e '^# shellcheck source=source.sh$' -e '^# shellcheck disable=SC1091$' -e '^source.*source\.sh"$' || exit 1)
3397

3498
# Combine parts in desired order and write to template.sh
99+
# Make template.sh temporately writeable for updating
100+
chmod 755 "${script_dir}/template.sh"
35101
{
36102
echo "${script_header}"
37103
echo "${source_body}"
38104
echo "${script_body}"
39-
} > template.sh
105+
} >"${script_dir}/template.sh"
40106

41-
# Make template.sh executable
42-
chmod +x template.sh
107+
# Make template.sh executable and read-only
108+
# Any changes to template.sh must go through source.sh and script.sh
109+
chmod 555 "${script_dir}/template.sh"
43110

44-
echo "Build template.sh successfully"
111+
echo "Build template.sh successfully."
45112
}
46113

47114
# Template, assemble

0 commit comments

Comments
 (0)