Skip to content

Commit 7f06340

Browse files
committed
bash-args: functions to handle bash arguments
- args:sub -- subcommands - args:fag -- flags - args:opt -- options - args:flag --bundle -- bundled flags (combined flags, clustered flags) - args:arg -- positional arguments - args:varg -- variadic arguments
0 parents  commit 7f06340

16 files changed

Lines changed: 2706 additions & 0 deletions

.deps/@help

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
# Copyright (c) 2025 Mihai Stancu (https://github.com/curatorium)
3+
4+
source /usr/local/bin/bash-import;
5+
6+
DOCFILE="$(dirname ${BASH_SOURCE[0]})/../bash-args.sh";
7+
export DOCFILE;
8+
9+
T=$'\t'
10+
11+
cat <<-TXT
12+
NAME
13+
${T}args -- Argument parsing functions for bash scripts.
14+
15+
USAGE
16+
${T}source ./bash-args.sh
17+
18+
FUNCTIONS
19+
TXT
20+
21+
for name in $(docs:list function); do
22+
cat <<-TXT | awk 'NF{print; blank=0} !NF && !blank++{print ""}'
23+
args:$name -- $(docs:tag "$name" desc)
24+
25+
$(docs:tag -i 1 "$name" usage)
26+
27+
$(docs:tag -i 1 "$name" flag,opt,arg)
28+
29+
TXT
30+
done | sed 's/^/\t/'

.deps/@readme

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/bin/bash
2+
# Copyright (c) 2025 Mihai Stancu (https://github.com/curatorium)
3+
4+
source /usr/local/bin/bash-import;
5+
6+
DOCFILE="$(dirname ${BASH_SOURCE[0]})/../bash-args.sh"
7+
8+
BEGIN_CODE='```bash'
9+
END_CODE='```'
10+
11+
# Generate README.md
12+
cat <<-MD
13+
# bash-args
14+
15+
> Argument parsing for bash scripts.
16+
17+
## Installation
18+
19+
$BEGIN_CODE
20+
curl -1fsSLR https://github.com/curatorium/bash-args/releases/latest/download/bash-args.sh -o .deps/bash-args.sh
21+
$END_CODE
22+
23+
## Usage
24+
25+
$BEGIN_CODE
26+
$(cat examples/usage.sh)
27+
$END_CODE
28+
29+
All functions operate on the \`ARGS\` array, consuming matched tokens and leaving unmatched ones for subsequent parsing. **Parsing order matters** — follow the numbering above.
30+
31+
Ideally inside functions you will be using \`local\` variables to avoid polluting the global scope.
32+
33+
### Why This Order
34+
35+
| Step | Reason |
36+
|------|--------|
37+
| \`args:sub\` first | Splits at the subcommand word. Parent parsers (steps 2-6) only see tokens before the boundary. |
38+
| \`args:flag\` / \`args:opt\` before \`args:flag --bundle\` | Flags and options each match their own token patterns, so their relative order is interchangeable. Both must run before bundled flags — otherwise \`-nfoo\` could be misread as combined flags instead of an option value. |
39+
| \`args:flag --bundle\` before \`args:arg\` | Combined short flags (\`-vfs\`) start with \`-\`. Since \`args:arg\` doesn't skip dash-prefixed tokens, it would capture them as positional values. Clean all flags/options first so only true positionals remain. |
40+
| \`args:arg\` before \`args:varg\` | Takes the first remaining positional. Must run before \`args:varg\` sweeps everything. |
41+
| \`args:varg\` always last | Sweeps all remaining tokens. Anything parsed after \`args:varg\` would find \`ARGS\` empty. |
42+
43+
## FUNCTIONS
44+
45+
**Parameter order matters.** Each function parses its own parameters positionally — pass them in the exact order shown in the usage line.
46+
MD
47+
48+
for name in $(docs:list function); do
49+
cat <<-MD
50+
51+
### \`args:$name\`
52+
53+
$(docs:tag "$name" desc)
54+
55+
MD
56+
57+
if [[ -n "$(docs:tag "$name" usage)" ]]; then
58+
cat <<-MD
59+
$BEGIN_CODE
60+
$(docs:tag "$name" usage)
61+
$END_CODE
62+
63+
MD
64+
fi
65+
66+
if [[ -n "$(docs:tag -f md "$name" flag,arg,opt)" ]]; then
67+
cat <<-MD
68+
| Parameter | Description |
69+
|-----------|-------------|
70+
$(docs:tag -f md "$name" flag,arg,opt)
71+
72+
MD
73+
fi
74+
75+
if [[ -n "$(docs:tag -f md "$name" return)" ]]; then
76+
cat <<-MD
77+
| Return | Description |
78+
|--------|-------------|
79+
$(docs:tag -f md "$name" return)
80+
81+
MD
82+
fi
83+
done
84+
85+
cat <<-MD
86+
87+
## Pattern Matching
88+
89+
Options and arguments support RegEx patterns for validation:
90+
91+
$BEGIN_CODE
92+
# Numeric validation
93+
args:opt port p '^[0-9]+\$'
94+
95+
# URL validation
96+
args:arg url '^https?://'
97+
98+
# Capture groups extract specific parts
99+
args:opt ord o '^:([0-9]{3})\$' # --ord :100 -> ord=100
100+
$END_CODE
101+
102+
When a pattern contains a capture group, the captured value is assigned to the variable instead of the full match.
103+
104+
## Examples
105+
106+
### Basic CLI Tool
107+
108+
$BEGIN_CODE
109+
$(cat examples/basic-cli.sh)
110+
$END_CODE
111+
112+
### Git-like Subcommands
113+
114+
$BEGIN_CODE
115+
$(cat examples/git-subcommands.sh)
116+
$END_CODE
117+
118+
### Subcommand with Boundary Separation
119+
120+
$BEGIN_CODE
121+
$(cat examples/subcommand-boundary.sh)
122+
$END_CODE
123+
124+
$BEGIN_CODE
125+
\$ mytool --verbose clone --verbose --branch main repo-url
126+
verbose mode
127+
# parent --verbose ✓, clone gets its own --verbose + --branch main + repo-url
128+
$END_CODE
129+
130+
### End-of-Options Separator
131+
132+
$BEGIN_CODE
133+
$(cat examples/end-of-options.sh)
134+
$END_CODE
135+
136+
The \`--\` separator stops \`args:flag\` and \`args:opt\` from scanning past it. Then \`args:varg\` consumes \`--\` and captures everything that remains.
137+
138+
$BEGIN_CODE
139+
\$ rm --recursive --output log.txt -- --weird-filename -rf
140+
# recursive=true, output=log.txt, files=(--weird-filename -rf)
141+
$END_CODE
142+
143+
### With Validation
144+
145+
$BEGIN_CODE
146+
$(cat examples/validation.sh)
147+
$END_CODE
148+
149+
## Limitations
150+
151+
- **No attached quoting.** Values with spaces must be passed as separate shell words (e.g., \`--name "foo bar"\`). Attached forms like \`--name="foo bar"\` may not preserve quoting as expected.
152+
- **\`args:flag --bundle\` ordering.** \`args:flag --bundle\` must be called after all \`args:flag\` and \`args:opt\` calls, but before \`args:arg\` calls. Otherwise, combined flags may consume characters intended as option values, or positional arguments starting with \`-\` may be misinterpreted.
153+
- **Subcommand detection is pattern-based.** \`args:sub\` finds the first token matching the pattern. If a parent option's value happens to match the subcommand pattern (e.g., \`--name clone clone ...\`), the value will be detected as the subcommand instead.
154+
155+
## License
156+
157+
MIT
158+
MD

.githooks/pre-commit

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
# Copyright (c) 2025 Mihai Stancu (https://github.com/curatorium)
3+
4+
shellcheck -s bash -x bash-args.sh ||
5+
{ echo "ERROR: shellcheck failed."; exit 1; }
6+
7+
bash-test bash-args.test > bash-args.test.md ||
8+
{ echo "ERROR: tests failed."; exit 1; }
9+
10+
.deps/@readme > README.md;
11+
git diff --quiet README.md ||
12+
{ echo "WARNING: README.md is out of date."; exit 1; }
13+
14+
git diff --quiet bash-args.test.md ||
15+
{ echo "WARNING: reports are out of date."; exit 1; };

.github/workflows/release.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Release
2+
3+
on:
4+
push: {tags: ['v*']}
5+
6+
permissions:
7+
contents: write
8+
9+
jobs:
10+
release:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Install dependencies
16+
run: |
17+
curl -1fsSLR https://github.com/curatorium/bash-import/releases/latest/download/bash-import -o /usr/local/bin/bash-import;
18+
chmod +x /usr/local/bin/bash-import;
19+
curl -1fsSLR https://github.com/curatorium/bash-import/releases/latest/download/bash-test -o /usr/local/bin/bash-test;
20+
chmod +x /usr/local/bin/bash-test;
21+
22+
- name: Run checks (shellcheck + tests)
23+
run: .githooks/pre-commit
24+
25+
- name: Build release artifacts
26+
run: bash-import pack bash-args.sh -o .dist/bash-args.sh
27+
28+
- name: Create release
29+
env:
30+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31+
run: gh release create "${{ github.ref_name }}" --title "${{ github.ref_name }}" --generate-notes --latest .dist/*

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.idea/
2+
.deps/@github/
3+
.deps/@http/
4+
.deps/@https/
5+
.dist/

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2025 Mihai Stancu (https://github.com/curatorium)
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

0 commit comments

Comments
 (0)