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'] +}