Translate

2021-04-06

Learning Rust. Part 10. Training project, editor. Many of the functions, commented + *current* code, complete (no, not "finished")


Contents:

1) The main() function

2) The header function

3) The item function

4) The list function

5) The rdl function

6) The open_file function

7) The write_file function

8) The comment_start function

I've read a lot since the last post about the editor "project". It's time to have a look.
It's very basic code since it's all about training.

That said, my idea was to use a main() as a multiplexer. Defined commands are meant to be matched against a number of functions doing the separate operations on the text, needed.  The  file to be created is like a ".ini"-file of the old type, well known from Windows. These file contain the following separate parts:

[header name]                        section header
item1 name: value                  section item
.
.
item(n) name: value               last item in section

[next header name]                next section header
other item: value                    etc. etc.

Also, it's advantageous to be able to add comments.

Defined commands are:

        "[[" => the text entered will be a header name
        "[" =>  the text entered will be a item name. A second intput will be the value 
        "/*" => the text entered will be a comment
        "*/" => when entered, it will end the comment
        "H" | "h" => will display some (currently not produced)
        "L" | "l" => will display the current file

To be able to react on the comment_X commands, I imported an external crate from https://crates.io/
that is "The Rust community’s crate registry" (like Google Play :). I wanted to be able to get character after character from the keyboard.
"The termios API is decades old." it's said by the author on the GitHub site. It's imported by editing the Cargo.toml file for this project, adding
 
[dependencies]
termios = "0.3"

to the file. When starting the program via "cargo run", the file will be fetched from the repository and the program will start (or fail). I have made no analysis of the contents of the crate, it's a black box delivering an array of the type [u8; 1], one u8 ( = The 8-bit unsigned integer type.). The "1" is telling us that the array contains exactly one element, an u8.

Let's have a look at the main() function. 

The main() function

fn main() {
    let fname_own= std::env::current_exe().unwrap(); // unused. Not implemented yet
    let fname_ini="/home/snkdb/rust/projects/playground/src/ed.ini";
    let msg = "\n[[=header|[=item|/*=start comment|*/=end comment|l=list file".to_string();
    println!("{}", msg);

    let input=rdl("Enter something: ".to_string());
    let input=input.trim_end_matches(' ');    // "input" will be a 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()),
        "H" | "h" => help(input.to_string()),
        "L" | "l" => list(fname_ini.to_string()),
//        "E" | "e" => edline(fname_ini.to_string()), // under development!
        &_ => ret(input)
        }

}

fn ret(input:&str){
    println!("Input is not acceptable {}",&input);
    main();
}

The first four lines ( with the exception of number one) contains the address to the file to be created [fname_ini], a short line helping the user to choose command to enter [msg] and the statement that prints the message.

The next two lines reads the input from the keyboard and trims unnecessary spaces from the input. The "input" from the keyboard will be compared to the commands and in each case call the corresponding function will be called. 

The last line is the case where none of the input matches the available commands and to get a meaningful response another function is called to deliver that. Rust as a standard expects "()" as a returned value meaning "nothing" which isn't very helpful. As an argument to that function is the input. that i may be displayed to the user.

Next on the menu the function to be called if "[[" is entered at the prompt in main()

The header function

fn header(section_header:String,fname_ini:String) {
    let f = open_file (fname_ini.to_string());            // call the open_file function, get file handle back (f)
    let prompt:String = "Section header chosen. Please enter the header text: >> ".to_string();
    let inp:String =rdl(prompt).trim().to_string();  // expecting user input (header name)

    let mut header = String::new();           // a String to save the input
    header.push_str("\n[");                        // let's add the first piece, "[" to the beginning of the header
    header.push_str(inp.as_str());              // let's add the second part, the user's 'inp', (header name)
    header.push_str("]\n");                         // let's add the third part. "]", the end of the header
    println!("Complete header is: {} ",header); // showing the complete result to the user

    let _res = write_file(header.to_string(), f);    // writing header to disk
    main();                                                       // here we leave the scope ={...} and the file will be closed.

}

Some things are given at this point. The call to let f = open_file (fname_ini.to_string()); needs a ".to_string" because the function is defined to need a String as argument. The result that come back (f, the file handle) is defined as "std::fs::File" and that is later used in a write_file statement.

The same goes for let inp:String =rdl(prompt).trim().to_string();, though the result is defined to be a String. The "read_line()" statment in the function automatically adds a NewLine which is not wanted. To get rid of it, ".trim()" is added. 

The three "header.push_str"-statements adds the three parts of the header in the correct order. "main();" restarts the program and gets us back to the prompt for the next action. 

The next function is "item()". after the section header comes the section items. The have name and values, both entered i the same function. This function will ask for the item name <Enter< and the
item value <Enter>, the write them to disk

The item function

fn item(section_item:String,fname_ini:String){
    let f = open_file (fname_ini.to_string());    // files are closed as soon as a we leave a scope = {...}
    
    // getting the section item & item 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 text: >>".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());    // to push things onto a String, they need to be "as_str"    
    inp_line.push_str(": ");                                // a char string like "something" is per definiton a &str
    inp_line.push_str(&inp_value);                   // since it refers to a &str it is a &str, push works

    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();

}
These statements requires an explanation. 

let n:String = String::from("\n");
inp_line = inp_line + &n;

For some reason, to add two or more strings together to a longer string (concatenation), the first may be a string but the next ones must be references to strings (string_a + &string_b). If you want to add a NewLine (\n) to an existing string inp_line you must first create a "n:String" containing "\n", then add that to the "inp_line" like this:
"let new_string:String = inp_line + &n".

If you'd like a NewLine also before your input_line it will become:
"let inp_line:String = n + &inp_line + &n". 

It seems to me that this method is adequate when one of the strings i a variable of unknown size. In the case you know what you'd like to push onto a string, you can add the "\n" directly before use as in the statement from the header function: "header.push_str("\n[");".

Next function:

fn comment_end(com_end:String){
   println!("comment_end was: {}",com_end);
}

This is just an acknowledgement of the fact that "*/" was sent from the keyboard before any action was taken. The user stopped the program without entering any command. It's a stop block, so to speak.

Next function:

fn help(cont_hlp:String){
    println!("Help was: {}",cont_hlp);
}

This just a place holder. No help text is currently available. When it is, the help action will reside here.

The next function is the "List" function.

The list function

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);
        }
    }
    main();
}

The argument is the filename, used in the first line that reads the whole file into a String, "cont_list". (I really have to put some work into my error handling. On error, the user doesn't get much help.)
I need to split cont_list into strings because I want to present them numbered. The edit function currently doesn't exist, I really don't know what it will contain but at least it should be possible to enter a line number to indicate which line is going o be edited. Then this function may be a part of "Edit".

To do that I need to split cont_list at each NewLine. To automatically set linenumbers it would me convenient to use something like an array index. I can think of two ways, arrays an vectors. If using arrays, I must know the length of the string first, that's doable, the create an array of that dimension. The length of an array can't be changed, it's not very flexible, nor very string oriented. I will use a vector since there is a special instruction to split lines before insertion into a vector.

let s_slice: &str = &*cont_list; will read the whole of cont_list. The "&" references (points to) it, the "*" is a de-reference that points to the contents.

Next I need a vector, creating it like let v: Vec<&str> and filling it by = s_slice.split('\n').collect();, that splits the string into strings at each point where a "\n" is found and then send it into my vector by .collect().

Now I need an line number. There is a method, using v[nfn 

] but that include finding the vector length and then incrementing the index. It seems easier to use the next() method where the for <some starting point in v.iter() will do that work for me. The next() is implicit using that. I create a separate line number, l_num:i32 setting it to -1 (let mut l_num:i32 = -1), incrementing it before each use.

Using the for method I walk through the vector, incrementing the line number and printing the line number plus the string, which is what I was after

Next, the two functions I've already discussed in earlier posts:

The rdl function


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 st3 = String::new();    // a string to stuff my input in. It will change, so "mut"
    match  std::io::stdin().read_line(&mut st3) {
        Ok(_) => (st3),
        Err(e) => (e.to_string()),
    }
}

The open_file function

fn open_file (fname:String) -> std::fs::File{
    // NOTE: Files are automatically closed when they go out of scope.
    // But: "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"

    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!
}


Next a function to be implemented (not checked in this context), the idea is you should be able to enter the path to the file you'r operating on when starting the app.
It will be incorporated later but I have to learn more things first... The "|" is used in "closures" and I'm not on that level. Yet.

fn get_exec_name() -> Option<String> {
    std::env::current_exe()
        .ok()
        .and_then(|pb| pb.file_name().map(|s| s.to_os_string()))
        .and_then(|s| s.into_string().ok())
}

Next the function use to write back the file to disk. The error handling should be more detailed than this but it will work. I sync the data to disk so I will know it's there before eventually doing some more editing.

The write_file function

 fn write_file(contents:String, mut f:std::fs::File)  -> std::io::Result<String> {
    f.write_all(contents.as_bytes())?;        // "?" means "If error, I'll return
    Ok("OK".to_string())                           // if not an error, caller can check for "OK" in return
 }

The "f" is the file handle which contains:
    File {
        fd: 3,                                                      // some arbitrary number assigned by the system
        path: "/something/someting else/.../filename",
        read: true,                                            // depends on rdl()  and OpenOptions()
        write: true,                                           // depends on rdl()  and OpenOptions()
    }
"f" is an argument to the function and emanates originally from rdl() and is an argument to all functions doing anything with the file on disk.

Next, the function that can create a comment. I thought I'f have special action codes, telling the program what to do next. They are shown in main(), like [[, [, L|l, H|h etc. The problem with comments is that eventually you want to finish a sentence with a NewLine and this will result in the command execution due to how the system works. I need to take a look at each character sent from the keyboard to stop this from happening, before the system get a chance to act on them. For that puspose i imported an external crate, Termios. Comments about how to do that is in the post titled "Learning Rust. Part 6. Training project, editor. Add external crate. Iter." This crate is the last one in this post. Before it comes the next homegrown function:

The comment_start function

fn comment_start(fname_ini:String) {
    let mut s = String::new();
    let a:&str = "1";
    let b:&str = "1";

    while a == "1" && b == "1" {
        s=t_get(s);
        let n1 = s.len()-1;
        let ch1 = s.chars().nth(n1).unwrap();
        print!("{}",ch1);
        io::stdout().flush().unwrap();

        s=t_get(s);
        let n2 = s.len()-1;
        let ch2 = s.chars().nth(n2).unwrap();
       print!("{}",ch2);
        io::stdout().flush().unwrap();
        if (ch1 == '*') && (ch2 == '/') {
            print!("\n\n{}",s);
        list(fname_ini.to_string());
        io::stdout().flush().unwrap();
        exit(0);
        }
        
        if ch2 == '*' {
            s=t_get(s);
            let n3 = s.len()-1;
            let ch3 = s.chars().nth(n3).unwrap();
            print!("{}",ch3);
            io::stdout().flush().unwrap();
            if ch3 == '/' {
            list(fname_ini.to_string());

            exit(0);
            }
        }
    }
}

This function begins with a declaration of a string "s", let mut s = String::new(); What's special with strings is, that they seem to survive being used in more than one scope ( moved beyond { } ). Since most problems I have had up to now are concerned with scopes and lifetimes, this is interesting. After declaring this string I use it in the "while" loop, and an "if" conditional without problems. 
One could argue: how do I know that? aren't you creating a new s for each of the following function parts? Answer: comment out the declaration and the compiler won't recognize the s in the paragraph beginning with "if ch2 == '*' {"

This seems to be tied to the way strings are stored in memory and that in it's turn is also (probably) tied to what you can "Copy" and not. The next post will deal with some of this. 

OK, back to work. There are also two &str slices declared for one sole purpose; to create a infinite loop  let a:&str = "1";     let b:&str = "1"; . This should have been done with the loop command but I didn't know that then. To start with, i totally missed that these variables wasn't accessible in the loop at all. That's what you get, being used to a language like PHP. Anyhow, the idea was from the beginning that this function should exit from any of the three blocks following the while statement so it didn't matter.

    while a == "1" && b == "1" {              // the infinite loop
        s = t_get(s);                                          // get 1 character from Termios. It's an array [u8, 1]
        let n1 = s.len()-1;                                  // Length is 1 and I need number 0 (*
        let ch1 = s.chars().nth(n1).unwrap();  // My char is the last byte
        print!("{}",ch1);                                   // informative for the user
        println!(" {} ", s);
        io::stdout().flush().unwrap();               // to stop chars from appearing at arbitrary points i tim :0)

(*  Termios declares an array of u8, and the number of elements is 1 => [u8; 1]. 
Now let's see what happens with the string "s".
 
Statements
s = t_get(s); 
 let n1 = s.len()-1;
 let ch1 = s.chars().nth(n1).unwrap(); 
occur in three places, every time a char is fetched. If I add the grayed out print statement to each just to get a picture of "s " whenever a new char has been fetched (entering abcde... at the keyboard) I get this result:

Enter something: /*
a a  
b ab  
c abc  
d abcd  
e abcde

To the left, the character entered, to the right the growing string "s". "s" grows because of how the function t_get() works and hopefully, when the comment function ends, "s" could be delivered to the caller in some way. This also shows that I have to use "let n1 = s.len()-1;" instead of just using s.chars().nth(0).unwrap(); which will work only when fetching the first character.

So, why are there three seemingly equal parts. The thing that decides that is I chose a two character code to end the comment action, " */ ", In part two I compare the first and the second characters. If they are "*/" then I quit but if the second would be "*" and I only had two parts, I would miss the command. I need one more to get the chance to compare. If the third char is "/" I will be able to catch it. If not, then it all starts over and more characters are accepted. 

When ending I could now send "s" to the write_file() function and be done. To be able to do that, however, I must either change the arguments to this function, adding the file descriptor since the write_file() needs it, OR I could do an open_file() in *this* function and send it to write_file(). 

Before I end this post, let's print all the present code, making it possible to copy:

use std::fs::{File,OpenOptions};
use std::io::{self, Read, Write, ErrorKind};
use std::process::exit;
use std::str;
use termios::{Termios, TCSANOW, ECHO, ICANON, tcsetattr};


fn main() {
    let fname_own= std::env::current_exe().unwrap(); // unused. Not implemented yet
    let fname_ini="/home/snkdb/rust/projects/playground/src/ed.ini";
    let msg = "\n[[=header|[=item|/*=start comment|*/=end comment|l=list file".to_string();
    println!("{}", msg);
    let input=rdl("Enter something: ".to_string());
    
    // trim_end_matches(' ') returns a string slice (&str) By default there is a 
    // newline character  at the end of the string 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()),
        "H" | "h" => help(input.to_string()),
        "L" | "l" => list(fname_ini.to_string()),
//        "E" | "e" => edline(fname_ini.to_string()), // under development!
        &_ => ret(input)    // must be str because 
        }
}

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());
//    println!("f: {:#?}",f);
    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); // printing this: "Complete header is: [test]:"
    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());
    // geting the section item & item 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 text: >>".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);
    // adding the item line to the file
    let res = write_file(inp_line.to_string(), f);
    main();
}


fn comment_end(com_end:String){
   println!("comment_end was: {}",com_end);
}
fn help(cont_hlp:String){
    println!("Help was: {}",cont_hlp);
}
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);
        }
    }
    main();
}
/*
fn edline(fname_ini:String){    // under development!
    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 lines = v.len()-1;
    let v_iter = v.iter();

    print!("Edit which line?: 1 - {}",lines);
    io::stdout().flush().unwrap();
    let mut st3 = String::new();    // a string to stufff my input in
    std::io::stdin().read_line(&mut st3).expect("edline: Failed reading from keyboard"); 
    exit;

}
*/

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 st3 = String::new();      // a string to stuff my input in
    match  std::io::stdin().read_line(&mut st3) {
        Ok(_) => (st3),
        Err(e) => (e.to_string()),
    }

}

fn open_file (fname:String) -> std::fs::File{
    // NOTE: Files are automatically closed when they go out of scope.
    // But: "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"
    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 get_exec_name() -> Option<String> {
    std::env::current_exe()
        .ok()
        .and_then(|pb| pb.file_name().map(|s| s.to_os_string()))
        .and_then(|s| s.into_string().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 comment_start(fname:String,line:u32) {
 fn comment_start(fname_ini:String) {
    let mut s = String::new();
    let a:&str = "1";
    let b:&str = "1";

    while a == "1" && b == "1" {
        s = t_get(s);
        let n1 = s.len()-1;
        let ch1 = s.chars().nth(n1).unwrap();
        print!("{}",ch1);
        io::stdout().flush().unwrap();

        s=t_get(s);
        let n2 = s.len()-1;
        let ch2 = s.chars().nth(n2).unwrap();
        print!("{}",ch2);
        io::stdout().flush().unwrap();
        if (ch1 == '*') && (ch2 == '/') {
            print!("\n\n{}",s);
        list(fname_ini.to_string());
        io::stdout().flush().unwrap();
        exit(0);
        }
        
        if ch2 == '*' {
            s=t_get(s);
            let n3 = s.len()-1;
            let ch3 = s.chars().nth(n3).unwrap();
            print!("{}",ch3);
            io::stdout().flush().unwrap();
            if ch3 == '/' {
            list(fname_ini.to_string());

            exit(0);
            }
        }
    }
}


// This is the Termios crate (with it's name chancged for clarity), as it is downloaded
fn getachar() ->  [u8;1]{      // resutl 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 
                                                                                            // original termios data
            return c;


}

fn t_get(mut s:String) -> String {
        let mut raw = getachar();
        if raw[0] > 127 {                                // checking this is an ASCII character
            raw = [7; 1];                                   // if not and Beep is enabled, it will beep (even in in the finished comment!?)

        }
        let  mybyte = str::from_utf8(&raw).unwrap();
        s.push_str(&mybyte);
        s                                                           // the string with the new char added returned here
}

To be continued...

Inga kommentarer:

Skicka en kommentar