From 114b1938244720db4fbbb1d3bb3deeffbe22fe8a Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Mon, 17 Oct 2022 20:58:33 +0800 Subject: [PATCH 01/13] split std/os; add typesafe std/paths --- lib/std/paths.nim | 168 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 lib/std/paths.nim diff --git a/lib/std/paths.nim b/lib/std/paths.nim new file mode 100644 index 0000000000000..f1c6d5e19b566 --- /dev/null +++ b/lib/std/paths.nim @@ -0,0 +1,168 @@ +import includes/osseps + +include system/inclrtl +import std/private/since + +import strutils, pathnorm + +when defined(nimPreviewSlimSystem): + import std/[syncio, assertions] + +const weirdTarget = defined(nimscript) or defined(js) + + +proc normalizePathEnd(path: var string, trailingSep = false) = + ## Ensures ``path`` has exactly 0 or 1 trailing `DirSep`, depending on + ## ``trailingSep``, and taking care of edge cases: it preservers whether + ## a path is absolute or relative, and makes sure trailing sep is `DirSep`, + ## not `AltSep`. Trailing `/.` are compressed, see examples. + if path.len == 0: return + var i = path.len + while i >= 1: + if path[i-1] in {DirSep, AltSep}: dec(i) + elif path[i-1] == '.' and i >= 2 and path[i-2] in {DirSep, AltSep}: dec(i) + else: break + if trailingSep: + # foo// => foo + path.setLen(i) + # foo => foo/ + path.add DirSep + elif i > 0: + # foo// => foo + path.setLen(i) + else: + # // => / (empty case was already taken care of) + path = $DirSep + +proc normalizePathEnd(path: string, trailingSep = false): string = + ## outplace overload + runnableExamples: + when defined(posix): + assert normalizePathEnd("/lib//.//", trailingSep = true) == "/lib/" + assert normalizePathEnd("lib/./.", trailingSep = false) == "lib" + assert normalizePathEnd(".//./.", trailingSep = false) == "." + assert normalizePathEnd("", trailingSep = true) == "" # not / ! + assert normalizePathEnd("/", trailingSep = false) == "/" # not "" ! + result = path + result.normalizePathEnd(trailingSep) + + +template endsWith(a: string, b: set[char]): bool = + a.len > 0 and a[^1] in b + +proc joinPathImpl(result: var string, state: var int, tail: string) = + let trailingSep = tail.endsWith({DirSep, AltSep}) or tail.len == 0 and result.endsWith({DirSep, AltSep}) + normalizePathEnd(result, trailingSep=false) + addNormalizePath(tail, result, state, DirSep) + normalizePathEnd(result, trailingSep=trailingSep) + + +func joinPath(head, tail: string): string = + ## Joins two directory names to one. + ## + ## returns normalized path concatenation of `head` and `tail`, preserving + ## whether or not `tail` has a trailing slash (or, if tail if empty, whether + ## head has one). + ## + ## See also: + ## * `joinPath(parts: varargs[string]) proc`_ + ## * `/ proc`_ + ## * `splitPath proc`_ + ## * `uri.combine proc `_ + ## * `uri./ proc `_ + runnableExamples: + when defined(posix): + assert joinPath("usr", "lib") == "usr/lib" + assert joinPath("usr", "lib/") == "usr/lib/" + assert joinPath("usr", "") == "usr" + assert joinPath("usr/", "") == "usr/" + assert joinPath("", "") == "" + assert joinPath("", "lib") == "lib" + assert joinPath("", "/lib") == "/lib" + assert joinPath("usr/", "/lib") == "usr/lib" + assert joinPath("usr/lib", "../bin") == "usr/bin" + + result = newStringOfCap(head.len + tail.len) + var state = 0 + joinPathImpl(result, state, head) + joinPathImpl(result, state, tail) + when false: + if len(head) == 0: + result = tail + elif head[len(head)-1] in {DirSep, AltSep}: + if tail.len > 0 and tail[0] in {DirSep, AltSep}: + result = head & substr(tail, 1) + else: + result = head & tail + else: + if tail.len > 0 and tail[0] in {DirSep, AltSep}: + result = head & tail + else: + result = head & DirSep & tail + +when doslikeFileSystem: + import std/private/ntpath + +type + Path* = distinct string + + +func joinPath*(parts: varargs[Path]): Path = + var estimatedLen = 0 + for p in parts: estimatedLen += p.string.len + var res = newStringOfCap(estimatedLen) + var state = 0 + for i in 0..high(parts): + joinPathImpl(res, state, parts[i].string) + result = Path(res) + +func splitPathImpl(path: string): tuple[head, tail: Path] = + ## Splits a directory into `(head, tail)` tuple, so that + ## ``head / tail == path`` (except for edge cases like "/usr"). + ## + ## See also: + ## * `joinPath(head, tail) proc`_ + ## * `joinPath(parts: varargs[string]) proc`_ + ## * `/ proc`_ + ## * `/../ proc`_ + ## * `relativePath proc`_ + runnableExamples: + assert splitPath("usr/local/bin") == ("usr/local", "bin") + assert splitPath("usr/local/bin/") == ("usr/local/bin", "") + assert splitPath("/bin/") == ("/bin", "") + when (NimMajor, NimMinor) <= (1, 0): + assert splitPath("/bin") == ("", "bin") + else: + assert splitPath("/bin") == ("/", "bin") + assert splitPath("bin") == ("", "bin") + assert splitPath("") == ("", "") + + when doslikeFileSystem: + let (drive, splitpath) = splitDrive(path) + let stop = drive.len + else: + const stop = 0 + + var sepPos = -1 + for i in countdown(len(path)-1, stop): + if path[i] in {DirSep, AltSep}: + sepPos = i + break + if sepPos >= 0: + result.head = Path(substr(path, 0, + when (NimMajor, NimMinor) <= (1, 0): + sepPos-1 + else: + if likely(sepPos >= 1): sepPos-1 else: 0 + )) + result.tail = Path(substr(path, sepPos+1)) + else: + when doslikeFileSystem: + result.head = Path(drive) + result.tail = Path(splitpath) + else: + result.head = Path("") + result.tail = Path(path) + +func splitPath*(path: Path): tuple[head, tail: Path] {.inline.} = + splitPathImpl(path.string) From 86c81a6e0b83d79c3c03b79bf295adc9e4bf357c Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Mon, 17 Oct 2022 22:32:57 +0800 Subject: [PATCH 02/13] add more files, dirs, paths --- lib/std/dirs.nim | 416 ++++++++++++++++++++++++++++++++++++++++++++++ lib/std/files.nim | 132 +++++++++++++++ lib/std/paths.nim | 75 ++++++++- 3 files changed, 618 insertions(+), 5 deletions(-) create mode 100644 lib/std/dirs.nim create mode 100644 lib/std/files.nim diff --git a/lib/std/dirs.nim b/lib/std/dirs.nim new file mode 100644 index 0000000000000..9699dfd76c2d4 --- /dev/null +++ b/lib/std/dirs.nim @@ -0,0 +1,416 @@ +import paths, files + +import std/oserrors + +const weirdTarget = defined(nimscript) or defined(js) + +when weirdTarget: + {.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".} +else: + {.pragma: noWeirdTarget.} + + +when defined(nimscript): + # for procs already defined in scriptconfig.nim + template noNimJs(body): untyped = discard +elif defined(js): + {.pragma: noNimJs, error: "this proc is not available on the js target".} +else: + {.pragma: noNimJs.} + +when weirdTarget: + discard +elif defined(windows): + import winlean, times +elif defined(posix): + import posix, times + + proc toTime(ts: Timespec): times.Time {.inline.} = + result = initTime(ts.tv_sec.int64, ts.tv_nsec.int) +else: + {.error: "OS module not ported to your operating system!".} + + +when defined(windows) and not weirdTarget: + template wrapUnary(varname, winApiProc, arg: untyped) = + var varname = winApiProc(newWideCString(arg)) + + template wrapBinary(varname, winApiProc, arg, arg2: untyped) = + var varname = winApiProc(newWideCString(arg), arg2) + proc findFirstFile(a: string, b: var WIN32_FIND_DATA): Handle = + result = findFirstFileW(newWideCString(a), b) + template findNextFile(a, b: untyped): untyped = findNextFileW(a, b) + template getCommandLine(): untyped = getCommandLineW() + + template getFilename(f: untyped): untyped = + $cast[WideCString](addr(f.cFileName[0])) + + proc skipFindData(f: WIN32_FIND_DATA): bool {.inline.} = + # Note - takes advantage of null delimiter in the cstring + const dot = ord('.') + result = f.cFileName[0].int == dot and (f.cFileName[1].int == 0 or + f.cFileName[1].int == dot and f.cFileName[2].int == 0) + + + +type + PathComponent* = enum ## Enumeration specifying a path component. + ## + ## See also: + ## * `walkDirRec iterator`_ + ## * `FileInfo object`_ + pcFile, ## path refers to a file + pcLinkToFile, ## path refers to a symbolic link to a file + pcDir, ## path refers to a directory + pcLinkToDir ## path refers to a symbolic link to a directory + +proc staticWalkDir(dir: string; relative: bool): seq[ + tuple[kind: PathComponent, path: string]] = + discard + +iterator walkDir*(dir: Path; relative = false, checkDir = false): + tuple[kind: PathComponent, path: Path] {.tags: [ReadDirEffect].} = + ## Walks over the directory `dir` and yields for each directory or file in + ## `dir`. The component type and full path for each item are returned. + ## + ## Walking is not recursive. If ``relative`` is true (default: false) + ## the resulting path is shortened to be relative to ``dir``. + ## + ## If `checkDir` is true, `OSError` is raised when `dir` + ## doesn't exist. + ## + ## **Example:** + ## + ## This directory structure: + ## + ## dirA / dirB / fileB1.txt + ## / dirC + ## / fileA1.txt + ## / fileA2.txt + ## + ## and this code: + runnableExamples("-r:off"): + import std/[strutils, sugar] + # note: order is not guaranteed + # this also works at compile time + assert collect(for k in walkDir("dirA"): k.path).join(" ") == + "dirA/dirB dirA/dirC dirA/fileA2.txt dirA/fileA1.txt" + ## See also: + ## * `walkPattern iterator`_ + ## * `walkFiles iterator`_ + ## * `walkDirs iterator`_ + ## * `walkDirRec iterator`_ + + when nimvm: + for k, v in items(staticWalkDir(dir.string, relative)): + yield (k, Path(v)) + else: + when weirdTarget: + for k, v in items(staticWalkDir(dir, relative)): + yield (k, v) + elif defined(windows): + var f: WIN32_FIND_DATA + var h = findFirstFile(string(dir / Path("*")), f) + if h == -1: + if checkDir: + raiseOSError(osLastError(), dir.string) + else: + defer: findClose(h) + while true: + var k = pcFile + if not skipFindData(f): + if (f.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) != 0'i32: + k = pcDir + if (f.dwFileAttributes and FILE_ATTRIBUTE_REPARSE_POINT) != 0'i32: + k = succ(k) + let xx = if relative: extractFilename(Path(getFilename(f))) + else: dir / extractFilename(Path(getFilename(f))) + yield (k, xx) + if findNextFile(h, f) == 0'i32: + let errCode = getLastError() + if errCode == ERROR_NO_MORE_FILES: break + else: raiseOSError(errCode.OSErrorCode) + else: + var d = opendir(dir) + if d == nil: + if checkDir: + raiseOSError(osLastError(), dir) + else: + defer: discard closedir(d) + while true: + var x = readdir(d) + if x == nil: break + var y = $cstring(addr x.d_name) + if y != "." and y != "..": + var s: Stat + let path = dir / y + if not relative: + y = path + var k = pcFile + + template kSetGeneric() = # pure Posix component `k` resolution + if lstat(path.cstring, s) < 0'i32: continue # don't yield + elif S_ISDIR(s.st_mode): + k = pcDir + elif S_ISLNK(s.st_mode): + k = getSymlinkFileKind(path) + + when defined(linux) or defined(macosx) or + defined(bsd) or defined(genode) or defined(nintendoswitch): + case x.d_type + of DT_DIR: k = pcDir + of DT_LNK: + if dirExists(path): k = pcLinkToDir + else: k = pcLinkToFile + of DT_UNKNOWN: + kSetGeneric() + else: # e.g. DT_REG etc + discard # leave it as pcFile + else: # assuming that field `d_type` is not present + kSetGeneric() + + yield (k, y) + +iterator walkDirRec*(dir: Path, + yieldFilter = {pcFile}, followFilter = {pcDir}, + relative = false, checkDir = false): Path {.tags: [ReadDirEffect].} = + ## Recursively walks over the directory `dir` and yields for each file + ## or directory in `dir`. + ## + ## If ``relative`` is true (default: false) the resulting path is + ## shortened to be relative to ``dir``, otherwise the full path is returned. + ## + ## If `checkDir` is true, `OSError` is raised when `dir` + ## doesn't exist. + ## + ## .. warning:: Modifying the directory structure while the iterator + ## is traversing may result in undefined behavior! + ## + ## Walking is recursive. `followFilter` controls the behaviour of the iterator: + ## + ## ===================== ============================================= + ## yieldFilter meaning + ## ===================== ============================================= + ## ``pcFile`` yield real files (default) + ## ``pcLinkToFile`` yield symbolic links to files + ## ``pcDir`` yield real directories + ## ``pcLinkToDir`` yield symbolic links to directories + ## ===================== ============================================= + ## + ## ===================== ============================================= + ## followFilter meaning + ## ===================== ============================================= + ## ``pcDir`` follow real directories (default) + ## ``pcLinkToDir`` follow symbolic links to directories + ## ===================== ============================================= + ## + ## + ## See also: + ## * `walkPattern iterator`_ + ## * `walkFiles iterator`_ + ## * `walkDirs iterator`_ + ## * `walkDir iterator`_ + + var stack = newseq[Path]() + var checkDir = checkDir + while stack.len > 0: + let d = stack.pop() + for k, p in walkDir(dir / d, relative = true, checkDir = checkDir): + let rel = d / p + if k in {pcDir, pcLinkToDir} and k in followFilter: + stack.add rel + if k in yieldFilter: + yield if relative: rel else: dir / rel + checkDir = false + # We only check top-level dir, otherwise if a subdir is invalid (eg. wrong + # permissions), it'll abort iteration and there would be no way to + # continue iteration. + # Future work can provide a way to customize this and do error reporting. + +proc rawRemoveDir(dir: string) {.noWeirdTarget.} = + when defined(windows): + wrapUnary(res, removeDirectoryW, dir) + let lastError = osLastError() + if res == 0'i32 and lastError.int32 != 3'i32 and + lastError.int32 != 18'i32 and lastError.int32 != 2'i32: + raiseOSError(lastError, dir) + else: + if rmdir(dir) != 0'i32 and errno != ENOENT: raiseOSError(osLastError(), dir) + +proc removeDir*(dir: Path, checkDir = false) {.tags: [ + WriteDirEffect, ReadDirEffect], gcsafe.} = + ## Removes the directory `dir` including all subdirectories and files + ## in `dir` (recursively). + ## + ## If this fails, `OSError` is raised. This does not fail if the directory never + ## existed in the first place, unless `checkDir` = true. + ## + ## See also: + ## * `tryRemoveFile proc`_ + ## * `removeFile proc`_ + ## * `existsOrCreateDir proc`_ + ## * `createDir proc`_ + ## * `copyDir proc`_ + ## * `copyDirWithPermissions proc`_ + ## * `moveDir proc`_ + for kind, path in walkDir(dir, checkDir = checkDir): + case kind + of pcFile, pcLinkToFile, pcLinkToDir: removeFile(path) + of pcDir: removeDir(path, true) + # for subdirectories there is no benefit in `checkDir = false` + # (unless perhaps for edge case of concurrent processes also deleting + # the same files) + rawRemoveDir(dir.string) + +proc dirExists*(dir: Path): bool {.tags: [ReadDirEffect], + noNimJs.} = + ## Returns true if the directory `dir` exists. If `dir` is a file, false + ## is returned. Follows symlinks. + ## + ## See also: + ## * `fileExists proc`_ + ## * `symlinkExists proc`_ + when defined(windows): + wrapUnary(a, getFileAttributesW, dir.string) + if a != -1'i32: + result = (a and FILE_ATTRIBUTE_DIRECTORY) != 0'i32 + else: + var res: Stat + result = stat(dir.string, res) >= 0'i32 and S_ISDIR(res.st_mode) + +proc rawCreateDir(dir: string): bool {.noWeirdTarget.} = + # Try to create one directory (not the whole path). + # returns `true` for success, `false` if the path has previously existed + # + # This is a thin wrapper over mkDir (or alternatives on other systems), + # so in case of a pre-existing path we don't check that it is a directory. + when defined(solaris): + let res = mkdir(dir, 0o777) + if res == 0'i32: + result = true + elif errno in {EEXIST, ENOSYS}: + result = false + else: + raiseOSError(osLastError(), dir) + elif defined(haiku): + let res = mkdir(dir, 0o777) + if res == 0'i32: + result = true + elif errno == EEXIST or errno == EROFS: + result = false + else: + raiseOSError(osLastError(), dir) + elif defined(posix): + let res = mkdir(dir, 0o777) + if res == 0'i32: + result = true + elif errno == EEXIST: + result = false + else: + #echo res + raiseOSError(osLastError(), dir) + else: + wrapUnary(res, createDirectoryW, dir) + + if res != 0'i32: + result = true + elif getLastError() == 183'i32: + result = false + else: + raiseOSError(osLastError(), dir) + +proc existsOrCreateDir*(dir: Path): bool {. + tags: [WriteDirEffect, ReadDirEffect], noWeirdTarget.} = + ## Checks if a `directory`:idx: `dir` exists, and creates it otherwise. + ## + ## Does not create parent directories (raises `OSError` if parent directories do not exist). + ## Returns `true` if the directory already exists, and `false` otherwise. + ## + ## See also: + ## * `removeDir proc`_ + ## * `createDir proc`_ + ## * `copyDir proc`_ + ## * `copyDirWithPermissions proc`_ + ## * `moveDir proc`_ + result = not rawCreateDir(dir.string) + if result: + # path already exists - need to check that it is indeed a directory + if not dirExists(dir): + raise newException(IOError, "Failed to create '" & dir.string & "'") + +# proc createDir*(dir: Path) {. +# tags: [WriteDirEffect, ReadDirEffect], noWeirdTarget.} = +# ## Creates the `directory`:idx: `dir`. +# ## +# ## The directory may contain several subdirectories that do not exist yet. +# ## The full path is created. If this fails, `OSError` is raised. +# ## +# ## It does **not** fail if the directory already exists because for +# ## most usages this does not indicate an error. +# ## +# ## See also: +# ## * `removeDir proc`_ +# ## * `existsOrCreateDir proc`_ +# ## * `copyDir proc`_ +# ## * `copyDirWithPermissions proc`_ +# ## * `moveDir proc`_ +# if dir.len == 0: +# return +# var omitNext = isAbsolute(dir) +# for p in parentDirs(dir, fromRoot=true): +# if omitNext: +# omitNext = false +# else: +# discard existsOrCreateDir(p) + +# proc copyDir*(source, dest: string) {.tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect], gcsafe, noWeirdTarget.} = +# ## Copies a directory from `source` to `dest`. +# ## +# ## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks +# ## are skipped. +# ## +# ## If this fails, `OSError` is raised. +# ## +# ## On the Windows platform this proc will copy the attributes from +# ## `source` into `dest`. +# ## +# ## On other platforms created files and directories will inherit the +# ## default permissions of a newly created file/directory for the user. +# ## Use `copyDirWithPermissions proc`_ +# ## to preserve attributes recursively on these platforms. +# ## +# ## See also: +# ## * `copyDirWithPermissions proc`_ +# ## * `copyFile proc`_ +# ## * `copyFileWithPermissions proc`_ +# ## * `removeDir proc`_ +# ## * `existsOrCreateDir proc`_ +# ## * `createDir proc`_ +# ## * `moveDir proc`_ +# createDir(dest) +# for kind, path in walkDir(source): +# var noSource = splitPath(path).tail +# if kind == pcDir: +# copyDir(path, dest / noSource) +# else: +# copyFile(path, dest / noSource, {cfSymlinkAsIs}) + +# proc moveDir*(source, dest: string) {.tags: [ReadIOEffect, WriteIOEffect], noWeirdTarget.} = +# ## Moves a directory from `source` to `dest`. +# ## +# ## Symlinks are not followed: if `source` contains symlinks, they themself are +# ## moved, not their target. +# ## +# ## If this fails, `OSError` is raised. +# ## +# ## See also: +# ## * `moveFile proc`_ +# ## * `copyDir proc`_ +# ## * `copyDirWithPermissions proc`_ +# ## * `removeDir proc`_ +# ## * `existsOrCreateDir proc`_ +# ## * `createDir proc`_ +# if not tryMoveFSObject(source, dest, isDir = true): +# # Fallback to copy & del +# copyDir(source, dest) +# removeDir(source) \ No newline at end of file diff --git a/lib/std/files.nim b/lib/std/files.nim new file mode 100644 index 0000000000000..2eb051d00b1c7 --- /dev/null +++ b/lib/std/files.nim @@ -0,0 +1,132 @@ +import paths + +import std/oserrors + + + +const weirdTarget = defined(nimscript) or defined(js) + +when weirdTarget: + {.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".} +else: + {.pragma: noWeirdTarget.} + + +when weirdTarget: + discard +elif defined(windows): + import winlean, times +elif defined(posix): + import posix, times + + proc toTime(ts: Timespec): times.Time {.inline.} = + result = initTime(ts.tv_sec.int64, ts.tv_nsec.int) +else: + {.error: "OS module not ported to your operating system!".} + + +when defined(windows) and not weirdTarget: + template deleteFile(file: untyped): untyped = deleteFileW(file) + template setFileAttributes(file, attrs: untyped): untyped = + setFileAttributesW(file, attrs) + +proc tryRemoveFile*(file: Path): bool {.tags: [WriteDirEffect], noWeirdTarget.} = + ## Removes the `file`. + ## + ## If this fails, returns `false`. This does not fail + ## if the file never existed in the first place. + ## + ## On Windows, ignores the read-only attribute. + ## + ## See also: + ## * `copyFile proc`_ + ## * `copyFileWithPermissions proc`_ + ## * `removeFile proc`_ + ## * `moveFile proc`_ + result = true + when defined(windows): + let f = newWideCString(file.string) + if deleteFile(f) == 0: + result = false + let err = getLastError() + if err == ERROR_FILE_NOT_FOUND or err == ERROR_PATH_NOT_FOUND: + result = true + elif err == ERROR_ACCESS_DENIED and + setFileAttributes(f, FILE_ATTRIBUTE_NORMAL) != 0 and + deleteFile(f) != 0: + result = true + else: + if unlink(file) != 0'i32 and errno != ENOENT: + result = false + +proc removeFile*(file: Path) {.tags: [WriteDirEffect], noWeirdTarget.} = + ## Removes the `file`. + ## + ## If this fails, `OSError` is raised. This does not fail + ## if the file never existed in the first place. + ## + ## On Windows, ignores the read-only attribute. + ## + ## See also: + ## * `removeDir proc`_ + ## * `copyFile proc`_ + ## * `copyFileWithPermissions proc`_ + ## * `tryRemoveFile proc`_ + ## * `moveFile proc`_ + if not tryRemoveFile(file): + raiseOSError(osLastError(), file.string) + +proc tryMoveFSObject(source, dest: string, isDir: bool): bool {.noWeirdTarget.} = + ## Moves a file (or directory if `isDir` is true) from `source` to `dest`. + ## + ## Returns false in case of `EXDEV` error or `AccessDeniedError` on Windows (if `isDir` is true). + ## In case of other errors `OSError` is raised. + ## Returns true in case of success. + when defined(windows): + let s = newWideCString(source) + let d = newWideCString(dest) + result = moveFileExW(s, d, MOVEFILE_COPY_ALLOWED or MOVEFILE_REPLACE_EXISTING) != 0'i32 + else: + result = c_rename(source, dest) == 0'i32 + + if not result: + let err = osLastError() + let isAccessDeniedError = + when defined(windows): + const AccessDeniedError = OSErrorCode(5) + isDir and err == AccessDeniedError + else: + err == EXDEV.OSErrorCode + if not isAccessDeniedError: + raiseOSError(err, $(source, dest)) + +proc moveFile*(source, dest: Path) {. + tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect], noWeirdTarget.} = + ## Moves a file from `source` to `dest`. + ## + ## Symlinks are not followed: if `source` is a symlink, it is itself moved, + ## not its target. + ## + ## If this fails, `OSError` is raised. + ## If `dest` already exists, it will be overwritten. + ## + ## Can be used to `rename files`:idx:. + ## + ## See also: + ## * `moveDir proc`_ + ## * `copyFile proc`_ + ## * `copyFileWithPermissions proc`_ + ## * `removeFile proc`_ + ## * `tryRemoveFile proc`_ + + if not tryMoveFSObject(source.string, dest.string, isDir = false): + when defined(windows): + doAssert false + else: + # Fallback to copy & del + copyFile(source, dest, {cfSymlinkAsIs}) + try: + removeFile(source) + except: + discard tryRemoveFile(dest) + raise diff --git a/lib/std/paths.nim b/lib/std/paths.nim index f1c6d5e19b566..4fb30feee8663 100644 --- a/lib/std/paths.nim +++ b/lib/std/paths.nim @@ -8,8 +8,13 @@ import strutils, pathnorm when defined(nimPreviewSlimSystem): import std/[syncio, assertions] -const weirdTarget = defined(nimscript) or defined(js) - +type + ReadDirEffect* = object of ReadIOEffect ## Effect that denotes a read + ## operation from the directory + ## structure. + WriteDirEffect* = object of WriteIOEffect ## Effect that denotes a write + ## operation to + ## the directory structure. proc normalizePathEnd(path: var string, trailingSep = false) = ## Ensures ``path`` has exactly 0 or 1 trailing `DirSep`, depending on @@ -100,12 +105,51 @@ func joinPath(head, tail: string): string = else: result = head & DirSep & tail +func isAbsoluteImpl(path: string): bool {.raises: [].} = + ## Checks whether a given `path` is absolute. + ## + ## On Windows, network paths are considered absolute too. + runnableExamples: + assert not "".isAbsolute + assert not ".".isAbsolute + when defined(posix): + assert "/".isAbsolute + assert not "a/".isAbsolute + assert "/a/".isAbsolute + + if len(path) == 0: return false + + when doslikeFileSystem: + var len = len(path) + result = (path[0] in {'/', '\\'}) or + (len > 1 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':') + elif defined(macos): + # according to https://perldoc.perl.org/File/Spec/Mac.html `:a` is a relative path + result = path[0] != ':' + elif defined(RISCOS): + result = path[0] == '$' + elif defined(posix) or defined(js): + # `or defined(js)` wouldn't be needed pending https://github.com/nim-lang/Nim/issues/13469 + # This works around the problem for posix, but Windows is still broken with nim js -d:nodejs + result = path[0] == '/' + else: + doAssert false # if ever hits here, adapt as needed + when doslikeFileSystem: import std/private/ntpath type Path* = distinct string +func len*(x: Path): int = + len(string(x)) + + +func isAbsolute*(path: Path): bool {.inline, raises: [].} = + result = isAbsoluteImpl(path.string) + +func joinPath*(head, tail: Path): Path {.inline.} = + result = Path(joinPath(head.string, tail.string)) func joinPath*(parts: varargs[Path]): Path = var estimatedLen = 0 @@ -116,6 +160,9 @@ func joinPath*(parts: varargs[Path]): Path = joinPathImpl(res, state, parts[i].string) result = Path(res) +func `/`*(head, tail: Path): Path {.inline.} = + result = joinPath(head, tail) + func splitPathImpl(path: string): tuple[head, tail: Path] = ## Splits a directory into `(head, tail)` tuple, so that ## ``head / tail == path`` (except for edge cases like "/usr"). @@ -150,9 +197,6 @@ func splitPathImpl(path: string): tuple[head, tail: Path] = break if sepPos >= 0: result.head = Path(substr(path, 0, - when (NimMajor, NimMinor) <= (1, 0): - sepPos-1 - else: if likely(sepPos >= 1): sepPos-1 else: 0 )) result.tail = Path(substr(path, sepPos+1)) @@ -166,3 +210,24 @@ func splitPathImpl(path: string): tuple[head, tail: Path] = func splitPath*(path: Path): tuple[head, tail: Path] {.inline.} = splitPathImpl(path.string) + +func extractFilename*(path: Path): Path = + ## Extracts the filename of a given `path`. + ## + ## This is the same as ``name & ext`` from `splitFile(path) proc`_. + ## + ## See also: + ## * `searchExtPos proc`_ + ## * `splitFile proc`_ + ## * `lastPathPart proc`_ + ## * `changeFileExt proc`_ + ## * `addFileExt proc`_ + runnableExamples: + assert extractFilename("foo/bar/") == "" + assert extractFilename("foo/bar") == "bar" + assert extractFilename("foo/bar.baz") == "bar.baz" + + if path.len == 0 or path.string[path.len-1] in {DirSep, AltSep}: + result = Path("") + else: + result = splitPath(path).tail From de5dce4bebd101454fc072a2b39877a232f0e245 Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Thu, 20 Oct 2022 22:23:14 +0800 Subject: [PATCH 03/13] add skelton --- lib/std/dirs.nim | 422 ++----------------------------------------- lib/std/files.nim | 136 ++------------ lib/std/paths.nim | 219 +--------------------- lib/std/symlinks.nim | 13 ++ 4 files changed, 43 insertions(+), 747 deletions(-) create mode 100644 lib/std/symlinks.nim diff --git a/lib/std/dirs.nim b/lib/std/dirs.nim index 9699dfd76c2d4..1fbe6253a8b08 100644 --- a/lib/std/dirs.nim +++ b/lib/std/dirs.nim @@ -1,416 +1,20 @@ -import paths, files +import paths -import std/oserrors +from std/private/osdirs import dirExists, createDir, existsOrCreateDir, removeDir, moveDir -const weirdTarget = defined(nimscript) or defined(js) -when weirdTarget: - {.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".} -else: - {.pragma: noWeirdTarget.} +proc dirExists*(dir: Path): bool {.tags: [ReadDirEffect].} = + result = dirExists(dir.string) +proc createDir*(dir: Path) {.tags: [WriteDirEffect, ReadDirEffect].} = + createDir(dir.string) -when defined(nimscript): - # for procs already defined in scriptconfig.nim - template noNimJs(body): untyped = discard -elif defined(js): - {.pragma: noNimJs, error: "this proc is not available on the js target".} -else: - {.pragma: noNimJs.} +proc existsOrCreateDir*(dir: Path): bool {.tags: [WriteDirEffect, ReadDirEffect].} = + result = existsOrCreateDir(dir.string) -when weirdTarget: - discard -elif defined(windows): - import winlean, times -elif defined(posix): - import posix, times +proc removeDir*(dir: Path, checkDir = false + ) {.tags: [WriteDirEffect, ReadDirEffect].} = + removeDir(dir.string, checkDir) - proc toTime(ts: Timespec): times.Time {.inline.} = - result = initTime(ts.tv_sec.int64, ts.tv_nsec.int) -else: - {.error: "OS module not ported to your operating system!".} - - -when defined(windows) and not weirdTarget: - template wrapUnary(varname, winApiProc, arg: untyped) = - var varname = winApiProc(newWideCString(arg)) - - template wrapBinary(varname, winApiProc, arg, arg2: untyped) = - var varname = winApiProc(newWideCString(arg), arg2) - proc findFirstFile(a: string, b: var WIN32_FIND_DATA): Handle = - result = findFirstFileW(newWideCString(a), b) - template findNextFile(a, b: untyped): untyped = findNextFileW(a, b) - template getCommandLine(): untyped = getCommandLineW() - - template getFilename(f: untyped): untyped = - $cast[WideCString](addr(f.cFileName[0])) - - proc skipFindData(f: WIN32_FIND_DATA): bool {.inline.} = - # Note - takes advantage of null delimiter in the cstring - const dot = ord('.') - result = f.cFileName[0].int == dot and (f.cFileName[1].int == 0 or - f.cFileName[1].int == dot and f.cFileName[2].int == 0) - - - -type - PathComponent* = enum ## Enumeration specifying a path component. - ## - ## See also: - ## * `walkDirRec iterator`_ - ## * `FileInfo object`_ - pcFile, ## path refers to a file - pcLinkToFile, ## path refers to a symbolic link to a file - pcDir, ## path refers to a directory - pcLinkToDir ## path refers to a symbolic link to a directory - -proc staticWalkDir(dir: string; relative: bool): seq[ - tuple[kind: PathComponent, path: string]] = - discard - -iterator walkDir*(dir: Path; relative = false, checkDir = false): - tuple[kind: PathComponent, path: Path] {.tags: [ReadDirEffect].} = - ## Walks over the directory `dir` and yields for each directory or file in - ## `dir`. The component type and full path for each item are returned. - ## - ## Walking is not recursive. If ``relative`` is true (default: false) - ## the resulting path is shortened to be relative to ``dir``. - ## - ## If `checkDir` is true, `OSError` is raised when `dir` - ## doesn't exist. - ## - ## **Example:** - ## - ## This directory structure: - ## - ## dirA / dirB / fileB1.txt - ## / dirC - ## / fileA1.txt - ## / fileA2.txt - ## - ## and this code: - runnableExamples("-r:off"): - import std/[strutils, sugar] - # note: order is not guaranteed - # this also works at compile time - assert collect(for k in walkDir("dirA"): k.path).join(" ") == - "dirA/dirB dirA/dirC dirA/fileA2.txt dirA/fileA1.txt" - ## See also: - ## * `walkPattern iterator`_ - ## * `walkFiles iterator`_ - ## * `walkDirs iterator`_ - ## * `walkDirRec iterator`_ - - when nimvm: - for k, v in items(staticWalkDir(dir.string, relative)): - yield (k, Path(v)) - else: - when weirdTarget: - for k, v in items(staticWalkDir(dir, relative)): - yield (k, v) - elif defined(windows): - var f: WIN32_FIND_DATA - var h = findFirstFile(string(dir / Path("*")), f) - if h == -1: - if checkDir: - raiseOSError(osLastError(), dir.string) - else: - defer: findClose(h) - while true: - var k = pcFile - if not skipFindData(f): - if (f.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) != 0'i32: - k = pcDir - if (f.dwFileAttributes and FILE_ATTRIBUTE_REPARSE_POINT) != 0'i32: - k = succ(k) - let xx = if relative: extractFilename(Path(getFilename(f))) - else: dir / extractFilename(Path(getFilename(f))) - yield (k, xx) - if findNextFile(h, f) == 0'i32: - let errCode = getLastError() - if errCode == ERROR_NO_MORE_FILES: break - else: raiseOSError(errCode.OSErrorCode) - else: - var d = opendir(dir) - if d == nil: - if checkDir: - raiseOSError(osLastError(), dir) - else: - defer: discard closedir(d) - while true: - var x = readdir(d) - if x == nil: break - var y = $cstring(addr x.d_name) - if y != "." and y != "..": - var s: Stat - let path = dir / y - if not relative: - y = path - var k = pcFile - - template kSetGeneric() = # pure Posix component `k` resolution - if lstat(path.cstring, s) < 0'i32: continue # don't yield - elif S_ISDIR(s.st_mode): - k = pcDir - elif S_ISLNK(s.st_mode): - k = getSymlinkFileKind(path) - - when defined(linux) or defined(macosx) or - defined(bsd) or defined(genode) or defined(nintendoswitch): - case x.d_type - of DT_DIR: k = pcDir - of DT_LNK: - if dirExists(path): k = pcLinkToDir - else: k = pcLinkToFile - of DT_UNKNOWN: - kSetGeneric() - else: # e.g. DT_REG etc - discard # leave it as pcFile - else: # assuming that field `d_type` is not present - kSetGeneric() - - yield (k, y) - -iterator walkDirRec*(dir: Path, - yieldFilter = {pcFile}, followFilter = {pcDir}, - relative = false, checkDir = false): Path {.tags: [ReadDirEffect].} = - ## Recursively walks over the directory `dir` and yields for each file - ## or directory in `dir`. - ## - ## If ``relative`` is true (default: false) the resulting path is - ## shortened to be relative to ``dir``, otherwise the full path is returned. - ## - ## If `checkDir` is true, `OSError` is raised when `dir` - ## doesn't exist. - ## - ## .. warning:: Modifying the directory structure while the iterator - ## is traversing may result in undefined behavior! - ## - ## Walking is recursive. `followFilter` controls the behaviour of the iterator: - ## - ## ===================== ============================================= - ## yieldFilter meaning - ## ===================== ============================================= - ## ``pcFile`` yield real files (default) - ## ``pcLinkToFile`` yield symbolic links to files - ## ``pcDir`` yield real directories - ## ``pcLinkToDir`` yield symbolic links to directories - ## ===================== ============================================= - ## - ## ===================== ============================================= - ## followFilter meaning - ## ===================== ============================================= - ## ``pcDir`` follow real directories (default) - ## ``pcLinkToDir`` follow symbolic links to directories - ## ===================== ============================================= - ## - ## - ## See also: - ## * `walkPattern iterator`_ - ## * `walkFiles iterator`_ - ## * `walkDirs iterator`_ - ## * `walkDir iterator`_ - - var stack = newseq[Path]() - var checkDir = checkDir - while stack.len > 0: - let d = stack.pop() - for k, p in walkDir(dir / d, relative = true, checkDir = checkDir): - let rel = d / p - if k in {pcDir, pcLinkToDir} and k in followFilter: - stack.add rel - if k in yieldFilter: - yield if relative: rel else: dir / rel - checkDir = false - # We only check top-level dir, otherwise if a subdir is invalid (eg. wrong - # permissions), it'll abort iteration and there would be no way to - # continue iteration. - # Future work can provide a way to customize this and do error reporting. - -proc rawRemoveDir(dir: string) {.noWeirdTarget.} = - when defined(windows): - wrapUnary(res, removeDirectoryW, dir) - let lastError = osLastError() - if res == 0'i32 and lastError.int32 != 3'i32 and - lastError.int32 != 18'i32 and lastError.int32 != 2'i32: - raiseOSError(lastError, dir) - else: - if rmdir(dir) != 0'i32 and errno != ENOENT: raiseOSError(osLastError(), dir) - -proc removeDir*(dir: Path, checkDir = false) {.tags: [ - WriteDirEffect, ReadDirEffect], gcsafe.} = - ## Removes the directory `dir` including all subdirectories and files - ## in `dir` (recursively). - ## - ## If this fails, `OSError` is raised. This does not fail if the directory never - ## existed in the first place, unless `checkDir` = true. - ## - ## See also: - ## * `tryRemoveFile proc`_ - ## * `removeFile proc`_ - ## * `existsOrCreateDir proc`_ - ## * `createDir proc`_ - ## * `copyDir proc`_ - ## * `copyDirWithPermissions proc`_ - ## * `moveDir proc`_ - for kind, path in walkDir(dir, checkDir = checkDir): - case kind - of pcFile, pcLinkToFile, pcLinkToDir: removeFile(path) - of pcDir: removeDir(path, true) - # for subdirectories there is no benefit in `checkDir = false` - # (unless perhaps for edge case of concurrent processes also deleting - # the same files) - rawRemoveDir(dir.string) - -proc dirExists*(dir: Path): bool {.tags: [ReadDirEffect], - noNimJs.} = - ## Returns true if the directory `dir` exists. If `dir` is a file, false - ## is returned. Follows symlinks. - ## - ## See also: - ## * `fileExists proc`_ - ## * `symlinkExists proc`_ - when defined(windows): - wrapUnary(a, getFileAttributesW, dir.string) - if a != -1'i32: - result = (a and FILE_ATTRIBUTE_DIRECTORY) != 0'i32 - else: - var res: Stat - result = stat(dir.string, res) >= 0'i32 and S_ISDIR(res.st_mode) - -proc rawCreateDir(dir: string): bool {.noWeirdTarget.} = - # Try to create one directory (not the whole path). - # returns `true` for success, `false` if the path has previously existed - # - # This is a thin wrapper over mkDir (or alternatives on other systems), - # so in case of a pre-existing path we don't check that it is a directory. - when defined(solaris): - let res = mkdir(dir, 0o777) - if res == 0'i32: - result = true - elif errno in {EEXIST, ENOSYS}: - result = false - else: - raiseOSError(osLastError(), dir) - elif defined(haiku): - let res = mkdir(dir, 0o777) - if res == 0'i32: - result = true - elif errno == EEXIST or errno == EROFS: - result = false - else: - raiseOSError(osLastError(), dir) - elif defined(posix): - let res = mkdir(dir, 0o777) - if res == 0'i32: - result = true - elif errno == EEXIST: - result = false - else: - #echo res - raiseOSError(osLastError(), dir) - else: - wrapUnary(res, createDirectoryW, dir) - - if res != 0'i32: - result = true - elif getLastError() == 183'i32: - result = false - else: - raiseOSError(osLastError(), dir) - -proc existsOrCreateDir*(dir: Path): bool {. - tags: [WriteDirEffect, ReadDirEffect], noWeirdTarget.} = - ## Checks if a `directory`:idx: `dir` exists, and creates it otherwise. - ## - ## Does not create parent directories (raises `OSError` if parent directories do not exist). - ## Returns `true` if the directory already exists, and `false` otherwise. - ## - ## See also: - ## * `removeDir proc`_ - ## * `createDir proc`_ - ## * `copyDir proc`_ - ## * `copyDirWithPermissions proc`_ - ## * `moveDir proc`_ - result = not rawCreateDir(dir.string) - if result: - # path already exists - need to check that it is indeed a directory - if not dirExists(dir): - raise newException(IOError, "Failed to create '" & dir.string & "'") - -# proc createDir*(dir: Path) {. -# tags: [WriteDirEffect, ReadDirEffect], noWeirdTarget.} = -# ## Creates the `directory`:idx: `dir`. -# ## -# ## The directory may contain several subdirectories that do not exist yet. -# ## The full path is created. If this fails, `OSError` is raised. -# ## -# ## It does **not** fail if the directory already exists because for -# ## most usages this does not indicate an error. -# ## -# ## See also: -# ## * `removeDir proc`_ -# ## * `existsOrCreateDir proc`_ -# ## * `copyDir proc`_ -# ## * `copyDirWithPermissions proc`_ -# ## * `moveDir proc`_ -# if dir.len == 0: -# return -# var omitNext = isAbsolute(dir) -# for p in parentDirs(dir, fromRoot=true): -# if omitNext: -# omitNext = false -# else: -# discard existsOrCreateDir(p) - -# proc copyDir*(source, dest: string) {.tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect], gcsafe, noWeirdTarget.} = -# ## Copies a directory from `source` to `dest`. -# ## -# ## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks -# ## are skipped. -# ## -# ## If this fails, `OSError` is raised. -# ## -# ## On the Windows platform this proc will copy the attributes from -# ## `source` into `dest`. -# ## -# ## On other platforms created files and directories will inherit the -# ## default permissions of a newly created file/directory for the user. -# ## Use `copyDirWithPermissions proc`_ -# ## to preserve attributes recursively on these platforms. -# ## -# ## See also: -# ## * `copyDirWithPermissions proc`_ -# ## * `copyFile proc`_ -# ## * `copyFileWithPermissions proc`_ -# ## * `removeDir proc`_ -# ## * `existsOrCreateDir proc`_ -# ## * `createDir proc`_ -# ## * `moveDir proc`_ -# createDir(dest) -# for kind, path in walkDir(source): -# var noSource = splitPath(path).tail -# if kind == pcDir: -# copyDir(path, dest / noSource) -# else: -# copyFile(path, dest / noSource, {cfSymlinkAsIs}) - -# proc moveDir*(source, dest: string) {.tags: [ReadIOEffect, WriteIOEffect], noWeirdTarget.} = -# ## Moves a directory from `source` to `dest`. -# ## -# ## Symlinks are not followed: if `source` contains symlinks, they themself are -# ## moved, not their target. -# ## -# ## If this fails, `OSError` is raised. -# ## -# ## See also: -# ## * `moveFile proc`_ -# ## * `copyDir proc`_ -# ## * `copyDirWithPermissions proc`_ -# ## * `removeDir proc`_ -# ## * `existsOrCreateDir proc`_ -# ## * `createDir proc`_ -# if not tryMoveFSObject(source, dest, isDir = true): -# # Fallback to copy & del -# copyDir(source, dest) -# removeDir(source) \ No newline at end of file +proc moveDir*(source, dest: Path) {.tags: [ReadIOEffect, WriteIOEffect].} = + moveDir(source.string, dest.string) diff --git a/lib/std/files.nim b/lib/std/files.nim index 2eb051d00b1c7..dc03140643566 100644 --- a/lib/std/files.nim +++ b/lib/std/files.nim @@ -1,132 +1,18 @@ import paths -import std/oserrors +from std/private/osfiles import fileExists, tryRemoveFile, removeFile, + removeFile, moveFile +proc fileExists*(filename: Path): bool {.inline, tags: [ReadDirEffect].} = + result = fileExists(filename.string) -const weirdTarget = defined(nimscript) or defined(js) +proc tryRemoveFile*(file: Path): bool {.inline, tags: [WriteDirEffect].} = + result = tryRemoveFile(file.string) -when weirdTarget: - {.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".} -else: - {.pragma: noWeirdTarget.} +proc removeFile*(file: Path) {.inline, tags: [WriteDirEffect].} = + removeFile(file.string) - -when weirdTarget: - discard -elif defined(windows): - import winlean, times -elif defined(posix): - import posix, times - - proc toTime(ts: Timespec): times.Time {.inline.} = - result = initTime(ts.tv_sec.int64, ts.tv_nsec.int) -else: - {.error: "OS module not ported to your operating system!".} - - -when defined(windows) and not weirdTarget: - template deleteFile(file: untyped): untyped = deleteFileW(file) - template setFileAttributes(file, attrs: untyped): untyped = - setFileAttributesW(file, attrs) - -proc tryRemoveFile*(file: Path): bool {.tags: [WriteDirEffect], noWeirdTarget.} = - ## Removes the `file`. - ## - ## If this fails, returns `false`. This does not fail - ## if the file never existed in the first place. - ## - ## On Windows, ignores the read-only attribute. - ## - ## See also: - ## * `copyFile proc`_ - ## * `copyFileWithPermissions proc`_ - ## * `removeFile proc`_ - ## * `moveFile proc`_ - result = true - when defined(windows): - let f = newWideCString(file.string) - if deleteFile(f) == 0: - result = false - let err = getLastError() - if err == ERROR_FILE_NOT_FOUND or err == ERROR_PATH_NOT_FOUND: - result = true - elif err == ERROR_ACCESS_DENIED and - setFileAttributes(f, FILE_ATTRIBUTE_NORMAL) != 0 and - deleteFile(f) != 0: - result = true - else: - if unlink(file) != 0'i32 and errno != ENOENT: - result = false - -proc removeFile*(file: Path) {.tags: [WriteDirEffect], noWeirdTarget.} = - ## Removes the `file`. - ## - ## If this fails, `OSError` is raised. This does not fail - ## if the file never existed in the first place. - ## - ## On Windows, ignores the read-only attribute. - ## - ## See also: - ## * `removeDir proc`_ - ## * `copyFile proc`_ - ## * `copyFileWithPermissions proc`_ - ## * `tryRemoveFile proc`_ - ## * `moveFile proc`_ - if not tryRemoveFile(file): - raiseOSError(osLastError(), file.string) - -proc tryMoveFSObject(source, dest: string, isDir: bool): bool {.noWeirdTarget.} = - ## Moves a file (or directory if `isDir` is true) from `source` to `dest`. - ## - ## Returns false in case of `EXDEV` error or `AccessDeniedError` on Windows (if `isDir` is true). - ## In case of other errors `OSError` is raised. - ## Returns true in case of success. - when defined(windows): - let s = newWideCString(source) - let d = newWideCString(dest) - result = moveFileExW(s, d, MOVEFILE_COPY_ALLOWED or MOVEFILE_REPLACE_EXISTING) != 0'i32 - else: - result = c_rename(source, dest) == 0'i32 - - if not result: - let err = osLastError() - let isAccessDeniedError = - when defined(windows): - const AccessDeniedError = OSErrorCode(5) - isDir and err == AccessDeniedError - else: - err == EXDEV.OSErrorCode - if not isAccessDeniedError: - raiseOSError(err, $(source, dest)) - -proc moveFile*(source, dest: Path) {. - tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect], noWeirdTarget.} = - ## Moves a file from `source` to `dest`. - ## - ## Symlinks are not followed: if `source` is a symlink, it is itself moved, - ## not its target. - ## - ## If this fails, `OSError` is raised. - ## If `dest` already exists, it will be overwritten. - ## - ## Can be used to `rename files`:idx:. - ## - ## See also: - ## * `moveDir proc`_ - ## * `copyFile proc`_ - ## * `copyFileWithPermissions proc`_ - ## * `removeFile proc`_ - ## * `tryRemoveFile proc`_ - - if not tryMoveFSObject(source.string, dest.string, isDir = false): - when defined(windows): - doAssert false - else: - # Fallback to copy & del - copyFile(source, dest, {cfSymlinkAsIs}) - try: - removeFile(source) - except: - discard tryRemoveFile(dest) - raise +proc moveFile*(source, dest: Path) {.inline, + tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect].} = + moveFile(source.string, dest.string) diff --git a/lib/std/paths.nim b/lib/std/paths.nim index 4fb30feee8663..b9c5953a01725 100644 --- a/lib/std/paths.nim +++ b/lib/std/paths.nim @@ -1,152 +1,10 @@ -import includes/osseps - -include system/inclrtl -import std/private/since - -import strutils, pathnorm - -when defined(nimPreviewSlimSystem): - import std/[syncio, assertions] - -type - ReadDirEffect* = object of ReadIOEffect ## Effect that denotes a read - ## operation from the directory - ## structure. - WriteDirEffect* = object of WriteIOEffect ## Effect that denotes a write - ## operation to - ## the directory structure. - -proc normalizePathEnd(path: var string, trailingSep = false) = - ## Ensures ``path`` has exactly 0 or 1 trailing `DirSep`, depending on - ## ``trailingSep``, and taking care of edge cases: it preservers whether - ## a path is absolute or relative, and makes sure trailing sep is `DirSep`, - ## not `AltSep`. Trailing `/.` are compressed, see examples. - if path.len == 0: return - var i = path.len - while i >= 1: - if path[i-1] in {DirSep, AltSep}: dec(i) - elif path[i-1] == '.' and i >= 2 and path[i-2] in {DirSep, AltSep}: dec(i) - else: break - if trailingSep: - # foo// => foo - path.setLen(i) - # foo => foo/ - path.add DirSep - elif i > 0: - # foo// => foo - path.setLen(i) - else: - # // => / (empty case was already taken care of) - path = $DirSep - -proc normalizePathEnd(path: string, trailingSep = false): string = - ## outplace overload - runnableExamples: - when defined(posix): - assert normalizePathEnd("/lib//.//", trailingSep = true) == "/lib/" - assert normalizePathEnd("lib/./.", trailingSep = false) == "lib" - assert normalizePathEnd(".//./.", trailingSep = false) == "." - assert normalizePathEnd("", trailingSep = true) == "" # not / ! - assert normalizePathEnd("/", trailingSep = false) == "/" # not "" ! - result = path - result.normalizePathEnd(trailingSep) - - -template endsWith(a: string, b: set[char]): bool = - a.len > 0 and a[^1] in b - -proc joinPathImpl(result: var string, state: var int, tail: string) = - let trailingSep = tail.endsWith({DirSep, AltSep}) or tail.len == 0 and result.endsWith({DirSep, AltSep}) - normalizePathEnd(result, trailingSep=false) - addNormalizePath(tail, result, state, DirSep) - normalizePathEnd(result, trailingSep=trailingSep) - - -func joinPath(head, tail: string): string = - ## Joins two directory names to one. - ## - ## returns normalized path concatenation of `head` and `tail`, preserving - ## whether or not `tail` has a trailing slash (or, if tail if empty, whether - ## head has one). - ## - ## See also: - ## * `joinPath(parts: varargs[string]) proc`_ - ## * `/ proc`_ - ## * `splitPath proc`_ - ## * `uri.combine proc `_ - ## * `uri./ proc `_ - runnableExamples: - when defined(posix): - assert joinPath("usr", "lib") == "usr/lib" - assert joinPath("usr", "lib/") == "usr/lib/" - assert joinPath("usr", "") == "usr" - assert joinPath("usr/", "") == "usr/" - assert joinPath("", "") == "" - assert joinPath("", "lib") == "lib" - assert joinPath("", "/lib") == "/lib" - assert joinPath("usr/", "/lib") == "usr/lib" - assert joinPath("usr/lib", "../bin") == "usr/bin" - - result = newStringOfCap(head.len + tail.len) - var state = 0 - joinPathImpl(result, state, head) - joinPathImpl(result, state, tail) - when false: - if len(head) == 0: - result = tail - elif head[len(head)-1] in {DirSep, AltSep}: - if tail.len > 0 and tail[0] in {DirSep, AltSep}: - result = head & substr(tail, 1) - else: - result = head & tail - else: - if tail.len > 0 and tail[0] in {DirSep, AltSep}: - result = head & tail - else: - result = head & DirSep & tail - -func isAbsoluteImpl(path: string): bool {.raises: [].} = - ## Checks whether a given `path` is absolute. - ## - ## On Windows, network paths are considered absolute too. - runnableExamples: - assert not "".isAbsolute - assert not ".".isAbsolute - when defined(posix): - assert "/".isAbsolute - assert not "a/".isAbsolute - assert "/a/".isAbsolute - - if len(path) == 0: return false - - when doslikeFileSystem: - var len = len(path) - result = (path[0] in {'/', '\\'}) or - (len > 1 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':') - elif defined(macos): - # according to https://perldoc.perl.org/File/Spec/Mac.html `:a` is a relative path - result = path[0] != ':' - elif defined(RISCOS): - result = path[0] == '$' - elif defined(posix) or defined(js): - # `or defined(js)` wouldn't be needed pending https://github.com/nim-lang/Nim/issues/13469 - # This works around the problem for posix, but Windows is still broken with nim js -d:nodejs - result = path[0] == '/' - else: - doAssert false # if ever hits here, adapt as needed - -when doslikeFileSystem: - import std/private/ntpath +from std/private/ospaths2 {.all.} import joinPathImpl, joinPath, splitPath, + ReadDirEffect, WriteDirEffect +export ReadDirEffect, WriteDirEffect type Path* = distinct string -func len*(x: Path): int = - len(string(x)) - - -func isAbsolute*(path: Path): bool {.inline, raises: [].} = - result = isAbsoluteImpl(path.string) func joinPath*(head, tail: Path): Path {.inline.} = result = Path(joinPath(head.string, tail.string)) @@ -161,73 +19,8 @@ func joinPath*(parts: varargs[Path]): Path = result = Path(res) func `/`*(head, tail: Path): Path {.inline.} = - result = joinPath(head, tail) - -func splitPathImpl(path: string): tuple[head, tail: Path] = - ## Splits a directory into `(head, tail)` tuple, so that - ## ``head / tail == path`` (except for edge cases like "/usr"). - ## - ## See also: - ## * `joinPath(head, tail) proc`_ - ## * `joinPath(parts: varargs[string]) proc`_ - ## * `/ proc`_ - ## * `/../ proc`_ - ## * `relativePath proc`_ - runnableExamples: - assert splitPath("usr/local/bin") == ("usr/local", "bin") - assert splitPath("usr/local/bin/") == ("usr/local/bin", "") - assert splitPath("/bin/") == ("/bin", "") - when (NimMajor, NimMinor) <= (1, 0): - assert splitPath("/bin") == ("", "bin") - else: - assert splitPath("/bin") == ("/", "bin") - assert splitPath("bin") == ("", "bin") - assert splitPath("") == ("", "") - - when doslikeFileSystem: - let (drive, splitpath) = splitDrive(path) - let stop = drive.len - else: - const stop = 0 - - var sepPos = -1 - for i in countdown(len(path)-1, stop): - if path[i] in {DirSep, AltSep}: - sepPos = i - break - if sepPos >= 0: - result.head = Path(substr(path, 0, - if likely(sepPos >= 1): sepPos-1 else: 0 - )) - result.tail = Path(substr(path, sepPos+1)) - else: - when doslikeFileSystem: - result.head = Path(drive) - result.tail = Path(splitpath) - else: - result.head = Path("") - result.tail = Path(path) + joinPath(head, tail) func splitPath*(path: Path): tuple[head, tail: Path] {.inline.} = - splitPathImpl(path.string) - -func extractFilename*(path: Path): Path = - ## Extracts the filename of a given `path`. - ## - ## This is the same as ``name & ext`` from `splitFile(path) proc`_. - ## - ## See also: - ## * `searchExtPos proc`_ - ## * `splitFile proc`_ - ## * `lastPathPart proc`_ - ## * `changeFileExt proc`_ - ## * `addFileExt proc`_ - runnableExamples: - assert extractFilename("foo/bar/") == "" - assert extractFilename("foo/bar") == "bar" - assert extractFilename("foo/bar.baz") == "bar.baz" - - if path.len == 0 or path.string[path.len-1] in {DirSep, AltSep}: - result = Path("") - else: - result = splitPath(path).tail + let res = splitPath(path.string) + result = (Path(res.head), Path(res.tail)) diff --git a/lib/std/symlinks.nim b/lib/std/symlinks.nim new file mode 100644 index 0000000000000..94a4b2050af1d --- /dev/null +++ b/lib/std/symlinks.nim @@ -0,0 +1,13 @@ +import paths + +from std/private/ossymlinks import symlinkExists, createSymlink, expandSymlink + + +proc symlinkExists*(link: Path): bool {.tags: [ReadDirEffect].} = + result = symlinkExists(link.string) + +proc createSymlink*(src, dest: Path) = + createSymlink(src.string, dest.string) + +proc expandSymlink*(symlinkPath: Path): Path = + result = expandSymlink(symlinkPath) From 8e73594875a100c1631f9d54933667752bbc703f Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Fri, 21 Oct 2022 00:39:18 +0800 Subject: [PATCH 04/13] polish --- lib/std/dirs.nim | 30 ++++++++++++++++++++++++++++-- lib/std/files.nim | 2 +- lib/std/symlinks.nim | 4 ++-- test.nim | 23 +++++++++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 test.nim diff --git a/lib/std/dirs.nim b/lib/std/dirs.nim index 1fbe6253a8b08..3b45b3c349b9b 100644 --- a/lib/std/dirs.nim +++ b/lib/std/dirs.nim @@ -1,7 +1,10 @@ -import paths +from paths import Path, ReadDirEffect, WriteDirEffect -from std/private/osdirs import dirExists, createDir, existsOrCreateDir, removeDir, moveDir +from std/private/osdirs import dirExists, createDir, existsOrCreateDir, removeDir, + moveDir, walkPattern, walkFiles, walkDirs, walkDir, + walkDirRec, PathComponent +export PathComponent proc dirExists*(dir: Path): bool {.tags: [ReadDirEffect].} = result = dirExists(dir.string) @@ -18,3 +21,26 @@ proc removeDir*(dir: Path, checkDir = false proc moveDir*(source, dest: Path) {.tags: [ReadIOEffect, WriteIOEffect].} = moveDir(source.string, dest.string) + +iterator walkPattern*(pattern: Path): Path {.tags: [ReadDirEffect].} = + for p in walkPattern(pattern.string): + yield Path(p) + +iterator walkFiles*(pattern: Path): Path {.tags: [ReadDirEffect].} = + for p in walkFiles(pattern.string): + yield Path(p) + +iterator walkDirs*(pattern: Path): Path {.tags: [ReadDirEffect].} = + for p in walkDirs(pattern.string): + yield Path(p) + +iterator walkDir*(dir: Path; relative = false, checkDir = false): + tuple[kind: PathComponent, path: Path] {.tags: [ReadDirEffect].} = + for (k, p) in walkDir(dir.string, relative, checkDir): + yield (k, Path(p)) + +iterator walkDirRec*(dir: Path, + yieldFilter = {pcFile}, followFilter = {pcDir}, + relative = false, checkDir = false): Path {.tags: [ReadDirEffect].} = + for p in walkDirRec(dir.string, yieldFilter, followFilter, relative, checkDir): + yield Path(p) diff --git a/lib/std/files.nim b/lib/std/files.nim index dc03140643566..afb1c626dbe17 100644 --- a/lib/std/files.nim +++ b/lib/std/files.nim @@ -1,4 +1,4 @@ -import paths +from paths import Path, ReadDirEffect, WriteDirEffect from std/private/osfiles import fileExists, tryRemoveFile, removeFile, removeFile, moveFile diff --git a/lib/std/symlinks.nim b/lib/std/symlinks.nim index 94a4b2050af1d..e31431b5aa0a4 100644 --- a/lib/std/symlinks.nim +++ b/lib/std/symlinks.nim @@ -1,4 +1,4 @@ -import paths +from paths import Path, ReadDirEffect from std/private/ossymlinks import symlinkExists, createSymlink, expandSymlink @@ -10,4 +10,4 @@ proc createSymlink*(src, dest: Path) = createSymlink(src.string, dest.string) proc expandSymlink*(symlinkPath: Path): Path = - result = expandSymlink(symlinkPath) + result = Path(expandSymlink(symlinkPath.string)) diff --git a/test.nim b/test.nim new file mode 100644 index 0000000000000..762564b764944 --- /dev/null +++ b/test.nim @@ -0,0 +1,23 @@ +var g: seq[int] + +proc j(b: int) = + g.insert(0, 0) + for i in 0 ..< 1: + discard + +proc c(w: int) = discard + +c(0) +c(0) +c(0) +c(0) +c(0) +c(0) +c(0) +c(0) +c(0) +c(0) +c(0) +c(0) + +j(3) \ No newline at end of file From 3ee131728ea79b7e9880c1635182f374d8ad726d Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Fri, 21 Oct 2022 20:00:57 +0800 Subject: [PATCH 05/13] add documentation --- lib/std/dirs.nim | 148 ++++++++++++++++++++++-- lib/std/files.nim | 44 +++++++ lib/std/paths.nim | 266 ++++++++++++++++++++++++++++++++++++++++++- lib/std/symlinks.nim | 23 +++- 4 files changed, 469 insertions(+), 12 deletions(-) diff --git a/lib/std/dirs.nim b/lib/std/dirs.nim index 3b45b3c349b9b..f78ac9d99495c 100644 --- a/lib/std/dirs.nim +++ b/lib/std/dirs.nim @@ -6,41 +6,173 @@ from std/private/osdirs import dirExists, createDir, existsOrCreateDir, removeDi export PathComponent -proc dirExists*(dir: Path): bool {.tags: [ReadDirEffect].} = +proc dirExists*(dir: Path): bool {.inline, tags: [ReadDirEffect].} = + ## Returns true if the directory `dir` exists. If `dir` is a file, false + ## is returned. Follows symlinks. result = dirExists(dir.string) -proc createDir*(dir: Path) {.tags: [WriteDirEffect, ReadDirEffect].} = +proc createDir*(dir: Path) {.inline, tags: [WriteDirEffect, ReadDirEffect].} = + ## Creates the `directory`:idx: `dir`. + ## + ## The directory may contain several subdirectories that do not exist yet. + ## The full path is created. If this fails, `OSError` is raised. + ## + ## It does **not** fail if the directory already exists because for + ## most usages this does not indicate an error. + ## + ## See also: + ## * `removeDir proc`_ + ## * `existsOrCreateDir proc`_ + ## * `copyDir proc`_ + ## * `copyDirWithPermissions proc`_ + ## * `moveDir proc`_ createDir(dir.string) -proc existsOrCreateDir*(dir: Path): bool {.tags: [WriteDirEffect, ReadDirEffect].} = +proc existsOrCreateDir*(dir: Path): bool {.inline, tags: [WriteDirEffect, ReadDirEffect].} = + ## Checks if a `directory`:idx: `dir` exists, and creates it otherwise. + ## + ## Does not create parent directories (raises `OSError` if parent directories do not exist). + ## Returns `true` if the directory already exists, and `false` otherwise. + ## + ## See also: + ## * `removeDir proc`_ + ## * `createDir proc`_ + ## * `copyDir proc`_ + ## * `copyDirWithPermissions proc`_ + ## * `moveDir proc`_ result = existsOrCreateDir(dir.string) proc removeDir*(dir: Path, checkDir = false - ) {.tags: [WriteDirEffect, ReadDirEffect].} = + ) {.inline, tags: [WriteDirEffect, ReadDirEffect].} = + ## Removes the directory `dir` including all subdirectories and files + ## in `dir` (recursively). + ## + ## If this fails, `OSError` is raised. This does not fail if the directory never + ## existed in the first place, unless `checkDir` = true. + ## + ## See also: + ## * `tryRemoveFile proc`_ + ## * `removeFile proc`_ + ## * `existsOrCreateDir proc`_ + ## * `createDir proc`_ + ## * `copyDir proc`_ + ## * `copyDirWithPermissions proc`_ + ## * `moveDir proc`_ removeDir(dir.string, checkDir) -proc moveDir*(source, dest: Path) {.tags: [ReadIOEffect, WriteIOEffect].} = +proc moveDir*(source, dest: Path) {.inline, tags: [ReadIOEffect, WriteIOEffect].} = + ## Moves a directory from `source` to `dest`. + ## + ## Symlinks are not followed: if `source` contains symlinks, they themself are + ## moved, not their target. + ## + ## If this fails, `OSError` is raised. + ## + ## See also: + ## * `moveFile proc`_ + ## * `copyDir proc`_ + ## * `copyDirWithPermissions proc`_ + ## * `removeDir proc`_ + ## * `existsOrCreateDir proc`_ + ## * `createDir proc`_ moveDir(source.string, dest.string) iterator walkPattern*(pattern: Path): Path {.tags: [ReadDirEffect].} = + ## Iterate over all the files and directories that match the `pattern`. + ## + ## On POSIX this uses the `glob`:idx: call. + ## `pattern` is OS dependent, but at least the `"*.ext"` + ## notation is supported. + ## + ## See also: + ## * `walkFiles iterator`_ + ## * `walkDirs iterator`_ + ## * `walkDir iterator`_ + ## * `walkDirRec iterator`_ for p in walkPattern(pattern.string): yield Path(p) iterator walkFiles*(pattern: Path): Path {.tags: [ReadDirEffect].} = + ## Iterate over all the files that match the `pattern`. + ## + ## On POSIX this uses the `glob`:idx: call. + ## `pattern` is OS dependent, but at least the `"*.ext"` + ## notation is supported. + ## + ## See also: + ## * `walkPattern iterator`_ + ## * `walkDirs iterator`_ + ## * `walkDir iterator`_ + ## * `walkDirRec iterator`_ for p in walkFiles(pattern.string): yield Path(p) iterator walkDirs*(pattern: Path): Path {.tags: [ReadDirEffect].} = + ## Iterate over all the directories that match the `pattern`. + ## + ## On POSIX this uses the `glob`:idx: call. + ## `pattern` is OS dependent, but at least the `"*.ext"` + ## notation is supported. + ## + ## See also: + ## * `walkPattern iterator`_ + ## * `walkFiles iterator`_ + ## * `walkDir iterator`_ + ## * `walkDirRec iterator`_ for p in walkDirs(pattern.string): yield Path(p) iterator walkDir*(dir: Path; relative = false, checkDir = false): - tuple[kind: PathComponent, path: Path] {.tags: [ReadDirEffect].} = - for (k, p) in walkDir(dir.string, relative, checkDir): - yield (k, Path(p)) + tuple[kind: PathComponent, path: Path] {.tags: [ReadDirEffect].} = + ## Walks over the directory `dir` and yields for each directory or file in + ## `dir`. The component type and full path for each item are returned. + ## + ## Walking is not recursive. If ``relative`` is true (default: false) + ## the resulting path is shortened to be relative to ``dir``. + ## + ## If `checkDir` is true, `OSError` is raised when `dir` + ## doesn't exist. + for (k, p) in walkDir(dir.string, relative, checkDir): + yield (k, Path(p)) iterator walkDirRec*(dir: Path, yieldFilter = {pcFile}, followFilter = {pcDir}, relative = false, checkDir = false): Path {.tags: [ReadDirEffect].} = + ## Recursively walks over the directory `dir` and yields for each file + ## or directory in `dir`. + ## + ## If ``relative`` is true (default: false) the resulting path is + ## shortened to be relative to ``dir``, otherwise the full path is returned. + ## + ## If `checkDir` is true, `OSError` is raised when `dir` + ## doesn't exist. + ## + ## .. warning:: Modifying the directory structure while the iterator + ## is traversing may result in undefined behavior! + ## + ## Walking is recursive. `followFilter` controls the behaviour of the iterator: + ## + ## ===================== ============================================= + ## yieldFilter meaning + ## ===================== ============================================= + ## ``pcFile`` yield real files (default) + ## ``pcLinkToFile`` yield symbolic links to files + ## ``pcDir`` yield real directories + ## ``pcLinkToDir`` yield symbolic links to directories + ## ===================== ============================================= + ## + ## ===================== ============================================= + ## followFilter meaning + ## ===================== ============================================= + ## ``pcDir`` follow real directories (default) + ## ``pcLinkToDir`` follow symbolic links to directories + ## ===================== ============================================= + ## + ## + ## See also: + ## * `walkPattern iterator`_ + ## * `walkFiles iterator`_ + ## * `walkDirs iterator`_ + ## * `walkDir iterator`_ for p in walkDirRec(dir.string, yieldFilter, followFilter, relative, checkDir): yield Path(p) diff --git a/lib/std/files.nim b/lib/std/files.nim index afb1c626dbe17..d036fdf6bef06 100644 --- a/lib/std/files.nim +++ b/lib/std/files.nim @@ -5,14 +5,58 @@ from std/private/osfiles import fileExists, tryRemoveFile, removeFile, proc fileExists*(filename: Path): bool {.inline, tags: [ReadDirEffect].} = + ## Returns true if `filename` exists and is a regular file or symlink. + ## + ## Directories, device files, named pipes and sockets return false. result = fileExists(filename.string) proc tryRemoveFile*(file: Path): bool {.inline, tags: [WriteDirEffect].} = + ## Removes the `file`. + ## + ## If this fails, returns `false`. This does not fail + ## if the file never existed in the first place. + ## + ## On Windows, ignores the read-only attribute. + ## + ## See also: + ## * `copyFile proc`_ + ## * `copyFileWithPermissions proc`_ + ## * `removeFile proc`_ + ## * `moveFile proc`_ result = tryRemoveFile(file.string) proc removeFile*(file: Path) {.inline, tags: [WriteDirEffect].} = + ## Removes the `file`. + ## + ## If this fails, `OSError` is raised. This does not fail + ## if the file never existed in the first place. + ## + ## On Windows, ignores the read-only attribute. + ## + ## See also: + ## * `removeDir proc`_ + ## * `copyFile proc`_ + ## * `copyFileWithPermissions proc`_ + ## * `tryRemoveFile proc`_ + ## * `moveFile proc`_ removeFile(file.string) proc moveFile*(source, dest: Path) {.inline, tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect].} = + ## Moves a file from `source` to `dest`. + ## + ## Symlinks are not followed: if `source` is a symlink, it is itself moved, + ## not its target. + ## + ## If this fails, `OSError` is raised. + ## If `dest` already exists, it will be overwritten. + ## + ## Can be used to `rename files`:idx:. + ## + ## See also: + ## * `moveDir proc`_ + ## * `copyFile proc`_ + ## * `copyFileWithPermissions proc`_ + ## * `removeFile proc`_ + ## * `tryRemoveFile proc`_ moveFile(source.string, dest.string) diff --git a/lib/std/paths.nim b/lib/std/paths.nim index b9c5953a01725..ad11266ac128b 100644 --- a/lib/std/paths.nim +++ b/lib/std/paths.nim @@ -1,5 +1,14 @@ +import std/private/osseps +export osseps + from std/private/ospaths2 {.all.} import joinPathImpl, joinPath, splitPath, - ReadDirEffect, WriteDirEffect + ReadDirEffect, WriteDirEffect, + isAbsolute, relativePath, normalizedPath, + normalizePathEnd, isRelativeTo, parentDir, + tailDir, isRootDir, parentDirs, `/../`, + searchExtPos, extractFilename, lastPathPart, + changeFileExt, addFileExt, cmpPaths, + unixToNativePath, absolutePath, normalizeExe export ReadDirEffect, WriteDirEffect type @@ -7,9 +16,32 @@ type func joinPath*(head, tail: Path): Path {.inline.} = + ## Joins two directory names to one. + ## + ## returns normalized path concatenation of `head` and `tail`, preserving + ## whether or not `tail` has a trailing slash (or, if tail if empty, whether + ## head has one). + ## + ## See also: + ## * `joinPath(parts: varargs[Path]) proc`_ + ## * `/ proc`_ + ## * `splitPath proc`_ + ## * `uri.combine proc `_ + ## * `uri./ proc `_ result = Path(joinPath(head.string, tail.string)) func joinPath*(parts: varargs[Path]): Path = + ## The same as `joinPath(head, tail) proc`_, + ## but works with any number of directory parts. + ## + ## You need to pass at least one element or the proc + ## will assert in debug builds and crash on release builds. + ## + ## See also: + ## * `joinPath(head, tail) proc`_ + ## * `/ proc`_ + ## * `/../ proc`_ + ## * `splitPath proc`_ var estimatedLen = 0 for p in parts: estimatedLen += p.string.len var res = newStringOfCap(estimatedLen) @@ -19,8 +51,240 @@ func joinPath*(parts: varargs[Path]): Path = result = Path(res) func `/`*(head, tail: Path): Path {.inline.} = + ## The same as `joinPath(head, tail) proc`_. + ## + ## See also: + ## * `/../ proc`_ + ## * `joinPath(head, tail) proc`_ + ## * `joinPath(parts: varargs[Path]) proc`_ + ## * `splitPath proc`_ + ## * `uri.combine proc `_ + ## * `uri./ proc `_ joinPath(head, tail) func splitPath*(path: Path): tuple[head, tail: Path] {.inline.} = + ## Splits a directory into `(head, tail)` tuple, so that + ## ``head / tail == path`` (except for edge cases like "/usr"). + ## + ## See also: + ## * `joinPath(head, tail) proc`_ + ## * `joinPath(parts: varargs[Path]) proc`_ + ## * `/ proc`_ + ## * `/../ proc`_ + ## * `relativePath proc`_ let res = splitPath(path.string) result = (Path(res.head), Path(res.tail)) + +func isAbsolute*(path: Path): bool {.inline, raises: [].} = + ## Checks whether a given `path` is absolute. + ## + ## On Windows, network paths are considered absolute too. + result = isAbsolute(path.string) + +proc relativePath*(path, base: Path, sep = DirSep): Path {.inline.} = + ## Converts `path` to a path relative to `base`. + ## + ## The `sep` (default: DirSep_) is used for the path normalizations, + ## this can be useful to ensure the relative path only contains `'/'` + ## so that it can be used for URL constructions. + ## + ## On Windows, if a root of `path` and a root of `base` are different, + ## returns `path` as is because it is impossible to make a relative path. + ## That means an absolute path can be returned. + ## + ## See also: + ## * `splitPath proc`_ + ## * `parentDir proc`_ + ## * `tailDir proc`_ + result = Path(relativePath(path.string, base.string, sep)) + +proc isRelativeTo*(path: Path, base: Path): bool {.inline.} = + ## Returns true if `path` is relative to `base`. + result = isRelativeTo(path.string, base.string) + +proc normalizedPath*(path: Path): Path {.inline, tags: [].} = + ## Returns a normalized path for the current OS. + ## + ## See also: + ## * `absolutePath proc`_ + ## * `normalizePath proc`_ for the in-place version + result = Path(normalizedPath(path.string)) + +proc normalizePathEnd*(path: var Path, trailingSep = false) {.borrow.} + +proc normalizePathEnd*(path: Path, trailingSep = false): Path {.borrow.} + +func parentDir*(path: Path): Path {.inline.} = + ## Returns the parent directory of `path`. + ## + ## This is similar to ``splitPath(path).head`` when ``path`` doesn't end + ## in a dir separator, but also takes care of path normalizations. + ## The remainder can be obtained with `lastPathPart(path) proc`_. + ## + ## See also: + ## * `relativePath proc`_ + ## * `splitPath proc`_ + ## * `tailDir proc`_ + ## * `parentDirs iterator`_ + result = Path(parentDir(path.string)) + +func tailDir*(path: Path): Path {.inline.} = + ## Returns the tail part of `path`. + ## + ## See also: + ## * `relativePath proc`_ + ## * `splitPath proc`_ + ## * `parentDir proc`_ + result = Path(tailDir(path.string)) + +func isRootDir*(path: Path): bool {.inline.} = + ## Checks whether a given `path` is a root directory. + result = isRootDir(path.string) + +iterator parentDirs*(path: Path, fromRoot=false, inclusive=true): Path = + ## Walks over all parent directories of a given `path`. + ## + ## If `fromRoot` is true (default: false), the traversal will start from + ## the file system root directory. + ## If `inclusive` is true (default), the original argument will be included + ## in the traversal. + ## + ## Relative paths won't be expanded by this iterator. Instead, it will traverse + ## only the directories appearing in the relative path. + ## + ## See also: + ## * `parentDir proc`_ + ## + for p in parentDirs(path.string, fromRoot, inclusive): + yield Path(p) + +func `/../`*(head, tail: Path): Path {.inline.} = + ## The same as ``parentDir(head) / tail``, unless there is no parent + ## directory. Then ``head / tail`` is performed instead. + ## + ## See also: + ## * `/ proc`_ + ## * `parentDir proc`_ + Path(`/../`(head.string, tail.string)) + +proc searchExtPos*(path: Path): int {.inline.} = + ## Returns index of the `'.'` char in `path` if it signifies the beginning + ## of extension. Returns -1 otherwise. + ## + ## See also: + ## * `splitFile proc`_ + ## * `extractFilename proc`_ + ## * `lastPathPart proc`_ + ## * `changeFileExt proc`_ + ## * `addFileExt proc`_ + result = searchExtPos(path.string) + +func extractFilename*(path: Path): Path {.inline.} = + ## Extracts the filename of a given `path`. + ## + ## This is the same as ``name & ext`` from `splitFile(path) proc`_. + ## + ## See also: + ## * `searchExtPos proc`_ + ## * `splitFile proc`_ + ## * `lastPathPart proc`_ + ## * `changeFileExt proc`_ + ## * `addFileExt proc`_ + result = Path(extractFilename(path.string)) + +func lastPathPart*(path: Path): Path {.inline.} = + ## Like `extractFilename proc`_, but ignores + ## trailing dir separator; aka: `baseName`:idx: in some other languages. + ## + ## See also: + ## * `searchExtPos proc`_ + ## * `splitFile proc`_ + ## * `extractFilename proc`_ + ## * `changeFileExt proc`_ + ## * `addFileExt proc`_ + result = Path(lastPathPart(path.string)) + +func changeFileExt*(filename, ext: Path): Path {.inline.} = + ## Changes the file extension to `ext`. + ## + ## If the `filename` has no extension, `ext` will be added. + ## If `ext` == "" then any extension is removed. + ## + ## `Ext` should be given without the leading `'.'`, because some + ## filesystems may use a different character. (Although I know + ## of none such beast.) + ## + ## See also: + ## * `searchExtPos proc`_ + ## * `splitFile proc`_ + ## * `extractFilename proc`_ + ## * `lastPathPart proc`_ + ## * `addFileExt proc`_ + result = Path(changeFileExt(filename.string, ext.string)) + +func addFileExt*(filename, ext: Path): Path {.inline.} = + ## Adds the file extension `ext` to `filename`, unless + ## `filename` already has an extension. + ## + ## `Ext` should be given without the leading `'.'`, because some + ## filesystems may use a different character. + ## (Although I know of none such beast.) + ## + ## See also: + ## * `searchExtPos proc`_ + ## * `splitFile proc`_ + ## * `extractFilename proc`_ + ## * `lastPathPart proc`_ + ## * `changeFileExt proc`_ + result = Path(addFileExt(filename.string, ext.string)) + +func cmpPaths*(pathA, pathB: Path): int {.inline.} = + ## Compares two paths. + ## + ## On a case-sensitive filesystem this is done + ## case-sensitively otherwise case-insensitively. Returns: + ## + ## | 0 if pathA == pathB + ## | < 0 if pathA < pathB + ## | > 0 if pathA > pathB + result = cmpPaths(pathA.string, pathB.string) + +func unixToNativePath*(path: Path, drive=Path("")): Path {.inline.} = + ## Converts an UNIX-like path to a native one. + ## + ## On an UNIX system this does nothing. Else it converts + ## `'/'`, `'.'`, `'..'` to the appropriate things. + ## + ## On systems with a concept of "drives", `drive` is used to determine + ## which drive label to use during absolute path conversion. + ## `drive` defaults to the drive of the current working directory, and is + ## ignored on systems that do not have a concept of "drives". + result = Path(unixToNativePath(path.string, drive.string)) + +proc getCurrentDir*(): Path {.inline, tags: [].} = + ## Returns the `current working directory`:idx: i.e. where the built + ## binary is run. + ## + ## So the path returned by this proc is determined at run time. + ## + ## See also: + ## * `getHomeDir proc`_ + ## * `getConfigDir proc`_ + ## * `getTempDir proc`_ + ## * `setCurrentDir proc`_ + ## * `currentSourcePath template `_ + ## * `getProjectPath proc `_ + result = Path(ospaths2.getCurrentDir()) + +proc setCurrentDir*(newDir: Path) {.inline, tags: [].} = + ## Sets the `current working directory`:idx:; `OSError` + ## is raised if `newDir` cannot been set. + ## + ## See also: + ## * `getHomeDir proc`_ + ## * `getConfigDir proc`_ + ## * `getTempDir proc`_ + ## * `getCurrentDir proc`_ + ospaths2.setCurrentDir(newDir.string) + +proc normalizeExe*(file: var Path) {.borrow.} diff --git a/lib/std/symlinks.nim b/lib/std/symlinks.nim index e31431b5aa0a4..cb469d8c3bdc7 100644 --- a/lib/std/symlinks.nim +++ b/lib/std/symlinks.nim @@ -3,11 +3,28 @@ from paths import Path, ReadDirEffect from std/private/ossymlinks import symlinkExists, createSymlink, expandSymlink -proc symlinkExists*(link: Path): bool {.tags: [ReadDirEffect].} = +proc symlinkExists*(link: Path): bool {.inline, tags: [ReadDirEffect].} = + ## Returns true if the symlink `link` exists. Will return true + ## regardless of whether the link points to a directory or file. result = symlinkExists(link.string) -proc createSymlink*(src, dest: Path) = +proc createSymlink*(src, dest: Path) {.inline.} = + ## Create a symbolic link at `dest` which points to the item specified + ## by `src`. On most operating systems, will fail if a link already exists. + ## + ## .. warning:: Some OS's (such as Microsoft Windows) restrict the creation + ## of symlinks to root users (administrators) or users with developer mode enabled. + ## + ## See also: + ## * `createHardlink proc`_ + ## * `expandSymlink proc`_ createSymlink(src.string, dest.string) -proc expandSymlink*(symlinkPath: Path): Path = +proc expandSymlink*(symlinkPath: Path): Path {.inline.} = + ## Returns a string representing the path to which the symbolic link points. + ## + ## On Windows this is a noop, `symlinkPath` is simply returned. + ## + ## See also: + ## * `createSymlink proc`_ result = Path(expandSymlink(symlinkPath.string)) From 3ccfb25f734c2c8432ed5ca36450daca1ba5143a Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Fri, 21 Oct 2022 21:36:23 +0800 Subject: [PATCH 06/13] add testcase --- lib/std/paths.nim | 36 +++++- tests/stdlib/tfilesanddirs.nim | 23 ++++ tests/stdlib/tpaths.nim | 206 +++++++++++++++++++++++++++++++++ 3 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 tests/stdlib/tfilesanddirs.nim create mode 100644 tests/stdlib/tpaths.nim diff --git a/lib/std/paths.nim b/lib/std/paths.nim index ad11266ac128b..e146125464674 100644 --- a/lib/std/paths.nim +++ b/lib/std/paths.nim @@ -7,8 +7,9 @@ from std/private/ospaths2 {.all.} import joinPathImpl, joinPath, splitPath, normalizePathEnd, isRelativeTo, parentDir, tailDir, isRootDir, parentDirs, `/../`, searchExtPos, extractFilename, lastPathPart, - changeFileExt, addFileExt, cmpPaths, - unixToNativePath, absolutePath, normalizeExe + changeFileExt, addFileExt, cmpPaths, splitFile, + unixToNativePath, absolutePath, normalizeExe, + normalizePath export ReadDirEffect, WriteDirEffect type @@ -75,6 +76,25 @@ func splitPath*(path: Path): tuple[head, tail: Path] {.inline.} = let res = splitPath(path.string) result = (Path(res.head), Path(res.tail)) +func splitFile*(path: Path): tuple[dir, name, ext: Path] {.inline.} = + ## Splits a filename into `(dir, name, extension)` tuple. + ## + ## `dir` does not end in DirSep_ unless it's `/`. + ## `extension` includes the leading dot. + ## + ## If `path` has no extension, `ext` is the empty string. + ## If `path` has no directory component, `dir` is the empty string. + ## If `path` has no filename component, `name` and `ext` are empty strings. + ## + ## See also: + ## * `searchExtPos proc`_ + ## * `extractFilename proc`_ + ## * `lastPathPart proc`_ + ## * `changeFileExt proc`_ + ## * `addFileExt proc`_ + let res = splitFile(path.string) + result = (Path(res.dir), Path(res.name), Path(res.ext)) + func isAbsolute*(path: Path): bool {.inline, raises: [].} = ## Checks whether a given `path` is absolute. ## @@ -288,3 +308,15 @@ proc setCurrentDir*(newDir: Path) {.inline, tags: [].} = ospaths2.setCurrentDir(newDir.string) proc normalizeExe*(file: var Path) {.borrow.} + +proc normalizePath*(path: var Path) {.borrow.} + +proc absolutePath*(path: Path, root = getCurrentDir()): Path = + ## Returns the absolute path of `path`, rooted at `root` (which must be absolute; + ## default: current directory). + ## If `path` is absolute, return it, ignoring `root`. + ## + ## See also: + ## * `normalizedPath proc`_ + ## * `normalizePath proc`_ + result = Path(absolutePath(path.string, root.string)) diff --git a/tests/stdlib/tfilesanddirs.nim b/tests/stdlib/tfilesanddirs.nim new file mode 100644 index 0000000000000..c04f143f6ea33 --- /dev/null +++ b/tests/stdlib/tfilesanddirs.nim @@ -0,0 +1,23 @@ +import std/[paths, files, dirs] + +from stdtest/specialpaths import buildDir +import std/[syncio, assertions] + +block fileOperations: + let files = @[Path"these.txt", Path"are.x", Path"testing.r", Path"files.q"] + let dirs = @[Path"some", Path"created", Path"test", Path"dirs"] + + let dname = Path"__really_obscure_dir_name" + + createDir(dname.Path) + doAssert dirExists(Path(dname)) + + # Test creating files and dirs + for dir in dirs: + createDir(Path(dname/dir)) + doAssert dirExists(Path(dname/dir)) + + for file in files: + let fh = open(string(dname/file), fmReadWrite) # createFile + fh.close() + doAssert fileExists(Path(dname/file)) diff --git a/tests/stdlib/tpaths.nim b/tests/stdlib/tpaths.nim new file mode 100644 index 0000000000000..224ce8e9ecc34 --- /dev/null +++ b/tests/stdlib/tpaths.nim @@ -0,0 +1,206 @@ +import std/paths +import std/assertions +import pathnorm + +proc normalizePath*(path: Path; dirSep = DirSep): Path = + result = Path(pathnorm.normalizePath(path.string, dirSep)) + +func `==`(x, y: Path): bool = + x.string == y.string + +block absolutePath: + doAssertRaises(ValueError): discard absolutePath(Path"a", Path"b") + doAssert absolutePath(Path"a") == getCurrentDir() / Path"a" + doAssert absolutePath(Path"a", Path"/b") == Path"/b" / Path"a" + when defined(posix): + doAssert absolutePath(Path"a", Path"/b/") == Path"/b" / Path"a" + doAssert absolutePath(Path"a", Path"/b/c") == Path"/b/c" / Path"a" + doAssert absolutePath(Path"/a", Path"b/") == Path"/a" + +block splitFile: + doAssert splitFile(Path"") == (Path"", Path"", Path"") + doAssert splitFile(Path"abc/") == (Path"abc", Path"", Path"") + doAssert splitFile(Path"/") == (Path"/", Path"", Path"") + doAssert splitFile(Path"./abc") == (Path".", Path"abc", Path"") + doAssert splitFile(Path".txt") == (Path"", Path".txt", Path"") + doAssert splitFile(Path"abc/.txt") == (Path"abc", Path".txt", Path"") + doAssert splitFile(Path"abc") == (Path"", Path"abc", Path"") + doAssert splitFile(Path"abc.txt") == (Path"", Path"abc", Path".txt") + doAssert splitFile(Path"/abc.txt") == (Path"/", Path"abc", Path".txt") + doAssert splitFile(Path"/foo/abc.txt") == (Path"/foo", Path"abc", Path".txt") + doAssert splitFile(Path"/foo/abc.txt.gz") == (Path"/foo", Path"abc.txt", Path".gz") + doAssert splitFile(Path".") == (Path"", Path".", Path"") + doAssert splitFile(Path"abc/.") == (Path"abc", Path".", Path"") + doAssert splitFile(Path"..") == (Path"", Path"..", Path"") + doAssert splitFile(Path"a/..") == (Path"a", Path"..", Path"") + doAssert splitFile(Path"/foo/abc....txt") == (Path"/foo", Path"abc...", Path".txt") + +# execShellCmd is tested in tosproc + +block ospaths: + doAssert unixToNativePath(Path"") == Path"" + doAssert unixToNativePath(Path".") == Path($CurDir) + doAssert unixToNativePath(Path"..") == Path($ParDir) + doAssert isAbsolute(unixToNativePath(Path"/")) + doAssert isAbsolute(unixToNativePath(Path"/", Path"a")) + doAssert isAbsolute(unixToNativePath(Path"/a")) + doAssert isAbsolute(unixToNativePath(Path"/a", Path"a")) + doAssert isAbsolute(unixToNativePath(Path"/a/b")) + doAssert isAbsolute(unixToNativePath(Path"/a/b", Path"a")) + doAssert unixToNativePath(Path"a/b") == joinPath(Path"a", Path"b") + + when defined(macos): + doAssert unixToNativePath(Path"./") == Path":" + doAssert unixToNativePath(Path"./abc") == Path":abc" + doAssert unixToNativePath(Path"../abc") == Path"::abc" + doAssert unixToNativePath(Path"../../abc") == Path":::abc" + doAssert unixToNativePath(Path"/abc", Path"a") == Path"abc" + doAssert unixToNativePath(Path"/abc/def", Path"a") == Path"abc:def" + elif doslikeFileSystem: + doAssert unixToNativePath(Path"./") == Path(".\\") + doAssert unixToNativePath(Path"./abc") == Path(".\\abc") + doAssert unixToNativePath(Path"../abc") == Path("..\\abc") + doAssert unixToNativePath(Path"../../abc") == Path("..\\..\\abc") + doAssert unixToNativePath(Path"/abc", Path"a") == Path("a:\\abc") + doAssert unixToNativePath(Path"/abc/def", Path"a") == Path("a:\\abc\\def") + else: + #Tests for unix + doAssert unixToNativePath(Path"./") == Path"./" + doAssert unixToNativePath(Path"./abc") == Path"./abc" + doAssert unixToNativePath(Path"../abc") == Path"../abc" + doAssert unixToNativePath(Path"../../abc") == Path"../../abc" + doAssert unixToNativePath(Path"/abc", Path"a") == Path"/abc" + doAssert unixToNativePath(Path"/abc/def", Path"a") == Path"/abc/def" + + block extractFilenameTest: + doAssert extractFilename(Path"") == Path"" + when defined(posix): + doAssert extractFilename(Path"foo/bar") == Path"bar" + doAssert extractFilename(Path"foo/bar.txt") == Path"bar.txt" + doAssert extractFilename(Path"foo/") == Path"" + doAssert extractFilename(Path"/") == Path"" + when doslikeFileSystem: + doAssert extractFilename(Path(r"foo\bar")) == Path"bar" + doAssert extractFilename(Path(r"foo\bar.txt")) == Path"bar.txt" + doAssert extractFilename(Path(r"foo\")) == Path"" + doAssert extractFilename(Path(r"C:\")) == Path"" + + block lastPathPartTest: + doAssert lastPathPart(Path"") == Path"" + when defined(posix): + doAssert lastPathPart(Path"foo/bar.txt") == Path"bar.txt" + doAssert lastPathPart(Path"foo/") == Path"foo" + doAssert lastPathPart(Path"/") == Path"" + when doslikeFileSystem: + doAssert lastPathPart(Path(r"foo\bar.txt")) == Path"bar.txt" + doAssert lastPathPart(Path(r"foo\")) == Path"foo" + + template canon(x): Path = normalizePath(Path(x), '/') + doAssert canon"/foo/../bar" == Path"/bar" + doAssert canon"foo/../bar" == Path"bar" + + doAssert canon"/f/../bar///" == Path"/bar" + doAssert canon"f/..////bar" == Path"bar" + + doAssert canon"../bar" == Path"../bar" + doAssert canon"/../bar" == Path"/../bar" + + doAssert canon("foo/../../bar/") == Path"../bar" + doAssert canon("./bla/blob/") == Path"bla/blob" + doAssert canon(".hiddenFile") == Path".hiddenFile" + doAssert canon("./bla/../../blob/./zoo.nim") == Path"../blob/zoo.nim" + + doAssert canon("C:/file/to/this/long") == Path"C:/file/to/this/long" + doAssert canon("") == Path"" + doAssert canon("foobar") == Path"foobar" + doAssert canon("f/////////") == Path"f" + + doAssert relativePath(Path"/foo/bar//baz.nim", Path"/foo", '/') == Path"bar/baz.nim" + doAssert normalizePath(Path"./foo//bar/../baz", '/') == Path"foo/baz" + + doAssert relativePath(Path"/Users/me/bar/z.nim", Path"/Users/other/bad", '/') == Path"../../me/bar/z.nim" + + doAssert relativePath(Path"/Users/me/bar/z.nim", Path"/Users/other", '/') == Path"../me/bar/z.nim" + + # `//` is a UNC path, `/` is the current working directory's drive, so can't + # run this test on Windows. + when not doslikeFileSystem: + doAssert relativePath(Path"/Users///me/bar//z.nim", Path"//Users/", '/') == Path"me/bar/z.nim" + doAssert relativePath(Path"/Users/me/bar/z.nim", Path"/Users/me", '/') == Path"bar/z.nim" + doAssert relativePath(Path"", Path"/users/moo", '/') == Path"" + doAssert relativePath(Path"foo", Path"", '/') == Path"foo" + doAssert relativePath(Path"/foo", Path"/Foo", '/') == (when FileSystemCaseSensitive: Path"../foo" else: Path".") + doAssert relativePath(Path"/Foo", Path"/foo", '/') == (when FileSystemCaseSensitive: Path"../Foo" else: Path".") + doAssert relativePath(Path"/foo", Path"/fOO", '/') == (when FileSystemCaseSensitive: Path"../foo" else: Path".") + doAssert relativePath(Path"/foO", Path"/foo", '/') == (when FileSystemCaseSensitive: Path"../foO" else: Path".") + + doAssert relativePath(Path"foo", Path".", '/') == Path"foo" + doAssert relativePath(Path".", Path".", '/') == Path"." + doAssert relativePath(Path"..", Path".", '/') == Path".." + + doAssert relativePath(Path"foo", Path"foo") == Path"." + doAssert relativePath(Path"", Path"foo") == Path"" + doAssert relativePath(Path"././/foo", Path"foo//./") == Path"." + + doAssert relativePath(getCurrentDir() / Path"bar", Path"foo") == Path"../bar".unixToNativePath + doAssert relativePath(Path"bar", getCurrentDir() / Path"foo") == Path"../bar".unixToNativePath + + when doslikeFileSystem: + doAssert relativePath(r"c:\foo.nim".Path, r"C:\".Path) == r"foo.nim".Path + doAssert relativePath(r"c:\foo\bar\baz.nim".Path, r"c:\foo".Path) == r"bar\baz.nim".Path + doAssert relativePath(r"c:\foo\bar\baz.nim".Path, r"d:\foo".Path) == r"c:\foo\bar\baz.nim".Path + doAssert relativePath(r"\foo\baz.nim".Path, r"\foo".Path) == r"baz.nim".Path + doAssert relativePath(r"\foo\bar\baz.nim".Path, r"\bar".Path) == r"..\foo\bar\baz.nim".Path + doAssert relativePath(r"\\foo\bar\baz.nim".Path, r"\\foo\bar".Path) == r"baz.nim".Path + doAssert relativePath(r"\\foo\bar\baz.nim".Path, r"\\foO\bar".Path) == r"baz.nim".Path + doAssert relativePath(r"\\foo\bar\baz.nim".Path, r"\\bar\bar".Path) == r"\\foo\bar\baz.nim".Path + doAssert relativePath(r"\\foo\bar\baz.nim".Path, r"\\foo\car".Path) == r"\\foo\bar\baz.nim".Path + doAssert relativePath(r"\\foo\bar\baz.nim".Path, r"\\goo\bar".Path) == r"\\foo\bar\baz.nim".Path + doAssert relativePath(r"\\foo\bar\baz.nim".Path, r"c:\".Path) == r"\\foo\bar\baz.nim".Path + doAssert relativePath(r"\\foo\bar\baz.nim".Path, r"\foo".Path) == r"\\foo\bar\baz.nim".Path + doAssert relativePath(r"c:\foo.nim".Path, r"\foo".Path) == r"c:\foo.nim".Path + + doAssert joinPath(Path"usr", Path"") == unixToNativePath(Path"usr") + doAssert joinPath(Path"", Path"lib") == Path"lib" + doAssert joinPath(Path"", Path"/lib") == unixToNativePath(Path"/lib") + doAssert joinPath(Path"usr/", Path"/lib") == unixToNativePath(Path"usr/lib") + doAssert joinPath(Path"", Path"") == unixToNativePath(Path"") # issue #13455 + doAssert joinPath(Path"", Path"/") == unixToNativePath(Path"/") + doAssert joinPath(Path"/", Path"/") == unixToNativePath(Path"/") + doAssert joinPath(Path"/", Path"") == unixToNativePath(Path"/") + doAssert joinPath(Path"/" / Path"") == unixToNativePath(Path"/") # weird test case... + doAssert joinPath(Path"/", Path"/a/b/c") == unixToNativePath(Path"/a/b/c") + doAssert joinPath(Path"foo/", Path"") == unixToNativePath(Path"foo/") + doAssert joinPath(Path"foo/", Path"abc") == unixToNativePath(Path"foo/abc") + doAssert joinPath(Path"foo//./", Path"abc/.//") == unixToNativePath(Path"foo/abc/") + doAssert joinPath(Path"foo", Path"abc") == unixToNativePath(Path"foo/abc") + doAssert joinPath(Path"", Path"abc") == unixToNativePath(Path"abc") + + doAssert joinPath(Path"zook/.", Path"abc") == unixToNativePath(Path"zook/abc") + + # controversial: inconsistent with `joinPath("zook/.","abc")` + # on linux, `./foo` and `foo` are treated a bit differently for executables + # but not `./foo/bar` and `foo/bar` + doAssert joinPath(Path".", Path"/lib") == unixToNativePath(Path"./lib") + doAssert joinPath(Path".", Path"abc") == unixToNativePath(Path"./abc") + + # cases related to issue #13455 + doAssert joinPath(Path"foo", Path"", Path"") == Path"foo" + doAssert joinPath(Path"foo", Path"") == Path"foo" + doAssert joinPath(Path"foo/", Path"") == unixToNativePath(Path"foo/") + doAssert joinPath(Path"foo/", Path".") == Path"foo" + doAssert joinPath(Path"foo", Path"./") == unixToNativePath(Path"foo/") + doAssert joinPath(Path"foo", Path"", Path"bar/") == unixToNativePath(Path"foo/bar/") + + # issue #13579 + doAssert joinPath(Path"/foo", Path"../a") == unixToNativePath(Path"/a") + doAssert joinPath(Path"/foo/", Path"../a") == unixToNativePath(Path"/a") + doAssert joinPath(Path"/foo/.", Path"../a") == unixToNativePath(Path"/a") + doAssert joinPath(Path"/foo/.b", Path"../a") == unixToNativePath(Path"/foo/a") + doAssert joinPath(Path"/foo///", Path"..//a/") == unixToNativePath(Path"/a/") + doAssert joinPath(Path"foo/", Path"../a") == unixToNativePath(Path"a") + + when doslikeFileSystem: + doAssert joinPath(Path"C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\Common7\\Tools\\", Path"..\\..\\VC\\vcvarsall.bat") == r"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat".Path + doAssert joinPath(Path"C:\\foo", Path"..\\a") == r"C:\a".Path + doAssert joinPath(Path"C:\\foo\\", Path"..\\a") == r"C:\a".Path \ No newline at end of file From b5a78562d8a45317a46ea665d185479c220f8b57 Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Fri, 21 Oct 2022 21:43:56 +0800 Subject: [PATCH 07/13] remove tryRemoveFile --- lib/std/files.nim | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/std/files.nim b/lib/std/files.nim index d036fdf6bef06..f16c716971e6d 100644 --- a/lib/std/files.nim +++ b/lib/std/files.nim @@ -10,21 +10,6 @@ proc fileExists*(filename: Path): bool {.inline, tags: [ReadDirEffect].} = ## Directories, device files, named pipes and sockets return false. result = fileExists(filename.string) -proc tryRemoveFile*(file: Path): bool {.inline, tags: [WriteDirEffect].} = - ## Removes the `file`. - ## - ## If this fails, returns `false`. This does not fail - ## if the file never existed in the first place. - ## - ## On Windows, ignores the read-only attribute. - ## - ## See also: - ## * `copyFile proc`_ - ## * `copyFileWithPermissions proc`_ - ## * `removeFile proc`_ - ## * `moveFile proc`_ - result = tryRemoveFile(file.string) - proc removeFile*(file: Path) {.inline, tags: [WriteDirEffect].} = ## Removes the `file`. ## From 268f66d5ec96dee6f51632d0f6fa9e90e0b02b96 Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Fri, 21 Oct 2022 21:45:56 +0800 Subject: [PATCH 08/13] clean up --- lib/std/dirs.nim | 1 - lib/std/files.nim | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/std/dirs.nim b/lib/std/dirs.nim index f78ac9d99495c..e89bfc668afeb 100644 --- a/lib/std/dirs.nim +++ b/lib/std/dirs.nim @@ -51,7 +51,6 @@ proc removeDir*(dir: Path, checkDir = false ## existed in the first place, unless `checkDir` = true. ## ## See also: - ## * `tryRemoveFile proc`_ ## * `removeFile proc`_ ## * `existsOrCreateDir proc`_ ## * `createDir proc`_ diff --git a/lib/std/files.nim b/lib/std/files.nim index f16c716971e6d..f83afaed73324 100644 --- a/lib/std/files.nim +++ b/lib/std/files.nim @@ -1,7 +1,7 @@ from paths import Path, ReadDirEffect, WriteDirEffect -from std/private/osfiles import fileExists, tryRemoveFile, removeFile, - removeFile, moveFile +from std/private/osfiles import fileExists, removeFile, + moveFile proc fileExists*(filename: Path): bool {.inline, tags: [ReadDirEffect].} = From 3571b5b8c9a22d68676549490bcbdf9ccd2e65ae Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Fri, 21 Oct 2022 23:49:22 +0800 Subject: [PATCH 09/13] Delete test.nim --- test.nim | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 test.nim diff --git a/test.nim b/test.nim deleted file mode 100644 index 762564b764944..0000000000000 --- a/test.nim +++ /dev/null @@ -1,23 +0,0 @@ -var g: seq[int] - -proc j(b: int) = - g.insert(0, 0) - for i in 0 ..< 1: - discard - -proc c(w: int) = discard - -c(0) -c(0) -c(0) -c(0) -c(0) -c(0) -c(0) -c(0) -c(0) -c(0) -c(0) -c(0) - -j(3) \ No newline at end of file From db84f37b2a04995ac44715c46c4b6d6570f5e5ba Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Sat, 22 Oct 2022 00:23:26 +0800 Subject: [PATCH 10/13] apply changes --- lib/std/paths.nim | 59 +++++++++-------------------------------- tests/stdlib/tpaths.nim | 36 ++++++++++++++----------- 2 files changed, 33 insertions(+), 62 deletions(-) diff --git a/lib/std/paths.nim b/lib/std/paths.nim index e146125464674..4b21b876d58ac 100644 --- a/lib/std/paths.nim +++ b/lib/std/paths.nim @@ -6,7 +6,7 @@ from std/private/ospaths2 {.all.} import joinPathImpl, joinPath, splitPath, isAbsolute, relativePath, normalizedPath, normalizePathEnd, isRelativeTo, parentDir, tailDir, isRootDir, parentDirs, `/../`, - searchExtPos, extractFilename, lastPathPart, + extractFilename, lastPathPart, changeFileExt, addFileExt, cmpPaths, splitFile, unixToNativePath, absolutePath, normalizeExe, normalizePath @@ -16,21 +16,6 @@ type Path* = distinct string -func joinPath*(head, tail: Path): Path {.inline.} = - ## Joins two directory names to one. - ## - ## returns normalized path concatenation of `head` and `tail`, preserving - ## whether or not `tail` has a trailing slash (or, if tail if empty, whether - ## head has one). - ## - ## See also: - ## * `joinPath(parts: varargs[Path]) proc`_ - ## * `/ proc`_ - ## * `splitPath proc`_ - ## * `uri.combine proc `_ - ## * `uri./ proc `_ - result = Path(joinPath(head.string, tail.string)) - func joinPath*(parts: varargs[Path]): Path = ## The same as `joinPath(head, tail) proc`_, ## but works with any number of directory parts. @@ -52,16 +37,18 @@ func joinPath*(parts: varargs[Path]): Path = result = Path(res) func `/`*(head, tail: Path): Path {.inline.} = - ## The same as `joinPath(head, tail) proc`_. + ## Joins two directory names to one. + ## + ## returns normalized path concatenation of `head` and `tail`, preserving + ## whether or not `tail` has a trailing slash (or, if tail if empty, whether + ## head has one). ## ## See also: - ## * `/../ proc`_ - ## * `joinPath(head, tail) proc`_ ## * `joinPath(parts: varargs[Path]) proc`_ ## * `splitPath proc`_ ## * `uri.combine proc `_ ## * `uri./ proc `_ - joinPath(head, tail) + Path(joinPath(head.string, tail.string)) func splitPath*(path: Path): tuple[head, tail: Path] {.inline.} = ## Splits a directory into `(head, tail)` tuple, so that @@ -76,7 +63,7 @@ func splitPath*(path: Path): tuple[head, tail: Path] {.inline.} = let res = splitPath(path.string) result = (Path(res.head), Path(res.tail)) -func splitFile*(path: Path): tuple[dir, name, ext: Path] {.inline.} = +func splitFile*(path: Path): tuple[dir, name: Path, ext: string] {.inline.} = ## Splits a filename into `(dir, name, extension)` tuple. ## ## `dir` does not end in DirSep_ unless it's `/`. @@ -87,13 +74,12 @@ func splitFile*(path: Path): tuple[dir, name, ext: Path] {.inline.} = ## If `path` has no filename component, `name` and `ext` are empty strings. ## ## See also: - ## * `searchExtPos proc`_ ## * `extractFilename proc`_ ## * `lastPathPart proc`_ ## * `changeFileExt proc`_ ## * `addFileExt proc`_ let res = splitFile(path.string) - result = (Path(res.dir), Path(res.name), Path(res.ext)) + result = (Path(res.dir), Path(res.name), res.ext) func isAbsolute*(path: Path): bool {.inline, raises: [].} = ## Checks whether a given `path` is absolute. @@ -187,25 +173,12 @@ func `/../`*(head, tail: Path): Path {.inline.} = ## * `parentDir proc`_ Path(`/../`(head.string, tail.string)) -proc searchExtPos*(path: Path): int {.inline.} = - ## Returns index of the `'.'` char in `path` if it signifies the beginning - ## of extension. Returns -1 otherwise. - ## - ## See also: - ## * `splitFile proc`_ - ## * `extractFilename proc`_ - ## * `lastPathPart proc`_ - ## * `changeFileExt proc`_ - ## * `addFileExt proc`_ - result = searchExtPos(path.string) - func extractFilename*(path: Path): Path {.inline.} = ## Extracts the filename of a given `path`. ## ## This is the same as ``name & ext`` from `splitFile(path) proc`_. ## ## See also: - ## * `searchExtPos proc`_ ## * `splitFile proc`_ ## * `lastPathPart proc`_ ## * `changeFileExt proc`_ @@ -217,14 +190,13 @@ func lastPathPart*(path: Path): Path {.inline.} = ## trailing dir separator; aka: `baseName`:idx: in some other languages. ## ## See also: - ## * `searchExtPos proc`_ ## * `splitFile proc`_ ## * `extractFilename proc`_ ## * `changeFileExt proc`_ ## * `addFileExt proc`_ result = Path(lastPathPart(path.string)) -func changeFileExt*(filename, ext: Path): Path {.inline.} = +func changeFileExt*(filename: Path, ext: string): Path {.inline.} = ## Changes the file extension to `ext`. ## ## If the `filename` has no extension, `ext` will be added. @@ -235,14 +207,13 @@ func changeFileExt*(filename, ext: Path): Path {.inline.} = ## of none such beast.) ## ## See also: - ## * `searchExtPos proc`_ ## * `splitFile proc`_ ## * `extractFilename proc`_ ## * `lastPathPart proc`_ ## * `addFileExt proc`_ - result = Path(changeFileExt(filename.string, ext.string)) + result = Path(changeFileExt(filename.string, ext)) -func addFileExt*(filename, ext: Path): Path {.inline.} = +func addFileExt*(filename: Path, ext: string): Path {.inline.} = ## Adds the file extension `ext` to `filename`, unless ## `filename` already has an extension. ## @@ -251,12 +222,11 @@ func addFileExt*(filename, ext: Path): Path {.inline.} = ## (Although I know of none such beast.) ## ## See also: - ## * `searchExtPos proc`_ ## * `splitFile proc`_ ## * `extractFilename proc`_ ## * `lastPathPart proc`_ ## * `changeFileExt proc`_ - result = Path(addFileExt(filename.string, ext.string)) + result = Path(addFileExt(filename.string, ext)) func cmpPaths*(pathA, pathB: Path): int {.inline.} = ## Compares two paths. @@ -301,9 +271,6 @@ proc setCurrentDir*(newDir: Path) {.inline, tags: [].} = ## is raised if `newDir` cannot been set. ## ## See also: - ## * `getHomeDir proc`_ - ## * `getConfigDir proc`_ - ## * `getTempDir proc`_ ## * `getCurrentDir proc`_ ospaths2.setCurrentDir(newDir.string) diff --git a/tests/stdlib/tpaths.nim b/tests/stdlib/tpaths.nim index 224ce8e9ecc34..08269d64d77ce 100644 --- a/tests/stdlib/tpaths.nim +++ b/tests/stdlib/tpaths.nim @@ -8,6 +8,10 @@ proc normalizePath*(path: Path; dirSep = DirSep): Path = func `==`(x, y: Path): bool = x.string == y.string + +func joinPath(head, tail: Path): Path {.inline.} = + head / tail + block absolutePath: doAssertRaises(ValueError): discard absolutePath(Path"a", Path"b") doAssert absolutePath(Path"a") == getCurrentDir() / Path"a" @@ -18,22 +22,22 @@ block absolutePath: doAssert absolutePath(Path"/a", Path"b/") == Path"/a" block splitFile: - doAssert splitFile(Path"") == (Path"", Path"", Path"") - doAssert splitFile(Path"abc/") == (Path"abc", Path"", Path"") - doAssert splitFile(Path"/") == (Path"/", Path"", Path"") - doAssert splitFile(Path"./abc") == (Path".", Path"abc", Path"") - doAssert splitFile(Path".txt") == (Path"", Path".txt", Path"") - doAssert splitFile(Path"abc/.txt") == (Path"abc", Path".txt", Path"") - doAssert splitFile(Path"abc") == (Path"", Path"abc", Path"") - doAssert splitFile(Path"abc.txt") == (Path"", Path"abc", Path".txt") - doAssert splitFile(Path"/abc.txt") == (Path"/", Path"abc", Path".txt") - doAssert splitFile(Path"/foo/abc.txt") == (Path"/foo", Path"abc", Path".txt") - doAssert splitFile(Path"/foo/abc.txt.gz") == (Path"/foo", Path"abc.txt", Path".gz") - doAssert splitFile(Path".") == (Path"", Path".", Path"") - doAssert splitFile(Path"abc/.") == (Path"abc", Path".", Path"") - doAssert splitFile(Path"..") == (Path"", Path"..", Path"") - doAssert splitFile(Path"a/..") == (Path"a", Path"..", Path"") - doAssert splitFile(Path"/foo/abc....txt") == (Path"/foo", Path"abc...", Path".txt") + doAssert splitFile(Path"") == (Path"", Path"", "") + doAssert splitFile(Path"abc/") == (Path"abc", Path"", "") + doAssert splitFile(Path"/") == (Path"/", Path"", "") + doAssert splitFile(Path"./abc") == (Path".", Path"abc", "") + doAssert splitFile(Path".txt") == (Path"", Path".txt", "") + doAssert splitFile(Path"abc/.txt") == (Path"abc", Path".txt", "") + doAssert splitFile(Path"abc") == (Path"", Path"abc", "") + doAssert splitFile(Path"abc.txt") == (Path"", Path"abc", ".txt") + doAssert splitFile(Path"/abc.txt") == (Path"/", Path"abc", ".txt") + doAssert splitFile(Path"/foo/abc.txt") == (Path"/foo", Path"abc", ".txt") + doAssert splitFile(Path"/foo/abc.txt.gz") == (Path"/foo", Path"abc.txt", ".gz") + doAssert splitFile(Path".") == (Path"", Path".", "") + doAssert splitFile(Path"abc/.") == (Path"abc", Path".", "") + doAssert splitFile(Path"..") == (Path"", Path"..", "") + doAssert splitFile(Path"a/..") == (Path"a", Path"..", "") + doAssert splitFile(Path"/foo/abc....txt") == (Path"/foo", Path"abc...", ".txt") # execShellCmd is tested in tosproc From 859b0e293df01dfc6f52568e975ba256b18dac83 Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Sat, 22 Oct 2022 00:53:10 +0800 Subject: [PATCH 11/13] add `add` and fixes --- lib/std/paths.nim | 33 ++++++++++++--------------------- tests/stdlib/tpaths.nim | 22 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/lib/std/paths.nim b/lib/std/paths.nim index 4b21b876d58ac..8ae758bb6133a 100644 --- a/lib/std/paths.nim +++ b/lib/std/paths.nim @@ -1,7 +1,9 @@ import std/private/osseps export osseps -from std/private/ospaths2 {.all.} import joinPathImpl, joinPath, splitPath, +import pathnorm + +from std/private/ospaths2 import joinPath, splitPath, ReadDirEffect, WriteDirEffect, isAbsolute, relativePath, normalizedPath, normalizePathEnd, isRelativeTo, parentDir, @@ -15,26 +17,17 @@ export ReadDirEffect, WriteDirEffect type Path* = distinct string +template endsWith(a: string, b: set[char]): bool = + a.len > 0 and a[^1] in b -func joinPath*(parts: varargs[Path]): Path = - ## The same as `joinPath(head, tail) proc`_, - ## but works with any number of directory parts. - ## - ## You need to pass at least one element or the proc - ## will assert in debug builds and crash on release builds. - ## - ## See also: - ## * `joinPath(head, tail) proc`_ - ## * `/ proc`_ - ## * `/../ proc`_ - ## * `splitPath proc`_ - var estimatedLen = 0 - for p in parts: estimatedLen += p.string.len - var res = newStringOfCap(estimatedLen) +func add(x: var string, tail: string) = var state = 0 - for i in 0..high(parts): - joinPathImpl(res, state, parts[i].string) - result = Path(res) + let trailingSep = tail.endsWith({DirSep, AltSep}) or tail.len == 0 and x.endsWith({DirSep, AltSep}) + normalizePathEnd(x, trailingSep=false) + addNormalizePath(tail, x, state, DirSep) + normalizePathEnd(x, trailingSep=trailingSep) + +func add*(x: var Path, y: Path) {.borrow.} func `/`*(head, tail: Path): Path {.inline.} = ## Joins two directory names to one. @@ -118,8 +111,6 @@ proc normalizedPath*(path: Path): Path {.inline, tags: [].} = proc normalizePathEnd*(path: var Path, trailingSep = false) {.borrow.} -proc normalizePathEnd*(path: Path, trailingSep = false): Path {.borrow.} - func parentDir*(path: Path): Path {.inline.} = ## Returns the parent directory of `path`. ## diff --git a/tests/stdlib/tpaths.nim b/tests/stdlib/tpaths.nim index 08269d64d77ce..2d1a3e2ca4573 100644 --- a/tests/stdlib/tpaths.nim +++ b/tests/stdlib/tpaths.nim @@ -1,6 +1,9 @@ import std/paths import std/assertions import pathnorm +from std/private/ospaths2 {.all.} import joinPathImpl +import std/sugar + proc normalizePath*(path: Path; dirSep = DirSep): Path = result = Path(pathnorm.normalizePath(path.string, dirSep)) @@ -8,6 +11,15 @@ proc normalizePath*(path: Path; dirSep = DirSep): Path = func `==`(x, y: Path): bool = x.string == y.string +func joinPath*(parts: varargs[Path]): Path = + var estimatedLen = 0 + var state = 0 + for p in parts: estimatedLen += p.string.len + var res = newStringOfCap(estimatedLen) + for i in 0..high(parts): + joinPathImpl(res, state, parts[i].string) + result = Path(res) + func joinPath(head, tail: Path): Path {.inline.} = head / tail @@ -165,19 +177,27 @@ block ospaths: doAssert relativePath(r"c:\foo.nim".Path, r"\foo".Path) == r"c:\foo.nim".Path doAssert joinPath(Path"usr", Path"") == unixToNativePath(Path"usr") + doAssert joinPath(Path"usr", Path"") == (Path"usr").dup(add Path"") doAssert joinPath(Path"", Path"lib") == Path"lib" + doAssert joinPath(Path"", Path"lib") == Path"".dup(add Path"lib") + doAssert joinPath(Path"", Path"/lib") == unixToNativePath(Path"/lib") doAssert joinPath(Path"", Path"/lib") == unixToNativePath(Path"/lib") - doAssert joinPath(Path"usr/", Path"/lib") == unixToNativePath(Path"usr/lib") + doAssert joinPath(Path"usr/", Path"/lib") == Path"usr/".dup(add Path"/lib") doAssert joinPath(Path"", Path"") == unixToNativePath(Path"") # issue #13455 + doAssert joinPath(Path"", Path"") == Path"".dup(add Path"") doAssert joinPath(Path"", Path"/") == unixToNativePath(Path"/") + doAssert joinPath(Path"", Path"/") == Path"".dup(add Path"/") doAssert joinPath(Path"/", Path"/") == unixToNativePath(Path"/") + doAssert joinPath(Path"/", Path"/") == Path"/".dup(add Path"/") doAssert joinPath(Path"/", Path"") == unixToNativePath(Path"/") doAssert joinPath(Path"/" / Path"") == unixToNativePath(Path"/") # weird test case... doAssert joinPath(Path"/", Path"/a/b/c") == unixToNativePath(Path"/a/b/c") doAssert joinPath(Path"foo/", Path"") == unixToNativePath(Path"foo/") doAssert joinPath(Path"foo/", Path"abc") == unixToNativePath(Path"foo/abc") doAssert joinPath(Path"foo//./", Path"abc/.//") == unixToNativePath(Path"foo/abc/") + doAssert Path"foo//./".dup(add Path"abc/.//") == unixToNativePath(Path"foo/abc/") doAssert joinPath(Path"foo", Path"abc") == unixToNativePath(Path"foo/abc") + doAssert Path"foo".dup(add Path"abc") == unixToNativePath(Path"foo/abc") doAssert joinPath(Path"", Path"abc") == unixToNativePath(Path"abc") doAssert joinPath(Path"zook/.", Path"abc") == unixToNativePath(Path"zook/abc") From 483c87d852b820ad247aeb02ed92b4924c071aee Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Sat, 22 Oct 2022 01:01:33 +0800 Subject: [PATCH 12/13] cleanup --- lib/std/paths.nim | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/lib/std/paths.nim b/lib/std/paths.nim index 8ae758bb6133a..770c282e42822 100644 --- a/lib/std/paths.nim +++ b/lib/std/paths.nim @@ -3,15 +3,15 @@ export osseps import pathnorm -from std/private/ospaths2 import joinPath, splitPath, - ReadDirEffect, WriteDirEffect, - isAbsolute, relativePath, normalizedPath, - normalizePathEnd, isRelativeTo, parentDir, - tailDir, isRootDir, parentDirs, `/../`, - extractFilename, lastPathPart, - changeFileExt, addFileExt, cmpPaths, splitFile, - unixToNativePath, absolutePath, normalizeExe, - normalizePath +from std/private/ospaths2 import joinPath, splitPath, + ReadDirEffect, WriteDirEffect, + isAbsolute, relativePath, normalizedPath, + normalizePathEnd, isRelativeTo, parentDir, + tailDir, isRootDir, parentDirs, `/../`, + extractFilename, lastPathPart, + changeFileExt, addFileExt, cmpPaths, splitFile, + unixToNativePath, absolutePath, normalizeExe, + normalizePath export ReadDirEffect, WriteDirEffect type @@ -37,7 +37,6 @@ func `/`*(head, tail: Path): Path {.inline.} = ## head has one). ## ## See also: - ## * `joinPath(parts: varargs[Path]) proc`_ ## * `splitPath proc`_ ## * `uri.combine proc `_ ## * `uri./ proc `_ @@ -48,8 +47,7 @@ func splitPath*(path: Path): tuple[head, tail: Path] {.inline.} = ## ``head / tail == path`` (except for edge cases like "/usr"). ## ## See also: - ## * `joinPath(head, tail) proc`_ - ## * `joinPath(parts: varargs[Path]) proc`_ + ## * `add proc`_ ## * `/ proc`_ ## * `/../ proc`_ ## * `relativePath proc`_ @@ -101,15 +99,6 @@ proc isRelativeTo*(path: Path, base: Path): bool {.inline.} = ## Returns true if `path` is relative to `base`. result = isRelativeTo(path.string, base.string) -proc normalizedPath*(path: Path): Path {.inline, tags: [].} = - ## Returns a normalized path for the current OS. - ## - ## See also: - ## * `absolutePath proc`_ - ## * `normalizePath proc`_ for the in-place version - result = Path(normalizedPath(path.string)) - -proc normalizePathEnd*(path: var Path, trailingSep = false) {.borrow.} func parentDir*(path: Path): Path {.inline.} = ## Returns the parent directory of `path`. @@ -269,6 +258,8 @@ proc normalizeExe*(file: var Path) {.borrow.} proc normalizePath*(path: var Path) {.borrow.} +proc normalizePathEnd*(path: var Path, trailingSep = false) {.borrow.} + proc absolutePath*(path: Path, root = getCurrentDir()): Path = ## Returns the absolute path of `path`, rooted at `root` (which must be absolute; ## default: current directory). From 825a84f99f175417c1c044bbbc551cf1e4216ff6 Mon Sep 17 00:00:00 2001 From: xflywind <43030857+ringabout@users.noreply.github.com> Date: Sat, 22 Oct 2022 01:03:17 +0800 Subject: [PATCH 13/13] cleanup --- lib/std/files.nim | 2 -- lib/std/paths.nim | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/std/files.nim b/lib/std/files.nim index f83afaed73324..f7fbd5acd71e0 100644 --- a/lib/std/files.nim +++ b/lib/std/files.nim @@ -22,7 +22,6 @@ proc removeFile*(file: Path) {.inline, tags: [WriteDirEffect].} = ## * `removeDir proc`_ ## * `copyFile proc`_ ## * `copyFileWithPermissions proc`_ - ## * `tryRemoveFile proc`_ ## * `moveFile proc`_ removeFile(file.string) @@ -43,5 +42,4 @@ proc moveFile*(source, dest: Path) {.inline, ## * `copyFile proc`_ ## * `copyFileWithPermissions proc`_ ## * `removeFile proc`_ - ## * `tryRemoveFile proc`_ moveFile(source.string, dest.string) diff --git a/lib/std/paths.nim b/lib/std/paths.nim index 770c282e42822..b3cd7de839c76 100644 --- a/lib/std/paths.nim +++ b/lib/std/paths.nim @@ -5,7 +5,7 @@ import pathnorm from std/private/ospaths2 import joinPath, splitPath, ReadDirEffect, WriteDirEffect, - isAbsolute, relativePath, normalizedPath, + isAbsolute, relativePath, normalizePathEnd, isRelativeTo, parentDir, tailDir, isRootDir, parentDirs, `/../`, extractFilename, lastPathPart, @@ -266,6 +266,5 @@ proc absolutePath*(path: Path, root = getCurrentDir()): Path = ## If `path` is absolute, return it, ignoring `root`. ## ## See also: - ## * `normalizedPath proc`_ ## * `normalizePath proc`_ result = Path(absolutePath(path.string, root.string))