Bash.Session (Bash v0.3.0)

Copy Markdown View Source

Session GenServer for maintaining Bash execution context.

Each session maintains its own environment variables, working directory, and I/O context for executing Bash commands.

VariableDescription
BASH_VERSION"5.3" Version information for this Bash.
CDPATHA colon-separated list of directories to search for directories given as arguments to cd.
GLOBIGNOREA colon-separated list of patterns describing filenames to be ignored by pathname expansion.
HISTFILEThe name of the file where your command history is stored.
HISTFILESIZEThe maximum number of lines this file can contain.
HISTSIZEThe maximum number of history lines that a running shell can access.
HOMEThe complete pathname to your login directory.
HOSTNAMEThe name of the current host.
HOSTTYPEThe type of CPU this version of Bash is running under.
IGNOREEOFControls 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.
MACHTYPEA string describing the current system Bash is running on.
MAILCHECKHow often, in seconds, Bash checks for new mail.
MAILPATH(Unsupported) A colon-separated list of filenames which Bash checks for new mail.
OSTYPEThe version of Unix this version of Bash is running on.
PATHA 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.
PWDThe full pathname of the current directory.
SHELLOPTSA colon-separated list of enabled shell options.
TERM(Always set to "dumb") The name of the current terminal type.
TIMEFORMATThe 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

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 within this session asynchronously.

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).

Sets an environment variable in the session.

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

t()

@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()],
  completed_jobs: [Bash.Job.t()],
  current: Bash.Execution.t() | 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}
  },
  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(),
  is_pipeline_tail: boolean(),
  job_supervisor: pid() | nil,
  jobs: %{required(pos_integer()) => pid()},
  next_job_number: pos_integer(),
  options: %{required(String.t()) => boolean()},
  output_collector: pid() | nil,
  pipe_stdin: term(),
  positional_params: [[String.t()]],
  previous_job: pos_integer() | nil,
  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

background_job(session, job_spec \\ nil)

@spec background_job(pid(), pos_integer() | nil) :: :ok | {:error, term()}

Send job to background (resume if stopped).

begin_execution(session, command, opts \\ [])

@spec begin_execution(t(), String.t(), keyword()) :: t()

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)

chdir(session, path)

Changes the working directory for the session.

close_fd(session, fd)

@spec close_fd(t(), non_neg_integer()) :: t()

Close a file descriptor.

Examples

session = Session.close_fd(session, 3)

end_execution(session, opts)

@spec end_execution(
  t(),
  keyword()
) :: t()

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)

execute(session, ast, opts \\ [])

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)

execute_async(session, ast)

Executes a command AST within this session asynchronously.

Returns immediately without waiting for the command to complete. The result will be stored in the session's command history.

Examples

{:ok, session} = Session.new()
:ok = Session.execute_async(session, ast)
# Command executes in background

execution(session, index)

@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)

flush_output(session)

@spec flush_output(pid()) :: {String.t(), String.t()}

Clear the accumulated output and return what was collected.

Useful for tests that want to run multiple commands and check output after each.

foreground_job(session, job_spec \\ nil)

@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.

get_all_env(session)

Gets all environment variables from the session.

get_command_history(session)

@spec get_command_history(pid()) :: [Bash.CommandResult.t()]

Get the command history for this session.

get_cwd(session)

Gets the current working directory for the session.

get_env(session, key)

Gets an environment variable from the session.

get_job(session, job_number)

@spec get_job(pid(), pos_integer()) :: {:ok, Bash.Job.t()} | {:error, :not_found}

Get a specific job by number.

get_output(session)

@spec get_output(pid()) :: {String.t(), String.t()}

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(~b"echo hello", session)
{stdout, stderr} = Session.get_output(session)
assert stdout =~ "hello"

get_state(session)

@spec get_state(pid()) :: t()

Get the session state (for builtins that need direct access).

get_var(session, var_name)

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")

get_var(session, var_name, index_or_key)

gets(session, opts \\ [])

@spec gets(
  t(),
  keyword()
) :: {:ok, String.t()} | :eof | {:error, term()}

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})

list(opts \\ [])

@spec list(keyword()) :: [{String.t(), pid()}]

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_apis(session)

@spec list_apis(pid() | t()) :: [String.t()]

List loaded API namespaces.

list_jobs(session)

@spec list_jobs(pid()) :: [Bash.Job.t()]

Get all jobs for this session.

load_api(session, module)

@spec load_api(pid(), module()) :: :ok

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

new(opts \\ [])

Creates a new session with default environment.

new_child(parent, opts \\ [])

@spec new_child(
  t() | pid(),
  keyword()
) :: {:ok, pid()} | {:error, term()}

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_fd(session, fd, path, modes)

@spec open_fd(t(), non_neg_integer(), String.t(), [atom()]) ::
  {:ok, t()} | {:error, term()}

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_stdin(session, content)

@spec open_stdin(t(), String.t()) :: t()

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")

pipe_forward(session)

@spec pipe_forward(t()) :: t()

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

pop_completed_jobs(session)

@spec pop_completed_jobs(pid()) :: [Bash.Job.t()]

Get and clear completed jobs for notification display.

puts(session, data)

@spec puts(t(), iodata()) :: t()

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"

read(session, source \\ :stdin, mode \\ :line)

@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 content
  • n when 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)

set_env(session, key, value)

Sets an environment variable in the session.

signal_job(session, job_spec, signal)

@spec signal_job(pid(), pos_integer(), atom() | integer()) :: :ok | {:error, term()}

Send signal to job.

start_background_job(session, opts)

@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: [])

start_link(opts)

stderr(session, opts \\ [])

@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)

stdout(session, opts \\ [])

@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)

stop(session)

@spec stop(pid()) :: :ok

Stops a session and its job supervisor.

wait_for_jobs(session, job_specs \\ nil)

@spec wait_for_jobs(pid(), [pos_integer()] | nil) ::
  {:ok, [integer()]} | {:error, term()}

Wait for job(s) to complete.

write(session, arg2, data)

@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)