jm

Odin for scripts: small packages and a runner, on core: only
Log | Files | Refs | README

path.odin (7204B)


      1 /*
      2 Package path is the file-system vocabulary a script reaches for: expand a
      3 home-relative path, make a directory tree, read and write whole files, list
      4 or walk a tree, and find the per-user state, config, cache and log folders on
      5 every platform.
      6 
      7 Every proc that can fail returns an os.Error so `must` composes:
      8 
      9 	cfg  := must(path.read(path.expand("~/.config/tool/cfg.json")))
     10 	must(path.mkdirs(path.state_dir("tool")))
     11 	must(path.write(out, rendered))
     12 
     13 Pure helpers (expand, join, base, dir, ext, stem, same) never fail.
     14 */
     15 package path
     16 
     17 import "base:runtime"
     18 import "core:os"
     19 import "core:path/filepath"
     20 import "core:slice"
     21 import "core:strings"
     22 
     23 // Re-exports, so a script needs one import for path work.
     24 base     :: filepath.base
     25 dir      :: filepath.dir
     26 ext      :: filepath.ext
     27 stem     :: filepath.stem
     28 clean    :: filepath.clean
     29 is_abs   :: filepath.is_abs
     30 exists   :: os.exists
     31 is_dir   :: os.is_dir
     32 is_file  :: os.is_file
     33 remove   :: os.remove
     34 rename   :: os.rename
     35 copy     :: os.copy_file
     36 glob     :: os.glob
     37 remove_all :: os.remove_all
     38 
     39 SEPARATOR :: filepath.SEPARATOR
     40 
     41 // home returns the current user's home directory.
     42 home :: proc(allocator := context.allocator) -> string {
     43 	h, err := os.user_home_dir(allocator)
     44 	if err != nil {
     45 		return ""
     46 	}
     47 	return h
     48 }
     49 
     50 // expand replaces a leading "~" or "~/" with the home directory and cleans
     51 // the result. Other paths come back cleaned.
     52 expand :: proc(p: string, allocator := context.allocator) -> string {
     53 	if p == "~" {
     54 		return home(allocator)
     55 	}
     56 	if strings.has_prefix(p, "~/") || strings.has_prefix(p, `~\`) {
     57 		return join(home(context.temp_allocator), p[2:], allocator = allocator)
     58 	}
     59 	cleaned, _ := filepath.clean(p, allocator)
     60 	return cleaned
     61 }
     62 
     63 // join joins path elements with the platform separator and cleans the result.
     64 join :: proc(elems: ..string, allocator := context.allocator) -> string {
     65 	s, _ := filepath.join(elems, allocator)
     66 	return s
     67 }
     68 
     69 // abs makes p absolute against the working directory.
     70 abs :: proc(p: string, allocator := context.allocator) -> (string, os.Error) {
     71 	return filepath.abs(p, allocator)
     72 }
     73 
     74 // mkdirs creates p and every missing parent. An existing directory is not
     75 // an error.
     76 mkdirs :: proc(p: string) -> os.Error {
     77 	err := os.make_directory_all(p)
     78 	if err == os.General_Error.Exist && os.is_dir(p) {
     79 		return nil
     80 	}
     81 	return err
     82 }
     83 
     84 // read returns the whole file as a string.
     85 read :: proc(p: string, allocator := context.allocator) -> (string, os.Error) {
     86 	data, err := os.read_entire_file_from_path(p, allocator)
     87 	return string(data), err
     88 }
     89 
     90 // read_lines returns the file split into lines, without line endings.
     91 read_lines :: proc(p: string, allocator := context.allocator) -> ([]string, os.Error) {
     92 	text, err := read(p, allocator)
     93 	if err != nil {
     94 		return nil, err
     95 	}
     96 	trimmed := strings.trim_right(text, "\r\n")
     97 	if trimmed == "" {
     98 		return nil, nil
     99 	}
    100 	parts, _ := strings.split_lines(trimmed, allocator)
    101 	for &part in parts {
    102 		part = strings.trim_right(part, "\r")
    103 	}
    104 	return parts, nil
    105 }
    106 
    107 // write replaces the file's contents, creating parents as needed.
    108 write :: proc(p: string, data: string) -> os.Error {
    109 	if err := mkdirs(filepath.dir(p)); err != nil {
    110 		return err
    111 	}
    112 	return os.write_entire_file(p, data)
    113 }
    114 
    115 // append_file adds data to the end of the file, creating it if needed.
    116 append_file :: proc(p: string, data: string) -> os.Error {
    117 	if err := mkdirs(filepath.dir(p)); err != nil {
    118 		return err
    119 	}
    120 	f, err := os.open(p, {.Write, .Append, .Create}, os.Permissions_Read_All + {.Write_User})
    121 	if err != nil {
    122 		return err
    123 	}
    124 	defer os.close(f)
    125 	_, werr := os.write_string(f, data)
    126 	return werr
    127 }
    128 
    129 // list returns the names in a directory, sorted.
    130 list :: proc(p: string, allocator := context.allocator) -> ([]string, os.Error) {
    131 	infos, err := os.read_all_directory_by_path(p, context.temp_allocator)
    132 	if err != nil {
    133 		return nil, err
    134 	}
    135 	names := make([]string, len(infos), allocator)
    136 	for info, i in infos {
    137 		names[i] = strings.clone(info.name, allocator)
    138 	}
    139 	slice.sort(names)
    140 	return names, nil
    141 }
    142 
    143 // walk returns the full path of every regular file under root, depth first,
    144 // sorted. Directories are descended but not listed.
    145 walk :: proc(root: string, allocator := context.allocator) -> ([]string, os.Error) {
    146 	w := os.walker_create(root)
    147 	defer os.walker_destroy(&w)
    148 	files := make([dynamic]string, allocator)
    149 	for info in os.walker_walk(&w) {
    150 		if info.type == .Regular {
    151 			append(&files, strings.clone(info.fullpath, allocator))
    152 		}
    153 	}
    154 	if _, err := os.walker_error(&w); err != nil {
    155 		return files[:], err
    156 	}
    157 	slice.sort(files[:])
    158 	return files[:], nil
    159 }
    160 
    161 // temp_dir creates a fresh directory under the system temp location. The
    162 // caller removes it with remove_all.
    163 temp_dir :: proc(prefix := "odin-", allocator := context.allocator) -> (string, os.Error) {
    164 	pattern := strings.concatenate({prefix, "*"}, context.temp_allocator)
    165 	return os.make_directory_temp("", pattern, allocator)
    166 }
    167 
    168 // same reports whether two paths name the same location after cleaning. Case
    169 // is folded on Windows and macOS, whose default file systems (NTFS, APFS as
    170 // shipped) are case-insensitive; other volumes on those systems may differ.
    171 same :: proc(a, b: string) -> bool {
    172 	ca, _ := filepath.clean(a, context.temp_allocator)
    173 	cb, _ := filepath.clean(b, context.temp_allocator)
    174 	when ODIN_OS == .Windows || ODIN_OS == .Darwin {
    175 		return strings.equal_fold(ca, cb)
    176 	} else {
    177 		return ca == cb
    178 	}
    179 }
    180 
    181 // Per-user application directories, created on first use. The base folders
    182 // come from core:os user_*_dir, whose doc comments in core/os/user.odin give:
    183 //
    184 //	             Linux                 macOS                          Windows
    185 //	state_dir    ~/.local/state/app    ~/Library/Application Support  %LOCALAPPDATA%\app
    186 //	config_dir   ~/.config/app         ~/Library/Application Support  %LOCALAPPDATA%\app
    187 //	cache_dir    ~/.cache/app          ~/Library/Caches/app           %LOCALAPPDATA%\app
    188 //	log_dir      ~/.local/state/app    ~/Library/Logs/app             %LOCALAPPDATA%\app
    189 state_dir :: proc(app: string, allocator := context.allocator) -> (string, os.Error) {
    190 	return app_dir(os.user_state_dir, app, allocator)
    191 }
    192 
    193 config_dir :: proc(app: string, allocator := context.allocator) -> (string, os.Error) {
    194 	base_dir, err := os.user_config_dir(context.temp_allocator)
    195 	if err != nil {
    196 		return "", err
    197 	}
    198 	return ensure(join(base_dir, app, allocator = allocator))
    199 }
    200 
    201 cache_dir :: proc(app: string, allocator := context.allocator) -> (string, os.Error) {
    202 	return app_dir(os.user_cache_dir, app, allocator)
    203 }
    204 
    205 log_dir :: proc(app: string, allocator := context.allocator) -> (string, os.Error) {
    206 	return app_dir(os.user_log_dir, app, allocator)
    207 }
    208 
    209 // ---- internals ----------------------------------------------------------
    210 
    211 Dir_Proc :: #type proc(allocator: runtime.Allocator) -> (string, os.Error)
    212 
    213 app_dir :: proc(base_of: Dir_Proc, app: string, allocator: runtime.Allocator) -> (string, os.Error) {
    214 	base_dir, err := base_of(context.temp_allocator)
    215 	if err != nil {
    216 		return "", err
    217 	}
    218 	return ensure(join(base_dir, app, allocator = allocator))
    219 }
    220 
    221 ensure :: proc(p: string) -> (string, os.Error) {
    222 	if err := mkdirs(p); err != nil {
    223 		return p, err
    224 	}
    225 	return p, nil
    226 }