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 }