Bash.Interop (Bash v0.6.0)

Copy Markdown View Source

Define Elixir functions callable from Bash scripts.

This module provides the defbash macro for creating functions that can be invoked from bash scripts using the namespace.function syntax.

Usage

defmodule MyApp.BashAPI do
  use Bash.Interop, namespace: "myapp"

  defbash greet(args, _state) do
    case args do
      [name | _] ->
        Bash.puts("Hello #{name}!\n")
        :ok

      [] ->
        {:error, "usage: myapp.greet NAME"}
    end
  end
end

Functions are loaded into a session with Bash.Session.load_api/2:

{:ok, session} = Bash.Session.new()
Bash.Session.load_api(session, MyApp.BashAPI)

Then callable from Bash as myapp.greet "World".

I/O Functions

Inside defbash functions, use the Bash module for I/O:

  • Bash.puts(message) - Write raw data to stdout (no newline added)
  • Bash.puts(:stderr, message) - Write raw data to stderr (no newline added)
  • Bash.stream(:stdin) - Get stdin as a lazy Stream
  • Bash.stream(:stdout, enumerable) - Stream each element to stdout
  • Bash.stream(:stderr, enumerable) - Stream each element to stderr
  • Bash.get_state() - Get the current session state map
  • Bash.update_state(updates) - Accumulate state update deltas

Return Values

defbash functions must return one of:

Success

  • :ok - Exit code 0, no additional output
  • {:ok, message} - Exit code 0, message (binary) written to stdout
  • {:ok, stream} - Exit code 0, lazy stream consumed to stdout
  • {:ok, exit_code} - Exit with exit_code (integer), no additional output

Error

  • {:error, exit_code} - Exit with exit_code (integer), no output
  • {:error, message} - Exit code 1, message (binary) written to stderr
  • {:error, stream} - Exit code 1, lazy stream consumed to stderr

Loop Control

Only valid when the function is called inside a for, while, or until loop:

  • :continue - Skip to the next loop iteration
  • :break - Break out of the enclosing loop

State Updates

Use Bash.update_state/1 to request changes to session state. Updates are accumulated as deltas and applied after execution completes. Common keys:

  • :variables - Map of variable name to string or Bash.Variable (merged, strings auto-wrapped as exported)
  • :working_dir - New working directory (replaced)
  • :options - Shell options map (merged)

Examples

Simple output

defbash hello(_args, _state) do
  Bash.puts("hello world\n")
  :ok
end

Return-based output

defbash version(_args, _state) do
  {:ok, "1.0.0\n"}
end

Error handling

defbash divide(args, _state) do
  case args do
    [a, b] ->
      b = String.to_integer(b)

      if b == 0 do
        {:error, "divide: division by zero\n"}
      else
        {:ok, "#{div(String.to_integer(a), b)}\n"}
      end

    _ ->
      {:error, "usage: math.divide A B\n"}
  end
end

Streaming stdin

defbash upcase(_args, _state) do
  Bash.stream(:stdin)
  |> Stream.each(fn chunk ->
    Bash.puts(String.upcase(chunk))
  end)
  |> Stream.run()

  :ok
end

Streaming output

defbash count(_args, _state) do
  {:ok, Stream.map(1..5, &"#{&1}\n")}
end

Custom exit code

defbash check(args, _state) do
  if File.exists?(List.first(args, "")) do
    :ok
  else
    {:ok, 1}
  end
end

State updates

defbash set_var(args, _state) do
  case args do
    [name, value] ->
      Bash.update_state(%{variables: %{name => Bash.Variable.new(value)}})
      :ok

    _ ->
      {:error, "usage: myapp.set_var NAME VALUE\n"}
  end
end

Loop control

# In a bash loop: for i in 1 2 3; do myapp.skip_even "$i"; done
defbash skip_even(args, _state) do
  n = args |> List.first() |> String.to_integer()

  if rem(n, 2) == 0 do
    :continue
  else
    Bash.puts("#{n}\n")
    :ok
  end
end

Summary

Functions

Sets up a module to define bash-callable functions.

Define a bash-callable function.

Functions

__using__(opts)

(macro)

Sets up a module to define bash-callable functions.

Options

  • :namespace - Required. The namespace prefix for bash calls.
  • :on_define - Optional. A callback function invoked at compile time when each defbash function is defined. Receives (function_name, module) and should return a metadata map or nil. This allows external modules to annotate functions using module attributes.

Example

defmodule MyApp.BashAPI do
  use Bash.Interop, namespace: "myapp"
end

Example with on_define callback

defmodule MyApp.RemoteBashAPI do
  use Bash.Interop,
    namespace: "remote",
    on_define: fn name, module ->
      execute_on = Module.get_attribute(module, :execute_on) || :guest
      Module.delete_attribute(module, :execute_on)
      %{execute_on: execute_on}
    end

  @execute_on :server
  defbash server_func(args, _state), do: {:ok, "runs on server"}

  defbash guest_func(args, _state), do: {:ok, "runs on guest"}
end

defbash(arg, list)

(macro)

Define a bash-callable function.

The function receives two arguments:

  • args - List of string arguments passed from bash
  • state - The current session state map

I/O

Inside the function body, use the Bash module for I/O:

  • Bash.puts(message) - Write raw data to stdout
  • Bash.puts(:stderr, message) - Write raw data to stderr
  • Bash.stream(:stdin) - Get stdin as a lazy Stream
  • Bash.stream(:stdout, enumerable) - Stream each element to stdout
  • Bash.stream(:stderr, enumerable) - Stream each element to stderr
  • Bash.get_state() - Get current session state
  • Bash.update_state(updates) - Accumulate state update deltas

Return Values

Success

  • :ok - Exit code 0, no additional output
  • {:ok, binary} - Exit code 0, binary written to stdout
  • {:ok, stream} - Exit code 0, lazy stream consumed to stdout
  • {:ok, exit_code} - Exit with integer code, no additional output

Error

  • {:error, exit_code} - Exit with integer code, no output
  • {:error, binary} - Exit code 1, binary written to stderr
  • {:error, stream} - Exit code 1, lazy stream consumed to stderr

Loop Control

  • :continue - Skip to next loop iteration (only valid inside loops)
  • :break - Break out of enclosing loop (only valid inside loops)

Examples

defbash greet(args, _state) do
  case args do
    [name | _] ->
      Bash.puts("Hello #{name}!\n")
      :ok

    [] ->
      {:error, "usage: myapp.greet NAME\n"}
  end
end

defbash upcase(_args, _state) do
  Bash.stream(:stdin)
  |> Stream.each(fn chunk ->
    Bash.puts(String.upcase(chunk))
  end)
  |> Stream.run()

  :ok
end

defbash count(_args, _state) do
  {:ok, Stream.map(1..5, &"#{&1}\n")}
end