-
Use four spaces for indentation. No tabs.
-
Shell/local variables and function names follow
snake_case: only lowercase letters, underscores, and digits. -
Environment variables use all-uppercase names. For example:
BASE_HOME,BASE_HOST,BASE_OS,BASE_SOURCES. -
In rare cases of global variables being shared between library functions and their callers, use all-uppercase names. For example:
OUTPUTandOUTPUT_ARRAY. -
Place most code inside functions and invoke the main function at the bottom of the script.
-
In libraries, have top-level code that prevents the file from being sourced more than once. For example:
[[ $__stdlib_sourced__ ]] && return __stdlib_sourced__=1
-
Make sure all local variables inside functions are declared
local. -
Use
__func__naming convention for special-purpose variables and functions. Use a leading underscore for "private" variables and functions. -
Double-quote all variable expansions, except:
- inside
[[ ]]or(( )) - places where we need word splitting to take place
- inside
-
Use
[[ $var ]]to check ifvarhas non-zero length, instead of[[ -n $var ]]. -
Use "compact" style for if statements and loops:
if condition; then ... fi while condition; do ... done for ((i=0; i < limit; i++)); do ... done
-
Make sure the code passes ShellCheck checks.
- Do not use
set -ein Base shell scripts or libraries. - Do not rely on implicit shell exit behavior for control flow.
- Prefer explicit error handling using helper functions such as:
runexit_if_errorfatal_error
- When a command may fail as part of normal flow, handle that failure
intentionally with
if,case,||, or an explicit return-code check. - A script should make its error-handling strategy obvious to the reader.
Rationale:
set -einteracts poorly with conditionals, pipelines, subshells, and sourced code.- Base is a wrapper- and library-heavy shell framework, so implicit exit rules make control flow harder to reason about.
- Explicit error handling is more verbose, but much easier to debug and maintain.
Base-owned CLIs should live in per-command directories.
Recommended layout:
cli/bash/commands/
setup/
setup.sh
README.md
tests/
doctor/
doctor.sh
README.md
tests/
Why:
- command code, docs, and tests stay together
- each command can grow without cluttering a shared flat directory
- the structure scales cleanly as Base adds more commands
Libraries should also live in per-library directories.
Recommended layout:
lib/bash/
std/
lib_std.sh
README.md
tests/
git/
lib_git.sh
README.md
tests/
Why:
- each library is treated as a module
- the README can describe the module in detail
- tests live next to the library they validate
Small framework-level singleton files may remain flat when they are not really "modules" in the same sense. Examples include:
cli/bash/bin/base-wrappercli/env/baseenv.sh
Even though commands and libraries live in per-module directories, keep high-level index READMEs at the parent level when helpful, for example:
cli/bash/bin/README.mdlib/bash/README.mdcli/bash/commands/README.md
Those top-level READMEs should act as catalogs and maps, while each module's
local README.md should document the module itself.
Base should support two wrapper modes, but they serve different purposes.
This is the default mode for commands owned by the Base repo.
Pattern:
cli/bash/bin/<command>.shis a symlink tobase-wrapperbase-wrapperresolves<command>by convention- the real script lives at
cli/bash/commands/<command>/<command>.sh
Use this mode for:
- commands that are part of Base itself
- commands that should appear as first-class Base entrypoints
- commands that benefit from command discovery, listing, and strict layout
Why this is the default:
- it gives Base a consistent command surface
- it works well with per-command docs and tests
- it keeps user-facing entrypoints separate from implementation files
This mode should be supported for wrapped Bash commands that live outside the Base repo.
Pattern:
#!/usr/bin/env base-wrapperUse this mode for:
- scripts in sibling repos that still want Base-managed execution behavior
- standalone wrapped scripts that should not be forced into Base's internal
commands/<name>/<name>.shlayout
Why this mode matters:
- Base is intended to support multiple repos in one workspace
- not every wrapped command should have to live physically inside Base
- a shebang-based wrapper is the more portable cross-repo mechanism
The standard is:
- Use symlink-dispatched wrapper mode as the default for Base-owned commands.
- Support shebang wrapper mode for wrapped commands that live outside Base.
- Do not force every wrapped command in the workspace to be relocated into the Base repo just to gain wrapper behavior.
In other words, the symlink convention is the preferred in-repo ergonomics, while the shebang convention is the preferred cross-repo portability story.
Base-managed shell startup files follow this separation of concerns:
bash_profile/zprofile- thin login-shell handoff
bashrc/zshrc- interactive shell bootstrap
base_defaults.sh/zsh_defaults.sh- optional shared interactive defaults
~/.baserc- machine-local overrides
Startup files should stay thin and predictable. Interactive shell behavior belongs in the rc files, not in the login profile files.