HORIZON HASKELLDocslts/ghc-9.14.x45dd5372026-10-11Search names, modules, packages, or :: a typeCtrl K

GHC 9.14.1 · lts/ghc-9.14.x · 45dd537 · 2026-10-11

Modulestreamly-core-0.3.1Haskell2010

Streamly.FileSystem.Path

File system paths that are extensible, high-performance and preserve the OS and filesystem encoding.

The Path type is built on top of Streamly's Array type, leveraging all its operations — including support for both pinned and unpinned representations. The API integrates with streams, prioritizes safety, flexibility, and performance. It supports configurable equality for cross-platform compatibility and user-defined path matching. It is designed for extensibility and fine-grained type safety as well. For type-safe adaptations, see the "Streamly.Internal.FileSystem.Path.*" modules.

Path is interconvertible with the OsPath type from the filepath package at zero runtime cost. While the API is mostly compatible with that of the filepath package, some differences exist due to a slightly different design philosophy focused on better safety.

Rooted vs Unrooted Paths

To ensure the safety of the path append operation, we distinguish between rooted paths and free path segments or unrooted paths. A path that starts from an explicit or implicit file system root is called a rooted path or an anchored path. For example, /usr/bin is a rooted path with / as an explicit root directory. Similarly, ./bin is a rooted path with the current directoy "." as an implicit root. A path that is not rooted is called an unrooted path or unanchored path; for example, local/bin is an unrooted path.

This distinction ensures the safety of the path append operation. You can append only an unrooted path to another path, it does not make sense to append a rooted path to another path. The default append operation in the Path module checks for this and fails if the operation is invalid.

Rooted vs unrooted distinction is a stricter form of relative vs absolute path distinction. In this model, for better safety, paths relative to the current directory are also treated in the same way as absolute paths, from the perspective of a path append operation. This is because the meaning of current directory is context dependent and dynamic, therefore, appending it to another path is not allowed. Only unrooted path segments (e.g. local/bin) can be appended to any other path using safe operations.

File vs. Directory Paths

By default, a path with a trailing separator (e.g. local/) is implicitly considered a directory path. However, the absence of a trailing separator does not indicate whether the path is a file or a directory — it could be either. Therefore, when using the Path type, the append operation allows appending to paths even if they lack a trailing separator.

Compatibility with the filepath package

Any path type can be converted to the FilePath type from the filepath package by using the toString operation. Operations to convert to and from the OsPath type at zero cost are provided in the streamly-filepath package. Zero-cost interconversion is possible because the Path type uses an underlying representation which is compatible with the OsPath type.

Path Creation Quasiquoter

The path quasiquoter is useful in creating valid paths that are checked during the compile time.

  • 3 types
  • 26 values

Setup

0 declarations

To execute the code examples provided in this module in ghci, please run the following commands first.

Example10 expressions
:m:set -XQuasiQuotesimport Control.Exception (SomeException, evaluate, try)import Data.Either (Either, isLeft)import Data.Maybe (fromJust, isJust, isNothing)import Streamly.FileSystem.Path (Path, path)import qualified Streamly.Data.Array as Arrayimport qualified Streamly.Data.Stream as Streamimport qualified Streamly.FileSystem.Path as Pathimport qualified Streamly.Unicode.Stream as Unicode

For APIs that have not been released yet.

Example1 expression
import qualified Streamly.Internal.FileSystem.Path as Path

Utilities:

Example1 expression
fails x = isLeft <$> (try (evaluate x) :: IO (Either SomeException String))

Type

2 declarations

Construction

4 declarations
valuevalidatePath :: MonadThrow m => Array OsWord -> m ()
#

Checks whether the filepath is valid; i.e., whether the operating system permits such a path for listing or creating files. These validations are operating system specific and file system independent. Throws an exception with a detailed explanation if the path is invalid.

Example1 expression
isValid = isJust . Path.validatePath . Path.encodeString

Validations:

Example2 expressions
isValid ""FalseisValid "\0"False

Other than these there may be maximum path component length and maximum path length restrictions enforced by the OS as well as the filesystem which we do not validate.

valuefromArray :: MonadThrow m => Array OsWord -> m Path
#

Convert an encoded array of OsWord into a value of type Path. The path is validated using validatePath.

Each OsWord should be encoded such that:

  • The input does not contain a NUL word.

  • Values from 1-128 are assumed to be ASCII characters.

Apart from the above, there are no restrictions on the encoding.

To bypass path validation checks, use unsafeFromArray.

Throws InvalidPath if validatePath fails on the resulting path.

Statically Verified String Literals

1 declaration

Quasiquoters.

valuepath :: QuasiQuoter
#

Generates a Path type from a quoted literal. Equivalent to using fromString on the static literal.

Example1 expression
Path.toString ([path|/usr/bin|] :: Path)"/usr/bin"

Statically Verified Strings

1 declaration

Template Haskell expression splices.

valuepathE :: String -> Q Exp
#

Generates a Haskell expression of type Path from a String. Equivalent to using fromString on the string passed.

Elimination

2 declarations
valuetoString :: Path -> [Char]
#

Decode the path to a Unicode string using strict UTF-8 decoding.

Path Info

2 declarations
valueisRooted :: Path -> Bool
#

A path that is attached to a root e.g. "/x" or "./x" are rooted paths. "/" is considered an absolute root and "." as a dynamic root. ".." is not considered a root, "../x" or "x/y" are not rooted paths.

Example1 expression
isRooted = Path.isRooted . Path.fromString_
Example4 expressions
isRooted "/"TrueisRooted "/x"TrueisRooted "."TrueisRooted "./x"True
valueisUnrooted :: Path -> Bool
#

A path that is not attached to a root e.g. a/b/c or ../b/c.

Example1 expression
isUnrooted = not . Path.isRooted
Example1 expression
isUnrooted = Path.isUnrooted . Path.fromString_
Example4 expressions
isUnrooted "x"TrueisUnrooted "x/y"TrueisUnrooted ".."TrueisUnrooted "../x"True

Joining

3 declarations
valueunsafeJoin :: Path -> Path -> Path
#

Like join but does not check if the second path is rooted.

Example1 expression
f a b = Path.toString $ Path.unsafeJoin (Path.fromString_ a) (Path.fromString_ b)
Example4 expressions
f "x" "y""x/y"f "x/" "y""x/y"f "x" "/y""x/y"f "x/" "/y""x/y"
valuejoin :: Path -> Path -> Path
#

Append a separator followed by another path to a Path. Fails if the second path is a rooted path. Use unsafeJoin to avoid failure if you know it is ok to append the rooted path.

Example1 expression
f a b = Path.toString $ Path.join a b
Example3 expressions
f [path|/usr|] [path|bin|]"/usr/bin"f [path|/usr/|] [path|bin|]"/usr/bin"fails (f [path|/usr|] [path|/bin|])True

Splitting root

1 declaration
valuesplitRoot :: Path -> Maybe (Path, Maybe Path)
#

If a path is rooted then separate the root and the remaining path, otherwise return Nothing. The non-root part is guaranteed to NOT start with a separator.

Some filepath package equivalent idioms:

Example6 expressions
splitDrive = Path.splitRootjoinDrive = Path.unsafeJointakeDrive = fmap fst . Path.splitRootdropDrive x = Path.splitRoot x >>= sndhasDrive = isJust . Path.splitRootisDrive = isNothing . dropDrive
Example2 expressions
toList (a,b) = (Path.toString a, fmap Path.toString b)split = fmap toList . Path.splitRoot . Path.fromString_
Example1 expression
split "/"Just ("/",Nothing)
Example1 expression
split "."Just (".",Nothing)
Example1 expression
split "./"Just ("./",Nothing)
Example1 expression
split "/home"Just ("/",Just "home")
Example1 expression
split "//"Just ("//",Nothing)
Example1 expression
split "./home"Just ("./",Just "home")
Example1 expression
split "home"Nothing

Splitting path components

1 declaration
valuesplitPath :: Monad m => Path -> Stream m Path
#

Split the path components keeping separators between path components attached to the dir part. Redundant separators are removed, only the first one is kept. Separators are not added either e.g. "." and ".." may not have trailing separators if the original path does not.

Example1 expression
split = Stream.toList . fmap Path.toString . Path.splitPath . Path.fromString_
Example1 expression
split "."["."]
Example1 expression
split "././"["./"]
Example1 expression
split "./a/b/."["./","a/","b/"]
Example1 expression
split ".."[".."]
Example1 expression
split "../"["../"]
Example1 expression
split "a/.."["a/",".."]
Example1 expression
split "/"["/"]
Example1 expression
split "//"["/"]
Example1 expression
split "/x"["/","x"]
Example1 expression
split "/./x/"["/","x/"]
Example1 expression
split "/x/./y"["/","x/","y"]
Example1 expression
split "/x/../y"["/","x/","../","y"]
Example1 expression
split "/x///y"["/","x/","y"]
Example1 expression
split "/x/\\y"["/","x/","\\y"]

Splitting file extension

3 declarations
valuesplitExtension :: Path -> Maybe (Path, Path)
#

Returns Just(filename, extension) if an extension is present otherwise returns Nothing.

A file name is considered to have an extension if the file name can be split into a non-empty filename followed by the extension separator "." followed by a non-empty extension with at least one character in addition to the extension separator. The shortest suffix obtained by this rule, starting with the extension separator, is returned as the extension and the remaining prefix part as the filename.

A directory name does not have an extension.

If you want a splitExtensions, you can use splitExtension until the extension returned is Nothing. dropExtensions, isExtensionOf can be implemented similarly.

Example2 expressions
toList (a,b) = (Path.toString a, Path.toString b)split = fmap toList . Path.splitExtension . Path.fromString_
Example1 expression
split "/"Nothing
Example1 expression
split "."Nothing
Example1 expression
split ".."Nothing
Example1 expression
split "x"Nothing
Example1 expression
split "/x"Nothing
Example1 expression
split "x/"Nothing
Example1 expression
split "./x"Nothing
Example1 expression
split "x/."Nothing
Example1 expression
split "x/y."Nothing
Example1 expression
split "/x.y"Just ("/x",".y")
Example1 expression
split "/x.y."Nothing
Example1 expression
split "/x.y.."Nothing
Example1 expression
split "x/.y"Nothing
Example1 expression
split ".x"Nothing
Example1 expression
split "x."Nothing
Example1 expression
split ".x.y"Just (".x",".y")
Example1 expression
split "x/y.z"Just ("x/y",".z")
Example1 expression
split "x.y.z"Just ("x.y",".z")
Example1 expression
split "x..y"Just ("x.",".y")
Example1 expression
split "..."Nothing
Example1 expression
split "..x"Just (".",".x")
Example1 expression
split "...x"Just ("..",".x")
Example1 expression
split "x/y.z/"Nothing
Example1 expression
split "x/y"Nothing
valuetakeExtension :: Path -> Maybe Path
#

Take the extension of a file if it has one.

Example2 expressions
takeExtension = fmap snd . Path.splitExtensionhasExtension = isJust . Path.splitExtension
Example1 expression
fmap Path.toString $ Path.takeExtension [path|/home/user/file.txt|]Just ".txt"

See splitExtension for more examples.

valuedropExtension :: Path -> Path
#

Drop the extension of a file if it has one.

Example1 expression
dropExtension p = maybe p fst $ Path.splitExtension p
Example1 expression
Path.toString $ Path.dropExtension [path|/home/user/file.txt|]"/home/user/file"

Splitting file and dir

4 declarations
valuesplitFile :: Path -> Maybe (Maybe Path, Path)
#

If the path does not look like a directory then return Just (Maybe dir, file) otherwise return Nothing. The path is not a directory if:

  • the path does not end with a separator

  • the path does not end with a . or .. component

Splits a single component path into Just (Nothing, path) if it does not look like a dir.

Example2 expressions
toList (a,b) = (fmap Path.toString a, Path.toString b)split = fmap toList . Path.splitFile . Path.fromString_
Example1 expression
split "/"Nothing
Example1 expression
split "."Nothing
Example1 expression
split "/."Nothing
Example1 expression
split ".."Nothing
> split "//" -- Posix

Nothing

Example1 expression
split "/home"Just (Just "/","home")
Example1 expression
split "./home"Just (Just "./","home")
Example1 expression
split "home"Just (Nothing,"home")
Example1 expression
split "x/"Nothing
Example1 expression
split "x/y"Just (Just "x/","y")
Example1 expression
split "x//y"Just (Just "x//","y")
Example1 expression
split "x/./y"Just (Just "x/./","y")
valuetakeFileName :: Path -> Maybe Path
#

Extracts the file name component (with extension) from a Path, if present.

Example2 expressions
takeFileName = fmap snd . Path.splitFilereplaceDirectory p x = fmap (flip Path.join x) (takeFileName p)
Example2 expressions
fmap Path.toString $ Path.takeFileName [path|/home/user/file.txt|]Just "file.txt"fmap Path.toString $ Path.takeFileName [path|/home/user/|]Nothing

See splitFile for more examples.

valuetakeDirectory :: Path -> Maybe Path
#

Returns the parent directory of the given Path, if any.

Example2 expressions
takeDirectory x = Path.splitFile x >>= fstreplaceFileName p x = fmap (flip Path.join x) (takeDirectory p)

To get an equivalent to takeDirectory from filepath use dropTrailingSeparators on the result.

Example2 expressions
fmap Path.toString $ Path.takeDirectory [path|/home/user/file.txt|]Just "/home/user/"fmap Path.toString $ Path.takeDirectory [path|file.txt|]Nothing
valuetakeFileBase :: Path -> Maybe Path
#

Extracts the file name dropping the extension, if any, from a Path.

Example1 expression
takeFileBase = fmap Path.dropExtension . Path.takeFileName
Example4 expressions
fmap Path.toString $ Path.takeFileBase [path|/home/user/file.txt|]Just "file"fmap Path.toString $ Path.takeFileBase [path|/home/user/file|]Just "file"fmap Path.toString $ Path.takeFileBase [path|/home/user/.txt|]Just ".txt"fmap Path.toString $ Path.takeFileBase [path|/home/user/|]Nothing

See splitFile for more examples.

Equality

5 declarations
datadata EqCfg
#

Options for path comparison operation. By default path comparison uses a strict criteria for equality. The following options are provided to control the strictness.

The default configuration is as follows:

Example1 expression
:{defaultMod = ignoreTrailingSeparators False           . ignoreCase False           . allowRelativeEquality False:}

When set to False (default):

Example2 expressions
cfg = Path.ignoreTrailingSeparators Falseeq a b = Path.eqPath cfg (Path.fromString_ a) (Path.fromString_ b)
Example1 expression
eq "x/"  "x"False

When set to True:

Example2 expressions
cfg = Path.ignoreTrailingSeparators Trueeq a b = Path.eqPath cfg (Path.fromString_ a) (Path.fromString_ b)
Example1 expression
eq "x/"  "x"True

Default: False

Allow relative paths to be treated as equal. When this is False relative paths will never match even if they are literally equal e.g. "./x" will not match "./x" because the meaning of "." in both cases could be different depending on what the user meant by current directory in each case.

When set to False (default):

Example5 expressions
cfg = Path.allowRelativeEquality Falseeq a b = Path.eqPath cfg (Path.fromString_ a) (Path.fromString_ b)eq "."  "."Falseeq "./x"  "./x"Falseeq "./x"  "x"False

When set to False (default):

Example5 expressions
cfg = Path.allowRelativeEquality Trueeq a b = Path.eqPath cfg (Path.fromString_ a) (Path.fromString_ b)eq "."  "."Trueeq "./x"  "./x"Trueeq "./x"  "x"True
Example1 expression
eq "./x"  "././x"True

Default: False

valueeqPath :: (EqCfg -> EqCfg) -> Path -> Path -> Bool
#

Checks whether two paths are logically equal. This function takes a configuration modifier to customize the notion of equality. For using the default configuration pass id as the modifier. For details about the defaults, see EqCfg.

eqPath performs some normalizations on the paths before comparing them, specifically it drops redundant path separators between path segments and redundant "/./" components between segments.

Default config options use strict equality, for strict equality both the paths must be absolute or both must be path segments without a leading root component (e.g. x/y). Also, both must be files or both must be directories.

In addition to the default config options, the following equality semantics are used:

  • An absolute path and a path relative to "." may be equal depending on the meaning of ".", however this routine treats them as unequal, it does not resolve the "." to a concrete path.

  • Two paths having ".." components may be equal after processing the ".." components even if we determined them to be unequal. However, if we determined them to be equal then they must be equal.

Using default config with case sensitive comparision, if eqPath returns equal then the paths are definitely equal, if it returns unequal then the paths may still be equal under some relaxed equality criterion.

Example1 expression
:{ eq a b = Path.eqPath id (Path.fromString_ a) (Path.fromString_ b):}
Example2 expressions
eq "x"  "x"Trueeq ".."  ".."True

Non-trailing separators and non-leading "." segments are ignored:

Example4 expressions
eq "/x"  "//x"Trueeq "x//y"  "x/y"Trueeq "x/./y"  "x/y"Trueeq "x/y/."  "x/y"True

Leading dot, relative paths are not equal by default:

Example3 expressions
eq "."  "."Falseeq "./x"  "./x"Falseeq "./x"  "x"False

Trailing separators are significant by default:

Example1 expression
eq "x/"  "x"False

Match is case sensitive by default:

Example1 expression
eq "x"  "X"False