diff --git a/Cargo.lock b/Cargo.lock index 606a548..74eec9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,17 +1,24 @@ [root] -name = "rusty-cloc" -version = "0.1.0" +name = "tokei" +version = "1.1.0" dependencies = [ - "getopts 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "glob 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "getopts" -version = "0.2.11" +name = "ansi_term" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "clap" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "log 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ansi_term 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "yaml-rust 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -20,15 +27,12 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "libc" -version = "0.1.8" +name = "strsim" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "log" -version = "0.3.1" +name = "yaml-rust" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", -] diff --git a/Cargo.toml b/Cargo.toml index 8dde448..557ac72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,14 @@ [package] -name = "rusty-cloc" -version = "0.1.0" +name = "tokei" +version = "1.1.0" authors = ["Aaronepower "] -[dependencies] -getopts = "0.2" -glob = "*" - [profile.dev] debug = true [profile.release] -opt-level = 3 \ No newline at end of file +opt-level = 3 + +[dependencies] +clap = {version = "*", features = ["yaml"]} +glob = "*" diff --git a/README.md b/README.md index 07d6974..dbdb329 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,67 @@ -# rusty-cloc -A CLOC(Count Lines Of Code) program, written in Rust. +# Tokei +A blazingly fast CLOC(Count Lines Of Code) program, written in Rust. # Options +``` +Aaron P. +A quick CLOC (Count Lines Of Code) tool -`--exclude-dir` exclude one, or more directories from the search. -###### Example -`rusty-cloc --exclude-dir=node_modules` +USAGE: + tokei [FLAGS] [OPTIONS] [--] ... -Will ignore everything within a folder named `node_modules` +FLAGS: + -h, --help Prints help information + -l, --languages prints out supported languages and their extensions + -V, --version Prints version information + +OPTIONS: + -e, --exclude ... Will ignore all files and directories containing the word ie --exclude node_modules + -s, --sort Will sort based on a certain column ie --sort=files will sort by file count. + +ARGS: + input... The input file(s)/directory(ies) + +``` + +# Supported Languages +``` +ActionScript (as) +C (c) +ColdFusion CFScript (cfc) +ColdFusion (cfm) +Clojure (clj) +CoffeeScript (coffee) +C++ (cpp) +C# (cs) +CSS (css) +D (d) +Dart (dart) +LISP (el) +Go (go) +C Header (h) +C++ Header (hpp) +Haskell (hs) +HTML (html) +Java (java) +JavaScript (js) +JSON (json) +JSX (jsx) +Objective-C (m) +Objective-C++ (mm) +Pascal (pas) +PHP (php) +Perl (pl) +Python (py) +R (r) +Ruby (rb) +Ruby HTML (rhtml) +Rust (rs) +Sass (sass) +BASH (sh) +SQL (sql) +Swift (swift) +TypeScript (ts) +XML (xml) +YAML (yml) +``` diff --git a/cli.yml b/cli.yml new file mode 100644 index 0000000..95fc17d --- /dev/null +++ b/cli.yml @@ -0,0 +1,25 @@ +name: Tokei +version: 1.1 +author: Aaron P. +about: A quick CLOC (Count Lines Of Code) tool +args: + - exclude: + short: e + long: exclude + multiple: true + help: Will ignore all files and directories containing the word ie --exclude node_modules + takes_value: true + - sort: + short: s + long: sort + takes_value: true + help: Will sort based on a certain column ie --sort=files will sort by file count. + - input: + index: 1 + multiple: true + required: true + help: The input file(s)/directory(ies) + - languages: + short: l + long: languages + help: prints out supported languages and their extensions diff --git a/src/fsutil.rs b/src/fsutil.rs index 7ba8bde..5c34013 100644 --- a/src/fsutil.rs +++ b/src/fsutil.rs @@ -41,7 +41,7 @@ pub fn contains_comments(file: &str, comment: &str) -> bool { return true } - false + false } pub fn get_all_files(path: String, ignored_directories: &Vec) -> Vec { @@ -49,13 +49,16 @@ pub fn get_all_files(path: String, ignored_directories: &Vec) -> Vec value, + Err(err) => panic!("ERROR: {:?}", err), + }; 'file: for entry in dir { - let entry = entry.unwrap(); + let entry = unwrap_rs_cont!(entry); let file_path = entry.path(); - let file_str = file_path.to_str().unwrap(); + let file_str = unwrap_opt_cont!(file_path.to_str()); let file_string = file_str.to_owned(); - let path_metadata = metadata(&file_string).unwrap(); + let path_metadata = unwrap_rs_cont!(metadata(file_str)); if path_metadata.is_dir() { for ignored_directory in ignored_directories { @@ -74,11 +77,15 @@ pub fn get_all_files(path: String, ignored_directories: &Vec) -> Vec value, + Err(err) => panic!("{:?}", err) + }; + for path_buf in iter { + let file_path = unwrap_opt_cont!(unwrap_rs_cont!(path_buf).as_path().to_str()).to_owned(); files.push(file_path); } } files -} \ No newline at end of file +} diff --git a/src/language.rs b/src/language.rs index 3606872..361bb63 100644 --- a/src/language.rs +++ b/src/language.rs @@ -6,11 +6,12 @@ pub struct Language<'a> { pub multi_line: &'a str, pub multi_line_end: &'a str, pub files: Vec, - pub code: u32, - pub comments: u32, - pub blanks: u32, - pub lines: u32, + pub code: usize, + pub comments: usize, + pub blanks: usize, + pub lines: usize, pub total: usize, + pub size: usize, } impl<'a> Language<'a> { @@ -30,6 +31,71 @@ impl<'a> Language<'a> { blanks: 0, lines: 0, total: 0, + size: 0, + } + } + + pub fn new_c(name: &'a str) -> Language<'a> { + Language { + name: name, + line_comment: "//", + multi_line: "/*", + multi_line_end: "*/", + files: Vec::new(), + code: 0, + comments: 0, + blanks: 0, + lines: 0, + total: 0, + size: 0, + } + } + + pub fn new_html(name: &'a str) -> Language<'a> { + Language { + name: name, + line_comment: "", + files: Vec::new(), + code: 0, + comments: 0, + blanks: 0, + lines: 0, + total: 0, + size: 0, + } + } + + pub fn new_blank(name: &'a str) -> Language<'a> { + Language { + name: name, + line_comment: "", + multi_line: "", + multi_line_end: "", + files: Vec::new(), + code: 0, + comments: 0, + blanks: 0, + lines: 0, + total: 0, + size: 0, + } + } + + pub fn new_single(name: &'a str, line_comment: &'a str) -> Language<'a> { + Language { + name: name, + line_comment: line_comment, + multi_line: "", + multi_line_end: "", + files: Vec::new(), + code: 0, + comments: 0, + blanks: 0, + lines: 0, + total: 0, + size: 0, } } @@ -40,13 +106,11 @@ impl<'a> Language<'a> { impl<'a> fmt::Display for Language<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut total; - - if self.total == 0 { - total = self.files.len() + let total = if self.total == 0 { + self.files.len() } else { - total = self.total; - } - write!(f," {: <15} {: >15} {:>15} {:>15} {:>15} {:>15} ", self.name, total, self.lines, self.blanks, self.comments, self.code) + self.total + }; + write!(f," {: <15} {: >15} {:>15} {:>15} {:>15} {:>15}", self.name, total, self.lines, self.blanks, self.comments, self.code) } -} \ No newline at end of file +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..9219d19 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,19 @@ +#[inline(always)] +macro_rules! unwrap_opt_cont { + ($option:expr) => { + match $option { + Some(result) => result, + None => continue, + } + } +} + +#[inline(always)] +macro_rules! unwrap_rs_cont { + ($result:expr) => { + match $result { + Ok(result) => result, + Err(_) => continue, + } + } +} diff --git a/src/main.rs b/src/main.rs index aa7e519..e3791e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,122 +1,140 @@ -extern crate getopts; +#[macro_use] +extern crate clap; +#[macro_use] +pub mod macros; pub mod language; pub mod fsutil; -use std::env; use std::io::Read; use std::path::Path; use std::fs::File; -use std::collections::HashMap; -use getopts::Options; +use std::collections::BTreeMap; + +use clap::App; + use language::Language; use fsutil::{get_all_files, contains_comments}; fn main() { - let args: Vec = env::args().collect(); - let mut opts = Options::new(); + let yaml = load_yaml!("../cli.yml"); + let matches = App::from_yaml(yaml).get_matches(); - opts.optflag("h", "help", "Print this help menu"); - opts.optopt("", "exclude-dir", - "Example: --exclude-dir=docs", - "\tDirectories wanted to be ignored"); + let mut languages: BTreeMap<&str, Language> = BTreeMap::new(); + languages.insert("as" , Language::new_c("ActionScript")); + languages.insert("c" , Language::new_c("C")); + languages.insert("cs" , Language::new_c("C#")); + languages.insert("clj" , Language::new_single("Clojure", ";,#,#_")); + languages.insert("coffee" , Language::new("CoffeeScript", "#", "###", "###")); + languages.insert("cfm" , Language::new("ColdFusion", "")); + languages.insert("cfc" , Language::new_c("ColdFusion CFScript")); + languages.insert("cpp" , Language::new_c("C++")); + languages.insert("css" , Language::new_c("CSS")); + languages.insert("d" , Language::new_c("D")); + languages.insert("dart" , Language::new_c("Dart")); + languages.insert("go" , Language::new_c("Go")); + languages.insert("h" , Language::new_c("C Header")); + languages.insert("hs" , Language::new_single("Haskell", "--")); + languages.insert("hpp" , Language::new_c("C++ Header")); + languages.insert("html" , Language::new_html("HTML")); + languages.insert("java" , Language::new_c("Java")); + languages.insert("js" , Language::new_c("JavaScript")); + languages.insert("json" , Language::new_blank("JSON")); + languages.insert("jsx" , Language::new_c("JSX")); + languages.insert("el" , Language::new("LISP", ";", "#|", "|#")); + languages.insert("m" , Language::new_c("Objective-C")); + languages.insert("mm" , Language::new_c("Objective-C++")); + languages.insert("php" , Language::new("PHP", "#,//","/*","*/")); + languages.insert("pas" , Language::new("Pascal", "//,(*","{","}")); + languages.insert("pl" , Language::new("Perl", "#","=","=cut")); + languages.insert("py" , Language::new("Python", "#","'''","'''")); + languages.insert("rs" , Language::new("Rust", "//,///,//!", "/*", "*/")); + languages.insert("r" , Language::new("R", "#","","")); + languages.insert("rb" , Language::new("Ruby", "#","=begin","=end")); + languages.insert("rhtml" , Language::new_html("Ruby HTML")); + languages.insert("sass" , Language::new_c("Sass")); + languages.insert("sh" , Language::new_single("BASH", "#")); + languages.insert("sql" , Language::new("SQL", "--", "/*", "*/")); + languages.insert("swift" , Language::new_c("Swift")); + languages.insert("ts" , Language::new_c("TypeScript")); + languages.insert("xml" , Language::new_html("XML")); + languages.insert("yml" , Language::new_single("YAML", "#")); - let matches = opts.parse(&args[1..]).unwrap(); - let mut ignored_directories: Vec = Vec::new(); - ignored_directories.push(".git".to_string()); + if matches.is_present("languages") { + for (ext, language) in languages { + println!("{:<25} ({})", language.name, ext); + } + return; + } - if matches.opt_present("h") { - let brief = format!("Usage: {} [options] [paths]", args[0].clone()); - println!("{}", opts.usage(&brief)); - return; + + let paths = matches.values_of("input").unwrap(); + + let mut ignored_directories: Vec = Vec::new(); + + if let Some(user_ignored) = matches.values_of("exclude") { + for ignored in user_ignored { + ignored_directories.push(ignored.to_owned()); + } } - if matches.opt_present("exclude-dir") { - let exclude_args = matches.opt_str("exclude-dir").unwrap(); - let exclude_vec = exclude_args.split(","); + let mut sort = String::new(); + if let Some(sort_by) = matches.value_of("sort") { + match &*sort_by.to_lowercase() { + "files" | "total" | "blanks" | "comments" | "code" => sort.push_str(&*sort_by.to_lowercase()), + _ => println!("--sort must be any of the following files, total, blanks, comments, code"), + } + } + let sort_empty = sort.is_empty(); - for excluded in exclude_vec { - ignored_directories.push(excluded.to_string()); - } - } - - if matches.free.is_empty() { - println!("ERROR: "); - println!("You must provide a file, or folder path as an argument."); - return; - } - - - let row = "----------------------------------------------------------------------------------------------------"; + let row = "--------------------------------------------------------------------------------------------------"; println!("{}", row); - println!(" {:<15} {:>15} {:>15} {:>15} {:>15} {:>15} ", - "language", "files", "total", "blanks", "comments", "code"); + println!(" {:<15} {:>15} {:>15} {:>15} {:>15} {:>15}", + "Language", "Files", "Total", "Blanks", "Comments", "Code"); println!("{}", row); - let mut languages: HashMap<&str, Language> = HashMap::new(); - languages.insert("cpp" , Language::new("C++", "//","/*","*/")); - languages.insert("hpp" , Language::new("C++ Header", "//","/*","*/")); - languages.insert("c" , Language::new("C", "//","/*","*/")); - languages.insert("h" , Language::new("C Header", "//","/*","*/")); - languages.insert("css" , Language::new("CSS", "//","/*","*/")); - languages.insert("java" , Language::new("Java", "//","/*","*/")); - languages.insert("js" , Language::new("JavaScript", "//","/*","*/")); - languages.insert("rs" , Language::new("Rust", "//","/*","*/")); - languages.insert("xml" , Language::new("XML", "")); - languages.insert("html" , Language::new("HTML", "")); - languages.insert("py" , Language::new("Python", "#","'''","'''")); - languages.insert("rb" , Language::new("Ruby", "#","=begin","=end")); - languages.insert("php" , Language::new("PHP", "#,//","/*","*/")); - - for path in matches.free { - let files = get_all_files(path, &ignored_directories); + for path in paths { + let files = get_all_files(path.to_owned(), &ignored_directories); for file in files { - let extension = match Path::new(&file).extension() { - Some(result) => result.to_str().unwrap(), - None => continue, - }; + let extension = unwrap_opt_cont!(unwrap_opt_cont!(Path::new(&file).extension()).to_str()); - let mut language = match languages.get_mut(extension) { - Some(result) => result, - None => continue, - }; - language.files.push(file.to_string()); + let lowercase: &str = &extension.to_lowercase(); + + let mut language = unwrap_opt_cont!(languages.get_mut(lowercase)); + language.files.push(file.to_owned()); } } - let mut total = Language::new("Total", "", "", ""); + let mut total = Language::new_blank("Total"); - for (_, language) in languages.iter_mut() { + for (_, language) in &mut languages { for file in language.files.iter() { - let mut buffer: Vec = Vec::new(); - let mut file_ref = match File::open(&file) { - Ok(result) => result, - _ => continue, - }; - - let _ = file_ref.read_to_end(&mut buffer); - - let contents = match String::from_utf8(buffer) { - Ok(result) => result, - Err(_) => continue, - }; + let mut file_ref = unwrap_rs_cont!(File::open(&file)); + let mut contents = String::new(); + let _ = unwrap_rs_cont!(file_ref.read_to_string(&mut contents)); + let mut is_in_comments = false; - for line in contents.lines() { + 'line: for line in contents.lines() { let line = line.trim(); language.lines += 1; - if line.starts_with(language.multi_line) { - language.comments += 1; - is_in_comments = true; - } else if contains_comments(line, language.multi_line) { - language.code += 1; - is_in_comments = true; - } + if line.trim().is_empty() { + language.blanks += 1; + continue; + } + if !language.multi_line.is_empty() { + if line.starts_with(language.multi_line) { + is_in_comments = true; + } else if contains_comments(line, language.multi_line) { + language.code += 1; + is_in_comments = true; + } + } if is_in_comments { if line.contains(language.multi_line_end) { @@ -129,16 +147,15 @@ fn main() { for single in single_comments { if line.starts_with(single) { language.comments += 1; - } else if line.trim().is_empty() { - language.blanks += 1; - } else { - language.code += 1; - } - } + continue 'line; + } + } + language.code += 1; } } - if !language.is_empty() { + + if !language.is_empty() && sort_empty { println!("{}", language); } @@ -149,7 +166,35 @@ fn main() { total.code += language.code; } + if !sort_empty { + let mut unsorted_vec:Vec<(&&str, &Language)> = languages.iter().collect(); + match &*sort { + "files" => { + unsorted_vec.sort_by(|a, b| b.1.files.len().cmp(&a.1.files.len())) + }, + "total" => { + unsorted_vec.sort_by(|a, b| b.1.lines.cmp(&a.1.lines)) + }, + "blanks" => { + unsorted_vec.sort_by(|a, b| b.1.blanks.cmp(&a.1.blanks)) + }, + "comments" => { + unsorted_vec.sort_by(|a, b| b.1.comments.cmp(&a.1.comments)) + }, + "code" => { + unsorted_vec.sort_by(|a, b| b.1.code.cmp(&a.1.code)) + }, + _ => unreachable!(), + }; + + for (_, language) in unsorted_vec { + if !language.is_empty() { + println!("{}", language); + } + } + } + println!("{}", row); println!("{}", total); println!("{}", row); -} \ No newline at end of file +}