From 10dff915005abd046294b6db847313fbd22a3ede Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Mon, 6 Oct 2025 10:38:12 +0300 Subject: [PATCH] os: cleanup; make os.Command available on windows too, add example, extract code to command.c.v and command_test.v (#25441) --- ...onitor_for_file_changes_with_inotifywait.v | 15 ++++ vlib/os/command.c.v | 80 +++++++++++++++++++ vlib/os/command_test.v | 32 ++++++++ vlib/os/os.v | 11 --- vlib/os/os_nix.c.v | 45 ----------- vlib/os/os_test.c.v | 33 -------- vlib/os/os_windows.c.v | 14 ---- 7 files changed, 127 insertions(+), 103 deletions(-) create mode 100644 examples/process/monitor_for_file_changes_with_inotifywait.v create mode 100644 vlib/os/command.c.v create mode 100644 vlib/os/command_test.v diff --git a/examples/process/monitor_for_file_changes_with_inotifywait.v b/examples/process/monitor_for_file_changes_with_inotifywait.v new file mode 100644 index 000000000..e9c33a949 --- /dev/null +++ b/examples/process/monitor_for_file_changes_with_inotifywait.v @@ -0,0 +1,15 @@ +import os + +os.find_abs_path_of_executable('inotifywait') or { + eprintln('This program uses the inotifywait executable, which is missing.') + exit(1) +} +mut cmd := os.start_new_command('inotifywait -q -r -m -e move,modify,create,delete .')! +defer { cmd.close() or {} } +for !cmd.eof { + line := cmd.read_line() + if line == '' && cmd.eof { + continue + } + eprintln('> notification: ${line}') +} diff --git a/vlib/os/command.c.v b/vlib/os/command.c.v new file mode 100644 index 000000000..1bc67eef5 --- /dev/null +++ b/vlib/os/command.c.v @@ -0,0 +1,80 @@ +module os + +import strings + +// Command represents a running shell command, that the parent process +// wishes to monitor for output on its stdout pipe. +pub struct Command { +mut: + f voidptr +pub mut: + eof bool + exit_code int +pub: + path string + redirect_stdout bool +} + +// start_new_command will create a new os.Command, and start it right away. +// The command will represent a process running the passed shell command `cmd`, +// in a way that you can later call c.read_line() to intercept the new child process +// output line by line, streaming while the command is running. +// See also c.eof, c.read_line(), c.close() and c.exit_code . +pub fn start_new_command(cmd string) !Command { + mut res := Command{ + path: cmd + } + res.start()! + return res +} + +// start will start the command. Use start_new_command/1 instead. +@[manualfree] +pub fn (mut c Command) start() ! { + pcmd := c.path + ' 2>&1' + defer { + unsafe { pcmd.free() } + } + c.f = vpopen(pcmd) + if isnil(c.f) { + return error('exec("${c.path}") failed') + } +} + +// read_line returns a single line from the stdout of the running command. +// Note: c.eof will be set to true, if the command ended while a line was read. +// The returned line will contain all of the accumulated output before the process ended. +// In practice, that often means, you will get a single '' and c.eof == true at the end. +@[manualfree] +pub fn (mut c Command) read_line() string { + buf := [4096]u8{} + mut res := strings.new_builder(1024) + defer { unsafe { res.free() } } + unsafe { + bufbp := &buf[0] + for C.fgets(&char(bufbp), 4096, c.f) != 0 { + len := vstrlen(bufbp) + for i in 0 .. len { + if bufbp[i] == `\n` { + res.write_ptr(bufbp, i) + final := res.str() + return final + } + } + res.write_ptr(bufbp, len) + } + } + c.eof = true + final := res.str() + return final +} + +// close will close the pipe to the command, and wait for the command to finish, +// then set .exit_code according to how its final process status. +pub fn (mut c Command) close() ! { + c.exit_code = vpclose(c.f) + c.f = unsafe { nil } + if c.exit_code == 127 { + return error_with_code('error', 127) + } +} diff --git a/vlib/os/command_test.v b/vlib/os/command_test.v new file mode 100644 index 000000000..8d69d59ee --- /dev/null +++ b/vlib/os/command_test.v @@ -0,0 +1,32 @@ +import os + +fn test_command() { + if os.user_os() == 'windows' { + eprintln('>>> skipping command test on windows') + return + } + + mut cmd := os.start_new_command('ls')! + for !cmd.eof { + line := cmd.read_line() + if line == '' { + continue + } + dump(line) + } + cmd.close()! + assert cmd.exit_code == 0 + + eprintln('-------------------------') + // This will return a non 0 code + mut cmd_to_fail := os.start_new_command('ls -M')! + for !cmd_to_fail.eof { + line := cmd_to_fail.read_line() + if line == '' { + continue + } + dump(line) + } + cmd_to_fail.close()! + assert cmd_to_fail.exit_code != 0 // 2 on linux, 1 on macos +} diff --git a/vlib/os/os.v b/vlib/os/os.v index 3aa5c396b..058e68ea2 100644 --- a/vlib/os/os.v +++ b/vlib/os/os.v @@ -24,17 +24,6 @@ pub: // stderr string // TODO } -pub struct Command { -mut: - f voidptr -pub mut: - eof bool - exit_code int -pub: - path string - redirect_stdout bool -} - @[unsafe] pub fn (mut result Result) free() { unsafe { result.output.free() } diff --git a/vlib/os/os_nix.c.v b/vlib/os/os_nix.c.v index 76d7fa9cb..a627b71f4 100644 --- a/vlib/os/os_nix.c.v +++ b/vlib/os/os_nix.c.v @@ -362,51 +362,6 @@ pub fn raw_execute(cmd string) Result { return execute(cmd) } -@[manualfree] -pub fn (mut c Command) start() ! { - pcmd := c.path + ' 2>&1' - defer { - unsafe { pcmd.free() } - } - c.f = vpopen(pcmd) - if isnil(c.f) { - return error('exec("${c.path}") failed') - } -} - -@[manualfree] -pub fn (mut c Command) read_line() string { - buf := [4096]u8{} - mut res := strings.new_builder(1024) - defer { - unsafe { res.free() } - } - unsafe { - bufbp := &buf[0] - for C.fgets(&char(bufbp), 4096, c.f) != 0 { - len := vstrlen(bufbp) - for i in 0 .. len { - if bufbp[i] == `\n` { - res.write_ptr(bufbp, i) - final := res.str() - return final - } - } - res.write_ptr(bufbp, len) - } - } - c.eof = true - final := res.str() - return final -} - -pub fn (mut c Command) close() ! { - c.exit_code = vpclose(c.f) - if c.exit_code == 127 { - return error_with_code('error', 127) - } -} - // symlink creates a symbolic link named target, which points to origin. // It returns a POSIX error message, if it can not do so. pub fn symlink(origin string, target string) ! { diff --git a/vlib/os/os_test.c.v b/vlib/os/os_test.c.v index 12c54d706..80f34e30a 100644 --- a/vlib/os/os_test.c.v +++ b/vlib/os/os_test.c.v @@ -993,39 +993,6 @@ fn test_execute_fc_get_output() { assert result.exit_code == -1 } -fn test_command() { - if os.user_os() == 'windows' { - eprintln('>>> os.Command is not implemented fully on Windows yet') - return - } - mut cmd := os.Command{ - path: 'ls' - } - - cmd.start() or { panic(err) } - for !cmd.eof { - cmd.read_line() - } - - cmd.close() or { panic(err) } - // dump( cmd ) - assert cmd.exit_code == 0 - - // This will return a non 0 code - mut cmd_to_fail := os.Command{ - path: 'ls -M' - } - - cmd_to_fail.start() or { panic(err) } - for !cmd_to_fail.eof { - cmd_to_fail.read_line() - } - - cmd_to_fail.close() or { panic(err) } - // dump( cmd_to_fail ) - assert cmd_to_fail.exit_code != 0 // 2 on linux, 1 on macos -} - fn test_reading_from_proc_cpuinfo() { // This test is only for plain linux systems (they have a /proc virtual filesystem). $if android { diff --git a/vlib/os/os_windows.c.v b/vlib/os/os_windows.c.v index 45d390252..b3a8cee73 100644 --- a/vlib/os/os_windows.c.v +++ b/vlib/os/os_windows.c.v @@ -568,20 +568,6 @@ pub fn posix_set_permission_bit(path_s string, mode u32, enable bool) { // windows has no concept of a permission mask, so do nothing } -// - -pub fn (mut c Command) start() ! { - panic('not implemented') -} - -pub fn (mut c Command) read_line() string { - panic('not implemented') -} - -pub fn (mut c Command) close() ! { - panic('not implemented') -} - fn C.GetLongPathName(short_path &u16, long_path &u16, long_path_bufsize u32) u32 // get_long_path has no meaning for *nix, but has for windows, where `c:\folder\some~1` for example -- 2.39.5