ospaths2.nim 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031
  1. include system/inclrtl
  2. import std/private/since
  3. import std/[strutils, pathnorm]
  4. import std/oserrors
  5. import oscommon
  6. export ReadDirEffect, WriteDirEffect
  7. when defined(nimPreviewSlimSystem):
  8. import std/[syncio, assertions, widestrs]
  9. ## .. importdoc:: osappdirs.nim, osdirs.nim, osseps.nim, os.nim
  10. const weirdTarget = defined(nimscript) or defined(js)
  11. when weirdTarget:
  12. discard
  13. elif defined(windows):
  14. import std/winlean
  15. elif defined(posix):
  16. import std/posix, system/ansi_c
  17. else:
  18. {.error: "OS module not ported to your operating system!".}
  19. when weirdTarget:
  20. {.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".}
  21. else:
  22. {.pragma: noWeirdTarget.}
  23. when defined(nimscript):
  24. # for procs already defined in scriptconfig.nim
  25. template noNimJs(body): untyped = discard
  26. elif defined(js):
  27. {.pragma: noNimJs, error: "this proc is not available on the js target".}
  28. else:
  29. {.pragma: noNimJs.}
  30. proc normalizePathAux(path: var string){.inline, raises: [], noSideEffect.}
  31. import std/private/osseps
  32. export osseps
  33. proc absolutePathInternal(path: string): string {.gcsafe.}
  34. proc normalizePathEnd*(path: var string, trailingSep = false) =
  35. ## Ensures ``path`` has exactly 0 or 1 trailing `DirSep`, depending on
  36. ## ``trailingSep``, and taking care of edge cases: it preservers whether
  37. ## a path is absolute or relative, and makes sure trailing sep is `DirSep`,
  38. ## not `AltSep`. Trailing `/.` are compressed, see examples.
  39. if path.len == 0: return
  40. var i = path.len
  41. while i >= 1:
  42. if path[i-1] in {DirSep, AltSep}: dec(i)
  43. elif path[i-1] == '.' and i >= 2 and path[i-2] in {DirSep, AltSep}: dec(i)
  44. else: break
  45. if trailingSep:
  46. # foo// => foo
  47. path.setLen(i)
  48. # foo => foo/
  49. path.add DirSep
  50. elif i > 0:
  51. # foo// => foo
  52. path.setLen(i)
  53. else:
  54. # // => / (empty case was already taken care of)
  55. path = $DirSep
  56. proc normalizePathEnd*(path: string, trailingSep = false): string =
  57. ## outplace overload
  58. runnableExamples:
  59. when defined(posix):
  60. assert normalizePathEnd("/lib//.//", trailingSep = true) == "/lib/"
  61. assert normalizePathEnd("lib/./.", trailingSep = false) == "lib"
  62. assert normalizePathEnd(".//./.", trailingSep = false) == "."
  63. assert normalizePathEnd("", trailingSep = true) == "" # not / !
  64. assert normalizePathEnd("/", trailingSep = false) == "/" # not "" !
  65. result = path
  66. result.normalizePathEnd(trailingSep)
  67. template endsWith(a: string, b: set[char]): bool =
  68. a.len > 0 and a[^1] in b
  69. proc joinPathImpl(result: var string, state: var int, tail: string) =
  70. let trailingSep = tail.endsWith({DirSep, AltSep}) or tail.len == 0 and result.endsWith({DirSep, AltSep})
  71. normalizePathEnd(result, trailingSep=false)
  72. addNormalizePath(tail, result, state, DirSep)
  73. normalizePathEnd(result, trailingSep=trailingSep)
  74. proc joinPath*(head, tail: string): string {.
  75. noSideEffect, rtl, extern: "nos$1".} =
  76. ## Joins two directory names to one.
  77. ##
  78. ## returns normalized path concatenation of `head` and `tail`, preserving
  79. ## whether or not `tail` has a trailing slash (or, if tail if empty, whether
  80. ## head has one).
  81. ##
  82. ## See also:
  83. ## * `joinPath(parts: varargs[string]) proc`_
  84. ## * `/ proc`_
  85. ## * `splitPath proc`_
  86. ## * `uri.combine proc <uri.html#combine,Uri,Uri>`_
  87. ## * `uri./ proc <uri.html#/,Uri,string>`_
  88. runnableExamples:
  89. when defined(posix):
  90. assert joinPath("usr", "lib") == "usr/lib"
  91. assert joinPath("usr", "lib/") == "usr/lib/"
  92. assert joinPath("usr", "") == "usr"
  93. assert joinPath("usr/", "") == "usr/"
  94. assert joinPath("", "") == ""
  95. assert joinPath("", "lib") == "lib"
  96. assert joinPath("", "/lib") == "/lib"
  97. assert joinPath("usr/", "/lib") == "usr/lib"
  98. assert joinPath("usr/lib", "../bin") == "usr/bin"
  99. result = newStringOfCap(head.len + tail.len)
  100. var state = 0
  101. joinPathImpl(result, state, head)
  102. joinPathImpl(result, state, tail)
  103. when false:
  104. if len(head) == 0:
  105. result = tail
  106. elif head[len(head)-1] in {DirSep, AltSep}:
  107. if tail.len > 0 and tail[0] in {DirSep, AltSep}:
  108. result = head & substr(tail, 1)
  109. else:
  110. result = head & tail
  111. else:
  112. if tail.len > 0 and tail[0] in {DirSep, AltSep}:
  113. result = head & tail
  114. else:
  115. result = head & DirSep & tail
  116. proc joinPath*(parts: varargs[string]): string {.noSideEffect,
  117. rtl, extern: "nos$1OpenArray".} =
  118. ## The same as `joinPath(head, tail) proc`_,
  119. ## but works with any number of directory parts.
  120. ##
  121. ## You need to pass at least one element or the proc
  122. ## will assert in debug builds and crash on release builds.
  123. ##
  124. ## See also:
  125. ## * `joinPath(head, tail) proc`_
  126. ## * `/ proc`_
  127. ## * `/../ proc`_
  128. ## * `splitPath proc`_
  129. runnableExamples:
  130. when defined(posix):
  131. assert joinPath("a") == "a"
  132. assert joinPath("a", "b", "c") == "a/b/c"
  133. assert joinPath("usr/lib", "../../var", "log") == "var/log"
  134. var estimatedLen = 0
  135. for p in parts: estimatedLen += p.len
  136. result = newStringOfCap(estimatedLen)
  137. var state = 0
  138. for i in 0..high(parts):
  139. joinPathImpl(result, state, parts[i])
  140. proc `/`*(head, tail: string): string {.noSideEffect, inline.} =
  141. ## The same as `joinPath(head, tail) proc`_.
  142. ##
  143. ## See also:
  144. ## * `/../ proc`_
  145. ## * `joinPath(head, tail) proc`_
  146. ## * `joinPath(parts: varargs[string]) proc`_
  147. ## * `splitPath proc`_
  148. ## * `uri.combine proc <uri.html#combine,Uri,Uri>`_
  149. ## * `uri./ proc <uri.html#/,Uri,string>`_
  150. runnableExamples:
  151. when defined(posix):
  152. assert "usr" / "" == "usr"
  153. assert "" / "lib" == "lib"
  154. assert "" / "/lib" == "/lib"
  155. assert "usr/" / "/lib/" == "usr/lib/"
  156. assert "usr" / "lib" / "../bin" == "usr/bin"
  157. result = joinPath(head, tail)
  158. when doslikeFileSystem:
  159. import std/private/ntpath
  160. proc splitPath*(path: string): tuple[head, tail: string] {.
  161. noSideEffect, rtl, extern: "nos$1".} =
  162. ## Splits a directory into `(head, tail)` tuple, so that
  163. ## ``head / tail == path`` (except for edge cases like "/usr").
  164. ##
  165. ## See also:
  166. ## * `joinPath(head, tail) proc`_
  167. ## * `joinPath(parts: varargs[string]) proc`_
  168. ## * `/ proc`_
  169. ## * `/../ proc`_
  170. ## * `relativePath proc`_
  171. runnableExamples:
  172. assert splitPath("usr/local/bin") == ("usr/local", "bin")
  173. assert splitPath("usr/local/bin/") == ("usr/local/bin", "")
  174. assert splitPath("/bin/") == ("/bin", "")
  175. when (NimMajor, NimMinor) <= (1, 0):
  176. assert splitPath("/bin") == ("", "bin")
  177. else:
  178. assert splitPath("/bin") == ("/", "bin")
  179. assert splitPath("bin") == ("", "bin")
  180. assert splitPath("") == ("", "")
  181. when doslikeFileSystem:
  182. let (drive, splitpath) = splitDrive(path)
  183. let stop = drive.len
  184. else:
  185. const stop = 0
  186. var sepPos = -1
  187. for i in countdown(len(path)-1, stop):
  188. if path[i] in {DirSep, AltSep}:
  189. sepPos = i
  190. break
  191. if sepPos >= 0:
  192. result.head = substr(path, 0,
  193. when (NimMajor, NimMinor) <= (1, 0):
  194. sepPos-1
  195. else:
  196. if likely(sepPos >= 1): sepPos-1 else: 0
  197. )
  198. result.tail = substr(path, sepPos+1)
  199. else:
  200. when doslikeFileSystem:
  201. result.head = drive
  202. result.tail = splitpath
  203. else:
  204. result.head = ""
  205. result.tail = path
  206. proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1", raises: [].} =
  207. ## Checks whether a given `path` is absolute.
  208. ##
  209. ## On Windows, network paths are considered absolute too.
  210. runnableExamples:
  211. assert not "".isAbsolute
  212. assert not ".".isAbsolute
  213. when defined(posix):
  214. assert "/".isAbsolute
  215. assert not "a/".isAbsolute
  216. assert "/a/".isAbsolute
  217. if len(path) == 0: return false
  218. when doslikeFileSystem:
  219. var len = len(path)
  220. result = (path[0] in {'/', '\\'}) or
  221. (len > 1 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':')
  222. elif defined(macos):
  223. # according to https://perldoc.perl.org/File/Spec/Mac.html `:a` is a relative path
  224. result = path[0] != ':'
  225. elif defined(RISCOS):
  226. result = path[0] == '$'
  227. elif defined(posix):
  228. result = path[0] == '/'
  229. elif defined(nodejs):
  230. {.emit: [result," = require(\"path\").isAbsolute(",path.cstring,");"].}
  231. else:
  232. raiseAssert "unreachable" # if ever hits here, adapt as needed
  233. when FileSystemCaseSensitive:
  234. template `!=?`(a, b: char): bool = a != b
  235. else:
  236. template `!=?`(a, b: char): bool = toLowerAscii(a) != toLowerAscii(b)
  237. when doslikeFileSystem:
  238. proc isAbsFromCurrentDrive(path: string): bool {.noSideEffect, raises: [].} =
  239. ## An absolute path from the root of the current drive (e.g. "\foo")
  240. path.len > 0 and
  241. (path[0] == AltSep or
  242. (path[0] == DirSep and
  243. (path.len == 1 or path[1] notin {DirSep, AltSep, ':'})))
  244. proc sameRoot(path1, path2: string): bool {.noSideEffect, raises: [].} =
  245. ## Return true if path1 and path2 have a same root.
  246. ##
  247. ## Detail of Windows path formats:
  248. ## https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats
  249. assert(isAbsolute(path1))
  250. assert(isAbsolute(path2))
  251. if isAbsFromCurrentDrive(path1) and isAbsFromCurrentDrive(path2):
  252. result = true
  253. elif cmpIgnoreCase(splitDrive(path1).drive, splitDrive(path2).drive) == 0:
  254. result = true
  255. else:
  256. result = false
  257. proc relativePath*(path, base: string, sep = DirSep): string {.
  258. rtl, extern: "nos$1".} =
  259. ## Converts `path` to a path relative to `base`.
  260. ##
  261. ## The `sep` (default: DirSep_) is used for the path normalizations,
  262. ## this can be useful to ensure the relative path only contains `'/'`
  263. ## so that it can be used for URL constructions.
  264. ##
  265. ## On Windows, if a root of `path` and a root of `base` are different,
  266. ## returns `path` as is because it is impossible to make a relative path.
  267. ## That means an absolute path can be returned.
  268. ##
  269. ## See also:
  270. ## * `splitPath proc`_
  271. ## * `parentDir proc`_
  272. ## * `tailDir proc`_
  273. runnableExamples:
  274. assert relativePath("/Users/me/bar/z.nim", "/Users/other/bad", '/') == "../../me/bar/z.nim"
  275. assert relativePath("/Users/me/bar/z.nim", "/Users/other", '/') == "../me/bar/z.nim"
  276. when not doslikeFileSystem: # On Windows, UNC-paths start with `//`
  277. assert relativePath("/Users///me/bar//z.nim", "//Users/", '/') == "me/bar/z.nim"
  278. assert relativePath("/Users/me/bar/z.nim", "/Users/me", '/') == "bar/z.nim"
  279. assert relativePath("", "/users/moo", '/') == ""
  280. assert relativePath("foo", ".", '/') == "foo"
  281. assert relativePath("foo", "foo", '/') == "."
  282. if path.len == 0: return ""
  283. var base = if base == ".": "" else: base
  284. var path = path
  285. path.normalizePathAux
  286. base.normalizePathAux
  287. let a1 = isAbsolute(path)
  288. let a2 = isAbsolute(base)
  289. if a1 and not a2:
  290. base = absolutePathInternal(base)
  291. elif a2 and not a1:
  292. path = absolutePathInternal(path)
  293. when doslikeFileSystem:
  294. if isAbsolute(path) and isAbsolute(base):
  295. if not sameRoot(path, base):
  296. return path
  297. var f = default PathIter
  298. var b = default PathIter
  299. var ff = (0, -1)
  300. var bb = (0, -1) # (int, int)
  301. result = newStringOfCap(path.len)
  302. # skip the common prefix:
  303. while f.hasNext(path) and b.hasNext(base):
  304. ff = next(f, path)
  305. bb = next(b, base)
  306. let diff = ff[1] - ff[0]
  307. if diff != bb[1] - bb[0]: break
  308. var same = true
  309. for i in 0..diff:
  310. if path[i + ff[0]] !=? base[i + bb[0]]:
  311. same = false
  312. break
  313. if not same: break
  314. ff = (0, -1)
  315. bb = (0, -1)
  316. # for i in 0..diff:
  317. # result.add base[i + bb[0]]
  318. # /foo/bar/xxx/ -- base
  319. # /foo/bar/baz -- path path
  320. # ../baz
  321. # every directory that is in 'base', needs to add '..'
  322. while true:
  323. if bb[1] >= bb[0]:
  324. if result.len > 0 and result[^1] != sep:
  325. result.add sep
  326. result.add ".."
  327. if not b.hasNext(base): break
  328. bb = b.next(base)
  329. # add the rest of 'path':
  330. while true:
  331. if ff[1] >= ff[0]:
  332. if result.len > 0 and result[^1] != sep:
  333. result.add sep
  334. for i in 0..ff[1] - ff[0]:
  335. result.add path[i + ff[0]]
  336. if not f.hasNext(path): break
  337. ff = f.next(path)
  338. when not defined(nimOldRelativePathBehavior):
  339. if result.len == 0: result.add "."
  340. proc isRelativeTo*(path: string, base: string): bool {.since: (1, 1).} =
  341. ## Returns true if `path` is relative to `base`.
  342. runnableExamples:
  343. doAssert isRelativeTo("./foo//bar", "foo")
  344. doAssert isRelativeTo("foo/bar", ".")
  345. doAssert isRelativeTo("/foo/bar.nim", "/foo/bar.nim")
  346. doAssert not isRelativeTo("foo/bar.nims", "foo/bar.nim")
  347. let path = path.normalizePath
  348. let base = base.normalizePath
  349. let ret = relativePath(path, base)
  350. result = path.len > 0 and not ret.startsWith ".."
  351. proc parentDirPos(path: string): int =
  352. var q = 1
  353. if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
  354. for i in countdown(len(path)-q, 0):
  355. if path[i] in {DirSep, AltSep}: return i
  356. result = -1
  357. proc parentDir*(path: string): string {.
  358. noSideEffect, rtl, extern: "nos$1".} =
  359. ## Returns the parent directory of `path`.
  360. ##
  361. ## This is similar to ``splitPath(path).head`` when ``path`` doesn't end
  362. ## in a dir separator, but also takes care of path normalizations.
  363. ## The remainder can be obtained with `lastPathPart(path) proc`_.
  364. ##
  365. ## See also:
  366. ## * `relativePath proc`_
  367. ## * `splitPath proc`_
  368. ## * `tailDir proc`_
  369. ## * `parentDirs iterator`_
  370. runnableExamples:
  371. assert parentDir("") == ""
  372. when defined(posix):
  373. assert parentDir("/usr/local/bin") == "/usr/local"
  374. assert parentDir("foo/bar//") == "foo"
  375. assert parentDir("//foo//bar//.") == "/foo"
  376. assert parentDir("./foo") == "."
  377. assert parentDir("/./foo//./") == "/"
  378. assert parentDir("a//./") == "."
  379. assert parentDir("a/b/c/..") == "a"
  380. result = pathnorm.normalizePath(path)
  381. when doslikeFileSystem:
  382. let (drive, splitpath) = splitDrive(result)
  383. result = splitpath
  384. var sepPos = parentDirPos(result)
  385. if sepPos >= 0:
  386. result = substr(result, 0, sepPos)
  387. normalizePathEnd(result)
  388. elif result == ".." or result == "." or result.len == 0 or result[^1] in {DirSep, AltSep}:
  389. # `.` => `..` and .. => `../..`(etc) would be a sensible alternative
  390. # `/` => `/` (as done with splitFile) would be a sensible alternative
  391. result = ""
  392. else:
  393. result = "."
  394. when doslikeFileSystem:
  395. if result.len == 0:
  396. discard
  397. elif drive.len > 0 and result.len == 1 and result[0] in {DirSep, AltSep}:
  398. result = drive
  399. else:
  400. result = drive & result
  401. proc tailDir*(path: string): string {.
  402. noSideEffect, rtl, extern: "nos$1".} =
  403. ## Returns the tail part of `path`.
  404. ##
  405. ## See also:
  406. ## * `relativePath proc`_
  407. ## * `splitPath proc`_
  408. ## * `parentDir proc`_
  409. runnableExamples:
  410. assert tailDir("/bin") == "bin"
  411. assert tailDir("bin") == ""
  412. assert tailDir("bin/") == ""
  413. assert tailDir("/usr/local/bin") == "usr/local/bin"
  414. assert tailDir("//usr//local//bin//") == "usr//local//bin//"
  415. assert tailDir("./usr/local/bin") == "usr/local/bin"
  416. assert tailDir("usr/local/bin") == "local/bin"
  417. var i = 0
  418. when doslikeFileSystem:
  419. let (drive, splitpath) = path.splitDrive
  420. if drive != "":
  421. return splitpath.strip(chars = {DirSep, AltSep}, trailing = false)
  422. while i < len(path):
  423. if path[i] in {DirSep, AltSep}:
  424. while i < len(path) and path[i] in {DirSep, AltSep}: inc i
  425. return substr(path, i)
  426. inc i
  427. result = ""
  428. proc isRootDir*(path: string): bool {.
  429. noSideEffect, rtl, extern: "nos$1".} =
  430. ## Checks whether a given `path` is a root directory.
  431. runnableExamples:
  432. assert isRootDir("")
  433. assert isRootDir(".")
  434. assert isRootDir("/")
  435. assert isRootDir("a")
  436. assert not isRootDir("/a")
  437. assert not isRootDir("a/b/c")
  438. when doslikeFileSystem:
  439. if splitDrive(path).path == "":
  440. return true
  441. result = parentDirPos(path) < 0
  442. iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
  443. ## Walks over all parent directories of a given `path`.
  444. ##
  445. ## If `fromRoot` is true (default: false), the traversal will start from
  446. ## the file system root directory.
  447. ## If `inclusive` is true (default), the original argument will be included
  448. ## in the traversal.
  449. ##
  450. ## Relative paths won't be expanded by this iterator. Instead, it will traverse
  451. ## only the directories appearing in the relative path.
  452. ##
  453. ## See also:
  454. ## * `parentDir proc`_
  455. ##
  456. runnableExamples:
  457. let g = "a/b/c"
  458. for p in g.parentDirs:
  459. echo p
  460. # a/b/c
  461. # a/b
  462. # a
  463. for p in g.parentDirs(fromRoot=true):
  464. echo p
  465. # a/
  466. # a/b/
  467. # a/b/c
  468. for p in g.parentDirs(inclusive=false):
  469. echo p
  470. # a/b
  471. # a
  472. if not fromRoot:
  473. var current = path
  474. if inclusive: yield path
  475. while true:
  476. if current.isRootDir: break
  477. current = current.parentDir
  478. yield current
  479. else:
  480. when doslikeFileSystem:
  481. let start = path.splitDrive.drive.len
  482. else:
  483. const start = 0
  484. for i in countup(start, path.len - 2): # ignore the last /
  485. # deal with non-normalized paths such as /foo//bar//baz
  486. if path[i] in {DirSep, AltSep} and
  487. (i == 0 or path[i-1] notin {DirSep, AltSep}):
  488. yield path.substr(0, i)
  489. if inclusive: yield path
  490. proc `/../`*(head, tail: string): string {.noSideEffect.} =
  491. ## The same as ``parentDir(head) / tail``, unless there is no parent
  492. ## directory. Then ``head / tail`` is performed instead.
  493. ##
  494. ## See also:
  495. ## * `/ proc`_
  496. ## * `parentDir proc`_
  497. runnableExamples:
  498. when defined(posix):
  499. assert "a/b/c" /../ "d/e" == "a/b/d/e"
  500. assert "a" /../ "d/e" == "a/d/e"
  501. when doslikeFileSystem:
  502. let (drive, head) = splitDrive(head)
  503. let sepPos = parentDirPos(head)
  504. if sepPos >= 0:
  505. result = substr(head, 0, sepPos-1) / tail
  506. else:
  507. result = head / tail
  508. when doslikeFileSystem:
  509. result = drive / result
  510. proc normExt(ext: string): string =
  511. if ext == "" or ext[0] == ExtSep: result = ext # no copy needed here
  512. else: result = ExtSep & ext
  513. proc searchExtPos*(path: string): int =
  514. ## Returns index of the `'.'` char in `path` if it signifies the beginning
  515. ## of the file extension. Returns -1 otherwise.
  516. ##
  517. ## See also:
  518. ## * `splitFile proc`_
  519. ## * `extractFilename proc`_
  520. ## * `lastPathPart proc`_
  521. ## * `changeFileExt proc`_
  522. ## * `addFileExt proc`_
  523. runnableExamples:
  524. assert searchExtPos("a/b/c") == -1
  525. assert searchExtPos("c.nim") == 1
  526. assert searchExtPos("a/b/c.nim") == 5
  527. assert searchExtPos("a.b.c.nim") == 5
  528. assert searchExtPos(".nim") == -1
  529. assert searchExtPos("..nim") == -1
  530. assert searchExtPos("a..nim") == 2
  531. # Unless there is any char that is not `ExtSep` before last `ExtSep` in the file name,
  532. # it is not a file extension.
  533. const DirSeps = when doslikeFileSystem: {DirSep, AltSep, ':'} else: {DirSep, AltSep}
  534. result = -1
  535. var i = path.high
  536. while i >= 1:
  537. if path[i] == ExtSep:
  538. break
  539. elif path[i] in DirSeps:
  540. return -1 # do not skip over path
  541. dec i
  542. for j in countdown(i - 1, 0):
  543. if path[j] in DirSeps:
  544. return -1
  545. elif path[j] != ExtSep:
  546. result = i
  547. break
  548. proc splitFile*(path: string): tuple[dir, name, ext: string] {.
  549. noSideEffect, rtl, extern: "nos$1".} =
  550. ## Splits a filename into `(dir, name, extension)` tuple.
  551. ##
  552. ## `dir` does not end in DirSep_ unless it's `/`.
  553. ## `extension` includes the leading dot.
  554. ##
  555. ## If `path` has no extension, `ext` is the empty string.
  556. ## If `path` has no directory component, `dir` is the empty string.
  557. ## If `path` has no filename component, `name` and `ext` are empty strings.
  558. ##
  559. ## See also:
  560. ## * `searchExtPos proc`_
  561. ## * `extractFilename proc`_
  562. ## * `lastPathPart proc`_
  563. ## * `changeFileExt proc`_
  564. ## * `addFileExt proc`_
  565. runnableExamples:
  566. var (dir, name, ext) = splitFile("usr/local/nimc.html")
  567. assert dir == "usr/local"
  568. assert name == "nimc"
  569. assert ext == ".html"
  570. (dir, name, ext) = splitFile("/usr/local/os")
  571. assert dir == "/usr/local"
  572. assert name == "os"
  573. assert ext == ""
  574. (dir, name, ext) = splitFile("/usr/local/")
  575. assert dir == "/usr/local"
  576. assert name == ""
  577. assert ext == ""
  578. (dir, name, ext) = splitFile("/tmp.txt")
  579. assert dir == "/"
  580. assert name == "tmp"
  581. assert ext == ".txt"
  582. var namePos = 0
  583. var dotPos = 0
  584. when doslikeFileSystem:
  585. let (drive, _) = splitDrive(path)
  586. let stop = len(drive)
  587. result.dir = drive
  588. else:
  589. const stop = 0
  590. for i in countdown(len(path) - 1, stop):
  591. if path[i] in {DirSep, AltSep} or i == 0:
  592. if path[i] in {DirSep, AltSep}:
  593. result.dir = substr(path, 0, if likely(i >= 1): i - 1 else: 0)
  594. namePos = i + 1
  595. if dotPos > i:
  596. result.name = substr(path, namePos, dotPos - 1)
  597. result.ext = substr(path, dotPos)
  598. else:
  599. result.name = substr(path, namePos)
  600. break
  601. elif path[i] == ExtSep and i > 0 and i < len(path) - 1 and
  602. path[i - 1] notin {DirSep, AltSep} and
  603. path[i + 1] != ExtSep and dotPos == 0:
  604. dotPos = i
  605. proc extractFilename*(path: string): string {.
  606. noSideEffect, rtl, extern: "nos$1".} =
  607. ## Extracts the filename of a given `path`.
  608. ##
  609. ## This is the same as ``name & ext`` from `splitFile(path) proc`_.
  610. ##
  611. ## See also:
  612. ## * `searchExtPos proc`_
  613. ## * `splitFile proc`_
  614. ## * `lastPathPart proc`_
  615. ## * `changeFileExt proc`_
  616. ## * `addFileExt proc`_
  617. runnableExamples:
  618. assert extractFilename("foo/bar/") == ""
  619. assert extractFilename("foo/bar") == "bar"
  620. assert extractFilename("foo/bar.baz") == "bar.baz"
  621. if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
  622. result = ""
  623. else:
  624. result = splitPath(path).tail
  625. proc lastPathPart*(path: string): string {.
  626. noSideEffect, rtl, extern: "nos$1".} =
  627. ## Like `extractFilename proc`_, but ignores
  628. ## trailing dir separator; aka: `baseName`:idx: in some other languages.
  629. ##
  630. ## See also:
  631. ## * `searchExtPos proc`_
  632. ## * `splitFile proc`_
  633. ## * `extractFilename proc`_
  634. ## * `changeFileExt proc`_
  635. ## * `addFileExt proc`_
  636. runnableExamples:
  637. assert lastPathPart("foo/bar/") == "bar"
  638. assert lastPathPart("foo/bar") == "bar"
  639. let path = path.normalizePathEnd(trailingSep = false)
  640. result = extractFilename(path)
  641. proc changeFileExt*(filename, ext: string): string {.
  642. noSideEffect, rtl, extern: "nos$1".} =
  643. ## Changes the file extension to `ext`.
  644. ##
  645. ## If the `filename` has no extension, `ext` will be added.
  646. ## If `ext` == "" then any extension is removed.
  647. ##
  648. ## `Ext` should be given without the leading `'.'`, because some
  649. ## filesystems may use a different character. (Although I know
  650. ## of none such beast.)
  651. ##
  652. ## See also:
  653. ## * `searchExtPos proc`_
  654. ## * `splitFile proc`_
  655. ## * `extractFilename proc`_
  656. ## * `lastPathPart proc`_
  657. ## * `addFileExt proc`_
  658. runnableExamples:
  659. assert changeFileExt("foo.bar", "baz") == "foo.baz"
  660. assert changeFileExt("foo.bar", "") == "foo"
  661. assert changeFileExt("foo", "baz") == "foo.baz"
  662. var extPos = searchExtPos(filename)
  663. if extPos < 0: result = filename & normExt(ext)
  664. else: result = substr(filename, 0, extPos-1) & normExt(ext)
  665. proc addFileExt*(filename, ext: string): string {.
  666. noSideEffect, rtl, extern: "nos$1".} =
  667. ## Adds the file extension `ext` to `filename`, unless
  668. ## `filename` already has an extension.
  669. ##
  670. ## `Ext` should be given without the leading `'.'`, because some
  671. ## filesystems may use a different character.
  672. ## (Although I know of none such beast.)
  673. ##
  674. ## See also:
  675. ## * `searchExtPos proc`_
  676. ## * `splitFile proc`_
  677. ## * `extractFilename proc`_
  678. ## * `lastPathPart proc`_
  679. ## * `changeFileExt proc`_
  680. runnableExamples:
  681. assert addFileExt("foo.bar", "baz") == "foo.bar"
  682. assert addFileExt("foo.bar", "") == "foo.bar"
  683. assert addFileExt("foo", "baz") == "foo.baz"
  684. var extPos = searchExtPos(filename)
  685. if extPos < 0: result = filename & normExt(ext)
  686. else: result = filename
  687. proc cmpPaths*(pathA, pathB: string): int {.
  688. noSideEffect, rtl, extern: "nos$1".} =
  689. ## Compares two paths.
  690. ##
  691. ## On a case-sensitive filesystem this is done
  692. ## case-sensitively otherwise case-insensitively. Returns:
  693. ##
  694. ## | `0` if pathA == pathB
  695. ## | `< 0` if pathA < pathB
  696. ## | `> 0` if pathA > pathB
  697. runnableExamples:
  698. when defined(macosx):
  699. assert cmpPaths("foo", "Foo") == 0
  700. elif defined(posix):
  701. assert cmpPaths("foo", "Foo") > 0
  702. let a = normalizePath(pathA)
  703. let b = normalizePath(pathB)
  704. if FileSystemCaseSensitive:
  705. result = cmp(a, b)
  706. else:
  707. when defined(nimscript):
  708. result = cmpic(a, b)
  709. elif defined(nimdoc): discard
  710. else:
  711. result = cmpIgnoreCase(a, b)
  712. proc unixToNativePath*(path: string, drive=""): string {.
  713. noSideEffect, rtl, extern: "nos$1".} =
  714. ## Converts an UNIX-like path to a native one.
  715. ##
  716. ## On an UNIX system this does nothing. Else it converts
  717. ## `'/'`, `'.'`, `'..'` to the appropriate things.
  718. ##
  719. ## On systems with a concept of "drives", `drive` is used to determine
  720. ## which drive label to use during absolute path conversion.
  721. ## `drive` defaults to the drive of the current working directory, and is
  722. ## ignored on systems that do not have a concept of "drives".
  723. when defined(unix):
  724. result = path
  725. else:
  726. if path.len == 0: return ""
  727. var start: int
  728. if path[0] == '/':
  729. # an absolute path
  730. when doslikeFileSystem:
  731. if drive != "":
  732. result = drive & ":" & DirSep
  733. else:
  734. result = $DirSep
  735. elif defined(macos):
  736. result = "" # must not start with ':'
  737. else:
  738. result = $DirSep
  739. start = 1
  740. elif path[0] == '.' and (path.len == 1 or path[1] == '/'):
  741. # current directory
  742. result = $CurDir
  743. start = when doslikeFileSystem: 1 else: 2
  744. else:
  745. result = ""
  746. start = 0
  747. var i = start
  748. while i < len(path): # ../../../ --> ::::
  749. if i+2 < path.len and path[i] == '.' and path[i+1] == '.' and path[i+2] == '/':
  750. # parent directory
  751. when defined(macos):
  752. if result[high(result)] == ':':
  753. add result, ':'
  754. else:
  755. add result, ParDir
  756. else:
  757. add result, ParDir & DirSep
  758. inc(i, 3)
  759. elif path[i] == '/':
  760. add result, DirSep
  761. inc(i)
  762. else:
  763. add result, path[i]
  764. inc(i)
  765. when not defined(nimscript):
  766. proc getCurrentDir*(): string {.rtl, extern: "nos$1", tags: [].} =
  767. ## Returns the `current working directory`:idx: i.e. where the built
  768. ## binary is run.
  769. ##
  770. ## So the path returned by this proc is determined at run time.
  771. ##
  772. ## See also:
  773. ## * `getHomeDir proc`_
  774. ## * `getConfigDir proc`_
  775. ## * `getTempDir proc`_
  776. ## * `setCurrentDir proc`_
  777. ## * `currentSourcePath template <system.html#currentSourcePath.t>`_
  778. ## * `getProjectPath proc <macros.html#getProjectPath>`_
  779. when defined(nodejs):
  780. var ret: cstring
  781. {.emit: "`ret` = process.cwd();".}
  782. return $ret
  783. elif defined(js):
  784. raiseAssert "use -d:nodejs to have `getCurrentDir` defined"
  785. elif defined(windows):
  786. var bufsize = MAX_PATH.int32
  787. var res = newWideCString(bufsize)
  788. while true:
  789. var L = getCurrentDirectoryW(bufsize, res)
  790. if L == 0'i32:
  791. raiseOSError(osLastError())
  792. elif L > bufsize:
  793. res = newWideCString(L)
  794. bufsize = L
  795. else:
  796. result = res$L
  797. break
  798. else:
  799. var bufsize = 1024 # should be enough
  800. result = newString(bufsize)
  801. while true:
  802. if getcwd(result.cstring, bufsize) != nil:
  803. setLen(result, c_strlen(result.cstring))
  804. break
  805. else:
  806. let err = osLastError()
  807. if err.int32 == ERANGE:
  808. bufsize = bufsize shl 1
  809. doAssert(bufsize >= 0)
  810. result = newString(bufsize)
  811. else:
  812. raiseOSError(osLastError())
  813. proc absolutePath*(path: string, root = getCurrentDir()): string =
  814. ## Returns the absolute path of `path`, rooted at `root` (which must be absolute;
  815. ## default: current directory).
  816. ## If `path` is absolute, return it, ignoring `root`.
  817. ##
  818. ## See also:
  819. ## * `normalizedPath proc`_
  820. ## * `normalizePath proc`_
  821. runnableExamples:
  822. assert absolutePath("a") == getCurrentDir() / "a"
  823. if isAbsolute(path): path
  824. else:
  825. if not root.isAbsolute:
  826. raise newException(ValueError, "The specified root is not absolute: " & root)
  827. joinPath(root, path)
  828. proc absolutePathInternal(path: string): string =
  829. absolutePath(path, getCurrentDir())
  830. proc normalizePath*(path: var string) {.rtl, extern: "nos$1", tags: [].} =
  831. ## Normalize a path.
  832. ##
  833. ## Consecutive directory separators are collapsed, including an initial double slash.
  834. ##
  835. ## On relative paths, double dot (`..`) sequences are collapsed if possible.
  836. ## On absolute paths they are always collapsed.
  837. ##
  838. ## .. warning:: URL-encoded and Unicode attempts at directory traversal are not detected.
  839. ## Triple dot is not handled.
  840. ##
  841. ## See also:
  842. ## * `absolutePath proc`_
  843. ## * `normalizedPath proc`_ for outplace version
  844. ## * `normalizeExe proc`_
  845. runnableExamples:
  846. when defined(posix):
  847. var a = "a///b//..//c///d"
  848. a.normalizePath()
  849. assert a == "a/c/d"
  850. path = pathnorm.normalizePath(path)
  851. when false:
  852. let isAbs = isAbsolute(path)
  853. var stack: seq[string] = @[]
  854. for p in split(path, {DirSep}):
  855. case p
  856. of "", ".":
  857. continue
  858. of "..":
  859. if stack.len == 0:
  860. if isAbs:
  861. discard # collapse all double dots on absoluta paths
  862. else:
  863. stack.add(p)
  864. elif stack[^1] == "..":
  865. stack.add(p)
  866. else:
  867. discard stack.pop()
  868. else:
  869. stack.add(p)
  870. if isAbs:
  871. path = DirSep & join(stack, $DirSep)
  872. elif stack.len > 0:
  873. path = join(stack, $DirSep)
  874. else:
  875. path = "."
  876. proc normalizePathAux(path: var string) = normalizePath(path)
  877. proc normalizedPath*(path: string): string {.rtl, extern: "nos$1", tags: [].} =
  878. ## Returns a normalized path for the current OS.
  879. ##
  880. ## See also:
  881. ## * `absolutePath proc`_
  882. ## * `normalizePath proc`_ for the in-place version
  883. runnableExamples:
  884. when defined(posix):
  885. assert normalizedPath("a///b//..//c///d") == "a/c/d"
  886. result = pathnorm.normalizePath(path)
  887. proc normalizeExe*(file: var string) {.since: (1, 3, 5).} =
  888. ## on posix, prepends `./` if `file` doesn't contain `/` and is not `"", ".", ".."`.
  889. runnableExamples:
  890. import std/sugar
  891. when defined(posix):
  892. doAssert "foo".dup(normalizeExe) == "./foo"
  893. doAssert "foo/../bar".dup(normalizeExe) == "foo/../bar"
  894. doAssert "".dup(normalizeExe) == ""
  895. when defined(posix):
  896. if file.len > 0 and DirSep notin file and file != "." and file != "..":
  897. file = "./" & file
  898. proc sameFile*(path1, path2: string): bool {.rtl, extern: "nos$1",
  899. tags: [ReadDirEffect], noWeirdTarget.} =
  900. ## Returns true if both pathname arguments refer to the same physical
  901. ## file or directory.
  902. ##
  903. ## Raises `OSError` if any of the files does not
  904. ## exist or information about it can not be obtained.
  905. ##
  906. ## This proc will return true if given two alternative hard-linked or
  907. ## sym-linked paths to the same file or directory.
  908. ##
  909. ## See also:
  910. ## * `sameFileContent proc`_
  911. when defined(windows):
  912. var success = true
  913. var f1 = openHandle(path1)
  914. var f2 = openHandle(path2)
  915. var lastErr: OSErrorCode
  916. if f1 != INVALID_HANDLE_VALUE and f2 != INVALID_HANDLE_VALUE:
  917. var fi1, fi2: BY_HANDLE_FILE_INFORMATION
  918. if getFileInformationByHandle(f1, addr(fi1)) != 0 and
  919. getFileInformationByHandle(f2, addr(fi2)) != 0:
  920. result = fi1.dwVolumeSerialNumber == fi2.dwVolumeSerialNumber and
  921. fi1.nFileIndexHigh == fi2.nFileIndexHigh and
  922. fi1.nFileIndexLow == fi2.nFileIndexLow
  923. else:
  924. lastErr = osLastError()
  925. success = false
  926. else:
  927. lastErr = osLastError()
  928. success = false
  929. discard closeHandle(f1)
  930. discard closeHandle(f2)
  931. if not success: raiseOSError(lastErr, $(path1, path2))
  932. else:
  933. var a, b: Stat
  934. if stat(path1, a) < 0'i32 or stat(path2, b) < 0'i32:
  935. raiseOSError(osLastError(), $(path1, path2))
  936. else:
  937. result = a.st_dev == b.st_dev and a.st_ino == b.st_ino