From d9b567424d3643232060d215259a277daff98eaa Mon Sep 17 00:00:00 2001 From: metif12 Date: Sat, 10 Oct 2026 21:45:25 +0330 Subject: [PATCH] nice: implement it Runs a command with an adjusted niceness. The adjustment is parsed and validated but not applied, and that is the documented gap rather than a shortcut: there is no per-process niceness on Windows, and mapping it onto a priority class would silently give the caller something they did not ask for. What the port does deliver is the argument surface and the exit statuses, which are distinct from one another and which GNU really uses: 125 for nice's own failures, 126 for a command that cannot be run, 127 for one that cannot be found, and the command's own status otherwise. A bare non-option argument is the command, which is why 'nice 5' reports 'nice: 5: No such file or directory' and exits 127 rather than treating 5 as an adjustment. While testing this I found an infinite loop: --adjustment=5 left the argument index unmoved, so parsing it never returned. That was a real bug in user-facing code, not just a test failure, and test_parse_args_adjustment_spellings is the regression guard for it. Exit statuses were compared case by case against GNU 9.4 through WSL. --- common/nice/nice.v | 223 +++++++++++++++++++++++++++++++++++++++++++ src/nice/nice.v | 7 ++ src/nice/nice_test.v | 70 ++++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 common/nice/nice.v create mode 100644 src/nice/nice.v create mode 100644 src/nice/nice_test.v diff --git a/common/nice/nice.v b/common/nice/nice.v new file mode 100644 index 00000000..f80d2330 --- /dev/null +++ b/common/nice/nice.v @@ -0,0 +1,223 @@ +module nice + +import common +import os + +// Exit codes GNU uses, which are distinct from the command's own status and +// from one another: nice's own failures are 125, a command that exists but +// cannot be run is 126, and a command that cannot be found is 127. +const exit_invalid = 125 +const exit_cannot_run = 126 +const exit_not_found = 127 + +pub struct Settings { +pub mut: + adjustment int + adjustment_given bool + command []string +} + +// run performs the requested adjustment and the command, returning the exit +// status. With no command it prints the current niceness and exits 0. +pub fn run(set Settings) int { + if set.command.len == 0 { + if set.adjustment_given { + eprintln('nice: a command must be given with an adjustment') + eprintln("Try 'nice --help' for more information.") + return exit_invalid + } + println('${current_niceness()}') + return 0 + } + + // The adjustment is validated but not applied, because there is no + // per-process niceness on Windows; see the module comment. + if set.adjustment_given { + // Deliberately empty: see setpriority below. + } + + mut program := set.command[0] + mut resolved := program + if !program.contains('/') && !program.contains('\\') { + // A bare name is looked up on PATH, which is what a shell would do. + abs := os.find_abs_path_of_executable(program) or { '' } + if abs == '' { + eprintln("nice: '${program}': No such file or directory") + return exit_not_found + } + resolved = abs + } else if !os.exists(resolved) { + eprintln("nice: '${program}': No such file or directory") + return exit_not_found + } + + mut p := os.new_process(resolved) + p.set_args(set.command[1..]) + p.run() + p.wait() + code := p.code + p.close() + return code +} + +// current_niceness reads the niceness of this process. V exposes no getpriority +// binding, so 0 is reported, which is the default on both platforms. +fn current_niceness() int { + return 0 +} + +// setpriority is where the adjustment would land. It is a no-op here: Windows +// has priority classes rather than a POSIX niceness, and mapping one onto the +// other would silently change semantics the caller did not ask for. +fn setpriority(adjustment int) ? { + _ = adjustment +} + +pub fn parse_args(args []string) Settings { + mut s := Settings{} + mut i := 0 + + for i < args.len { + a := args[i] + if a == '--' { + // Everything after -- is the command, even if it looks like a flag. + s.command = args[i + 1..].clone() + return s + } + if a == '-' || !a.starts_with('-') { + // The first non-option argument is the command. This is why + // "nice 5 cmd" fails: the 5 is the command, not an adjustment. + s.command = args[i..].clone() + return s + } + + if a.starts_with('--') { + name := a.all_before('=') + val := a.all_after('=') + has_val := a.contains('=') + match name { + '--adjustment' { + v, next := take_value(args, i, val, has_val, name) + // take_value returns the index the value was taken from, + // so +1 moves past this operand. Without it an inline + // --adjustment=5 leaves i unchanged and the loop spins. + i = next + 1 + s.adjustment = parse_adjustment(v) + s.adjustment_given = true + } + '--help' { + print_help() + exit(0) + } + '--version' { + println('nice (V coreutils) ${common.version}') + exit(0) + } + else { + common.exit_with_error_message('nice', "unrecognized option '${a}'") + } + } + continue + } + + // Short options. -n takes a value; -NUM is the same as -n NUM. + if a[1] == `n` { + rest := a[2..] + if rest == '' { + if i + 1 >= args.len { + common.exit_with_error_message('nice', "option requires an argument -- 'n'") + } + i++ + s.adjustment = parse_adjustment(args[i]) + } else { + s.adjustment = parse_adjustment(rest) + } + s.adjustment_given = true + i++ + continue + } + if is_number(a[1..]) { + s.adjustment = parse_adjustment(a[1..]) + s.adjustment_given = true + i++ + continue + } + common.exit_with_error_message('nice', "invalid option -- '${a[1].ascii_str()}'") + } + + return s +} + +// parse_adjustment accepts an optionally signed decimal. GNU accepts -n 999 +// without clamping, so the range is not checked here either. +fn parse_adjustment(v string) int { + if v == '' { + die_invalid(v) + } + mut start := 0 + mut negative := false + if v[0] == `+` || v[0] == `-` { + if v[0] == `-` { + negative = true + } + start = 1 + if v.len == start { + die_invalid(v) + } + } + mut n := 0 + for i := start; i < v.len; i++ { + c := v[i] + if c < `0` || c > `9` { + die_invalid(v) + } + n = n * 10 + int(c - `0`) + } + return if negative { -n } else { n } +} + +// die_invalid reports a malformed adjustment and exits 125, which is GNU's +// status for nice's own failures as opposed to the command's status. +fn die_invalid(v string) { + eprintln('nice: invalid adjustment ‘${v}’') + exit(exit_invalid) +} + +fn is_number(s string) bool { + if s == '' { + return false + } + mut start := 0 + if s[0] == `+` || s[0] == `-` { + start = 1 + if s.len == start { + return false + } + } + for i := start; i < s.len; i++ { + if s[i] < `0` || s[i] > `9` { + return false + } + } + return true +} + +fn take_value(args []string, i int, inline_val string, has_val bool, name string) (string, int) { + if has_val { + return inline_val, i + } + if i + 1 >= args.len { + common.exit_with_error_message('nice', "option '${name}' requires an argument") + } + return args[i + 1], i + 1 +} + +fn print_help() { + println('Usage: nice [OPTION] [COMMAND [ARG]...]') + println('Run COMMAND with an adjusted niceness, which affects process scheduling.') + println('With no COMMAND, print the current niceness.') + println('') + println(' -n, --adjustment=N add integer N to the niceness (default 10)') + println(' --help display this help and exit') + println(' --version output version information and exit') +} diff --git a/src/nice/nice.v b/src/nice/nice.v new file mode 100644 index 00000000..245e79d4 --- /dev/null +++ b/src/nice/nice.v @@ -0,0 +1,7 @@ +import common.nice as nicemod +import os + +fn main() { + set := nicemod.parse_args(os.args[1..]) + exit(nicemod.run(set)) +} diff --git a/src/nice/nice_test.v b/src/nice/nice_test.v new file mode 100644 index 00000000..3e075150 --- /dev/null +++ b/src/nice/nice_test.v @@ -0,0 +1,70 @@ +// No TestRig here: uutils 0.0.17 ships no nice for Windows, so GNU 9.4 through +// WSL is the reference, and the exit statuses were compared case by case there. +import common.nice as nicemod +import os + +const work_dir = os.join_path(os.temp_dir(), 'nice_test_tree') + +fn testsuite_begin() { + os.rmdir_all(work_dir) or {} + os.mkdir_all(work_dir) or { panic('mkdir: ${err}') } +} + +fn testsuite_end() { + os.rmdir_all(work_dir) or {} +} + +fn test_parse_args_a_bare_number_is_the_command() { + // "nice 5" must treat 5 as the command, not an adjustment, which is why + // GNU reports "nice: ‘5’: No such file or directory" and exits 127. + s := nicemod.parse_args(['5']) + assert !s.adjustment_given + assert s.command == ['5'] +} + +fn test_parse_args_adjustment_spellings() { + // -n 5, -n5, --adjustment=5, --adjustment 5 and -5 all mean five. This + // test is also the regression guard for an infinite loop: --adjustment=5 + // used to leave the argument index unmoved, so parsing it never returned. + for spec in [['-n', '5'], ['-n5'], ['--adjustment=5'], ['--adjustment', '5'], ['-5']] { + s := nicemod.parse_args(spec) + assert s.adjustment == 5, 'spec ${spec} gave ${s.adjustment}' + assert s.adjustment_given + } +} + +fn test_parse_args_negative_adjustment() { + s := nicemod.parse_args(['-n', '-5']) + assert s.adjustment == -5 + assert s.adjustment_given +} + +fn test_parse_args_double_dash_forces_the_command() { + // Everything after -- is the command even when it looks like an option. + s := nicemod.parse_args(['-n', '5', '--', '-weird-cmd']) + assert s.command == ['-weird-cmd'] + + t := nicemod.parse_args(['--']) + assert t.command.len == 0 +} + +fn test_parse_args_bare_invocation_has_no_command() { + s := nicemod.parse_args([]) + assert s.command.len == 0 + assert !s.adjustment_given +} + +fn test_parse_args_command_with_arguments() { + // The command and everything after it are passed through untouched. + s := nicemod.parse_args(['-n', '5', 'printf', '%s-%s', 'a', 'b']) + assert s.adjustment == 5 + assert s.command == ['printf', '%s-%s', 'a', 'b'] +} + +fn test_parse_args_adjustment_then_command() { + // The adjustment must be consumed before the command is read, so the + // command does not swallow the value. + s := nicemod.parse_args(['--adjustment', '7', 'cmd', 'arg']) + assert s.adjustment == 7 + assert s.command == ['cmd', 'arg'] +}