diff --git a/PRIMITIVES.md b/PRIMITIVES.md index 29abaf2..6583793 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -84,7 +84,7 @@ Things you'll find here include: * `chr` * Return the ASCII character of the given number. * `cons` - * Join two specified lists. + * Add the element to the start of the given (potentialy empty) list. * `contains?` * Does the specified hash contain the given key? * `date` @@ -108,6 +108,8 @@ Things you'll find here include: * Return the contents of the given file, as a string. * `file:stat` * Return details of the given path. +* `file:write` + * Write the specified content to the provided path. * `gensym` * Generate, and return, a unique symbol. Useful for macro definitions. * `get` @@ -188,8 +190,11 @@ Functions here include: * `and` * Logical operator, are all elements true? * `append` + * Append the given entry to the specified list. * `apply` + * Return the result of calling the specified function on every element in the supplied list, as a list. * `apply-hash` + * Apply the given function to everyu key in the specified hash, and return the result as list. * `boolean?` * Is the given thing a boolean? * `butlast` @@ -223,6 +228,11 @@ Functions here include: * Return the size of the path, from the information provided by `(file:stat)`. * `file:stat:uid` * Return the UID of the path, from the information provided by `(file:stat)`. +* `file:which` + * Locate the specified binary's location, upon the users' PATH. + * NOTE: This is almost certainly Unix/Linux/Darwin only, and will fail upon Windows systems. +* `file:write` + * Write the specified content to the given path. * `filter` * Remove every element from the given list, unless the function returns true. * `first` @@ -273,6 +283,7 @@ Functions here include: * `range` * Return a list of numbers between the given start/end, using the specified step-size. * `reduce` + * Our reduce function, with the list, function and accumulator. * `repeat` * Run the given body N times. * `repeated` diff --git a/builtins/builtins.go b/builtins/builtins.go index 1e6cf58..c8e34c1 100644 --- a/builtins/builtins.go +++ b/builtins/builtins.go @@ -114,6 +114,7 @@ func PopulateEnvironment(env *env.Environment) { env.Set("file:lines", &primitive.Procedure{F: fileLinesFn, Help: helpMap["file:lines"], Args: []primitive.Symbol{primitive.Symbol("path")}}) env.Set("file:read", &primitive.Procedure{F: fileReadFn, Help: helpMap["file:read"], Args: []primitive.Symbol{primitive.Symbol("path")}}) env.Set("file:stat", &primitive.Procedure{F: fileStatFn, Help: helpMap["file:stat"], Args: []primitive.Symbol{primitive.Symbol("path")}}) + env.Set("file:write", &primitive.Procedure{F: fileWriteFn, Help: helpMap["file:write"], Args: []primitive.Symbol{primitive.Symbol("path"), primitive.Symbol("content")}}) env.Set("file?", &primitive.Procedure{F: fileFn, Help: helpMap["file?"], Args: []primitive.Symbol{primitive.Symbol("path")}}) env.Set("gensym", &primitive.Procedure{F: gensymFn, Help: helpMap["gensym"]}) env.Set("get", &primitive.Procedure{F: getFn, Help: helpMap["get"], Args: []primitive.Symbol{primitive.Symbol("hash"), primitive.Symbol("key")}}) @@ -616,6 +617,31 @@ func fileStatFn(env *env.Environment, args []primitive.Primitive) primitive.Prim return res } +// fileWriteFn implements file:write +func fileWriteFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { + // We need two arguments + if len(args) != 2 { + return primitive.ArityError() + } + + // Path is a string + path, ok := args[0].(primitive.String) + if !ok { + return primitive.Error("argument not a string") + } + + // Content is a string + content, ok := args[1].(primitive.String) + if !ok { + return primitive.Error("argument not a string") + } + + err := os.WriteFile(path.ToString(), []byte(content.ToString()), 0777) + if err != nil { + return primitive.Error(fmt.Sprintf("failed to write to %s:%s", path.ToString(), err)) + } + return primitive.Nil{} +} // gensymFn is the implementation of (gensym ..) func gensymFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { // symbol characters diff --git a/builtins/builtins_test.go b/builtins/builtins_test.go index ac7698d..1081e76 100644 --- a/builtins/builtins_test.go +++ b/builtins/builtins_test.go @@ -1223,6 +1223,84 @@ func TestFileStat(t *testing.T) { } } +// TestFileWrite tests file:write +func TestFileWrite(t *testing.T) { + + // calling with no argument + out := fileWriteFn(ENV, []primitive.Primitive{}) + + // Will lead to an error + _, ok := out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + + } + + // Call with the wrong type + out = fileWriteFn(ENV, []primitive.Primitive{ + primitive.Number(3), + primitive.String("cake")}) + + _, ok = out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + + // Call with the wrong type + out = fileWriteFn(ENV, []primitive.Primitive{ + primitive.String("/tmp/blah.txt"), + primitive.Number(3)}) + + _, ok = out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + + // Now we can try to write to something real + // Create a temporary file to write to + tmpfile, err := os.CreateTemp("", "exists") + if err != nil { + t.Fatalf("failed to create a temporary file") + } + defer os.Remove(tmpfile.Name()) + // Try to write there + result := fileWriteFn(ENV, []primitive.Primitive{ + primitive.String(tmpfile.Name()), + primitive.String("cake")}) + + // Should be no errors + _, ok = result.(primitive.Nil) + if !ok { + t.Fatalf("expected nil, got %v", out) + } + + // Now create a temmporary directory, and use that + // as the destination - which will fail once it is + // removed + path, err2 := os.MkdirTemp("", "directory_") + if err2 != nil { + t.Fatalf("failed to create temporary directory") + } + + // path beneath the temporary directory + target := filepath.Join(path, "foo.txt") + + // remove the direcotry + os.RemoveAll(path) + + + failure := fileWriteFn(ENV, []primitive.Primitive{ + primitive.String(target), + primitive.String("cake")}) + + // Should be no errors + _, ok = failure.(primitive.Error) + if !ok { + t.Fatalf("expected failure writing beneath a directory which was removed, got %v", out) + } + +} + // TestGenSym tests gensym func TestGenSym(t *testing.T) { diff --git a/builtins/help.txt b/builtins/help.txt index dd84047..7087193 100644 --- a/builtins/help.txt +++ b/builtins/help.txt @@ -109,16 +109,14 @@ file:lines file:lines returns the contents of the given file, as a list of lines. -See also: file:read +See also: file:read, file:write %% file:read file:read returns the contents of the given file, as a string. -See also: file:lines +See also: file:lines, file:write %% - - file:stat file:stat returns a list containing details of the given file/directory, @@ -129,6 +127,12 @@ The return value is (NAME SIZE UID GID MODE). See also: file:stat:gid file:stat:mode file:stat:size file:stat:uid Example: (print (file:stat "/etc/passwd")) %% +file:write + +Write the given content to the specified path. + +Example: (file:write "/tmp/test.txt" "I like cake.") +%% gensym gensym returns a symbol which is guaranteed to be unique. It is primarily