Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.6.0 (2026-10-13)

- BREAKING - `Bash.Session.execute_async/3` now returns `{:ok, exec_ref}` instead of `:ok`. Pass the `Bash.Session.ExecRef` to `Bash.Session.await/2` to get the result. Only one foreground execution runs per session at a time; additional calls queue behind it and share state like sequential chunks of one script.
- Add `Bash.Session.await/2` with an optional timeout. `Bash.Session.execute/3` also accepts a `:timeout` option.
- Add `Bash.Session.signal/3` to cancel a running or queued execution by session pid or `ExecRef`. `:sigint` and `:sigterm` run the script's `INT`/`TERM` and `EXIT` traps (exit codes 130/143); `:sigkill` skips traps (exit code 137). The `:grace` option escalates to `:sigkill` if a cooperative cancel doesn't land in time.
- Fix type warning on Elixir 1.20.

## 0.5.1 (2026-04-13)

- Load API modules before executing. thanks @lostbean
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ and the ability to extend Bash with Elixir functions.

```elixir
# Add to mix.exs
{:bash, "~> 0.5.1"}
{:bash, "~> 0.6.0"}
```

```elixir
Expand Down
3 changes: 3 additions & 0 deletions lib/bash/ast/for_loop.ex
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,9 @@ defmodule Bash.AST.ForLoop do
{:exit, result} ->
{:exit, %{exit_code: result.exit_code}, env_acc}

{:exit, result, _updates} ->
{:exit, %{exit_code: result.exit_code}, env_acc}

{:error, result} ->
{:error, result}
end
Expand Down
3 changes: 3 additions & 0 deletions lib/bash/ast/while_loop.ex
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,9 @@ defmodule Bash.AST.WhileLoop do
{:exit, result} ->
{:exit, %{exit_code: result.exit_code}, env_acc}

{:exit, result, _updates} ->
{:exit, %{exit_code: result.exit_code}, env_acc}

{:error, result} ->
{:error, result}
end
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Bash.MixProject do
use Mix.Project

@source_url "https://github.com/tv-labs/bash"
@version "0.5.1"
@version "0.6.0"

def project do
[
Expand Down
36 changes: 36 additions & 0 deletions test/bash/control_flow_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -399,5 +399,41 @@ defmodule Bash.ControlFlowTest do

assert String.contains?(get_stdout(result), "should reach here")
end

test "set -e exits from a looping function called inside a for loop", %{session: session} do
script = """
set -e
fails() { for j in 1; do false; done; }
for i in 1 2; do
echo $i
fails
done
echo "should not reach here"
"""

{:ok, ast} = Parser.parse(String.trim(script))
{:exit, result} = Session.execute(session, ast)

assert result.exit_code == 1
assert get_stdout(result) == "1\n"
end

test "set -e exits from a looping function called inside a while loop", %{session: session} do
script = """
set -e
fails() { for j in 1; do false; done; }
while true; do
echo looped
fails
done
echo "should not reach here"
"""

{:ok, ast} = Parser.parse(String.trim(script))
{:exit, result} = Session.execute(session, ast)

assert result.exit_code == 1
assert get_stdout(result) == "looped\n"
end
end
end
13 changes: 6 additions & 7 deletions test/bash/session_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -858,12 +858,9 @@ defmodule Bash.SessionTest do
pid = pid_arg(self())

# session_test.spin is a defbash with no yield points — cooperative
# cancel can't land while it's running.
{:ok, ast} =
Bash.Parser.parse("""
session_test.notify '#{pid}'
session_test.spin
""")
# cancel can't land while it's running. It notifies from inside the
# builtin so the signal can't land in the yield before it starts.
{:ok, ast} = Bash.Parser.parse("session_test.spin '#{pid}'")

{:ok, ref} = Session.execute_async(session, ast)
assert_receive :script_running, 1000
Expand Down Expand Up @@ -1158,7 +1155,9 @@ defmodule Bash.SessionTest do
raise "boom"
end

defbash spin(_args, _state) do
defbash spin([pid_str], _state) do
pid = pid_str |> String.to_charlist() |> :erlang.list_to_pid()
send(pid, :script_running)
spin_loop()
end

Expand Down
Loading