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
22 changes: 22 additions & 0 deletions common/common.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
161 changes: 161 additions & 0 deletions common/stdbuf/stdbuf.v
Original file line number Diff line number Diff line change
@@ -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.')
}
38 changes: 38 additions & 0 deletions src/stdbuf/main.v
Original file line number Diff line number Diff line change
@@ -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'
68 changes: 68 additions & 0 deletions src/stdbuf/stdbuf_test.v
Original file line number Diff line number Diff line change
@@ -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']
}
Loading