Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement file:write #55

Merged
merged 2 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
17 changes: 17 additions & 0 deletions stdlib/stdlib.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,23 @@ This is used by both 'upper' and 'lower'."
(nil? info) ""
true (nth info 4)))))

(set! file:which (fn* (binary)
"Return the complete path to the specified binary, found via the users' PATH setting.

If the binary does not exist in a directory located upon the PATH nil will be returned.

NOTE: This is a non-portable function!

1. It assumes that the environmental variable PATH exists.
2. It assumes $PATH can be split by ':'
3. It assumes '/' works as a directory separator.
"
(let* (path (split (getenv "PATH") ":")
res (filter path (lambda (dir) (exists? (join (list dir "/" binary))))))
(if res
(join (list (car res) "/" binary))))))


;; Define a legacy alias
(alias slurp file:read)

Expand Down