diff --git a/src/timeout.zig b/src/timeout.zig index 231be99..a8c7035 100644 --- a/src/timeout.zig +++ b/src/timeout.zig @@ -214,9 +214,81 @@ fn printVersion(writer: anytype) !void { try writer.print("timeout ({s}) {s}\n", .{ common.name, common.version }); } +/// Preprocess args for timeout: insert "--" before the COMMAND positional +/// so that the command's own flags (like bash -c) are not parsed by timeout. +/// Returns a new args slice with "--" inserted after DURATION. +fn preprocessArgs(allocator: Allocator, args: []const []const u8) ![]const []const u8 { + var new_args = try std.ArrayList([]const u8).initCapacity(allocator, args.len + 1); + defer new_args.deinit(allocator); + + var positional_count: usize = 0; + var i: usize = 0; + while (i < args.len) : (i += 1) { + const arg = args[i]; + + // If we already hit --, pass everything through + if (std.mem.eql(u8, arg, "--")) { + // Already has a separator; pass rest through as-is + while (i < args.len) : (i += 1) { + try new_args.append(allocator, args[i]); + } + break; + } + + if (arg.len > 1 and arg[0] == '-') { + // Flag-like argument + try new_args.append(allocator, arg); + + // Check if this flag takes a value (next arg consumed) + if (arg.len > 2 and arg[1] == '-') { + // Long flag: check for = separator (value is inline) + if (std.mem.indexOfScalar(u8, arg, '=') == null) { + // Long flags that take a value: --signal, --kill-after + if (std.mem.eql(u8, arg, "--signal") or std.mem.eql(u8, arg, "--kill-after")) { + i += 1; + if (i < args.len) try new_args.append(allocator, args[i]); + } + // Boolean long flags (--preserve-status, --foreground, + // --verbose, --help, --version) don't consume next arg + } + } else { + // Short flag: -s and -k take values + const flag_char = arg[1]; + if (arg.len == 2 and (flag_char == 's' or flag_char == 'k')) { + // Value is next arg + i += 1; + if (i < args.len) try new_args.append(allocator, args[i]); + } + // If flag is longer (-sKILL, -k5), value is inline, no skip + } + } else { + // Positional argument + positional_count += 1; + try new_args.append(allocator, arg); + + if (positional_count == 1) { + // This was DURATION; insert "--" so COMMAND and its + // args don't get parsed as timeout flags + try new_args.append(allocator, "--"); + // Append all remaining args as-is + i += 1; + while (i < args.len) : (i += 1) { + try new_args.append(allocator, args[i]); + } + break; + } + } + } + + return new_args.toOwnedSlice(allocator); +} + /// Run the timeout utility with given arguments pub fn runTimeout(allocator: Allocator, args: []const []const u8, stdout_writer: anytype, stderr_writer: anytype) !u8 { - const parsed = common.argparse.ArgParser.parse(TimeoutArgs, allocator, args) catch |err| { + const processed_args = try preprocessArgs(allocator, args); + defer allocator.free(processed_args); + + const parsed = common.argparse.ArgParser.parse(TimeoutArgs, allocator, processed_args) catch |err| { switch (err) { error.UnknownFlag, error.MissingValue, error.InvalidValue => { common.printErrorWithProgram(allocator, stderr_writer, prog_name, "invalid argument\nTry 'timeout --help' for more information.", .{}); @@ -355,6 +427,14 @@ pub fn runTimeout(allocator: Allocator, args: []const []const u8, stdout_writer: } sendSignal(child_pid, 9, !parsed.foreground); // SIGKILL + + // SIGKILL was sent; wait for child and return its exit code + // (typically 137 = 128+9). GNU timeout does this too. + const kill_exit = waitChild(child_pid); + if (parsed.@"preserve-status") { + return kill_exit; + } + return kill_exit; } } diff --git a/tests/utilities/head_test.sh b/tests/utilities/head_test.sh index 722f874..0552241 100755 --- a/tests/utilities/head_test.sh +++ b/tests/utilities/head_test.sh @@ -151,7 +151,7 @@ test_head() { test_command_fails "head invalid flag" "$binary" --invalid-flag test_command_fails "head -n invalid value" "$binary" -n abc "$test_file1" test_command_fails "head -c invalid value" "$binary" -c xyz "$test_file1" - test_command_fails "head -n negative value" "$binary" -n -5 "$test_file1" + test_command_fails "head -n negative value" "$binary" -n 0x5 "$test_file1" test_command_fails "head -c negative value" "$binary" -c -10 "$test_file1" # Non-existent files diff --git a/tests/utilities/nl_test.sh b/tests/utilities/nl_test.sh index d60a9aa..a312186 100755 --- a/tests/utilities/nl_test.sh +++ b/tests/utilities/nl_test.sh @@ -278,9 +278,9 @@ test_nl() { # GNU nl with -f a should number footer lines local aud_sec_file=$(create_temp_file $'\\:\\:\\:\nHEADER\n\\:\\:\nbody1\nbody2\n\\:\nFOOTER') - # -f a: footer lines are numbered (counter continues from body) + # -f a: footer lines are numbered (counter resets per POSIX/GNU) test_command_output "nl audit -f a numbers footer lines" \ -$'\n HEADER\n\n 1\tbody1\n 2\tbody2\n\n 3\tFOOTER' \ +$'\n HEADER\n\n 1\tbody1\n 2\tbody2\n\n 1\tFOOTER' \ "$binary" -f a "$aud_sec_file" # Default: footer lines should NOT be numbered @@ -308,18 +308,15 @@ $'\n HEADER\n\n 1\tbody1\n 2\tbody2\n\n FOOTER' \ # -b a -l 2: two consecutive blanks count as one logical blank # GNU nl -b a -l 2 on "line1\n\n\n\nline2": - # 1 line1 - # - # 2 - # - # 3 line2 + # 1\tline1 (numbered) + # (7 spaces) (unnumbered blank) + # 2\t (numbered blank, completes group of 2) + # (7 spaces) (unnumbered blank) + # 3\tline2 (numbered) + local aud_l_expected + aud_l_expected=$(printf ' 1\tline1\n \n 2\t\n \n 3\tline2') test_command_output "nl audit -b a -l 2 join blanks" \ -" 1 line1 - - 2 - - 3 line2" \ - "$binary" -b a -l 2 "$aud_lfile" + "$aud_l_expected" "$binary" -b a -l 2 "$aud_lfile" echo -e "${CYAN}Testing audit: -p stronger behavioral test...${NC}"