diff --git a/.gitignore b/.gitignore index ea8c4bf..96ef6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 35eb881..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "mkfile" -version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index f58ad1d..4e36e5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,9 @@ [package] name = "mkfile" -version = "0.2.1" -authors = ["Alex Wanderman <58393741+AlexWanderman@users.noreply.github.com>"] +version = "0.3.0" edition = "2021" -description = "Rust CLI app to create text files. No external dependencies." -readme = "readme.md" +authors = ["Alex Wanderman <58393741+AlexWanderman@users.noreply.github.com>"] +description = "CLI app for creating text files (with no external dependencies)." repository = "https://github.com/AlexWanderman/mkfile" license = "MIT" categories = ["command-line-utilities"] -exclude = [".github"] diff --git a/README b/README new file mode 100644 index 0000000..8f12679 --- /dev/null +++ b/README @@ -0,0 +1,55 @@ +# Overview ![Crates.io](https://img.shields.io/crates/v/mkfile) + +Minimal Rust CLI app with no external dependencies. Creates text files. May +create parent directories recursively, override existing files and output +verbosely. Default text for new files supported. + +Install with `cargo install mkfile`. + +# Description + +mkfile \[OPTION\]... PATH... + +Options: +- -d --dry - perform "dry" run, always verbose; +- -v --verbose - print a message for each file; +- -p --parents - create parent directories recursively; +- -o --override - override already existing files; +- -T --text "STRING" - default text for each file; +- --help - display help message and exit; +- --version - display version message and exit. + +# Usage example + +Basic example. Create new file silently. +``` +$ mkfile file.txt +``` + +Create multiple files (with text, verbosely). +``` +$ mkfile file1.txt file2.txt file3.txt -vT "Default text" +/home/user/file1.txt: Created +/home/user/file2.txt: Created +/home/user/file3.txt: Created +``` + +Create file with parent directory (verbosely). +``` +$ mkfile -vp parent/file.txt +/home/user/Documents/Rust/mkfile/parent/file.txt: Created with parent +``` + +Dry run example. Be aware that /root_file.txt will not be created without root privileges. +``` +$ mkfile -d new_dir/file.txt new_file.txt existing_file.txt /root_file.txt +/home/user/new_dir/file.txt: Parent does not exist +/home/user/new_file.txt: To be created +/home/user/existing_file.txt: Already exist +/root_file.txt: To be created +``` + +# TODO + +- Create tests +- chmod parameters diff --git a/readme.md b/readme.md deleted file mode 100644 index fef9f68..0000000 --- a/readme.md +++ /dev/null @@ -1,46 +0,0 @@ -# Overview - -Minimal Rust CLI app with no external dependencies. Creates text files. May -create parent directories recursively, override existing files and output -verbosely. - -![Crates.io](https://img.shields.io/crates/v/mkfile) - -# Description - -mkfile \[OPTION\]... PATH... - -Options: -- -v --verbose - print a message for each file; -- -p --parents - create parent directories recursively; -- -o --override - override already existing files; -- --help - display help message and exit; -- --version - display version message and exit. - -# Usage example - -Create a bunch of files in verbose mode. Some of the files couldn't be created because we didn't include -p (--parent) option to create parent directories. - -``` -$ mkfile -v /file.txt /test/file.txt /home/user/file.txt /home/user/test/file.txt - -/file.txt: Permission denied (os error 13) -/test/file.txt: No such file or directory (os error 2) -/home/user/file.txt: Created -/home/user/test/file.txt: No such file or directory (os error 2) -``` - -We failed to create a file because it already exist, but we didn't include -o (--override) option to override it. - -``` -$ mkfile -v ~/file.txt - -/home/user/file.txt: File already exist -``` - -# TODO - -- Create binary files -- Default text parameter for all files -- Default binary parameter for all files in binary mode -- chmod parameter diff --git a/src/main.rs b/src/main.rs index 5550633..ed31adb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,110 +1,171 @@ -use std::env; -use std::fs; -use std::fs::File; -use std::path::Path; +use std::{env, fs, path::Path}; fn main() { - // Collect args + // Stage 0: args let args: Vec = env::args().collect(); - // Return if no args and print help message - if args.len() <= 1 { - print_help_msg(); - return; - } + // Stage 1: options + let mut do_dry_run = false; + let mut do_verbose_output = false; + let mut do_create_parents = false; + let mut do_overwrite = false; + let mut default_text = String::new(); - // Options and paths - let mut options = Vec::::new(); - let mut options_wrong = Vec::::new(); - let mut paths = Vec::<&Path>::new(); + let mut wrong_options = Vec::::new(); + let mut paths = Vec::::new(); - let mut is_verbose = false; - let mut create_parents = false; - let mut do_override = false; + // Using an iterator to peek the next value inside a loop. Skipping + // the path of the executable (with .next()). + let mut args_iter = args.iter(); + args_iter.next(); - // [1..] since the first arg is the executable path - for arg in &args[1..] { - let a = arg.chars().nth(0); - let b = arg.chars().nth(1); - let c = arg.chars().nth(2); + while let Some(arg) = args_iter.next() { + let options = if arg.chars().all(|x| x.eq(&'-')) { + // - or -- are wrong options + wrong_options.push(arg.to_string()); + continue; + } else if arg.starts_with("--") { + // Single long option + vec![arg.strip_prefix("--").unwrap().to_string()] + } else if arg.starts_with("-") { + // Single or multiple short options + arg.strip_prefix("-") + .unwrap() + .chars() + .map(|x| x.to_string()) + .collect() + } else { + // Not an option + paths.push(arg.to_string()); + continue; + }; - // Long option --option - if a.eq(&Some('-')) && b.eq(&Some('-')) && c.is_some() { - options.push(arg[2..].to_string()); - } - // Short option -o (single) or -vao (multiple) - else if a.eq(&Some('-')) && b.ne(&Some('-')) { - for opt in arg.chars() { - match opt { - '-' => {} - _ => options.push(opt.to_string()), - } - } - } - // Paths - else { - paths.push(Path::new(arg)); - } - } + for option in options { + match option.as_str() { + "d" | "dry" => do_dry_run = true, + "v" | "verbose" => do_verbose_output = true, + "p" | "parents" => do_create_parents = true, + "o" | "overwrite" => do_overwrite = true, + "T" | "text" => { + // Default text was already written + if !default_text.is_empty() { + eprintln!("Error! Multiple default text options are not supported."); + return; + } - // Proceed options - for opt in &options { - match opt.as_str() { - "v" | "verbose" => is_verbose = true, - "p" | "parents" => create_parents = true, - "o" | "override" => do_override = true, - "help" => { - print_help_msg(); - return; - } - "version" => { - print_version_msg(); - return; + let text = args_iter.next(); + + // Default text was not provided + if text.is_none() { + eprintln!("Error! Default text was not provided."); + return; + } + + default_text = text.unwrap().to_string() + "\n"; + } + "help" => { + print_help_msg(); + return; + } + "version" => { + print_version_msg(); + return; + } + _ => wrong_options.push(option), } - _ => options_wrong.push(opt.to_string()), } } - // Output wrong options and return - if !options_wrong.is_empty() { - let opt_list = options_wrong.join(", "); - - if options_wrong.len() == 1 { - eprintln!("Wrong option: {opt_list}"); + if !wrong_options.is_empty() { + if wrong_options.len() == 1 { + let option = wrong_options.first().unwrap(); + eprintln!("Error! Wrong option: {option}."); } else { - eprintln!("Wrong options: {opt_list}"); + let options = wrong_options.join(", "); + eprintln!("Error! Wrong options: {options}."); } + return; + } + // Stage 2: files + if paths.is_empty() { + println!("Warning! No files were provided. To get a hint use --help.\n"); return; } for path in paths { - // Skip file if exist, but do_override is false - if path.exists() && !do_override { - println!("{}: File already exist", path.display()); - continue; + let full_path_str = if path.starts_with("/") { + path.to_string() + } else { + let mut full_path = env::current_dir().unwrap(); + full_path.push(&path); + full_path.display().to_string() + }; + + let full_path = Path::new(&full_path_str); + let full_parent_path = full_path.parent().unwrap_or(Path::new("/")); + + let mut is_overwritten = false; + let mut is_parented = false; + + // Overwritten file + match full_path.try_exists() { + Ok(true) => { + if !do_overwrite { + eprintln!("{}: Already exist", &full_path_str); + continue; + } + is_overwritten = true; + } + Err(e) if do_verbose_output => eprintln!("{}: {}", &full_path_str, e), + _ => {} } - // Create parent directories - if create_parents { - let parent = path.parent().unwrap_or(Path::new("")); - if !parent.exists() { - match fs::create_dir_all(parent) { - Err(e) if is_verbose => { - println!("{}: {}", path.display(), e); - continue; + // New file with parent + match full_parent_path.try_exists() { + Ok(false) => { + if !do_create_parents { + eprintln!("{}: Parent does not exist", &full_path_str); + continue; + } + + if !do_dry_run { + match fs::create_dir_all(&full_parent_path) { + Err(e) if do_verbose_output => eprintln!("{}: {}", &full_path_str, e), + _ => {} } - _ => {} - }; + } + + is_parented = true; } + Err(e) if do_verbose_output => eprintln!("{}: {}", &full_path_str, e), + _ => {} } - // Create files - match File::create(path) { - Ok(_) if is_verbose => println!("{}: Created", path.display()), - Err(e) if is_verbose => println!("{}: {}", path.display(), e), - _ => {} - }; + // New file + if !do_dry_run { + match fs::write(&full_path, &default_text) { + Ok(()) if do_verbose_output => { + let created = if is_overwritten { + "Overwritten" + } else { + "Created" + }; + let parented = if is_parented { "with parent" } else { "" }; + println!("{}: {} {}", &full_path_str, created, parented); + } + Err(e) if do_verbose_output => eprintln!("{}: {}", &full_path_str, e), + _ => {} + } + } else { + let created = if is_overwritten { + "overwritten" + } else { + "created" + }; + let parented = if is_parented { "with parent" } else { "" }; + println!("{}: To be {} {}", &full_path_str, created, parented); + } } } @@ -112,11 +173,13 @@ fn print_help_msg() { println!("Usage: mkfile [OPTION]... PATH..."); println!("Create file(s), if they do not already exist.\n"); println!("Options:"); - println!("-v --verbose print a message for each file"); - println!("-p --parents create parent directories recursively"); - println!("-o --override override already existing files"); - println!(" --help display this help and exit"); - println!(" --version output version information and exit (todo!)"); + println!("-d --dry perform \"dry\" run, always verbose"); + println!("-v --verbose print a message for each file"); + println!("-p --parents create parent directories recursively"); + println!("-o --overwrite overwrite already existing files"); + println!("-T --text \"STRING\" default text for every file"); + println!(" --help display this help and exit"); + println!(" --version output version information and exit"); } fn print_version_msg() {