Skip to content
shellmap

Check if a script is sourced or executed

Detect at runtime whether the current script was sourced into the parent shell (`. script` / `source script`) or executed in a subshell (`bash script`) — so the same file can be used both as a library and as a CLI.

How to check if a script is sourced or executed in each shell

Bashunix
[[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo sourced || echo executed

`BASH_SOURCE[0]` is the path to the CURRENT FILE; `$0` is the name of the invoking process. When sourced, they differ (BASH_SOURCE points at the source file, $0 points at the parent shell). When executed, they match.

Zshunix
[[ "${ZSH_EVAL_CONTEXT:-}" == *":file"* ]] && echo sourced || echo executed

`ZSH_EVAL_CONTEXT` is a colon-separated stack: `toplevel:cmdsubst:file` etc. The presence of `:file` (or just `file`) in the chain means we're being sourced from a file.

Fishunix
if test (status fish-path) = (status filename); echo executed; else; echo sourced; end

`status filename` is the path to the running script; `status fish-path` is the path to the fish binary itself. When sourced INTERACTIVELY, fish-path == filename. Compare by querying `status -i` (interactive shell — most likely sourced from rc) and `status -t` (subjob).

PowerShellwindows
if (`$MyInvocation.InvocationName -eq '.') { 'sourced' } else { 'executed' }

pwsh dot-sourcing uses literal `.` (`. .\script.ps1`) — `$MyInvocation.InvocationName` reflects this. Module-imported scripts (`Import-Module .\script.ps1`) show `Import-Module` as the InvocationName.

cmd.exewindows
echo cmd has no native sourced-vs-executed distinction

`call script.bat` (invoke within current cmd) vs `script.bat` (start fresh cmd) are the two patterns, but the script itself can't tell which mode it was called in (no introspection variable like BASH_SOURCE).

Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.

Gotchas & notes

  • **The "dual-mode script" use case**: a single file that's `source-able` to expose helper functions AND `executable` to run a default action. Pattern: `# function definitions here …` then at the bottom: `if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@"; fi`. When sourced (e.g. `source utils.sh`), the file just defines functions and returns. When executed (`./utils.sh`), `main "$@"` runs at the bottom. This is the Python `if __name__ == "__main__":` equivalent — extremely common in well-designed shell libraries.
  • **Why `return` vs `exit` is the practical difference**: `return` is only valid INSIDE a function OR in a sourced script (it stops processing without killing the shell). `exit` always kills the current process. If your script may be sourced and you `exit 1` on an error, you just killed the user's terminal session! Defensive pattern: define `die() { [[ "${BASH_SOURCE[0]}" != "${0}" ]] && return 1 || exit 1; }` and call `die` instead of `exit`. The `return` works when sourced, `exit` works when executed.
  • **Cross-shell portability is hard**: `BASH_SOURCE` is bash-only, `ZSH_EVAL_CONTEXT` is zsh-only, fish has its own `status` API. A truly portable detector is hard. One workaround: write two scripts — the bash/zsh detector idiom in one file, the fish one in another. Another: use Python or another scripting language with predictable behaviour if you need true cross-shell. POSIX sh has NOTHING here — `$0` reflects the invoking process even when sourced, with no other introspection. If your audience is "POSIX sh users", you cannot reliably support the dual-mode pattern.
  • **pwsh dot-sourcing vs module-import vs script-execution** are THREE distinct modes: (1) `.\script.ps1` — executes in a CHILD scope (variables defined inside don't leak out); (2) `. .\script.ps1` — dot-sourcing, runs in the CALLER's scope (variables leak out — used for rc files); (3) `Import-Module .\script.ps1` — formally imports as a module with explicit `Export-ModuleMember` controls. `$MyInvocation.InvocationName` returns `.\script.ps1`, `.`, and `Import-Module` respectively. To support all three modes, check `$MyInvocation.MyCommand.Path` for the file path, and use `$MyInvocation.InvocationName` for behavioural branching.
  • **Subprocess vs sourced — different lifecycle implications**: sourced code SHARES the parent shell's environment (variables, functions, cwd, traps); subprocess code starts fresh. A sourced script that does `cd /tmp` changes the parent's cwd; an executed one does not. A sourced script's `trap "echo bye" EXIT` fires when the PARENT exits (potentially never within your test session); an executed one fires at script end. Both behaviours are useful — but using the wrong one causes "why does my deployment cd into /tmp permanently after running deploy.sh" or "why is the trap not firing when I source the file" bugs. Knowing whether you're sourced is the first step to handling it.

Related commands

Related tasks