Skip to content
Open
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
82 changes: 81 additions & 1 deletion src/timeout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.", .{});
Expand Down Expand Up @@ -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;
}
}

Expand Down
2 changes: 1 addition & 1 deletion tests/utilities/head_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 10 additions & 13 deletions tests/utilities/nl_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}"

Expand Down
Loading