summary refs log tree commit diff stats
path: root/lib/pure/ospaths.nim
blob: 56671ee6247aaf5042b57f10882ff09aabee1a23 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
#
#
#            Nim's Runtime Library
#        (c) Copyright 2015 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

# Included by the ``os`` module but a module in its own right for NimScript
# support.

when not declared(os):
  {.pragma: rtl.}
  import strutils

when defined(nimscript) or (defined(nimdoc) and not declared(os)):
  {.pragma: rtl.}
  {.push hint[ConvFromXtoItselfNotNeeded]:off.}

when not declared(getEnv) or defined(nimscript):
  type
    ReadEnvEffect* = object of ReadIOEffect   ## effect that denotes a read
                                              ## from an environment variable
    WriteEnvEffect* = object of WriteIOEffect ## effect that denotes a write
                                              ## to an environment variable

    ReadDirEffect* = object of ReadIOEffect   ## effect that denotes a write
                                              ## operation to the directory
                                              ## structure
    WriteDirEffect* = object of WriteIOEffect ## effect that denotes a write
                                              ## operation to
                                              ## the directory structure

    OSErrorCode* = distinct int32 ## Specifies an OS Error Code.

  {.deprecated: [FReadEnv: ReadEnvEffect, FWriteEnv: WriteEnvEffect,
      FReadDir: ReadDirEffect,
      FWriteDir: WriteDirEffect,
      TOSErrorCode: OSErrorCode
  ].}
  const
    doslike = defined(windows) or defined(OS2) or defined(DOS)
      # DOS-like filesystem

  when defined(Nimdoc): # only for proper documentation:
    const
      CurDir* = '.'
        ## The constant string used by the operating system to refer to the
        ## current directory.
        ##
        ## For example: '.' for POSIX or ':' for the classic Macintosh.

      ParDir* = ".."
        ## The constant string used by the operating system to refer to the
        ## parent directory.
        ##
        ## For example: ".." for POSIX or "::" for the classic Macintosh.

      DirSep* = '/'
        ## The character used by the operating system to separate pathname
        ## components, for example, '/' for POSIX or ':' for the classic
        ## Macintosh.

      AltSep* = '/'
        ## An alternative character used by the operating system to separate
        ## pathname components, or the same as `DirSep` if only one separator
        ## character exists. This is set to '/' on Windows systems
        ## where `DirSep` is a backslash.

      PathSep* = ':'
        ## The character conventionally used by the operating system to separate
        ## search patch components (as in PATH), such as ':' for POSIX
        ## or ';' for Windows.

      FileSystemCaseSensitive* = true
        ## true if the file system is case sensitive, false otherwise. Used by
        ## `cmpPaths` to compare filenames properly.

      ExeExt* = ""
        ## The file extension of native executables. For example:
        ## "" for POSIX, "exe" on Windows.

      ScriptExt* = ""
        ## The file extension of a script file. For example: "" for POSIX,
        ## "bat" on Windows.

      DynlibFormat* = "lib$1.so"
        ## The format string to turn a filename into a `DLL`:idx: file (also
        ## called `shared object`:idx: on some operating systems).

  elif defined(macos):
    const
      CurDir* = ':'
      ParDir* = "::"
      DirSep* = ':'
      AltSep* = Dirsep
      PathSep* = ','
      FileSystemCaseSensitive* = false
      ExeExt* = ""
      ScriptExt* = ""
      DynlibFormat* = "$1.dylib"

    #  MacOS paths
    #  ===========
    #  MacOS directory separator is a colon ":" which is the only character not
    #  allowed in filenames.
    #
    #  A path containing no colon or which begins with a colon is a partial
    #  path.
    #  E.g. ":kalle:petter" ":kalle" "kalle"
    #
    #  All other paths are full (absolute) paths. E.g. "HD:kalle:" "HD:"
    #  When generating paths, one is safe if one ensures that all partial paths
    #  begin with a colon, and all full paths end with a colon.
    #  In full paths the first name (e g HD above) is the name of a mounted
    #  volume.
    #  These names are not unique, because, for instance, two diskettes with the
    #  same names could be inserted. This means that paths on MacOS are not
    #  waterproof. In case of equal names the first volume found will do.
    #  Two colons "::" are the relative path to the parent. Three is to the
    #  grandparent etc.
  elif doslike:
    const
      CurDir* = '.'
      ParDir* = ".."
      DirSep* = '\\' # seperator within paths
      AltSep* = '/'
      PathSep* = ';' # seperator between paths
      FileSystemCaseSensitive* = false
      ExeExt* = "exe"
      ScriptExt* = "bat"
      DynlibFormat* = "$1.dll"
  elif defined(PalmOS) or defined(MorphOS):
    const
      DirSep* = '/'
      AltSep* = Dirsep
      PathSep* = ';'
      ParDir* = ".."
      FileSystemCaseSensitive* = false
      ExeExt* = ""
      ScriptExt* = ""
      DynlibFormat* = "$1.prc"
  elif defined(RISCOS):
    const
      DirSep* = '.'
      AltSep* = '.'
      ParDir* = ".." # is this correct?
      PathSep* = ','
      FileSystemCaseSensitive* = true
      ExeExt* = ""
      ScriptExt* = ""
      DynlibFormat* = "lib$1.so"
  else: # UNIX-like operating system
    const
      CurDir* = '.'
      ParDir* = ".."
      DirSep* = '/'
      AltSep* = DirSep
      PathSep* = ':'
      FileSystemCaseSensitive* = true
      ExeExt* = ""
      ScriptExt* = ""
      DynlibFormat* = when defined(macosx): "lib$1.dylib" else: "lib$1.so"

  const
    ExtSep* = '.'
      ## The character which separates the base filename from the extension;
      ## for example, the '.' in ``os.nim``.


  proc joinPath*(head, tail: string): string {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Joins two directory names to one.
    ##
    ## For example on Unix:
    ##
    ## .. code-block:: nim
    ##   joinPath("usr", "lib")
    ##
    ## results in:
    ##
    ## .. code-block:: nim
    ##   "usr/lib"
    ##
    ## If head is the empty string, tail is returned. If tail is the empty
    ## string, head is returned with a trailing path separator. If tail starts
    ## with a path separator it will be removed when concatenated to head. Other
    ## path separators not located on boundaries won't be modified. More
    ## examples on Unix:
    ##
    ## .. code-block:: nim
    ##   assert joinPath("usr", "") == "usr/"
    ##   assert joinPath("", "lib") == "lib"
    ##   assert joinPath("", "/lib") == "/lib"
    ##   assert joinPath("usr/", "/lib") == "usr/lib"
    if len(head) == 0:
      result = tail
    elif head[len(head)-1] in {DirSep, AltSep}:
      if tail[0] in {DirSep, AltSep}:
        result = head & substr(tail, 1)
      else:
        result = head & tail
    else:
      if tail[0] in {DirSep, AltSep}:
        result = head & tail
      else:
        result = head & DirSep & tail

  proc joinPath*(parts: varargs[string]): string {.noSideEffect,
    rtl, extern: "nos$1OpenArray".} =
    ## The same as `joinPath(head, tail)`, but works with any number of
    ## directory parts. You need to pass at least one element or the proc
    ## will assert in debug builds and crash on release builds.
    result = parts[0]
    for i in 1..high(parts):
      result = joinPath(result, parts[i])

  proc `/` * (head, tail: string): string {.noSideEffect.} =
    ## The same as ``joinPath(head, tail)``
    ##
    ## Here are some examples for Unix:
    ##
    ## .. code-block:: nim
    ##   assert "usr" / "" == "usr/"
    ##   assert "" / "lib" == "lib"
    ##   assert "" / "/lib" == "/lib"
    ##   assert "usr/" / "/lib" == "usr/lib"
    return joinPath(head, tail)

  proc splitPath*(path: string): tuple[head, tail: string] {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Splits a directory into (head, tail), so that
    ## ``head / tail == path`` (except for edge cases like "/usr").
    ##
    ## Examples:
    ##
    ## .. code-block:: nim
    ##   splitPath("usr/local/bin") -> ("usr/local", "bin")
    ##   splitPath("usr/local/bin/") -> ("usr/local/bin", "")
    ##   splitPath("bin") -> ("", "bin")
    ##   splitPath("/bin") -> ("", "bin")
    ##   splitPath("") -> ("", "")
    var sepPos = -1
    for i in countdown(len(path)-1, 0):
      if path[i] in {DirSep, AltSep}:
        sepPos = i
        break
    if sepPos >= 0:
      result.head = substr(path, 0, sepPos-1)
      result.tail = substr(path, sepPos+1)
    else:
      result.head = ""
      result.tail = path

  proc parentDirPos(path: string): int =
    var q = 1
    if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
    for i in countdown(len(path)-q, 0):
      if path[i] in {DirSep, AltSep}: return i
    result = -1

  proc parentDir*(path: string): string {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Returns the parent directory of `path`.
    ##
    ## This is often the same as the ``head`` result of ``splitPath``.
    ## If there is no parent, "" is returned.
    ## | Example: ``parentDir("/usr/local/bin") == "/usr/local"``.
    ## | Example: ``parentDir("/usr/local/bin/") == "/usr/local"``.
    let sepPos = parentDirPos(path)
    if sepPos >= 0:
      result = substr(path, 0, sepPos-1)
    else:
      result = ""

  proc tailDir*(path: string): string {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Returns the tail part of `path`..
    ##
    ## | Example: ``tailDir("/usr/local/bin") == "local/bin"``.
    ## | Example: ``tailDir("usr/local/bin/") == "local/bin"``.
    ## | Example: ``tailDir("bin") == ""``.
    var q = 1
    if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
    for i in 0..len(path)-q:
      if path[i] in {DirSep, AltSep}:
        return substr(path, i+1)
    result = ""

  proc isRootDir*(path: string): bool {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Checks whether a given `path` is a root directory
    result = parentDirPos(path) < 0

  iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
    ## Walks over all parent directories of a given `path`
    ##
    ## If `fromRoot` is set, the traversal will start from the file system root
    ## diretory. If `inclusive` is set, the original argument will be included
    ## in the traversal.
    ##
    ## Relative paths won't be expanded by this proc. Instead, it will traverse
    ## only the directories appearing in the relative path.
    if not fromRoot:
      var current = path
      if inclusive: yield path
      while true:
        if current.isRootDir: break
        current = current.parentDir
        yield current
    else:
      for i in countup(0, path.len - 2): # ignore the last /
        # deal with non-normalized paths such as /foo//bar//baz
        if path[i] in {DirSep, AltSep} and
            (i == 0 or path[i-1] notin {DirSep, AltSep}):
          yield path.substr(0, i)

      if inclusive: yield path

  proc `/../` * (head, tail: string): string {.noSideEffect.} =
    ## The same as ``parentDir(head) / tail`` unless there is no parent
    ## directory. Then ``head / tail`` is performed instead.
    let sepPos = parentDirPos(head)
    if sepPos >= 0:
      result = substr(head, 0, sepPos-1) / tail
    else:
      result = head / tail

  proc normExt(ext: string): string =
    if ext == "" or ext[0] == ExtSep: result = ext # no copy needed here
    else: result = ExtSep & ext

  proc searchExtPos(s: string): int =
    # BUGFIX: do not search until 0! .DS_Store is no file extension!
    result = -1
    for i in countdown(len(s)-1, 1):
      if s[i] == ExtSep:
        result = i
        break
      elif s[i] in {DirSep, AltSep}:
        break # do not skip over path

  proc splitFile*(path: string): tuple[dir, name, ext: string] {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Splits a filename into (dir, filename, extension).
    ## `dir` does not end in `DirSep`.
    ## `extension` includes the leading dot.
    ##
    ## Example:
    ##
    ## .. code-block:: nim
    ##   var (dir, name, ext) = splitFile("usr/local/nimc.html")
    ##   assert dir == "usr/local"
    ##   assert name == "nimc"
    ##   assert ext == ".html"
    ##
    ## If `path` has no extension, `ext` is the empty string.
    ## If `path` has no directory component, `dir` is the empty string.
    ## If `path` has no filename component, `name` and `ext` are empty strings.
    if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
      result = (path, "", "")
    else:
      var sepPos = -1
      var dotPos = path.len
      for i in countdown(len(path)-1, 0):
        if path[i] == ExtSep:
          if dotPos == path.len and i > 0 and
              path[i-1] notin {DirSep, AltSep}: dotPos = i
        elif path[i] in {DirSep, AltSep}:
          sepPos = i
          break
      result.dir = substr(path, 0, sepPos-1)
      result.name = substr(path, sepPos+1, dotPos-1)
      result.ext = substr(path, dotPos)

  proc extractFilename*(path: string): string {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Extracts the filename of a given `path`. This is the same as
    ## ``name & ext`` from ``splitFile(path)``.
    if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
      result = ""
    else:
      result = splitPath(path).tail


  proc changeFileExt*(filename, ext: string): string {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Changes the file extension to `ext`.
    ##
    ## If the `filename` has no extension, `ext` will be added.
    ## If `ext` == "" then any extension is removed.
    ## `Ext` should be given without the leading '.', because some
    ## filesystems may use a different character. (Although I know
    ## of none such beast.)
    var extPos = searchExtPos(filename)
    if extPos < 0: result = filename & normExt(ext)
    else: result = substr(filename, 0, extPos-1) & normExt(ext)

  proc addFileExt*(filename, ext: string): string {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Adds the file extension `ext` to `filename`, unless
    ## `filename` already has an extension.
    ##
    ## `Ext` should be given without the leading '.', because some
    ## filesystems may use a different character.
    ## (Although I know of none such beast.)
    var extPos = searchExtPos(filename)
    if extPos < 0: result = filename & normExt(ext)
    else: result = filename

  proc cmpPaths*(pathA, pathB: string): int {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Compares two paths.
    ##
    ## On a case-sensitive filesystem this is done
    ## case-sensitively otherwise case-insensitively. Returns:
    ##
    ## | 0 iff pathA == pathB
    ## | < 0 iff pathA < pathB
    ## | > 0 iff pathA > pathB
    if FileSystemCaseSensitive:
      result = cmp(pathA, pathB)
    else:
      when defined(nimscript):
        result = cmpic(pathA, pathB)
      elif defined(nimdoc): discard
      else:
        result = cmpIgnoreCase(pathA, pathB)

  proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1".} =
    ## Checks whether a given `path` is absolute.
    ##
    ## On Windows, network paths are considered absolute too.
    when doslike:
      var len = len(path)
      result = (len > 1 and path[0] in {'/', '\\'}) or
               (len > 2 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':')
    elif defined(macos):
      result = path.len > 0 and path[0] != ':'
    elif defined(RISCOS):
      result = path[0] == '$'
    elif defined(posix):
      result = path[0] == '/'

  proc unixToNativePath*(path: string, drive=""): string {.
    noSideEffect, rtl, extern: "nos$1".} =
    ## Converts an UNIX-like path to a native one.
    ##
    ## On an UNIX system this does nothing. Else it converts
    ## '/', '.', '..' to the appropriate things.
    ##
    ## On systems with a concept of "drives", `drive` is used to determine
    ## which drive label to use during absolute path conversion.
    ## `drive` defaults to the drive of the current working directory, and is
    ## ignored on systems that do not have a concept of "drives".

    when defined(unix):
      result = path
    else:
      var start: int
      if path[0] == '/':
        # an absolute path
        when doslike:
          if drive != "":
            result = drive & ":" & DirSep
          else:
            result = $DirSep
        elif defined(macos):
          result = "" # must not start with ':'
        else:
          result = $DirSep
        start = 1
      elif path[0] == '.' and path[1] == '/':
        # current directory
        result = $CurDir
        start = 2
      else:
        result = ""
        start = 0

      var i = start
      while i < len(path): # ../../../ --> ::::
        if path[i] == '.' and path[i+1] == '.' and path[i+2] == '/':
          # parent directory
          when defined(macos):
            if result[high(result)] == ':':
              add result, ':'
            else:
              add result, ParDir
          else:
            add result, ParDir & DirSep
          inc(i, 3)
        elif path[i] == '/':
          add result, DirSep
          inc(i)
        else:
          add result, path[i]
          inc(i)

when defined(nimdoc) and not declared(os):
  proc getEnv(x: string): string = discard
  proc existsFile(x: string): bool = discard

when declared(getEnv) or defined(nimscript):
  proc getHomeDir*(): string {.rtl, extern: "nos$1",
    tags: [ReadEnvEffect, ReadIOEffect].} =
    ## Returns the home directory of the current user.
    ##
    ## This proc is wrapped by the expandTilde proc for the convenience of
    ## processing paths coming from user configuration files.
    when defined(windows): return string(getEnv("USERPROFILE")) & "\\"
    else: return string(getEnv("HOME")) & "/"

  proc getConfigDir*(): string {.rtl, extern: "nos$1",
    tags: [ReadEnvEffect, ReadIOEffect].} =
    ## Returns the config directory of the current user for applications.
    when defined(windows): return string(getEnv("APPDATA")) & "\\"
    else: return string(getEnv("HOME")) & "/.config/"

  proc getTempDir*(): string {.rtl, extern: "nos$1",
    tags: [ReadEnvEffect, ReadIOEffect].} =
    ## Returns the temporary directory of the current user for applications to
    ## save temporary files in.
    when defined(windows): return string(getEnv("TEMP")) & "\\"
    else: return "/tmp/"

  proc expandTilde*(path: string): string {.
    tags: [ReadEnvEffect, ReadIOEffect].} =
    ## Expands a path starting with ``~/`` to a full path.
    ##
    ## If `path` starts with the tilde character and is followed by `/` or `\\`
    ## this proc will return the reminder of the path appended to the result of
    ## the getHomeDir() proc, otherwise the input path will be returned without
    ## modification.
    ##
    ## The behaviour of this proc is the same on the Windows platform despite
    ## not having this convention. Example:
    ##
    ## .. code-block:: nim
    ##   let configFile = expandTilde("~" / "appname.cfg")
    ##   echo configFile
    ##   # --> C:\Users\amber\appname.cfg
    if len(path) > 1 and path[0] == '~' and (path[1] == '/' or path[1] == '\\'):
      result = getHomeDir() / path.substr(2)
    else:
      result = path

  when not declared(split):
    iterator split(s: string, sep: char): string =
      var last = 0
      if len(s) > 0:
        while last <= len(s):
          var first = last
          while last < len(s) and s[last] != sep: inc(last)
          yield substr(s, first, last-1)
          inc(last)

  when not defined(windows) and declared(os):
    proc checkSymlink(path: string): bool =
      var rawInfo: Stat
      if lstat(path, rawInfo) < 0'i32: result = false
      else: result = S_ISLNK(rawInfo.st_mode)

  proc findExe*(exe: string, followSymlinks: bool = true): string {.
    tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect].} =
    ## Searches for `exe` in the current working directory and then
    ## in directories listed in the ``PATH`` environment variable.
    ## Returns "" if the `exe` cannot be found. On DOS-like platforms, `exe`
    ## is added the `ExeExt <#ExeExt>`_ file extension if it has none.
    ## If the system supports symlinks it also resolves them until it
    ## meets the actual file. This behavior can be disabled if desired.
    result = addFileExt(exe, ExeExt)
    if existsFile(result): return
    var path = string(getEnv("PATH"))
    for candidate in split(path, PathSep):
      when defined(windows):
        var x = (if candidate[0] == '"' and candidate[^1] == '"':
                  substr(candidate, 1, candidate.len-2) else: candidate) /
               result
      else:
        var x = expandTilde(candidate) / result
      if existsFile(x):
        when not defined(windows) and declared(os):
          while followSymlinks: # doubles as if here
            if x.checkSymlink:
              var r = newString(256)
              var len = readlink(x, r, 256)
              if len < 0:
                raiseOSError(osLastError())
              if len > 256:
                r = newString(len+1)
                len = readlink(x, r, len)
              setLen(r, len)
              if isAbsolute(r):
                x = r
              else:
                x = parentDir(x) / r
            else:
              break
        return x
    result = ""

when defined(nimscript) or (defined(nimdoc) and not declared(os)):
  {.pop.} # hint[ConvFromXtoItselfNotNeeded]:off