just-bashit 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- just_bashit/__init__.py +3 -0
- just_bashit/_launcher.py +23 -0
- just_bashit/datetime.sh +115 -0
- just_bashit/environment.sh +204 -0
- just_bashit/file.sh +257 -0
- just_bashit/format.sh +223 -0
- just_bashit/function-template.sh +139 -0
- just_bashit/get-jb.sh +177 -0
- just_bashit/inspect.sh +220 -0
- just_bashit/install-deps.sh +300 -0
- just_bashit/just-runit +682 -0
- just_bashit/logging.sh +174 -0
- just_bashit/match.sh +64 -0
- just_bashit/network.sh +172 -0
- just_bashit/path.sh +79 -0
- just_bashit/pkg.sh +155 -0
- just_bashit/script-template +113 -0
- just_bashit/template.toml +80 -0
- just_bashit/toml.sh +331 -0
- just_bashit-0.2.0.dist-info/METADATA +64 -0
- just_bashit-0.2.0.dist-info/RECORD +23 -0
- just_bashit-0.2.0.dist-info/WHEEL +4 -0
- just_bashit-0.2.0.dist-info/entry_points.txt +4 -0
just_bashit/just-runit
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ############################################################################
|
|
3
|
+
# EXECUTABLE: just-runit (aliases: jb run, jbx) #
|
|
4
|
+
# PACKAGE: just-bashit version 0.2.0 #
|
|
5
|
+
# ############################################################################
|
|
6
|
+
# Ephemeral bash tool runner. Fetch a script from a URL or namespace, call #
|
|
7
|
+
# an optional function, then discard — no installation, no env pollution. #
|
|
8
|
+
# Analogous to uvx but for any bash/Python script reachable over HTTPS. #
|
|
9
|
+
# ############################################################################
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
IFS=$'\n\t'
|
|
12
|
+
|
|
13
|
+
# ---- defaults ---------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/just-runit"
|
|
16
|
+
_ME="$(basename "$0")"
|
|
17
|
+
_VERSION="0.1.4"
|
|
18
|
+
_DEFAULT_TTL=3600
|
|
19
|
+
|
|
20
|
+
# Default namespace base URL (just-buildit org pages root).
|
|
21
|
+
_JB_DEFAULT_NS="just-buildit"
|
|
22
|
+
_JB_DEFAULT_BASE="https://just-buildit.github.io"
|
|
23
|
+
|
|
24
|
+
# just-bashit raw source base — scripts co-fetched for inter-lib calls.
|
|
25
|
+
_JBS_BASE="https://raw.githubusercontent.com/just-buildit/just-bashit/main/src"
|
|
26
|
+
# All src/ libs; fetched together so relative inter-source calls resolve.
|
|
27
|
+
_JBS_LIBS=(
|
|
28
|
+
datetime environment file format
|
|
29
|
+
function-template logging match network path
|
|
30
|
+
pkg toml
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# ---- top-level dispatch (when called as "jb" or "just-buildit") -------------
|
|
34
|
+
|
|
35
|
+
if [[ ${_ME} == "jb" || ${_ME} == "just-buildit" ]]; then
|
|
36
|
+
SUBCMD="${1:-}"
|
|
37
|
+
case "${SUBCMD}" in
|
|
38
|
+
run)
|
|
39
|
+
shift
|
|
40
|
+
_ME="${_ME} run"
|
|
41
|
+
;;
|
|
42
|
+
cache)
|
|
43
|
+
shift
|
|
44
|
+
_CACHE_OP="${1:-}"
|
|
45
|
+
case "${_CACHE_OP}" in
|
|
46
|
+
clear)
|
|
47
|
+
shift
|
|
48
|
+
_CACHE_TARGET="${1:-all}"
|
|
49
|
+
case "${_CACHE_TARGET}" in
|
|
50
|
+
all)
|
|
51
|
+
rm -rf "${_CACHE_DIR}"
|
|
52
|
+
echo "cleared ${_CACHE_DIR}"
|
|
53
|
+
;;
|
|
54
|
+
jbs)
|
|
55
|
+
rm -rf "${_CACHE_DIR}/jbs"
|
|
56
|
+
echo "cleared ${_CACHE_DIR}/jbs"
|
|
57
|
+
;;
|
|
58
|
+
*)
|
|
59
|
+
# Treat arg as a URL — remove its cache entry.
|
|
60
|
+
_key=$(printf '%s' "${_CACHE_TARGET}" | sha256sum | cut -d' ' -f1)
|
|
61
|
+
_removed=0
|
|
62
|
+
for _f in "${_CACHE_DIR}/${_key}".{sh,py} "${_CACHE_DIR}/${_key}".meta; do
|
|
63
|
+
[[ -f ${_f} ]] && {
|
|
64
|
+
rm -f "${_f}"
|
|
65
|
+
_removed=1
|
|
66
|
+
}
|
|
67
|
+
done
|
|
68
|
+
if [[ ${_removed} -eq 1 ]]; then
|
|
69
|
+
echo "cleared cache entry for ${_CACHE_TARGET}"
|
|
70
|
+
else
|
|
71
|
+
echo "${_ME}: no cache entry found for ${_CACHE_TARGET}" >&2
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
;;
|
|
77
|
+
"" | -h | --help)
|
|
78
|
+
cat <<-EOF
|
|
79
|
+
Usage: ${_ME} cache <operation> [TARGET]
|
|
80
|
+
|
|
81
|
+
Operations:
|
|
82
|
+
clear [all] Remove all cached scripts and aliases (default)
|
|
83
|
+
clear jbs Remove the just-bashit co-fetch bundle only
|
|
84
|
+
clear <url> Remove the cache entry for a specific URL
|
|
85
|
+
EOF
|
|
86
|
+
;;
|
|
87
|
+
*)
|
|
88
|
+
echo "${_ME} cache: unknown operation '${_CACHE_OP}'" >&2
|
|
89
|
+
echo "Run '${_ME} cache -h' for usage." >&2
|
|
90
|
+
exit 1
|
|
91
|
+
;;
|
|
92
|
+
esac
|
|
93
|
+
exit 0
|
|
94
|
+
;;
|
|
95
|
+
install)
|
|
96
|
+
# Find jb.toml walking up from CWD.
|
|
97
|
+
_toml=""
|
|
98
|
+
_dir="${PWD}"
|
|
99
|
+
while [[ "${_dir}" != "/" ]]; do
|
|
100
|
+
if [[ -f "${_dir}/jb.toml" ]]; then
|
|
101
|
+
_toml="${_dir}/jb.toml"
|
|
102
|
+
break
|
|
103
|
+
fi
|
|
104
|
+
_dir="$(dirname "${_dir}")"
|
|
105
|
+
done
|
|
106
|
+
if [[ -z "${_toml}" ]]; then
|
|
107
|
+
echo "${_ME}: no jb.toml found (searched up from ${PWD})" >&2
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
printf ' reading %s\n' "${_toml}"
|
|
111
|
+
# Extract all source = "..." values from [tools.*] sections.
|
|
112
|
+
mapfile -t _sources < <(awk '
|
|
113
|
+
/^\[tools\./ { in_t=1; next }
|
|
114
|
+
/^\[/ { in_t=0 }
|
|
115
|
+
in_t && /^[[:space:]]*source[[:space:]]*=/ {
|
|
116
|
+
gsub(/.*=[[:space:]]*"|".*/, ""); print
|
|
117
|
+
}
|
|
118
|
+
' "${_toml}")
|
|
119
|
+
if [[ ${#_sources[@]} -eq 0 ]]; then
|
|
120
|
+
echo "${_ME}: no [tools.*] entries with source in ${_toml}"
|
|
121
|
+
exit 0
|
|
122
|
+
fi
|
|
123
|
+
_runner="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
124
|
+
_ok=0
|
|
125
|
+
_fail=0
|
|
126
|
+
for _src in "${_sources[@]}"; do
|
|
127
|
+
printf ' -> %s ' "${_src}"
|
|
128
|
+
if "${_runner}" -l "${_src}" >/dev/null 2>&1; then
|
|
129
|
+
printf 'ok\n'
|
|
130
|
+
((_ok++)) || true
|
|
131
|
+
else
|
|
132
|
+
printf 'failed\n' >&2
|
|
133
|
+
((_fail++)) || true
|
|
134
|
+
fi
|
|
135
|
+
done
|
|
136
|
+
printf ' %d fetched' "${_ok}"
|
|
137
|
+
[[ ${_fail} -gt 0 ]] && printf ', %d failed' "${_fail}"
|
|
138
|
+
printf '\n'
|
|
139
|
+
[[ ${_fail} -gt 0 ]] && exit 1 || exit 0
|
|
140
|
+
;;
|
|
141
|
+
version | -V | --version)
|
|
142
|
+
echo "${_ME} v${_VERSION}"
|
|
143
|
+
exit 0
|
|
144
|
+
;;
|
|
145
|
+
"" | help | -h | --help)
|
|
146
|
+
cat <<-EOF
|
|
147
|
+
Usage: ${_ME} <command> [OPTIONS] [ARGS...]
|
|
148
|
+
|
|
149
|
+
Commands:
|
|
150
|
+
run Ephemeral script runner (alias: jbx)
|
|
151
|
+
install Pre-fetch all tools declared in jb.toml
|
|
152
|
+
cache Manage the local script cache
|
|
153
|
+
version Print version and exit
|
|
154
|
+
|
|
155
|
+
Run '${_ME} <command> -h' for details.
|
|
156
|
+
EOF
|
|
157
|
+
exit 0
|
|
158
|
+
;;
|
|
159
|
+
*)
|
|
160
|
+
echo "${_ME}: unknown command '${SUBCMD}'" >&2
|
|
161
|
+
echo "Run '${_ME} help' for usage." >&2
|
|
162
|
+
exit 1
|
|
163
|
+
;;
|
|
164
|
+
esac
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# ---- option state -----------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
OPT_REFRESH=0
|
|
170
|
+
OPT_NO_CACHE=0
|
|
171
|
+
OPT_CLEAN=0
|
|
172
|
+
OPT_VERBOSE=0
|
|
173
|
+
OPT_LIST=0
|
|
174
|
+
OPT_TTL="${_DEFAULT_TTL}"
|
|
175
|
+
OPT_CHECKSUM=""
|
|
176
|
+
OPT_PASS=()
|
|
177
|
+
OPTARG=""
|
|
178
|
+
OPTIND=1
|
|
179
|
+
|
|
180
|
+
# ---- help -------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
read -r -d '' HELP <<-EOF || true
|
|
183
|
+
Usage: ${_ME} [OPTIONS] SPEC [FUNCTION [ARGS...]]
|
|
184
|
+
or: jbx [OPTIONS] SPEC [FUNCTION [ARGS...]]
|
|
185
|
+
|
|
186
|
+
Ephemeral tool runner. Resolve SPEC to a script, optionally call
|
|
187
|
+
FUNCTION with ARGS, then discard — no installation, no env pollution.
|
|
188
|
+
|
|
189
|
+
SPEC forms:
|
|
190
|
+
NAME bare name, resolved via default namespace (just-buildit)
|
|
191
|
+
NS:NAME explicit namespace (e.g. just-bashit:logging)
|
|
192
|
+
gh:USER/REPO/PATH GitHub raw, default branch main
|
|
193
|
+
gh:USER/REPO/PATH@REF GitHub raw at a specific ref/tag/sha
|
|
194
|
+
https://URL direct URL
|
|
195
|
+
|
|
196
|
+
Namespaces:
|
|
197
|
+
just-buildit ${_JB_DEFAULT_BASE}/ (default)
|
|
198
|
+
just-bashit ${_JBS_BASE}/
|
|
199
|
+
|
|
200
|
+
Resolution order for NAME / NS:NAME:
|
|
201
|
+
1. aliases.toml at namespace base
|
|
202
|
+
2. Direct URL probe: NS_BASE/NAME.sh then NS_BASE/NAME.py
|
|
203
|
+
|
|
204
|
+
Options:
|
|
205
|
+
-h Show this message and exit.
|
|
206
|
+
-l List functions defined by SPEC then exit.
|
|
207
|
+
-r Refresh — ignore and overwrite cached copy.
|
|
208
|
+
-n No-cache — fetch fresh and discard (nothing written).
|
|
209
|
+
-c Clean environment (minimal, like sudo without -E).
|
|
210
|
+
-p VARS Comma-separated var names to pass through with -c.
|
|
211
|
+
-t TTL Cache TTL in seconds (default 3600). 0 = keep forever.
|
|
212
|
+
-k HASH Verify script before running: sha256:HASH or md5:HASH.
|
|
213
|
+
-v Verbose.
|
|
214
|
+
|
|
215
|
+
Language detection (automatic):
|
|
216
|
+
.sh / bash shebang Sourced into a bash subshell; FUNCTION supported.
|
|
217
|
+
.py / python shebang Run with uv run (PEP 723 inline deps) or python3.
|
|
218
|
+
FUNCTION not supported for Python — pass args directly.
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
${_ME} install-deps -s apt
|
|
222
|
+
${_ME} just-bashit:logging log "hello"
|
|
223
|
+
${_ME} just-bashit:datetime iso-8601-basic -m
|
|
224
|
+
${_ME} gh:user/repo/tools/deploy.sh run --env prod
|
|
225
|
+
${_ME} gh:user/repo/tool.sh@v2.1.0 setup
|
|
226
|
+
${_ME} https://example.com/tool.sh
|
|
227
|
+
${_ME} https://example.com/tool.py --flag value
|
|
228
|
+
${_ME} -l just-bashit:logging
|
|
229
|
+
${_ME} -c -p HOME,TERM https://example.com/tool.sh func arg
|
|
230
|
+
${_ME} -n -k sha256:abc123 https://example.com/tool.sh fn
|
|
231
|
+
EOF
|
|
232
|
+
|
|
233
|
+
# ---- parse options ----------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
while getopts ":hlrncvp:t:k:" option; do
|
|
236
|
+
case $option in
|
|
237
|
+
h)
|
|
238
|
+
echo "${HELP}"
|
|
239
|
+
exit 0
|
|
240
|
+
;;
|
|
241
|
+
l) OPT_LIST=1 ;;
|
|
242
|
+
r) OPT_REFRESH=1 ;;
|
|
243
|
+
n) OPT_NO_CACHE=1 ;;
|
|
244
|
+
c) OPT_CLEAN=1 ;;
|
|
245
|
+
v) OPT_VERBOSE=1 ;;
|
|
246
|
+
p) IFS=',' read -ra OPT_PASS <<<"${OPTARG}" ;;
|
|
247
|
+
t) OPT_TTL="${OPTARG}" ;;
|
|
248
|
+
k) OPT_CHECKSUM="${OPTARG}" ;;
|
|
249
|
+
\?)
|
|
250
|
+
echo "Invalid option: -${OPTARG}"
|
|
251
|
+
echo "${HELP}"
|
|
252
|
+
exit 1
|
|
253
|
+
;;
|
|
254
|
+
esac
|
|
255
|
+
done
|
|
256
|
+
shift "$((OPTIND - 1))"
|
|
257
|
+
|
|
258
|
+
[[ $# -eq 0 ]] && {
|
|
259
|
+
echo "${HELP}"
|
|
260
|
+
exit 1
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
RAW_SPEC="${1}"
|
|
264
|
+
shift
|
|
265
|
+
# Only treat the next arg as a function name if it doesn't look like a flag.
|
|
266
|
+
# Flags (starting with -) and missing args fall through to ARGS unchanged.
|
|
267
|
+
FUNC="${1:-}"
|
|
268
|
+
if [[ -n ${FUNC} && ${FUNC} != -* ]]; then
|
|
269
|
+
shift || true
|
|
270
|
+
else
|
|
271
|
+
FUNC=""
|
|
272
|
+
fi
|
|
273
|
+
ARGS=("$@")
|
|
274
|
+
|
|
275
|
+
# ---- helpers ----------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
_log() { [[ ${OPT_VERBOSE} -eq 1 ]] && echo "${_ME}: $*" >&2 || true; }
|
|
278
|
+
_err() {
|
|
279
|
+
echo "${_ME}: error: $*" >&2
|
|
280
|
+
exit 1
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# ---- namespace resolution ---------------------------------------------------
|
|
284
|
+
|
|
285
|
+
# Parse aliases.toml from NS_BASE/aliases.toml and look up a name.
|
|
286
|
+
# Returns the resolved URL, or empty string if not found.
|
|
287
|
+
_lookup_alias() {
|
|
288
|
+
local base="${1}" name="${2}"
|
|
289
|
+
local aliases_url="${base}/aliases.toml"
|
|
290
|
+
local key
|
|
291
|
+
key=$(printf '%s' "${base}" | sha256sum | cut -d' ' -f1)
|
|
292
|
+
local aliases_cache="${_CACHE_DIR}/aliases-${key}.toml"
|
|
293
|
+
|
|
294
|
+
if [[ ${OPT_REFRESH} -eq 1 ]] || ! _cache_valid "${aliases_cache}"; then
|
|
295
|
+
mkdir -p "${_CACHE_DIR}"
|
|
296
|
+
_log "fetching ${aliases_url}"
|
|
297
|
+
if ! curl -sSL --proto '=https' --tlsv1.2 --max-time 10 \
|
|
298
|
+
-o "${aliases_cache}.new" "${aliases_url}" 2>/dev/null; then
|
|
299
|
+
rm -f "${aliases_cache}.new"
|
|
300
|
+
echo ""
|
|
301
|
+
return
|
|
302
|
+
fi
|
|
303
|
+
mv "${aliases_cache}.new" "${aliases_cache}"
|
|
304
|
+
printf 'ts=%s\nurl=%s\n' "$(date +%s)" "${aliases_url}" \
|
|
305
|
+
>"$(_meta_path "${aliases_cache}")"
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
# Parse: find key under [aliases] section.
|
|
309
|
+
awk -v target="${name}" '
|
|
310
|
+
/^\[aliases\]/ { in_sec=1; next }
|
|
311
|
+
/^\[/ { in_sec=0 }
|
|
312
|
+
in_sec && /^[[:space:]]*[^#=[:space:]]/ {
|
|
313
|
+
split($0, a, /[[:space:]]*=[[:space:]]*/)
|
|
314
|
+
key = a[1]; gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
|
315
|
+
val = a[2]; gsub(/^[[:space:]"]+|["[:space:]]+$/, "", val)
|
|
316
|
+
if (key == target) { print val; exit }
|
|
317
|
+
}
|
|
318
|
+
' "${aliases_cache}"
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Probe whether a URL exists (HEAD, no download). Returns 0 if 2xx/3xx.
|
|
322
|
+
_head_ok() {
|
|
323
|
+
local url="${1}"
|
|
324
|
+
local code
|
|
325
|
+
code=$(curl -sSL --proto '=https' --tlsv1.2 --max-time 5 \
|
|
326
|
+
-o /dev/null -w '%{http_code}' --head "${url}" 2>/dev/null)
|
|
327
|
+
[[ ${code} == "200" || ${code} == "301" || ${code} == "302" ]]
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Resolve NS:NAME or bare NAME against a namespace base URL.
|
|
331
|
+
# Returns a URL or the internal "jbs:NAME" marker for just-bashit.
|
|
332
|
+
_resolve_ns() {
|
|
333
|
+
local ns="${1}" name="${2}"
|
|
334
|
+
|
|
335
|
+
# Map namespace to base URL.
|
|
336
|
+
local base
|
|
337
|
+
case "${ns}" in
|
|
338
|
+
just-buildit) base="${_JB_DEFAULT_BASE}" ;;
|
|
339
|
+
just-bashit)
|
|
340
|
+
# just-bashit uses the co-fetch mechanism; return internal marker.
|
|
341
|
+
echo "jbs:${name}"
|
|
342
|
+
return
|
|
343
|
+
;;
|
|
344
|
+
*)
|
|
345
|
+
_err "unknown namespace '${ns}'; known: just-buildit, just-bashit"
|
|
346
|
+
;;
|
|
347
|
+
esac
|
|
348
|
+
|
|
349
|
+
# 1. Check aliases.toml first (one cached round-trip covers all names).
|
|
350
|
+
local alias_url
|
|
351
|
+
alias_url=$(_lookup_alias "${base}" "${name}")
|
|
352
|
+
if [[ -n ${alias_url} ]]; then
|
|
353
|
+
_log "resolved '${name}' via aliases.toml -> ${alias_url}"
|
|
354
|
+
echo "${alias_url}"
|
|
355
|
+
return
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# 2. Probe direct .sh then .py.
|
|
359
|
+
local url_sh="${base}/${name}.sh"
|
|
360
|
+
local url_py="${base}/${name}.py"
|
|
361
|
+
if _head_ok "${url_sh}"; then
|
|
362
|
+
_log "resolved '${name}' via direct probe -> ${url_sh}"
|
|
363
|
+
echo "${url_sh}"
|
|
364
|
+
return
|
|
365
|
+
fi
|
|
366
|
+
if _head_ok "${url_py}"; then
|
|
367
|
+
_log "resolved '${name}' via direct probe -> ${url_py}"
|
|
368
|
+
echo "${url_py}"
|
|
369
|
+
return
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
_err "cannot resolve '${name}' in namespace '${ns}' (checked ${base}/aliases.toml and ${url_sh})"
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# ---- URL resolution ---------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
_resolve() {
|
|
378
|
+
local raw="${1}"
|
|
379
|
+
case "${raw}" in
|
|
380
|
+
just-bashit:*)
|
|
381
|
+
# Explicit just-bashit namespace; use co-fetch mechanism.
|
|
382
|
+
echo "jbs:${raw#just-bashit:}"
|
|
383
|
+
;;
|
|
384
|
+
*://*)
|
|
385
|
+
case "${raw}" in
|
|
386
|
+
https://*) echo "${raw}" ;;
|
|
387
|
+
http://*) _err "HTTP not allowed; use HTTPS" ;;
|
|
388
|
+
*) _err "unrecognised URL scheme: ${raw}" ;;
|
|
389
|
+
esac
|
|
390
|
+
;;
|
|
391
|
+
gh:*)
|
|
392
|
+
local spec="${raw#gh:}" ref="main"
|
|
393
|
+
if [[ ${spec} == *@* ]]; then
|
|
394
|
+
ref="${spec##*@}"
|
|
395
|
+
spec="${spec%@*}"
|
|
396
|
+
fi
|
|
397
|
+
local user repo path
|
|
398
|
+
user="${spec%%/*}"
|
|
399
|
+
spec="${spec#*/}"
|
|
400
|
+
repo="${spec%%/*}"
|
|
401
|
+
path="${spec#*/}"
|
|
402
|
+
echo "https://raw.githubusercontent.com/${user}/${repo}/${ref}/${path}"
|
|
403
|
+
;;
|
|
404
|
+
*:*)
|
|
405
|
+
# NS:NAME form (not a URL scheme — no :// present).
|
|
406
|
+
local ns="${raw%%:*}" name="${raw#*:}"
|
|
407
|
+
_resolve_ns "${ns}" "${name}"
|
|
408
|
+
;;
|
|
409
|
+
*)
|
|
410
|
+
# Bare name — resolve against default namespace.
|
|
411
|
+
_resolve_ns "${_JB_DEFAULT_NS}" "${raw}"
|
|
412
|
+
;;
|
|
413
|
+
esac
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
# ---- cache helpers ----------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
_cache_key() { printf '%s' "${1}" | sha256sum | cut -d' ' -f1; }
|
|
419
|
+
|
|
420
|
+
_cache_ext() { case "${1}" in *.py) echo ".py" ;; *) echo ".sh" ;; esac }
|
|
421
|
+
|
|
422
|
+
_cache_path() {
|
|
423
|
+
local url="${1}"
|
|
424
|
+
echo "${_CACHE_DIR}/$(_cache_key "${url}")$(_cache_ext "${url}")"
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
_meta_path() { echo "${1%.*}.meta"; }
|
|
428
|
+
|
|
429
|
+
_cache_valid() {
|
|
430
|
+
local cached="${1}"
|
|
431
|
+
[[ -f ${cached} ]] || return 1
|
|
432
|
+
local meta
|
|
433
|
+
meta=$(_meta_path "${cached}")
|
|
434
|
+
[[ -f ${meta} ]] || return 1
|
|
435
|
+
[[ ${OPT_TTL} -eq 0 ]] && return 0
|
|
436
|
+
local ts now
|
|
437
|
+
ts=$(grep '^ts=' "${meta}" | cut -d= -f2)
|
|
438
|
+
now=$(date +%s)
|
|
439
|
+
((now - ts < OPT_TTL))
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
_fetch_to() {
|
|
443
|
+
local url="${1}" dest="${2}"
|
|
444
|
+
_log "fetching ${url}"
|
|
445
|
+
curl -sSL -o "${dest}" "${url}"
|
|
446
|
+
printf 'ts=%s\nurl=%s\n' "$(date +%s)" "${url}" >"$(_meta_path "${dest}")"
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
# ---- acquire script ---------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
# For just-bashit scripts, all src/ libs are co-fetched into cache/jbs/ so
|
|
452
|
+
# that relative inter-source calls (e.g. logging.sh -> format.sh) resolve.
|
|
453
|
+
_acquire_jbs() {
|
|
454
|
+
local lib="${1}"
|
|
455
|
+
local jbs_dir="${_CACHE_DIR}/jbs"
|
|
456
|
+
mkdir -p "${jbs_dir}"
|
|
457
|
+
|
|
458
|
+
local needs_fetch=0
|
|
459
|
+
local dest="${jbs_dir}/${lib}.sh"
|
|
460
|
+
|
|
461
|
+
[[ ${OPT_REFRESH} -eq 1 ]] && needs_fetch=1
|
|
462
|
+
[[ ! -f ${dest} ]] && needs_fetch=1
|
|
463
|
+
! _cache_valid "${dest}" && needs_fetch=1
|
|
464
|
+
|
|
465
|
+
if [[ ${needs_fetch} -eq 1 ]]; then
|
|
466
|
+
_log "fetching all jbs libs to ${jbs_dir}"
|
|
467
|
+
for l in "${_JBS_LIBS[@]}"; do
|
|
468
|
+
_fetch_to "${_JBS_BASE}/${l}.sh" "${jbs_dir}/${l}.sh"
|
|
469
|
+
done
|
|
470
|
+
# fetch the target itself when it is not one of the core libs
|
|
471
|
+
if ! printf '%s\n' "${_JBS_LIBS[@]}" | grep -qx "${lib}"; then
|
|
472
|
+
_fetch_to "${_JBS_BASE}/${lib}.sh" "${dest}"
|
|
473
|
+
fi
|
|
474
|
+
else
|
|
475
|
+
_log "cache hit: ${dest}"
|
|
476
|
+
fi
|
|
477
|
+
|
|
478
|
+
echo "${dest}"
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
_acquire() {
|
|
482
|
+
local url="${1}"
|
|
483
|
+
|
|
484
|
+
if [[ ${url} == jbs:* ]]; then
|
|
485
|
+
_acquire_jbs "${url#jbs:}"
|
|
486
|
+
return
|
|
487
|
+
fi
|
|
488
|
+
|
|
489
|
+
# Scripts fetched directly from _JBS_BASE need their sibling libs available
|
|
490
|
+
# at ${_SCRIPT_DIR}/ (where _SCRIPT_DIR = dirname of the cached file).
|
|
491
|
+
# Route through _acquire_jbs so all libs land together in cache/jbs/.
|
|
492
|
+
if [[ ${url} == "${_JBS_BASE}/"* ]]; then
|
|
493
|
+
local _lib="${url#${_JBS_BASE}/}"
|
|
494
|
+
_acquire_jbs "${_lib%.sh}"
|
|
495
|
+
return
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
if [[ ${OPT_NO_CACHE} -eq 1 ]]; then
|
|
499
|
+
local ext tmp
|
|
500
|
+
ext=$(_cache_ext "${url}")
|
|
501
|
+
tmp=$(mktemp "/tmp/just-runit.XXXXXX${ext}")
|
|
502
|
+
# shellcheck disable=SC2064
|
|
503
|
+
trap "rm -f '${tmp}' '$(_meta_path "${tmp}")'" EXIT
|
|
504
|
+
_fetch_to "${url}" "${tmp}"
|
|
505
|
+
echo "${tmp}"
|
|
506
|
+
return
|
|
507
|
+
fi
|
|
508
|
+
|
|
509
|
+
mkdir -p "${_CACHE_DIR}"
|
|
510
|
+
local cached
|
|
511
|
+
cached=$(_cache_path "${url}")
|
|
512
|
+
|
|
513
|
+
if [[ ${OPT_REFRESH} -eq 1 ]] || ! _cache_valid "${cached}"; then
|
|
514
|
+
_fetch_to "${url}" "${cached}"
|
|
515
|
+
else
|
|
516
|
+
_log "cache hit: ${cached}"
|
|
517
|
+
fi
|
|
518
|
+
|
|
519
|
+
echo "${cached}"
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
# ---- checksum ---------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
_verify() {
|
|
525
|
+
local file="${1}" spec="${2}"
|
|
526
|
+
local algo="${spec%%:*}" expected="${spec#*:}" actual
|
|
527
|
+
case "${algo}" in
|
|
528
|
+
sha256) actual=$(sha256sum "${file}" | cut -d' ' -f1) ;;
|
|
529
|
+
md5) actual=$(md5sum "${file}" | cut -d' ' -f1) ;;
|
|
530
|
+
*) _err "unsupported checksum algorithm: ${algo}" ;;
|
|
531
|
+
esac
|
|
532
|
+
[[ ${actual} == "${expected}" ]] ||
|
|
533
|
+
_err "checksum mismatch (got ${actual}, expected ${expected})"
|
|
534
|
+
_log "checksum OK"
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
# ---- language detection -----------------------------------------------------
|
|
538
|
+
|
|
539
|
+
# Check shebang first, fall back to URL extension.
|
|
540
|
+
_detect_lang() {
|
|
541
|
+
local script="${1}" url="${2}"
|
|
542
|
+
local shebang
|
|
543
|
+
shebang=$(head -1 "${script}" 2>/dev/null || true)
|
|
544
|
+
case "${shebang}" in
|
|
545
|
+
*python*)
|
|
546
|
+
echo "python"
|
|
547
|
+
return
|
|
548
|
+
;;
|
|
549
|
+
esac
|
|
550
|
+
case "${url}" in
|
|
551
|
+
*.py)
|
|
552
|
+
echo "python"
|
|
553
|
+
return
|
|
554
|
+
;;
|
|
555
|
+
esac
|
|
556
|
+
echo "bash"
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
# ---- list functions ---------------------------------------------------------
|
|
560
|
+
|
|
561
|
+
_list_fns() {
|
|
562
|
+
local script="${1}" lang="${2}"
|
|
563
|
+
if [[ ${lang} == "python" ]]; then
|
|
564
|
+
python3 - "${script}" <<'PYEOF'
|
|
565
|
+
import ast, sys
|
|
566
|
+
src = open(sys.argv[1]).read()
|
|
567
|
+
names = sorted(
|
|
568
|
+
node.name
|
|
569
|
+
for node in ast.walk(ast.parse(src))
|
|
570
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
571
|
+
)
|
|
572
|
+
print('\n'.join(names))
|
|
573
|
+
PYEOF
|
|
574
|
+
return
|
|
575
|
+
fi
|
|
576
|
+
bash -c "
|
|
577
|
+
_b=\$(declare -F | awk '{print \$3}')
|
|
578
|
+
# shellcheck source=/dev/null
|
|
579
|
+
source '${script}' 2>/dev/null || true
|
|
580
|
+
_a=\$(declare -F | awk '{print \$3}')
|
|
581
|
+
comm -13 <(printf '%s\n' \${_b} | sort) \
|
|
582
|
+
<(printf '%s\n' \${_a} | sort)
|
|
583
|
+
"
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
# ---- execute ----------------------------------------------------------------
|
|
587
|
+
|
|
588
|
+
_run_python() {
|
|
589
|
+
local script="${1}"
|
|
590
|
+
shift
|
|
591
|
+
|
|
592
|
+
if ! command -v uv >/dev/null 2>&1; then
|
|
593
|
+
_log "uv not found — installing via jb run"
|
|
594
|
+
"${0}" https://astral.sh/uv/install.sh
|
|
595
|
+
hash -r 2>/dev/null || true
|
|
596
|
+
command -v uv >/dev/null 2>&1 ||
|
|
597
|
+
_err "uv install failed; try manually: jb run https://astral.sh/uv/install.sh"
|
|
598
|
+
fi
|
|
599
|
+
|
|
600
|
+
_log "using uv run"
|
|
601
|
+
if [[ ${OPT_CLEAN} -eq 1 ]]; then
|
|
602
|
+
local env_pairs=("HOME=${HOME}" "TERM=${TERM:-}" "PATH=${PATH}")
|
|
603
|
+
local v
|
|
604
|
+
for v in "${OPT_PASS[@]+"${OPT_PASS[@]}"}"; do
|
|
605
|
+
env_pairs+=("${v}=${!v:-}")
|
|
606
|
+
done
|
|
607
|
+
env -i "${env_pairs[@]}" uv run "${script}" "$@"
|
|
608
|
+
else
|
|
609
|
+
uv run "${script}" "$@"
|
|
610
|
+
fi
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
_run() {
|
|
614
|
+
local script="${1}" func="${2:-}" lang="${3}"
|
|
615
|
+
shift 3 || true
|
|
616
|
+
|
|
617
|
+
if [[ ${lang} == "python" ]]; then
|
|
618
|
+
[[ -n ${func} ]] &&
|
|
619
|
+
_err "function mode not supported for Python; pass args directly"
|
|
620
|
+
_run_python "${script}" "$@"
|
|
621
|
+
return
|
|
622
|
+
fi
|
|
623
|
+
|
|
624
|
+
# Validate func is defined after sourcing the script. Using declare -F
|
|
625
|
+
# rather than the before/after diff in _list_fns so that script functions
|
|
626
|
+
# which shadow same-named env functions are accepted.
|
|
627
|
+
# In verbose mode, also report when the script function shadows a
|
|
628
|
+
# pre-existing command or builtin.
|
|
629
|
+
if [[ -n ${func} ]]; then
|
|
630
|
+
local _probe
|
|
631
|
+
_probe=$(bash -c "
|
|
632
|
+
_pre=\$(type -t ${func@Q} 2>/dev/null || true)
|
|
633
|
+
source ${script@Q} 2>/dev/null
|
|
634
|
+
declare -F ${func@Q} >/dev/null 2>&1 && echo \"ok:\${_pre}\" || echo notfound
|
|
635
|
+
")
|
|
636
|
+
case "${_probe}" in
|
|
637
|
+
notfound) _err "'${func}' is not a function defined by this script" ;;
|
|
638
|
+
ok:file) _log "note: script function '${func}' shadows a shell command" ;;
|
|
639
|
+
ok:builtin) _log "note: script function '${func}' shadows a shell builtin" ;;
|
|
640
|
+
ok:alias) _log "note: script function '${func}' shadows a shell alias" ;;
|
|
641
|
+
esac
|
|
642
|
+
fi
|
|
643
|
+
|
|
644
|
+
# Build the command string; %q ensures safe quoting of paths/names.
|
|
645
|
+
local cmd
|
|
646
|
+
if [[ -n ${func} ]]; then
|
|
647
|
+
cmd=$(printf '. %q && %q "$@"' "${script}" "${func}")
|
|
648
|
+
else
|
|
649
|
+
cmd=$(printf '. %q' "${script}")
|
|
650
|
+
fi
|
|
651
|
+
|
|
652
|
+
if [[ ${OPT_CLEAN} -eq 1 ]]; then
|
|
653
|
+
local env_pairs=("HOME=${HOME}" "TERM=${TERM:-}" "PATH=${PATH}")
|
|
654
|
+
local v
|
|
655
|
+
for v in "${OPT_PASS[@]+"${OPT_PASS[@]}"}"; do
|
|
656
|
+
env_pairs+=("${v}=${!v:-}")
|
|
657
|
+
done
|
|
658
|
+
env -i "${env_pairs[@]}" bash -c "${cmd}" -- "$@"
|
|
659
|
+
else
|
|
660
|
+
bash -c "${cmd}" -- "$@"
|
|
661
|
+
fi
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
# ---- main -------------------------------------------------------------------
|
|
665
|
+
|
|
666
|
+
URL=$(_resolve "${RAW_SPEC}")
|
|
667
|
+
_log "resolved: ${URL}"
|
|
668
|
+
|
|
669
|
+
SCRIPT=$(_acquire "${URL}")
|
|
670
|
+
_log "script: ${SCRIPT}"
|
|
671
|
+
|
|
672
|
+
[[ -n ${OPT_CHECKSUM} ]] && _verify "${SCRIPT}" "${OPT_CHECKSUM}"
|
|
673
|
+
|
|
674
|
+
SCRIPT_LANG=$(_detect_lang "${SCRIPT}" "${RAW_SPEC}")
|
|
675
|
+
_log "language: ${SCRIPT_LANG}"
|
|
676
|
+
|
|
677
|
+
if [[ ${OPT_LIST} -eq 1 ]]; then
|
|
678
|
+
_list_fns "${SCRIPT}" "${SCRIPT_LANG}"
|
|
679
|
+
exit 0
|
|
680
|
+
fi
|
|
681
|
+
|
|
682
|
+
_run "${SCRIPT}" "${FUNC}" "${SCRIPT_LANG}" "${ARGS[@]+"${ARGS[@]}"}"
|