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:* functions. #36

Merged
merged 2 commits into from
Oct 16, 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
119 changes: 103 additions & 16 deletions builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package builtins

import (
"bufio"
"bytes"
"flag"
"fmt"
Expand All @@ -19,6 +20,7 @@ import (
"sort"
"strconv"
"strings"
"syscall"
"time"

"github.com/skx/yal/env"
Expand Down Expand Up @@ -111,6 +113,9 @@ func PopulateEnvironment(env *env.Environment) {
env.Set("error", &primitive.Procedure{F: errorFn, Help: helpMap["error"]})
env.Set("exists?", &primitive.Procedure{F: existsFn, Help: helpMap["exists?"]})
env.Set("file?", &primitive.Procedure{F: fileFn, Help: helpMap["file?"]})
env.Set("file:lines", &primitive.Procedure{F: fileLinesFn, Help: helpMap["file:lines"]})
env.Set("file:read", &primitive.Procedure{F: fileReadFn, Help: helpMap["file:read"]})
env.Set("file:stat", &primitive.Procedure{F: fileStatFn, Help: helpMap["file:stat"]})
env.Set("gensym", &primitive.Procedure{F: gensymFn, Help: helpMap["gensym"]})
env.Set("get", &primitive.Procedure{F: getFn, Help: helpMap["get"]})
env.Set("getenv", &primitive.Procedure{F: getenvFn, Help: helpMap["getenv"]})
Expand All @@ -128,7 +133,6 @@ func PopulateEnvironment(env *env.Environment) {
env.Set("print", &primitive.Procedure{F: printFn, Help: helpMap["print"]})
env.Set("set", &primitive.Procedure{F: setFn, Help: helpMap["set"]})
env.Set("shell", &primitive.Procedure{F: shellFn, Help: helpMap["shell"]})
env.Set("slurp", &primitive.Procedure{F: slurpFn, Help: helpMap["slurp"]})
env.Set("sort", &primitive.Procedure{F: sortFn, Help: helpMap["sort"]})
env.Set("split", &primitive.Procedure{F: splitFn, Help: helpMap["split"]})
env.Set("sprintf", &primitive.Procedure{F: sprintfFn, Help: helpMap["sprintf"]})
Expand Down Expand Up @@ -485,6 +489,104 @@ func fileFn(env *env.Environment, args []primitive.Primitive) primitive.Primitiv
return primitive.Bool(false)
}

// fileLinesFn implements (file:lines)
func fileLinesFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {
// We only need a single argument
if len(args) != 1 {
return primitive.Error("invalid argument count")
}

// Which is a string
fName, ok := args[0].(primitive.String)
if !ok {
return primitive.Error("argument not a string")
}

// Return value,
var res primitive.List

// Open the file
file, err := os.Open(fName.ToString())
if err != nil {
return primitive.Error(fmt.Sprintf("failed to open %s:%s", fName.ToString(), err))
}
defer file.Close()

// Read each line, and append to our list.
scanner := bufio.NewScanner(file)
for scanner.Scan() {
res = append(res, primitive.String(scanner.Text()))
}

// All done.
return res
}

// fileReadFn implements (file:read)
func fileReadFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {
// We only need a single argument
if len(args) != 1 {
return primitive.Error("invalid argument count")
}

// Which is a string
fName, ok := args[0].(primitive.String)
if !ok {
return primitive.Error("argument not a string")
}

data, err := os.ReadFile(fName.ToString())
if err != nil {
return primitive.Error(fmt.Sprintf("error reading %s %s", fName.ToString(), err))
}
return primitive.String(string(data))
}

// fileStatFn implements (file:lines)
//
// Return value is (NAME SIZE UID GID MODE)
func fileStatFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {
// We only need a single argument
if len(args) != 1 {
return primitive.Error("invalid argument count")
}

// Which is a string
fName, ok := args[0].(primitive.String)
if !ok {
return primitive.Error("argument not a string")
}

// Stat the entry
info, err := os.Stat(fName.ToString())

if err != nil {
return primitive.Nil{}
}

var UID int
var GID int
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
UID = int(stat.Uid)
GID = int(stat.Gid)
} else {
// we are not in linux, this won't work anyway in windows,
// but maybe you want to log warnings
UID = os.Getuid()
GID = os.Getgid()
}

var res primitive.List

res = append(res, primitive.String(info.Name()))
res = append(res, primitive.Number(info.Size()))
res = append(res, primitive.Number(UID))
res = append(res, primitive.Number(GID))
res = append(res, primitive.String(info.Mode().String()))

return res
}

// gensymFn is the implementation of (gensym ..)
func gensymFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {
// symbol characters
Expand Down Expand Up @@ -944,7 +1046,6 @@ func setFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive
}

// shellFn runs a command via the shell
// slurpFn returns the contents of the specified file
func shellFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {

// We need one argument
Expand Down Expand Up @@ -987,20 +1088,6 @@ func shellFn(env *env.Environment, args []primitive.Primitive) primitive.Primiti
return ret
}

// slurpFn returns the contents of the specified file
func slurpFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {
if len(args) != 1 {
return primitive.Error("wrong number of arguments")
}

fName := args[0].ToString()
data, err := os.ReadFile(fName)
if err != nil {
return primitive.Error(fmt.Sprintf("error reading %s %s", fName, err))
}
return primitive.String(string(data))
}

// sortFn implements (sort)
func sortFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {
// If we have only a single argument
Expand Down
87 changes: 43 additions & 44 deletions builtins/builtins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,49 @@ func TestFile(t *testing.T) {

}

// TestFileRead tests file:read
func TestFileRead(t *testing.T) {

// calling with no argument
out := fileReadFn(ENV, []primitive.Primitive{})

// Will lead to an error
_, ok := out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}

// Call with a file that doesn't exist
out = fileReadFn(ENV, []primitive.Primitive{
primitive.String("path/not/found")})

_, ok = out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}

// Create a temporary file, and read the contents
tmp, _ := os.CreateTemp("", "yal")
err := os.WriteFile(tmp.Name(), []byte("I like cake"), 0777)
if err != nil {
t.Fatalf("failed to write to file")
}
defer os.Remove(tmp.Name())

str := fileReadFn(ENV, []primitive.Primitive{
primitive.String(tmp.Name())})

// Will lead to an error
txt, ok2 := str.(primitive.String)
if !ok2 {
t.Fatalf("expected string, got %v", out)
}

if txt.ToString() != "I like cake" {
t.Fatalf("re-reading the temporary file gave bogus contents")
}
}

// TestGenSym tests gensym
func TestGenSym(t *testing.T) {

Expand Down Expand Up @@ -2087,50 +2130,6 @@ func TestShell(t *testing.T) {
}
}

// TestSlurp tests slurp
func TestSlurp(t *testing.T) {

// calling with no argument
out := slurpFn(ENV, []primitive.Primitive{})

// Will lead to an error
_, ok := out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}

// Call with a file that doesn't exist
out = slurpFn(ENV, []primitive.Primitive{
primitive.String("path/not/found")})

_, ok = out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}

// Create a temporary file, and read the contents
tmp, _ := os.CreateTemp("", "yal")
err := os.WriteFile(tmp.Name(), []byte("I like cake"), 0777)
if err != nil {
t.Fatalf("failed to write to file")
}
defer os.Remove(tmp.Name())

str := slurpFn(ENV, []primitive.Primitive{
primitive.String(tmp.Name())})

// Will lead to an error
txt, ok2 := str.(primitive.String)
if !ok2 {
t.Fatalf("expected string, got %v", out)
}

if txt.ToString() != "I like cake" {
t.Fatalf("re-reading the temporary file gave bogus contents")
}

}

func TestSort(t *testing.T) {

// No arguments
Expand Down
24 changes: 24 additions & 0 deletions builtins/help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,30 @@ More specifically something is regarded as a file if it is NOT a directory.
See also: directory? exists?
Example: (print (file? "/dev/null"))
%%
file:lines

file:lines returns the contents of the given file, as a list of lines

See also: file:read
%%
file:read

file:read returns the contents of the given file, as a string.

See also: file:lines
%%


file:stat

file:stat returns a list containing details of the given file/directory,
or an error if it couldn't be found.

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"))
%%
gensym

gensym returns a symbol which is guaranteed to be unique. It is primarily
Expand Down
33 changes: 33 additions & 0 deletions stdlib/stdlib.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -415,3 +415,36 @@
"Return everything but the last element from the specified list."

(take (dec (length l)) l)))

;; Wrappers for our file functions
(set! file:stat:size (fn* (path)
"Return the size of the given file, return -1 on error."
(let* (info (file:stat path))
(cond
(nil? info) -1
true (nth info 1)))))

(set! file:stat:uid (fn* (path)
"Return the UID of the given file owner, return '' on error."
(let* (info (file:stat path))
(cond
(nil? info) ""
true (nth info 2)))))


(set! file:stat:gid (fn* (path)
"Return the GID of the given file owner, return '' on error."
(let* (info (file:stat path))
(cond
(nil? info) ""
true (nth info 3)))))

(set! file:stat:mode (fn* (path)
"Return the mode of the given file, return '' on error."
(let* (info (file:stat path))
(cond
(nil? info) ""
true (nth info 4)))))

; Slurp used to be a primitive for reading file contents
(set! slurp file:read)