Skip to content

Commit

Permalink
Merge pull request #36 from skx/32-files
Browse files Browse the repository at this point in the history
Implement file:* functions.
  • Loading branch information
skx authored Oct 16, 2022
2 parents 5271d29 + e297710 commit 9c4f68d
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 60 deletions.
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)

0 comments on commit 9c4f68d

Please sign in to comment.