This guide shows how to embed BashTab in your own project using git submodules and the module system. We’ll use a Python project as the running example, but the pattern applies to any language.
1. Add BashTab as a submodule
cd your-project
git submodule add https://github.com/evagreendev/BashTab.git deps/bash-tab
Library vs Binary
BashTab modules can be used in two ways, just like Rust crates:
| Mode | Your module acts as… | Entrypoint | Caching |
|---|---|---|---|
| Binary | A standalone shell environment | activate script | Sets BU_TOP_LEVEL_MODULE, calls bu_mark_load_complete |
| Library | A dependency of another project | *_bu_module.sh (appends to BU_MODULE_LIST) | Inherits the host’s cache key |
The same module can do both — activate is the “binary” entrypoint, *_bu_module.sh is the “library” entrypoint. Only the binary/top-level calls bu_mark_load_complete.
2. Scaffold your module
source deps/bash-tab/activate
bu new-module --name myproject
This creates:
myproject/
├── activate ← "binary" entrypoint (sets BU_TOP_LEVEL_MODULE)
├── myproject_bu_module.sh ← "library" registration (appends to BU_MODULE_LIST)
├── myproject_bu_preinit.sh ← registers command dirs (runs in both modes)
└── commands/
3. Customize the preinit callback
myproject_bu_preinit.sh is sourced during bu import-environment. This is where you register your project’s commands, set up shell integrations, and load language-specific completions:
#!/usr/bin/env bash
source "$BU_NULL"
bu_pushd_current "$BASH_SOURCE"
# Register commands from the local commands/ directory
bu import-environment +i -c ./commands -ns prefix
# Language-specific completions
if command -v uv &>/dev/null; then
eval "$(uv generate-shell-completion bash)"
fi
bu_popd_silent
Friendly spellings belong to the embedder
Core registers only canonical command names (e.g. invoke-remote-command). If your project wants a shorter or friendlier spelling of a built-in, declare it as a plain alias in your preinit rather than shadowing the command name with a script:
bu_preinit_register_new_alias invoke-command invoke-remote-command '{...}'
Preinit aliases and the command scan are both complete before any dispatch, so there is no ordering hazard: a project can never collide with itself the way a core late-bound alias could.
4. Customize the activate script
myproject/activate is the “binary” entrypoint — it sets BU_TOP_LEVEL_MODULE so the command registry can be cached, and calls bu_mark_load_complete after initialization. Bootstrap the full environment — BashTab, your module, Python venv, and anything else your project needs:
#!/usr/bin/env bash
function myproject_activate()
{
local myproject_invocation_dir=$PWD
pushd "$(dirname -- "${BASH_SOURCE}")" &>/dev/null
local myproject_dir=$PWD
eval "$(fzf --bash)"
if command -v bu &>/dev/null; then
bu import-environment --reset-leaky --no-init
fi
# Register this module and its library dependencies in BU_MODULE_LIST.
export BU_MODULE_LIST="myproject:0.1.0:$myproject_dir/myproject_bu_preinit.sh;"
# Library dependencies (opt-in by presence, idempotent, diamond-safe):
# source "$BU_DIR/lib/bu_module_require.sh"
# bu_module_require somelib --dir "$myproject_dir/../somelib" --git-url https://github.com/you/somelib --branch main
# Set the top-level module key so the command registry can be cached.
# Must be set BEFORE sourcing bu_entrypoint.sh.
export BU_TOP_LEVEL_MODULE="${BU_TOP_LEVEL_MODULE:-myproject}"
source "$BU_DIR"/bu_entrypoint.sh
# Cache the command registry so subsequent activations skip the scan.
# No-op if the cache was already loaded.
bu_mark_load_complete
bu_scope_push_function
bu_scope_add_cleanup bu_popd_silent
# Python venv
if [[ -d .venv ]]; then
source .venv/bin/activate
fi
bu_scope_pop_function
}
myproject_activate "$@"
5. Add custom commands
source ./activate
bu new-command --dir commands --name deploy
bu new-command --dir commands --name run-tests --source
Now bu deploy and bu run-tests are available with autocomplete.
6. Use it
cd your-project
source ./activate
bu # see your commands alongside built-ins
bu get-module # verify your module is loaded
bu deploy --help # auto-generated help
Cache management
After the first activation, the command registry is cached. Subsequent shell startups load from the cache and skip the scan entirely.
bu get-cache # list cached projects
bu clear-cache myproject # invalidate after adding/removing commands
bu clear-cache --all # invalidate all caches
7. Use your module as a library
If another project wants to use your module as a dependency (library mode), they source your *_bu_module.sh (never your activate) so your preinit callbacks run during their init and your commands appear alongside theirs. The recommended way is bu_module_require, sourced before bu_entrypoint.sh:
# In the host's activate script, BEFORE source bu_entrypoint.sh:
source "$BU_DIR/lib/bu_module_require.sh"
bu_module_require myproject --dir "/path/to/myproject"
bu_module_require is opt-in by presence (missing = one INFO line, rc 0), idempotent, and diamond-safe for nested requires. Add --required to abort the activate when the dependency is missing, and --git-url URL [--branch B] to autoclone it when absent (interactive ttys are prompted first).
Module registration (updated pattern)
The module script (myproject_bu_module.sh) appends to BU_MODULE_LIST:
#!/usr/bin/env bash
myproject_DIR=$(realpath -- "$(dirname -- "${BASH_SOURCE}")")
BU_MODULE_LIST+="myproject:0.1.0:$myproject_DIR/myproject_bu_preinit.sh;"
This makes the module visible to bu get-module and future module introspection tools. The legacy raw-array pattern still works but won’t appear in module listings.
Key concepts
| Concept | Purpose |
|---|---|
BU_MODULE_LIST | Semicolon-separated list of name:version:preinit_path entries. The sole module registry. |
| Module script | Appends to BU_MODULE_LIST. Top-level sets it directly. |
| Preinit callback | Runs during bu import-environment. Registers commands, aliases, keybindings. |
activate | Bootstrap script. Users source ./activate to enter the project environment. |
commands/ | Directory of bu subcommand scripts. Registered by preinit via bu import-environment -c. |