Add Dir and Env to CLI platform

This commit is contained in:
Richard Feldman 2022-09-12 16:12:36 -04:00
parent 4ccd726141
commit 21b74f6dbb
No known key found for this signature in database
GPG key ID: F1F21AA5B1D9E43B
10 changed files with 208 additions and 19 deletions

View file

@ -0,0 +1,25 @@
interface Dir
exposes [ReadErr, DeleteErr, DirEntry, deleteEmptyDir, deleteRecursive, list]
imports [Effect, Task.{ Task }, InternalTask, Path.{ Path }, InternalPath, InternalDir]
ReadErr : InternalDir.ReadErr
DeleteErr : InternalDir.DeleteErr
DirEntry : InternalDir.DirEntry
## Lists the files and directories inside the directory.
list : Path -> Task (List Path) [DirReadErr Path ReadErr]* [Read [File]*]*
list = \path ->
effect = Effect.map (Effect.dirList (InternalPath.toBytes path)) \result ->
when result is
Ok entries -> Ok (List.map entries InternalPath.fromOsBytes)
Err err -> Err (DirReadErr path err)
InternalTask.fromEffect effect
## Deletes a directory if it's empty.
deleteEmptyDir : Path -> Task {} [DirDeleteErr Path DeleteErr]* [Write [File]*]*
## Recursively deletes the directory as well as all files and directories inside it.
deleteRecursive : Path -> Task {} [DirDeleteErr Path DeleteErr]* [Write [File]*]*

View file

@ -6,6 +6,8 @@ hosted Effect
always, always,
forever, forever,
loop, loop,
dirList,
cwd,
stdoutLine, stdoutLine,
stderrLine, stderrLine,
stdinLine, stdinLine,
@ -15,7 +17,7 @@ hosted Effect
fileWriteUtf8, fileWriteUtf8,
fileWriteBytes, fileWriteBytes,
] ]
imports [InternalHttp.{ Request, Response }, InternalFile] imports [InternalHttp.{ Request, Response }, InternalFile, InternalDir]
generates Effect with [after, map, always, forever, loop] generates Effect with [after, map, always, forever, loop]
stdoutLine : Str -> Effect {} stdoutLine : Str -> Effect {}
@ -26,5 +28,8 @@ fileWriteBytes : List U8, List U8 -> Effect (Result {} InternalFile.WriteErr)
fileWriteUtf8 : List U8, Str -> Effect (Result {} InternalFile.WriteErr) fileWriteUtf8 : List U8, Str -> Effect (Result {} InternalFile.WriteErr)
fileDelete : List U8 -> Effect (Result {} InternalFile.WriteErr) fileDelete : List U8 -> Effect (Result {} InternalFile.WriteErr)
fileReadBytes : List U8 -> Effect (Result (List U8) InternalFile.ReadErr) fileReadBytes : List U8 -> Effect (Result (List U8) InternalFile.ReadErr)
dirList : List U8 -> Effect (Result (List (List U8)) InternalDir.ReadErr)
cwd : Effect (List U8)
sendRequest : Box Request -> Effect Response sendRequest : Box Request -> Effect Response

View file

@ -0,0 +1,15 @@
interface Env
exposes [cwd]
imports [Task.{ Task }, Path.{ Path }, InternalPath, Effect, InternalTask]
## Reads the [current working directory](https://en.wikipedia.org/wiki/Working_directory)
## from the environment.
cwd : Task Path [CwdUnavailable]* [Env]*
cwd =
effect = Effect.map Effect.cwd \bytes ->
if List.isEmpty bytes then
Err CwdUnavailable
else
Ok (InternalPath.fromArbitraryBytes bytes)
InternalTask.fromEffect effect

View file

@ -6,6 +6,8 @@ ReadErr : InternalFile.ReadErr
WriteErr : InternalFile.WriteErr WriteErr : InternalFile.WriteErr
## Encodes a value using the given `EncodingFormat` and writes it to a file.
##
## For example, suppose you have a [JSON](https://en.wikipedia.org/wiki/JSON) ## For example, suppose you have a [JSON](https://en.wikipedia.org/wiki/JSON)
## `EncodingFormat` named `Json.toCompactUtf8`. Then you can use that format ## `EncodingFormat` named `Json.toCompactUtf8`. Then you can use that format
## to write some encodable data to a file as JSON, like so: ## to write some encodable data to a file as JSON, like so:
@ -31,7 +33,7 @@ write = \path, val, fmt ->
# TODO handle encoding errors here, once they exist # TODO handle encoding errors here, once they exist
writeBytes path bytes writeBytes path bytes
## Write bytes to a file. ## Writes bytes to a file.
## ##
## # Writes the bytes 1, 2, 3 to the file `myfile.dat`. ## # Writes the bytes 1, 2, 3 to the file `myfile.dat`.
## File.writeBytes (Path.fromStr "myfile.dat") [1, 2, 3] ## File.writeBytes (Path.fromStr "myfile.dat") [1, 2, 3]
@ -43,7 +45,7 @@ writeBytes : Path, List U8 -> Task {} [FileWriteErr Path WriteErr]* [Write [File
writeBytes = \path, bytes -> writeBytes = \path, bytes ->
toWriteTask path \pathBytes -> Effect.fileWriteBytes pathBytes bytes toWriteTask path \pathBytes -> Effect.fileWriteBytes pathBytes bytes
## Write a [Str] to a file, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8). ## Writes a [Str] to a file, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8).
## ##
## # Writes "Hello!" encoded as UTF-8 to the file `myfile.txt`. ## # Writes "Hello!" encoded as UTF-8 to the file `myfile.txt`.
## File.writeUtf8 (Path.fromStr "myfile.txt") "Hello!" ## File.writeUtf8 (Path.fromStr "myfile.txt") "Hello!"
@ -55,7 +57,7 @@ writeUtf8 : Path, Str -> Task {} [FileWriteErr Path WriteErr]* [Write [File]*]*
writeUtf8 = \path, str -> writeUtf8 = \path, str ->
toWriteTask path \bytes -> Effect.fileWriteUtf8 bytes str toWriteTask path \bytes -> Effect.fileWriteUtf8 bytes str
## Delete a file from the filesystem. ## Deletes a file from the filesystem.
## ##
## # Deletes the file named ## # Deletes the file named
## File.delete (Path.fromStr "myfile.dat") [1, 2, 3] ## File.delete (Path.fromStr "myfile.dat") [1, 2, 3]
@ -75,7 +77,7 @@ delete : Path -> Task {} [FileWriteErr Path WriteErr]* [Write [File]*]*
delete = \path -> delete = \path ->
toWriteTask path \bytes -> Effect.fileDelete bytes toWriteTask path \bytes -> Effect.fileDelete bytes
## Read all the bytes in a file. ## Reads all the bytes in a file.
## ##
## # Read all the bytes in `myfile.txt`. ## # Read all the bytes in `myfile.txt`.
## File.readBytes (Path.fromStr "myfile.txt") ## File.readBytes (Path.fromStr "myfile.txt")
@ -87,7 +89,7 @@ readBytes : Path -> Task (List U8) [FileReadErr Path ReadErr]* [Read [File]*]*
readBytes = \path -> readBytes = \path ->
toReadTask path \bytes -> Effect.fileReadBytes bytes toReadTask path \bytes -> Effect.fileReadBytes bytes
## Read a [Str] from a file containing [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-encoded text. ## Reads a [Str] from a file containing [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-encoded text.
## ##
## # Reads UTF-8 encoded text into a `Str` from the file `myfile.txt`. ## # Reads UTF-8 encoded text into a `Str` from the file `myfile.txt`.
## File.readUtf8 (Path.fromStr "myfile.txt") ## File.readUtf8 (Path.fromStr "myfile.txt")

View file

@ -0,0 +1,35 @@
interface FileMetadata
exposes [FileMetadata, bytes, type, isReadonly, mode]
imports []
# Design note: this is an opaque type rather than a type alias so that
# we can add new operating system info if new OS releases introduce them,
# as a backwards-compatible change.
FileMetadata := {
bytes : U64,
type : [File, Dir, Symlink],
isReadonly : Bool,
mode : [Unix U32, NonUnix],
}
bytes : FileMetadata -> U64
bytes = \@FileMetadata info -> info.bytes
isReadonly : FileMetadata -> Bool
isReadonly = \@FileMetadata info -> info.isReadonly
type : FileMetadata -> [File, Dir, Symlink]
type = \@FileMetadata info -> info.type
mode : FileMetadata -> [Unix U32, NonUnix]
mode = \@FileMetadata info -> info.mode
# TODO need to create a Time module and return something like Time.Utc here.
# lastModified : FileMetadata -> Utc
# TODO need to create a Time module and return something like Time.Utc here.
# lastAccessed : FileMetadata -> Utc
# TODO need to create a Time module and return something like Time.Utc here.
# created : FileMetadata -> Utc

View file

@ -0,0 +1,41 @@
interface InternalDir
exposes [ReadErr, DeleteErr, DirEntry]
imports [FileMetadata.{ FileMetadata }, Path.{ Path }]
DirEntry : {
path : Path,
type : [File, Dir, Symlink],
metadata : FileMetadata,
}
ReadErr : [
NotFound,
Interrupted,
InvalidFilename,
PermissionDenied,
TooManySymlinks, # aka FilesystemLoop
TooManyHardlinks,
TimedOut,
StaleNetworkFileHandle,
NotADirectory,
OutOfMemory,
Unsupported,
Unrecognized I32 Str,
]
DeleteErr : [
NotFound,
Interrupted,
InvalidFilename,
PermissionDenied,
TooManySymlinks, # aka FilesystemLoop
TooManyHardlinks,
TimedOut,
StaleNetworkFileHandle,
NotADirectory,
ReadOnlyFilesystem,
DirectoryNotEmpty,
OutOfMemory,
Unsupported,
Unrecognized I32 Str,
]

View file

@ -5,6 +5,8 @@ interface InternalPath
wrap, wrap,
unwrap, unwrap,
toBytes, toBytes,
fromArbitraryBytes,
fromOsBytes,
] ]
imports [] imports []
@ -61,3 +63,11 @@ toBytes = \@InternalPath path ->
FromOperatingSystem bytes -> bytes FromOperatingSystem bytes -> bytes
ArbitraryBytes bytes -> bytes ArbitraryBytes bytes -> bytes
FromStr str -> Str.toUtf8 str FromStr str -> Str.toUtf8 str
fromArbitraryBytes : List U8 -> InternalPath
fromArbitraryBytes = \bytes ->
@InternalPath (ArbitraryBytes bytes)
fromOsBytes : List U8 -> InternalPath
fromOsBytes = \bytes ->
@InternalPath (FromOperatingSystem bytes)

View file

@ -6,6 +6,7 @@ interface Path
WindowsRoot, WindowsRoot,
# toComponents, # toComponents,
# walkComponents, # walkComponents,
display,
fromStr, fromStr,
fromBytes, fromBytes,
withExtension, withExtension,
@ -85,10 +86,10 @@ fromBytes = \bytes ->
## with the given [Charset]. (Use [Env.charset] to get the current system charset.) ## with the given [Charset]. (Use [Env.charset] to get the current system charset.)
## ##
## For a conversion to [Str] that is lossy but does not return a [Result], see ## For a conversion to [Str] that is lossy but does not return a [Result], see
## [displayUtf8]. ## [display].
# toInner : Path -> [Str Str, Bytes (List U8)] # toInner : Path -> [Str Str, Bytes (List U8)]
## Assumes a path is encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8), ## Assumes a path is encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8),
## and converts it to a string using [Str.displayUtf8]. ## and converts it to a string using [Str.display].
## ##
## This conversion is lossy because the path may contain invalid UTF-8 bytes. If that happens, ## This conversion is lossy because the path may contain invalid UTF-8 bytes. If that happens,
## any invalid bytes will be replaced with the [Unicode replacement character](https://unicode.org/glossary/#replacement_character) ## any invalid bytes will be replaced with the [Unicode replacement character](https://unicode.org/glossary/#replacement_character)
@ -103,17 +104,21 @@ fromBytes = \bytes ->
## Converting paths to strings can be an unreliable operation, because operating systems ## Converting paths to strings can be an unreliable operation, because operating systems
## don't record the paths' encodings. This means it's possible for the path to have been ## don't record the paths' encodings. This means it's possible for the path to have been
## encoded with a different character set than UTF-8 even if UTF-8 is the system default, ## encoded with a different character set than UTF-8 even if UTF-8 is the system default,
## which means when [displayUtf8] converts them to a string, the string may include gibberish. ## which means when [display] converts them to a string, the string may include gibberish.
## [Here is an example.](https://unix.stackexchange.com/questions/667652/can-a-file-path-be-invalid-utf-8/667863#667863) ## [Here is an example.](https://unix.stackexchange.com/questions/667652/can-a-file-path-be-invalid-utf-8/667863#667863)
## ##
## If you happen to know the [Charset] that was used to encode the path, you can use ## If you happen to know the [Charset] that was used to encode the path, you can use
## [toStrUsingCharset] instead of [displayUtf8]. ## [toStrUsingCharset] instead of [display].
# displayUtf8 : Path -> Str display : Path -> Str
# displayUtf8 = \path -> display = \path ->
# when InternalPath.unwrap path is when InternalPath.unwrap path is
# FromStr str -> str FromStr str -> str
# FromOperatingSystem bytes | ArbitraryBytes bytes -> FromOperatingSystem bytes | ArbitraryBytes bytes ->
# Str.displayUtf8 bytes when Str.fromUtf8 bytes is
Ok str -> str
# TODO: this should use the builtin Str.display to display invalid UTF-8 chars in just the right spots, but that does not exist yet!
Err _ -> "<22>"
# isEq : Path, Path -> Bool # isEq : Path, Path -> Bool
# isEq = \p1, p2 -> # isEq = \p1, p2 ->
# when InternalPath.unwrap p1 is # when InternalPath.unwrap p1 is

View file

@ -238,6 +238,50 @@ pub extern "C" fn roc_fx_fileDelete(roc_path: &RocList<u8>) -> RocResult<(), Rea
} }
} }
#[no_mangle]
pub extern "C" fn roc_fx_cwd() -> RocList<u8> {
// TODO instead, call getcwd on UNIX and GetCurrentDirectory on Windows
match std::env::current_dir() {
Ok(path_buf) => os_str_to_roc_path(path_buf.into_os_string().as_os_str()),
Err(_) => {
// Default to empty path
RocList::empty()
}
}
}
#[no_mangle]
pub extern "C" fn roc_fx_dirList(
// TODO: this RocResult should use Dir.WriteErr - but right now it's File.WriteErr
// because glue doesn't have Dir.WriteErr yet.
roc_path: &RocList<u8>,
) -> RocResult<RocList<RocList<u8>>, WriteErr> {
println!("Dir.list...");
match std::fs::read_dir(path_from_roc_path(roc_path)) {
Ok(dir_entries) => RocResult::ok(
dir_entries
.map(|opt_dir_entry| match opt_dir_entry {
Ok(entry) => os_str_to_roc_path(entry.path().into_os_string().as_os_str()),
Err(_) => {
todo!("handle dir_entry path didn't resolve")
}
})
.collect::<RocList<RocList<u8>>>(),
),
Err(_) => {
todo!("handle Dir.list error");
}
}
}
#[cfg(target_family = "unix")]
/// TODO convert from EncodeWide to RocPath on Windows
fn os_str_to_roc_path(os_str: &OsStr) -> RocList<u8> {
use std::os::unix::ffi::OsStrExt;
RocList::from(os_str.as_bytes())
}
#[no_mangle] #[no_mangle]
pub extern "C" fn roc_fx_sendRequest(roc_request: &glue::Request) -> glue::Response { pub extern "C" fn roc_fx_sendRequest(roc_request: &glue::Request) -> glue::Response {
let mut builder = reqwest::blocking::ClientBuilder::new(); let mut builder = reqwest::blocking::ClientBuilder::new();

View file

@ -1,12 +1,18 @@
app "file-io" app "file-io"
packages { pf: "cli-platform/main.roc" } packages { pf: "cli-platform/main.roc" }
imports [pf.Stdout, pf.Stderr, pf.Task, pf.File, pf.Path] imports [pf.Stdout, pf.Stderr, pf.Task, pf.File, pf.Path, pf.Env, pf.Dir]
provides [main] to pf provides [main] to pf
main : Task.Task {} [] [Write [File, Stdout, Stderr], Read [File]] main : Task.Task {} [] [Write [File, Stdout, Stderr], Read [File], Env]
main = main =
path = Path.fromStr "out.txt" path = Path.fromStr "out.txt"
task = task =
cwd <- Env.cwd |> Task.await
cwdStr = Path.display cwd
_ <- Stdout.line "cwd: \(cwdStr)" |> Task.await
dirEntries <- Dir.list cwd |> Task.await
contentsStr = Str.joinWith (List.map dirEntries Path.display) "\n "
_ <- Stdout.line "Directory contents:\n \(contentsStr)\n" |> Task.await
_ <- Stdout.line "Writing a string to out.txt" |> Task.await _ <- Stdout.line "Writing a string to out.txt" |> Task.await
_ <- File.writeUtf8 path "a string!" |> Task.await _ <- File.writeUtf8 path "a string!" |> Task.await
contents <- File.readUtf8 path |> Task.await contents <- File.readUtf8 path |> Task.await
@ -18,4 +24,5 @@ main =
Err (FileWriteErr _ Unsupported) -> Stderr.line "Err: Unsupported" Err (FileWriteErr _ Unsupported) -> Stderr.line "Err: Unsupported"
Err (FileWriteErr _ (Unrecognized _ other)) -> Stderr.line "Err: \(other)" Err (FileWriteErr _ (Unrecognized _ other)) -> Stderr.line "Err: \(other)"
Err (FileReadErr _ _) -> Stderr.line "Error reading file" Err (FileReadErr _ _) -> Stderr.line "Error reading file"
_ -> Stdout.line "Successfully wrote a string to out.txt" Err _ -> Stderr.line "Uh oh, there was an error!"
Ok _ -> Stdout.line "Successfully wrote a string to out.txt"