Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
# macOS
.DS_Store

# Editors
.vscode/

# Python
__pycache__/
*.py[cod]
.venv/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
71 changes: 71 additions & 0 deletions cli/bash/bin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# `cli/bash/bin`

This directory holds the user-facing Bash entrypoints.

## Layout

- `bash-wrapper`
The shared dispatcher used to launch Bash commands.
- `<command>` symlinks
Each command symlink points to `bash-wrapper`. The wrapper uses the invoked filename to decide which command to run.
- `tests/`
Wrapper-specific BATS coverage for `bash-wrapper`.

## How `bash-wrapper` Works

The wrapper supports two invocation styles:

```bash
bash-wrapper <command> [args...]
<command> [args...]
```

Behavior:

- When invoked as `bash-wrapper`, the first argument is treated as the command name.
- When invoked through a symlink, the symlink name is treated as the command name.
- Commands are resolved under `../commands/<name>/main.sh`.
- As a compatibility fallback, `../commands/<name>/<name>.sh` is also supported.

## What the Wrapper Provides

Before sourcing the command script, `bash-wrapper`:

- resolves the repository, CLI, and Bash root directories
- exports wrapper metadata:
- `BANYAN_REPO_ROOT`
- `BANYAN_CLI_ROOT`
- `BANYAN_BASH_ROOT`
- `BANYAN_BASH_BIN_DIR`
- `BANYAN_BASH_COMMAND_NAME`
- `BANYAN_BASH_COMMAND_DIR`
- `BANYAN_BASH_COMMAND_SCRIPT`
- preloads `../lib/std/lib_std.sh`

That means command scripts can use the stdlib helpers without sourcing `lib_std.sh` themselves.

The wrapper also sets `BANYAN_BASH_BOOTSTRAP_SOURCE` before loading the stdlib so stdlib path detection still treats the command script as the real caller.

## Examples

Direct dispatch:

```bash
cli/bash/bin/bash-wrapper my-command --flag value
```

Symlink dispatch:

```bash
ln -s bash-wrapper cli/bash/bin/my-command
cli/bash/bin/my-command --flag value
```

## Tests

Run the wrapper test suite with:

```bash
cd cli/bash
bats bin/tests/bash-wrapper.bats
```
139 changes: 139 additions & 0 deletions cli/bash/bin/bash-wrapper
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env bash

die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}

resolve_script_path() {
local source_path="${1:-}" link_dir target

[[ -n "$source_path" ]] || return 1

if [[ "$source_path" != */* ]]; then
source_path="$(command -v -- "$source_path" 2>/dev/null || true)"
[[ -n "$source_path" ]] || return 1
fi

while [[ -L "$source_path" ]]; do
link_dir="$(cd "$(dirname "$source_path")" && pwd -P)"
target="$(readlink "$source_path")"
if [[ "$target" == /* ]]; then
source_path="$target"
else
source_path="$link_dir/$target"
fi
done

link_dir="$(cd "$(dirname "$source_path")" && pwd -P)"
printf '%s/%s\n' "$link_dir" "$(basename "$source_path")"
}

list_commands() {
local commands_dir="$1"
local command_dir command_name found=0

[[ -d "$commands_dir" ]] || return 0

while IFS= read -r command_dir; do
[[ -d "$command_dir" ]] || continue
command_name="$(basename "$command_dir")"
if [[ -f "$command_dir/main.sh" || -f "$command_dir/${command_name}.sh" ]]; then
printf ' %s\n' "$command_name"
found=1
fi
done < <(find "$commands_dir" -mindepth 1 -maxdepth 1 -type d | sort)

((found)) || printf ' (none yet)\n'
}

print_usage() {
local wrapper_name="$1"
local commands_dir="$2"

cat <<EOF
Usage:
${wrapper_name} <command> [args...]
<command> [args...]

Behavior:
- When invoked as 'bash-wrapper', the first argument is treated as the command name.
- When invoked through a symlink, the symlink name is treated as the command name.
- Commands are resolved under cli/bash/commands/<name>/main.sh.
- As a compatibility fallback, cli/bash/commands/<name>/<name>.sh is also accepted.

Available commands:
EOF
list_commands "$commands_dir"
}

main() {
local invoked_as wrapper_path bin_dir bash_root cli_root repo_root commands_dir stdlib_path
local command_name command_dir command_script

invoked_as="$(basename "$0")"
wrapper_path="$(resolve_script_path "$0")" || die "Unable to resolve wrapper path for '$0'."
bin_dir="$(cd "$(dirname "$wrapper_path")" && pwd -P)"
bash_root="$(cd "$bin_dir/.." && pwd -P)"
cli_root="$(cd "$bash_root/.." && pwd -P)"
repo_root="$(cd "$cli_root/.." && pwd -P)"
commands_dir="$bash_root/commands"

if [[ "$invoked_as" == "bash-wrapper" ]]; then
case "${1:-}" in
"" )
print_usage "$invoked_as" "$commands_dir"
exit 1
;;
-h|--help|help )
print_usage "$invoked_as" "$commands_dir"
exit 0
;;
--list|list )
list_commands "$commands_dir"
exit 0
;;
esac

command_name="$1"
shift
else
command_name="$invoked_as"
fi

case "$command_name" in
""|.|..|*/* )
die "Invalid command name '$command_name'."
;;
esac

command_dir="$commands_dir/$command_name"
if [[ -f "$command_dir/main.sh" ]]; then
command_script="$command_dir/main.sh"
elif [[ -f "$command_dir/${command_name}.sh" ]]; then
command_script="$command_dir/${command_name}.sh"
else
die "Command '$command_name' was not found under '$command_dir'."
fi

export BANYAN_REPO_ROOT="$repo_root"
export BANYAN_CLI_ROOT="$cli_root"
export BANYAN_BASH_ROOT="$bash_root"
export BANYAN_BASH_BIN_DIR="$bin_dir"
export BANYAN_BASH_COMMAND_NAME="$command_name"
export BANYAN_BASH_COMMAND_DIR="$command_dir"
export BANYAN_BASH_COMMAND_SCRIPT="$command_script"

stdlib_path="$bash_root/lib/std/lib_std.sh"
[[ -f "$stdlib_path" ]] || die "Required stdlib '$stdlib_path' was not found."

BANYAN_BASH_BOOTSTRAP_SOURCE="$command_script"
# Source the stdlib in the wrapper shell so command scripts inherit the shared helpers.
source "$stdlib_path"
unset BANYAN_BASH_BOOTSTRAP_SOURCE

# Source the command in the same shell so it can use stdlib helpers without per-command boilerplate.
source "$command_script"
}

main "$@"
1 change: 1 addition & 0 deletions cli/bash/bin/test_cmd
Loading
Loading