From 8fccf85a9abf976a48bbcd66ccd94d08ed7e4c4e Mon Sep 17 00:00:00 2001 From: metif12 Date: Sun, 11 Oct 2026 13:03:19 +0330 Subject: [PATCH] stdbuf: implement it Runs a command with modified buffering for its standard streams. Options are -i, -o and -e, each taking L for line buffering, 0 for unbuffered, or a byte count with an optional k/m/g suffix. The modes are collected and validated but not applied, and that gap is stated rather than hidden: applying them means preloading a compiled shared library that overrides setvbuf in the child, and building that library at run time is not something this port does. The command still runs, with its default buffering, which is the same tradeoff nice makes for niceness. Exit codes are GNU's and were measured: 125 for anything stdbuf itself could not do, 127 for a command that could not be found, and the command's own status otherwise. This also lands common.die and common.fail_with_help in common.v, because the exit status differs by failure kind and the existing common.exit_with_error_message always exited 1. Those two helpers were introduced by the nix-utils branch, which now only needs to drop its copy. --- common/common.v | 22 ++++++ common/stdbuf/stdbuf.v | 161 +++++++++++++++++++++++++++++++++++++++ src/stdbuf/main.v | 38 +++++++++ src/stdbuf/stdbuf_test.v | 68 +++++++++++++++++ 4 files changed, 289 insertions(+) create mode 100644 common/stdbuf/stdbuf.v create mode 100644 src/stdbuf/main.v create mode 100644 src/stdbuf/stdbuf_test.v diff --git a/common/common.v b/common/common.v index 8050fc87..97d77396 100644 --- a/common/common.v +++ b/common/common.v @@ -66,6 +66,28 @@ pub fn exit_with_error_message(tool_name string, error string) { exit(1) } +// die reports a GNU-style failure and exits. The status is passed in because +// the tools disagree about it: chown uses 1 for an operand problem while chroot +// and runcon use 125 for anything they could not do themselves. +@[noreturn] +pub fn die(tool_name string, message string, code int) { + if message.len > 0 { + eprintln('${tool_name}: ${message}') + } + exit(code) +} + +// fail_with_help reports a usage failure and exits 1, which is what GNU does +// for a malformed command line regardless of the tool. +@[noreturn] +pub fn fail_with_help(tool_name string, message string) { + if message.len > 0 { + eprintln(': ') + } + eprintln("Try '${tool_name} --help' for more information.") + exit(1) +} + // A common way to exit with a custom error code (default: 1) // and the ability to direct the user to help (default: false) // to make it easier to match the output of the GNU coreutils diff --git a/common/stdbuf/stdbuf.v b/common/stdbuf/stdbuf.v new file mode 100644 index 00000000..86b866b5 --- /dev/null +++ b/common/stdbuf/stdbuf.v @@ -0,0 +1,161 @@ +module stdbuf + +import common + +const app_name = 'stdbuf' + +// Which stream an -i, -o or -e option applies to. +pub enum Stream { + input + output + error +} + +// How a stream is buffered. +pub enum Mode { + unbuffered // 0 + line // L + bytes // an explicit size +} + +pub struct Settings { +pub mut: + // One mode per stream, in the order the options were given. + modes []StreamMode + command []string +} + +pub struct StreamMode { +pub: + stream Stream + mode Mode + size u64 +} + +pub fn parse_args(args []string) Settings { + mut s := Settings{} + mut command := []string{} + + for i := 0; i < args.len; i++ { + a := args[i] + if a == '--help' { + print_help() + exit(0) + } + if a == '--version' { + println('stdbuf (V coreutils) ${common.version}') + exit(0) + } + if a == '--' { + command = args[i + 1..].clone() + break + } + if !a.starts_with('-') || a == '-' { + // The first non-option operand starts the command. + command = args[i..].clone() + break + } + + if a.starts_with('-') && a.len > 1 { + c := a[1] + if c != `i` && c != `o` && c != `e` { + common.die(app_name, "invalid option -- '${c.ascii_str()}'", 125) + } + value := if a.len > 2 { + a[2..] + } else { + if i + 1 >= args.len { + common.die(app_name, "option requires an argument -- '${c.ascii_str()}'", + 125) + } + i++ + args[i] + } + stream := if c == `i` { + Stream.input + } else if c == `o` { + Stream.output + } else { + Stream.error + } + mode, size := parse_mode(value) + s.modes << StreamMode{ + stream: stream + mode: mode + size: size + } + } + } + + s.command = command + return s +} + +// parse_mode reads an -i/-o/-e value. L means line buffered, 0 means +// unbuffered, and anything else is a byte count with an optional k/m/g +// suffix. +fn parse_mode(v string) (Mode, u64) { + if v == '' { + return Mode.bytes, 0 + } + if v == 'L' { + return Mode.line, 0 + } + last := v[v.len - 1] + mut mult := u64(1) + mut digits := v + match last { + `k` { + mult = 1024 + digits = v[..v.len - 1] + } + `K` { + mult = 1024 + digits = v[..v.len - 1] + } + `m` { + mult = 1024 * 1024 + digits = v[..v.len - 1] + } + `M` { + mult = 1024 * 1024 + digits = v[..v.len - 1] + } + `g` { + mult = 1024 * 1024 * 1024 + digits = v[..v.len - 1] + } + `G` { + mult = 1024 * 1024 * 1024 + digits = v[..v.len - 1] + } + else {} + } + mut n := u64(0) + for c in digits.bytes() { + if c < `0` || c > `9` { + common.die(app_name, "invalid mode '${v}': Value too large for defined data type", + 125) + } + n = n * 10 + u64(c - `0`) + } + if v == '0' { + return Mode.unbuffered, 0 + } + return Mode.bytes, n * mult +} + +fn print_help() { + println('Usage: stdbuf OPTION... COMMAND') + println('Run COMMAND, with modified buffering operations for its standard streams.') + println('') + println(' -i, --input=MODE adjust standard input stream buffering') + println(' -o, --output=MODE adjust standard output stream buffering') + println(' -e, --error=MODE adjust standard error stream buffering') + println(' --help display this help and exit') + println(' --version output version information and exit') + println('') + println("If MODE is 'L' the corresponding stream will be line buffered.") + println("If MODE is '0' the corresponding stream will be unbuffered.") + println('Otherwise MODE is a number which may be followed by a size suffix.') +} diff --git a/src/stdbuf/main.v b/src/stdbuf/main.v new file mode 100644 index 00000000..6d2d6f7c --- /dev/null +++ b/src/stdbuf/main.v @@ -0,0 +1,38 @@ +import common +import common.stdbuf +import os + +fn main() { + set := stdbuf.parse_args(os.args[1..]) + if set.command.len == 0 { + common.die(app_name, 'missing operand', 125) + } + + // The buffer modes are collected but not applied: applying them means + // preloading a compiled shared library that overrides setvbuf in the + // child, and building that library at run time is not something this + // port does. The command still runs, with its default buffering, which + // is the same tradeoff nice makes for niceness. See the module comment. + _ = set.modes + + if !os.exists(set.command[0]) && !set.command[0].contains('/') + && !set.command[0].contains('\\') { + abs := os.find_abs_path_of_executable(set.command[0]) or { '' } + if abs == '' { + common.die(app_name, "failed to run command '${set.command[0]}': No such file or directory", + 127) + } + } + if (set.command[0].contains('/') || set.command[0].contains('\\')) + && !os.exists(set.command[0]) { + common.die(app_name, "failed to run command '${set.command[0]}': No such file or directory", + 127) + } + + os.execvp(set.command[0], set.command[1..]) or { + common.die(app_name, "failed to run command '${set.command[0]}': No such file or directory", + 127) + } +} + +const app_name = 'stdbuf' diff --git a/src/stdbuf/stdbuf_test.v b/src/stdbuf/stdbuf_test.v new file mode 100644 index 00000000..0fa91cd5 --- /dev/null +++ b/src/stdbuf/stdbuf_test.v @@ -0,0 +1,68 @@ +import common.stdbuf + +fn test_parse_args_defaults() { + s := stdbuf.parse_args([]) + assert s.modes.len == 0 + assert s.command.len == 0 +} + +fn test_parse_args_line_mode() { + // -oL is the line-buffered spelling. + s := stdbuf.parse_args(['-oL', 'cat']) + assert s.modes.len == 1 + assert s.modes[0].stream == .output + assert s.modes[0].mode == .line + assert s.command == ['cat'] +} + +fn test_parse_args_unbuffered() { + s := stdbuf.parse_args(['-o0', 'cat']) + assert s.modes.len == 1 + assert s.modes[0].mode == .unbuffered + assert s.modes[0].size == 0 +} + +fn test_parse_args_byte_count() { + s := stdbuf.parse_args(['-o', '4096', 'cat']) + assert s.modes.len == 1 + assert s.modes[0].mode == .bytes + assert s.modes[0].size == 4096 +} + +fn test_parse_args_size_suffixes() { + s := stdbuf.parse_args(['-o', '1K', 'cat']) + assert s.modes[0].size == 1024 + + m := stdbuf.parse_args(['-o', '1M', 'cat']) + assert m.modes[0].size == 1024 * 1024 + + g := stdbuf.parse_args(['-o', '1G', 'cat']) + assert g.modes[0].size == 1024 * 1024 * 1024 +} + +fn test_parse_args_all_three_streams() { + s := stdbuf.parse_args(['-i0', '-oL', '-e0', 'cat']) + assert s.modes.len == 3 + assert s.modes[0].stream == .input + assert s.modes[1].stream == .output + assert s.modes[2].stream == .error + assert s.command == ['cat'] +} + +fn test_parse_args_separate_value() { + // The value may be its own argument as well as attached. + s := stdbuf.parse_args(['-o', 'L', 'cat', 'arg']) + assert s.modes[0].mode == .line + assert s.command == ['cat', 'arg'] +} + +fn test_parse_args_command_with_arguments() { + s := stdbuf.parse_args(['-o0', 'printf', '%s', 'x']) + assert s.command == ['printf', '%s', 'x'] +} + +fn test_parse_args_double_dash() { + // Everything after -- is the command even when it looks like an option. + s := stdbuf.parse_args(['-o0', '--', '-weird-cmd']) + assert s.command == ['-weird-cmd'] +}