From 64be70b3afbb02623a5faae143c3a5e479b899cc Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Mon, 19 Jun 2023 07:32:55 +0000 Subject: [PATCH] File operations * read * write * append * exists? * tests for each --- Readme.org | 8 +- src/lex.rs | 5 +- src/stl.rs | 2 + src/stl/file.rs | 172 +++++++++++++++++++++++++++++++++++++++++ tests/test_lib_file.rs | 80 +++++++++++++++++++ 5 files changed, 261 insertions(+), 6 deletions(-) create mode 100644 src/stl/file.rs create mode 100644 tests/test_lib_file.rs diff --git a/Readme.org b/Readme.org index 463f546..0ce26e6 100644 --- a/Readme.org +++ b/Readme.org @@ -1,3 +1,4 @@ + #+Title: Relish: Rusty Expressive LIsp SHell #+Author: Ava Hahn @@ -155,13 +156,11 @@ Note: this section only tracks the state of incomplete TODO items. Having everyt (See tag: v0.3.0) ** TODO v1.0 tasks - islist type query +- assert function +- set library - Can pass args to relish scripts (via interpreter) - Can pass args to relish scripts (via command line) - File operations - - read-to-string - - write-to-file - - file exists? - - (path functions) - (add this all to the readme) - finish basic goals in the [[file:snippets/interactive-devel.rls][interactive development library]] - Release CI @@ -170,7 +169,6 @@ Note: this section only tracks the state of incomplete TODO items. Having everyt - Post release to relevant channels ** TODO v1.1 tasks - finish stretch goals in the [[file:snippets/interactive-devel.rls][interactive development library]] -- Stl boolean assert builtin - History length configurable (env var?) - Lex function - Read function (Input + Lex) diff --git a/src/lex.rs b/src/lex.rs index b42490e..14dda4b 100644 --- a/src/lex.rs +++ b/src/lex.rs @@ -82,7 +82,10 @@ fn process(document: &String) -> Result, String> { delim = *d; if delim == '*' { - token.push(ESCAPES[&c]); + token.push(ESCAPES.get(&c) + .cloned() + .or(Some(c)) + .unwrap()); delim_stack.pop(); continue; diff --git a/src/stl.rs b/src/stl.rs index 8a37c6d..876542d 100644 --- a/src/stl.rs +++ b/src/stl.rs @@ -32,6 +32,7 @@ pub mod control; pub mod decl; pub mod math; pub mod strings; +pub mod file; pub const CONSOLE_XDIM_VNAME: &str = "_RELISH_WIDTH"; pub const CONSOLE_YDIM_VNAME: &str = "_RELISH_HEIGHT"; @@ -66,6 +67,7 @@ pub fn static_stdlib(syms: &mut SymTable) { control::add_control_lib(syms); boolean::add_bool_lib(syms); math::add_math_lib(syms); + file::add_file_lib(syms); syms.insert( "call".to_string(), diff --git a/src/stl/file.rs b/src/stl/file.rs new file mode 100644 index 0000000..fc3a709 --- /dev/null +++ b/src/stl/file.rs @@ -0,0 +1,172 @@ +/* relish: versatile lisp shell + * Copyright (C) 2021 Aidan Hahn + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use crate::segment::{Ctr, Seg, Type}; +use crate::sym::{SymTable, Symbol, ValueType, Args}; +use crate::error::{Traceback, start_trace}; +use std::io::Write; +use std::fs::{File, read_to_string, OpenOptions}; +use std::rc::Rc; +use std::path::Path; + + +const READ_TO_STRING_DOCSTRING: &str = "Takes one input (filename). +If file exists, returns a string containing file contents. +If the file does not exist returns error."; +fn read_to_string_callback(ast: &Seg, _syms: &mut SymTable) -> Result { + if let Ctr::String(ref filename) = *ast.car { + let res = read_to_string(filename); + if let Ok(s) = res { + Ok(Ctr::String(s)) + } else { + Err(start_trace( + ("read-file", res.err().unwrap().to_string()) + .into())) + } + } else { + Err(start_trace(("read-file", "impossible arg").into())) + } +} + +const WRITE_TO_FILE_DOCSTRING: &str = "Takes two inputs: a filename and a string of content. +Writes contents to the file and returns None."; +fn write_to_file_callback(ast: &Seg, _syms: &mut SymTable) -> Result { + if let Ctr::String(ref filename) = *ast.car { + if let Ctr::Seg(ref next) = *ast.cdr { + if let Ctr::String(ref body) = *next.car { + let fres = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(filename); + if fres.is_err() { + Err(start_trace( + ("write-file", + format!("couldn't open file: {}", fres.err().unwrap().to_string())) + .into())) + } else { + if let Err(e) = write!(&mut fres.unwrap(), "{}", body) { + Err(start_trace( + ("write-file", format!("failed to write to file: {}", e)) + .into())) + } else { + Ok(Ctr::None) + } + } + } else { + Err(start_trace(("write-file", "impossible arg").into())) + } + } else { + Err(start_trace(("write-file", "not enough args").into())) + } + } else { + Err(start_trace(("write-file", "impossible arg").into())) + } +} + +const APPEND_TO_FILE_DOCSTRING: &str = "Takes two inputs: a filename and a string of content. +Appends content to the end of the file and returns None"; +fn append_to_file_callback(ast: &Seg, _syms: &mut SymTable) -> Result { + if let Ctr::String(ref filename) = *ast.car { + if let Ctr::Seg(ref next) = *ast.cdr { + if let Ctr::String(ref body) = *next.car { + let fres = File::options().append(true).open(filename); + if fres.is_err() { + Err(start_trace( + ("append-file", + format!("couldn't open file: {}", fres.err().unwrap().to_string())) + .into())) + } else { + if let Err(e) = write!(&mut fres.unwrap(), "{}", body) { + Err(start_trace( + ("append-file", format!("failed to write to file: {}", e)) + .into())) + } else { + Ok(Ctr::None) + } + } + } else { + Err(start_trace(("append-file", "impossible arg").into())) + } + } else { + Err(start_trace(("append-file", "not enough args").into())) + } + } else { + Err(start_trace(("append-file", "impossible arg").into())) + } +} + +const IS_FILE_EXISTS_DOCSTRING: &str = "Takes one input: a filename. +Returns true or false depending on if the file exists."; +fn is_file_exists_callback(ast: &Seg, _syms: &mut SymTable) -> Result { + if let Ctr::String(ref filename) = *ast.car { + Ok(Ctr::Bool(Path::new(&filename).exists())) + } else { + Err(Traceback::new().with_trace(("exists?", "impossible arg").into())) + } +} + +pub fn add_file_lib(syms: &mut SymTable) { + syms.insert( + "read-file".to_string(), + Symbol { + name: String::from("read-file"), + args: Args::Strict(vec![Type::String]), + conditional_branches: false, + docs: READ_TO_STRING_DOCSTRING.to_string(), + value: ValueType::Internal(Rc::new(read_to_string_callback)), + ..Default::default() + } + ); + + syms.insert( + "write-file".to_string(), + Symbol { + name: String::from("write-file"), + args: Args::Strict(vec![Type::String, Type::String]), + conditional_branches: false, + docs: WRITE_TO_FILE_DOCSTRING.to_string(), + value: ValueType::Internal(Rc::new(write_to_file_callback)), + ..Default::default() + } + ); + + syms.insert( + "append-file".to_string(), + Symbol { + name: String::from("append-file"), + args: Args::Strict(vec![Type::String, Type::String]), + conditional_branches: false, + docs: APPEND_TO_FILE_DOCSTRING.to_string(), + value: ValueType::Internal(Rc::new(append_to_file_callback)), + ..Default::default() + } + ); + + syms.insert( + "exists?".to_string(), + Symbol { + name: String::from("exists?"), + args: Args::Strict(vec![Type::String]), + conditional_branches: false, + docs: IS_FILE_EXISTS_DOCSTRING.to_string(), + value: ValueType::Internal(Rc::new(is_file_exists_callback)), + ..Default::default() + } + ); + +} diff --git a/tests/test_lib_file.rs b/tests/test_lib_file.rs new file mode 100644 index 0000000..578f844 --- /dev/null +++ b/tests/test_lib_file.rs @@ -0,0 +1,80 @@ +mod file_lib_tests { + use relish::ast::{eval, lex, SymTable}; + use relish::stdlib::{dynamic_stdlib, static_stdlib}; + + #[test] + fn test_fexists() { + let document = "(exists? '/tmp')"; + let result = "true"; + + let mut syms = SymTable::new(); + static_stdlib(&mut syms); + dynamic_stdlib(&mut syms, None); + assert_eq!( + *eval(&lex(&document.to_string()).unwrap(), &mut syms) + .unwrap() + .to_string(), + result.to_string(), + ); + } + + #[test] + fn test_fexists_doesnt() { + let document = "(exists? 'cargo.timtam')"; + let result = "false"; + + let mut syms = SymTable::new(); + static_stdlib(&mut syms); + dynamic_stdlib(&mut syms, None); + assert_eq!( + *eval(&lex(&document.to_string()).unwrap(), &mut syms) + .unwrap() + .to_string(), + result.to_string(), + ); + } + + #[test] + fn test_write_file() { + let document = " + (let ((s 'test') + (t '/tmp/relish-lib-test-file-1')) + (write-file t s) + (echo (read-file t)) + (eq? (read-file t) s))"; + let result = "true"; + + let mut syms = SymTable::new(); + static_stdlib(&mut syms); + dynamic_stdlib(&mut syms, None); + assert_eq!( + *eval(&lex(&document.to_string()).unwrap(), &mut syms) + .unwrap() + .to_string(), + result.to_string(), + ); + } + + #[test] + fn test_append_file() { + let document = " + (let ((s 'test') + (t '/tmp/relish-lib-test-file-2')) + (write-file t s) + (append-file t s) + (eq? (read-file t) + (concat s s)))"; + let result = "true"; + + let mut syms = SymTable::new(); + static_stdlib(&mut syms); + dynamic_stdlib(&mut syms, None); + assert_eq!( + *eval(&lex(&document.to_string()).unwrap(), &mut syms) + .unwrap() + .to_string(), + result.to_string(), + ); + } + +}