Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* `awk` implements GNU-compatible `strftime([format [, timestamp [, utc-flag]]])` arguments and defaults
* `mix bash_fixtures.gen printf` enumerates a printf conversion × flag/width/precision matrix (`printf_matrix`) — #70 item 2
* `mix bash_fixtures.gen test` enumerates a `test`/`[` operator × revealing-shape matrix (`test_matrix`) — #70 item 2
* `mix bash_fixtures.gen varop` enumerates a `${var op word}` parameter-expansion matrix (`varop_matrix`) — #70 item 2
Expand Down
47 changes: 47 additions & 0 deletions lib/just_bash/commands/awk/evaluator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ defmodule JustBash.Commands.Awk.Evaluator do
"""

alias JustBash.Commands.Awk.{AST, Formatter}
alias JustBash.Commands.Date, as: DateCommand
alias JustBash.FS
alias JustBash.Limit

@default_strftime_format "%a %b %e %H:%M:%S %Z %Y"

@type state :: %{
nr: non_neg_integer(),
nf: non_neg_integer(),
Expand Down Expand Up @@ -1607,6 +1610,27 @@ defmodule JustBash.Commands.Awk.Evaluator do
Formatter.format_printf(format, args)
end

defp evaluate_function("strftime", [], state) do
format =
state.arrays
|> Map.get("PROCINFO", %{})
|> Map.get("strftime", @default_strftime_format)

format_strftime(format, DateTime.utc_now(), false)
end

defp evaluate_function("strftime", [format], _state) do
format_strftime(format, DateTime.utc_now(), false)
end

defp evaluate_function("strftime", [format, timestamp], _state) do
format_strftime(format, timestamp, false)
end

defp evaluate_function("strftime", [format, timestamp, utc_flag], _state) do
format_strftime(format, timestamp, truthy?(utc_flag))
end

# Math functions
defp evaluate_function("int", [arg], _state) do
parse_number(arg) |> trunc()
Expand Down Expand Up @@ -1664,6 +1688,29 @@ defmodule JustBash.Commands.Awk.Evaluator do

defp evaluate_function(_name, _args, _state), do: ""

defp format_strftime(format, %DateTime{} = datetime, utc?) do
datetime
|> strftime_datetime(utc?)
|> DateCommand.format_datetime(to_string(format))
end

defp format_strftime(format, timestamp, utc?) do
case timestamp |> parse_number() |> trunc() |> DateTime.from_unix() do
{:ok, datetime} ->
datetime
|> strftime_datetime(utc?)
|> DateCommand.format_datetime(to_string(format))

{:error, :invalid_unix_time} ->
""
end
end

# GNU awk names the forced UTC zone "GMT". Without the flag, JustBash uses
# its UTC sandbox clock and keeps the normal "UTC" zone name.
defp strftime_datetime(datetime, true), do: %{datetime | zone_abbr: "GMT"}
defp strftime_datetime(datetime, false), do: datetime

# Format a value for output - integers print without .0
# Format array keys: 0.0 -> "0", 1.0 -> "1", "1.0" -> "1", etc.
defp format_array_key(value) when is_float(value) do
Expand Down
4 changes: 3 additions & 1 deletion lib/just_bash/commands/date.ex
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,9 @@ defmodule JustBash.Commands.Date do
#
# `yr_spec` is the padding flag a compound conversion forwards to the year
# fields of its sub-format, and nil at the top level. See `yearish/3`.
defp format_datetime(datetime, format), do: scan(format, datetime, nil, [])
@doc false
@spec format_datetime(DateTime.t(), binary()) :: binary()
def format_datetime(datetime, format), do: scan(format, datetime, nil, [])

defp scan(<<>>, _datetime, _yr_spec, acc), do: acc |> Enum.reverse() |> IO.iodata_to_binary()

Expand Down
69 changes: 69 additions & 0 deletions test/commands/awk_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,75 @@ defmodule JustBash.Commands.AwkTest do
end
end

describe "strftime() function" do
test "formats explicit timestamps in local UTC and forced UTC" do
bash = JustBash.new()

{result, _} =
JustBash.exec(
bash,
~s|TZ=UTC awk 'BEGIN { print strftime("%F %T %Z %z", 0); print strftime("%F %T %Z %z", 1718458200, 1) }'|
)

assert result.stdout ==
"1970-01-01 00:00:00 UTC +0000\n2024-06-15 13:30:00 GMT +0000\n"

assert result.exit_code == 0
end

test "uses the current time and documented format defaults" do
before = DateTime.utc_now() |> DateTime.to_unix()

{result, _} =
JustBash.exec(
JustBash.new(),
~s|TZ=UTC awk 'BEGIN { print strftime(); print strftime("%s") }'|
)

after_time = DateTime.utc_now() |> DateTime.to_unix()
[default_time, timestamp, ""] = String.split(result.stdout, "\n")

assert default_time =~
~r/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [ 0-9][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2} UTC [0-9]{4}$/

assert String.to_integer(timestamp) in before..after_time
assert result.exit_code == 0
end

test "uses PROCINFO strftime as the default format" do
{result, _} =
JustBash.exec(
JustBash.new(),
~s|TZ=UTC awk 'BEGIN { PROCINFO["strftime"] = "%F"; print length(strftime()) }'|
)

assert result.stdout == "10\n"
assert result.exit_code == 0
end

test "coerces invalid timestamp text to the epoch" do
{result, _} =
JustBash.exec(
JustBash.new(),
~s|awk 'BEGIN { print strftime("%F", "not-a-timestamp", 1) }'|
)

assert result.stdout == "1970-01-01\n"
assert result.exit_code == 0
end

test "returns an empty string for an out-of-range timestamp or empty format" do
{result, _} =
JustBash.exec(
JustBash.new(),
~s|awk 'BEGIN { print strftime("%Y", 1e100, 1); print strftime("", 0, 1) }'|
)

assert result.stdout == "\n\n"
assert result.exit_code == 0
end
end

describe "gsub and sub" do
test "gsub replaces all occurrences" do
bash = JustBash.new(files: %{"/data.txt" => "hello world\n"})
Expand Down
15 changes: 15 additions & 0 deletions test/fixtures/bash_cases/awk.json
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,21 @@
"name": "awk edge cases comparison: awk string coercion in arithmetic",
"script": "echo '10abc 5' | awk '{print $1 + $2}'",
"content_hash": "587a86f56c3a1f53"
},
{
"name": "awk strftime comparison: explicit timestamp and UTC flag",
"script": "TZ=UTC LC_ALL=C awk 'BEGIN { print strftime(\"%F %T %Z %z\", 0); print strftime(\"%F %T %Z %z\", 1718458200, 1) }'",
"content_hash": "4b852164672823b4"
},
{
"name": "awk strftime comparison: invalid timestamp inputs and empty format",
"script": "TZ=UTC LC_ALL=C awk 'BEGIN { print strftime(\"%F\", \"not-a-timestamp\", 1); print strftime(\"%Y\", 1e100, 1); print strftime(\"\", 0, 1) }'",
"content_hash": "4ae0bc0e390c8664"
},
{
"name": "awk strftime comparison: omitted arguments use documented defaults",
"script": "TZ=UTC LC_ALL=C awk 'BEGIN { value = strftime(); print length(value), (index(value, \" UTC \") > 0); print length(strftime(\"%F\")); PROCINFO[\"strftime\"] = \"%Y\"; print length(strftime()) }'",
"content_hash": "f8b630f4b23a831d"
}
]
}
21 changes: 21 additions & 0 deletions test/fixtures/bash_expected/awk.json
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,27 @@
"name": "awk edge cases comparison: awk string coercion in arithmetic",
"stderr": "",
"stdout": "15\n"
},
{
"content_hash": "4b852164672823b4",
"exit_code": 0,
"name": "awk strftime comparison: explicit timestamp and UTC flag",
"stderr": "",
"stdout": "1970-01-01 00:00:00 UTC +0000\n2024-06-15 13:30:00 GMT +0000\n"
},
{
"content_hash": "4ae0bc0e390c8664",
"exit_code": 0,
"name": "awk strftime comparison: invalid timestamp inputs and empty format",
"stderr": "",
"stdout": "1970-01-01\n\n\n"
},
{
"content_hash": "f8b630f4b23a831d",
"exit_code": 0,
"name": "awk strftime comparison: omitted arguments use documented defaults",
"stderr": "",
"stdout": "28 1\n10\n4\n"
}
],
"suite": "awk"
Expand Down