Session GenServer for maintaining Bash execution context.
Each session maintains its own environment variables, working directory, and I/O context for executing Bash commands.
| Variable | Description |
|---|---|
BASH_VERSION | "5.3" Version information for this Bash. |
CDPATH | A colon-separated list of directories to search for directories given as arguments to cd. |
GLOBIGNORE | A colon-separated list of patterns describing filenames to be ignored by pathname expansion. |
HISTFILE | The name of the file where your command history is stored. |
HISTFILESIZE | The maximum number of lines this file can contain. |
HISTSIZE | The maximum number of history lines that a running shell can access. |
HOME | The complete pathname to your login directory. |
HOSTNAME | The name of the current host. |
HOSTTYPE | The type of CPU this version of Bash is running under. |
IGNOREEOF | Controls the action of the shell on receipt of an EOF character as the sole input. If set, then the value of it is the number of EOF characters that can be seen in a row on an empty line before the shell will exit (default 10). When unset, EOF signifies the end of input. |
MACHTYPE | A string describing the current system Bash is running on. |
MAILCHECK | How often, in seconds, Bash checks for new mail. |
MAILPATH | (Unsupported) A colon-separated list of filenames which Bash checks for new mail. |
OSTYPE | The version of Unix this version of Bash is running on. |
PATH | A colon-separated list of directories to search when looking for commands. |
PROMPT_COMMAND | (Unsupported) A command to be executed before the printing of each primary prompt. |
PS1 | (Unsupported) The primary prompt string. |
PS2 | (Unsupported) The secondary prompt string. |
PWD | The full pathname of the current directory. |
SHELLOPTS | A colon-separated list of enabled shell options. |
TERM | (Always set to "dumb") The name of the current terminal type. |
TIMEFORMAT | The output format for timing statistics displayed by the time reserved word. |
auto_resume | (Unsupported) Non-null means a command word appearing on a line by itself is first looked for in the list of currently stopped jobs. If found there, that job is foregrounded. A value of exact means that the command word must exactly match a command in the list of stopped jobs. A value of substring means that the command word must match a substring of the job. Any other value means that the command must be a prefix of a stopped job. |
histchars | (Unsupported) Characters controlling history expansion and quick substitution. The first character is the history substitution character, usually !. The second is the quick substitution character, usually ^. The third is the history comment character, usually #. |
| HISTIGNORE | A colon-separated list of patterns used to decide which commands should be saved on the history
list. |
Summary
Functions
Awaits the result of an execution started with execute_async/3.
Send job to background (resume if stopped).
Begin a new command execution with fresh StringIO streams.
Changes the working directory for the session.
Close a file descriptor.
End the current execution and move it to completed executions.
Executes a command AST within this session synchronously.
Executes a command AST asynchronously and returns a handle.
Get a specific execution by index.
Clear the accumulated output and return what was collected.
Bring job to foreground.
Gets all environment variables from the session.
Get the command history for this session.
Gets the current working directory for the session.
Gets an environment variable from the session.
Get a specific job by number.
Get accumulated output from the session's output collector.
Get the session state (for builtins that need direct access).
Gets a variable value from the session, with optional index/key for arrays.
Read a line from stdin (convenience wrapper for read/3).
Lists all running sessions.
List loaded API namespaces.
Get all jobs for this session.
Load an Elixir API module into a session.
Creates a new session with default environment.
Creates a child session that inherits state from a parent.
Open a file descriptor for reading or writing.
Open a StringIO device for stdin from a string.
Wire the previous execution's stdout to the current stdin for pipeline stages.
Get and clear completed jobs for notification display.
Write a line to stdout (convenience wrapper for write/3).
Read from an input source.
Sets an environment variable in the session.
Sends a signal to a foreground execution.
Send signal to job.
Start a background job and return its job number and OS PID.
Get the merged stderr content from all completed executions.
Get the merged stdout content from all completed executions.
Stops a session and its job supervisor.
Wait for job(s) to complete.
Write to an output destination.
Types
@type signal() :: :sigint | :sigkill | :sigterm | :sighup | :sigquit | :sigusr1 | :sigusr2 | :sigcont | :sigstop | :int | :kill | :term | :hup | :quit | :usr1 | :usr2 | :cont | :stop | integer()
@type t() :: %Bash.Session{ aliases: %{required(String.t()) => String.t()}, call_stack: [ %{ line_number: pos_integer(), function_name: String.t(), source_file: String.t() } ], call_timeout: timeout(), command_history: [Bash.CommandResult.t()], command_policy: Bash.CommandPolicy.t(), completed_jobs: [Bash.Job.t()], current: Bash.Execution.t() | nil, current_execution: running_execution() | nil, current_function_name: term(), current_job: pos_integer() | nil, dir_stack: [String.t()], elixir_modules: %{required(String.t()) => module()}, executions: [Bash.Execution.t()], file_descriptors: %{ required(non_neg_integer()) => pid() | {:coproc, pid(), :read | :write} }, filesystem: Bash.Filesystem.t(), functions: %{required(String.t()) => Bash.AST.Function.t()}, hash: %{required(String.t()) => {pos_integer(), String.t()}}, id: String.t(), in_function: boolean(), in_loop: boolean(), in_trap: boolean(), is_pipeline_tail: boolean(), job_supervisor: pid() | nil, jobs: %{required(pos_integer()) => pid()}, local_vars: MapSet.t(String.t()), next_job_number: pos_integer(), options: %{required(atom()) => boolean()}, output_collector: pid() | nil, pending_executions: [pending_execution()], pipe_stdin: term(), positional_params: [[String.t()]], previous_job: pos_integer() | nil, saved_vars: [map()], signal_jobs_fn: term(), special_vars: %{required(String.t()) => integer() | String.t() | nil}, start_background_job_fn: term(), start_runtime_ms: term(), stderr: pid() | nil, stderr_sink: Bash.Sink.t() | nil, stdin: pid() | nil, stdin_device: pid() | nil, stdout: pid() | nil, stdout_sink: Bash.Sink.t() | nil, traps: %{required(String.t()) => String.t() | :ignore}, variables: %{required(String.t()) => Bash.Variable.t()}, working_dir: String.t() }
Functions
@spec await(Bash.Session.ExecRef.t(), timeout()) :: {:ok | :error | :exit | :exec, Bash.CommandResult.t()} | {:error, :timeout | {:session_down, term()}}
Awaits the result of an execution started with execute_async/3.
Blocks until the execution completes, is cancelled, or timeout elapses.
Returns:
{:ok | :error | :exit | :exec, CommandResult.t()}— execution finished (or was cancelled — exit code reflects the signal: 130/143/137 witherror: :cancelled).{:error, :timeout}—timeoutexceeded; the execution is still in flight or queued. Call again later orsignal/2to cancel.{:error, {:session_down, reason}}— the session GenServer crashed while this execution was outstanding.
Examples
{:ok, exec_ref} = Session.execute_async(session, ast)
# Wait indefinitely
{:ok, result} = Session.await(exec_ref)
# Wait at most 5 seconds; cancel and try again on timeout
case Session.await(exec_ref, 5_000) do
{:error, :timeout} ->
Session.signal(exec_ref, :sigint)
Session.await(exec_ref)
result ->
result
end
@spec background_job(pid(), pos_integer() | nil) :: :ok | {:error, term()}
Send job to background (resume if stopped).
Begin a new command execution with fresh StringIO streams.
Creates a new Execution struct with separate stdout/stderr streams and sets it as the current execution.
Options
:pipeline_tail- Whether this command is at the end of a pipeline (default: true). Only pipeline tail commands forward to user sinks.
Examples
session = Session.begin_execution(session, "echo hello")
session = Session.begin_execution(session, "cat", pipeline_tail: false)
Changes the working directory for the session.
@spec close_fd(t(), non_neg_integer()) :: t()
Close a file descriptor.
Examples
session = Session.close_fd(session, 3)
End the current execution and move it to completed executions.
Marks the execution with the given exit code and timestamp, then appends it to the executions list.
Options
:exit_code- The exit code for the execution (default: 0)
Examples
session = Session.end_execution(session, exit_code: 0)
session = Session.end_execution(session, exit_code: 1)
Executes a command AST within this session synchronously.
Blocks until the command completes and returns the result.
Options
:on_output- Callback function for streaming output. When provided, output is streamed to the callback as it arrives instead of being accumulated in the result. The callback receives{:stdout, binary}or{:stderr, binary}tuples.
Examples
# Standard execution (accumulates output)
{:ok, session} = Session.new()
{:ok, result} = Session.execute(session, ast)
# Streaming execution (output flows to callback)
{:ok, session} = Session.new()
{:ok, result} = Session.execute(session, ast, on_output: fn
{:stdout, data} -> IO.write(data)
{:stderr, data} -> IO.write(:stderr, data)
end)
@spec execute_async(pid(), Bash.AST.t(), keyword()) :: {:ok, Bash.Session.ExecRef.t()}
Executes a command AST asynchronously and returns a handle.
Returns {:ok, exec_ref} immediately. Pass the exec_ref to await/2
to retrieve the result, or to signal/2 to interrupt the execution.
Bash semantics allow only one foreground execution per session at a time.
If another execution is already running, this call queues behind it; the
queued execution starts as soon as the current one finishes (or is
cancelled). State (variables, traps, working directory) flows from one
chunk to the next, so two execute_async/3 calls behave like appending
AST chunks to a single sequential script — but each chunk has its own
awaitable handle and can be cancelled independently.
Examples
{:ok, session} = Session.new()
{:ok, exec_ref} = Session.execute_async(session, ast)
{:ok, result} = Session.await(exec_ref)
# Cancel an in-flight execution; user-defined `trap` handlers run.
{:ok, exec_ref} = Session.execute_async(session, long_running_ast)
:ok = Session.signal(exec_ref, :sigint)
{:error, %CommandResult{exit_code: 130}} = Session.await(exec_ref)
# Queue multiple chunks; each runs after the previous one completes.
{:ok, ref1} = Session.execute_async(session, parsed("echo first"))
{:ok, ref2} = Session.execute_async(session, parsed("echo second"))
{:ok, _} = Session.await(ref1)
{:ok, _} = Session.await(ref2)
# Cancel a queued chunk before it runs:
{:ok, ref3} = Session.execute_async(session, parsed("never"))
:ok = Session.signal(ref3, :sigint)
{:error, %CommandResult{exit_code: 130}} = Session.await(ref3)
@spec execution(t(), non_neg_integer()) :: Bash.Execution.t() | nil
Get a specific execution by index.
Examples
exec = Session.execution(session, 0)
Execution.stdout_contents(exec)
Clear the accumulated output and return what was collected.
Useful for tests that want to run multiple commands and check output after each.
@spec foreground_job(pid(), pos_integer() | nil) :: {:ok, Bash.CommandResult.t()} | {:error, term()}
Bring job to foreground.
Blocks until the job completes and returns a CommandResult.
Gets all environment variables from the session.
@spec get_command_history(pid()) :: [Bash.CommandResult.t()]
Get the command history for this session.
Gets the current working directory for the session.
Gets an environment variable from the session.
@spec get_job(pid(), pos_integer()) :: {:ok, Bash.Job.t()} | {:error, :not_found}
Get a specific job by number.
Get accumulated output from the session's output collector.
Returns {stdout, stderr} tuple with all output captured during execution.
This is the primary way to retrieve output in tests.
Examples
{:ok, session} = Session.new()
{:ok, _, _} = Bash.run(~BASH"echo hello", session)
{stdout, stderr} = Session.get_output(session)
assert stdout =~ "hello"
Get the session state (for builtins that need direct access).
Gets a variable value from the session, with optional index/key for arrays.
Retrieves the session state and delegates to Variable.get/2 or Variable.get/3.
Examples
Session.get_var(session, "myvar")
Session.get_var(session, "myarray", 0)
Session.get_var(session, "myassoc", "key")
Read a line from stdin (convenience wrapper for read/3).
Options
:source- Source to read from (default: :stdin):delimiter- Line delimiter (default: "\n")
Examples
{:ok, line} = Session.gets(session)
{:ok, line} = Session.gets(session, source: {:fd, 3})
Lists all running sessions.
Returns a list of tuples containing the session ID and pid.
Options
:registry- The registry to query (default:Bash.SessionRegistry)
Examples
iex> {:ok, session} = Bash.Session.new()
iex> sessions = Bash.Session.list()
iex> Enum.any?(sessions, fn {_id, pid} -> pid == session end)
true
List loaded API namespaces.
@spec list_jobs(pid()) :: [Bash.Job.t()]
Get all jobs for this session.
Load an Elixir API module into a session.
The module must use Bash.Interop and define a namespace.
Once loaded, functions defined with defbash become callable from
bash scripts as namespace.function_name.
Examples
# Load into a running session (recommended)
{:ok, session} = Session.new()
:ok = Session.load_api(session, MyApp.BashAPI)
# Or load at session creation
{:ok, session} = Session.new(apis: [MyApp.BashAPI])
# Now myapp.* functions are available in bash scripts
Creates a new session with default environment.
Options
:id— unique session identifier (auto-generated if omitted):working_dir— initial working directory (defaults toFile.cwd!()):env— map of environment variables to set (e.g.,%{"HOME" => "/root"}):env_include— list of host env vars to include (defaults to all):env_exclude— list of host env vars to exclude:options— shell options map (e.g.,%{hashall: true, braceexpand: true}):command_policy—%CommandPolicy{}struct or keyword list for building one. Controls which commands the session can execute across all categories (builtins, externals, functions, interop). SeeBash.CommandPolicy.# Block all external commands Bash.Session.new(command_policy: [commands: :no_external]) # Allow only builtins and specific externals Bash.Session.new(command_policy: [commands: [{:allow, [:builtins, "cat", "grep"]}]]) # Allow builtins and functions, block externals and interop Bash.Session.new(command_policy: [commands: [{:allow, [:builtins, :functions]}]]) # Deny specific commands Bash.Session.new(command_policy: [commands: [{:disallow, ["rm"]}, {:allow, :all}]]):apis— list ofBash.Interopmodules to load at creation:aliases— map of command aliases:functions— map of pre-defined bash functions:script_name— value of$0(defaults to"bash"):args— positional parameters ($1,$2, etc.):call_timeout— timeout for GenServer calls (defaults to:infinity)
Creates a child session that inherits state from a parent.
The child session inherits (per bash behavior):
- Environment variables
- Working directory
- Functions
- Shell options
The child session does NOT inherit (per bash behavior):
- Aliases
- Hash table (command path cache)
The child session gets its own:
- Job supervisor and job table
- Session ID
This is used for subshell execution where changes to env/cwd should not propagate back to the parent.
Open a file descriptor for reading or writing.
Examples
session = Session.open_fd(session, 3, "/path/to/file", [:read])
session = Session.open_fd(session, 4, "/path/to/output", [:write])
Open a StringIO device for stdin from a string.
Useful for providing initial input to a session or pipeline.
Examples
session = Session.open_stdin(session, "line1\nline2\n")
Wire the previous execution's stdout to the current stdin for pipeline stages.
Takes the stdout content from the last completed execution and creates a new StringIO device for reading as the next command's stdin.
Examples
# After cmd1 completes:
session = Session.pipe_forward(session)
# Now stdin reads from cmd1's stdout
@spec pop_completed_jobs(pid()) :: [Bash.Job.t()]
Get and clear completed jobs for notification display.
Write a line to stdout (convenience wrapper for write/3).
Appends a newline to the data.
Examples
session = Session.puts(session, "hello") # writes "hello\n"
@spec read(t(), :stdin | {:fd, non_neg_integer()}, :line | :all | non_neg_integer()) :: {:ok, String.t()} | :eof | {:error, term()}
Read from an input source.
Sources
:stdin- Read from session's stdin{:fd, n}- Read from file descriptor n
Modes
:line- Read a single line (default):all- Read all available contentnwhen is_integer(n) - Read n bytes
Examples
{:ok, line} = Session.read(session, :stdin, :line)
{:ok, all} = Session.read(session, :stdin, :all)
{:ok, chunk} = Session.read(session, {:fd, 3}, 1024)
Sets an environment variable in the session.
@spec signal(pid() | Bash.Session.ExecRef.t(), signal(), keyword()) :: :ok | {:error, :not_found}
Sends a signal to a foreground execution.
Targeting
- Pass a session
pid()— signals the currently running foreground execution, if any. Pending (queued) executions are unaffected. - Pass an
ExecRef.t()— signals that specific execution, whether it's currently running or still queued.
Returns :ok if a signal was delivered, {:error, :not_found} if no
matching execution is in flight.
Signals
:sigint— cooperative cancel. The script'sINTandEXITtraps run (with output captured in the session's collector); exit code 130.:sigterm— cooperative cancel. The script'sTERMandEXITtraps run; exit code 143.:sigkill— untrappable hard kill. No traps run. Use this when traps could hang. Exit code 137.
Cancelling a queued (not-yet-running) execution removes it from the queue
and produces an {:error, %CommandResult{exit_code: ..., error: :cancelled}}
via await/2. No traps run because the script never executed.
For signaling background jobs (started via & or job control), use
signal_job/3.
Options
:grace— milliseconds to wait for a cooperative:sigint/:sigtermto land before escalating to:sigkill. Useful when a script may be stuck in a non-yielding builtin or a misbehaving trap. Ignored for:sigkilland for queued (not-running) executions.
Examples
# Signal a specific execution by ref
{:ok, exec_ref} = Session.execute_async(session, long_ast)
:ok = Session.signal(exec_ref, :sigint)
{:error, %CommandResult{exit_code: 130}} = Session.await(exec_ref)
# Signal whatever is running by session pid (no ref needed)
:ok = Session.signal(session, :sigterm)
# Hard kill — bypasses traps
:ok = Session.signal(exec_ref, :sigkill)
# Cooperative cancel with automatic escalation to :sigkill after 5s
:ok = Session.signal(exec_ref, :sigint, grace: 5_000)
# Cancel a queued execution before it runs
{:ok, ref1} = Session.execute_async(session, current_ast)
{:ok, ref2} = Session.execute_async(session, queued_ast)
:ok = Session.signal(ref2, :sigint)
{:error, %CommandResult{exit_code: 130}} = Session.await(ref2)
@spec signal_job(pid(), pos_integer(), atom() | integer()) :: :ok | {:error, term()}
Send signal to job.
@spec start_background_job( pid(), keyword() ) :: {:ok, job_number :: pos_integer(), os_pid :: pos_integer() | nil} | {:error, term()}
Start a background job and return its job number and OS PID.
Options
:command- Command name to execute (required):args- List of arguments (default: [])
@spec stderr( t(), keyword() ) :: Enumerable.t() | String.t()
Get the merged stderr content from all completed executions.
Options
:index- Get stderr from a specific execution by index
Examples
# All stderr as a stream
Session.stderr(session) |> Enum.to_list()
# Specific execution's stderr
Session.stderr(session, index: 0)
@spec stdout( t(), keyword() ) :: Enumerable.t() | String.t()
Get the merged stdout content from all completed executions.
Options
:index- Get stdout from a specific execution by index
Examples
# All stdout as a stream
Session.stdout(session) |> Enum.to_list()
# Specific execution's stdout
Session.stdout(session, index: 0)
@spec stop(pid()) :: :ok
Stops a session and its job supervisor.
@spec wait_for_jobs(pid(), [pos_integer()] | nil) :: {:ok, [integer()]} | {:error, term()}
Wait for job(s) to complete.
@spec write(t(), :stdout | :stderr | {:fd, non_neg_integer()}, iodata()) :: t()
Write to an output destination.
Writes to the current execution's StringIO stream. If the session is at the pipeline tail and has user-provided sinks, also forwards to those.
Destinations
:stdout- Write to stdout:stderr- Write to stderr{:fd, n}- Write to file descriptor n
Examples
session = Session.write(session, :stdout, "hello\n")
session = Session.write(session, :stderr, "error message\n")
session = Session.write(session, {:fd, 3}, data)