Translate

2021-03-17

Learning Rust. Part 6. Training project, editor. Add external crate. Iter.


How to add a new external crate:
https://doc.rust-lang.org/cargo/guide/dependencies.html


 The second function, Items

Since the second function ('fn section_item') doesn't contain much new compared with the first ('fn section_header') I think won't need many comments. There's one thing though...
To stop the program from exiting after each function, I call 'main()' at the very end. This statement is now added to the first function also.  

There's a risk here: the first instance of the script calling the next and so on. I haven't fully checked that yet, I haven't made a "cargo release" yet. We'll see later.

The second function looks like this:

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 & item value
    let mut prompt:String = "Section item chosen. Please enter the item name: >>".to_string();
    let inp_name:String =rdl(prompt).trim().to_string();
    let mut prompt:String = "Section item chosen. Please enter the item text: ";  
    let inp_value:String =rdl(prompt).trim().to_string();
    // putting together the complete item line

    // constructing the output
    let mut inp_line = String::new();

    inp_line.push_str(&inp_name.as_str());    // text from console, item name
    let delim:String=String::from(": ");
    inp_line.push_str(&delim);                           // add delimiter
    inp_line.push_str(&inp_value);                    // text from console, item 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);
    let res = match res {
        Ok(res) => res,
        Err(error) => error.to_string(),
    }
    main();
}

Console example running the whole application:

Enter something: [[
I didn't expect that to work! [[
Header was: [[
Section header chosen. Please enter the header text: >> test1
Complete header is:  
[test1]
 
Enter something: [
Item was: [
Section item chosen. Please enter the item name: >>item3
Section item chosen. Please enter the item name text: >>whatever
Complete item line is: item3: whatever

File ed.ini contents:

___________            // space here

[test]
item0: foo
item1: bar

[test1]
item3: whatever
-----------            // end here


The sixth function, List

This will read the .ini file and print the lines, numbered. It will make it easier to do rudimentary
editing later. The green statements are the new things I've not touched on earlier, pointing to contents
by dereferencing ('*') and creating an .iter of the vector and then iterating over it ("for val in...").

fn list(fname_ini:String){
    let  cont_list = std::fs::read_to_string(&fname_ini).expect("Something went wrong reading the file");

    /* to construct a vector I need a slice (type=&str) instead of a String. Since each element must be
    a whole line I split the whole slice at each ' \n'.
    &*cont_list=points to cont_list&*cont_list=points to cont_list contents */
    let s_slice: &str = &*cont_list;
    let v: Vec<&str> = s_slice.split('\n').collect()   // splitting on '\n' and storing lines in a vector
                                                                                     // to deal with vector elements, use  'v.iter()'
    let mut l_num:i32 = -1;                                         // the line number, initalized, signed number
    for val in v.iter() {
        l_num += 1;
        if l_num > 0 {
            println!("{} {}", l_num, val);
        }
    }
    main();
}

I've added a couple of lines before the statement "let input=rdl("Enter something: ".to_string());"
in main(), to show a little help:
  " let msg = "\n[[=header, [=item, /*=start comment, */=end comment, l=list file".to_string();
    println!("{}", msg);"

snkdb@pf-ZBook-15:~/rust/projects/mutability_moving_borrowing/srccargo run ed.ini

[[=header, [=item, /*=start comment, */=end comment, l=list file
Enter something: l
1 [test]
2 item0: foo
3 item1: bar
4  
5 [test1]
6 item3: whatever

[[=header|[=item|/*=start comment|*/=end comment|l=list file
Enter something: ^C
snkdb@pf-ZBook-15:~/rust/projects/mutability_moving_borrowing/src$

2021-03-16

Learning Rust. Part 4. Training project, editor. match, string manipulation. Command line arguments



TIP: To test code one can use the ambitious https://play.rust-lang.org/ . However, a *much faster* and practical solution is to add an extra project (suggestion: call it "playground") in which you develop the code. When everything works, copy it to the project src directory containing the "real" project. This is the solution I ended up using.

TIP: https://www.linuxjournal.com/content/getting-started-rust-working-files-and-doing-file-io
seems to be a good page explaining, as stated  in the URL, file i/o!

To stop getting warnings about unused imports and dead code, place #![allow(dead_code)] and
#![allow(unused)] in the absolute beginning of the code, before any 'use' statements. Cleanup when code is finished is a recommendation :0).

Having many modules in a script, getting an error, at least I want to know *what script* failed. To do that one can save the file name of the script running. like:

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

}

I've not analyzed the ins and outs of this function so I won't comment on it for now. But if a part of it is used like :

let fname= std::env::current_exe();
println!("{:?}",fname);

you'll get something like this:

Ok("/home/snkdb/rust/projects/ed/target/debug/ed")

The reason is that the result is not just the filename as a string it's an: 

enum Result<T, E> {
   Ok(T),
   Err(E),
}

And the "Ok(/home/....)" is the Ok[T] part. To get the result as a string I have to write:
let fname= std::env::current_exe().unwrap();


println! will then display :
"/home/snkdb/rust/projects/ed/target/debug/ed"
which was the thing I wanted. 

One other way is to use 'match' (below, reading from the keyboard): 

     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()),
    }

The 'st3' will be delivered as the result and the separation of the result from the 'enum' is accomplished.
Those two variants are standard ways to get the result. There are more ways and one of the the things that differentiate them is how and when to display the error message. 
If I got this right, an error here would result in a panic, displaying the 'e' message. To avoid panics and let the caller of the function handle the error, other methods must be used. More on that later (I hope).


Using the function rdl() (from https://a-zproj.blogspot.com/2021/03/began-to-learn-rust.html) when reading from the keyboard I will enter things that the script need, to decide what the next step will be.

Let's have a look at what code I currently have:

fn main() {
    let input=rdl("Enter something: ".to_string());       // reading from the keyboard
    println!("{:?}",input);    
}

fn rdl(prompt:String) -> std::string::String {
    print!("{}", prompt);                // print whatever 'prompt is
    io::stdout().flush().unwrap();
    let mut st3 = String::new();      // a string to stuff my input in
    match  std::io::stdin().read_line(&mut st3) {
        Ok(_) => (st3),                        // if OK this is the result, the String
        Err(e) => (e.to_string()),
    }
}

So; I have a program reading from the keyboard, displaying the input. Not much. Besides, if I enter '[[' it will print "[[\n". After some browsing I came up with a function that removes "\n". Let's add that:

 fn main() {
    let fname= std::env::current_exe().unwrap();        // not doing much just now
    let input=rdl("Enter something: ".to_string());       // reading from the key board
    let mut input=input.trim_end_matches('\n');
    println!("{:?}",input)    
}

I'm thinking about making this into a very basic .ini-file editor. That was the first thing i created programming in Visual Basic 3.0 27 years ago :0)

Let's set up some rules about things I want to be able to create in my .ini file:
        if I as the first argument enter "[[" it will be a section header
        if I enter "[" it will be a section item
        if I enter "/*" it will be a comment
        if I enter "*/ it will be the end of the comment
        if I enter "h" or "H" some help would appear
        if I enter "l" or "L" the file will be listed on the console
 At least 6 different choices must be made to activate (probably) as many different functions.

One way to do that is using "if blabla== whatever", sorry, I meant 'if "[[" == input{' but I was defeated  in my ongoing war against the compiler. Then I found that using 'match' was a much more compact way of doing the same. After applying that method, winning that round  (Yay!), I tested the "if..." variant again and it worked. 

This is not an uncommon experience when trying to learn Rust (for me personally, at least). You try what you think will work, you are presented with a lot of exotic hurdles, give up after some wrestling,  try something else that mysteriously work and at the end, again testing what didn't work before, finding it mysteriously works. Phew!
 A typical 'hurdle' is something along the lines of : 'expected one of `,`, `.`, `?`, `}`, or an operator'.' If you're trying to learn Rust I bet you've met that bastard...
'
Back to the script, the main() function  has an 'match' statement added. NOTE: the last line, starting with '&_' must exist. This is where you should end up when none of the other conditions are true! at least the compiler says so. We'll see in the long run.... Also a function to execute, in each case a conditional statement ends up being true, are added last. They just print something encouraging..

fn main() {
    let fname= std::env::current_exe().unwrap();
    let input=rdl("Enter something: ".to_string());
    let input=input.trim_end_matches('\n').to_string();
    if "[[" == input{
        println!("I didn't expect that to work! {}", input);   // :0)
    }

    match input.as_str() {
        "[[" => header(input.to_string()),        // function, below, verifying it works
        "[" =>  item(input.to_string()),            // etc.
        "/*" => comment_start(input.to_string()),
        "*/" => comment_end(input.to_string()),
        "H" | "h" => help(input.to_string()),
        "L" | "l" => help(input.to_string()),
        &_ => println!("Input is not acceptable {}",input.to_string())    // must have, == everything else
        }
}

// Added needed functions to check it works:
fn header(section_header:String){
    println!("Header was: {}",section_header);
}
fn item(section_item:String){
   println!("Item was: {}",section_item);
}
fn comment_start(com_strt:String){
   println!("Com_strt was: {}",com_strt);
}
fn comment_end(com_end:String){
   println!("Com_end was: {}",com_end);
}
fn help(cont_hlp:String){
    println!("Help was: {}",cont_hlp);
}
fn list(cont_lst:String){
    println!("List was: {}",cont_lst);
}

Now I have six functions that will force me to learn some more Rust, no doubt..
The first one (fn header) will have to do the following:
  • Present a new prompt
  • accept a string as input
  • add "[[" to that input
  • add the input to that and
  • add the "]]" at the end
  • open the file, the .ini-file
  • write it to the .ini-file.
Seeing that we need to open a file, the lazy programmer will pick that function from the first example in this series. Let's add it and display what we've got so far (the whole shebang!):

use std::fs::{File,OpenOptions};
use std::io::{self, Write, Error,ErrorKind};
use std::result::Result;

 #[allow(dead_code)]
 #[allow(unused)]
fn main() {
    let fname_own= std::env::current_exe().unwrap();    // changed, doesn't matter for the moment
    let fname_ini="/home/snkdb/rust/projects/ed/src/ed.ini";    // this is the file we're working on
    let input=rdl("Enter something: ".to_string());
    let input=input.trim_end_matches('\n').to_string();
    if "[[" == input{
        println!("I didn't expect that to work! {}", input);
    }

    match input.as_str() {
        "[[" => header(input.to_string(),fname_ini.to_string()),  // this points to the fn 'header'
        "[" =>  item(input.to_string()),
        "/*" => comment_start(input.to_string()),
        "*/" => comment_end(input.to_string()),
        "H" | "h" => help(input.to_string()),
        "L" | "l" => help(input.to_string()),
        &_ => println!("Input is not acceptable {}",input.to_string())
        }
}


fn header(section_header:String,fname_ini:String){
    println!("Header was: {}",section_header);
    
}
fn item(section_item:String){
   println!("Item was: {}",section_item);
}
fn comment_start(com_strt:String){
   println!("Com_strt was: {}",com_strt);
}
fn comment_end(com_end:String){
   println!("Com_end was: {}",com_end);
}
fn help(cont_hlp:String){
    println!("Help was: {}",cont_hlp);
}
fn list(cont_lst:String){
    println!("List was: {}",cont_lst);
}

fn rdl(prompt:String) -> std::string::String {
    print!("{}", prompt);                // print whatever 'prompt is
    io::stdout().flush().unwrap();
    let mut st3 = String::new();    // a string to stufff 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".
        Worth noting is that file handles can't be copied or dereferenced */
    let f = OpenOptions::new().read(true).append(true).open(&fname);
    let 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!
}


The first function. Header

OK so good so far. Lets concentrate on the first one (fn header):
It looks like:
    fn header(section_header:String,fname_ini:String){
    println!("Header was: {}",section_header);
    
}
The 'section_header:String' is our input from the first prompt, the '[['. Since the first example,
https://a-zproj.blogspot.com/2021/03/began-to-learn-rust.html
we have a piece of code getting input from the console, let's use it. We also have to supply a filename as a String and this was the reason for renaming 'fname' to fname_own' in the beginning. Let's add a statement opening the file:
    fn header(section_header:String,fname_ini:String){
    println!("Header was: {}",section_header);
    let mut f = open_file (fname_ini.to_string());
}

Then we'd like to chat with the user, we need some text to enter into the .ini-file header we're writing. We're lucky, the function getting things read from the console already exists. Let's use it for sending a prompt to rdl() and to get the answer:
    fn header(section_header:String,fname_ini:String){
    println!("Header was: {}",section_header);
    let mut f = open_file (fname_ini.to_string());
    let prompt:String = "Header chosen. Please enter the header text: >> ".to_string();
    let inp:String =rdl(prompt).to_string();
}

There's one thing I'd like to do here. Eventual white spaces ought to be removed. after battling with the documentation I came up with this:
   fn header(section_header:String,fname_ini:String){
    println!("Header was: {}",section_header);
    let mut f = open_file (fname_ini.to_string());
    let prompt:String = "Header chosen. Please enter the header text: >> ".to_string();
    let inp:String =rdl(prompt).trim().to_string();  // several trim_ functions exist. This trims the ends
}

To make header complete, I'd like to add ']]' at the end of it, before writing.
To that end we need to use some simple and user freindly statements (sigh):
Lets add them:
   fn header(section_header:String,fname_ini:String){
        println!("Header was: {}",section_header);
        let mut f = open_file (fname_ini.to_string());    // is "mut" really needed? Not sure
        let prompt:String = "Header chosen. Please enter the header text: >> ".to_string();
        let inp:String =rdl(prompt).trim().to_string();  

        let mut header = String::new();                    // because it will change!
        header.push_str(section_header.as_str());   // lets add the first piece, "[[". Push_str needs                                                                                               // "as_str" format 
        let header_postfix="]]".to_string();
        header.push_str(inp.as_str());                      // let's add the second part, 'inp'
        header.push_str(&header_postfix);             // lets add the third part. "]]".

        let n:String = String::from("\n");                // let's add a NewLine (needing 2 statements!)
        header = header + &n;
 
    println!("Complete header is: {}: ", header); // printing this: "Complete header is: [[test]]:"
}

Trying "section_header.push_str(inp.as_str()).push_str(inp.as_str(&header_postfix)))' may seem tempting but it doesn't work. The 'push_str()' does not refer to the 'section_header', the "header" is a new variable used to assemble the text written to disk. Since the header is complete I want to write it to my file 'fname_ini' (with some error checking)

  fn header(section_header:String,fname_ini:String){
    println!("Header was: {}",section_header);
    let mut f = open_file (fname_ini.to_string());
    let prompt:String = "Header chosen. Please enter the header text: >> ".to_string();
    let inp:String =rdl(prompt).trim().to_string();
  
    let mut header = String::new();                           // as in the first Learning Rust-example. 
    header.push_str(section_header.as_str());           // lets add the first piece, "[["
    header.push_str(inp.as_str());                              // let's add the second part, 'inp'
    let header_postfix="]]".to_string();
    header.push_str(&header_postfix);                      // lets add the third part. "]]".
    let n:String = String::from("\n");                         // let's add a NewLine (needing 2 statements!)
    header =  String::from("\n") + &header + &n; 
    /* solving copy problem of 'n' (see below)
    header =  n + &header + &n; will produce the error: ----- move occurs because `n` has type `std::string::String`, which does not implement the `Copy` trait */

    let res = write_file(header.to_string(), f);
    let res = match res {
        Ok(res) => res,
        Err(error) => error.to_string(),
    };
}

OK, we have a header function that is working. My primary goal here is to take the shortest route to working functions. Functionality beyond that will have to wait, for the moment. when the applikation is run, this is the console output:

Enter something: [[
I didn't expect that to work! [[
Header was: [[
Header chosen. Please enter the header text: >> test
Complete header is: [test]

after repeated use the file ed.ini will look like:
[test]
[test]
[test]
[test]


2021-03-01

Learning Rust. The start.

 


                                        Learning Rust

"Learning Rust" refers to myself. It's my learning process, my inner dialouge.

For different reasons I chose to learn Rust. It's a steep learning curve, it's not like other languages I've used or come in contact with.

Since I'm not taking a course, used to be learning my skills by myself, I thought I'd do it the same way I always do, read the documentation and test things, gaining my knowledge gradually, as I went along.

This is the one time I feel I could have had some use of a course. Everything is weird. My first thought was that, maybe this is not something to recommend to those who are new to programming. On the other hand, I carry a lot of ideas in my backpack of Life, about how programming should work and maybe that's why it is so hard. Maybe people, new to programming concepts, are those who would master this language most easily. Always two sides to the coin.

Anyway, I decided to take notes of every stage on my journey in a blog post format. This will inevitably make my first post most useless, because getting my first program working, it would probably contain mistakes and misconceptions that I was not aware of at all. If I'm right in this, an eventual reader have to read post after post, gradually coming to realize, as I did, that I really didn't had a clue to what I was doing, getting the program to work.

OK, that's how things are . I write. Maybe somebody reads, who am I to know.

The Rust symbol image preceding these blog posts is the one used in the documentation when examples are shown of something that doesn't work.  I bet a Rust learner will visit a lot of those.