6 | import Data.String.Extra
11 | import Text.Quantity
15 | export infixl 5 </>
, />
22 | dirSeparator = if isWindows then '\\' else '/'
26 | pathSeparator : Char
27 | pathSeparator = if isWindows then ';' else ':'
65 | volume : Maybe Volume
75 | (==) (UNC l1 l2) (UNC r1 r2) = l1 == r1 && r1 == r2
76 | (==) (Disk l) (Disk r) = l == r
81 | (==) CurDir CurDir = True
82 | (==) ParentDir ParentDir = True
83 | (==) (Normal l) (Normal r) = l == r
88 | (==) (MkPath l1 l2 l3 _) (MkPath r1 r2 r3 _) =
89 | l1 == r1 && l2 == r2 && l3 == r3
94 | emptyPath = MkPath Nothing False [] False
103 | show ParentDir = ".."
104 | show (Normal normal) = normal
108 | show (UNC server share) = "\\\\" ++ server ++ "\\" ++ share
109 | show (Disk disk) = String.singleton disk ++ ":"
116 | sep = String.singleton dirSeparator
117 | showVol = maybe "" show path.volume
118 | showRoot = if path.hasRoot then sep else ""
119 | showBody = join sep $
map show path.body
120 | showTrail = if path.hasTrailSep then sep else ""
122 | showVol ++ showRoot ++ showBody ++ showTrail
128 | data PathTokenKind = PTText | PTPunct Char
130 | Eq PathTokenKind where
131 | (==) PTText PTText = True
132 | (==) (PTPunct c1) (PTPunct c2) = c1 == c2
136 | PathToken = Token PathTokenKind
138 | TokenKind PathTokenKind where
139 | TokType PTText = String
140 | TokType (PTPunct _) = ()
142 | tokValue PTText x = x
143 | tokValue (PTPunct _) _ = ()
145 | pathTokenMap : TokenMap PathToken
146 | pathTokenMap = toTokenMap $
147 | [ (is '/', PTPunct '/')
148 | , (is '\\', PTPunct '\\')
149 | , (is ':', PTPunct ':')
150 | , (is '?', PTPunct '?')
151 | , (some $
non $
oneOf "/\\:?", PTText)
154 | lexPath : String -> List (WithBounds PathToken)
157 | (tokens, _, _, _) = lex pathTokenMap str
162 | bodySeparator : Grammar state PathToken True ()
163 | bodySeparator = (match $
PTPunct '\\') <|> (match $
PTPunct '/')
170 | verbatim : Grammar state PathToken True ()
173 | ignore $
count (exactly 2) $
match $
PTPunct '\\'
174 | match $
PTPunct '?'
175 | match $
PTPunct '\\'
179 | unc : Grammar state PathToken True Volume
182 | ignore $
count (exactly 2) $
match $
PTPunct '\\'
183 | server <- match PTText
185 | share <- match PTText
186 | pure $
UNC server share
189 | verbatimUnc : Grammar state PathToken True Volume
193 | server <- match PTText
195 | share <- match PTText
196 | pure $
UNC server share
199 | disk : Grammar state PathToken True Volume
202 | text <- match PTText
203 | disk <- case unpack text of
204 | (disk :: xs) => pure disk
205 | [] => fail "Expects disk"
206 | match $
PTPunct ':'
207 | pure $
Disk (toUpper disk)
210 | verbatimDisk : Grammar state PathToken True Volume
217 | parseVolume : Grammar state PathToken True Volume
224 | parseBody : Grammar state PathToken True Body
227 | text <- match PTText
228 | pure $
case text of
231 | normal => Normal normal
233 | parsePath : Grammar state PathToken False Path
236 | vol <- optional parseVolume
237 | root <- optional (some bodySeparator)
238 | body <- sepBy (some bodySeparator) parseBody
239 | trailSep <- optional (some bodySeparator)
240 | let body = filter (\case Normal s => ltrim s /= "";
_ => True) body
241 | let body = case body of
243 | (x::xs) => x :: delete CurDir xs
244 | pure $
MkPath vol (isJust root) body (isJust trailSep)
247 | tryParse : String -> Maybe Path
249 | case parse parsePath (lexPath str) of
250 | Right (path, []) => Just path
276 | parse : String -> Path
278 | case parse parsePath (lexPath str) of
279 | Right (path, _) => path
286 | isAbsolute' : Path -> Bool
289 | case path.volume of
290 | Just (UNC _ _) => True
291 | Just (Disk _) => path.hasRoot
296 | append' : (left : Path) -> (right : Path) -> Path
297 | append' left right =
298 | if isAbsolute' right || isJust right.volume then
300 | else if hasRoot right then
301 | { volume := left.volume } right
303 | { body := left.body ++ right.body, hasTrailSep := right.hasTrailSep } left
305 | splitPath' : Path -> List Path
307 | case splitRoot path of
308 | (Just root, other) => root :: iterateBody (path.body) (path.hasTrailSep)
309 | (Nothing, other) => iterateBody (path.body) (path.hasTrailSep)
311 | splitRoot : Path -> (Maybe Path, Path)
312 | splitRoot path@(MkPath Nothing False _ _) = (Nothing, path)
313 | splitRoot (MkPath vol root xs trailSep) =
314 | (Just $
MkPath vol root [] False, MkPath Nothing False xs trailSep)
316 | iterateBody : List Body -> (trailSep : Bool) -> List Path
317 | iterateBody [] _ = []
318 | iterateBody [x] trailSep = [MkPath Nothing False [x] trailSep]
319 | iterateBody (x::y::xs) trailSep =
320 | (MkPath Nothing False [x] False) :: iterateBody (y::xs) trailSep
322 | splitParent' : Path -> Maybe (Path, Path)
323 | splitParent' path =
328 | parent = { body := init (x::xs), hasTrailSep := False } path
329 | child = MkPath Nothing False [last (x::xs)] path.hasTrailSep
331 | Just (parent, child)
333 | parent' : Path -> Maybe Path
334 | parent' = map fst . splitParent'
336 | fileName' : Path -> Maybe String
338 | findNormal $
reverse path.body
340 | findNormal : List Body -> Maybe String
341 | findNormal ((Normal normal)::xs) = Just normal
342 | findNormal (CurDir::xs) = findNormal xs
343 | findNormal _ = Nothing
345 | setFileName' : (name : String) -> Path -> Path
346 | setFileName' name path =
347 | if isJust (fileName' path) then
348 | append' (fromMaybe emptyPath $
parent' path) (parse name)
350 | append' path (parse name)
353 | splitFileName : String -> (String, String)
354 | splitFileName name =
355 | case break (== '.') $
reverse $
unpack name of
356 | (_, []) => (name, "")
357 | (_, ['.']) => (name, "")
358 | (revExt, (dot :: revStem)) =>
359 | ((pack $
reverse revStem), (pack $
reverse revExt))
369 | splitExtensions : String -> (String, List String)
370 | splitExtensions name = case map pack $
split (== '.') (unpack name) of
371 | ("" ::: base :: exts) => ("." ++ base, exts)
372 | (base ::: exts) => (base, exts)
387 | isAbsolute : String -> Bool
388 | isAbsolute = isAbsolute' . parse
392 | isRelative : String -> Bool
393 | isRelative = not . isAbsolute
409 | (/>) : (left : Path) -> (right : String) -> Path
410 | (/>) left right = append' left (parse right)
426 | (</>) : (left : String) -> (right : String) -> String
427 | (</>) left right = show $
parse left /> right
435 | joinPath : List String -> String
436 | joinPath xs = show $
foldl (/>) (parse "") xs
445 | splitPath : String -> List String
446 | splitPath = map show . splitPath' . parse
454 | splitParent : String -> Maybe (String, String)
457 | (parent, child) <- splitParent' (parse path)
458 | pure (show parent, show child)
464 | parent : String -> Maybe String
465 | parent = map show . parent' . parse
473 | parents : String -> List String
474 | parents = map show . List.iterate parent' . parse
484 | isBaseOf : (base : String) -> (target : String) -> Bool
485 | isBaseOf base target =
487 | MkPath baseVol baseRoot baseBody _ = parse base
488 | MkPath targetVol targetRoot targetBody _ = parse target
490 | baseVol == targetVol
491 | && baseRoot == targetRoot
492 | && (baseBody `isPrefixOf` targetBody)
498 | dropBase : (base : String) -> (target : String) -> Maybe String
499 | dropBase base target =
501 | let MkPath baseVol baseRoot baseBody _ = parse base
502 | let MkPath targetVol targetRoot targetBody targetTrialSep = parse target
503 | if baseVol == targetVol && baseRoot == targetRoot then Just () else Nothing
504 | body <- dropBody baseBody targetBody
505 | pure $
show $
MkPath Nothing False body targetTrialSep
507 | dropBody : (base : List Body) -> (target : List Body) -> Maybe (List Body)
508 | dropBody [] ys = Just ys
509 | dropBody xs [] = Nothing
510 | dropBody (x::xs) (y::ys) = if x == y then dropBody xs ys else Nothing
519 | fileName : String -> Maybe String
520 | fileName = fileName' . parse
531 | fileStem : String -> Maybe String
532 | fileStem path = pure $
fst $
splitFileName !(fileName path)
543 | extension : String -> Maybe String
544 | extension path = fileName path >>=
545 | filter (/= "") . Just . snd . splitFileName
555 | extensions : String -> Maybe (List String)
556 | extensions path = snd . splitExtensions <$> fileName path
563 | setFileName : (name : String) -> String -> String
564 | setFileName name path = show $
setFileName' name (parse path)
577 | (<.>) : String -> (extension : String) -> String
581 | ext = pack $
dropWhile (\char => char == '.' || isSpace char) (unpack ext)
582 | ext = if ext == "" then "" else "." ++ ext
584 | case fileName' path' of
586 | let (stem, _) = splitFileName name in
587 | show $
setFileName' (stem ++ ext) path'
592 | dropExtension : String -> String
593 | dropExtension path = path <.> ""