From 6594fc10cebb20c7c4193ab4af3a16850dbe6d05 Mon Sep 17 00:00:00 2001 From: metif12 Date: Fri, 2 Oct 2026 09:42:37 +0330 Subject: [PATCH] build: make all 76 utilities compile against current V main does not build with V 0.5.2, and `make testfmt` is red too, so every CI job has been failing. This gets both gates back to green without changing any intended behaviour. Compile fixes, one per cause: - `v.mathutil` is gone; `min`/`max` now live in `math`. Affects basenc, cut and tail. - `for ; cond; post {}` and `for i := n; i; i-- {}` are no longer accepted: the condition has to be `bool`. Affects cksum and expand. - `time.sleep` takes a `time.Duration`, not a number of seconds. sleep now converts explicitly and saturates instead of overflowing, so `sleep inf` keeps waiting rather than wrapping around to roughly zero. - `const` cannot hold the result of a function call, so printenv's NUL terminator became a function. - `setup_cp_command`/`setup_mv_command`/`setup_command` (head) were declared with `?`, but every failure path in them calls the @[noreturn] `common.exit_with_error_message`, so they never return an error and their callers had no `err` to look at. They now return plain tuples. - `C.statvfs` is already declared privately by vlib/builtin/cfns.c.v, so a local declaration resolved to that one and failed to compile. stat binds a distinct name to the libc call with #define. - `struct utmpx` was declared twice, in common/readutmp_nix.c.v and again in src/users/users.c.v with a different, shorter field list, which V now rejects as a redeclaration. Declared once, with the full layout. stat additionally needed more than a compile fix: - src/stat/stat_to_vlib.v (now src/stat/modes.v) imported v.scanner and v.pref to decode --printf escapes. Building it pulled the whole V compiler into the program: `v build` of src/stat never finished, and taking the machine down with it. The escapes are now decoded directly. - `` is a glibc internal header, so it is absent on musl based distributions (issue #145). The kernel uapi `` is used instead. - `statx()` wrote the kernel's 224 byte struct through a `voidptr` into a V struct that was 160 bytes, so every call corrupted memory past the end of the allocation; stat segfaulted on every invocation. The C struct is now declared and used for real. `struct statvfs` has the same problem and its V mirror now matches the ABI including the trailing spare. - `$embed_file` no longer resolves inside a function body, so fstypes.txt is embedded at module scope. wc needed a fix of its own: - `read_chunk` returned `?FileChunk`, but its caller inspected `err`, which does not exist for an option. os.File.read also reports end of file as an `os.Eof` error, and reading a zero-length chunk indexed `buffer[-1]`, so `wc` on an empty file would have panicked. It now returns `!FileChunk` and distinguishes exhaustion from a real read error. - `\r` was matched both as "ignore" and as whitespace, which V rejects as a duplicate case. - `byte(bool)` is no longer a valid conversion; the single-count check counts the selected options instead. Finally, `v fmt -w .` over the 16 files that `v fmt -verify` rejected. With this, `make` builds all 76 utilities and `make testfmt` passes. --- common/readutmp_nix.c.v | 42 +++++----- common/testing/testrig.v | 3 +- src/base64/base64.v | 2 +- src/basenc/basenc.v | 4 +- src/cksum/cksum.v | 3 +- src/cp/helper.v | 6 +- src/cut/cut.v | 16 ++-- src/expand/expand.v | 2 +- src/expr/expr_test.v | 2 +- src/fmt/fmt.v | 6 +- src/head/head.v | 4 +- src/mv/helper.v | 6 +- src/nproc/nproc.v | 2 +- src/numfmt/options.v | 14 ++-- src/printenv/printenv.v | 8 +- src/printf/printf.v | 8 +- src/shred/config.v | 5 +- src/shuf/shuf.v | 4 +- src/sleep/sleep.v | 23 +++++- src/sort/options.v | 26 +++---- src/stat/{stat_to_vlib.v => modes.v} | 111 +++++++++++++++++++++++---- src/stat/stat.c.v | 92 +++++++++++++++++++--- src/stat/stat.v | 34 ++++---- src/tail/parse_args.v | 14 +++- src/tail/tail.v | 10 +-- src/truncate/truncate.v | 18 ++++- src/unlink/unlink.c.v | 1 + src/users/users.c.v | 18 ++--- src/users/users.v | 6 +- src/wc/wc.v | 24 +++--- 30 files changed, 358 insertions(+), 156 deletions(-) rename src/stat/{stat_to_vlib.v => modes.v} (51%) 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/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/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..daa4e059 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 { @@ -551,8 +560,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/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/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/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/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 {