This took 19 days from the day i downloaded Rust (excepting breakfast, dinner, garden work, walks and other everyday chores. One thing happened that I didn't expect at all: a mail came from Rust's org telling me they found my intense interest in the documentation so encouraging that they choose to push me up to "trusted" level. I won't deny it had some effect :0)
This is the last part of seven iterations. The first is in https://a-zproj.blogspot.com/2021/03/learning-rust-part-4-training-project.html
The editor now has the following commands implemented:
Commands:
[[=header, [=item, /*=start comment, */=end comment, a=add line, i=insert comment, l=list, d=delete line(s)
"Completed" means just that, the functions work but no more. There are (some) safeguards for entering wrong things from the keyboard and possibly a number of not existing/bad handling of errors, generally. One more thing one could wish is a way to go back to older versions of the edited file. There is none. But:
It's working, after a fashion. Functions have been added; delete-lines() and add_a_line(). Delete_lines accepts more than one line number to be deleted, they do not have to be sequential. add_a_line() accepts only one. Also check_num() is added as to stop entering text when numbers are expected.
I've learned many things and that was the goal.
Most of the code below has been commented earlier. Have you been following this subject up to now, I'm certain you'll handle the few new things.
The first two lines:
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
gets the arguments, which makes it possible to enter the .ini-file name I'd like to process. The first argument (args[0]) is the program's own name, the next is the first argument.
From that follows that the next line will contain the file name:
let fname_ini=args[1].to_string();
and during development the program will be started by "cargo run ed.ini".
#![allow(warnings)]
use std::fs;
use std::fs::{File,OpenOptions};
use std::io::{self, Read, Write, ErrorKind};
use std::process::exit;
use std::str;
use std::char;
use termios::{Termios, TCSANOW, ECHO, ICANON, tcsetattr};
use std::string::String;
use std::io::BufWriter;
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
let fname_ini=args[1].to_string();
let msg = "\nCommands:\n[[=header, [=item, /*=start comment, */=end comment, a=add line, i=insert comment, l=list, d=delete line(s) ".to_string();
println!("{}", msg);
let input=rdl("\nEnter command:\n".to_string());
// trim_end_matches(' ') returns a string slice (&str) By default there is a
// newline character at the end of the string produced by rdl() so you need to use trim!
let input=input.trim_end_matches('\n'); // returns a string slice (&str)
match input {
"[[" => header(input.to_string(),fname_ini.to_string()),
"[" => item(input.to_string(),fname_ini.to_string()),
"/*" => comment_start(fname_ini.to_string()),
"*/" => comment_end(input.to_string()),
"A" | "a" => add_a_line(fname_ini.to_string()),
"D" | "d" => delete_lines(fname_ini.to_string()),
"H" | "h" => help(input.to_string()), // theoretically. No help yet
"I" | "i" => insert_comment(fname_ini.to_string()),
"L" | "l" => list(fname_ini.to_string()),
&_ => ret(input) // must be str because
}
main();
}
fn ret(input:&str){
println!("Input is not acceptable {}",&input);
main();
}
fn header(section_header:String,fname_ini:String) {
println!("Header was: {}",section_header);
let f = open_file (fname_ini.to_string());
let prompt:String = "Section header chosen. Please enter the header text: >> ".to_string();
let inp:String =rdl(prompt).trim().to_string(); // .<=to_string()
let mut header = String::new(); // as in the first example.
header.push_str("\n["); // let's add the first piece, "["
header.push_str(inp.as_str()); // let's add the second part, 'inp'
header.push_str("]\n"); // let's add the third part. "]"
println!("Complete header is: {} ",header);
let _res = write_file(header, f);
main();
}
fn item(section_item:String,fname_ini:String){
println!("Item was: {}",section_item);
let f = open_file (fname_ini.to_string()); // getting the section item & value value
let prompt:String = "Section item chosen. Please enter the item name: ".to_string();
let inp_name:String =rdl(prompt).trim().to_string();
let prompt:String = "Section item chosen. Please enter the item value: ".to_string();
let inp_value:String =rdl(prompt).trim().to_string(); // putting together the complete item line
let mut inp_line = String::new();
inp_line.push_str(&inp_name.as_str());
let delim:String=String::from(": ");
inp_line.push_str(&delim);
inp_line.push_str(&inp_value);
let n:String = String::from("\n"); // let's add a NewLine (needing 2 statements!)
inp_line = inp_line + &n;
println!("Complete item line is: {} ",inp_line);
let res = write_file(inp_line.to_string(), f); // adding the item line to the file
main();
}
fn comment_end(com_end:String){
println!("comment_end was: {}",com_end); // if you start program and immediatly end it
}
fn help(cont_hlp:String){
println!("Help was: {}",cont_hlp); // there ought to be some help text but nooo...
}
// just showing the user what the current file looks like by listing it
fn list(fname_ini:String){
let cont_list = std::fs::read_to_string(&fname_ini).expect("Something went wrong reading the file");
let s_slice: &str = &*cont_list;
let v: Vec<&str> = s_slice.split('\n').collect();
let mut l_num:i32 = -1;
print!("{}","\n");
for val in v.iter() {
l_num += 1;
if l_num > 0 {
println!("{} {}",l_num,val);
}
}
return;
}
fn rdl(prompt:String) -> std::string::String {
print!("{}", prompt); // print whatever 'prompt is
io::stdout().flush().unwrap(); // to force printout NOW, otherwise lines will come in wrong order!
let mut st = String::new(); // a string to stuff my input into
match std::io::stdin().read_line(&mut st) {
Ok(_) => (st),
Err(e) => (e.to_string()),
}
}
fn rdl_num(prompt:String,lines:usize) -> usize {
print!("{}: ",prompt);
io::stdout().flush().unwrap();
let mut lnum_try = String::new();
std::io::stdin().read_line(&mut lnum_try);
lnum_try = lnum_try.trim().to_string();
let n = lnum_try.parse::<usize>().unwrap();
return n;
}
fn open_file (fname:String) -> std::fs::File{
/* NOTE: Files are automatically closed when they go out of scope.
"The most general way for your function to use files is to take an open file handle as parameter, as this allows it to also use file handles that are not part of the filesystem." (Though, not to forget, file handles can't be copied or de-referenced!)
*/
let f = OpenOptions::new().read(true).append(true).open(&fname);
let mut f = match f {
Ok(f) => return f,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create(&fname){
Ok(f) => return f,
Err(e) => panic!("Failed creating file 'data' {:?}", e)
}, // one more is coming, a comma
other_err => {
panic!("Failed opening file 'data' {:?}", other_err)
}
}
}; // END of "let f = match f {", a semicolon!
}
fn copy_file(source:String,target:String) -> std::io::Result<()> {
fs::copy(source, target)?; // Copy foo.txt to bar.txt
Ok(())
}
fn write_file(contents:String,mut f:std::fs::File) -> std::io::Result<String> {
f.write_all(contents.as_bytes())?;
f.sync_data().expect("write_file: Failed syncing data");
Ok("OK".to_string())
}
fn delete_file(fname:String) -> std::io::Result<()> {
fs::remove_file(fname)?;
Ok(())
}
fn create_empty(fname:String) -> std::io::Result<()> {
File::create(fname)?;
Ok(())
}
// Getting char after char, checking for "*/" which will end it
fn comment_start(fname_ini:String) {
let fname_slice: &str = &fname_ini;
let mut s = String::new();
let a:&str = "1";
let b:&str = "1";
while a == "1" && b == "1" { // infinite loop
s = t_get(s,fname_slice.to_string()); // getting the next char
if s.len()<1 {
comment_start(fname_slice.to_string());
}
let n1 = s.len()-1;
let ch1 = s.chars().nth(n1).unwrap(); // checking the last char
s=t_get(s,fname_slice.to_string());
if s.len()<1 {
comment_start(fname_slice.to_string());
}
let n2 = s.len()-1;
let ch2 = s.chars().nth(n2).unwrap();
if (ch1 == '*') && (ch2 == '/') {
let s=add_comment_tags(s); // tags are "/*" and "*/"
let f = open_file(fname_slice.to_string());
write_file(s,f);
print!("\n");
list(fname_slice.to_string());
io::stdout().flush().unwrap();
return;
}
if ch2 == '*' { // if the check before misses...
s=t_get(s,fname_slice.to_string());
if s.len()<1 {
comment_start(fname_slice.to_string());
}
let n3 = s.len()-1;
let ch3 = s.chars().nth(n3).unwrap()
if ch3 == '/' { // this will catch it ("*/").
let s=add_comment_tags(s)
let f = open_file(fname_slice.to_string());
write_file(s,f);
list(fname_slice.to_string());
return
}
}
}
}
// shortening comment_start() by not repeating this twice tags are "/*" and "*/"
fn add_comment_tags(mut s:String) -> std::string::String {
let prefix:String = "\n/* ".to_string();
let suffix:String = "\n".to_string();
s = prefix + &s + &suffix;
s
}
// This is the Termios crate (it's name changed for clarity), as it is downloaded
fn getachar() -> [u8;1]{ // result is an ARRAY!
let stdin = 0; // couldn't get std::os::unix::io::FromRawFd to work on /dev/stdin or /dev/tty //
let termios = Termios::from_fd(stdin).unwrap();
let mut new_termios = termios.clone(); // make a mutable copy of termios // that we will modify
new_termios.c_lflag &= !(ICANON | ECHO); // no echo and canonical mode
tcsetattr(stdin, TCSANOW, &mut new_termios).unwrap();
let stdout = io::stdout();
let mut reader = io::stdin();
let mut buffer = [0;1]; // read exactly one byte [into this ARRAY!]
stdout.lock().flush().unwrap();
reader.read_exact(&mut buffer).unwrap();
let c = buffer;
tcsetattr(stdin, TCSANOW, & termios).unwrap(); // reset the stdin to nothing
return c;
}
fn t_get(mut s:String, fname_ini:String) -> String {
// This is a wrapper for Termios, (backspace editing is possible,
// remember, the expanding line is shown to the user, char after char)
// It gets one character to be added to the s:String
let mut raw = getachar(); // get a char
// print!("{}",raw[0]); // if activated, shows dec # of u8
if raw[0] > 127 { // non-ASCII char is beeping
raw = [7; 1];
}
if raw[0] == 127 { // backspace!
if s.len()<1 {
comment_start(fname_ini);
}
s.pop(); // here the backspace is effected. Remove last char
print!(" {}",s); // you'll get the string back minus the last character
return s; // here
};
let mybyte = str::from_utf8(&raw).unwrap();
s.push_str(&mybyte);
let n1 = s.len()-1;
let ch1 = s.chars().nth(n1).unwrap();
io::stdout().flush().unwrap();
s // the String + the new char at the end, returned (if no backspace)
}
/* conceptually this function works like insert_comment() [previously named "comment_start()"]. The file is read in, placed in a vector which is the written back to the original file name. Here, this is done w/o any intermediate file. */
fn add_a_line(fname_ini:String) {
let fname = fname_ini.as_str();
list(fname.to_string());
let cont = std::fs::read_to_string(&fname_ini).expect("Something went wrong reading the file");
cont.trim();
let s_slice: &str = &*cont;
let v_cont: Vec<&str> = s_slice.split('\n').collect(); // to be able to index
let v_cont_lines = v_cont.len()-1;
if v_cont_lines == 0 {
println!("The file is empty. Add at least a [section]!");
main();
}
let mut prompt="Enter line number after which you wish to add a line:
".to_string();
let mut inp:String =rdl(prompt).trim().to_string();
let mut inp_num = check_num(fname_ini.to_string(),inp);
if inp_num =="Bad number".to_string(){ // inp_num should be a number:String
add_a_line(fname.to_string());
}
if inp_num =="".to_string(){ // inp_num is (should be) a a number:String
println!("Input is empty");
add_a_line(fname.to_string());
}
let inp_ix = inp_num.parse::<usize>().unwrap(); // convert it to an usize!
let mut x:usize = 0;
let mut string_cont = String::new();
while x <= inp_ix { // adding the lines to the output file up // to and including the input linenumber
string_cont.push_str(v_cont[x]);
string_cont.push_str("\n");
x += 1;
}
let my_prompt="Enter the line you wish to add. End with <Enter>: ".to_string();
let new_line:String = rdl(my_prompt).trim().to_string();
if new_line =="".to_string() {
println!("Input string is empty");
add_a_line(fname.to_string());
}
let nl = "\n";
string_cont = string_cont + &new_line + &nl; // by adding a &String it becomes a &str
while x > inp_ix && x < v_cont_lines && inp_ix > 1 {
string_cont.push_str(v_cont[x]); // which means this will work! All lines after the
// input line number are added
string_cont.push_str("\n");
x += 1;
}
let bu = ".bu";
let target = fname.to_string() + &bu; // create target name (="<fname>.bu")
copy_file(fname.to_string(),target); // use it for copying
delete_file(fname.to_string()); // delete the original
println!("string_cont:{:?}",string_cont);
let mut f=open_file(fname.to_string());
let mut f = BufWriter::new(f);
f.write_all(string_cont.as_bytes()).expect("Unable to write string_cont1");
f.flush().unwrap(); // necessary, otherwise no output
main();
}
fn delete_lines(fname_ini:String) {
// deletes lines in a file displayed with list()
// input line numbers are withheld when writing it back
// Since all lines corresponding to the input line numbers, are withheld
// they do not have to be sequential.
// a backup file is created and will not be removed afterwards
let v = fname_ini.as_str();
let cont = std::fs::read_to_string(&fname_ini).expect("Something went wrong reading the file");
cont.trim();
let s_slice: &str = &*cont;
let v_cont: Vec<&str> = s_slice.split('\n').collect(); // to be able to index
let v_cont_lines = v_cont.len()-1; // file is now read and stored in a vector
list(fname_ini.to_string());
let prompt="Delete line: Please enter line number(s); Commas in between!: ".to_string();
let mut inp:String =rdl(prompt).trim().to_string(); // line numbers are read
let mut inp_num = check_num(fname_ini.to_string(),inp); // line numbers are rchecked and stored in a vector
let inp_num_slice:&str = &*inp_num;
let v_inp_num:Vec<&str> = inp_num_slice.split(',').collect();
let v_inp_num_len = v_inp_num.len();
let mut result = String::new();
let mut found:bool = false;
let mut x:usize = 0;
while x < v_cont_lines { // for each line in the file... for i in (0..v_inp_num_len) { // the line number is checked against
// each vector item
if &*x.to_string() == v_inp_num[i]{ // dereference bcs left side i usize and right side is &str. This was not the first thing to come to my mind...
found = true;
}
}
if found == false {
result.push_str(v_cont[x]);
result.push_str("\n");
}else{
found = false;
}
x += 1;
}
let bu = ".bu";
let target = fname_ini.to_string() + &bu; // create target name (="<fname>.bu")
copy_file(fname_ini.to_string(),target).expect("Unable to copy file to backup");
delete_file(fname_ini.to_string());
let mut file = open_file(fname_ini.to_string());
file.write_all(result.as_bytes()).expect("Unable to write string_cont1");
file.sync_data().expect("write_file: Failed syncing data");
return
}
fn check_num(fname_ini:String,mut st:String) -> std::string::String{
// checking all characters are numeric:ish. Commas and whitespace are removed.
// If all's good the string is returned, otherwise err message
st.retain(|c| !c.is_whitespace()); //"retain all characters c such that f(c) returns false" =' '. |c|
// defines whatever is tested, followed by a the test
st.trim_matches(','); // trim ev. last ','
let s_slice: &str = &*st;
let v_cont: Vec<&str> = s_slice.split(',').collect();
let v_cont_len = v_cont.len();
let mut x:usize = 0;
let mut s_num = String::new();
let mut p:String = "".to_string();
while x < v_cont_len {
if v_cont[x].to_string().chars().all(char::is_numeric) {
x += 1;
continue;
}else{
print!("This is not a number: {:?}",v_cont[x]);
let prompt=" <Enter> to continue".to_string();
let mut inp:String =rdl(prompt).trim().to_string();
return ("Bad number".to_string());
}
x += 1;
}
st
}
/*The function makes it possible to enter a comment after a specific line number.
It contains the following actions:
Read the file to edit, store it in a vector (to be able to index it).
Make a backup copy of the file. Create/overwrite an empty temp file where the comment will be saved.
Creating a comment with function start_comment(), storing it in the tmp file.
Moving the vector's string elements/lines up to the specific line from the vector to a string
Moving the remaining vector elements to another string
Writing the fist string, the comment and the last string to the original file
Done
*/
fn insert_comment(fname_ini:String){
let cont = std::fs::read_to_string(&fname_ini).expect("Something went wrong reading the file");
let s_slice: &str = &*cont;
let v_cont: Vec<&str> = s_slice.split('\n').collect(); // to be able to index
let lines = v_cont.len()-1;
let fname = fname_ini.as_str(); //String can't be copied, make str for copying
list(fname.to_string()); // showing the file (w. line numbers)
let bu = ".bu";
let tmp = ".tmp";
let s_orig = String::new();
let s_new = String::new();
let n:String = "\n".to_string();
let mut lnum:usize = rdl_num("The line number for the start of your comment: ".to_string(),lines);
if lnum > 0 {
lnum -= 1;
}
let target = fname.to_string() + &bu; // create target name (="<fname>.bu")
let target_slice = target.as_str(); // for using name several times w/o moving
let tmpfile = fname.to_string() + tmp; // create tmp file name
let tmpfile_slice = tmpfile.as_str(); // for using name several times w/o moving
let mut f = create_empty(tmpfile_slice.to_string()); // Creating empty tmp file
copy_file(fname.to_string(),target_slice.to_string()); // copy original to; backup
println!("Backup copy is named {}",target_slice.to_string());
println!("Created working copy {}",tmpfile_slice.to_string());
println!("Enter comment. Enter '*/' to end comment");
comment_start(tmpfile_slice.to_string());
let mut x:usize = 0;
let mut string_cont1 = String::new();
while x <= lnum {
string_cont1.push_str(v_cont[x]);
string_cont1.push_str("\n");
x += 1;
}
let comment = std::fs::read_to_string(tmpfile_slice.to_string()).expect("Something went wrong reading the file");
let mut x = lnum + 1;
let mut string_cont2 = String::new();
while x > lnum && x < lines {
string_cont2.push_str(v_cont[x]);
string_cont2.push_str("\n");
x += 1;
}
string_cont2.trim();
delete_file(fname.to_string());
let mut f=open_file(fname.to_string());
let mut f = BufWriter::new(f);
f.write_all(string_cont1.as_bytes()).expect("Unable to write string_cont1");
f.write_all(comment.as_bytes()).expect("Unable to write comment");
f.write_all(string_cont2.as_bytes()).expect("Unable to write string_cont2");
f.flush().unwrap(); // BufWriter needs this otherwise there may be no output
return
}
And that is the end of the training project. For now. It's one thing to get something working, quite another to do it an elegant fashion :0)