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