Files
Nim/lib/pure/ospaths.nim
Andreas Rumpf b67ea9e73b fixes #6327
2017-09-05 09:03:09 +02:00

561 lines
19 KiB
Nim

#
#
# Nim's Runtime Library
# (c) Copyright 2015 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
# Forwarded by the ``os`` module but a module in its own right for NimScript
# support.
include "system/inclrtl"
import strutils
type
ReadEnvEffect* = object of ReadIOEffect ## effect that denotes a read
## from an environment variable
WriteEnvEffect* = object of WriteIOEffect ## effect that denotes a write
## to an environment variable
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
OSErrorCode* = distinct int32 ## Specifies an OS Error Code.
{.deprecated: [FReadEnv: ReadEnvEffect, FWriteEnv: WriteEnvEffect,
FReadDir: ReadDirEffect,
FWriteDir: WriteDirEffect,
TOSErrorCode: OSErrorCode
].}
const
doslikeFileSystem* = defined(windows) or defined(OS2) or defined(DOS)
when defined(Nimdoc): # only for proper documentation:
const
CurDir* = '.'
## The constant string used by the operating system to refer to the
## current directory.
##
## For example: '.' for POSIX or ':' for the classic Macintosh.
ParDir* = ".."
## The constant string used by the operating system to refer to the
## parent directory.
##
## For example: ".." for POSIX or "::" for the classic Macintosh.
DirSep* = '/'
## The character used by the operating system to separate pathname
## components, for example, '/' for POSIX or ':' for the classic
## Macintosh.
AltSep* = '/'
## An alternative character used by the operating system to separate
## pathname components, or the same as `DirSep` if only one separator
## character exists. This is set to '/' on Windows systems
## where `DirSep` is a backslash.
PathSep* = ':'
## The character conventionally used by the operating system to separate
## search patch components (as in PATH), such as ':' for POSIX
## or ';' for Windows.
FileSystemCaseSensitive* = true
## true if the file system is case sensitive, false otherwise. Used by
## `cmpPaths` to compare filenames properly.
ExeExt* = ""
## The file extension of native executables. For example:
## "" for POSIX, "exe" on Windows.
ScriptExt* = ""
## The file extension of a script file. For example: "" for POSIX,
## "bat" on Windows.
DynlibFormat* = "lib$1.so"
## The format string to turn a filename into a `DLL`:idx: file (also
## called `shared object`:idx: on some operating systems).
elif defined(macos):
const
CurDir* = ':'
ParDir* = "::"
DirSep* = ':'
AltSep* = Dirsep
PathSep* = ','
FileSystemCaseSensitive* = false
ExeExt* = ""
ScriptExt* = ""
DynlibFormat* = "$1.dylib"
# MacOS paths
# ===========
# MacOS directory separator is a colon ":" which is the only character not
# allowed in filenames.
#
# A path containing no colon or which begins with a colon is a partial
# path.
# E.g. ":kalle:petter" ":kalle" "kalle"
#
# All other paths are full (absolute) paths. E.g. "HD:kalle:" "HD:"
# When generating paths, one is safe if one ensures that all partial paths
# begin with a colon, and all full paths end with a colon.
# In full paths the first name (e g HD above) is the name of a mounted
# volume.
# These names are not unique, because, for instance, two diskettes with the
# same names could be inserted. This means that paths on MacOS are not
# waterproof. In case of equal names the first volume found will do.
# Two colons "::" are the relative path to the parent. Three is to the
# grandparent etc.
elif doslikeFileSystem:
const
CurDir* = '.'
ParDir* = ".."
DirSep* = '\\' # seperator within paths
AltSep* = '/'
PathSep* = ';' # seperator between paths
FileSystemCaseSensitive* = false
ExeExt* = "exe"
ScriptExt* = "bat"
DynlibFormat* = "$1.dll"
elif defined(PalmOS) or defined(MorphOS):
const
DirSep* = '/'
AltSep* = Dirsep
PathSep* = ';'
ParDir* = ".."
FileSystemCaseSensitive* = false
ExeExt* = ""
ScriptExt* = ""
DynlibFormat* = "$1.prc"
elif defined(RISCOS):
const
DirSep* = '.'
AltSep* = '.'
ParDir* = ".." # is this correct?
PathSep* = ','
FileSystemCaseSensitive* = true
ExeExt* = ""
ScriptExt* = ""
DynlibFormat* = "lib$1.so"
else: # UNIX-like operating system
const
CurDir* = '.'
ParDir* = ".."
DirSep* = '/'
AltSep* = DirSep
PathSep* = ':'
FileSystemCaseSensitive* = true
ExeExt* = ""
ScriptExt* = ""
DynlibFormat* = when defined(macosx): "lib$1.dylib" else: "lib$1.so"
const
ExtSep* = '.'
## The character which separates the base filename from the extension;
## for example, the '.' in ``os.nim``.
proc joinPath*(head, tail: string): string {.
noSideEffect, rtl, extern: "nos$1".} =
## Joins two directory names to one.
##
## For example on Unix:
##
## .. code-block:: nim
## joinPath("usr", "lib")
##
## results in:
##
## .. code-block:: nim
## "usr/lib"
##
## If head is the empty string, tail is returned. If tail is the empty
## string, head is returned with a trailing path separator. If tail starts
## with a path separator it will be removed when concatenated to head. Other
## path separators not located on boundaries won't be modified. More
## examples on Unix:
##
## .. code-block:: nim
## assert joinPath("usr", "") == "usr/"
## assert joinPath("", "lib") == "lib"
## assert joinPath("", "/lib") == "/lib"
## assert joinPath("usr/", "/lib") == "usr/lib"
if len(head) == 0:
result = tail
elif head[len(head)-1] in {DirSep, AltSep}:
if tail[0] in {DirSep, AltSep}:
result = head & substr(tail, 1)
else:
result = head & tail
else:
if tail[0] in {DirSep, AltSep}:
result = head & tail
else:
result = head & DirSep & tail
proc joinPath*(parts: varargs[string]): string {.noSideEffect,
rtl, extern: "nos$1OpenArray".} =
## The same as `joinPath(head, tail)`, 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.
result = parts[0]
for i in 1..high(parts):
result = joinPath(result, parts[i])
proc `/` * (head, tail: string): string {.noSideEffect.} =
## The same as ``joinPath(head, tail)``
##
## Here are some examples for Unix:
##
## .. code-block:: nim
## assert "usr" / "" == "usr/"
## assert "" / "lib" == "lib"
## assert "" / "/lib" == "/lib"
## assert "usr/" / "/lib" == "usr/lib"
return joinPath(head, tail)
proc splitPath*(path: string): tuple[head, tail: string] {.
noSideEffect, rtl, extern: "nos$1".} =
## Splits a directory into (head, tail), so that
## ``head / tail == path`` (except for edge cases like "/usr").
##
## Examples:
##
## .. code-block:: nim
## splitPath("usr/local/bin") -> ("usr/local", "bin")
## splitPath("usr/local/bin/") -> ("usr/local/bin", "")
## splitPath("bin") -> ("", "bin")
## splitPath("/bin") -> ("", "bin")
## splitPath("") -> ("", "")
var sepPos = -1
for i in countdown(len(path)-1, 0):
if path[i] in {DirSep, AltSep}:
sepPos = i
break
if sepPos >= 0:
result.head = substr(path, 0, sepPos-1)
result.tail = substr(path, sepPos+1)
else:
result.head = ""
result.tail = path
proc parentDirPos(path: string): int =
var q = 1
if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
for i in countdown(len(path)-q, 0):
if path[i] in {DirSep, AltSep}: return i
result = -1
proc parentDir*(path: string): string {.
noSideEffect, rtl, extern: "nos$1".} =
## Returns the parent directory of `path`.
##
## This is often the same as the ``head`` result of ``splitPath``.
## If there is no parent, "" is returned.
## | Example: ``parentDir("/usr/local/bin") == "/usr/local"``.
## | Example: ``parentDir("/usr/local/bin/") == "/usr/local"``.
let sepPos = parentDirPos(path)
if sepPos >= 0:
result = substr(path, 0, sepPos-1)
else:
result = ""
proc tailDir*(path: string): string {.
noSideEffect, rtl, extern: "nos$1".} =
## Returns the tail part of `path`..
##
## | Example: ``tailDir("/usr/local/bin") == "local/bin"``.
## | Example: ``tailDir("usr/local/bin/") == "local/bin"``.
## | Example: ``tailDir("bin") == ""``.
var q = 1
if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
for i in 0..len(path)-q:
if path[i] in {DirSep, AltSep}:
return substr(path, i+1)
result = ""
proc isRootDir*(path: string): bool {.
noSideEffect, rtl, extern: "nos$1".} =
## Checks whether a given `path` is a root directory
result = parentDirPos(path) < 0
iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
## Walks over all parent directories of a given `path`
##
## If `fromRoot` is set, the traversal will start from the file system root
## diretory. If `inclusive` is set, the original argument will be included
## in the traversal.
##
## Relative paths won't be expanded by this proc. Instead, it will traverse
## only the directories appearing in the relative path.
if not fromRoot:
var current = path
if inclusive: yield path
while true:
if current.isRootDir: break
current = current.parentDir
yield current
else:
for i in countup(0, path.len - 2): # ignore the last /
# deal with non-normalized paths such as /foo//bar//baz
if path[i] in {DirSep, AltSep} and
(i == 0 or path[i-1] notin {DirSep, AltSep}):
yield path.substr(0, i)
if inclusive: yield path
proc `/../`*(head, tail: string): string {.noSideEffect.} =
## The same as ``parentDir(head) / tail`` unless there is no parent
## directory. Then ``head / tail`` is performed instead.
let sepPos = parentDirPos(head)
if sepPos >= 0:
result = substr(head, 0, sepPos-1) / tail
else:
result = head / tail
proc normExt(ext: string): string =
if ext == "" or ext[0] == ExtSep: result = ext # no copy needed here
else: result = ExtSep & ext
proc searchExtPos*(path: string): int =
## Returns index of the '.' char in `path` if it signifies the beginning
## of extension. Returns -1 otherwise.
# BUGFIX: do not search until 0! .DS_Store is no file extension!
result = -1
for i in countdown(len(path)-1, 1):
if path[i] == ExtSep:
result = i
break
elif path[i] in {DirSep, AltSep}:
break # do not skip over path
proc splitFile*(path: string): tuple[dir, name, ext: string] {.
noSideEffect, rtl, extern: "nos$1".} =
## Splits a filename into (dir, filename, extension).
## `dir` does not end in `DirSep`.
## `extension` includes the leading dot.
##
## Example:
##
## .. code-block:: nim
## var (dir, name, ext) = splitFile("usr/local/nimc.html")
## assert dir == "usr/local"
## assert name == "nimc"
## assert ext == ".html"
##
## 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.
if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
result = (path, "", "")
else:
var sepPos = -1
var dotPos = path.len
for i in countdown(len(path)-1, 0):
if path[i] == ExtSep:
if dotPos == path.len and i > 0 and
path[i-1] notin {DirSep, AltSep}: dotPos = i
elif path[i] in {DirSep, AltSep}:
sepPos = i
break
result.dir = substr(path, 0, sepPos-1)
result.name = substr(path, sepPos+1, dotPos-1)
result.ext = substr(path, dotPos)
proc extractFilename*(path: string): string {.
noSideEffect, rtl, extern: "nos$1".} =
## Extracts the filename of a given `path`. This is the same as
## ``name & ext`` from ``splitFile(path)``.
if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
result = ""
else:
result = splitPath(path).tail
proc changeFileExt*(filename, ext: string): string {.
noSideEffect, rtl, extern: "nos$1".} =
## 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.)
var extPos = searchExtPos(filename)
if extPos < 0: result = filename & normExt(ext)
else: result = substr(filename, 0, extPos-1) & normExt(ext)
proc addFileExt*(filename, ext: string): string {.
noSideEffect, rtl, extern: "nos$1".} =
## 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.)
var extPos = searchExtPos(filename)
if extPos < 0: result = filename & normExt(ext)
else: result = filename
proc cmpPaths*(pathA, pathB: string): int {.
noSideEffect, rtl, extern: "nos$1".} =
## Compares two paths.
##
## On a case-sensitive filesystem this is done
## case-sensitively otherwise case-insensitively. Returns:
##
## | 0 iff pathA == pathB
## | < 0 iff pathA < pathB
## | > 0 iff pathA > pathB
if FileSystemCaseSensitive:
result = cmp(pathA, pathB)
else:
when defined(nimscript):
result = cmpic(pathA, pathB)
elif defined(nimdoc): discard
else:
result = cmpIgnoreCase(pathA, pathB)
proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1".} =
## Checks whether a given `path` is absolute.
##
## On Windows, network paths are considered absolute too.
when doslikeFileSystem:
var len = len(path)
result = (len > 0 and path[0] in {'/', '\\'}) or
(len > 1 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':')
elif defined(macos):
result = path.len > 0 and path[0] != ':'
elif defined(RISCOS):
result = path[0] == '$'
elif defined(posix):
result = path[0] == '/'
proc unixToNativePath*(path: string, drive=""): string {.
noSideEffect, rtl, extern: "nos$1".} =
## 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".
when defined(unix):
result = path
else:
var start: int
if path[0] == '/':
# an absolute path
when doslikeFileSystem:
if drive != "":
result = drive & ":" & DirSep
else:
result = $DirSep
elif defined(macos):
result = "" # must not start with ':'
else:
result = $DirSep
start = 1
elif path[0] == '.' and path[1] == '/':
# current directory
result = $CurDir
start = 2
else:
result = ""
start = 0
var i = start
while i < len(path): # ../../../ --> ::::
if path[i] == '.' and path[i+1] == '.' and path[i+2] == '/':
# parent directory
when defined(macos):
if result[high(result)] == ':':
add result, ':'
else:
add result, ParDir
else:
add result, ParDir & DirSep
inc(i, 3)
elif path[i] == '/':
add result, DirSep
inc(i)
else:
add result, path[i]
inc(i)
include "includes/oserr"
when not defined(nimscript):
include "includes/osenv"
proc getHomeDir*(): string {.rtl, extern: "nos$1",
tags: [ReadEnvEffect, ReadIOEffect].} =
## Returns the home directory of the current user.
##
## This proc is wrapped by the expandTilde proc for the convenience of
## processing paths coming from user configuration files.
when defined(windows): return string(getEnv("USERPROFILE")) & "\\"
else: return string(getEnv("HOME")) & "/"
proc getConfigDir*(): string {.rtl, extern: "nos$1",
tags: [ReadEnvEffect, ReadIOEffect].} =
## Returns the config directory of the current user for applications.
##
## On non-Windows OSs, this proc conforms to the XDG Base Directory
## spec. Thus, this proc returns the value of the XDG_CONFIG_DIR environment
## variable if it is set, and returns the default configuration directory,
## "~/.config/", otherwise.
##
## An OS-dependent trailing slash is always present at the end of the
## returned string; `\\` on Windows and `/` on all other OSs.
when defined(windows): return string(getEnv("APPDATA")) & "\\"
elif getEnv("XDG_CONFIG_DIR"): return string(getEnv("XDG_CONFIG_DIR")) & "/"
else: return string(getEnv("HOME")) & "/.config/"
proc getTempDir*(): string {.rtl, extern: "nos$1",
tags: [ReadEnvEffect, ReadIOEffect].} =
## Returns the temporary directory of the current user for applications to
## save temporary files in.
##
## **Please do not use this**: On Android, it currently
## returns ``getHomeDir()``, and on other Unix based systems it can cause
## security problems too. That said, you can override this implementation
## by adding ``-d:tempDir=mytempname`` to your compiler invokation.
when defined(tempDir):
const tempDir {.strdefine.}: string = nil
return tempDir
elif defined(windows): return string(getEnv("TEMP")) & "\\"
elif defined(android): return getHomeDir()
else: return "/tmp/"
proc expandTilde*(path: string): string {.
tags: [ReadEnvEffect, ReadIOEffect].} =
## Expands a path starting with ``~/`` to a full path.
##
## If `path` starts with the tilde character and is followed by `/` or `\\`
## this proc will return the reminder of the path appended to the result of
## the getHomeDir() proc, otherwise the input path will be returned without
## modification.
##
## The behaviour of this proc is the same on the Windows platform despite
## not having this convention. Example:
##
## .. code-block:: nim
## let configFile = expandTilde("~" / "appname.cfg")
## echo configFile
## # --> C:\Users\amber\appname.cfg
if len(path) > 1 and path[0] == '~' and (path[1] == '/' or path[1] == '\\'):
result = getHomeDir() / path.substr(2)
else:
result = path