Skip to content

Commit 7818be2

Browse files
committed
bash-args: functions to handle bash arguments
- flags - options - positional arguments - variadic arguments
0 parents  commit 7818be2

10 files changed

Lines changed: 1668 additions & 0 deletions

File tree

.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/import;
5+
6+
DOCFILE="$(dirname ${BASH_SOURCE[0]})/../args";
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 /usr/local/bin/args
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: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/bin/bash
2+
# Copyright (c) 2025 Mihai Stancu (https://github.com/curatorium)
3+
4+
source /usr/local/bin/import;
5+
6+
DOCFILE="$(dirname ${BASH_SOURCE[0]})/../args"
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://raw.githubusercontent.com/curatorium/bash-args/main/args -o .deps/args
21+
$END_CODE
22+
23+
## Usage
24+
25+
$BEGIN_CODE
26+
source ./args
27+
28+
# Initialize the ARGS array with script arguments
29+
ARGS=("\$@")
30+
31+
# Parse flags, options, and positional arguments
32+
args:flag verbose v # variable \$verbose will be set to true if flag present
33+
args:opt name n # variable \$name will be set to the value provided
34+
args:arg file # variable \$file will be set to $1 if present
35+
args:varg files # variable \$files will be set to the rest of values from \$ARGS
36+
$END_CODE
37+
38+
All functions operate on the \`ARGS\` array, consuming matched tokens and leaving unmatched ones for subsequent parsing.
39+
40+
Ideally inside functions you will be using \`local\` variables to avoid polluting the global scope.
41+
42+
## FUNCTIONS
43+
MD
44+
45+
for name in $(docs:list function); do
46+
cat <<-MD
47+
48+
### \`args:$name\`
49+
50+
$(docs:tag "$name" desc)
51+
52+
MD
53+
54+
if [[ -n "$(docs:tag "$name" usage)" ]]; then
55+
cat <<-MD
56+
$BEGIN_CODE
57+
$(docs:tag "$name" usage)
58+
$END_CODE
59+
60+
MD
61+
fi
62+
63+
if [[ -n "$(docs:tag -f md "$name" flag)" ]]; then
64+
cat <<-MD
65+
| Flag | Description |
66+
|------|-------------|
67+
$(docs:tag -f md "$name" flag)
68+
69+
MD
70+
fi
71+
72+
if [[ -n "$(docs:tag -f md "$name" arg)" ]]; then
73+
cat <<-MD
74+
| Argument | Description |
75+
|----------|-------------|
76+
$(docs:tag -f md "$name" arg)
77+
78+
MD
79+
fi
80+
81+
if [[ -n "$(docs:tag -f md "$name" opt)" ]]; then
82+
cat <<-MD
83+
| Option | Description |
84+
|--------|-------------|
85+
$(docs:tag -f md "$name" opt)
86+
87+
MD
88+
fi
89+
90+
if [[ -n "$(docs:tag -f md "$name" return)" ]]; then
91+
cat <<-MD
92+
| Return | Description |
93+
|--------|-------------|
94+
$(docs:tag -f md "$name" return)
95+
96+
MD
97+
fi
98+
done
99+
100+
cat <<-MD
101+
102+
## Pattern Matching
103+
104+
Options and arguments support RegEx patterns for validation:
105+
106+
$BEGIN_CODE
107+
# Numeric validation
108+
args:opt port p '^\[0-9\]+\$'
109+
110+
# URL validation
111+
args:arg url '^https?://'
112+
113+
# Capture groups extract specific parts
114+
args:opt ord o '^:(\[0-9\]{3})\$' # --ord :100 -> ord=100
115+
$END_CODE
116+
117+
When a pattern contains a capture group, the captured value is assigned to the variable instead of the full match.
118+
119+
## Examples
120+
121+
### Basic CLI Tool
122+
123+
$BEGIN_CODE
124+
#!/bin/bash
125+
source /usr/local/bin/args
126+
127+
ARGS=("\$@")
128+
129+
args:flag verbose v
130+
args:flag -r help h --err "Missing required --help flag"
131+
args:opt output o
132+
args:arg input
133+
args:varg -o extra
134+
135+
echo "verbose: \$verbose"
136+
echo "output: \$output"
137+
echo "input: \$input"
138+
echo "extra: \${extra[*]}"
139+
$END_CODE
140+
141+
### Git-like Subcommands
142+
143+
$BEGIN_CODE
144+
#!/bin/bash
145+
source /usr/local/bin/args
146+
147+
ARGS=("\$@")
148+
149+
args:flag verbose v
150+
args:arg command '^(clone|pull|push)\$' --err "Unknown command"
151+
args:varg -o cmd_args
152+
153+
case "\$command" in
154+
clone) git_clone "\${cmd_args[@]}" ;;
155+
pull) git_pull "\${cmd_args[@]}" ;;
156+
push) git_push "\${cmd_args[@]}" ;;
157+
esac
158+
$END_CODE
159+
160+
### With Validation
161+
162+
$BEGIN_CODE
163+
#!/bin/bash
164+
source /usr/local/bin/args
165+
166+
ARGS=("\$@")
167+
168+
args:opt port p '^\[0-9\]+\$' --err "Port must be numeric" || exit 1
169+
args:opt host h '^\[a-z0-9.-\]+\$' --err "Invalid hostname" || exit 1
170+
args:arg url '^https?://' --err "URL must start with http(s)://" || exit 1
171+
172+
echo "Connecting to \$url via \$host:\$port"
173+
$END_CODE
174+
175+
## Limitations
176+
177+
- **No \`--\` separator handling.** There is no built-in support for \`--\` to signal end-of-options. Arguments after \`--\` are not treated differently.
178+
- **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.
179+
- **\`--combined\` flag ordering.** Combined short flags (\`-vfs\`) must be parsed after all other \`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.
180+
181+
## License
182+
183+
MIT
184+
MD

.githooks/pre-commit

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
# Copyright (c) 2025 Mihai Stancu (https://github.com/curatorium)
3+
4+
set -Eeuo pipefail;
5+
shopt -s inherit_errexit;
6+
shopt -s nullglob;
7+
8+
REPO="$(git rev-parse --show-toplevel)"
9+
10+
shellcheck -x "$REPO/args" ||
11+
(echo "ERROR: args failed shellcheck." && exit 1);
12+
13+
"$REPO/args.test" > "$REPO/args.test.md" ||
14+
(echo "ERROR: args tests failed." && exit 1);
15+
16+
[[ "$("$REPO/.deps/readme")" == "$(cat "$REPO/README.md")" ]] ||
17+
(echo "Docs are out of date." && exit 1);
18+
19+
[[ "$("$REPO/args.test")" == "$(cat "$REPO/args.test.md")" ]] ||
20+
(echo "Test reports are out of date.." && exit 1);

.github/workflows/release.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 import
16+
run: |
17+
curl -1fsSLR https://github.com/curatorium/bash-import/releases/latest/download/import -o /usr/local/bin/import;
18+
chmod +x /usr/local/bin/import;
19+
20+
- name: Run checks (shellcheck + tests)
21+
run: .githooks/pre-commit
22+
23+
- name: Build release artifacts
24+
run: import pack -f args -o .dist/args
25+
26+
- name: Create release
27+
env:
28+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29+
run: gh release create "${{ github.ref_name }}" --title "${{ github.ref_name }}" --generate-notes --latest .dist/args

.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)