diff --git a/.clog.toml b/.clog.toml new file mode 100644 index 0000000..ff1f5e6 --- /dev/null +++ b/.clog.toml @@ -0,0 +1,10 @@ +[clog] +repository = "https://github.com/kbknapp/cargo-count" +outfile = "CHANGELOG.md" +from-latest-tag = true + +[sections] +Performance = ["perf"] +Improvements = ["impr", "im", "imp"] +Documentation = ["docs"] +Deprecations = ["depr"] diff --git a/.gitignore b/.gitignore index 37727f9..0fb9d07 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ # Generated by Cargo /target/ + +# temp files +.*~ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7bd72a4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: rust +rust: + - nightly + - beta + - stable +before_script: + - | + pip install 'travis-cargo<0.2' --user && + export PATH=$HOME/.local/bin:$PATH +script: + - | + travis-cargo build && + travis-cargo test + +env: + global: + secure: JLBlgHY6OEmhJ8woewNJHmuBokTNUv7/WvLkJGV8xk0t6bXBwSU0jNloXwlH7FiQTc4TccX0PumPDD4MrMgxIAVFPmmmlQOCmdpYP4tqZJ8xo189E5zk8lKF5OyaVYCs5SMmFC3cxCsKjfwGIexNu3ck5Uhwe9jI0tqgkgM3URA= diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a22d75f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,93 @@ +[root] +name = "cargo-count" +version = "0.1.0" +dependencies = [ + "ansi_term 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "glob 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", + "regex_macros 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "tabwriter 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "aho-corasick" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ansi_term" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "clap" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "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)", +] + +[[package]] +name = "glob" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "regex_macros" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "regex 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "strsim" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "tabwriter" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-width" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..02a012e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "cargo-count" +version = "0.1.0" +authors = ["Kevin K "] + +[dependencies] +clap = "~1.2.1" +glob = "~0.2.10" +tabwriter = "~0.1.23" +regex = "~0.1.41" + +[dependencies.ansi_term] +version = "~0.6.3" +optional = true + +[dependencies.regex_macros] +version = "*" +optional = true + +[features] +default = ["color"] +color = ["ansi_term"] +debug = [] +# unstable = ["regex_macros"] +unstable = [] diff --git a/LICENSE b/LICENSE-MIT similarity index 100% rename from LICENSE rename to LICENSE-MIT diff --git a/README.md b/README.md index ef24bea..4c64a85 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,97 @@ # cargo-count -a cargo subcommand for counting lines of code in Rust (based on rusty-cloc by Aaronepower) +Linux: [![Build Status](https://travis-ci.org/kbknapp/cargo-count.svg?branch=master)](https://travis-ci.org/kbknapp/cargo-count) + +A cargo subcommand for displaying line counts of source code in projects + +## Demo + +Running `cargo count -s , --unsafe-statistics` in the [Rust](https://github.com/rust-lang/rust) repo yields these results: + +``` +Gathering information... + Language Files Lines Blanks Comments Code Unsafe (%) + -------- ----- ----- ------ -------- ---- ---------- + Rust 6,015 527,198 63,300 161,789 302,109 1,162 (0.38%) + CSS 4 1,262 99 490 673 + Python 31 4,797 822 680 3,295 + C 54 9,962 1,154 2,945 5,863 5,836 (99.54%) + C Header 13 1,865 243 650 972 937 (96.40%) + JavaScript 4 1,118 131 142 845 + C++ 4 1,611 185 81 1,345 1,345 (100.00%) + -------- ----- ----- ------ -------- ---- ---------- +Totals: 6,125 547,813 65,934 166,777 315,102 9,280 (2.95%) +``` + +The `-s ,` sets a `,` character as the thousands separator, and `--unsafe-statistics` looks for, and counts `unsafe` blocks. + +## Compiling + +Follow these instructions to compile `cargo-count`, then skip down to Installation. + + 1. Ensure you have current version of `cargo` and [Rust](https://www.rust-lang.org) installed + 2. Clone the project `$ git clone https://github.com/kbknapp/cargo-count && cd cargo-count` + 3. Build the project `$ cargo build --release` + 4. Once complete, the binary will be located at `target/release/cargo-count` + +## Installation and Usage + +All you need to do is place `cargo-count` somewhere in your `$PATH`. Then run `cargo count` anywhere in your project directory. For full details see below. + +### Linux / OS X + +You have two options, place `cargo-count` into a directory that is already located in your `$PATH` variable (To see which directories those are, open a terminal and type `echo "${PATH//:/\n}"`, the quotation marks are important), or you can add a custom directory to your `$PATH` + +**Option 1** +If you have write permission to a directory listed in your `$PATH` or you have root permission (or via `sudo`), simply copy the `cargo-count` to that directory `# sudo cp cargo-count /usr/local/bin` + +**Option 2** +If you do not have root, `sudo`, or write permission to any directory already in `$PATH` you can create a directory inside your home directory, and add that. Many people use `$HOME/.bin` to keep it hidden (and not clutter your home directory), or `$HOME/bin` if you want it to be always visible. Here is an example to make the directory, add it to `$PATH`, and copy `cargo-count` there. + +Simply change `bin` to whatever you'd like to name the directory, and `.bashrc` to whatever your shell startup file is (usually `.bashrc`, `.bash_profile`, or `.zshrc`) + +```sh +$ mkdir ~/bin +$ echo "export PATH=$PATH:$HOME/bin" >> ~/.bashrc +$ cp cargo-count ~/bin +$ source ~/.bashrc +``` + +### Windows + +On Windows 7/8 you can add directory to the `PATH` variable by opening a command line as an administrator and running + +```sh +C:\> setx path "%path%;C:\path\to\cargo-count\binary" +``` + +Otherwise, ensure you have the `cargo-count` binary in the directory which you operating in the command line from, because Windows automatically adds your current directory to PATH (i.e. if you open a command line to `C:\my_project\` to use `cargo-count` ensure `cargo-count.exe` is inside that directory as well). + + +### Options + +There are a few options for using `cargo-count` which should be somewhat self explanitory. + +``` +USAGE: + cargo count [FLAGS] [OPTIONS] [--] [ARGS] + +FLAGS: + -h, --help Prints help information + --ignore Ignore files and streams with invalid UTF-8 + --unsafe-statistics Displays percentages of "unsafe" code + -V, --version Prints version information + -v, --verbose Print verbose output + +OPTIONS: + -l, --language ... The languages to count by file extension (i.e. '-l js py cpp') + -e, --exclude ... Files or directories to exclude + -s, --separator Set the thousands separator for pretty printing + +ARGS: + to_count... The file or directory to count + (defaults to current working directory when omitted) +``` + +## License + +`cargo-count` is released under the terms of either the MIT or Apache 2.0 license. See the LICENSE-MIT or LICENSE-APACHE file for the details. diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..f386b3f --- /dev/null +++ b/src/error.rs @@ -0,0 +1,62 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::fmt::Result as FmtResult; + +use fmt::Format; + +#[derive(Debug)] +#[allow(dead_code)] +pub enum CliError { + Generic(String), + UnknownExt(String), + Unknown +} + +// Copies clog::error::Error; +impl CliError { + /// Return whether this was a fatal error or not. + #[allow(dead_code)] + pub fn is_fatal(&self) -> bool { + // For now all errors are fatal + true + } + + /// Print this error and immediately exit the program. + /// + /// If the error is non-fatal then the error is printed to stdout and the + /// exit status will be `0`. Otherwise, when the error is fatal, the error + /// is printed to stderr and the exit status will be `1`. + pub fn exit(&self) -> ! { + if self.is_fatal() { + wlnerr!("{}", self); + ::std::process::exit(1) + } else { + println!("{}", self); + ::std::process::exit(0) + } + } +} + +impl Display for CliError { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "{} {}", Format::Error("error:"), self.description()) + } +} + +impl Error for CliError { + fn description<'a>(&'a self) -> &'a str { + match *self { + CliError::Generic(ref d) => &*d, + CliError::UnknownExt(ref d) => &*d, + CliError::Unknown => "An unknown fatal error has occurred, please consider filing a bug-report!" + } + } + + fn cause(&self) -> Option<&Error> { + match *self { + CliError::Generic(..) => None, + CliError::UnknownExt(..) => None, + CliError::Unknown => None, + } + } +} diff --git a/src/fmt.rs b/src/fmt.rs new file mode 100644 index 0000000..edf78bd --- /dev/null +++ b/src/fmt.rs @@ -0,0 +1,76 @@ +use std::fmt; + +#[cfg(all(feature = "color", not(target_os = "windows")))] +use ansi_term::Colour::{Red, Green, Yellow}; +#[cfg(all(feature = "color", not(target_os = "windows")))] +use ansi_term::ANSIString; + +#[allow(dead_code)] +pub enum Format { + Error(T), + Warning(T), + Good(T), +} + +#[cfg(all(feature = "color", not(target_os = "windows")))] +impl> Format { + fn format(&self) -> ANSIString { + match *self { + Format::Error(ref e) => Red.bold().paint(e.as_ref()), + Format::Warning(ref e) => Yellow.paint(e.as_ref()), + Format::Good(ref e) => Green.paint(e.as_ref()), + } + } + +} + +#[cfg(all(feature = "color", not(target_os = "windows")))] +impl> fmt::Display for Format { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", &self.format()) + } +} + +#[cfg(any(not(feature = "color"), target_os = "windows"))] +impl Format { + fn format(&self) -> &T { + match *self { + Format::Error(ref e) => e, + Format::Warning(ref e) => e, + Format::Good(ref e) => e, + } + } +} + +#[cfg(any(not(feature = "color"), target_os = "windows"))] +impl fmt::Display for Format { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", &self.format()) + } +} + +pub fn format_number(n: u64, sep: Option) -> String { + debugln!("executing; format_number; n={}", n); + let s = format!("{}", n); + if let Some(sep) = sep { + debugln!("There was a separator {}", sep); + let mut ins_sep = s.len() % 3; + ins_sep = if ins_sep == 0 { 3 } else {ins_sep}; + let mut ret = vec![]; + for (i, c) in s.chars().enumerate() { + debugln!("iter; c={}; ins_sep={}; ret={:?}", c, ins_sep, ret); + if ins_sep == 0 && i != 0 { + debugln!("Inserting the separator"); + ret.push(sep); + ins_sep = 3; + } + ret.push(c); + ins_sep -= 1; + } + debugln!("returning; ret={}", ret.iter().cloned().collect::()); + ret.iter().cloned().collect() + } else { + debugln!("There was not a separator"); + s + } +} \ No newline at end of file diff --git a/src/fsutil.rs b/src/fsutil.rs new file mode 100644 index 0000000..b45d43d --- /dev/null +++ b/src/fsutil.rs @@ -0,0 +1,56 @@ +use std::fs; +use std::fs::metadata; +use std::path::PathBuf; + +use glob; + +pub fn get_all_files<'a>(path: &'a str, exclude: &Vec<&'a str>) -> Vec { + debugln!("executing; get_all_files; path={:?}; exclude={:?};", path, exclude); + let mut files = vec![]; + + debugln!("Getting metadata"); + if let Ok(result) = metadata(&path) { + debugln!("Found"); + if result.is_dir() { + debugln!("It's a dir"); + let dir = fs::read_dir(&path).unwrap(); + 'file: for entry in dir { + let entry = entry.unwrap(); + let file_path = entry.path(); + let file_str = file_path.to_str().expect("file_path isn't a valid str"); + let file_string = file_str.to_owned(); + let path_metadata = metadata(&file_string).unwrap(); + + for ignored in exclude { + debugln!("iter; ignored={:?}", ignored); + if file_str.contains(ignored) { + debugln!("iter; ignored={:?}", ignored); + continue 'file; + } + } + if path_metadata.is_dir() { + for file in get_all_files(&*file_string, &exclude) { + files.push(file); + } + } else if path_metadata.is_file() { + files.push(PathBuf::from(file_str)); + } + } + } else { + debugln!("It's a file"); + if !exclude.contains(&path) { + debugln!("It's not excluded"); + files.push(PathBuf::from(path)); + } else { + debugln!("It's excluded"); + } + } + } else { + for path_buf in glob::glob(&path).ok().expect("failed to get files from glob") { + let file_path = path_buf.unwrap(); + files.push(file_path); + } + } + + files +} \ No newline at end of file diff --git a/src/language.rs b/src/language.rs new file mode 100644 index 0000000..36c9f1c --- /dev/null +++ b/src/language.rs @@ -0,0 +1,269 @@ +use std::fmt as StdFmt; +use std::ops::Deref; +use std::path::PathBuf; + +use fmt; + +#[derive(Debug)] +pub enum Language { + C, + Header, + Hpp, + Cpp, + Css, + Html, + Java, + JavaScript, + Perl, + Php, + Python, + Ruby, + Rust, + Xml, + Toml, + Go +} + +impl Language { + pub fn from_ext(ext: &str) -> Option { + match ext { + "cpp" => Some(Language::Cpp), + "hpp" => Some(Language::Hpp), + "c" => Some(Language::C), + "h" => Some(Language::Header), + "css" => Some(Language::Css), + "java" => Some(Language::Java), + "js" => Some(Language::JavaScript), + "rs" => Some(Language::Rust), + "xml" => Some(Language::Xml), + "html" => Some(Language::Html), + "py" => Some(Language::Python), + "rb" => Some(Language::Ruby), + "php" => Some(Language::Php), + "toml" => Some(Language::Toml), + "pl" => Some(Language::Perl), + "go" => Some(Language::Go), + _ => None + } + } + + pub fn name(&self) -> &str { + match *self { + Language::Cpp => "C++", + Language::Hpp => "C++ Header", + Language::C => "C", + Language::Header => "C Header", + Language::Css => "CSS", + Language::Java => "Java", + Language::JavaScript => "JavaScript", + Language::Rust => "Rust", + Language::Xml => "XML", + Language::Html => "HTML", + Language::Python => "Python", + Language::Ruby => "Ruby", + Language::Php => "PHP", + Language::Toml => "TOML", + Language::Perl => "Perl", + Language::Go => "Go" + } + } + + pub fn extension(&self) -> &str { + match *self { + Language::Cpp => "cpp", + Language::Hpp => "hpp", + Language::C => "c", + Language::Header => "h", + Language::Css => "css", + Language::Java => "java", + Language::JavaScript => "js", + Language::Rust => "rs", + Language::Xml => "xml", + Language::Html => "html", + Language::Python => "py", + Language::Ruby => "rb", + Language::Php => "php", + Language::Perl => "pl", + Language::Toml => "toml", + Language::Go => "go" + } + } + + pub fn is_unsafe(&self) -> bool { + match *self { + Language::C | Language::Cpp | Language::Hpp | Language::Header | + Language::Rust => true, + _ => false + } + } + + pub fn unsafe_keyword(&self) -> Option<&str> { + match *self { + Language::Rust => Some("unsafe"), + _ => None + } + } +} + +impl StdFmt::Display for Language { + fn fmt(&self, f: &mut StdFmt::Formatter) -> StdFmt::Result { + write!(f, "{}", self.name()) + } +} + +pub trait Comment { + type Rep; + fn single(&self) -> Option::Rep>>; + fn multi_start(&self) -> Option<::Rep>; + fn multi_end(&self) -> Option<::Rep>; +} + +impl Comment for Language { + type Rep = &'static str; + + fn single(&self) -> Option::Rep>> { + match *self { + Language::C | + Language::Cpp | + Language::Hpp | + Language::Header | + Language::Css | + Language::Java | + Language::JavaScript | + Language::Rust | + Language::Go => Some(vec!["//"]), + Language::Php => Some(vec!["//", "#"]), + Language::Xml | + Language::Html => Some(vec![""), + Language::Ruby => Some("=end"), + Language::Python => Some("'''"), + Language::Toml | + Language::Perl => None + } + } +} + +#[derive(Debug)] +pub struct Count { + pub lang: Language, + pub files: Vec, + pub code: u64, + pub comments: u64, + pub blanks: u64, + pub lines: u64, + pub usafe: u64, + pub sep: Option, +} + +impl Count { + pub fn new(lang: Language, sep: Option) -> Self { + Count { + lang: lang, + files: vec![], + code: 0, + comments: 0, + blanks: 0, + lines: 0, + usafe: 0, + sep: sep + } + } + + #[allow(dead_code)] + pub fn is_empty(&self) -> bool { + self.code == 0 && self.comments == 0 && self.blanks == 0 && self.lines == 0 + } + + pub fn add_file(&mut self, f: PathBuf) { + self.files.push(f); + } + + pub fn lines(&self) -> String { + fmt::format_number(self.lines, self.sep) + } + + pub fn code(&self) -> String { + fmt::format_number(self.code, self.sep) + } + + pub fn blanks(&self) -> String { + fmt::format_number(self.blanks, self.sep) + } + + pub fn usafe(&self) -> String { + fmt::format_number(self.usafe, self.sep) + } + + pub fn comments(&self) -> String { + fmt::format_number(self.comments, self.sep) + } + + pub fn total_files(&self) -> String { + fmt::format_number(self.files.len() as u64, self.sep) + } + +} + +impl Deref for Count { + type Target = Language; + fn deref(&self) -> &::Target { + &self.lang + } +} + +impl StdFmt::Display for Count { + fn fmt(&self, f: &mut StdFmt::Formatter) -> StdFmt::Result { + write!(f, + "{}\t{}\t{}\t{}\t{}\t{}", + self.lang, + self.total_files(), + 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..c1b9c0d --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,69 @@ +macro_rules! cli_try { + ($t:expr) => ({ + use ::std::error::Error; + match $t { + Ok(o) => o, + Err(e) => return Err(CliError::Generic(e.description().to_owned())) + } + }) +} +macro_rules! wlnerr( + ($($arg:tt)*) => ({ + use std::io::{Write, stderr}; + writeln!(&mut stderr(), $($arg)*).ok(); + }) +); + +macro_rules! werr( + ($($arg:tt)*) => ({ + use std::io::{Write, stderr}; + write!(&mut stderr(), $($arg)*).ok(); + }) +); + +macro_rules! verbose( + ($cfg:ident, $($arg:tt)*) => ({ + if $cfg.verbose { + use std::io::{Write, stdout}; + write!(&mut stdout(), $($arg)*).ok(); + } + }) +); + +macro_rules! verboseln( + ($cfg:ident, $($arg:tt)*) => ({ + if $cfg.verbose { + use std::io::{Write, stdout}; + writeln!(&mut stdout(), $($arg)*).ok(); + } + }) +); + +// #[cfg(not(unstable))] +macro_rules! regex( + ($s:expr) => ({::regex::Regex::new($s).unwrap()}) +); + +#[cfg(feature = "debug")] +macro_rules! debugln { + ($fmt:expr) => (println!(concat!("**DEBUG** ", $fmt))); + ($fmt:expr, $($arg:tt)*) => (println!(concat!("**DEBUG** ",$fmt), $($arg)*)); +} + +#[cfg(feature = "debug")] +macro_rules! debug { + ($fmt:expr) => (print!(concat!("**DEBUG** ", $fmt))); + ($fmt:expr, $($arg:tt)*) => (println!(concat!("**DEBUG** ",$fmt), $($arg)*)); +} + +#[cfg(not(feature = "debug"))] +macro_rules! debugln { + ($fmt:expr) => (); + ($fmt:expr, $($arg:tt)*) => (); +} + +#[cfg(not(feature = "debug"))] +macro_rules! debug { + ($fmt:expr) => (); + ($fmt:expr, $($arg:tt)*) => (); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..407f7d4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,370 @@ +#[macro_use] +extern crate clap; +#[cfg(feature = "color")] +extern crate ansi_term; +extern crate tabwriter; +extern crate glob; +// #![cfg_attr(feature = "unstable", feature(plugin))] +// #![cfg_attr(feature = "unstable", plugin(regex_macros))] +extern crate regex; + + +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; +use std::fs::File; +use std::env; + +use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; +use tabwriter::TabWriter; + +use language::{Language, Count, Comment}; +use error::CliError; +use fmt::Format; + +#[macro_use] +mod macros; +mod fmt; +mod language; +mod fsutil; +mod error; + +type CliResult = Result; + +struct Config<'a> { + verbose: bool, + thousands: Option, + ignore_utf8: bool, + usafe: bool, + exclude: Vec<&'a str>, + exts: Option>, + to_count: Vec +} + +impl<'a> Config<'a> { + fn from_matches(m: &'a ArgMatches<'a, 'a>) -> CliResult { + if let Some(ext_vec) = m.values_of("exts") { + for e in ext_vec { + if let None = Language::from_ext(e) { + return Err(CliError::UnknownExt(format!("unsupported source code extension '{}'", e.to_owned()))) + } + } + } + Ok(Config { + verbose: m.is_present("verbose"), + thousands: m.value_of("sep").map(|s| s.chars().nth(0).unwrap() ), + usafe: m.is_present("unsafe-statistics"), + ignore_utf8: m.is_present("ignore"), + exclude: m.values_of("paths").unwrap_or(vec![".git"]), + to_count: if let Some(v) = m.values_of("to_count") { + debugln!("There are some"); + let mut ret = vec![]; + for p in v { + ret.push(PathBuf::from(p)); + } + debugln!("found files or dirs: {:?}", ret); + ret + } else { + debugln!("There aren't any, using cwd"); + vec![cli_try!(env::current_dir())] + }, + exts: m.values_of("exts") + }) + } +} + +fn main() { + debugln!("executing; cmd=cargo-count; args={:?}", env::args().collect::>()); + let m = App::new("cargo-count") + .version(&*format!("v{}", crate_version!())) + // We have to lie about our binary name since this will be a third party + // subcommand for cargo + .bin_name("cargo") + // Global version uses the version we supplied (Cargo.toml) for all subcommands as well + .settings(&[AppSettings::GlobalVersion, + AppSettings::SubcommandRequired]) + // We use a subcommand because parsed after `cargo` is sent to the third party plugin + // which will be interpreted as a subcommand/positional arg by clap + .subcommand(SubCommand::with_name("count") + .author("Kevin K. ") + .about("Displays line counts of code for cargo projects") + .args_from_usage("-e, --exclude [paths]... 'Files or directories to exclude' + --unsafe-statistics 'Displays percentages of \"unsafe\" code' + --ignore 'Ignore files and streams with invalid UTF-8' + -l, --language [exts]... 'The languages to count by file extension (i.e. \'-l js py cpp\')' + -v, --verbose 'Print verbose output' + [to_count]... 'The file or directory to count{n}\ + (defaults to current working directory when omitted)'") + .arg(Arg::from_usage("-s, --separator [sep] 'Set the thousands separator for pretty printing'") + .validator(single_char))) + .get_matches(); + + if let Some(m) = m.subcommand_matches("count") { + let cfg = Config::from_matches(m).unwrap_or_else(|e| e.exit()); + println!("Gathering information..."); + if let Err(e) = execute(cfg) { + e.exit(); + } + } +} + +fn execute(cfg: Config) -> CliResult<()> { + debugln!("executing; cmd=execute;"); + verboseln!(cfg, "{}: {}", Format::Warning("Excluding"), cfg.exclude.connect(", ")); + verbose!(cfg, "{}", + if cfg.exts.is_some() { + format!("{} including files with extension: {}\n", Format::Warning("Only"), cfg.exts.as_ref().unwrap().connect(", ")) + } else { + "".to_owned() + } + ); + + let mut tw = TabWriter::new(vec![]); + cli_try!(write!(&mut tw, "\tLanguage\tFiles\tLines\tBlanks\tComments\tCode{}\n", if cfg.usafe {"\tUnsafe (%)"} else {""})); + cli_try!(write!(&mut tw, "\t--------\t-----\t-----\t------\t--------\t----{}\n", if cfg.usafe {"\t----------"} else {""})); + + debugln!("Checking for files or dirs to count from cli"); + + let mut langs: Vec = vec![]; + + for path in cfg.to_count { + debugln!("iter; path={:?};", path); + if let Some(f) = path.to_str() { + let files = fsutil::get_all_files(f, &cfg.exclude); + + for file in files { + debugln!("iter; file={:?};", file); + let extension = match Path::new(&file).extension() { + Some(result) => { + if let Some(ref exts) = cfg.exts { + if !exts.contains(&result.to_str().unwrap_or("")) { continue } + } + result.to_str().unwrap() + }, + None => continue, + }; + + debugln!("found extension: {:?}", extension); + if let Some(pos_lang) = Language::from_ext(extension) { + debugln!("Extension is valid"); + let mut found = false; + debugln!("Searching for previous entries of that type"); + for l in langs.iter_mut() { + if l.lang.extension() == extension { + debugln!("Found"); + found = true; + l.add_file(PathBuf::from(&file)); + break; + } + } + if !found { + debugln!("Not found, creating new"); + let mut c = Count::new(pos_lang, cfg.thousands); + c.add_file(PathBuf::from(&file)); + langs.push(c); + } + } else { + debugln!("extension wasn't valid"); + } + } + } else { + debugln!("path couldn't be converted to a str"); + } + + } + + let mut tot: usize = 0; + let mut tot_lines: u64 = 0; + let mut tot_comments: u64 = 0; + let mut tot_blanks: u64 = 0; + let mut tot_code: u64 = 0; + let mut tot_usafe: u64 = 0; + + for count in langs.iter_mut() { + debugln!("iter; count={:?};", count); + let re = if let Some(kw) = count.lang.unsafe_keyword() { + regex!(&*format!("[\\[\\] \\{{\\}}]{}[\\[\\] \\{{\\}}\n]", kw)) + } else { + regex!("") + }; + for file in count.files.iter() { + debugln!("iter; file={:?};", file); + let mut buffer = String::new(); + + let mut file_ref = cli_try!(File::open(&file)); + + if cfg.ignore_utf8 { + if let Err(..) = file_ref.read_to_string(&mut buffer) { + continue + } + } else { + cli_try!(file_ref.read_to_string(&mut buffer)); + } + + let mut is_in_comments = false; + let mut is_in_unsafe = false; + let mut bracket_count: i64 = 0; + + 'new_line: for line in buffer.lines() { + let line = line.trim(); + debugln!("iter; line={:?};", line); + count.lines += 1; + + if is_in_comments { + debugln!("still in comments"); + if line.contains(count.multi_end().unwrap()) { + debugln!("line contained ending comment, stopping comments"); + is_in_comments = false; + } + count.comments += 1; + continue; + } + debugln!("not in comments"); + + if line.trim().is_empty() { + debugln!("line was empty"); + count.blanks += 1; + continue; + } + debugln!("Line isn't empty"); + + if let Some(ms) = count.multi_start() { + debugln!("This file type has a multi start of: {:?}", ms); + if line.starts_with(ms) { + debugln!("line starts with multi comment"); + count.comments += 1; + is_in_comments = line.contains(count.multi_end().unwrap()); + debugln!("line also contained a multi end: {:?}", is_in_comments); + continue; + } else if line.contains(ms) { + debugln!("line contains a multi start"); + count.code += 1; + is_in_comments = line.contains(count.multi_end().unwrap()); + debugln!("line also contained a multi end: {:?}", is_in_comments); + continue; + } + } else { + debugln!("No multi line comments for this type"); + } + debugln!("No multi line comments for this line"); + + if let Some(single_comments) = count.single() { + debugln!("This type has single line comments: {:?}", single_comments); + for single in single_comments { + if line.starts_with(single) { + debugln!("Line started with a comment"); + count.comments += 1; + continue 'new_line; + } else { + debugln!("Line dind't start with a comment"); + } + } + } else { + debugln!("No single line comments for this type"); + } + + if cfg.usafe { + debugln!("Calculating --unsafe-statistics"); + if count.lang.is_unsafe() { + debugln!("The language is not safe"); + if let Some(kw) = count.lang.unsafe_keyword() { + debugln!("There is a keyword: {}", kw); + debugln!("line={:?}", line); + if re.is_match(line) { + debugln!("It contained the keyword; usafe_line={:?}", line); + count.usafe += 1; + let after_usafe = line.split(kw).collect::>()[1]; + debugln!("after_usafe={:?}", after_usafe); + is_in_unsafe = in_unsafe(after_usafe, None); + debugln!("after counting brackets; is_in_unsafe={:?}; bracket_count={:?}", is_in_unsafe, bracket_count); + } else if is_in_unsafe { + debugln!("It didn't contain the keyword, but we are still in unsafe"); + count.usafe += 1; + is_in_unsafe = in_unsafe(line, Some(bracket_count)); + debugln!("after counting brackets; is_in_unsafe={:?}; bracket_count={:?}", is_in_unsafe, bracket_count); + } else { + debugln!("It didn't contain the keyword, and we are not in unsafe"); + } + + if bracket_count < 0 { + debugln!("bracket_count < 0; resetting"); + bracket_count = 0 + } + } else { + debugln!("Language is unsafe, incing the count"); + count.usafe += 1; + } + } + } + count.code += 1; + } + } + + if !cfg.usafe { + cli_try!(write!(&mut tw, "\t{}\n", count)); + } else { + let usafe_per = (count.usafe as f64 / count.code as f64) * 100.00f64; + cli_try!(write!(&mut tw, "\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n", + count.lang.name(), + count.total_files(), + count.lines(), + count.blanks(), + count.comments(), + count.code(), + if usafe_per == 00f64 { "".to_owned() } else { format!("{} ({:.2}%)", count.usafe(), usafe_per) } + )); + } + + tot += count.files.len(); + tot_lines += count.lines; + tot_comments += count.comments; + tot_blanks += count.blanks; + tot_code += count.code; + tot_usafe += count.usafe; + } + + cli_try!(write!(&mut tw, "\t--------\t-----\t-----\t------\t--------\t----{}\n", if cfg.usafe { "\t----------"}else{""})); + cli_try!(write!(&mut tw, "{}\t\t{}\t{}\t{}\t{}\t{}{}\n", + "Totals:", + fmt::format_number(tot as u64, cfg.thousands), + fmt::format_number(tot_lines, cfg.thousands), + fmt::format_number(tot_blanks, cfg.thousands), + fmt::format_number(tot_comments, cfg.thousands), + fmt::format_number(tot_code, cfg.thousands), + if cfg.usafe { + format!("\t{} ({:.2}%)", fmt::format_number(tot_usafe, cfg.thousands), ((tot_usafe as f64 / tot_code as f64) * 100.00f64) as f64) + } else { + "".to_owned() + })); + + cli_try!(tw.flush()); + + verboseln!(cfg, "{} {}", Format::Good("Displaying"), "the results:"); + if tot > 0 { + cli_try!(write!(io::stdout(), "{}", String::from_utf8(tw.unwrap()).ok().expect("failed to get valid utf8 results"))); + } else { + println!("\n\tNo source files were found matching the specified criteria"); + } + + Ok(()) +} + +fn in_unsafe(line: &str, count: Option) -> bool { + let mut b: i64 = count.unwrap_or(0); + for c in line.chars() { + match c { + '{' => b += 1, + '}' => b -= 1, + _ => (), + } + } + + b > 0 +} + +fn single_char(s: String) -> Result<(), String> { + if s.len() != 1 { + Err(format!("the --separator argument option only accepts a single character but found '{}'", Format::Warning(s))) + } else { + Ok(()) + } +} \ No newline at end of file