diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bffa5b..124b7eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 9fae026..f3aded2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/bash/ast/for_loop.ex b/lib/bash/ast/for_loop.ex index cc4a872..aedcf40 100644 --- a/lib/bash/ast/for_loop.ex +++ b/lib/bash/ast/for_loop.ex @@ -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 diff --git a/lib/bash/ast/while_loop.ex b/lib/bash/ast/while_loop.ex index f517f76..f7eb40c 100644 --- a/lib/bash/ast/while_loop.ex +++ b/lib/bash/ast/while_loop.ex @@ -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 diff --git a/mix.exs b/mix.exs index c169692..fa98fc5 100644 --- a/mix.exs +++ b/mix.exs @@ -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 [ diff --git a/test/bash/control_flow_test.exs b/test/bash/control_flow_test.exs index 6661212..9eabacd 100644 --- a/test/bash/control_flow_test.exs +++ b/test/bash/control_flow_test.exs @@ -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 diff --git a/test/bash/session_test.exs b/test/bash/session_test.exs index 48fe828..bd4b3f9 100644 --- a/test/bash/session_test.exs +++ b/test/bash/session_test.exs @@ -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 @@ -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