v2 / vlib / os / os.v
1254 lines · 1173 sloc · 32.58 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module os
5
6import strings
7
8// Eof error means that we reach the end of the file.
9pub struct Eof {
10 Error
11}
12
13pub const max_path_len = 4096
14
15pub const wd_at_startup = getwd()
16
17const f_ok = 0
18
19const x_ok = 1
20
21const w_ok = 2
22
23const r_ok = 4
24
25const read_lines_chunk_size = 128 * 1024
26
27pub struct Result {
28pub:
29 exit_code int
30 output string
31 // stderr string // TODO
32}
33
34@[unsafe]
35pub fn (mut result Result) free() {
36 unsafe { result.output.free() }
37}
38
39// executable_fallback is used when there is not a more platform specific and accurate implementation.
40// It relies on path manipulation of os.args[0] and os.wd_at_startup, so it may not work properly in
41// all cases, but it should be better, than just using os.args[0] directly.
42fn executable_fallback() string {
43 if args.len == 0 {
44 // we are early in the bootstrap, os.args has not been initialized yet :-|
45 return ''
46 }
47 mut exepath := args[0]
48 $if windows {
49 if !exepath.contains('.exe') {
50 exepath += '.exe'
51 }
52 }
53 if !is_abs_path(exepath) {
54 other_separator := $if windows { '/' } $else { '\\' }
55 rexepath := exepath.replace(other_separator, path_separator)
56 if rexepath.contains(path_separator) {
57 exepath = join_path_single(wd_at_startup, exepath)
58 } else {
59 // no choice but to try to walk the PATH folders :-| ...
60 foundpath := find_abs_path_of_executable(exepath) or { '' }
61 if foundpath != '' {
62 exepath = foundpath
63 }
64 }
65 }
66 exepath = real_path(exepath)
67 return exepath
68}
69
70// cp_all will recursively copy `src` to `dst`, optionally overwriting files or dirs in `dst`.
71pub fn cp_all(src string, dst string, overwrite bool) ! {
72 source_path := real_path(src)
73 dest_path := real_path(dst)
74 if !exists(source_path) {
75 return error("Source path doesn't exist")
76 }
77 // single file copy
78 if !is_dir(source_path) {
79 fname := file_name(source_path)
80 adjusted_path := if is_dir(dest_path) {
81 join_path_single(dest_path, fname)
82 } else {
83 dest_path
84 }
85 if exists(adjusted_path) {
86 if overwrite {
87 rm(adjusted_path)!
88 } else {
89 return error('Destination file path already exist')
90 }
91 }
92 cp(source_path, adjusted_path)!
93 return
94 }
95 if !exists(dest_path) {
96 mkdir(dest_path)!
97 }
98 if !is_dir(dest_path) {
99 return error('Destination path is not a valid directory')
100 }
101 files := ls(source_path)!
102 for file in files {
103 sp := join_path_single(source_path, file)
104 dp := join_path_single(dest_path, file)
105 if is_dir(sp) {
106 if !exists(dp) {
107 mkdir(dp)!
108 }
109 }
110 cp_all(sp, dp, overwrite) or {
111 rmdir(dp) or { return err }
112 return err
113 }
114 }
115}
116
117@[params]
118pub struct MvParams {
119pub:
120 overwrite bool = true
121}
122
123// mv_by_cp copies files or folders from `source` to `target`.
124// If copying is successful, `source` is deleted.
125// It may be used when the paths are not on the same mount/partition.
126pub fn mv_by_cp(source string, target string, opts MvParams) ! {
127 cp_all(source, target, opts.overwrite)!
128 if is_dir(source) {
129 rmdir_all(source)!
130 return
131 }
132 rm(source)!
133}
134
135// mv moves files or folders from `src` to `dst`.
136pub fn mv(source string, target string, opts MvParams) ! {
137 if !opts.overwrite && exists(target) {
138 return error('target path already exist')
139 }
140 rename(source, target) or { mv_by_cp(source, target, opts)! }
141}
142
143// read_lines reads the file in `path` into an array of lines.
144@[manualfree]
145pub fn read_lines(path string) ![]string {
146 mut file := open(path)!
147 defer {
148 file.close()
149 }
150 return read_lines_from_open_file(mut file)
151}
152
153@[manualfree]
154fn read_lines_from_open_file(mut file File) ![]string {
155 mut buf := []u8{len: read_lines_chunk_size}
156 mut lines := []string{}
157 mut line := strings.new_builder(read_lines_chunk_size)
158 mut pending_cr := false
159 for {
160 nread := file.read(mut buf) or {
161 if err is Eof {
162 break
163 }
164 unsafe {
165 line.free()
166 lines.free()
167 }
168 return err
169 }
170 if nread <= 0 {
171 break
172 }
173 mut segment_start := 0
174 for i := 0; i < nread; i++ {
175 c := buf[i]
176 if pending_cr {
177 pending_cr = false
178 if c == `\n` {
179 segment_start = i + 1
180 continue
181 }
182 }
183 if c == `\n` || c == `\r` {
184 if i > segment_start {
185 line << buf[segment_start..i]
186 }
187 lines << line.str()
188 segment_start = i + 1
189 pending_cr = c == `\r`
190 }
191 }
192 if segment_start < nread {
193 line << buf[segment_start..nread]
194 }
195 }
196 if line.len > 0 {
197 lines << line.str()
198 }
199 unsafe { line.free() }
200 return lines
201}
202
203// write_lines writes the given array of `lines` to `path`.
204// The lines are separated by `\n` .
205pub fn write_lines(path string, lines []string) ! {
206 mut f := create(path)!
207 defer {
208 f.close()
209 }
210 for line in lines {
211 f.writeln(line)!
212 }
213}
214
215// sigint_to_signal_name will translate `si` signal integer code to it's string code representation.
216pub fn sigint_to_signal_name(si int) string {
217 // POSIX signals:
218 match si {
219 1 { return 'SIGHUP' }
220 2 { return 'SIGINT' }
221 3 { return 'SIGQUIT' }
222 4 { return 'SIGILL' }
223 6 { return 'SIGABRT' }
224 8 { return 'SIGFPE' }
225 9 { return 'SIGKILL' }
226 11 { return 'SIGSEGV' }
227 13 { return 'SIGPIPE' }
228 14 { return 'SIGALRM' }
229 15 { return 'SIGTERM' }
230 else {}
231 }
232
233 $if linux {
234 // From `man 7 signal` on linux:
235 match si {
236 // TODO: dependent on platform
237 // works only on x86/ARM/most others
238 10 { // , 30, 16
239 return 'SIGUSR1'
240 }
241 12 { // , 31, 17
242 return 'SIGUSR2'
243 }
244 17 { // , 20, 18
245 return 'SIGCHLD'
246 }
247 18 { // , 19, 25
248 return 'SIGCONT'
249 }
250 19 { // , 17, 23
251 return 'SIGSTOP'
252 }
253 20 { // , 18, 24
254 return 'SIGTSTP'
255 }
256 21 { // , 26
257 return 'SIGTTIN'
258 }
259 22 { // , 27
260 return 'SIGTTOU'
261 }
262 // /////////////////////////////
263 5 {
264 return 'SIGTRAP'
265 }
266 7 {
267 return 'SIGBUS'
268 }
269 else {}
270 }
271 }
272 return 'unknown'
273}
274
275// rmdir_all recursively removes the specified directory.
276pub fn rmdir_all(path string) ! {
277 mut err_msg := ''
278 mut err_code := -1
279 items := ls(path)!
280 for item in items {
281 fullpath := join_path_single(path, item)
282 if is_dir(fullpath) && !is_link(fullpath) {
283 rmdir_all(fullpath) or {
284 err_msg = err.msg()
285 err_code = err.code()
286 }
287 } else {
288 rm(fullpath) or {
289 err_msg = err.msg()
290 err_code = err.code()
291 }
292 }
293 }
294 rmdir(path) or {
295 err_msg = err.msg()
296 err_code = err.code()
297 }
298 if err_msg != '' {
299 return error_with_code(err_msg, err_code)
300 }
301}
302
303// is_dir_empty will return a `bool` whether or not `path` is empty.
304// Note that it will return `true` if `path` does not exist.
305@[manualfree]
306pub fn is_dir_empty(path string) bool {
307 items := ls(path) or { return true }
308 res := items.len == 0
309 unsafe { items.free() }
310 return res
311}
312
313// file_ext will return the part after the last occurrence of `.` in `path`.
314// The `.` is included.
315// Examples:
316// ```v
317// assert os.file_ext('file.v') == '.v'
318// assert os.file_ext('.ignore_me') == ''
319// assert os.file_ext('.') == ''
320// ```
321pub fn file_ext(opath string) string {
322 if opath.len < 3 {
323 return ''
324 }
325 path := file_name(opath)
326 pos := path.last_index_u8(`.`)
327 if pos == -1 {
328 return ''
329 }
330 if pos + 1 >= path.len || pos == 0 {
331 return ''
332 }
333 return path[pos..]
334}
335
336// dir returns all but the last element of path, typically the path's directory.
337// After dropping the final element, trailing slashes are removed.
338// If the path is empty, dir returns ".". If the path consists entirely of separators,
339// dir returns a single separator.
340// The returned path does not end in a separator unless it is the root directory.
341pub fn dir(path string) string {
342 if path == '' {
343 return '.'
344 }
345 detected_path_separator := if path.contains('/') { '/' } else { '\\' }
346 pos := path.last_index(detected_path_separator) or { return '.' }
347 if pos == 0 {
348 return detected_path_separator
349 }
350 return path[..pos]
351}
352
353// base returns the last element of path.
354// Trailing path separators are removed before extracting the last element.
355// If the path is empty, base returns ".". If the path consists entirely of separators, base returns a
356// single separator.
357pub fn base(path string) string {
358 if path == '' {
359 return '.'
360 }
361 detected_path_separator := if path.contains('/') { '/' } else { '\\' }
362 if path == detected_path_separator {
363 return detected_path_separator
364 }
365 if path.ends_with(detected_path_separator) {
366 path2 := path[..path.len - 1]
367 pos := path2.last_index(detected_path_separator) or { return path2.clone() }
368 return path2[pos + 1..]
369 }
370 pos := path.last_index(detected_path_separator) or { return path.clone() }
371 return path[pos + 1..]
372}
373
374// file_name will return all characters found after the last occurrence of `path_separator`.
375// file extension is included.
376pub fn file_name(path string) string {
377 detected_path_separator := if path.contains('/') { '/' } else { '\\' }
378 return path.all_after_last(detected_path_separator)
379}
380
381// split_path will split `path` into (`dir`,`filename`,`ext`).
382// Examples:
383// ```v
384// dir,filename,ext := os.split_path('/usr/lib/test.so')
385// assert [dir,filename,ext] == ['/usr/lib','test','.so']
386// ```
387pub fn split_path(path string) (string, string, string) {
388 if path == '' {
389 return '.', '', ''
390 } else if path == '.' {
391 return '.', '', ''
392 } else if path == '..' {
393 return '..', '', ''
394 }
395
396 detected_path_separator := if path.contains('/') { '/' } else { '\\' }
397
398 if path == detected_path_separator {
399 return detected_path_separator, '', ''
400 }
401 if path.ends_with(detected_path_separator) {
402 return path[..path.len - 1], '', ''
403 }
404 mut dir := '.'
405 /*
406 TODO: JS backend does not support IfGuard yet.
407 */
408 pos := path.last_index(detected_path_separator) or { -1 }
409 if pos == -1 {
410 dir = '.'
411 } else if pos == 0 {
412 dir = detected_path_separator
413 } else {
414 dir = path[..pos]
415 }
416 file_name := path.all_after_last(detected_path_separator)
417 pos_ext := file_name.last_index_u8(`.`)
418 if pos_ext == -1 || pos_ext == 0 || pos_ext + 1 >= file_name.len {
419 return dir, file_name, ''
420 }
421 return dir, file_name[..pos_ext], file_name[pos_ext..]
422}
423
424// input_opt returns a one-line string from stdin, after printing a prompt.
425// Returns `none` in case of an error (end of input).
426pub fn input_opt(prompt string) ?string {
427 print(prompt)
428 flush()
429 res := get_raw_line()
430 if res.len > 0 {
431 return res.trim_right('\r\n')
432 }
433 return none
434}
435
436// input returns a one-line string from stdin, after printing a prompt.
437// Returns `<EOF>` in case of an error (end of input).
438pub fn input(prompt string) string {
439 res := input_opt(prompt) or { return '<EOF>' }
440 return res
441}
442
443// get_line returns a one-line string from stdin.
444pub fn get_line() string {
445 str := get_raw_line()
446 $if windows {
447 return str.trim_right('\r\n')
448 }
449 return str.trim_right('\n')
450}
451
452// get_lines returns an array of strings read from stdin.
453// reading is stopped when an empty line is read.
454pub fn get_lines() []string {
455 mut line := ''
456 mut inputstr := []string{}
457 for {
458 line = get_line()
459 if line.len <= 0 {
460 break
461 }
462 line = line.trim_space()
463 inputstr << line
464 }
465 return inputstr
466}
467
468// get_lines_joined returns a string of the values read from stdin.
469// reading is stopped when an empty line is read.
470pub fn get_lines_joined() string {
471 return get_lines().join('')
472}
473
474// get_raw_lines reads *all* input lines from stdin, as an array of strings.
475// Note: unlike os.get_lines, empty lines (that contain only `\r\n` or `\n`),
476// will be present in the output.
477// Reading is stopped, only on EOF of stdin.
478pub fn get_raw_lines() []string {
479 mut line := ''
480 mut lines := []string{}
481 for {
482 line = get_raw_line()
483 if line.len <= 0 {
484 break
485 }
486 lines << line
487 }
488 return lines
489}
490
491// get_raw_lines_joined reads *all* input lines from stdin.
492// It returns them as one large string. Note: unlike os.get_lines_joined,
493// empty lines (that contain only `\r\n` or `\n`), will be present in
494// the output.
495// Reading is stopped, only on EOF of stdin.
496pub fn get_raw_lines_joined() string {
497 return get_raw_lines().join('')
498}
499
500// get_trimmed_lines reads *all* input lines from stdin, as an array of strings.
501// The ending new line characters `\r` and `\n`, are removed from each line.
502// Note: unlike os.get_lines, empty lines will be present in the output as empty strings ''.
503// Reading is stopped, only on EOF of stdin.
504pub fn get_trimmed_lines() []string {
505 mut lines := []string{}
506 for {
507 mut line := get_raw_line()
508 if line.len <= 0 {
509 break
510 }
511 mut end := line.len
512 if end > 0 && line[end - 1] == `\n` {
513 end--
514 }
515 if end > 0 && line[end - 1] == `\r` {
516 end--
517 }
518 lines << line#[..end]
519 }
520 return lines
521}
522
523// user_os returns the current user's operating system name.
524pub fn user_os() string {
525 $if linux {
526 return 'linux'
527 }
528 $if macos {
529 return 'macos'
530 }
531 $if windows {
532 return 'windows'
533 }
534 $if freebsd {
535 return 'freebsd'
536 }
537 $if openbsd {
538 return 'openbsd'
539 }
540 $if netbsd {
541 return 'netbsd'
542 }
543 $if dragonfly {
544 return 'dragonfly'
545 }
546 $if android {
547 return 'android'
548 }
549 $if termux {
550 return 'termux'
551 }
552 $if solaris {
553 return 'solaris'
554 }
555 $if qnx {
556 return 'qnx'
557 }
558 $if haiku {
559 return 'haiku'
560 }
561 $if serenity {
562 return 'serenity'
563 }
564 //$if plan9 {
565 // return 'plan9'
566 //}
567 $if vinix {
568 return 'vinix'
569 }
570 if getenv('TERMUX_VERSION') != '' {
571 return 'termux'
572 }
573 return 'unknown'
574}
575
576// user_names returns an array containing the names of all users on the system.
577pub fn user_names() ![]string {
578 $if windows {
579 result := execute('wmic useraccount get name')
580 if result.exit_code != 0 {
581 return error('Failed to get user names. Exited with code ${result.exit_code}: ${result.output}')
582 }
583 mut users := result.output.split_into_lines()
584 // windows command prints an empty line at the end of output
585 users.delete(users.len - 1)
586 return users
587 } $else {
588 lines := read_lines('/etc/passwd')!
589 mut users := []string{cap: lines.len}
590 for line in lines {
591 end_name := line.index(':') or { line.len }
592 users << line[0..end_name]
593 }
594 return users
595 }
596}
597
598// home_dir returns the path to the current user's home directory.
599pub fn home_dir() string {
600 $if windows {
601 return getenv('USERPROFILE')
602 } $else {
603 return getenv('HOME')
604 }
605}
606
607// expand_tilde_to_home expands the character `~` in `path` to the user's home directory.
608// See also `home_dir()`.
609pub fn expand_tilde_to_home(path string) string {
610 if path == '~' {
611 hdir := home_dir()
612 return hdir.trim_right(path_separator)
613 }
614 source := '~' + path_separator
615 if path.starts_with(source) {
616 hdir := home_dir()
617 trimmed := hdir.trim_right(path_separator)
618 final := trimmed + path_separator
619 result := path.replace_once(source, final)
620 return result
621 }
622 return path
623}
624
625// write_file writes `text` data to a file with the given `path`.
626// If `path` already exists, it will be overwritten.
627pub fn write_file(path string, text string) ! {
628 mut f := create(path)!
629 defer {
630 f.close()
631 }
632 unsafe { f.write_full_buffer(text.str, usize(text.len))! }
633}
634
635pub struct ExecutableNotFoundError {
636 Error
637}
638
639pub fn (err ExecutableNotFoundError) msg() string {
640 return 'os: failed to find executable'
641}
642
643fn error_failed_to_find_executable() IError {
644 return &ExecutableNotFoundError{}
645}
646
647fn find_abs_path_of_executable_in_path_env(exe_name string, env_path string) !string {
648 for suffix in executable_suffixes {
649 fexepath := exe_name + suffix
650 if is_abs_path(fexepath) {
651 return fexepath
652 }
653 if fexepath.contains(path_separator) {
654 if is_file(fexepath) && is_executable(fexepath) {
655 return abs_path(fexepath)
656 }
657 continue
658 }
659 mut res := ''
660 paths := env_path.split(path_delimiter)
661 for p in paths {
662 found_abs_path := join_path_single(p, fexepath)
663 $if trace_find_abs_path_of_executable ? {
664 dump(found_abs_path)
665 }
666 if is_file(found_abs_path) && is_executable(found_abs_path) {
667 res = found_abs_path
668 break
669 }
670 }
671 if res.len > 0 {
672 return abs_path(res)
673 }
674 }
675 return error_failed_to_find_executable()
676}
677
678// find_abs_path_of_executable searches the environment PATH for the absolute path of the given executable name.
679pub fn find_abs_path_of_executable(exe_name string) !string {
680 if exe_name == '' {
681 return error('expected non empty `exe_name`')
682 }
683 return find_abs_path_of_executable_in_path_env(exe_name, getenv('PATH'))
684}
685
686// exists_in_system_path returns `true` if `prog` exists in the system's PATH.
687pub fn exists_in_system_path(prog string) bool {
688 find_abs_path_of_executable(prog) or { return false }
689 return true
690}
691
692// is_file returns a `bool` indicating whether the given `path` is a file.
693pub fn is_file(path string) bool {
694 return exists(path) && !is_dir(path)
695}
696
697// join_path joins any number of path elements into a single path, separating
698// them with a platform-specific path_separator. Empty elements are ignored.
699// Windows platform output will rewrite forward slashes to backslash.
700// Consider looking at the unit tests in os_test.v for semi-formal API.
701@[manualfree]
702pub fn join_path(base string, dirs ...string) string {
703 // TODO: fix freeing of `dirs` when the passed arguments are variadic,
704 // but do not free the arr, when `os.join_path(base, ...arr)` is called.
705 mut sb := strings.new_builder(base.len + dirs.len * 50)
706 defer {
707 unsafe { sb.free() }
708 }
709 sbase := base.trim_right('\\/')
710 defer {
711 unsafe { sbase.free() }
712 }
713 sb.write_string(sbase)
714 for d in dirs {
715 if d != '' {
716 sb.write_string(path_separator)
717 sb.write_string(d)
718 }
719 }
720 normalize_path_in_builder(mut sb)
721 mut res := sb.str()
722 if base == '' {
723 res = res.trim_left(path_separator)
724 }
725 return res
726}
727
728// join_path_single appends the `elem` after `base`, separated with a
729// platform-specific path_separator. Empty elements are ignored.
730@[manualfree]
731pub fn join_path_single(base string, elem string) string {
732 // TODO: deprecate this and make it `return os.join_path(base, elem)`,
733 // when freeing variadic args vs ...arr is solved in the compiler
734 mut sb := strings.new_builder(base.len + elem.len + 1)
735 defer {
736 unsafe { sb.free() }
737 }
738 sbase := base.trim_right('\\/')
739 defer {
740 unsafe { sbase.free() }
741 }
742 sb.write_string(sbase)
743 if elem != '' {
744 sb.write_string(path_separator)
745 sb.write_string(elem)
746 }
747 normalize_path_in_builder(mut sb)
748 mut res := sb.str()
749 if base == '' {
750 res = res.trim_left(path_separator)
751 }
752 return res
753}
754
755@[direct_array_access]
756fn normalize_path_in_builder(mut sb strings.Builder) {
757 mut fs := `\\`
758 mut rs := `/`
759 $if windows {
760 fs = `/`
761 rs = `\\`
762 }
763 for idx in 0 .. sb.len {
764 unsafe {
765 if sb[idx] == fs {
766 sb[idx] = rs
767 }
768 }
769 }
770 for idx in 0 .. sb.len - 3 {
771 if sb[idx] == rs && sb[idx + 1] == `.` && sb[idx + 2] == rs {
772 unsafe {
773 // let `/foo/./bar.txt` become `/foo/bar.txt` in place
774 for j := idx + 1; j < sb.len - 2; j++ {
775 sb[j] = sb[j + 2]
776 }
777 sb.len -= 2
778 }
779 }
780 if sb[idx] == rs && sb[idx + 1] == rs {
781 unsafe {
782 // let `/foo//bar.txt` become `/foo/bar.txt` in place
783 for j := idx + 1; j < sb.len - 1; j++ {
784 sb[j] = sb[j + 1]
785 }
786 sb.len -= 1
787 }
788 }
789 }
790}
791
792@[params]
793pub struct WalkParams {
794pub:
795 hidden bool
796}
797
798// walk_ext returns a recursive list of all files in `path` ending with `ext`.
799// For listing only one level deep, see: `os.ls`
800pub fn walk_ext(path string, ext string, opts WalkParams) []string {
801 mut res := []string{}
802 impl_walk_ext(path, ext, mut res, opts)
803 return res
804}
805
806fn impl_walk_ext(path string, ext string, mut out []string, opts WalkParams) {
807 if !is_dir(path) {
808 return
809 }
810 mut files := ls(path) or { return }
811 separator := if path.ends_with(path_separator) { '' } else { path_separator }
812 for file in files {
813 if !opts.hidden && file.starts_with('.') {
814 continue
815 }
816 p := path + separator + file
817 if is_dir(p) && !is_link(p) {
818 impl_walk_ext(p, ext, mut out, opts)
819 } else if file.ends_with(ext) {
820 out << p
821 }
822 }
823}
824
825// walk traverses the given directory `path`.
826// When a file is encountered, it will call the callback `f` with current file as argument.
827// Note: walk can be called even for deeply nested folders,
828// since it does not recurse, but processes them iteratively.
829// For listing only one level deep, see: `os.ls`
830pub fn walk(path string, f fn (string)) {
831 if path == '' {
832 return
833 }
834 if !is_dir(path) {
835 return
836 }
837 mut remaining := []string{cap: 1000}
838 clean_path := path.trim_right(path_separator)
839 $if windows {
840 remaining << clean_path.replace('/', '\\')
841 } $else {
842 remaining << clean_path
843 }
844 for remaining.len > 0 {
845 cpath := remaining.pop()
846 pkind := kind_of_existing_path(cpath)
847 if pkind.is_link || !pkind.is_dir {
848 f(cpath)
849 continue
850 }
851 mut files := ls(cpath) or { continue }
852 for idx := files.len - 1; idx >= 0; idx-- {
853 remaining << cpath + path_separator + files[idx]
854 }
855 }
856}
857
858// FnWalkContextCB is used to define the callback functions, passed to os.walk_context.
859pub type FnWalkContextCB = fn (voidptr, string)
860
861// walk_with_context traverses the given directory `path`.
862// For each encountered file *and* directory, it will call your `fcb` callback,
863// passing it the arbitrary `context` in its first parameter,
864// and the path to the file in its second parameter.
865// Note: walk_with_context can be called even for deeply nested folders,
866// since it does not recurse, but processes them iteratively.
867// For listing only one level deep, see: `os.ls`
868pub fn walk_with_context(path string, context voidptr, fcb FnWalkContextCB) {
869 if path == '' {
870 return
871 }
872 if !is_dir(path) {
873 return
874 }
875 mut remaining := []string{cap: 1000}
876 clean_path := path.trim_right(path_separator)
877 $if windows {
878 remaining << clean_path.replace('/', '\\')
879 } $else {
880 remaining << clean_path
881 }
882 mut loops := 0
883 for remaining.len > 0 {
884 loops++
885 cpath := remaining.pop()
886 // call `fcb` for everything, but the initial folder:
887 if loops > 1 {
888 fcb(context, cpath)
889 }
890 pkind := kind_of_existing_path(cpath)
891 if pkind.is_link || !pkind.is_dir {
892 continue
893 }
894 mut files := ls(cpath) or { continue }
895 for idx := files.len - 1; idx >= 0; idx-- {
896 remaining << cpath + path_separator + files[idx]
897 }
898 }
899}
900
901// log will print "os.log: "+`s` ...
902pub fn log(s string) {
903 println('os.log: ' + s)
904}
905
906@[params]
907pub struct MkdirParams {
908pub:
909 mode u32 = 0o777 // note that the actual mode is affected by the process's umask
910}
911
912// mkdir_all will create a valid full path of all directories given in `path`.
913pub fn mkdir_all(opath string, params MkdirParams) ! {
914 if exists(opath) {
915 if is_dir(opath) {
916 return
917 }
918 return error('path `${opath}` already exists, and is not a folder')
919 }
920 other_separator := $if windows { '/' } $else { '\\' }
921 path := opath.replace(other_separator, path_separator)
922 mut p := if path.starts_with(path_separator) { path_separator } else { '' }
923 path_parts := path.trim_left(path_separator).split(path_separator)
924 for subdir in path_parts {
925 p += subdir + path_separator
926 if exists(p) && is_dir(p) {
927 continue
928 }
929 mkdir(p, params) or { return error('folder: ${p}, error: ${err}') }
930 }
931}
932
933fn create_folder_when_it_does_not_exist(path string) {
934 if is_dir(path) || is_link(path) {
935 return
936 }
937 mut error_msg := ''
938 for _ in 0 .. 10 {
939 mkdir_all(path, mode: 0o700) or {
940 if is_dir(path) || is_link(path) {
941 // A race had been won, and the `path` folder had been created, by another concurrent V program.
942 // We are fine with that, since the folder now exists, even though this process did not create it.
943 // We can just use it too ¯\_(ツ)_/¯ .
944 return
945 }
946 error_msg = err.msg()
947 sleep_ms(1) // wait a bit, before a retry, to let the other process finish its folder creation
948 continue
949 }
950 break
951 }
952 if is_dir(path) || is_link(path) {
953 return
954 }
955 // There was something wrong, that could not be solved, by just retrying
956 // There is no choice, but to report it back :-\
957 panic(error_msg)
958}
959
960fn xdg_home_folder(ename string, lpath string) string {
961 xdg_folder := getenv(ename)
962 dir := if xdg_folder != '' {
963 xdg_folder
964 } else {
965 join_path_single(home_dir(), lpath)
966 }
967 create_folder_when_it_does_not_exist(dir)
968 return dir
969}
970
971// cache_dir returns the path to a *writable* user-specific folder, suitable for writing non-essential data.
972// See: https://specifications.freedesktop.org/basedir-spec/latest/ .
973// There is a single base directory relative to which user-specific non-essential
974// (cached) data should be written. This directory is defined by the environment
975// variable `$XDG_CACHE_HOME`.
976// `$XDG_CACHE_HOME` defines the base directory relative to which user specific
977// non-essential data files should be stored. If `$XDG_CACHE_HOME` is either not set
978// or empty, a default equal to `$HOME/.cache` should be used.
979// Note: This function ensures that the returned directory exists and panics if directory creation fails.
980pub fn cache_dir() string {
981 return xdg_home_folder('XDG_CACHE_HOME', '.cache')
982}
983
984// data_dir returns the path to a *writable* user-specific folder, suitable for writing application data.
985// On Windows, that is `%LocalAppData%`, or if that is not available,
986// `%USERPROFILE%/AppData/Local`.
987// On the rest, that is `$XDG_DATA_HOME`, or if that is not available,
988// `$HOME/.local/share`.
989// Note: This function ensures that the returned directory exists and panics if directory creation fails.
990pub fn data_dir() string {
991 $if windows {
992 local_app_data := getenv('LocalAppData')
993 if local_app_data != '' {
994 create_folder_when_it_does_not_exist(local_app_data)
995 return local_app_data
996 }
997 home := home_dir()
998 if home != '' {
999 dir := join_path(home, 'AppData', 'Local')
1000 create_folder_when_it_does_not_exist(dir)
1001 return dir
1002 }
1003 }
1004 return xdg_home_folder('XDG_DATA_HOME', '.local/share')
1005}
1006
1007// state_dir returns a *writable* folder user-specific folder.
1008// It is suitable for storing state data, that should persist between (application) restarts,
1009// but that is not important or portable enough to the user that it should be stored in os.data_dir().
1010// See: https://specifications.freedesktop.org/basedir-spec/latest/ .
1011// `$XDG_STATE_HOME` defines the base directory relative to which user-specific state files should be stored.
1012// If `$XDG_STATE_HOME` is either not set or empty, a default equal to
1013// `$HOME/.local/state should be used`.
1014// It may contain:
1015// * actions history (logs, history, recently used files, …)
1016// * current state of the application that can be reused on a restart (view, layout, open files, undo history, …)
1017// Note: This function ensures that the returned directory exists and panics if directory creation fails.
1018pub fn state_dir() string {
1019 return xdg_home_folder('XDG_STATE_HOME', '.local/state')
1020}
1021
1022// local_bin_dir returns `$HOME/.local/bin`, which is *guaranteed* to be in the PATH of the current user.
1023// It is compatible with stributions, following the XDG spec from https://specifications.freedesktop.org/basedir-spec/latest/ :
1024// > User-specific executable files may be stored in `$HOME/.local/bin`.
1025// > Distributions should ensure this directory shows up in the UNIX $PATH environment variable, at an appropriate place.
1026// Note: This function ensures that the returned directory exists and panics if directory creation fails.
1027pub fn local_bin_dir() string {
1028 return xdg_home_folder('LOCAL_BIN_DIR', '.local/bin') // provides a way to test by setting an env variable
1029}
1030
1031// temp_dir returns the path to a folder, that is suitable for storing temporary files.
1032pub fn temp_dir() string {
1033 mut path := getenv('TMPDIR')
1034 $if windows {
1035 if path == '' {
1036 // TODO: see Qt's implementation?
1037 // https://doc.qt.io/qt-5/qdir.html#tempPath
1038 // https://github.com/qt/qtbase/blob/e164d61ca8263fc4b46fdd916e1ea77c7dd2b735/src/corelib/io/qfilesystemengine_win.cpp#L1275
1039 path = getenv('TEMP')
1040 if path == '' {
1041 path = getenv('TMP')
1042 }
1043 if path == '' {
1044 path = 'C:/tmp'
1045 }
1046 }
1047 path = get_long_path(path) or { path }
1048 }
1049 $if macos {
1050 // avoid /var/folders/6j/cmsk8gd90pd.... on macs
1051 return '/tmp'
1052 }
1053 $if android {
1054 // TODO: test+use '/data/local/tmp' on Android before using cache_dir()
1055 if path == '' {
1056 path = cache_dir()
1057 }
1058 }
1059 $if termux {
1060 path = '/data/data/com.termux/files/usr/tmp'
1061 }
1062 if path == '' {
1063 path = '/tmp'
1064 }
1065 return path
1066}
1067
1068// vtmp_dir returns the path to a folder, that is writable to V programs, *and* specific to the OS user.
1069// It can be overridden by setting the env variable `VTMP`.
1070// Note: This function ensures that the returned directory exists and panics if directory creation fails.
1071pub fn vtmp_dir() string {
1072 mut vtmp := getenv('VTMP')
1073 if vtmp.len > 0 {
1074 create_folder_when_it_does_not_exist(vtmp)
1075 return vtmp
1076 }
1077 uid := getuid()
1078 vtmp = join_path_single(temp_dir(), 'v_${uid}')
1079 create_folder_when_it_does_not_exist(vtmp)
1080 setenv('VTMP', vtmp, true)
1081 return vtmp
1082}
1083
1084fn default_vmodules_path() string {
1085 hdir := home_dir()
1086 if hdir != '' {
1087 return join_path_single(hdir, '.vmodules')
1088 }
1089 // In some hermetic CI/sandbox environments HOME/USERPROFILE is intentionally
1090 // missing. Fall back to a writable user-specific temp path.
1091 return join_path_single(vtmp_dir(), '.vmodules')
1092}
1093
1094// vmodules_dir returns the path to a folder, where v stores its global modules.
1095pub fn vmodules_dir() string {
1096 paths := vmodules_paths()
1097 if paths.len > 0 {
1098 return paths[0]
1099 }
1100 return default_vmodules_path()
1101}
1102
1103// vmodules_paths returns a list of paths, where v looks up for modules.
1104// You can customize it through setting the environment variable `VMODULES`.
1105pub fn vmodules_paths() []string {
1106 mut path := getenv('VMODULES')
1107 if path == '' {
1108 // unsafe { path.free() }
1109 path = default_vmodules_path()
1110 }
1111 defer {
1112 // unsafe { path.free() }
1113 }
1114 splitted := path.split(path_delimiter)
1115 defer {
1116 // unsafe { splitted.free() }
1117 }
1118 mut list := []string{cap: splitted.len}
1119 for i in 0 .. splitted.len {
1120 si := splitted[i]
1121 trimmed := si.trim_right(path_separator)
1122 list << trimmed
1123 // unsafe { trimmed.free() }
1124 // unsafe { si.free() }
1125 }
1126 return list
1127}
1128
1129// resource_abs_path returns an absolute path, for the given `path`.
1130// (the path is expected to be relative to the executable program)
1131// See https://discordapp.com/channels/592103645835821068/592294828432424960/630806741373943808
1132// It gives a convenient way to access program resources like images, fonts, sounds and so on,
1133// *no matter* how the program was started, and what is the current working directory.
1134@[manualfree]
1135pub fn resource_abs_path(path string) string {
1136 exe := executable()
1137 dexe := dir(exe)
1138 mut base_path := real_path(dexe)
1139 vresource := getenv('V_RESOURCE_PATH')
1140 if vresource.len != 0 {
1141 unsafe { base_path.free() }
1142 base_path = vresource
1143 }
1144 fp := join_path_single(base_path, path)
1145 res := real_path(fp)
1146 unsafe {
1147 fp.free()
1148 vresource.free()
1149 base_path.free()
1150 dexe.free()
1151 exe.free()
1152 }
1153 return res
1154}
1155
1156pub struct Uname {
1157pub mut:
1158 sysname string
1159 nodename string
1160 release string
1161 version string
1162 machine string
1163}
1164
1165// execute_or_panic returns the os.Result of executing `cmd`, or panic with its output on failure.
1166pub fn execute_or_panic(cmd string) Result {
1167 res := execute(cmd)
1168 if res.exit_code != 0 {
1169 eprintln('failed cmd: ${cmd}')
1170 eprintln('failed code: ${res.exit_code}')
1171 panic(res.output)
1172 }
1173 return res
1174}
1175
1176// execute_or_exit returns the os.Result of executing `cmd`, or exit with its output on failure.
1177pub fn execute_or_exit(cmd string) Result {
1178 res := execute(cmd)
1179 if res.exit_code != 0 {
1180 eprintln('failed cmd: ${cmd}')
1181 eprintln('failed code: ${res.exit_code}')
1182 eprintln(res.output)
1183 exit(1)
1184 }
1185 return res
1186}
1187
1188// execute_opt returns the os.Result of executing `cmd`, or an error with its output on failure.
1189pub fn execute_opt(cmd string) !Result {
1190 res := execute(cmd)
1191 if res.exit_code != 0 {
1192 return error(res.output)
1193 }
1194 return res
1195}
1196
1197// quoted path - return a quoted version of the path, depending on the platform.
1198pub fn quoted_path(path string) string {
1199 $if windows {
1200 return if path.ends_with(path_separator) {
1201 '"${path + path_separator}"'
1202 } else {
1203 '"${path}"'
1204 }
1205 } $else {
1206 return "'" + path.replace("'", "'\\''") + "'"
1207 }
1208}
1209
1210// config_dir returns the path to the user configuration directory (depending on the platform).
1211// On Windows, that is `%AppData%`.
1212// On macOS, that is `~/Library/Application Support`.
1213// On the rest, that is `$XDG_CONFIG_HOME`, or if that is not available, `~/.config`.
1214// If the path cannot be determined, it returns an error.
1215// (for example, when `$HOME` on Linux, or `%AppData%` on Windows is not defined)
1216pub fn config_dir() !string {
1217 $if windows {
1218 app_data := getenv('AppData')
1219 if app_data != '' {
1220 return app_data
1221 }
1222 } $else $if macos || darwin || ios {
1223 home := home_dir()
1224 if home != '' {
1225 return home + '/Library/Application Support'
1226 }
1227 } $else {
1228 xdg_home := getenv('XDG_CONFIG_HOME')
1229 if xdg_home != '' {
1230 return xdg_home
1231 }
1232 home := home_dir()
1233 if home != '' {
1234 return home + '/.config'
1235 }
1236 }
1237 return error('Cannot find config directory')
1238}
1239
1240// Stat struct modeled on POSIX.
1241pub struct Stat {
1242pub:
1243 dev u64 // ID of device containing file
1244 inode u64 // Inode number
1245 mode u32 // File type and user/group/world permission bits
1246 nlink u64 // Number of hard links to file
1247 uid u32 // Owner user ID
1248 gid u32 // Owner group ID
1249 rdev u64 // Device ID (if special file)
1250 size u64 // Total size in bytes
1251 atime i64 // Last access (seconds since UNIX epoch)
1252 mtime i64 // Last modified (seconds since UNIX epoch)
1253 ctime i64 // Last status change (seconds since UNIX epoch)
1254}
1255