ospaths.nim 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2015 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. # Forwarded by the ``os`` module but a module in its own right for NimScript
  10. # support.
  11. include "system/inclrtl"
  12. import strutils
  13. type
  14. ReadEnvEffect* = object of ReadIOEffect ## effect that denotes a read
  15. ## from an environment variable
  16. WriteEnvEffect* = object of WriteIOEffect ## effect that denotes a write
  17. ## to an environment variable
  18. ReadDirEffect* = object of ReadIOEffect ## effect that denotes a read
  19. ## operation from the directory
  20. ## structure
  21. WriteDirEffect* = object of WriteIOEffect ## effect that denotes a write
  22. ## operation to
  23. ## the directory structure
  24. OSErrorCode* = distinct int32 ## Specifies an OS Error Code.
  25. {.deprecated: [FReadEnv: ReadEnvEffect, FWriteEnv: WriteEnvEffect,
  26. FReadDir: ReadDirEffect,
  27. FWriteDir: WriteDirEffect,
  28. TOSErrorCode: OSErrorCode
  29. ].}
  30. const
  31. doslikeFileSystem* = defined(windows) or defined(OS2) or defined(DOS)
  32. when defined(Nimdoc): # only for proper documentation:
  33. const
  34. CurDir* = '.'
  35. ## The constant string used by the operating system to refer to the
  36. ## current directory.
  37. ##
  38. ## For example: '.' for POSIX or ':' for the classic Macintosh.
  39. ParDir* = ".."
  40. ## The constant string used by the operating system to refer to the
  41. ## parent directory.
  42. ##
  43. ## For example: ".." for POSIX or "::" for the classic Macintosh.
  44. DirSep* = '/'
  45. ## The character used by the operating system to separate pathname
  46. ## components, for example, '/' for POSIX or ':' for the classic
  47. ## Macintosh.
  48. AltSep* = '/'
  49. ## An alternative character used by the operating system to separate
  50. ## pathname components, or the same as `DirSep` if only one separator
  51. ## character exists. This is set to '/' on Windows systems
  52. ## where `DirSep` is a backslash.
  53. PathSep* = ':'
  54. ## The character conventionally used by the operating system to separate
  55. ## search patch components (as in PATH), such as ':' for POSIX
  56. ## or ';' for Windows.
  57. FileSystemCaseSensitive* = true
  58. ## true if the file system is case sensitive, false otherwise. Used by
  59. ## `cmpPaths` to compare filenames properly.
  60. ExeExt* = ""
  61. ## The file extension of native executables. For example:
  62. ## "" for POSIX, "exe" on Windows.
  63. ScriptExt* = ""
  64. ## The file extension of a script file. For example: "" for POSIX,
  65. ## "bat" on Windows.
  66. DynlibFormat* = "lib$1.so"
  67. ## The format string to turn a filename into a `DLL`:idx: file (also
  68. ## called `shared object`:idx: on some operating systems).
  69. elif defined(macos):
  70. const
  71. CurDir* = ':'
  72. ParDir* = "::"
  73. DirSep* = ':'
  74. AltSep* = Dirsep
  75. PathSep* = ','
  76. FileSystemCaseSensitive* = false
  77. ExeExt* = ""
  78. ScriptExt* = ""
  79. DynlibFormat* = "$1.dylib"
  80. # MacOS paths
  81. # ===========
  82. # MacOS directory separator is a colon ":" which is the only character not
  83. # allowed in filenames.
  84. #
  85. # A path containing no colon or which begins with a colon is a partial
  86. # path.
  87. # E.g. ":kalle:petter" ":kalle" "kalle"
  88. #
  89. # All other paths are full (absolute) paths. E.g. "HD:kalle:" "HD:"
  90. # When generating paths, one is safe if one ensures that all partial paths
  91. # begin with a colon, and all full paths end with a colon.
  92. # In full paths the first name (e g HD above) is the name of a mounted
  93. # volume.
  94. # These names are not unique, because, for instance, two diskettes with the
  95. # same names could be inserted. This means that paths on MacOS are not
  96. # waterproof. In case of equal names the first volume found will do.
  97. # Two colons "::" are the relative path to the parent. Three is to the
  98. # grandparent etc.
  99. elif doslikeFileSystem:
  100. const
  101. CurDir* = '.'
  102. ParDir* = ".."
  103. DirSep* = '\\' # seperator within paths
  104. AltSep* = '/'
  105. PathSep* = ';' # seperator between paths
  106. FileSystemCaseSensitive* = false
  107. ExeExt* = "exe"
  108. ScriptExt* = "bat"
  109. DynlibFormat* = "$1.dll"
  110. elif defined(PalmOS) or defined(MorphOS):
  111. const
  112. DirSep* = '/'
  113. AltSep* = Dirsep
  114. PathSep* = ';'
  115. ParDir* = ".."
  116. FileSystemCaseSensitive* = false
  117. ExeExt* = ""
  118. ScriptExt* = ""
  119. DynlibFormat* = "$1.prc"
  120. elif defined(RISCOS):
  121. const
  122. DirSep* = '.'
  123. AltSep* = '.'
  124. ParDir* = ".." # is this correct?
  125. PathSep* = ','
  126. FileSystemCaseSensitive* = true
  127. ExeExt* = ""
  128. ScriptExt* = ""
  129. DynlibFormat* = "lib$1.so"
  130. else: # UNIX-like operating system
  131. const
  132. CurDir* = '.'
  133. ParDir* = ".."
  134. DirSep* = '/'
  135. AltSep* = DirSep
  136. PathSep* = ':'
  137. FileSystemCaseSensitive* = true
  138. ExeExt* = ""
  139. ScriptExt* = ""
  140. DynlibFormat* = when defined(macosx): "lib$1.dylib" else: "lib$1.so"
  141. const
  142. ExtSep* = '.'
  143. ## The character which separates the base filename from the extension;
  144. ## for example, the '.' in ``os.nim``.
  145. proc joinPath*(head, tail: string): string {.
  146. noSideEffect, rtl, extern: "nos$1".} =
  147. ## Joins two directory names to one.
  148. ##
  149. ## For example on Unix:
  150. ##
  151. ## .. code-block:: nim
  152. ## joinPath("usr", "lib")
  153. ##
  154. ## results in:
  155. ##
  156. ## .. code-block:: nim
  157. ## "usr/lib"
  158. ##
  159. ## If head is the empty string, tail is returned. If tail is the empty
  160. ## string, head is returned with a trailing path separator. If tail starts
  161. ## with a path separator it will be removed when concatenated to head. Other
  162. ## path separators not located on boundaries won't be modified. More
  163. ## examples on Unix:
  164. ##
  165. ## .. code-block:: nim
  166. ## assert joinPath("usr", "") == "usr/"
  167. ## assert joinPath("", "lib") == "lib"
  168. ## assert joinPath("", "/lib") == "/lib"
  169. ## assert joinPath("usr/", "/lib") == "usr/lib"
  170. if len(head) == 0:
  171. result = tail
  172. elif head[len(head)-1] in {DirSep, AltSep}:
  173. if tail[0] in {DirSep, AltSep}:
  174. result = head & substr(tail, 1)
  175. else:
  176. result = head & tail
  177. else:
  178. if tail[0] in {DirSep, AltSep}:
  179. result = head & tail
  180. else:
  181. result = head & DirSep & tail
  182. proc joinPath*(parts: varargs[string]): string {.noSideEffect,
  183. rtl, extern: "nos$1OpenArray".} =
  184. ## The same as `joinPath(head, tail)`, but works with any number of
  185. ## directory parts. You need to pass at least one element or the proc
  186. ## will assert in debug builds and crash on release builds.
  187. result = parts[0]
  188. for i in 1..high(parts):
  189. result = joinPath(result, parts[i])
  190. proc `/` * (head, tail: string): string {.noSideEffect.} =
  191. ## The same as ``joinPath(head, tail)``
  192. ##
  193. ## Here are some examples for Unix:
  194. ##
  195. ## .. code-block:: nim
  196. ## assert "usr" / "" == "usr/"
  197. ## assert "" / "lib" == "lib"
  198. ## assert "" / "/lib" == "/lib"
  199. ## assert "usr/" / "/lib" == "usr/lib"
  200. return joinPath(head, tail)
  201. proc splitPath*(path: string): tuple[head, tail: string] {.
  202. noSideEffect, rtl, extern: "nos$1".} =
  203. ## Splits a directory into (head, tail), so that
  204. ## ``head / tail == path`` (except for edge cases like "/usr").
  205. ##
  206. ## Examples:
  207. ##
  208. ## .. code-block:: nim
  209. ## splitPath("usr/local/bin") -> ("usr/local", "bin")
  210. ## splitPath("usr/local/bin/") -> ("usr/local/bin", "")
  211. ## splitPath("bin") -> ("", "bin")
  212. ## splitPath("/bin") -> ("", "bin")
  213. ## splitPath("") -> ("", "")
  214. var sepPos = -1
  215. for i in countdown(len(path)-1, 0):
  216. if path[i] in {DirSep, AltSep}:
  217. sepPos = i
  218. break
  219. if sepPos >= 0:
  220. result.head = substr(path, 0, sepPos-1)
  221. result.tail = substr(path, sepPos+1)
  222. else:
  223. result.head = ""
  224. result.tail = path
  225. proc parentDirPos(path: string): int =
  226. var q = 1
  227. if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
  228. for i in countdown(len(path)-q, 0):
  229. if path[i] in {DirSep, AltSep}: return i
  230. result = -1
  231. proc parentDir*(path: string): string {.
  232. noSideEffect, rtl, extern: "nos$1".} =
  233. ## Returns the parent directory of `path`.
  234. ##
  235. ## This is often the same as the ``head`` result of ``splitPath``.
  236. ## If there is no parent, "" is returned.
  237. ## | Example: ``parentDir("/usr/local/bin") == "/usr/local"``.
  238. ## | Example: ``parentDir("/usr/local/bin/") == "/usr/local"``.
  239. let sepPos = parentDirPos(path)
  240. if sepPos >= 0:
  241. result = substr(path, 0, sepPos-1)
  242. else:
  243. result = ""
  244. proc tailDir*(path: string): string {.
  245. noSideEffect, rtl, extern: "nos$1".} =
  246. ## Returns the tail part of `path`..
  247. ##
  248. ## | Example: ``tailDir("/usr/local/bin") == "local/bin"``.
  249. ## | Example: ``tailDir("usr/local/bin/") == "local/bin"``.
  250. ## | Example: ``tailDir("bin") == ""``.
  251. var q = 1
  252. if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
  253. for i in 0..len(path)-q:
  254. if path[i] in {DirSep, AltSep}:
  255. return substr(path, i+1)
  256. result = ""
  257. proc isRootDir*(path: string): bool {.
  258. noSideEffect, rtl, extern: "nos$1".} =
  259. ## Checks whether a given `path` is a root directory
  260. result = parentDirPos(path) < 0
  261. iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
  262. ## Walks over all parent directories of a given `path`
  263. ##
  264. ## If `fromRoot` is set, the traversal will start from the file system root
  265. ## diretory. If `inclusive` is set, the original argument will be included
  266. ## in the traversal.
  267. ##
  268. ## Relative paths won't be expanded by this proc. Instead, it will traverse
  269. ## only the directories appearing in the relative path.
  270. if not fromRoot:
  271. var current = path
  272. if inclusive: yield path
  273. while true:
  274. if current.isRootDir: break
  275. current = current.parentDir
  276. yield current
  277. else:
  278. for i in countup(0, path.len - 2): # ignore the last /
  279. # deal with non-normalized paths such as /foo//bar//baz
  280. if path[i] in {DirSep, AltSep} and
  281. (i == 0 or path[i-1] notin {DirSep, AltSep}):
  282. yield path.substr(0, i)
  283. if inclusive: yield path
  284. proc `/../`*(head, tail: string): string {.noSideEffect.} =
  285. ## The same as ``parentDir(head) / tail`` unless there is no parent
  286. ## directory. Then ``head / tail`` is performed instead.
  287. let sepPos = parentDirPos(head)
  288. if sepPos >= 0:
  289. result = substr(head, 0, sepPos-1) / tail
  290. else:
  291. result = head / tail
  292. proc normExt(ext: string): string =
  293. if ext == "" or ext[0] == ExtSep: result = ext # no copy needed here
  294. else: result = ExtSep & ext
  295. proc searchExtPos*(path: string): int =
  296. ## Returns index of the '.' char in `path` if it signifies the beginning
  297. ## of extension. Returns -1 otherwise.
  298. # BUGFIX: do not search until 0! .DS_Store is no file extension!
  299. result = -1
  300. for i in countdown(len(path)-1, 1):
  301. if path[i] == ExtSep:
  302. result = i
  303. break
  304. elif path[i] in {DirSep, AltSep}:
  305. break # do not skip over path
  306. proc splitFile*(path: string): tuple[dir, name, ext: string] {.
  307. noSideEffect, rtl, extern: "nos$1".} =
  308. ## Splits a filename into (dir, filename, extension).
  309. ## `dir` does not end in `DirSep`.
  310. ## `extension` includes the leading dot.
  311. ##
  312. ## Example:
  313. ##
  314. ## .. code-block:: nim
  315. ## var (dir, name, ext) = splitFile("usr/local/nimc.html")
  316. ## assert dir == "usr/local"
  317. ## assert name == "nimc"
  318. ## assert ext == ".html"
  319. ##
  320. ## If `path` has no extension, `ext` is the empty string.
  321. ## If `path` has no directory component, `dir` is the empty string.
  322. ## If `path` has no filename component, `name` and `ext` are empty strings.
  323. if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
  324. result = (path, "", "")
  325. else:
  326. var sepPos = -1
  327. var dotPos = path.len
  328. for i in countdown(len(path)-1, 0):
  329. if path[i] == ExtSep:
  330. if dotPos == path.len and i > 0 and
  331. path[i-1] notin {DirSep, AltSep}: dotPos = i
  332. elif path[i] in {DirSep, AltSep}:
  333. sepPos = i
  334. break
  335. result.dir = substr(path, 0, sepPos-1)
  336. result.name = substr(path, sepPos+1, dotPos-1)
  337. result.ext = substr(path, dotPos)
  338. proc extractFilename*(path: string): string {.
  339. noSideEffect, rtl, extern: "nos$1".} =
  340. ## Extracts the filename of a given `path`. This is the same as
  341. ## ``name & ext`` from ``splitFile(path)``.
  342. if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
  343. result = ""
  344. else:
  345. result = splitPath(path).tail
  346. proc changeFileExt*(filename, ext: string): string {.
  347. noSideEffect, rtl, extern: "nos$1".} =
  348. ## Changes the file extension to `ext`.
  349. ##
  350. ## If the `filename` has no extension, `ext` will be added.
  351. ## If `ext` == "" then any extension is removed.
  352. ## `Ext` should be given without the leading '.', because some
  353. ## filesystems may use a different character. (Although I know
  354. ## of none such beast.)
  355. var extPos = searchExtPos(filename)
  356. if extPos < 0: result = filename & normExt(ext)
  357. else: result = substr(filename, 0, extPos-1) & normExt(ext)
  358. proc addFileExt*(filename, ext: string): string {.
  359. noSideEffect, rtl, extern: "nos$1".} =
  360. ## Adds the file extension `ext` to `filename`, unless
  361. ## `filename` already has an extension.
  362. ##
  363. ## `Ext` should be given without the leading '.', because some
  364. ## filesystems may use a different character.
  365. ## (Although I know of none such beast.)
  366. var extPos = searchExtPos(filename)
  367. if extPos < 0: result = filename & normExt(ext)
  368. else: result = filename
  369. proc cmpPaths*(pathA, pathB: string): int {.
  370. noSideEffect, rtl, extern: "nos$1".} =
  371. ## Compares two paths.
  372. ##
  373. ## On a case-sensitive filesystem this is done
  374. ## case-sensitively otherwise case-insensitively. Returns:
  375. ##
  376. ## | 0 iff pathA == pathB
  377. ## | < 0 iff pathA < pathB
  378. ## | > 0 iff pathA > pathB
  379. if FileSystemCaseSensitive:
  380. result = cmp(pathA, pathB)
  381. else:
  382. when defined(nimscript):
  383. result = cmpic(pathA, pathB)
  384. elif defined(nimdoc): discard
  385. else:
  386. result = cmpIgnoreCase(pathA, pathB)
  387. proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1".} =
  388. ## Checks whether a given `path` is absolute.
  389. ##
  390. ## On Windows, network paths are considered absolute too.
  391. when doslikeFileSystem:
  392. var len = len(path)
  393. result = (len > 0 and path[0] in {'/', '\\'}) or
  394. (len > 1 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':')
  395. elif defined(macos):
  396. result = path.len > 0 and path[0] != ':'
  397. elif defined(RISCOS):
  398. result = path[0] == '$'
  399. elif defined(posix):
  400. result = path[0] == '/'
  401. proc unixToNativePath*(path: string, drive=""): string {.
  402. noSideEffect, rtl, extern: "nos$1".} =
  403. ## Converts an UNIX-like path to a native one.
  404. ##
  405. ## On an UNIX system this does nothing. Else it converts
  406. ## '/', '.', '..' to the appropriate things.
  407. ##
  408. ## On systems with a concept of "drives", `drive` is used to determine
  409. ## which drive label to use during absolute path conversion.
  410. ## `drive` defaults to the drive of the current working directory, and is
  411. ## ignored on systems that do not have a concept of "drives".
  412. when defined(unix):
  413. result = path
  414. else:
  415. var start: int
  416. if path[0] == '/':
  417. # an absolute path
  418. when doslikeFileSystem:
  419. if drive != "":
  420. result = drive & ":" & DirSep
  421. else:
  422. result = $DirSep
  423. elif defined(macos):
  424. result = "" # must not start with ':'
  425. else:
  426. result = $DirSep
  427. start = 1
  428. elif path[0] == '.' and path[1] == '/':
  429. # current directory
  430. result = $CurDir
  431. start = 2
  432. else:
  433. result = ""
  434. start = 0
  435. var i = start
  436. while i < len(path): # ../../../ --> ::::
  437. if path[i] == '.' and path[i+1] == '.' and path[i+2] == '/':
  438. # parent directory
  439. when defined(macos):
  440. if result[high(result)] == ':':
  441. add result, ':'
  442. else:
  443. add result, ParDir
  444. else:
  445. add result, ParDir & DirSep
  446. inc(i, 3)
  447. elif path[i] == '/':
  448. add result, DirSep
  449. inc(i)
  450. else:
  451. add result, path[i]
  452. inc(i)
  453. include "includes/oserr"
  454. when not defined(nimscript):
  455. include "includes/osenv"
  456. proc getHomeDir*(): string {.rtl, extern: "nos$1",
  457. tags: [ReadEnvEffect, ReadIOEffect].} =
  458. ## Returns the home directory of the current user.
  459. ##
  460. ## This proc is wrapped by the expandTilde proc for the convenience of
  461. ## processing paths coming from user configuration files.
  462. when defined(windows): return string(getEnv("USERPROFILE")) & "\\"
  463. else: return string(getEnv("HOME")) & "/"
  464. proc getConfigDir*(): string {.rtl, extern: "nos$1",
  465. tags: [ReadEnvEffect, ReadIOEffect].} =
  466. ## Returns the config directory of the current user for applications.
  467. ##
  468. ## On non-Windows OSs, this proc conforms to the XDG Base Directory
  469. ## spec. Thus, this proc returns the value of the XDG_CONFIG_DIR environment
  470. ## variable if it is set, and returns the default configuration directory,
  471. ## "~/.config/", otherwise.
  472. ##
  473. ## An OS-dependent trailing slash is always present at the end of the
  474. ## returned string; `\\` on Windows and `/` on all other OSs.
  475. when defined(windows): return string(getEnv("APPDATA")) & "\\"
  476. elif getEnv("XDG_CONFIG_DIR"): return string(getEnv("XDG_CONFIG_DIR")) & "/"
  477. else: return string(getEnv("HOME")) & "/.config/"
  478. proc getTempDir*(): string {.rtl, extern: "nos$1",
  479. tags: [ReadEnvEffect, ReadIOEffect].} =
  480. ## Returns the temporary directory of the current user for applications to
  481. ## save temporary files in.
  482. ##
  483. ## **Please do not use this**: On Android, it currently
  484. ## returns ``getHomeDir()``, and on other Unix based systems it can cause
  485. ## security problems too. That said, you can override this implementation
  486. ## by adding ``-d:tempDir=mytempname`` to your compiler invokation.
  487. when defined(tempDir):
  488. const tempDir {.strdefine.}: string = nil
  489. return tempDir
  490. elif defined(windows): return string(getEnv("TEMP")) & "\\"
  491. elif defined(android): return getHomeDir()
  492. else: return "/tmp/"
  493. proc expandTilde*(path: string): string {.
  494. tags: [ReadEnvEffect, ReadIOEffect].} =
  495. ## Expands a path starting with ``~/`` to a full path.
  496. ##
  497. ## If `path` starts with the tilde character and is followed by `/` or `\\`
  498. ## this proc will return the reminder of the path appended to the result of
  499. ## the getHomeDir() proc, otherwise the input path will be returned without
  500. ## modification.
  501. ##
  502. ## The behaviour of this proc is the same on the Windows platform despite
  503. ## not having this convention. Example:
  504. ##
  505. ## .. code-block:: nim
  506. ## let configFile = expandTilde("~" / "appname.cfg")
  507. ## echo configFile
  508. ## # --> C:\Users\amber\appname.cfg
  509. if len(path) > 1 and path[0] == '~' and (path[1] == '/' or path[1] == '\\'):
  510. result = getHomeDir() / path.substr(2)
  511. else:
  512. result = path