v/vlib/os/os.v

682 lines
17 KiB
V
Raw Normal View History

// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved.
2019-06-23 04:21:30 +02:00
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
2019-06-22 20:20:28 +02:00
module os
2019-10-24 11:36:57 +02:00
pub const (
max_path_len = 4096
2020-12-15 08:58:33 +01:00
wd_at_startup = getwd()
2019-06-22 20:20:28 +02:00
)
2020-12-15 08:58:33 +01:00
const (
f_ok = 0
x_ok = 1
w_ok = 2
r_ok = 4
)
2019-10-31 22:57:16 +01:00
2021-04-04 16:05:06 +02:00
pub struct Result {
pub:
exit_code int
output string
// stderr string // TODO
}
[unsafe]
pub fn (mut result Result) free() {
unsafe { result.output.free() }
}
2020-08-27 16:57:01 +02:00
// 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) ? {
2020-12-15 08:33:31 +01:00
source_path := real_path(src)
dest_path := real_path(dst)
if !exists(source_path) {
2021-01-13 06:05:27 +01:00
return error("Source path doesn't exist")
2019-11-06 21:05:35 +01:00
}
2019-12-19 22:29:37 +01:00
// single file copy
2020-12-15 08:33:31 +01:00
if !is_dir(source_path) {
adjusted_path := if is_dir(dest_path) {
join_path(dest_path, file_name(source_path))
} else {
dest_path
}
2020-12-15 08:33:31 +01:00
if exists(adjusted_path) {
if overwrite {
rm(adjusted_path) ?
2020-12-15 08:33:31 +01:00
} else {
2019-11-19 00:25:55 +01:00
return error('Destination file path already exist')
}
2019-11-06 21:05:35 +01:00
}
2020-12-15 08:33:31 +01:00
cp(source_path, adjusted_path) ?
2020-06-29 15:06:26 +02:00
return
2019-11-06 21:05:35 +01:00
}
if !exists(dest_path) {
mkdir(dest_path) ?
}
2020-12-15 08:33:31 +01:00
if !is_dir(dest_path) {
2019-11-06 21:05:35 +01:00
return error('Destination path is not a valid directory')
}
2020-12-15 08:33:31 +01:00
files := ls(source_path) ?
2019-11-06 21:05:35 +01:00
for file in files {
2020-12-15 08:33:31 +01:00
sp := join_path(source_path, file)
dp := join_path(dest_path, file)
if is_dir(sp) {
2021-02-12 00:55:36 +01:00
if !exists(dp) {
mkdir(dp) ?
}
2019-11-06 21:05:35 +01:00
}
2020-03-02 19:30:04 +01:00
cp_all(sp, dp, overwrite) or {
rmdir(dp) or { return err }
return err
2019-11-06 21:05:35 +01:00
}
}
}
// mv_by_cp first copies the source file, and if it is copied successfully, deletes the source file.
2020-08-27 16:57:01 +02:00
// may be used when you are not sure that the source and target are on the same mount/partition.
2020-06-25 12:06:47 +02:00
pub fn mv_by_cp(source string, target string) ? {
2020-12-15 08:33:31 +01:00
cp(source, target) ?
rm(source) ?
}
// read_lines reads the file in `path` into an array of lines.
[manualfree]
2019-11-19 00:25:55 +01:00
pub fn read_lines(path string) ?[]string {
2020-12-15 08:33:31 +01:00
buf := read_file(path) ?
2021-03-22 10:20:55 +01:00
res := buf.split_into_lines()
unsafe { buf.free() }
return res
2019-06-22 20:20:28 +02:00
}
2020-08-27 16:57:01 +02:00
// 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 {
2020-12-15 08:33:31 +01:00
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
2020-12-15 08:33:31 +01:00
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' }
2019-12-19 22:29:37 +01:00
// /////////////////////////////
2020-12-15 08:33:31 +01:00
5 { return 'SIGTRAP' }
7 { return 'SIGBUS' }
else {}
2020-01-21 16:58:47 +01:00
}
}
return 'unknown'
}
2020-08-27 16:57:01 +02:00
// rmdir_all recursively removes the specified directory.
pub fn rmdir_all(path string) ? {
mut ret_err := ''
2020-12-15 08:33:31 +01:00
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)
}
}
2020-08-27 16:57:01 +02:00
// is_dir_empty will return a `bool` whether or not `path` is empty.
pub fn is_dir_empty(path string) bool {
2020-12-15 08:33:31 +01:00
items := ls(path) or { return true }
return items.len == 0
}
2020-08-27 16:57:01 +02:00
// file_ext will return the part after the last occurence of `.` in `path`.
// The `.` is included.
2020-03-26 14:18:08 +01:00
pub fn file_ext(path string) string {
2020-12-15 08:33:31 +01:00
pos := path.last_index('.') or { return '' }
2020-03-07 22:26:26 +01:00
return path[pos..]
2019-06-26 11:28:06 +02:00
}
// 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(opath string) string {
if opath == '' {
return '.'
}
path := opath.replace_each(['/', path_separator, r'\', path_separator])
2020-12-15 08:33:31 +01:00
pos := path.last_index(path_separator) or { return '.' }
if pos == 0 && path_separator == '/' {
return '/'
}
2020-03-07 22:26:26 +01:00
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(opath string) string {
if opath == '' {
return '.'
}
path := opath.replace_each(['/', path_separator, r'\', path_separator])
if path == path_separator {
return path_separator
}
if path.ends_with(path_separator) {
2020-12-15 08:33:31 +01:00
path2 := path[..path.len - 1]
pos := path2.last_index(path_separator) or { return path2.clone() }
return path2[pos + 1..]
2020-03-07 22:26:26 +01:00
}
2020-12-15 08:33:31 +01:00
pos := path.last_index(path_separator) or { return path.clone() }
return path[pos + 1..]
2019-06-22 20:20:28 +02:00
}
2020-08-27 16:57:01 +02:00
// file_name will return all characters found after the last occurence of `path_separator`.
// file extension is included.
pub fn file_name(opath string) string {
path := opath.replace_each(['/', path_separator, r'\', path_separator])
return path.all_after_last(path_separator)
2019-06-22 20:20:28 +02:00
}
// 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 {
2020-04-25 21:03:51 +02:00
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
2020-04-25 21:03:51 +02:00
}
2019-08-22 22:19:31 +02:00
// get_line returns a one-line string from stdin
2019-06-22 20:20:28 +02:00
pub fn get_line() string {
2019-12-19 22:29:37 +01:00
str := get_raw_line()
2019-09-15 18:07:40 +02:00
$if windows {
return str.trim_right('\r\n')
}
return str.trim_right('\n')
2019-06-22 20:20:28 +02:00
}
2020-08-27 16:57:01 +02:00
// get_lines returns an array of strings read from from stdin.
// reading is stopped when an empty line is read.
2019-07-24 18:14:13 +02:00
pub fn get_lines() []string {
2019-12-19 22:29:37 +01:00
mut line := ''
mut inputstr := []string{}
2019-12-19 22:29:37 +01:00
for {
line = get_line()
if line.len <= 0 {
2019-12-19 22:29:37 +01:00
break
}
line = line.trim_space()
inputstr << line
}
return inputstr
2019-07-24 18:14:13 +02:00
}
2020-08-27 16:57:01 +02:00
// get_lines_joined returns a string of the values read from from stdin.
// reading is stopped when an empty line is read.
2019-07-24 18:14:13 +02:00
pub fn get_lines_joined() string {
2019-10-26 19:21:07 +02:00
mut line := ''
mut inputstr := ''
for {
line = get_line()
if line.len <= 0 {
break
}
line = line.trim_space()
inputstr += line
}
return inputstr
2019-07-24 18:14:13 +02:00
}
// 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
}
2019-10-26 19:21:07 +02:00
// user_os returns current user operating system name.
2019-06-22 20:20:28 +02:00
pub fn user_os() string {
$if linux {
return 'linux'
}
2019-12-03 14:29:24 +01:00
$if macos {
return 'macos'
2019-06-22 20:20:28 +02:00
}
$if windows {
return 'windows'
}
2019-07-15 17:24:40 +02:00
$if freebsd {
2019-08-22 22:19:31 +02:00
return 'freebsd'
}
2019-07-21 12:22:41 +02:00
$if openbsd {
2019-08-22 22:19:31 +02:00
return 'openbsd'
}
2019-07-21 12:22:41 +02:00
$if netbsd {
2019-08-22 22:19:31 +02:00
return 'netbsd'
}
2019-07-21 12:22:41 +02:00
$if dragonfly {
2019-08-22 22:19:31 +02:00
return 'dragonfly'
}
2019-12-19 22:29:37 +01:00
$if android {
return 'android'
}
2019-09-26 23:30:41 +02:00
$if solaris {
return 'solaris'
}
2019-12-03 09:26:47 +01:00
$if haiku {
return 'haiku'
}
2021-07-13 10:37:33 +02:00
$if serenity {
return 'serenity'
}
$if vinix {
return 'vinix'
}
2019-06-22 20:20:28 +02:00
return 'unknown'
}
2020-12-15 08:58:33 +01:00
// home_dir returns path to the user's home directory.
2019-06-22 20:20:28 +02:00
pub fn home_dir() string {
$if windows {
2020-12-15 08:33:31 +01:00
return getenv('USERPROFILE')
} $else {
2020-12-15 08:33:31 +01:00
// println('home_dir() call')
// res:= os.getenv('HOME')
// println('res="$res"')
return getenv('HOME')
2019-06-22 20:20:28 +02:00
}
}
// 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 == '~' {
return home_dir().trim_right(path_separator)
}
if path.starts_with('~' + path_separator) {
return path.replace_once('~' + path_separator, home_dir().trim_right(path_separator) +
path_separator)
}
return path
}
2019-08-22 22:19:31 +02:00
// write_file writes `text` data to a file in `path`.
pub fn write_file(path string, text string) ? {
2020-12-15 08:33:31 +01:00
mut f := create(path) ?
2021-09-08 12:09:32 +02:00
unsafe { f.write_full_buffer(text.str, usize(text.len)) ? }
2019-06-22 20:20:28 +02:00
f.close()
}
2020-08-27 16:57:01 +02:00
// 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
2020-03-13 22:19:02 +01:00
// 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'
}
}
2020-12-15 08:33:31 +01:00
if !is_abs_path(exepath) {
rexepath := exepath.replace_each(['/', path_separator, r'\', path_separator])
if rexepath.contains(path_separator) {
2021-01-25 10:26:20 +01:00
exepath = join_path(os.wd_at_startup, exepath)
2020-12-15 08:33:31 +01:00
} else {
2020-03-13 22:19:02 +01:00
// no choice but to try to walk the PATH folders :-| ...
2020-12-15 08:33:31 +01:00
foundpath := find_abs_path_of_executable(exepath) or { '' }
2020-03-13 22:19:02 +01:00
if foundpath.len > 0 {
exepath = foundpath
}
}
}
2020-12-15 08:33:31 +01:00
exepath = real_path(exepath)
2020-03-13 22:19:02 +01:00
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`')
}
2020-12-15 08:33:31 +01:00
if is_abs_path(exepath) {
return real_path(exepath)
2020-03-13 22:19:02 +01:00
}
mut res := ''
2020-12-15 08:33:31 +01:00
paths := getenv('PATH').split(path_delimiter)
2020-03-13 22:19:02 +01:00
for p in paths {
2020-12-15 08:33:31 +01:00
found_abs_path := join_path(p, exepath)
if exists(found_abs_path) && is_executable(found_abs_path) {
2020-03-13 22:19:02 +01:00
res = found_abs_path
break
}
}
2020-12-15 08:33:31 +01:00
if res.len > 0 {
return real_path(res)
2020-03-13 22:19:02 +01:00
}
return error('failed to find executable')
}
2020-08-27 16:57:01 +02:00
// exists_in_system_path returns `true` if `prog` exists in the system's PATH
pub fn exists_in_system_path(prog string) bool {
2020-12-15 08:33:31 +01:00
find_abs_path_of_executable(prog) or { return false }
return true
}
2020-09-04 22:27:52 +02:00
// 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)
}
2020-08-27 16:57:01 +02:00
// is_abs_path returns `true` if `path` is absolute.
2020-03-10 16:09:37 +01:00
pub fn is_abs_path(path string) bool {
2021-05-09 09:45:34 +02:00
if path.len == 0 {
return false
}
2020-03-07 22:26:26 +01:00
$if windows {
2021-01-23 10:33:53 +01:00
return path[0] == `/` || // incase we're in MingGW bash
(path[0].is_letter() && path.len > 1 && path[1] == `:`)
2020-03-07 22:26:26 +01:00
}
return path[0] == `/`
}
2020-08-27 16:57:01 +02:00
// join_path returns a path as string from input string parameter(s).
[manualfree]
2020-03-09 02:23:34 +01:00
pub fn join_path(base string, dirs ...string) string {
mut result := []string{}
2020-03-07 22:26:26 +01:00
result << base.trim_right('\\/')
for d in dirs {
result << d
}
res := result.join(path_separator)
unsafe { result.free() }
return res
2020-03-07 22:26:26 +01:00
}
2020-08-27 16:57:01 +02:00
// walk_ext returns a recursive list of all files in `path` ending with `ext`.
pub fn walk_ext(path string, ext string) []string {
mut res := []string{}
impl_walk_ext(path, ext, mut res)
return res
}
2021-10-24 17:48:54 +02:00
fn impl_walk_ext(path string, ext string, mut out []string) {
2020-12-15 08:33:31 +01:00
if !is_dir(path) {
return
2019-12-19 22:29:37 +01:00
}
mut files := ls(path) or { return }
2020-12-15 08:33:31 +01:00
separator := if path.ends_with(path_separator) { '' } else { path_separator }
for file in files {
2019-08-16 14:05:11 +02:00
if file.starts_with('.') {
2019-08-22 22:19:31 +02:00
continue
}
p := path + separator + file
2020-12-15 08:33:31 +01:00
if is_dir(p) && !is_link(p) {
impl_walk_ext(p, ext, mut out)
2020-12-15 08:33:31 +01:00
} else if file.ends_with(ext) {
out << p
2019-08-22 22:19:31 +02:00
}
}
}
// walk traverses the given directory `path`.
// When a file is encountred it will call the
// callback function `f` with current file as argument.
2020-12-15 08:33:31 +01:00
pub fn walk(path string, f fn (string)) {
if path.len == 0 {
return
}
2020-12-15 08:33:31 +01:00
if !is_dir(path) {
2020-02-21 20:14:01 +01:00
return
2019-12-19 22:29:37 +01:00
}
2020-12-15 08:33:31 +01:00
mut files := ls(path) or { return }
mut local_path_separator := path_separator
if path.ends_with(path_separator) {
local_path_separator = ''
}
2019-10-24 14:17:09 +02:00
for file in files {
p := path + local_path_separator + file
2020-12-15 08:33:31 +01:00
if is_dir(p) && !is_link(p) {
2020-02-11 10:26:46 +01:00
walk(p, f)
2020-12-15 08:33:31 +01:00
} else if exists(p) {
2020-02-11 10:26:46 +01:00
f(p)
2019-10-24 14:17:09 +02:00
}
}
return
}
// 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 encountred file, 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.
pub fn walk_with_context(path string, context voidptr, fcb FnWalkContextCB) {
if path.len == 0 {
return
}
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_with_context(p, context, fcb)
} else {
fcb(context, p)
}
}
return
}
2020-08-27 16:57:01 +02:00
// log will print "os.log: "+`s` ...
pub fn log(s string) {
2021-01-23 10:32:00 +01:00
println('os.log: ' + s)
}
2020-08-27 16:57:01 +02:00
// mkdir_all will create a valid full path of all directories given in `path`.
pub fn mkdir_all(opath string) ? {
path := opath.replace('/', path_separator)
2020-12-15 08:33:31 +01:00
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 {
2020-12-15 08:33:31 +01:00
p += subdir + path_separator
if exists(p) && is_dir(p) {
continue
}
2020-12-15 08:33:31 +01:00
mkdir(p) or { return error('folder: $p, error: $err') }
}
}
2019-10-15 17:08:46 +02:00
2020-03-08 15:57:47 +01:00
// 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 {
2020-12-15 08:33:31 +01:00
xdg_cache_home := getenv('XDG_CACHE_HOME')
if xdg_cache_home != '' {
return xdg_cache_home
}
}
2020-12-15 08:33:31 +01:00
cdir := join_path(home_dir(), '.cache')
if !is_dir(cdir) && !is_link(cdir) {
mkdir(cdir) or { panic(err) }
}
return cdir
}
2020-08-27 16:57:01 +02:00
// temp_dir returns the path to a folder, that is suitable for storing temporary files.
2020-03-10 15:02:09 +01:00
pub fn temp_dir() string {
2020-12-15 08:33:31 +01:00
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
2020-12-15 08:33:31 +01:00
path = getenv('TEMP')
2019-12-19 22:29:37 +01:00
if path == '' {
2020-12-15 08:33:31 +01:00
path = getenv('TMP')
2019-12-19 22:29:37 +01:00
}
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 == '' {
2020-12-15 08:33:31 +01:00
path = cache_dir()
}
}
2019-12-30 05:25:26 +01:00
if path == '' {
path = '/tmp'
}
return path
}
2019-11-24 04:27:02 +01:00
fn default_vmodules_path() string {
2020-12-15 08:33:31 +01:00
return join_path(home_dir(), '.vmodules')
}
2020-12-15 08:33:31 +01:00
// 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]
}
2020-12-15 08:33:31 +01:00
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 {
2020-12-15 08:33:31 +01:00
mut path := getenv('VMODULES')
if path == '' {
2020-12-15 08:33:31 +01:00
path = default_vmodules_path()
}
2020-12-15 08:33:31 +01:00
list := path.split(path_delimiter).map(it.trim_right(path_separator))
return list
}
2020-08-27 16:57:01 +02:00
// 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)
2020-12-15 08:33:31 +01:00
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
}
2020-06-14 15:46:30 +02:00
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
}