Skip to content

Commit

Permalink
rewrite file handle, adjust file.seek() (#36)
Browse files Browse the repository at this point in the history
* rewrite file handle, adjust file.seek()

Signed-off-by: Flipez <code@brauser.io>

* add file.content(), file.read(INT)

Signed-off-by: Flipez <code@brauser.io>

* readAll from 0, update tests and docs

Signed-off-by: Flipez <code@brauser.io>

* enhance tests

Signed-off-by: Flipez <code@brauser.io>

* improve error handling

Signed-off-by: Flipez <code@brauser.io>

* improve read and seek

Signed-off-by: Flipez <code@brauser.io>

* improve file.write; add nil check to seek

Signed-off-by: Flipez <code@brauser.io>
  • Loading branch information
Flipez authored Jan 15, 2022
1 parent 189458c commit 24b74b8
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 58 deletions.
28 changes: 21 additions & 7 deletions docs/content/en/docs/literals/file.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,45 @@ Closes the file pointer. Returns always `true`.



### content()
> Returns `STRING|ERROR`
Reads content of the file and returns it. Resets the position to 0 after read.



### lines()
> Returns `ARRAY|ERROR`
If successfull, returns all lines of the file as array elements, otherwise `null`. Resets the position to 0 after read.



### read()
### position()
> Returns `INTEGER`
Returns the position of the current file handle. -1 if the file is closed.



### read(INTEGER)
> Returns `STRING|ERROR`
Reads content of the file and returns it. Resets the position to 0 after read.
Reads the given amount of bytes from the file. Sets the position to the bytes that where actually read. At the end of file EOF error is returned.



### rewind()
> Returns `BOOLEAN`
### seek(INTEGER, INTEGER)
> Returns `INTEGER|ERROR`
Resets the read pointer back to position `0`. Always returns `true`.
Seek sets the offset for the next Read or Write on file to offset, interpreted according to whence. 0 means relative to the origin of the file, 1 means relative to the current offset, and 2 means relative to the end.



### write(STRING)
> Returns `BOOLEAN|NULL`
> Returns `BOOLEAN|ERROR`
Writes the given string to the file. Returns `true` on success, `false` on failure and `null` if pointer is invalid.
Writes the given string to the file. Returns `true` on success.



Expand Down
4 changes: 3 additions & 1 deletion evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,9 @@ func TestBuiltinFunctions(t *testing.T) {
{`exit("Error")`, "argument to `exit` must be INTEGER, got=STRING"},
{`open()`, "wrong number of arguments. got=0, want=1"},
{`open(1)`, "argument to `file` not supported, got=INTEGER"},
{`open("main.go", 1)`, "argument mode to `file` not supported, got=INTEGER"},
{`open("fixtures/module.rl", 1)`, "argument mode to `file` not supported, got=INTEGER"},
{`open("fixtures/module.rl", "r", 1)`, "argument perm to `file` not supported, got=INTEGER"},
{`open("fixtures/module.rl", "nope", "0644").read(1)`, "Failed to invoke method: read"},
}

for _, tt := range tests {
Expand Down
8 changes: 5 additions & 3 deletions object/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ type Error struct {
Message string
}

func (e *Error) Type() ObjectType { return ERROR_OBJ }
func (e *Error) Inspect() string { return "ERROR: " + e.Message }
func (e *Error) InvokeMethod(method string, env Environment, args ...Object) Object { return nil }
func (e *Error) Type() ObjectType { return ERROR_OBJ }
func (e *Error) Inspect() string { return "ERROR: " + e.Message }
func (e *Error) InvokeMethod(method string, env Environment, args ...Object) Object {
return objectMethodLookup(e, method, args)
}
145 changes: 105 additions & 40 deletions object/file.go
Original file line number Diff line number Diff line change
@@ -1,62 +1,69 @@
package object

import (
"bufio"
"fmt"
"io/fs"
"io/ioutil"
"os"
"strconv"
"strings"
)

type File struct {
Filename string
Reader *bufio.Reader
Writer *bufio.Writer
Position int64
Handle *os.File
}

func (f *File) Type() ObjectType { return FILE_OBJ }
func (f *File) Inspect() string { return fmt.Sprintf("<file:%s>", f.Filename) }
func (f *File) Open(mode string) error {
func (f *File) Open(mode string, perm string) error {
if f.Filename == "!STDIN!" {
f.Reader = bufio.NewReader(os.Stdin)
f.Handle = os.Stdin
return nil
}
if f.Filename == "!STDOUT!" {
f.Writer = bufio.NewWriter(os.Stdout)
f.Handle = os.Stdout
return nil
}
if f.Filename == "!STDERR!" {
f.Writer = bufio.NewWriter(os.Stderr)
f.Handle = os.Stderr
return nil
}

md := os.O_RDONLY

if mode == "w" {
switch mode {
case "r":
case "w":
md = os.O_WRONLY
case "wa":
md = os.O_WRONLY | os.O_APPEND
case "rw":
md = os.O_RDWR
case "rwa":
md = os.O_RDWR | os.O_APPEND
default:
return fmt.Errorf("invalid file mode, got `%s`", mode)
}

os.Remove(f.Filename)
} else {
if strings.Contains(mode, "w") && strings.Contains(mode, "a") {
md = os.O_WRONLY
md |= os.O_APPEND
}
if md != os.O_RDONLY {
md = md | os.O_CREATE
}

file, err := os.OpenFile(f.Filename, os.O_CREATE|md, 0644)
filePerm, err := strconv.ParseUint(perm, 10, 32)
if err != nil {
return err
}

f.Handle = file

if md == os.O_RDONLY {
f.Reader = bufio.NewReader(file)
} else {
f.Writer = bufio.NewWriter(file)
file, err := os.OpenFile(f.Filename, md, fs.FileMode(filePerm))
if err != nil {
return err
}

f.Handle = file
f.Position = 0

return nil
}

Expand All @@ -70,6 +77,7 @@ func init() {
method: func(o Object, _ []Object) Object {
f := o.(*File)
f.Handle.Close()
f.Position = -1
return &Boolean{Value: true}
},
},
Expand All @@ -92,46 +100,101 @@ func init() {
return &Array{Elements: result}
},
},
"read": ObjectMethod{
"content": ObjectMethod{
description: "Reads content of the file and returns it. Resets the position to 0 after read.",
returnPattern: [][]string{
[]string{STRING_OBJ, ERROR_OBJ},
},
method: readFile,
},
"rewind": ObjectMethod{
description: "Resets the read pointer back to position `0`. Always returns `true`.",
"position": ObjectMethod{
description: "Returns the position of the current file handle. -1 if the file is closed.",
returnPattern: [][]string{
[]string{BOOLEAN_OBJ},
[]string{INTEGER_OBJ},
},
method: func(o Object, _ []Object) Object {
f := o.(*File)
f.Handle.Seek(0, 0)
return &Boolean{Value: true}
return &Integer{Value: f.Position}
},
},
"read": ObjectMethod{
description: "Reads the given amount of bytes from the file. Sets the position to the bytes that where actually read. At the end of file EOF error is returned.",
argPattern: [][]string{
[]string{INTEGER_OBJ},
},
returnPattern: [][]string{
[]string{STRING_OBJ, ERROR_OBJ},
},
method: func(o Object, args []Object) Object {
f := o.(*File)
bytesAmount := args[0].(*Integer).Value
if f.Handle == nil {
return &Error{Message: "Invalid file handle."}
}

buffer := make([]byte, bytesAmount)
bytesRealRead, err := f.Handle.Read(buffer)
f.Position += int64(bytesRealRead)

if err != nil {
return &Error{Message: err.Error()}
}

return &String{Value: string(buffer)}
},
},
"seek": ObjectMethod{
description: "Seek sets the offset for the next Read or Write on file to offset, interpreted according to whence. 0 means relative to the origin of the file, 1 means relative to the current offset, and 2 means relative to the end.",
argPattern: [][]string{
[]string{INTEGER_OBJ},
[]string{INTEGER_OBJ},
},
returnPattern: [][]string{
[]string{INTEGER_OBJ, ERROR_OBJ},
},
method: func(o Object, args []Object) Object {
f := o.(*File)

if f.Handle == nil {
return &Error{Message: "Invalid file handle."}
}

seekAmount := args[0].(*Integer).Value
seekRelative := args[1].(*Integer).Value
newOffset, err := f.Handle.Seek(seekAmount, int(seekRelative))
f.Position = newOffset

if err != nil {
return &Error{Message: err.Error()}
}

return &Integer{Value: f.Position}
},
},
"write": ObjectMethod{
description: "Writes the given string to the file. Returns `true` on success, `false` on failure and `null` if pointer is invalid.",
description: "Writes the given string to the file. Returns `true` on success.",
returnPattern: [][]string{
[]string{BOOLEAN_OBJ, NULL_OBJ},
[]string{BOOLEAN_OBJ, ERROR_OBJ},
},
argPattern: [][]string{
[]string{STRING_OBJ},
},
method: func(o Object, args []Object) Object {
f := o.(*File)
content := []byte(args[0].(*String).Value)

if f.Writer == nil {
return (&Null{})
if f.Handle == nil {
return &Error{Message: "Invalid file handle."}
}

_, err := f.Writer.Write([]byte(args[0].(*String).Value))
if err == nil {
f.Writer.Flush()
return &Boolean{Value: true}
bytesWritten, err := f.Handle.Write(content)
f.Position += int64(bytesWritten)

if err != nil {
return &Error{Message: err.Error()}
}

return &Boolean{Value: false}
return &Boolean{Value: true}
},
},
}
Expand All @@ -143,15 +206,17 @@ func (f *File) InvokeMethod(method string, env Environment, args ...Object) Obje

func readFile(o Object, _ []Object) Object {
f := o.(*File)
if f.Reader == nil {
return (&String{Value: ""})
f.Handle.Seek(0, 0)
if f.Handle == nil {
return &Error{Message: "Invalid file handle."}
}

file, err := ioutil.ReadAll(f.Reader)
file, err := ioutil.ReadAll(f.Handle)
if err != nil {
return (&Error{Message: err.Error()})
return &Error{Message: err.Error()}
}

f.Handle.Seek(0, 0)
f.Position = 0
return &String{Value: string(file)}
}
26 changes: 21 additions & 5 deletions object/file_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
package object_test

import (
"os"
"testing"
)

func TestFileObjectMethods(t *testing.T) {
tests := []inputTestCase{
{`let a = open("../main.go"); a.close()`, true},
{`let a = open("../fixtures/module.rl"); a.read()`, "a = 1\nA = 5\n\nSum = fn(a, b) {\n return a + b\n}\n"},
{`let a = open("../fixtures/module.rl"); a.lines().size()`, 7},
{`(open("").wat().lines().size() == open("").methods().size() + 1).plz_s()`, "true"},
{`open("").type()`, "FILE"},
{`open("../fixtures/module.rl").close()`, true},
{`a = open("../fixtures/module.rl"); a.close(); a.position()`, -1},
{`open("../fixtures/module.rl").content().size()`, 49},
{`open("../fixtures/module.rl").content(1)`, "To many arguments: want=0, got=1"},
{`open("../fixtures/module.rl").read()`, "To few arguments: want=1, got=0"},
{`open("../fixtures/module.rl").read(1)`, "a"},
{`open("../fixtures/module.rl").position()`, 0},
{`a = open("../fixtures/module.rl"); a.read(1); a.content(); a.position()`, 0},
{`a = open("../fixtures/module.rl"); a.read(1); a.position()`, 1},
{`a = open("../fixtures/module.rl"); a.read(1); a.content().size()`, 49},
{`open("../fixtures/module.rl").lines().size()`, 7},
{`a = open("../fixtures/module.rl"); a.read(25); a.lines().size()`, 7},
{`open("../fixtures/nope")`, "open ../fixtures/nope: no such file or directory"},
{`open("../fixtures/nope").content()`, "Failed to invoke method: content"},
{`a = open("../fixtures/nope", "rw"); a.content()`, ""},
{`(open("../fixtures/module.rl").wat().lines().size() == open("../fixtures/module.rl").methods().size() + 1).plz_s()`, "true"},
{`open("").type()`, "ERROR"},
{`open("../fixtures/module.rl").type()`, "FILE"},
}

testInput(t, tests)

os.Remove("../fixtures/nope")
}
17 changes: 15 additions & 2 deletions stdlib/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
func openFunction(args ...object.Object) object.Object {
path := ""
mode := "r"
perm := "0644"

if len(args) < 1 {
return newError("wrong number of arguments. got=%d, want=1", len(args))
Expand All @@ -19,7 +20,7 @@ func openFunction(args ...object.Object) object.Object {
return newError("argument to `file` not supported, got=%s", args[0].Type())
}

if len(args) > 1 {
if len(args) == 2 {
switch args[1].(type) {
case *object.String:
mode = args[1].(*object.String).Value
Expand All @@ -28,7 +29,19 @@ func openFunction(args ...object.Object) object.Object {
}
}

if len(args) == 3 {
switch args[2].(type) {
case *object.String:
perm = args[2].(*object.String).Value
default:
return newError("argument perm to `file` not supported, got=%s", args[2].Type())
}
}

file := &object.File{Filename: path}
file.Open(mode)
err := file.Open(mode, perm)
if err != nil {
return newError(err.Error())
}
return (file)
}

1 comment on commit 24b74b8

@vercel
Copy link

@vercel vercel bot commented on 24b74b8 Jan 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.