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/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[@]}"}"