Contents:
1) The main() function
2) The header function
3) The item function
4) The list function
5) The rdl 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())
}
The "f" is the file handle which contains:
io::stdout().flush().unwrap(); // to stop chars from appearing at arbitrary points i tim :0)
Statements
let n1 = s.len()-1;
let ch1 = s.chars().nth(n1).unwrap();
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.
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().
Inga kommentarer:
Skicka en kommentar