// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module os import strings pub const max_path_len = 4096 pub const wd_at_startup = getwd() const f_ok = 0 const x_ok = 1 const w_ok = 2 const r_ok = 4 pub struct Result { pub: exit_code int output string // 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() } } // executable_fallback is used when there is not a more platform specific and accurate implementation. // It relies on path manipulation of os.args[0] and os.wd_at_startup, so it may not work properly in // all cases, but it should be better, than just using os.args[0] directly. fn executable_fallback() string { if args.len == 0 { // we are early in the bootstrap, os.args has not been initialized yet :-| return '' } mut exepath := args[0] $if windows { if !exepath.contains('.exe') { exepath += '.exe' } } if !is_abs_path(exepath) { other_separator := if path_separator == '/' { '\\' } else { '/' } rexepath := exepath.replace(other_separator, path_separator) if rexepath.contains(path_separator) { exepath = join_path_single(wd_at_startup, exepath) } else { // no choice but to try to walk the PATH folders :-| ... foundpath := find_abs_path_of_executable(exepath) or { '' } if foundpath != '' { exepath = foundpath } } } exepath = real_path(exepath) return exepath } // cp_all will recursively copy `src` to `dst`, optionally overwriting files or dirs in `dst`. pub fn cp_all(src string, dst string, overwrite bool) ! { source_path := real_path(src) dest_path := real_path(dst) if !exists(source_path) { return error("Source path doesn't exist") } // single file copy if !is_dir(source_path) { fname := file_name(source_path) adjusted_path := if is_dir(dest_path) { join_path_single(dest_path, fname) } else { dest_path } if exists(adjusted_path) { if overwrite { rm(adjusted_path)! } else { return error('Destination file path already exist') } } cp(source_path, adjusted_path)! return } if !exists(dest_path) { mkdir(dest_path)! } if !is_dir(dest_path) { return error('Destination path is not a valid directory') } files := ls(source_path)! for file in files { sp := join_path_single(source_path, file) dp := join_path_single(dest_path, file) if is_dir(sp) { if !exists(dp) { mkdir(dp)! } } cp_all(sp, dp, overwrite) or { rmdir(dp) or { return err } return err } } } @[params] pub struct MvParams { pub: overwrite bool = true } // mv_by_cp copies files or folders from `source` to `target`. // If copying is successful, `source` is deleted. // It may be used when the paths are not on the same mount/partition. pub fn mv_by_cp(source string, target string, opts MvParams) ! { cp_all(source, target, opts.overwrite)! if is_dir(source) { rmdir_all(source)! return } rm(source)! } // mv moves files or folders from `src` to `dst`. pub fn mv(source string, target string, opts MvParams) ! { if !opts.overwrite && exists(target) { return error('target path already exist') } rename(source, target) or { mv_by_cp(source, target, opts)! } } // read_lines reads the file in `path` into an array of lines. @[manualfree] pub fn read_lines(path string) ![]string { buf := read_file(path)! res := buf.split_into_lines() unsafe { buf.free() } return res } // write_lines writes the given array of `lines` to `path`. // The lines are separated by `\n` . pub fn write_lines(path string, lines []string) ! { mut f := create(path)! defer { f.close() } for line in lines { f.writeln(line)! } } // sigint_to_signal_name will translate `si` signal integer code to it's string code representation. pub fn sigint_to_signal_name(si int) string { // POSIX signals: match si { 1 { return 'SIGHUP' } 2 { return 'SIGINT' } 3 { return 'SIGQUIT' } 4 { return 'SIGILL' } 6 { return 'SIGABRT' } 8 { return 'SIGFPE' } 9 { return 'SIGKILL' } 11 { return 'SIGSEGV' } 13 { return 'SIGPIPE' } 14 { return 'SIGALRM' } 15 { return 'SIGTERM' } else {} } $if linux { // From `man 7 signal` on linux: match si { // TODO: dependent on platform // works only on x86/ARM/most others 10 { // , 30, 16 return 'SIGUSR1' } 12 { // , 31, 17 return 'SIGUSR2' } 17 { // , 20, 18 return 'SIGCHLD' } 18 { // , 19, 25 return 'SIGCONT' } 19 { // , 17, 23 return 'SIGSTOP' } 20 { // , 18, 24 return 'SIGTSTP' } 21 { // , 26 return 'SIGTTIN' } 22 { // , 27 return 'SIGTTOU' } // ///////////////////////////// 5 { return 'SIGTRAP' } 7 { return 'SIGBUS' } else {} } } return 'unknown' } // rmdir_all recursively removes the specified directory. pub fn rmdir_all(path string) ! { mut ret_err := '' items := ls(path)! for item in items { fullpath := join_path_single(path, item) if is_dir(fullpath) && !is_link(fullpath) { rmdir_all(fullpath) or { ret_err = err.msg() } } else { rm(fullpath) or { ret_err = err.msg() } } } rmdir(path) or { ret_err = err.msg() } if ret_err.len > 0 { return error(ret_err) } } // is_dir_empty will return a `bool` whether or not `path` is empty. // Note that it will return `true` if `path` does not exist. @[manualfree] pub fn is_dir_empty(path string) bool { items := ls(path) or { return true } res := items.len == 0 unsafe { items.free() } return res } // file_ext will return the part after the last occurrence of `.` in `path`. // The `.` is included. // Examples: // ```v // assert os.file_ext('file.v') == '.v' // assert os.file_ext('.ignore_me') == '' // assert os.file_ext('.') == '' // ``` pub fn file_ext(opath string) string { if opath.len < 3 { return '' } path := file_name(opath) pos := path.last_index_u8(`.`) if pos == -1 { return '' } if pos + 1 >= path.len || pos == 0 { return '' } return path[pos..] } // dir returns all but the last element of path, typically the path's directory. // After dropping the final element, trailing slashes are removed. // If the path is empty, dir returns ".". If the path consists entirely of separators, // dir returns a single separator. // The returned path does not end in a separator unless it is the root directory. pub fn dir(path string) string { if path == '' { return '.' } detected_path_separator := if path.contains('/') { '/' } else { '\\' } pos := path.last_index(detected_path_separator) or { return '.' } if pos == 0 { return detected_path_separator } return path[..pos] } // base returns the last element of path. // Trailing path separators are removed before extracting the last element. // If the path is empty, base returns ".". If the path consists entirely of separators, base returns a // single separator. pub fn base(path string) string { if path == '' { return '.' } detected_path_separator := if path.contains('/') { '/' } else { '\\' } if path == detected_path_separator { return detected_path_separator } if path.ends_with(detected_path_separator) { path2 := path[..path.len - 1] pos := path2.last_index(detected_path_separator) or { return path2.clone() } return path2[pos + 1..] } pos := path.last_index(detected_path_separator) or { return path.clone() } return path[pos + 1..] } // file_name will return all characters found after the last occurrence of `path_separator`. // file extension is included. pub fn file_name(path string) string { detected_path_separator := if path.contains('/') { '/' } else { '\\' } return path.all_after_last(detected_path_separator) } // split_path will split `path` into (`dir`,`filename`,`ext`). // Examples: // ```v // dir,filename,ext := os.split_path('/usr/lib/test.so') // assert [dir,filename,ext] == ['/usr/lib','test','.so'] // ``` pub fn split_path(path string) (string, string, string) { if path == '' { return '.', '', '' } else if path == '.' { return '.', '', '' } else if path == '..' { return '..', '', '' } detected_path_separator := if path.contains('/') { '/' } else { '\\' } if path == detected_path_separator { return detected_path_separator, '', '' } if path.ends_with(detected_path_separator) { return path[..path.len - 1], '', '' } mut dir := '.' /* TODO: JS backend does not support IfGuard yet. */ pos := path.last_index(detected_path_separator) or { -1 } if pos == -1 { dir = '.' } else if pos == 0 { dir = detected_path_separator } else { dir = path[..pos] } file_name := path.all_after_last(detected_path_separator) pos_ext := file_name.last_index_u8(`.`) if pos_ext == -1 || pos_ext == 0 || pos_ext + 1 >= file_name.len { return dir, file_name, '' } return dir, file_name[..pos_ext], file_name[pos_ext..] } // input_opt returns a one-line string from stdin, after printing a prompt. // Returns `none` in case of an error (end of input). pub fn input_opt(prompt string) ?string { print(prompt) flush() res := get_raw_line() if res.len > 0 { return res.trim_right('\r\n') } return none } // input returns a one-line string from stdin, after printing a prompt. // Returns `` in case of an error (end of input). pub fn input(prompt string) string { res := input_opt(prompt) or { return '' } return res } // get_line returns a one-line string from stdin. pub fn get_line() string { str := get_raw_line() $if windows { return str.trim_right('\r\n') } return str.trim_right('\n') } // get_lines returns an array of strings read from stdin. // reading is stopped when an empty line is read. pub fn get_lines() []string { mut line := '' mut inputstr := []string{} for { line = get_line() if line.len <= 0 { break } line = line.trim_space() inputstr << line } return inputstr } // get_lines_joined returns a string of the values read from stdin. // reading is stopped when an empty line is read. pub fn get_lines_joined() string { return get_lines().join('') } // get_raw_lines reads *all* input lines from stdin, as an array of strings. // Note: unlike os.get_lines, empty lines (that contain only `\r\n` or `\n`), // will be present in the output. // Reading is stopped, only on EOF of stdin. pub fn get_raw_lines() []string { mut line := '' mut lines := []string{} for { line = get_raw_line() if line.len <= 0 { break } lines << line } return lines } // get_raw_lines_joined reads *all* input lines from stdin. // It returns them as one large string. Note: unlike os.get_lines_joined, // empty lines (that contain only `\r\n` or `\n`), will be present in // the output. // Reading is stopped, only on EOF of stdin. pub fn get_raw_lines_joined() string { return get_raw_lines().join('') } // get_trimmed_lines reads *all* input lines from stdin, as an array of strings. // The ending new line characters `\r` and `\n`, are removed from each line. // Note: unlike os.get_lines, empty lines will be present in the output as empty strings ''. // Reading is stopped, only on EOF of stdin. pub fn get_trimmed_lines() []string { mut lines := []string{} for { mut line := get_raw_line() if line.len <= 0 { break } mut end := line.len if end > 0 && line[end - 1] == `\n` { end-- } if end > 0 && line[end - 1] == `\r` { end-- } lines << line#[..end] } return lines } // user_os returns the current user's operating system name. pub fn user_os() string { $if linux { return 'linux' } $if macos { return 'macos' } $if windows { return 'windows' } $if freebsd { return 'freebsd' } $if openbsd { return 'openbsd' } $if netbsd { return 'netbsd' } $if dragonfly { return 'dragonfly' } $if android { return 'android' } $if termux { return 'termux' } $if solaris { return 'solaris' } $if qnx { return 'qnx' } $if haiku { return 'haiku' } $if serenity { return 'serenity' } //$if plan9 { // return 'plan9' //} $if vinix { return 'vinix' } if getenv('TERMUX_VERSION') != '' { return 'termux' } return 'unknown' } // user_names returns an array containing the names of all users on the system. pub fn user_names() ![]string { $if windows { result := execute('wmic useraccount get name') if result.exit_code != 0 { return error('Failed to get user names. Exited with code ${result.exit_code}: ${result.output}') } mut users := result.output.split_into_lines() // windows command prints an empty line at the end of output users.delete(users.len - 1) return users } $else { lines := read_lines('/etc/passwd')! mut users := []string{cap: lines.len} for line in lines { end_name := line.index(':') or { line.len } users << line[0..end_name] } return users } } // home_dir returns the path to the current user's home directory. pub fn home_dir() string { $if windows { return getenv('USERPROFILE') } $else { return getenv('HOME') } } // expand_tilde_to_home expands the character `~` in `path` to the user's home directory. // See also `home_dir()`. pub fn expand_tilde_to_home(path string) string { if path == '~' { hdir := home_dir() return hdir.trim_right(path_separator) } source := '~' + path_separator if path.starts_with(source) { hdir := home_dir() trimmed := hdir.trim_right(path_separator) final := trimmed + path_separator result := path.replace_once(source, final) return result } return path } // write_file writes `text` data to a file with the given `path`. // If `path` already exists, it will be overwritten. pub fn write_file(path string, text string) ! { mut f := create(path)! unsafe { f.write_full_buffer(text.str, usize(text.len))! } f.close() } pub struct ExecutableNotFoundError { Error } pub fn (err ExecutableNotFoundError) msg() string { return 'os: failed to find executable' } fn error_failed_to_find_executable() IError { return &ExecutableNotFoundError{} } // find_abs_path_of_executable searches the environment PATH for the absolute path of the given executable name. pub fn find_abs_path_of_executable(exe_name string) !string { if exe_name == '' { return error('expected non empty `exe_name`') } for suffix in executable_suffixes { fexepath := exe_name + suffix if is_abs_path(fexepath) { return fexepath } mut res := '' path := getenv('PATH') paths := path.split(path_delimiter) for p in paths { found_abs_path := join_path_single(p, fexepath) $if trace_find_abs_path_of_executable ? { dump(found_abs_path) } if is_file(found_abs_path) && is_executable(found_abs_path) { res = found_abs_path break } } if res.len > 0 { return abs_path(res) } } return error_failed_to_find_executable() } // exists_in_system_path returns `true` if `prog` exists in the system's PATH. pub fn exists_in_system_path(prog string) bool { find_abs_path_of_executable(prog) or { return false } return true } // is_file returns a `bool` indicating whether the given `path` is a file. pub fn is_file(path string) bool { return exists(path) && !is_dir(path) } // join_path joins any number of path elements into a single path, separating // them with a platform-specific path_separator. Empty elements are ignored. // Windows platform output will rewrite forward slashes to backslash. // Consider looking at the unit tests in os_test.v for semi-formal API. @[manualfree] pub fn join_path(base string, dirs ...string) string { // TODO: fix freeing of `dirs` when the passed arguments are variadic, // but do not free the arr, when `os.join_path(base, ...arr)` is called. mut sb := strings.new_builder(base.len + dirs.len * 50) defer { unsafe { sb.free() } } sbase := base.trim_right('\\/') defer { unsafe { sbase.free() } } sb.write_string(sbase) for d in dirs { if d != '' { sb.write_string(path_separator) sb.write_string(d) } } normalize_path_in_builder(mut sb) mut res := sb.str() if base == '' { res = res.trim_left(path_separator) } return res } // join_path_single appends the `elem` after `base`, separated with a // platform-specific path_separator. Empty elements are ignored. @[manualfree] pub fn join_path_single(base string, elem string) string { // TODO: deprecate this and make it `return os.join_path(base, elem)`, // when freeing variadic args vs ...arr is solved in the compiler mut sb := strings.new_builder(base.len + elem.len + 1) defer { unsafe { sb.free() } } sbase := base.trim_right('\\/') defer { unsafe { sbase.free() } } sb.write_string(sbase) if elem != '' { sb.write_string(path_separator) sb.write_string(elem) } normalize_path_in_builder(mut sb) mut res := sb.str() if base == '' { res = res.trim_left(path_separator) } return res } @[direct_array_access] fn normalize_path_in_builder(mut sb strings.Builder) { mut fs := `\\` mut rs := `/` $if windows { fs = `/` rs = `\\` } for idx in 0 .. sb.len { unsafe { if sb[idx] == fs { sb[idx] = rs } } } for idx in 0 .. sb.len - 3 { if sb[idx] == rs && sb[idx + 1] == `.` && sb[idx + 2] == rs { unsafe { // let `/foo/./bar.txt` become `/foo/bar.txt` in place for j := idx + 1; j < sb.len - 2; j++ { sb[j] = sb[j + 2] } sb.len -= 2 } } if sb[idx] == rs && sb[idx + 1] == rs { unsafe { // let `/foo//bar.txt` become `/foo/bar.txt` in place for j := idx + 1; j < sb.len - 1; j++ { sb[j] = sb[j + 1] } sb.len -= 1 } } } } @[params] pub struct WalkParams { pub: hidden bool } // walk_ext returns a recursive list of all files in `path` ending with `ext`. // For listing only one level deep, see: `os.ls` pub fn walk_ext(path string, ext string, opts WalkParams) []string { mut res := []string{} impl_walk_ext(path, ext, mut res, opts) return res } fn impl_walk_ext(path string, ext string, mut out []string, opts WalkParams) { if !is_dir(path) { return } mut files := ls(path) or { return } separator := if path.ends_with(path_separator) { '' } else { path_separator } for file in files { if !opts.hidden && file.starts_with('.') { continue } p := path + separator + file if is_dir(p) && !is_link(p) { impl_walk_ext(p, ext, mut out, opts) } else if file.ends_with(ext) { out << p } } } // walk traverses the given directory `path`. // When a file is encountered, it will call the callback `f` with current file as argument. // Note: walk can be called even for deeply nested folders, // since it does not recurse, but processes them iteratively. // For listing only one level deep, see: `os.ls` pub fn walk(path string, f fn (string)) { if path == '' { return } if !is_dir(path) { return } mut remaining := []string{cap: 1000} clean_path := path.trim_right(path_separator) $if windows { remaining << clean_path.replace('/', '\\') } $else { remaining << clean_path } for remaining.len > 0 { cpath := remaining.pop() pkind := kind_of_existing_path(cpath) if pkind.is_link || !pkind.is_dir { f(cpath) continue } mut files := ls(cpath) or { continue } for idx := files.len - 1; idx >= 0; idx-- { remaining << cpath + path_separator + files[idx] } } } // FnWalkContextCB is used to define the callback functions, passed to os.walk_context. pub type FnWalkContextCB = fn (voidptr, string) // walk_with_context traverses the given directory `path`. // For each encountered file *and* directory, it will call your `fcb` callback, // passing it the arbitrary `context` in its first parameter, // and the path to the file in its second parameter. // Note: walk_with_context can be called even for deeply nested folders, // since it does not recurse, but processes them iteratively. // For listing only one level deep, see: `os.ls` pub fn walk_with_context(path string, context voidptr, fcb FnWalkContextCB) { if path == '' { return } if !is_dir(path) { return } mut remaining := []string{cap: 1000} clean_path := path.trim_right(path_separator) $if windows { remaining << clean_path.replace('/', '\\') } $else { remaining << clean_path } mut loops := 0 for remaining.len > 0 { loops++ cpath := remaining.pop() // call `fcb` for everything, but the initial folder: if loops > 1 { fcb(context, cpath) } pkind := kind_of_existing_path(cpath) if pkind.is_link || !pkind.is_dir { continue } mut files := ls(cpath) or { continue } for idx := files.len - 1; idx >= 0; idx-- { remaining << cpath + path_separator + files[idx] } } } // log will print "os.log: "+`s` ... pub fn log(s string) { println('os.log: ' + s) } @[params] pub struct MkdirParams { pub: mode u32 = 0o777 // note that the actual mode is affected by the process's umask } // mkdir_all will create a valid full path of all directories given in `path`. pub fn mkdir_all(opath string, params MkdirParams) ! { if exists(opath) { if is_dir(opath) { return } return error('path `${opath}` already exists, and is not a folder') } other_separator := if path_separator == '/' { '\\' } else { '/' } path := opath.replace(other_separator, path_separator) mut p := if path.starts_with(path_separator) { path_separator } else { '' } path_parts := path.trim_left(path_separator).split(path_separator) for subdir in path_parts { p += subdir + path_separator if exists(p) && is_dir(p) { continue } mkdir(p, params) or { return error('folder: ${p}, error: ${err}') } } } fn create_folder_when_it_does_not_exist(path string) { if is_dir(path) || is_link(path) { return } mut error_msg := '' for _ in 0 .. 10 { mkdir_all(path, mode: 0o700) or { if is_dir(path) || is_link(path) { // A race had been won, and the `path` folder had been created, by another concurrent V program. // We are fine with that, since the folder now exists, even though this process did not create it. // We can just use it too ¯\_(ツ)_/¯ . return } error_msg = err.msg() sleep_ms(1) // wait a bit, before a retry, to let the other process finish its folder creation continue } break } if is_dir(path) || is_link(path) { return } // There was something wrong, that could not be solved, by just retrying // There is no choice, but to report it back :-\ panic(error_msg) } fn xdg_home_folder(ename string, lpath string) string { xdg_folder := getenv(ename) dir := if xdg_folder != '' { xdg_folder } else { join_path_single(home_dir(), lpath) } create_folder_when_it_does_not_exist(dir) return dir } // cache_dir returns the path to a *writable* user-specific folder, suitable for writing non-essential data. // See: https://specifications.freedesktop.org/basedir-spec/latest/ . // There is a single base directory relative to which user-specific non-essential // (cached) data should be written. This directory is defined by the environment // variable `$XDG_CACHE_HOME`. // `$XDG_CACHE_HOME` defines the base directory relative to which user specific // non-essential data files should be stored. If `$XDG_CACHE_HOME` is either not set // or empty, a default equal to `$HOME/.cache` should be used. pub fn cache_dir() string { return xdg_home_folder('XDG_CACHE_HOME', '.cache') } // data_dir returns the path to a *writable* user-specific folder, suitable for writing application data. // See: https://specifications.freedesktop.org/basedir-spec/latest/ . // There is a single base directory relative to which user-specific data files should be written. // This directory is defined by the environment variable `$XDG_DATA_HOME`. // If `$XDG_DATA_HOME` is either not set or empty, a default equal to // `$HOME/.local/share` should be used. pub fn data_dir() string { return xdg_home_folder('XDG_DATA_HOME', '.local/share') } // state_dir returns a *writable* folder user-specific folder. // It is suitable for storing state data, that should persist between (application) restarts, // but that is not important or portable enough to the user that it should be stored in os.data_dir(). // See: https://specifications.freedesktop.org/basedir-spec/latest/ . // `$XDG_STATE_HOME` defines the base directory relative to which user-specific state files should be stored. // If `$XDG_STATE_HOME` is either not set or empty, a default equal to // `$HOME/.local/state should be used`. // It may contain: // * actions history (logs, history, recently used files, …) // * current state of the application that can be reused on a restart (view, layout, open files, undo history, …) pub fn state_dir() string { return xdg_home_folder('XDG_STATE_HOME', '.local/state') } // local_bin_dir returns `$HOME/.local/bin`, which is *guaranteed* to be in the PATH of the current user. // It is compatible with stributions, following the XDG spec from https://specifications.freedesktop.org/basedir-spec/latest/ : // > User-specific executable files may be stored in `$HOME/.local/bin`. // > Distributions should ensure this directory shows up in the UNIX $PATH environment variable, at an appropriate place. pub fn local_bin_dir() string { return xdg_home_folder('LOCAL_BIN_DIR', '.local/bin') // provides a way to test by setting an env variable } // temp_dir returns the path to a folder, that is suitable for storing temporary files. pub fn temp_dir() string { mut path := getenv('TMPDIR') $if windows { if path == '' { // TODO: see Qt's implementation? // https://doc.qt.io/qt-5/qdir.html#tempPath // https://github.com/qt/qtbase/blob/e164d61ca8263fc4b46fdd916e1ea77c7dd2b735/src/corelib/io/qfilesystemengine_win.cpp#L1275 path = getenv('TEMP') if path == '' { path = getenv('TMP') } if path == '' { path = 'C:/tmp' } } path = get_long_path(path) or { path } } $if macos { // avoid /var/folders/6j/cmsk8gd90pd.... on macs return '/tmp' } $if android { // TODO: test+use '/data/local/tmp' on Android before using cache_dir() if path == '' { path = cache_dir() } } $if termux { path = '/data/data/com.termux/files/usr/tmp' } if path == '' { path = '/tmp' } return path } // vtmp_dir returns the path to a folder, that is writable to V programs, *and* specific to the OS user. // It can be overridden by setting the env variable `VTMP`. pub fn vtmp_dir() string { mut vtmp := getenv('VTMP') if vtmp.len > 0 { create_folder_when_it_does_not_exist(vtmp) return vtmp } uid := getuid() vtmp = join_path_single(temp_dir(), 'v_${uid}') create_folder_when_it_does_not_exist(vtmp) setenv('VTMP', vtmp, true) return vtmp } fn default_vmodules_path() string { hdir := home_dir() res := join_path_single(hdir, '.vmodules') return res } // vmodules_dir returns the path to a folder, where v stores its global modules. pub fn vmodules_dir() string { paths := vmodules_paths() if paths.len > 0 { return paths[0] } return default_vmodules_path() } // vmodules_paths returns a list of paths, where v looks up for modules. // You can customize it through setting the environment variable `VMODULES`. pub fn vmodules_paths() []string { mut path := getenv('VMODULES') if path == '' { // unsafe { path.free() } path = default_vmodules_path() } defer { // unsafe { path.free() } } splitted := path.split(path_delimiter) defer { // unsafe { splitted.free() } } mut list := []string{cap: splitted.len} for i in 0 .. splitted.len { si := splitted[i] trimmed := si.trim_right(path_separator) list << trimmed // unsafe { trimmed.free() } // unsafe { si.free() } } return list } // resource_abs_path returns an absolute path, for the given `path`. // (the path is expected to be relative to the executable program) // See https://discordapp.com/channels/592103645835821068/592294828432424960/630806741373943808 // It gives a convenient way to access program resources like images, fonts, sounds and so on, // *no matter* how the program was started, and what is the current working directory. @[manualfree] pub fn resource_abs_path(path string) string { exe := executable() dexe := dir(exe) mut base_path := real_path(dexe) vresource := getenv('V_RESOURCE_PATH') if vresource.len != 0 { unsafe { base_path.free() } base_path = vresource } fp := join_path_single(base_path, path) res := real_path(fp) unsafe { fp.free() vresource.free() base_path.free() dexe.free() exe.free() } return res } pub struct Uname { pub mut: sysname string nodename string release string version string machine string } // execute_or_panic returns the os.Result of executing `cmd`, or panic with its output on failure. pub fn execute_or_panic(cmd string) Result { res := execute(cmd) if res.exit_code != 0 { eprintln('failed cmd: ${cmd}') eprintln('failed code: ${res.exit_code}') panic(res.output) } return res } // execute_or_exit returns the os.Result of executing `cmd`, or exit with its output on failure. pub fn execute_or_exit(cmd string) Result { res := execute(cmd) if res.exit_code != 0 { eprintln('failed cmd: ${cmd}') eprintln('failed code: ${res.exit_code}') eprintln(res.output) exit(1) } return res } // execute_opt returns the os.Result of executing `cmd`, or an error with its output on failure. pub fn execute_opt(cmd string) !Result { res := execute(cmd) if res.exit_code != 0 { return error(res.output) } return res } // quoted path - return a quoted version of the path, depending on the platform. pub fn quoted_path(path string) string { $if windows { return if path.ends_with(path_separator) { '"${path + path_separator}"' } else { '"${path}"' } } $else { return "'${path}'" } } // config_dir returns the path to the user configuration directory (depending on the platform). // On Windows, that is `%AppData%`. // On macOS, that is `~/Library/Application Support`. // On the rest, that is `$XDG_CONFIG_HOME`, or if that is not available, `~/.config`. // If the path cannot be determined, it returns an error. // (for example, when `$HOME` on Linux, or `%AppData%` on Windows is not defined) pub fn config_dir() !string { $if windows { app_data := getenv('AppData') if app_data != '' { return app_data } } $else $if macos || darwin || ios { home := home_dir() if home != '' { return home + '/Library/Application Support' } } $else { xdg_home := getenv('XDG_CONFIG_HOME') if xdg_home != '' { return xdg_home } home := home_dir() if home != '' { return home + '/.config' } } return error('Cannot find config directory') } // Stat struct modeled on POSIX. pub struct Stat { pub: dev u64 // ID of device containing file inode u64 // Inode number mode u32 // File type and user/group/world permission bits nlink u64 // Number of hard links to file uid u32 // Owner user ID gid u32 // Owner group ID rdev u64 // Device ID (if special file) size u64 // Total size in bytes atime i64 // Last access (seconds since UNIX epoch) mtime i64 // Last modified (seconds since UNIX epoch) ctime i64 // Last status change (seconds since UNIX epoch) }