Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/just_bash.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ defmodule JustBash do
env: %{},
cwd: "/home/user",
functions: %{},
custom_builtin_registry: nil,
exit_code: 0,
last_exit_code: 0,
network: %{enabled: false, allow_list: []},
Expand Down Expand Up @@ -87,6 +88,7 @@ defmodule JustBash do
env: map(),
cwd: String.t(),
functions: map(),
custom_builtin_registry: {module(), term()} | nil,
exit_code: non_neg_integer(),
last_exit_code: non_neg_integer(),
network: network_config(),
Expand Down Expand Up @@ -125,6 +127,7 @@ defmodule JustBash do
cwd = Keyword.get(opts, :cwd, "/home/user")
network = Keyword.get(opts, :network, %{enabled: false, allow_list: []})
http_client = Keyword.get(opts, :http_client)
custom_builtin_registry = Keyword.get(opts, :custom_builtin_registry, nil)

default_env = %{
"HOME" => cwd,
Expand All @@ -142,6 +145,7 @@ defmodule JustBash do
env: Map.merge(default_env, env),
cwd: cwd,
functions: %{},
custom_builtin_registry: custom_builtin_registry,
exit_code: 0,
last_exit_code: 0,
network: Map.merge(%{enabled: false, allow_list: []}, network),
Expand Down
48 changes: 42 additions & 6 deletions lib/just_bash/commands/registry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,54 @@ defmodule JustBash.Commands.Registry do
@doc """
Get the module that implements the given command.
"""
@spec get(String.t()) :: module() | nil
def get(name), do: Map.get(@commands, name)
@spec get(JustBash.t(), String.t()) :: module() | nil
def get(bash, cmd) do
get_custom(bash, cmd) || Map.get(@commands, cmd)
end

@doc """
Check if a command exists.
"""
@spec exists?(String.t()) :: boolean()
def exists?(name), do: Map.has_key?(@commands, name)
@spec exists?(JustBash.t(), String.t()) :: boolean()
def exists?(bash, name) do
Map.has_key?(@commands, name) || exists_custom?(bash, name)
end

@doc """
List all available command names.
"""
@spec list() :: [String.t()]
def list, do: Map.keys(@commands)
@spec list(JustBash.t()) :: [String.t()]
def list(bash) do
Enum.uniq(Map.keys(@commands) ++ list_custom(bash))
end

defp get_custom(%JustBash{custom_builtin_registry: nil}, _cmd), do: nil

defp get_custom(%JustBash{custom_builtin_registry: module}, cmd) when is_atom(module) do
module.get(cmd)
end

defp get_custom(%JustBash{custom_builtin_registry: {module, ctx}}, cmd) do
module.get(cmd, ctx)
end

defp exists_custom?(%JustBash{custom_builtin_registry: nil}, _cmd), do: false

defp exists_custom?(%JustBash{custom_builtin_registry: module}, cmd) when is_atom(module) do
module.exists?(cmd)
end

defp exists_custom?(%JustBash{custom_builtin_registry: {module, ctx}}, cmd) do
module.exists?(cmd, ctx)
end

defp list_custom(%JustBash{custom_builtin_registry: nil}), do: []

defp list_custom(%JustBash{custom_builtin_registry: module}) when is_atom(module) do
module.list()
end

defp list_custom(%JustBash{custom_builtin_registry: {module, ctx}}) do
module.list(ctx)
end
end
6 changes: 3 additions & 3 deletions lib/just_bash/commands/which.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ defmodule JustBash.Commands.Which do

{output, all_found} =
Enum.reduce(opts.names, {"", true}, fn name, {acc_out, acc_found} ->
paths = find_command(bash.fs, path_dirs, name, opts.show_all)
paths = find_command(bash, path_dirs, name, opts.show_all)
accumulate_paths(paths, acc_out, acc_found, opts.silent)
end)

Expand Down Expand Up @@ -74,7 +74,7 @@ defmodule JustBash.Commands.Which do
parse_args(rest, %{opts | names: opts.names ++ [name]})
end

defp find_command(fs, path_dirs, name, show_all) do
defp find_command(%{fs: fs} = bash, path_dirs, name, show_all) do
results =
Enum.flat_map(path_dirs, fn dir ->
if dir == "" do
Expand All @@ -83,7 +83,7 @@ defmodule JustBash.Commands.Which do
full_path = Path.join(dir, name)

cond do
Registry.exists?(name) ->
Registry.exists?(bash, name) ->
[full_path]

file_exists?(fs, full_path) ->
Expand Down
5 changes: 4 additions & 1 deletion lib/just_bash/interpreter/executor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -600,10 +600,13 @@ defmodule JustBash.Interpreter.Executor do
end

defp execute_builtin(bash, cmd, args, stdin) do
case Registry.get(cmd) do
case Registry.get(bash, cmd) do
nil ->
{%{stdout: "", stderr: "bash: #{cmd}: command not found\n", exit_code: 127}, bash}

builtin when is_function(builtin, 3) ->
builtin.(bash, args, stdin)

module ->
module.execute(bash, args, stdin)
end
Expand Down
91 changes: 91 additions & 0 deletions test/custom_builtin_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
defmodule CustomBuiltinTest do
use ExUnit.Case
doctest JustBash

defmodule HelloBuiltinModule do
@behaviour JustBash.Commands.Command

alias JustBash.Commands.Command

@impl true
def names, do: ["hello_module"]

@impl true
def execute(bash, args, _stdin) do
output = "hello " <> Enum.join(args, "")
{Command.ok(output <> "\n"), bash}
end
end

defmodule CustomBuiltinRegistry do
# without context
def get("hello") do
fn bash, args, _stdin ->
output = "hello " <> Enum.join(args, "")
{JustBash.Commands.Command.ok(output <> "\n"), bash}
end
end

def get("hello_module"), do: HelloBuiltinModule
def get(_), do: nil

def exists?("hello"), do: true
def exists?("hello_module"), do: true
def exists?(_), do: false

# with context
def get("hello", username) do
fn bash, _args, _stdin ->
output = "hello #{username}"
{JustBash.Commands.Command.ok(output <> "\n"), bash}
end
end

def get(_cmd, _ctx), do: nil

def exists?("hello", "jose"), do: true
def exists?(_cmd, _ctx), do: false
end

describe "custom builtins:" do
test "can register and execute custom builtin via function" do
bash = JustBash.new(custom_builtin_registry: CustomBuiltinRegistry)
{result, _} = JustBash.exec(bash, "hello world")
assert result.stdout == "hello world\n"
end

test "can register and execute custom builtin via module" do
bash = JustBash.new(custom_builtin_registry: CustomBuiltinRegistry)
{result, _} = JustBash.exec(bash, "hello world")
assert result.stdout == "hello world\n"
end

test "can find custom command with which" do
bash = JustBash.new(custom_builtin_registry: CustomBuiltinRegistry)
{result, _} = JustBash.exec(bash, "which hello")
assert result.stdout == "/bin/hello\n"
end
end

describe "custom builtins with context:" do
test "can get custom builtin with more context" do
bash = JustBash.new(custom_builtin_registry: {CustomBuiltinRegistry, "jose"})
{result, _} = JustBash.exec(bash, "hello")
assert result.stdout == "hello jose\n"
end

test "can find custom command with which and context" do
bash = JustBash.new(custom_builtin_registry: {CustomBuiltinRegistry, "jose"})
{result, _} = JustBash.exec(bash, "which hello")
assert result.stdout == "/bin/hello\n"
assert result.exit_code == 0
end

test "which detects method presence by context" do
bash = JustBash.new(custom_builtin_registry: {CustomBuiltinRegistry, "stranger"})
{result, _} = JustBash.exec(bash, "which hello")
assert result.stdout == ""
assert result.exit_code == 1
end
end
end