Skip to content

Commit

Permalink
Implement file:write
Browse files Browse the repository at this point in the history
This matches file:read, and allows writing (string) data to a named
file in a simple fashion.

* Add the primitive.
* Add the test-cases.
* Add the documentation.

This closes #49.
  • Loading branch information
skx committed Oct 17, 2022
1 parent 58f2d6e commit eab11bf
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 5 deletions.
13 changes: 12 additions & 1 deletion PRIMITIVES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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`
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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`
Expand Down
26 changes: 26 additions & 0 deletions builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")}})
Expand Down Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions builtins/builtins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down
12 changes: 8 additions & 4 deletions builtins/help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down

0 comments on commit eab11bf

Please sign in to comment.