diff --git a/common/pwd/pwd_nix.c.v b/common/pwd/pwd_nix.c.v index aa30f817..e32721fe 100644 --- a/common/pwd/pwd_nix.c.v +++ b/common/pwd/pwd_nix.c.v @@ -22,11 +22,21 @@ struct C.passwd { pw_shell &char } +// getgrgid returns a `struct group`, not a `struct passwd`. Reading it through +// a passwd only worked because gr_name happens to sit at the same offset. +struct C.group { + gr_name &char + gr_passwd &char + gr_gid u32 + gr_mem voidptr // char ** +} + fn C.getpwuid(int) &C.passwd -fn C.getgrgid(int) &C.passwd +fn C.getgrgid(int) &C.group fn C.getpwnam(&char) &C.passwd -fn C.getgroups(int, &int) int -fn C.getgrouplist(&char, int, &int, &int) int +// gid_t is a 32 bit type, so both of these fill a u32 array. +fn C.getgroups(int, &u32) int +fn C.getgrouplist(&char, u32, &u32, &int) int pub fn get_uid_for_name(username string) !int { r := C.getpwnam(username.str) @@ -60,7 +70,7 @@ pub fn get_name_for_gid(gid int) !string { } return os.error_posix() } - return cstring_to_vstring(r.pw_name) + return cstring_to_vstring(r.gr_name) } } @@ -82,26 +92,29 @@ pub fn get_groups(username string) ![]int { user := get_userinfo_for_name(username)! unsafe { mut count := init_group_buf_size - mut groups := []int{len: count} - mut res := C.getgrouplist(username.str, user.gid, &groups[0], &count) + // getgrouplist stores gid_t values, which are 32 bits wide. Handing it + // a []int would leave the upper half of every slot holding whatever was + // there before, so the ids come back as things like 103079215108. + mut gids := []u32{len: count} + mut res := C.getgrouplist(username.str, u32(user.gid), &gids[0], &count) // If the buffer was not big enough, count will be updated with the // number we need. if res == -1 { - groups = []int{len: count} - res = C.getgrouplist(username.str, user.gid, &groups[0], &count) + gids = []u32{len: count} + res = C.getgrouplist(username.str, u32(user.gid), &gids[0], &count) } - return groups[0..count] + return gids[0..count].map(int(it)) } } fn get_effective_groups_up_to(limit int) !(int, []int) { - mut groups := []int{len: init_group_buf_size} unsafe { - num_groups := C.getgroups(limit, &groups[0]) + gids := []u32{len: init_group_buf_size} + num_groups := C.getgroups(limit, &gids[0]) if num_groups < 0 { return os.error_posix() } - return num_groups, groups + return num_groups, gids[0..num_groups].map(int(it)) } } diff --git a/common/readutmp_nix.c.v b/common/readutmp_nix.c.v index 61a2ccd4..0b5b59f8 100644 --- a/common/readutmp_nix.c.v +++ b/common/readutmp_nix.c.v @@ -4,30 +4,30 @@ module common #include #include -// TODO: conflicts with `timeval` in -// -/* -pub struct C.timeval { - tv_sec i64 // Seconds. - tv_usec i64 // Microseconds. -}*/ - -// -pub struct C.timeval { -pub: - tv_sec u64 // Seconds. - tv_usec u64 // Microseconds. +// `struct utmpx` was previously declared twice, once here and once in +// src/users/users.c.v with a different (shorter) field list, which V rejects as +// a redeclaration. Declare it once, here, with the glibc layout. +// +// The fixed size fields are UT_LINESIZE/UT_NAMESIZE/UT_HOSTSIZE (32/32/256) and +// the element size has to match what getutxent() writes, because read_utmp() +// copies entries into a []C.utmpx. +pub struct C.exit_status { + et_code u16 + et_term u16 } pub struct C.utmpx { - ut_type i16 // Type of login. - ut_pid int // Process ID of login process. - ut_line [32]char // Devicename. - ut_id [4]char // Inittab ID. - ut_user [32]char // Username. - ut_host [256]char // Hostname for remote login. - ut_tv C.timeval // TODO: Declare sub struct correctly - ut_addr_v6 [4]int // Internet address of remote host. + ut_type i16 // Type of login. + ut_pid int // Process ID of login process. + ut_line [32]u8 // Devicename. + ut_id [4]u8 // Inittab ID. + ut_user [32]u8 // Username. + ut_host [256]u8 // Hostname for remote login. + ut_exit C.exit_status // Exit status of the process. + ut_session_id i32 // Session ID. + ut_tv C.timeval // Time entry. + ut_addr_v6 [4]i32 // Internet address of remote host. + __unused [20]u8 // Reserved. } // sets the name of the utmp-format file for the other utmp functions to access. diff --git a/common/testing/testrig.v b/common/testing/testrig.v index c87cceda..5206fa5c 100644 --- a/common/testing/testrig.v +++ b/common/testing/testrig.v @@ -84,8 +84,7 @@ pub fn (rig TestRig) assert_platform_util() { } else { // uptime was moved to procps-ng and may not be available in coreutils - assert - ver == 'uptime (GNU coreut' || ver == 'uptime (coreutils)' || ver == 'uptime from procps' + assert ver == 'uptime (GNU coreut' || ver == 'uptime (coreutils)' || ver == 'uptime from procps' } } } diff --git a/src/base64/base64.v b/src/base64/base64.v index 456c9a87..cd1cda29 100644 --- a/src/base64/base64.v +++ b/src/base64/base64.v @@ -159,7 +159,7 @@ fn main() { exit(1) } if wraping_opt < 0 { - eprintln('${application_name}: invalid wrap size: \'${wraping_opt}\'') + eprintln("${application_name}: invalid wrap size: '${wraping_opt}'") exit(1) } diff --git a/src/basenc/basenc.v b/src/basenc/basenc.v index b9175f70..f04a2e99 100644 --- a/src/basenc/basenc.v +++ b/src/basenc/basenc.v @@ -1,5 +1,5 @@ import os -import v.mathutil +import math import encoding.base64 import encoding.base32 @@ -51,7 +51,7 @@ fn print_encoded(encoded string, options Options) { return } for start := 0; start < encoded.len; start += options.wrap { - end := mathutil.min(start + options.wrap, encoded.len) + end := math.min(start + options.wrap, encoded.len) // safe to use string slicing because all chars // are in printable ascii println(encoded[start..end]) diff --git a/src/cksum/cksum.v b/src/cksum/cksum.v index 146aef5f..f4e4bad4 100644 --- a/src/cksum/cksum.v +++ b/src/cksum/cksum.v @@ -70,8 +70,9 @@ fn calc_sums(args Args) { } mut len_counter := total_length - for ; len_counter; len_counter >>= 8 { + for len_counter > 0 { crc = (crc << 8) ^ crctab[0][((crc >> 24) ^ len_counter) & 0xFF] + len_counter >>= 8 } crc = ~crc & 0xffff_ffff diff --git a/src/cp/helper.v b/src/cp/helper.v index 9c36db01..b259355d 100644 --- a/src/cp/helper.v +++ b/src/cp/helper.v @@ -69,7 +69,7 @@ fn success_exit(messages ...string) { exit(0) } -fn setup_cp_command(args []string) ?(CpCommand, []string, string) { +fn setup_cp_command(args []string) (CpCommand, []string, string) { mut fp := common.flag_parser(args) fp.application('cp') fp.limit_free_args_to_at_least(1) or { common.exit_with_error_message(name, err.msg()) } @@ -143,9 +143,7 @@ fn setup_cp_command(args []string) ?(CpCommand, []string, string) { } pub fn run_cp(args []string) { - cp, sources, dest := setup_cp_command(args) or { - common.exit_with_error_message(name, err.msg()) - } + cp, sources, dest := setup_cp_command(args) if sources.len > 1 && !os.is_dir(dest) { common.exit_with_error_message(name, target_not_dir(dest)) } diff --git a/src/cut/cut.v b/src/cut/cut.v index c3764386..03084132 100644 --- a/src/cut/cut.v +++ b/src/cut/cut.v @@ -3,8 +3,8 @@ import arrays import common import flag import io +import math import os -import v.mathutil const app_name = 'cut' const space_comma = ' ,' @@ -124,13 +124,13 @@ fn combine_ranges_and_zero_index(ranges []Range, max int, complement bool) []Ran outer: for range in ranges.sorted(a.start < b.start) { start := range.start - 1 - end := if range.end == -1 { max } else { mathutil.min(max, range.end) } + end := if range.end == -1 { max } else { math.min(max, range.end) } for mut combined_range in combined_ranges { if range_overlaps_range(start, end, combined_range.start, combined_range.end) { combined_range = Range{ - start: mathutil.min(start, combined_range.start) - end: mathutil.max(end, combined_range.end) + start: math.min(start, combined_range.start) + end: math.max(end, combined_range.end) } continue outer } @@ -189,7 +189,7 @@ fn get_args(args []string) Args { delimiter := fp.string('delimter', `d`, '', 'use instead of TAB for field delimter') fields := fp.string('fields', `f`, '', 'select only fields; also print any line${wrap}' + - 'that contains no delimiter character, unless the${wrap}-s option is specified') + 'that contains no delimiter character, unless the${wrap}-s option is specified') fp.bool('', `n`, false, '(ignored)') complement := fp.bool('complement', ` `, false, 'complement the set of selected bytes, characters${wrap}or fields') @@ -207,9 +207,9 @@ fn get_args(args []string) Args { 'range, or many ranges separated by commas. Selected input is written${eol}' + 'in the same order that it is read, and is written exactly once.${eol}${eol}' + 'Each range is one of:${eol}${eol}' + - ' N N\'th byte, character or field, counted from 1${eol}' + - ' N- from N\'th byte, character or field, to end of line${eol}' + - ' N-M from N\'th to M\'th (included) byte, character or field${eol}' + " -M from first to M'th (included) byte, character or field") + " N N'th byte, character or field, counted from 1${eol}" + + " N- from N'th byte, character or field, to end of line${eol}" + + " N-M from N'th to M'th (included) byte, character or field${eol}" + " -M from first to M'th (included) byte, character or field") fp.footer(common.coreutils_footer()) file_args := fp.finalize() or { exit_error(err.msg()) } diff --git a/src/expand/expand.v b/src/expand/expand.v index 0e54e215..8771e1c0 100644 --- a/src/expand/expand.v +++ b/src/expand/expand.v @@ -8,7 +8,7 @@ const nl = '\n' fn process_line(line string, initial bool, tabs int) { mut sp := '' - for i := tabs; i; i-- { + for _ in 0 .. tabs { sp += ' ' } diff --git a/src/expr/expr_test.v b/src/expr/expr_test.v index 5dd9c71b..7d15be0e 100644 --- a/src/expr/expr_test.v +++ b/src/expr/expr_test.v @@ -161,7 +161,7 @@ const mb_tests = [ // fail: multi; broken UTF-8 is treated in different way // "substr \u03B1bc\xB4ef 3 3", 'match abcdef ab', - 'match abcdef "\\(ab\\)"' + 'match abcdef "\\(ab\\)"', // fail: single; this is a good news, regex in vlib supports UTF-8 // "match \u03B1bc\u03B4ef .bc", // fail: both; broken regex of vlib, see #10885 diff --git a/src/fmt/fmt.v b/src/fmt/fmt.v index 1226fdb9..441ff77c 100644 --- a/src/fmt/fmt.v +++ b/src/fmt/fmt.v @@ -241,11 +241,11 @@ fn process_args(args []string) App { crown_marg := fp.bool('crown-margin', `c`, false, 'preserve the indentation of the first two lines within' + - '${pad}a paragraph, and align the left margin of each' + - '${pad}subsequent line with that of the second line') + '${pad}a paragraph, and align the left margin of each' + + '${pad}subsequent line with that of the second line') prefix_str := fp.string('prefix', `p`, '', 'reformat only lines beginning with STRING, reattaching ' + - '${pad}the prefix to reformatted lines') + '${pad}the prefix to reformatted lines') split_only := fp.bool('split-only', `s`, false, 'split long lines, but do not refill') tagged_par := fp.bool('tagged-paragraph', `t`, false, 'indentation of first line different from second') diff --git a/src/fmt/fmt_test.v b/src/fmt/fmt_test.v index dfe90e51..13ae6c05 100644 --- a/src/fmt/fmt_test.v +++ b/src/fmt/fmt_test.v @@ -24,13 +24,15 @@ fn to_tmp_file(data []string) string { } fn setup() (fn (s string), fn () []string) { - mut result := []string{} - mut result_ref := &result - out_fn := fn [mut result_ref] (s string) { - result_ref << s + // The array header has to be heap allocated: a pointer to a local would + // dangle as soon as setup() returned, and the closures would then append to + // freed stack memory. + mut result := &[]string{} + out_fn := fn [mut result] (s string) { + (*result) << s } - result_fn := fn [mut result_ref] () []string { - return *result_ref + result_fn := fn [result] () []string { + return *result } return out_fn, result_fn } diff --git a/src/head/head.v b/src/head/head.v index 5c02bca4..73fe15ee 100644 --- a/src/head/head.v +++ b/src/head/head.v @@ -294,7 +294,7 @@ fn wrap_long_command_description(description string, max_cols int) string { return buf.str() } -fn setup_command(args []string) ?(HeadCommand, []InputFile) { +fn setup_command(args []string) (HeadCommand, []InputFile) { mut fp := common.flag_parser(args) fp.application(name) fp.usage_example('[OPTION]... [FILE]...') @@ -330,7 +330,7 @@ fn setup_command(args []string) ?(HeadCommand, []InputFile) { } fn run_head(args []string) { - head, mut files := setup_command(args) or { common.exit_with_error_message(name, err.msg()) } + head, mut files := setup_command(args) head.run(mut files) } diff --git a/src/mv/helper.v b/src/mv/helper.v index 4bccee39..61513532 100644 --- a/src/mv/helper.v +++ b/src/mv/helper.v @@ -48,9 +48,7 @@ fn int_yes(prompt string) bool { } pub fn run_mv(args []string) { - mv, sources, dest := setup_mv_command(args) or { - common.exit_with_error_message(name, err.msg()) - } + mv, sources, dest := setup_mv_command(args) if sources.len > 1 && !os.is_dir(dest) { common.exit_with_error_message(name, target_not_dir(dest)) } @@ -59,7 +57,7 @@ pub fn run_mv(args []string) { } } -fn setup_mv_command(args []string) ?(MvCommand, []string, string) { +fn setup_mv_command(args []string) (MvCommand, []string, string) { mut fp := common.flag_parser(args) fp.application('mv') fp.limit_free_args_to_at_least(1) or { common.exit_with_error_message(name, err.msg()) } diff --git a/src/nproc/nproc.v b/src/nproc/nproc.v index 533c78b8..4638dd74 100644 --- a/src/nproc/nproc.v +++ b/src/nproc/nproc.v @@ -25,7 +25,7 @@ fn main() { // Check if ignored CPUs is > 0 if ignored_cpus < 0 { - println('nproc: invalid number: \'${ignored_cpus}\'') + println("nproc: invalid number: '${ignored_cpus}'") exit(1) } diff --git a/src/numfmt/numfmt.v b/src/numfmt/numfmt.v index 2a106662..f1a6efa5 100644 --- a/src/numfmt/numfmt.v +++ b/src/numfmt/numfmt.v @@ -63,7 +63,7 @@ fn numfmt(number string, mut app App, options Options) !string { mut result := match true { options.pformat.len != 0 { unsafe { strconv.v_sprintf(options.pformat, n) } } options.grouping { commaize(n) } - options.to == 'none' { n.str() } + options.to == 'none' { number_str(n) } else { num_to_str(n, options) or { handle_error(err.msg(), mut app, options) } } } @@ -170,7 +170,19 @@ fn readable_size(size f64, unit Unit, rounding string) !string { } sz /= kb } - return size.str() + return number_str(size) +} + +// number_str renders n the way GNU numfmt does: plain decimal digits, never +// exponent notation, and no trailing ".0" on an integral value. f64.str() +// switches to scientific notation above a million and always keeps the +// fractional part, which would turn 2000000 into "2e+06" and 2000 into "2000.0". +fn number_str(n f64) string { + s := n.strlong() + if s.ends_with('.0') { + return s[..s.len - 2] + } + return s } fn scale_number(num f64, pow int, options Options) i64 { diff --git a/src/numfmt/options.v b/src/numfmt/options.v index 4ff0d7bd..c6268fd6 100644 --- a/src/numfmt/options.v +++ b/src/numfmt/options.v @@ -45,21 +45,21 @@ fn get_options() Options { 'use locale-defined grouping of digits, e.g. 1,000,000') header := fp.int('header', 0, 1, 'print (without converting) the first N header lines; \n${flag.space}' + - 'defaults to 1 if not specified') + 'defaults to 1 if not specified') invalid := fp.string('invalid', 0, 'abort', 'failure mode for invalid numbers: MODE can be:\n${flag.space}' + - 'abort (default), fail, warn, ignore') + 'abort (default), fail, warn, ignore') padding := fp.int('padding', 0, 0, 'pad the output to N characters; positive N will\n${flag.space}' + - 'right-align; negative N will left-align; padding is\n${flag.space}' + - 'ignored if the output is wider than N; the default is to\n${flag.space}' + - 'automatically pad if a whitespace is found') + 'right-align; negative N will left-align; padding is\n${flag.space}' + + 'ignored if the output is wider than N; the default is to\n${flag.space}' + + 'automatically pad if a whitespace is found') round := fp.string('round', 0, 'from-zero', 'use METHOD for rounding when scaling; METHOD can be:\n${flag.space}' + - 'up, down, from-zero (default), towards-zero, nearest') + 'up, down, from-zero (default), towards-zero, nearest') suffix := fp.string('suffix', 0, '', 'add SUFFIX to output numbers, and accept optional SUFFIX\n${flag.space}' + - 'in input numbers') + 'in input numbers') to := fp.string('to', 0, 'none', 'auto-scale output numbers to UNIT\n${flag.space}' + 'none, si, iec, iec-i') to_unit := fp.int('to-unit', 0, 1, 'the output unit size (instead of the default 1)') diff --git a/src/printenv/printenv.v b/src/printenv/printenv.v index 05f180ce..44367c52 100644 --- a/src/printenv/printenv.v +++ b/src/printenv/printenv.v @@ -1,7 +1,9 @@ import os import common -const zero_byte = byte(0).ascii_str() +fn zero_byte() string { + return u8(0).ascii_str() +} // Exit status: // 0 if all variables specified were found @@ -34,7 +36,7 @@ fn main() { mut s := '${k}=${v}' if opt_nul_terminate { print(s) - print(zero_byte) + print(zero_byte()) } else { println(s) } @@ -49,7 +51,7 @@ fn main() { } if opt_nul_terminate { print(v) - print(zero_byte) + print(zero_byte()) } else { println(v) } diff --git a/src/printf/printf.v b/src/printf/printf.v index 58c486fc..c76ffab7 100644 --- a/src/printf/printf.v +++ b/src/printf/printf.v @@ -242,7 +242,10 @@ fn apply_posix_escape(s string) string { } } } - return if has_unprintable { '\$\'${upout}\'' } else { s.replace_each([ + return if has_unprintable { + "\$'${upout}'" + } else { + s.replace_each([ '|', '\\|', '&', @@ -269,7 +272,8 @@ fn apply_posix_escape(s string) string { '\\"', ' ', '\\ ', - ]) } + ]) + } } // A code below is modded version of vlib/strconv/vprintf.v whose parameter to be an array of string diff --git a/src/shred/config.v b/src/shred/config.v index b1a8ef4c..c9776eb0 100644 --- a/src/shred/config.v +++ b/src/shred/config.v @@ -6,7 +6,7 @@ import os @[version: '0.1'] struct Config { force bool @[short: f; xdoc: 'change permissions to allow writing if necessary'] - iterations int = 3 @[short: n; xdoc: 'overwrite N times instead of the default (3)'] + iterations int = 3 @[short: n; xdoc: 'overwrite N times instead of the default (3)'] random_source string @[xdoc: 'get random bytes from '] size string @[short: s; xdoc: 'shred this many bytes (suffixes like K, M, G accepted)\n'] rm bool @[only: u; xdoc: 'deallocate and remove file after overwriting'] @@ -26,8 +26,7 @@ fn get_args() (Config, []string) { description: 'Usage: shred [OPTION]... FILE...\n' + 'Overwrite the specified FILE(s) repeatedly, in order to make it harder\n' + 'for even very expensive hardware probing to recover the data.' - footer: - '\nDelete FILE(s) if --remove (-u) is specified. The default is not to remove\n' + 'the files because it is common to operate on device files like /dev/hda,\n' + 'and those files usually should not be removed.\n\n' + 'The --remove parameter indicates how to remove a directory entry:\n' + " 'unlink' => use a standard unlink call.\n" + " 'wipe' => also first obfuscate bytes in the name.\n" + " 'wipesync' => also sync each obfuscated byte to the device.\n" + "The default mode is 'wipesync', but note it can be expensive.\n\n" + 'CAUTION: shred assumes the file system and hardware overwrite data in place.\n' + 'Although this is common, many platforms operate otherwise. Also, backups\n' + 'and mirrors may contain unremovable copies that will let a shredded file\n' + 'be recovered later.\n' + common.coreutils_footer() + footer: '\nDelete FILE(s) if --remove (-u) is specified. The default is not to remove\n' + 'the files because it is common to operate on device files like /dev/hda,\n' + 'and those files usually should not be removed.\n\n' + 'The --remove parameter indicates how to remove a directory entry:\n' + " 'unlink' => use a standard unlink call.\n" + " 'wipe' => also first obfuscate bytes in the name.\n" + " 'wipesync' => also sync each obfuscated byte to the device.\n" + "The default mode is 'wipesync', but note it can be expensive.\n\n" + 'CAUTION: shred assumes the file system and hardware overwrite data in place.\n' + 'Although this is common, many platforms operate otherwise. Also, backups\n' + 'and mirrors may contain unremovable copies that will let a shredded file\n' + 'be recovered later.\n' + common.coreutils_footer() ) or { panic(err) } println(doc) exit(0) diff --git a/src/shuf/shuf.v b/src/shuf/shuf.v index 51ec4137..5fce1649 100644 --- a/src/shuf/shuf.v +++ b/src/shuf/shuf.v @@ -88,7 +88,7 @@ fn shuffle_lines(lines []string, settings Settings) []string { exit(1) } mut bytes := io.read_all(io.ReadAllConfig{ reader: file, read_to_end_of_stream: true }) or { - eprintln('${app_name}: ${settings.random_source}: Can\'t read file') + eprintln("${app_name}: ${settings.random_source}: Can't read file") exit(1) } mut seed := u32(0) @@ -159,7 +159,7 @@ fn register_lines_by_file(lines []string, fname string, zero_terminated bool) [] if zero_terminated { mut bytes := io.read_all(io.ReadAllConfig{ reader: file, read_to_end_of_stream: true }) or { - eprintln('${app_name}: ${fname}: Can\'t read file') + eprintln("${app_name}: ${fname}: Can't read file") exit(1) } diff --git a/src/sleep/sleep.v b/src/sleep/sleep.v index c65d9cb0..bd6ef7a9 100644 --- a/src/sleep/sleep.v +++ b/src/sleep/sleep.v @@ -6,6 +6,11 @@ import common const cmd_ns = 'sleep' +// max_duration is the largest sleep time.Duration we are willing to hand to +// time.sleep(), and max_sleep_seconds is that same value expressed in seconds. +const max_duration = time.Duration(max_i64) +const max_sleep_seconds = 9223372036.854776 + // // str="-1.8e+308", ret = -inf, endptr => NULL // str="1.8e+308", ret = +inf, endptr => NULL @@ -51,6 +56,22 @@ fn invalid_time_interval_argument(s string) string { return "${cmd_ns}: invalid time interval '${s}'" } +// seconds_to_duration converts a possibly infinite number of seconds +// into a time.Duration. A Duration is an integer nanosecond count, so +// saturate instead of overflowing: an unbounded sleep must keep waiting +// rather than wrap around to (near) zero. +fn seconds_to_duration(seconds f64) time.Duration { + nanoseconds := seconds * 1e9 + // `!(x < y)` also catches NaN and +inf, both of which must saturate + if !(nanoseconds < max_sleep_seconds) { + return max_duration + } + if nanoseconds <= 0.0 { + return 0 + } + return time.Duration(nanoseconds) +} + fn main() { mut fp := common.flag_parser(os.args) fp.application(cmd_ns) @@ -103,7 +124,7 @@ fn main() { // if seconds = +inf, it would not sleep // but original `sleep` would sleep t := time.ticks() - time.sleep(seconds * time.second) + time.sleep(seconds_to_duration(seconds)) $if trace_sleep_ticks ? { println(time.ticks() - t) } diff --git a/src/sort/options.v b/src/sort/options.v index 8819b2ec..52317bf9 100644 --- a/src/sort/options.v +++ b/src/sort/options.v @@ -40,11 +40,11 @@ fn get_options() Options { 'consider only printable characters') numeric := fp.bool('numeric-sort', `n`, false, 'Restrict the sort key to an initial numeric\n${flag.space}' + - 'string, consisting of optional characters,\n${flag.space}' + - 'optional character, and zero or\n${flag.space}' + - 'more digits, which shall be sorted by arithmetic\n${flag.space}' + - 'value. An empty digit string shall be treated as\n${flag.space}' + - 'zero. Leading zeros shall not affect ordering.') + 'string, consisting of optional characters,\n${flag.space}' + + 'optional character, and zero or\n${flag.space}' + + 'more digits, which shall be sorted by arithmetic\n${flag.space}' + + 'value. An empty digit string shall be treated as\n${flag.space}' + + 'zero. Leading zeros shall not affect ordering.') reverse := fp.bool('reverse', `r`, false, 'reverse the result of comparisons\n\nOther options:') check_diagnose := fp.bool('', `c`, false, 'check for sorted input; do not sort') @@ -78,14 +78,14 @@ fn get_options() Options { numeric: numeric reverse: reverse // other options - check_diagnose: check_diagnose - check_quiet: check_quiet - sort_keys: sort_keys - field_separator: field_separator - merge: merge - output_file: output_file - unique: unique - files: scan_files_arg(files) + check_diagnose: check_diagnose + check_quiet: check_quiet + sort_keys: sort_keys + field_separator: field_separator + merge: merge + output_file: output_file + unique: unique + files: scan_files_arg(files) } } diff --git a/src/stat/stat_to_vlib.v b/src/stat/modes.v similarity index 51% rename from src/stat/stat_to_vlib.v rename to src/stat/modes.v index 42454c1b..d1f98f99 100644 --- a/src/stat/stat_to_vlib.v +++ b/src/stat/modes.v @@ -1,6 +1,14 @@ import os -import v.scanner -import v.pref +import strings + +// This file used to be called `stat_to_vlib.v`: it was a staging copy of the +// file mode helpers on their way into V's os module. The types have since +// landed in vlib/os, so it is no longer a copy of anything. +// +// It also used to import v.scanner/v.pref and decode --printf escapes with the +// V language scanner. Pulling the compiler into a coreutils binary made `v +// build` of this directory take minutes (it effectively hung), so the escape +// handling below is a plain hand-written decoder instead. pub enum FileType { unknown @@ -33,13 +41,6 @@ pub: special bool // setuid for owner, setgid for group, sticky for others } -pub enum FilePermissionGroup { - unknown - owner - group - world -} - @[inline] fn if_str(cond bool, str_if_true string, str_if_false string) string { return if cond { str_if_true } else { str_if_false } @@ -107,13 +108,93 @@ fn filetype_to_string(typ FileType) string { } } -// raw_to_printf_string converts a raw string into a printf-able string -// example: r"\t[\x76]" => "\t[\x76]" = " [v]" +// raw_to_printf_string decodes the backslash escapes accepted by GNU stat's +// --printf. Example: r"\t[\x76]" becomes " [v]". +// +// Recognised: \a \b \e \f \n \r \t \v \\ \" plus \NNN (1-3 octal digits) and +// \xHH (1-2 hex digits). Anything else is passed through as the escaped +// character, with a warning, which is what GNU does. fn raw_to_printf_string(raw_string string) string { - mut sc := scanner.new_scanner("'${raw_string}'", .skip_comments, &pref.Preferences{}) - return sc.scan().lit.replace(r'\a', '\a').replace(r'\b', '\b').replace(r'\e', '\e').replace(r'\f', - '\f').replace(r'\n', '\n').replace(r'\r', '\r').replace(r'\t', '\t').replace(r'\v', '\v').replace(r'\\', - '\\').replace(r"\'", "'").replace(r'\"', '"').replace(r'\?', '\?') + mut sb := strings.new_builder(raw_string.len) + mut i := 0 + for i < raw_string.len { + c := raw_string[i] + if c != `\\` { + sb.write_byte(c) + i++ + continue + } + i++ + if i == raw_string.len { + eprintln('stat: warning: backslash at end of format') + sb.write_byte(`\\`) + break + } + e := raw_string[i] + i++ + match e { + `a` { sb.write_byte(7) } + `b` { sb.write_byte(8) } + `e` { sb.write_byte(27) } + `f` { sb.write_byte(12) } + `n` { sb.write_byte(10) } + `r` { sb.write_byte(13) } + `t` { sb.write_byte(9) } + `v` { sb.write_byte(11) } + `\\` { sb.write_byte(`\\`) } + `"` { sb.write_byte(`"`) } + `0`...`7` { + // \NNN: up to three octal digits + mut value := int(e - `0`) + mut digits := 1 + for digits < 3 && i < raw_string.len && is_octal_digit(raw_string[i]) { + value = value * 8 + int(raw_string[i] - `0`) + i++ + digits++ + } + sb.write_byte(u8(value)) + } + `x` { + // \xHH: up to two hex digits + mut value := 0 + mut digits := 0 + for digits < 2 && i < raw_string.len { + d := hex_digit_value(raw_string[i]) or { break } + value = value * 16 + d + i++ + digits++ + } + if digits == 0 { + eprintln("stat: warning: unrecognized escape '\\x'") + sb.write_byte(`x`) + } else { + sb.write_byte(u8(value)) + } + } + else { + eprintln("stat: warning: unrecognized escape '\\${e}'") + sb.write_byte(e) + } + } + } + return sb.str() +} + +fn is_octal_digit(c u8) bool { + return c >= `0` && c <= `7` +} + +fn hex_digit_value(c u8) !int { + if c >= `0` && c <= `9` { + return int(c - `0`) + } + if c >= `a` && c <= `f` { + return int(c - `a`) + 10 + } + if c >= `A` && c <= `F` { + return int(c - `A`) + 10 + } + return error('not a hex digit') } fn filemode_to_string(mode u16) string { diff --git a/src/stat/stat.c.v b/src/stat/stat.c.v index 0192010b..dfee2a94 100644 --- a/src/stat/stat.c.v +++ b/src/stat/stat.c.v @@ -4,11 +4,55 @@ import os #include #include #include -#include +// The kernel uapi definitions of statx()/STATX_* live here. Do not use +// for that: it is a glibc internal header, so it is missing on +// musl based distributions (see issue #145). +#include + +// vlib/builtin/cfns.c.v already declares a private `fn C.statvfs`, so a +// `fn C.statvfs` here resolves to that one and fails to compile. Give the libc +// call a distinct V-level name instead. +#define vcu_statvfs statvfs + +// Mirror the kernel's `struct statx` exactly. Passing a V struct through a +// voidptr and hoping for the best truncates the kernel's 224 byte write to the +// size of the V view, which corrupts memory. +struct C.statx_timestamp { + tv_sec i64 + tv_nsec u32 + reserved i32 +} // Ref: https://www.man7.org/linux/man-pages/man2/statx.2.html -fn C.statx(int, &char, int, u32, voidptr) int -fn C.statvfs(&char, voidptr) int +struct C.statx { + stx_mask u32 + stx_blksize u32 + stx_attributes u64 + stx_nlink u32 + stx_uid u32 + stx_gid u32 + stx_mode u16 + __spare0 [1]u16 + stx_ino u64 + stx_size u64 + stx_blocks u64 + stx_attributes_mask u64 + stx_atime C.statx_timestamp + stx_btime C.statx_timestamp + stx_ctime C.statx_timestamp + stx_mtime C.statx_timestamp + stx_rdev_major u32 + stx_rdev_minor u32 + stx_dev_major u32 + stx_dev_minor u32 + stx_mnt_id u64 + stx_dio_mem_align u32 + stx_dio_offset_align u32 + __spare3 [12]u32 +} + +fn C.statx(int, &char, int, u32, &C.statx) int +fn C.vcu_statvfs(&char, voidptr) int fn C.readlink(pathname &char, buf &char, bufsiz usize) int const c_at_statx_sync_as_stat = 0x0000 // C.AT_STATX_SYNC_AS_STAT from fcntl.h @@ -18,8 +62,7 @@ const c_at_symlink_nofollow = 0x0100 // C.AT_SYMLINK_NOFOLLOW from fcntl.h const c_chmod_bits = C.S_ISUID | C.S_ISGID | C.S_ISVTX | C.S_IRWXU | C.S_IRWXG | C.S_IRWXO fn statx(path string, dereference bool, cache_mode CacheMode) !Statx { - mut s := Statx{} - ptr := voidptr(&s) + mut c := C.statx{} unsafe { symlink_flag := if dereference { 0 } else { c_at_symlink_nofollow } sync_flag := match cache_mode { @@ -29,19 +72,50 @@ fn statx(path string, dereference bool, cache_mode CacheMode) !Statx { } res := C.statx(0, os.abs_path(path).str, sync_flag | symlink_flag, - C.STATX_BASIC_STATS | C.STATX_BTIME, ptr) + C.STATX_BASIC_STATS | C.STATX_BTIME, &c) if res != 0 { return os.error_posix() } } - return s + return Statx{ + stx_mask: c.stx_mask + stx_blksize: c.stx_blksize + stx_attributes: c.stx_attributes + stx_nlink: c.stx_nlink + stx_uid: c.stx_uid + stx_gid: c.stx_gid + stx_mode: c.stx_mode + stx_ino: c.stx_ino + stx_size: c.stx_size + stx_blocks: c.stx_blocks + stx_attributes_mask: c.stx_attributes_mask + stx_atime: to_timestamp(c.stx_atime) + stx_btime: to_timestamp(c.stx_btime) + stx_ctime: to_timestamp(c.stx_ctime) + stx_mtime: to_timestamp(c.stx_mtime) + stx_rdev_major: c.stx_rdev_major + stx_rdev_minor: c.stx_rdev_minor + stx_dev_major: c.stx_dev_major + stx_dev_minor: c.stx_dev_minor + stx_mnt_id: c.stx_mnt_id + stx_dio_mem_align: c.stx_dio_mem_align + stx_dio_offset_align: c.stx_dio_offset_align + } +} + +fn to_timestamp(ts C.statx_timestamp) StatxTimestamp { + return StatxTimestamp{ + tv_sec: ts.tv_sec + tv_nsec: ts.tv_nsec + } } +// statvfs() passes the V Statvfs struct through a voidptr, so that struct has +// to match the kernel's `struct statvfs` exactly, trailing spare included. fn statvfs(path string) !Statvfs { mut s := Statvfs{} - ptr := voidptr(&s) unsafe { - res := C.statvfs(os.abs_path(path).str, ptr) + res := C.vcu_statvfs(os.abs_path(path).str, voidptr(&s)) if res != 0 { return os.error_posix() } diff --git a/src/stat/stat.v b/src/stat/stat.v index abe71a12..320069af 100644 --- a/src/stat/stat.v +++ b/src/stat/stat.v @@ -13,6 +13,11 @@ const app = common.CoreutilInfo{ help: $embed_file('help.txt').to_string() } +// fstypes_data maps the magic numbers the kernel reports for a filesystem to +// its type name. Embedded at module scope: $embed_file inside a function body +// does not resolve on all V versions. +const fstypes_data = $embed_file('fstypes.txt') + // Settings for Utility: stat struct Settings { mut: @@ -66,18 +71,22 @@ struct Statx { stx_dio_offset_align u32 } +// Field types and order mirror the kernel's `struct statvfs`, including the +// trailing spare, because statvfs() hands this struct to libc through a +// voidptr. Every f_* member is `unsigned long`/`fsblkcnt_t`, hence usize. struct Statvfs { - f_bsize u64 // Filesystem block size - f_frsize u64 // Fragment size - f_blocks u64 // Size of fs in f_frsize units - f_bfree u64 // Number of free blocks - f_bavail u64 // Number of free blocks for unprivileged users - f_files u64 // Number of inodes - f_ffree u64 // Number of free inodes - f_favail u64 // Number of free inodes for unprivileged users - f_fsid u64 // Filesystem ID - f_flag u64 // Mount flags - f_namemax u64 // Maximum filename length + f_bsize usize // Filesystem block size + f_frsize usize // Fragment size + f_blocks usize // Size of fs in f_frsize units + f_bfree usize // Number of free blocks + f_bavail usize // Number of free blocks for unprivileged users + f_files usize // Number of inodes + f_ffree usize // Number of free inodes + f_favail usize // Number of free inodes for unprivileged users + f_fsid usize // Filesystem ID + f_flag usize // Mount flags + f_namemax usize // Maximum filename length + f_spare [6]i32 // __f_spare in the C struct; V rejects leading underscores } enum CacheMode { @@ -91,6 +100,10 @@ enum CacheMode { const tokens_statx = 'aAbBCdDfFgGhimnNosrRtTuUwWxXyYzZV'.bytes() const tokens_statvfs = 'abcdfilnsStT'.bytes() +// The two character format tokens, which need to be matched before the single +// character ones. +const two_char_tokens = ['Hd', 'Ld'] + @[inline] fn make_dev(major u32, minor u32) u32 { return major << 8 | minor @@ -161,6 +174,14 @@ fn process_token(token string, st Statx, path string, mtab []MountInfo) string { 'D' { '${make_dev(st.stx_dev_major, st.stx_dev_minor):x}' } + 'Hd' { + // major device number, decimal; GNU's default Device: field + '${st.stx_dev_major}' + } + 'Ld' { + // minor device number, decimal; GNU's default Device: field + '${st.stx_dev_minor}' + } 'f' { '${st.stx_mode:4x}' } @@ -330,11 +351,13 @@ fn scan_num(s &string) int { return (*s).len } -// TODO: When we upgrade to coreutil 9.4 compatibility, we will need two-byte tokens -// if (*s).len > 1 && (*s)[..2] in ['Hd', 'Hr', 'Ld', 'Lr'] { -// return 2 -// } +// Some of GNU's format tokens are two characters wide, e.g. %Hd and %Ld for +// the major and minor device numbers. They have to be recognised before the +// single character tokens, otherwise %Hd would be read as %H followed by "d". fn scan_for_tokens(s &string, tokens []u8) int { + if (*s).len > 1 && (*s)[..2] in two_char_tokens { + return 2 + } if (*s).len > 0 { if (*s)[0] in tokens { return 1 @@ -498,7 +521,7 @@ fn args() Settings { if st.file_system { st.format = ' File: "%n"\n ID: %-8i Namelen: %-7l Type: %T\nBlock size: %-10s Fundamental block size: %S\nBlocks: Total: %-10b Free: %-10f Available: %a\nInodes: Total: %-10c Free: %d\n' } else { - st.format = ' File: %V\n Size: %-10s\tBlocks: %-10b IO Block: %-6o %F\nDevice: %Dh/%dd\tInode: %-10i Links: %h\nAccess: (%04a/%10A) Uid: (%5u/%8U) Gid: (%5g/%8G)\nAccess: %x\nModify: %y\nChange: %z\n Birth: %w\n' + st.format = ' File: %V\n Size: %-10s\tBlocks: %-10b IO Block: %-6o %F\nDevice: %Hd,%Ld\tInode: %-10i Links: %h\nAccess: (%04a/%10A) Uid: (%5u/%8U) Gid: (%5g/%8G)\nAccess: %x\nModify: %y\nChange: %z\n Birth: %w\n' } } } else { @@ -551,8 +574,7 @@ fn get_mount_list() []MountInfo { } fn get_fs_list() map[string]u32 { - embedded_file := $embed_file('fstypes.txt') - s := embedded_file.to_string() + s := fstypes_data.to_string() assert s[s.len - 1] == `\n`, 'fstypes.txt must be newline-terminated.' mut fslist := map[string]u32{} for i := 0; i < s.len; { diff --git a/src/sum/sum_test.v b/src/sum/sum_test.v index 21168625..ba5fe000 100644 --- a/src/sum/sum_test.v +++ b/src/sum/sum_test.v @@ -1,5 +1,7 @@ import os -import io.util +// Aliased: the module is called `util`, which would clash with the `util` +// constant below holding this utility's name. +import io.util as io_util import common.testing const eol = testing.output_eol() @@ -97,7 +99,7 @@ fn test_sysv_several_files_succeeds() { } fn sum_arbitrary_value(value string, arg string) !os.Result { - mut f, path := util.temp_file()! + mut f, path := io_util.temp_file()! f.write_string('${value}\n')! f.close() res := $if windows { diff --git a/src/tail/parse_args.v b/src/tail/parse_args.v index 31f6e8ca..858f67ff 100644 --- a/src/tail/parse_args.v +++ b/src/tail/parse_args.v @@ -34,14 +34,14 @@ fn parse_args(args []string) Args { bytes_arg := fp.string('bytes', `c`, '-1', 'output the last NUM bytes; or use -c + to output ${wrap}' + - 'starting with byte of each file') + 'starting with byte of each file') follow_arg := fp.bool('follow', `f`, false, 'output appended data as the file grows') f_arg := fp.bool('', `F`, false, 'same as --follow=name --retry') lines_arg := fp.string('lines', `n`, '10', 'output the last NUM lines, instead of the last 10; or us${wrap}' + - '-n +NUM to skip NUM-1 lines at the start') + '-n +NUM to skip NUM-1 lines at the start') pid_arg := fp.string('pid', ` `, '', 'with -f, terminate after process ID, PID dies') quiet_arg := fp.bool('quiet', `q`, false, 'never output headers giving file names') @@ -72,9 +72,9 @@ fn parse_args(args []string) Args { files := scan_files_arg(files_arg) return Args{ - bytes: string_to_i64(bytes_arg) or { exit_error(err.msg()) } + bytes: string_to_i64(bytes_arg) or { exit_error(invalid_number('bytes', bytes_arg)) } follow: follow_arg || f_arg - lines: string_to_i64(lines_arg) or { exit_error(err.msg()) } + lines: string_to_i64(lines_arg) or { exit_error(invalid_number('lines', lines_arg)) } pid: pid_arg quiet: quiet_arg || silent_arg retry: f_arg || retry_arg @@ -86,6 +86,12 @@ fn parse_args(args []string) Args { } } +// invalid_number matches the diagnostic GNU tail prints for a +// -c/-n operand it cannot parse. +fn invalid_number(kind string, value string) string { + return 'invalid number of ${kind}: ‘${value}’' +} + fn scan_files_arg(files_arg []string) []string { mut files := []string{} diff --git a/src/tail/string_to_i64.v b/src/tail/string_to_i64.v index b44ce979..d90e109b 100644 --- a/src/tail/string_to_i64.v +++ b/src/tail/string_to_i64.v @@ -1,32 +1,35 @@ // convert strings like 10K to i164 const block = i64(512) +// binary (IEC) multipliers, i.e. GNU's suffix without the extra i // **1 const kilo = i64(1024) -const kilobyte = i64(1000) // **2 const mega = kilo * kilo -const megabyte = kilobyte * kilobyte // **3 const giga = mega * kilo -const gigabyte = megabyte * kilobyte // **4 const terra = giga * kilo -const terrabyte = gigabyte * kilobyte // **5 const peta = terra * kilo -const petabyte = terra * kilobyte // **6 const exa = peta * kilo -const exabyte = peta * kilo // **7 const zetta = exa * kilo + +// decimal (SI) multipliers +const kilobyte = i64(1000) +const megabyte = kilobyte * kilobyte +const gigabyte = megabyte * kilobyte +const terrabyte = gigabyte * kilobyte +const petabyte = terrabyte * kilobyte +const exabyte = petabyte * kilobyte const zettabyte = exabyte * kilobyte fn string_to_i64(s string) ?i64 { @@ -52,17 +55,23 @@ fn string_to_i64(s string) ?i64 { multiplier := match suffix.to_lower() { 'b' { block } 'k' { kilo } - 'kb', 'kib' { kilobyte } + 'kb' { kilobyte } + 'kib' { kilo } 'm' { mega } - 'mb', 'mib' { megabyte } + 'mb' { megabyte } + 'mib' { mega } 'g' { giga } - 'gb', 'gib' { gigabyte } + 'gb' { gigabyte } + 'gib' { giga } 't' { terra } - 'tb', 'tib' { terrabyte } + 'tb' { terrabyte } + 'tib' { terra } 'p' { peta } - 'pb', 'pib' { petabyte } + 'pb' { petabyte } + 'pib' { peta } 'e' { exa } - 'eb', 'eib' { exabyte } + 'eb' { exabyte } + 'eib' { exa } // oddball formats found in __xstrtol source 'c' { 1 } 'w' { 2 } @@ -70,7 +79,8 @@ fn string_to_i64(s string) ?i64 { } result := number * multiplier - if result == 0 && number != 0 { + if number != 0 && result / number != multiplier { + // the multiplication wrapped around return none } return result diff --git a/src/tail/string_to_i64_test.v b/src/tail/string_to_i64_test.v index 5d1eeaae..6c56f063 100644 --- a/src/tail/string_to_i64_test.v +++ b/src/tail/string_to_i64_test.v @@ -1,38 +1,48 @@ module main -fn test_string_to_i64_conversions() { - assert string_to_i64('1')! == 1 - assert string_to_i64('+1')! == 1 - assert string_to_i64('-1')! == -1 - - assert string_to_i64('2b')! == 2 * block - - assert string_to_i64('11k')! == 11 * kilo - assert string_to_i64('12K')! == 12 * kilo - assert string_to_i64('13KB')! == 13 * kilobyte - assert string_to_i64('14KiB')! == 14 * kilobyte - - assert string_to_i64('15M')! == 15 * mega - assert string_to_i64('16MB')! == 16 * megabyte - assert string_to_i64('17mib')! == 17 * megabyte - - assert string_to_i64('18T')! == 18 * terra - assert string_to_i64('18TB')! == 18 * terrabyte - assert string_to_i64('18TiB')! == 18 * terrabyte - - assert string_to_i64('10P')! == 10 * peta - assert string_to_i64('10PB')! == 10 * petabyte - assert string_to_i64('10PiB')! == 10 * petabyte - - assert string_to_i64('5E')! == 5 * exa - assert string_to_i64('5EB')! == 5 * exabyte - assert string_to_i64('5EiB')! == 5 * exabyte - - overflow_r := string_to_i64('5R') or { -1 } - assert overflow_r == -1 +// string_to_i64 returns an option, so it has to be unwrapped explicitly; `!` +// does not work on an option. +fn to_i64(s string) i64 { + return string_to_i64(s) or { panic('string_to_i64(' + s + ') returned none') } +} - overflow_q := string_to_i64('5Q') or { -1 } - assert overflow_q == -1 +fn test_string_to_i64_conversions() { + assert to_i64('1') == 1 + assert to_i64('+1') == 1 + assert to_i64('-1') == -1 + + assert to_i64('2b') == 2 * block + + assert to_i64('11k') == 11 * kilo + assert to_i64('12K') == 12 * kilo + assert to_i64('13KB') == 13 * kilobyte + assert to_i64('14KiB') == 14 * kilo + + assert to_i64('15M') == 15 * mega + assert to_i64('16MB') == 16 * megabyte + assert to_i64('17mib') == 17 * mega + + assert to_i64('18T') == 18 * terra + assert to_i64('18TB') == 18 * terrabyte + assert to_i64('18TiB') == 18 * terra + + assert to_i64('10P') == 10 * peta + assert to_i64('10PB') == 10 * petabyte + assert to_i64('10PiB') == 10 * peta + + assert to_i64('5E') == 5 * exa + assert to_i64('5EB') == 5 * exabyte + assert to_i64('5EiB') == 5 * exa + + // GNU tail's help mentions Z, Y, R and Q, but it rejects all of them + // with "Value too large for defined data type" + for big in ['1Z', '1ZB', '1ZiB', '1Y', '1YB', '1R', '1RB', '1Q'] { + assert string_to_i64(big) == none + } + + // an out of range multiple must be rejected rather than wrap around + overflow_e := string_to_i64('20E') or { -1 } + assert overflow_e == -1 // invalid checks t0 := string_to_i64('') or { -1 } diff --git a/src/tail/tail.v b/src/tail/tail.v index 78d6dbde..7f01d1fb 100644 --- a/src/tail/tail.v +++ b/src/tail/tail.v @@ -1,7 +1,7 @@ // tail - output the last part of files import os import time -import v.mathutil +import math const app_name = 'tail' @@ -82,7 +82,7 @@ fn tail_bytes(file FileInfo, args Args, stat_size u64, out_fn fn (string)) { pos := if args.from_start { args.bytes } else { - mathutil.max(u64(0), stat_size - u64(args.bytes)) + math.max(u64(0), stat_size - u64(args.bytes)) } mut f := os.open(file.name) or { if args.retry { @@ -112,7 +112,7 @@ fn tail_file(file FileInfo, args Args, stat_size u64, out_fn fn (string)) { mut pos := i64(0) loop1: for pos <= end { - len := mathutil.min(end - pos, buf_size) + len := math.min(end - pos, buf_size) f.read_bytes_into(u64(pos), mut buf) or { exit_error(err.msg()) } for i := 0; i < len; i += 1 { @@ -133,8 +133,8 @@ fn tail_file(file FileInfo, args Args, stat_size u64, out_fn fn (string)) { mut pos := end loop2: for pos > 0 { - len := mathutil.min(pos, buf_size) - pos = mathutil.max(pos - buf_size, 0) + len := math.min(pos, buf_size) + pos = math.max(pos - buf_size, 0) f.read_bytes_into(u64(pos), mut buf) or { exit_error(err.msg()) } for i := len - 1; i >= 0; i -= 1 { diff --git a/src/tail/tail_test.v b/src/tail/tail_test.v index 64082246..4b39cf02 100644 --- a/src/tail/tail_test.v +++ b/src/tail/tail_test.v @@ -5,13 +5,15 @@ import os fn setup() (fn (s string), fn () string) { os.chdir(os.dir(@FILE)) or { exit_error(err.msg()) } - mut result := []string{} - mut result_ref := &result - out_fn := fn [mut result_ref] (s string) { - result_ref << s + // The array header has to be heap allocated: a pointer to a local would + // dangle as soon as setup() returned, and the closures would then append to + // freed stack memory. + mut result := &[]string{} + out_fn := fn [mut result] (s string) { + (*result) << s } - result_fn := fn [mut result_ref] () string { - return result_ref.join('') + result_fn := fn [result] () string { + return result.join('') } return out_fn, result_fn } diff --git a/src/truncate/truncate.v b/src/truncate/truncate.v index 1402f829..a0b918a5 100644 --- a/src/truncate/truncate.v +++ b/src/truncate/truncate.v @@ -202,8 +202,13 @@ fn truncate(settings Settings) { f.close() } if os.exists(fname) { - block_size := if settings.io_blocks { get_block_size(fname) or { - default_block_size} } else { 1 } + block_size := if settings.io_blocks { + get_block_size(fname) or { + default_block_size + } + } else { + 1 + } size := calc_target_size(get_size(settings.reference), settings.size_opt, block_size) os.truncate(fname, size) or { app.quit(message: err.msg()) } @@ -216,8 +221,13 @@ fn truncate(settings Settings) { // If --no-create is set, nothing is done but no error is generated // This is behavior from the original GNU coreutil. if os.exists(fname) { - block_size := if settings.io_blocks { get_block_size(fname) or { - default_block_size} } else { 1 } + block_size := if settings.io_blocks { + get_block_size(fname) or { + default_block_size + } + } else { + 1 + } size := calc_target_size(get_size(fname), settings.size_opt, block_size) os.truncate(fname, size) or { app.quit(message: err.msg()) } } diff --git a/src/truncate/truncate_test.v b/src/truncate/truncate_test.v index 67010835..da10714b 100644 --- a/src/truncate/truncate_test.v +++ b/src/truncate/truncate_test.v @@ -28,7 +28,7 @@ fn test_size_parser() { s = parse_size_opt('-24GB') assert s.mode == .subtract - assert s.size == 24 * 1000 * 1000 * 1000 + assert s.size == u64(24) * 1000 * 1000 * 1000 s = parse_size_opt('<13kB') assert s.mode == .at_most @@ -46,12 +46,12 @@ fn test_size_parser() { s = parse_size_opt('/14PB') assert s.mode == .round_down - assert s.size == 14 * 1000 * 1000 * 1000 * 1000 * 1000 + assert s.size == u64(14) * 1000 * 1000 * 1000 * 1000 * 1000 assert s != t s = parse_size_opt('%1E') assert s.mode == .round_up - assert s.size == 1 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 + assert s.size == u64(1024) * 1024 * 1024 * 1024 * 1024 * 1024 // TODO: Test u64 overflows } diff --git a/src/unlink/unlink.c.v b/src/unlink/unlink.c.v index 6a00b43e..e450f253 100644 --- a/src/unlink/unlink.c.v +++ b/src/unlink/unlink.c.v @@ -1,6 +1,7 @@ import os #include + $if !windows { #include } diff --git a/src/users/users.c.v b/src/users/users.c.v index b7e3b2dc..9fb26cc4 100644 --- a/src/users/users.c.v +++ b/src/users/users.c.v @@ -1,23 +1,19 @@ import common -pub struct C.utmpx { - ut_type i16 // Type of login. - ut_pid int // Process ID of login process. - ut_line &char // Devicename. - ut_id &char // Inittab ID. - ut_user &char // Username. -} +// `struct utmpx` is picked up from via common/readutmp_nix.c.v. A +// hand written copy here listed only the fields it needed and dropped the +// fixed size char arrays, so its element size did not match the C struct. fn utmp_users(filename &char) []string { mut utmp_buf := []C.utmpx{} - mut users := []string{} + mut names := []string{} common.read_utmp(filename, mut utmp_buf, .user_process) unsafe { for u in utmp_buf { - users << cstring_to_vstring(u.ut_user) + names << cstring_to_vstring(&u.ut_user[0]) } } // Obtain sorted order as GNU coreutils - users.sort() - return users + names.sort() + return names } diff --git a/src/users/users.v b/src/users/users.v index 121fa227..3a89d358 100644 --- a/src/users/users.v +++ b/src/users/users.v @@ -13,9 +13,9 @@ mut: } fn users(settings Settings) { - users := utmp_users(settings.input_file).join(' ') - print(users) - if users != '' { + names := utmp_users(settings.input_file).join(' ') + print(names) + if names != '' { print(common.eol()) } } diff --git a/src/wc/wc.v b/src/wc/wc.v index e928a36d..ba8ff5ff 100644 --- a/src/wc/wc.v +++ b/src/wc/wc.v @@ -59,7 +59,7 @@ fn get_count(chunk FileChunk, last_line_length u32) (Count, u32) { } line_length = 0 } - space, carriage_return, vertical_tab, form_feed { + space, vertical_tab, form_feed { prev_char_is_space = true line_length++ } @@ -96,14 +96,19 @@ mut: mutex sync.Mutex } -fn (mut file_reader FileReader) read_chunk(mut buffer []u8) ?FileChunk { +// read_chunk reads the next buffer_size bytes. os.File.read signals +// exhaustion with an os.Eof error, which the caller has to tell apart from a +// genuine read failure. +fn (mut file_reader FileReader) read_chunk(mut buffer []u8) !FileChunk { file_reader.mutex.@lock() defer { file_reader.mutex.unlock() } - nbytes := - file_reader.file.read(mut buffer) or { return none } // Propagate error. Either EOF or read error. + nbytes := file_reader.file.read(mut buffer) or { return err } + if nbytes == 0 { + return os.Eof{} + } mut chunk := FileChunk{file_reader.last_char_is_space, buffer[..nbytes].clone(), false} file_reader.last_char_is_space = is_space(buffer[nbytes - 1]) if nbytes < buffer.len { @@ -121,16 +126,14 @@ fn file_reader_counter(mut file_reader FileReader) Count { for { chunk := file_reader.read_chunk(mut buffer) or { match err { - none { - // EOF 'error', just break out of the loop. + os.Eof { break } else { - println(err) + eprintln('${application_name}: ${err}') + exit(1) } } - - exit(1) } count, line_length = get_count(chunk, line_length) @@ -256,7 +259,8 @@ fn main() { max_line_length_len := max_line_length.str().len mut col_size := int(0) - if byte(bytes_opt) + byte(chars_opt) + byte(lines_opt) + byte(words_opt) + byte(maxline_opt) == 1 { + // A single selected count is printed without a header or column padding. + if [bytes_opt, chars_opt, lines_opt, words_opt, maxline_opt].filter(it).len == 1 { col_size = 0 } else { if total_line_count_len > col_size {