diff --git a/lib/just_bash/commands/echo.ex b/lib/just_bash/commands/echo.ex index 8d0c683..f79a08e 100644 --- a/lib/just_bash/commands/echo.ex +++ b/lib/just_bash/commands/echo.ex @@ -35,7 +35,11 @@ defmodule JustBash.Commands.Echo do defp parse_flags(rest, flags), do: {flags, rest} - defp interpret_escapes(str) do + # Also used by printf's %b directive, which shares echo -e's escape + # dialect (\0NNN octals, unlike $'...' ANSI-C quoting's \NNN). + @doc false + @spec interpret_escapes(String.t()) :: binary() + def interpret_escapes(str) do do_interpret_escapes(str, "") end diff --git a/lib/just_bash/commands/printf.ex b/lib/just_bash/commands/printf.ex index f970a94..a3e90b1 100644 --- a/lib/just_bash/commands/printf.ex +++ b/lib/just_bash/commands/printf.ex @@ -3,8 +3,9 @@ defmodule JustBash.Commands.Printf do @behaviour JustBash.Commands.Command alias JustBash.Commands.Command + alias JustBash.Commands.Echo - @format_regex ~r/%(-)?(0)?(\d+)?(?:\.(\d+))?([sdxXofec%])/ + @format_regex ~r/%(-)?(0)?(\d+)?(?:\.(\d+))?([sdxXofecb%])/ @impl true def names, do: ["printf"] @@ -42,7 +43,14 @@ defmodule JustBash.Commands.Printf do defp recycle_format(format, args, acc) do {result, remaining} = apply_formats_with_remaining(format, args) - recycle_format(format, remaining, [result | acc]) + + if length(remaining) < length(args) do + recycle_format(format, remaining, [result | acc]) + else + # A pass that consumed no arguments (an unrecognized directive) must + # not recycle the format forever. + recycle_format(format, [], [result | acc]) + end end defp apply_formats_with_remaining(format, args) do @@ -97,6 +105,10 @@ defmodule JustBash.Commands.Printf do {padded, []} end + defp do_format("b", arg, _precision) do + Echo.interpret_escapes(arg) + end + defp do_format("s", arg, precision) do case precision do "" -> arg diff --git a/test/commands/utilities_test.exs b/test/commands/utilities_test.exs index 02f0b6a..ac26725 100644 --- a/test/commands/utilities_test.exs +++ b/test/commands/utilities_test.exs @@ -136,6 +136,36 @@ defmodule JustBash.Commands.UtilitiesTest do assert result.stdout == "a\tb" end + test "printf %b expands escape sequences in the argument" do + bash = JustBash.new() + {result, _} = JustBash.exec(bash, ~S[printf "%b" "A\tB\n"]) + assert result.stdout == "A\tB\n" + assert result.exit_code == 0 + end + + @tag timeout: 5_000 + test "printf %b with hex escapes emits raw bytes and terminates" do + bash = JustBash.new() + {result, _} = JustBash.exec(bash, ~S[printf "%b" "A\xffB\n"]) + assert result.stdout == <> + end + + test "printf %b recycles the format across arguments" do + bash = JustBash.new() + {result, _} = JustBash.exec(bash, ~S[printf "%b\n" "a\tb" "c"]) + assert result.stdout == "a\tb\nc\n" + end + + # Bash errors on unknown directives; we pass them through literally. + # Either way, a directive that consumes no argument must not recycle + # the format forever. + @tag timeout: 5_000 + test "printf with an unrecognized directive terminates instead of looping" do + bash = JustBash.new() + {result, _} = JustBash.exec(bash, ~S[printf "%v\n" x]) + assert result.stdout == "%v\n" + end + test "printf with %x hex format" do bash = JustBash.new() {result, _} = JustBash.exec(bash, "printf '%x' 255")