656 lines
16 KiB
V
656 lines
16 KiB
V
// Copyright (c) 2019-2021 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
|
|
|
|
pub const (
|
|
args = []string{}
|
|
max_path_len = 4096
|
|
wd_at_startup = getwd()
|
|
)
|
|
|
|
const (
|
|
f_ok = 0
|
|
x_ok = 1
|
|
w_ok = 2
|
|
r_ok = 4
|
|
)
|
|
|
|
pub struct Result {
|
|
pub:
|
|
exit_code int
|
|
output string
|
|
// stderr string // TODO
|
|
}
|
|
|
|
[unsafe]
|
|
pub fn (mut result Result) free() {
|
|
unsafe { result.output.free() }
|
|
}
|
|
|
|
// 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) {
|
|
adjusted_path := if is_dir(dest_path) {
|
|
join_path(dest_path, file_name(source_path))
|
|
} 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(source_path, file)
|
|
dp := join_path(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
|
|
}
|
|
}
|
|
}
|
|
|
|
// mv_by_cp first copies the source file, and if it is copied successfully, deletes the source file.
|
|
// may be used when you are not sure that the source and target are on the same mount/partition.
|
|
pub fn mv_by_cp(source string, target string) ? {
|
|
cp(source, target) ?
|
|
rm(source) ?
|
|
}
|
|
|
|
// read_lines reads the file in `path` into an array of lines.
|
|
pub fn read_lines(path string) ?[]string {
|
|
buf := read_file(path) ?
|
|
res := buf.split_into_lines()
|
|
unsafe { buf.free() }
|
|
return res
|
|
}
|
|
|
|
// 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(path, item)
|
|
if is_dir(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.
|
|
pub fn is_dir_empty(path string) bool {
|
|
items := ls(path) or { return true }
|
|
return items.len == 0
|
|
}
|
|
|
|
// file_ext will return the part after the last occurence of `.` in `path`.
|
|
// The `.` is included.
|
|
pub fn file_ext(path string) string {
|
|
pos := path.last_index('.') or { 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 '.'
|
|
}
|
|
pos := path.last_index(path_separator) or { return '.' }
|
|
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 '.'
|
|
}
|
|
if path == path_separator {
|
|
return path_separator
|
|
}
|
|
if path.ends_with(path_separator) {
|
|
path2 := path[..path.len - 1]
|
|
pos := path2.last_index(path_separator) or { return path2.clone() }
|
|
return path2[pos + 1..]
|
|
}
|
|
pos := path.last_index(path_separator) or { return path.clone() }
|
|
return path[pos + 1..]
|
|
}
|
|
|
|
// file_name will return all characters found after the last occurence of `path_separator`.
|
|
// file extension is included.
|
|
pub fn file_name(path string) string {
|
|
return path.all_after_last(path_separator)
|
|
}
|
|
|
|
// input_opt returns a one-line string from stdin, after printing a prompt.
|
|
// In the event of error (end of input), it returns `none`.
|
|
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.
|
|
// In the event of error (end of input), it returns '<EOF>'.
|
|
pub fn input(prompt string) string {
|
|
res := input_opt(prompt) or { return '<EOF>' }
|
|
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 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 from stdin.
|
|
// reading is stopped when an empty line is read.
|
|
pub fn get_lines_joined() string {
|
|
mut line := ''
|
|
mut inputstr := ''
|
|
for {
|
|
line = get_line()
|
|
if line.len <= 0 {
|
|
break
|
|
}
|
|
line = line.trim_space()
|
|
inputstr += line
|
|
}
|
|
return inputstr
|
|
}
|
|
|
|
// get_raw_lines_joined reads *all* input lines from stdin.
|
|
// It returns them as one large string. NB: 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 {
|
|
mut line := ''
|
|
mut lines := []string{}
|
|
for {
|
|
line = get_raw_line()
|
|
if line.len <= 0 {
|
|
break
|
|
}
|
|
lines << line
|
|
}
|
|
res := lines.join('')
|
|
return res
|
|
}
|
|
|
|
// user_os returns current user 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 solaris {
|
|
return 'solaris'
|
|
}
|
|
$if haiku {
|
|
return 'haiku'
|
|
}
|
|
$if serenity {
|
|
return 'serenity'
|
|
}
|
|
$if vinix {
|
|
return 'vinix'
|
|
}
|
|
return 'unknown'
|
|
}
|
|
|
|
// home_dir returns path to the user's home directory.
|
|
pub fn home_dir() string {
|
|
$if windows {
|
|
return getenv('USERPROFILE')
|
|
} $else {
|
|
// println('home_dir() call')
|
|
// res:= os.getenv('HOME')
|
|
// println('res="$res"')
|
|
return getenv('HOME')
|
|
}
|
|
}
|
|
|
|
// write_file writes `text` data to a file in `path`.
|
|
pub fn write_file(path string, text string) ? {
|
|
mut f := create(path) ?
|
|
unsafe { f.write_full_buffer(text.str, size_t(text.len)) ? }
|
|
f.close()
|
|
}
|
|
|
|
// write_file_array writes the data in `buffer` to a file in `path`.
|
|
pub fn write_file_array(path string, buffer array) ? {
|
|
mut f := create(path) ?
|
|
unsafe { f.write_full_buffer(buffer.data, size_t(buffer.len * buffer.element_size)) ? }
|
|
f.close()
|
|
}
|
|
|
|
// 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 os.args.len == 0 {
|
|
// we are early in the bootstrap, os.args has not been initialized yet :-|
|
|
return ''
|
|
}
|
|
mut exepath := os.args[0]
|
|
$if windows {
|
|
if !exepath.contains('.exe') {
|
|
exepath += '.exe'
|
|
}
|
|
}
|
|
if !is_abs_path(exepath) {
|
|
if exepath.contains(path_separator) {
|
|
exepath = join_path(os.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.len > 0 {
|
|
exepath = foundpath
|
|
}
|
|
}
|
|
}
|
|
exepath = real_path(exepath)
|
|
return exepath
|
|
}
|
|
|
|
// find_exe_path walks the environment PATH, just like most shell do, it returns
|
|
// the absolute path of the executable if found
|
|
pub fn find_abs_path_of_executable(exepath string) ?string {
|
|
if exepath == '' {
|
|
return error('expected non empty `exepath`')
|
|
}
|
|
if is_abs_path(exepath) {
|
|
return real_path(exepath)
|
|
}
|
|
mut res := ''
|
|
paths := getenv('PATH').split(path_delimiter)
|
|
for p in paths {
|
|
found_abs_path := join_path(p, exepath)
|
|
if exists(found_abs_path) && is_executable(found_abs_path) {
|
|
res = found_abs_path
|
|
break
|
|
}
|
|
}
|
|
if res.len > 0 {
|
|
return real_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)
|
|
}
|
|
|
|
// is_abs_path returns `true` if `path` is absolute.
|
|
pub fn is_abs_path(path string) bool {
|
|
if path.len == 0 {
|
|
return false
|
|
}
|
|
$if windows {
|
|
return path[0] == `/` || // incase we're in MingGW bash
|
|
(path[0].is_letter() && path.len > 1 && path[1] == `:`)
|
|
}
|
|
return path[0] == `/`
|
|
}
|
|
|
|
// join_path returns a path as string from input string parameter(s).
|
|
[manualfree]
|
|
pub fn join_path(base string, dirs ...string) string {
|
|
mut result := []string{}
|
|
result << base.trim_right('\\/')
|
|
for d in dirs {
|
|
result << d
|
|
}
|
|
res := result.join(path_separator)
|
|
unsafe { result.free() }
|
|
return res
|
|
}
|
|
|
|
// walk_ext returns a recursive list of all files in `path` ending with `ext`.
|
|
pub fn walk_ext(path string, ext string) []string {
|
|
if !is_dir(path) {
|
|
return []
|
|
}
|
|
mut files := ls(path) or { return [] }
|
|
mut res := []string{}
|
|
separator := if path.ends_with(path_separator) { '' } else { path_separator }
|
|
for file in files {
|
|
if file.starts_with('.') {
|
|
continue
|
|
}
|
|
p := path + separator + file
|
|
if is_dir(p) && !is_link(p) {
|
|
res << walk_ext(p, ext)
|
|
} else if file.ends_with(ext) {
|
|
res << p
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
// walk recursively traverses the given directory `path`.
|
|
// When a file is encountred it will call the callback function with current file as argument.
|
|
pub fn walk(path string, f fn (string)) {
|
|
if !is_dir(path) {
|
|
return
|
|
}
|
|
mut files := ls(path) or { return }
|
|
mut local_path_separator := path_separator
|
|
if path.ends_with(path_separator) {
|
|
local_path_separator = ''
|
|
}
|
|
for file in files {
|
|
p := path + local_path_separator + file
|
|
if is_dir(p) && !is_link(p) {
|
|
walk(p, f)
|
|
} else if exists(p) {
|
|
f(p)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// log will print "os.log: "+`s` ...
|
|
pub fn log(s string) {
|
|
//$if macos {
|
|
// Use NSLog() on macos
|
|
// C.darwin_log(s)
|
|
//} $else {
|
|
println('os.log: ' + s)
|
|
//}
|
|
}
|
|
|
|
// mkdir_all will create a valid full path of all directories given in `path`.
|
|
pub fn mkdir_all(path string) ? {
|
|
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) or { return error('folder: $p, error: $err') }
|
|
}
|
|
}
|
|
|
|
// cache_dir returns the path to a *writable* user specific folder, suitable for writing non-essential data.
|
|
pub fn cache_dir() string {
|
|
// See: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
|
// 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.
|
|
$if !windows {
|
|
xdg_cache_home := getenv('XDG_CACHE_HOME')
|
|
if xdg_cache_home != '' {
|
|
return xdg_cache_home
|
|
}
|
|
}
|
|
cdir := join_path(home_dir(), '.cache')
|
|
if !is_dir(cdir) && !is_link(cdir) {
|
|
mkdir(cdir) or { panic(err) }
|
|
}
|
|
return cdir
|
|
}
|
|
|
|
// 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'
|
|
}
|
|
}
|
|
}
|
|
$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 path == '' {
|
|
path = '/tmp'
|
|
}
|
|
return path
|
|
}
|
|
|
|
fn default_vmodules_path() string {
|
|
return join_path(home_dir(), '.vmodules')
|
|
}
|
|
|
|
// 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 == '' {
|
|
path = default_vmodules_path()
|
|
}
|
|
list := path.split(path_delimiter).map(it.trim_right(path_separator))
|
|
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 {
|
|
base_path = vresource
|
|
}
|
|
fp := join_path(base_path, path)
|
|
res := real_path(fp)
|
|
unsafe {
|
|
fp.free()
|
|
base_path.free()
|
|
}
|
|
return res
|
|
}
|
|
|
|
pub struct Uname {
|
|
pub mut:
|
|
sysname string
|
|
nodename string
|
|
release string
|
|
version string
|
|
machine string
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// is_atty returns 1 if the `fd` file descriptor is open and refers to a terminal
|
|
pub fn is_atty(fd int) int {
|
|
$if windows {
|
|
mut mode := u32(0)
|
|
osfh := voidptr(C._get_osfhandle(fd))
|
|
C.GetConsoleMode(osfh, voidptr(&mode))
|
|
return int(mode)
|
|
} $else {
|
|
return C.isatty(fd)
|
|
}
|
|
}
|
|
|
|
pub fn glob(patterns ...string) ?[]string {
|
|
mut matches := []string{}
|
|
for pattern in patterns {
|
|
native_glob_pattern(pattern, mut matches) ?
|
|
}
|
|
matches.sort()
|
|
return matches
|
|
}
|