ptagslib/
bin.rs

1use crate::cmd_ctags::CmdCtags;
2use crate::cmd_git::CmdGit;
3use anyhow::{Context, Error};
4use dirs;
5use serde_derive::{Deserialize, Serialize};
6use std::fs;
7use std::io::BufRead;
8use std::io::{stdout, BufWriter, Read, Write};
9use std::path::PathBuf;
10use std::process::Output;
11use std::str;
12use structopt::{clap, StructOpt};
13use structopt_toml::StructOptToml;
14use time::{Duration, Instant};
15use toml;
16
17// ---------------------------------------------------------------------------------------------------------------------
18// Options
19// ---------------------------------------------------------------------------------------------------------------------
20
21#[derive(Debug, Deserialize, Serialize, StructOpt, StructOptToml)]
22#[serde(default)]
23#[structopt(name = "ptags")]
24#[structopt(long_version = option_env!("LONG_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")))]
25#[structopt(setting = clap::AppSettings::AllowLeadingHyphen)]
26#[structopt(setting = clap::AppSettings::ColoredHelp)]
27pub struct Opt {
28    /// Number of threads
29    #[structopt(short = "t", long = "thread", default_value = "8")]
30    pub thread: usize,
31
32    /// Output filename ( filename '-' means output to stdout )
33    #[structopt(short = "f", long = "file", default_value = "tags", parse(from_os_str))]
34    pub output: PathBuf,
35
36    /// Search directory
37    #[structopt(name = "DIR", default_value = ".", parse(from_os_str))]
38    pub dir: PathBuf,
39
40    /// Show statistics
41    #[structopt(short = "s", long = "stat")]
42    pub stat: bool,
43
44    /// Filename of input file list
45    #[structopt(short = "L", long = "list")]
46    pub list: Option<String>,
47
48    /// Path to ctags binary
49    #[structopt(long = "bin-ctags", default_value = "ctags", parse(from_os_str))]
50    pub bin_ctags: PathBuf,
51
52    /// Path to git binary
53    #[structopt(long = "bin-git", default_value = "git", parse(from_os_str))]
54    pub bin_git: PathBuf,
55
56    /// Options passed to ctags
57    #[structopt(short = "c", long = "opt-ctags", number_of_values = 1)]
58    pub opt_ctags: Vec<String>,
59
60    /// Options passed to git
61    #[structopt(short = "g", long = "opt-git", number_of_values = 1)]
62    pub opt_git: Vec<String>,
63
64    /// Options passed to git-lfs
65    #[structopt(long = "opt-git-lfs", number_of_values = 1)]
66    pub opt_git_lfs: Vec<String>,
67
68    /// Verbose mode
69    #[structopt(short = "v", long = "verbose")]
70    pub verbose: bool,
71
72    /// Exclude git-lfs tracked files
73    #[structopt(long = "exclude-lfs")]
74    pub exclude_lfs: bool,
75
76    /// Include untracked files
77    #[structopt(long = "include-untracked")]
78    pub include_untracked: bool,
79
80    /// Include ignored files
81    #[structopt(long = "include-ignored")]
82    pub include_ignored: bool,
83
84    /// Include submodule files
85    #[structopt(long = "include-submodule")]
86    pub include_submodule: bool,
87
88    /// Validate UTF8 sequence of tag file
89    #[structopt(long = "validate-utf8")]
90    pub validate_utf8: bool,
91
92    /// Disable tags sort
93    #[structopt(long = "unsorted")]
94    pub unsorted: bool,
95
96    /// Glob pattern of exclude file ( ex. --exclude '*.rs' )
97    #[structopt(short = "e", long = "exclude", number_of_values = 1)]
98    pub exclude: Vec<String>,
99
100    /// Generate shell completion file
101    #[structopt(
102        long = "completion",
103        possible_values = &["bash", "fish", "zsh", "powershell"]
104    )]
105    pub completion: Option<String>,
106
107    /// Generate configuration sample file
108    #[structopt(long = "config")]
109    pub config: bool,
110}
111
112// ---------------------------------------------------------------------------------------------------------------------
113// Functions
114// ---------------------------------------------------------------------------------------------------------------------
115
116macro_rules! watch_time (
117    ( $func:block ) => (
118        {
119            let beg = Instant::now();
120            $func;
121            Instant::now() - beg
122        }
123    );
124);
125
126pub fn git_files(opt: &Opt) -> Result<Vec<String>, Error> {
127    let list = CmdGit::get_files(&opt)?;
128    let mut files = vec![String::from(""); opt.thread];
129
130    for (i, f) in list.iter().enumerate() {
131        files[i % opt.thread].push_str(f);
132        files[i % opt.thread].push_str("\n");
133    }
134
135    Ok(files)
136}
137
138pub fn input_files(file: &String, opt: &Opt) -> Result<Vec<String>, Error> {
139    let mut list = Vec::new();
140    if file == &String::from("-") {
141        let stdin = std::io::stdin();
142        for line in stdin.lock().lines() {
143            list.push(String::from(line?));
144        }
145    } else {
146        for line in fs::read_to_string(file)?.lines() {
147            list.push(String::from(line));
148        }
149    }
150
151    let mut files = vec![String::from(""); opt.thread];
152
153    for (i, f) in list.iter().enumerate() {
154        files[i % opt.thread].push_str(f);
155        files[i % opt.thread].push_str("\n");
156    }
157
158    Ok(files)
159}
160
161fn call_ctags(opt: &Opt, files: &[String]) -> Result<Vec<Output>, Error> {
162    Ok(CmdCtags::call(&opt, &files)?)
163}
164
165fn get_tags_header(opt: &Opt) -> Result<String, Error> {
166    Ok(CmdCtags::get_tags_header(&opt).context("failed to get ctags header")?)
167}
168
169fn write_tags(opt: &Opt, outputs: &[Output]) -> Result<(), Error> {
170    let mut iters = Vec::new();
171    let mut lines = Vec::new();
172    for o in outputs {
173        let mut iter = if opt.validate_utf8 {
174            str::from_utf8(&o.stdout)?.lines()
175        } else {
176            unsafe { str::from_utf8_unchecked(&o.stdout).lines() }
177        };
178        lines.push(iter.next());
179        iters.push(iter);
180    }
181
182    let mut f = if opt.output.to_str().unwrap_or("") == "-" {
183        BufWriter::new(Box::new(stdout()) as Box<dyn Write>)
184    } else {
185        let f = fs::File::create(&opt.output)?;
186        BufWriter::new(Box::new(f) as Box<dyn Write>)
187    };
188
189    f.write(get_tags_header(&opt)?.as_bytes())?;
190
191    while lines.iter().any(|x| x.is_some()) {
192        let mut min = 0;
193        for i in 1..lines.len() {
194            if opt.unsorted {
195                if !lines[i].is_none() && lines[min].is_none() {
196                    min = i;
197                }
198            } else {
199                if !lines[i].is_none()
200                    && (lines[min].is_none() || lines[i].unwrap() < lines[min].unwrap())
201                {
202                    min = i;
203                }
204            }
205        }
206        f.write(lines[min].unwrap().as_bytes())?;
207        f.write("\n".as_bytes())?;
208        lines[min] = iters[min].next();
209    }
210
211    Ok(())
212}
213
214// ---------------------------------------------------------------------------------------------------------------------
215// Run
216// ---------------------------------------------------------------------------------------------------------------------
217
218pub fn run_opt(opt: &Opt) -> Result<(), Error> {
219    if opt.config {
220        let toml = toml::to_string(&opt)?;
221        println!("{}", toml);
222        return Ok(());
223    }
224
225    match opt.completion {
226        Some(ref x) => {
227            let shell = match x.as_str() {
228                "bash" => clap::Shell::Bash,
229                "fish" => clap::Shell::Fish,
230                "zsh" => clap::Shell::Zsh,
231                "powershell" => clap::Shell::PowerShell,
232                _ => clap::Shell::Bash,
233            };
234            Opt::clap().gen_completions("ptags", shell, "./");
235            return Ok(());
236        }
237        None => {}
238    }
239
240    let files;
241    let time_git_files;
242    if let Some(ref list) = opt.list {
243        files = input_files(list, &opt).context("failed to get file list")?;
244        time_git_files = Duration::seconds(0);
245    } else {
246        time_git_files = watch_time!({
247            files = git_files(&opt).context("failed to get file list")?;
248        });
249    }
250
251    let outputs;
252    let time_call_ctags = watch_time!({
253        outputs = call_ctags(&opt, &files).context("failed to call ctags")?;
254    });
255
256    let time_write_tags = watch_time!({
257        let _ = write_tags(&opt, &outputs)
258            .context(format!("failed to write file ({:?})", &opt.output))?;
259    });
260
261    if opt.stat {
262        let sum: usize = files.iter().map(|x| x.lines().count()).sum();
263
264        eprintln!("\nStatistics");
265        eprintln!("- Options");
266        eprintln!("    thread    : {}\n", opt.thread);
267
268        eprintln!("- Searched files");
269        eprintln!("    total     : {}\n", sum);
270
271        eprintln!("- Elapsed time[ms]");
272        eprintln!("    git_files : {}", time_git_files.whole_milliseconds());
273        eprintln!("    call_ctags: {}", time_call_ctags.whole_milliseconds());
274        eprintln!("    write_tags: {}", time_write_tags.whole_milliseconds());
275    }
276
277    Ok(())
278}
279
280#[cfg_attr(tarpaulin, skip)]
281pub fn run() -> Result<(), Error> {
282    let cfg_path = match dirs::home_dir() {
283        Some(mut path) => {
284            path.push(".ptags.toml");
285            if path.exists() {
286                Some(path)
287            } else {
288                None
289            }
290        }
291        None => None,
292    };
293
294    let opt = match cfg_path {
295        Some(path) => {
296            let mut f =
297                fs::File::open(&path).context(format!("failed to open file ({:?})", path))?;
298            let mut s = String::new();
299            let _ = f.read_to_string(&mut s);
300            Opt::from_args_with_toml(&s).context(format!("failed to parse toml ({:?})", path))?
301        }
302        None => Opt::from_args(),
303    };
304    run_opt(&opt)
305}
306
307// ---------------------------------------------------------------------------------------------------------------------
308// Test
309// ---------------------------------------------------------------------------------------------------------------------
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use std::path::Path;
315
316    #[test]
317    fn test_run() {
318        let args = vec!["ptags"];
319        let opt = Opt::from_iter(args.iter());
320        let ret = run_opt(&opt);
321        assert!(ret.is_ok());
322    }
323
324    #[test]
325    fn test_run_opt() {
326        let args = vec!["ptags", "-s", "-v", "--validate-utf8", "--unsorted"];
327        let opt = Opt::from_iter(args.iter());
328        let ret = run_opt(&opt);
329        assert!(ret.is_ok());
330    }
331
332    #[test]
333    fn test_run_fail() {
334        let args = vec!["ptags", "--bin-git", "aaa"];
335        let opt = Opt::from_iter(args.iter());
336        let ret = run_opt(&opt);
337        assert_eq!(
338            &format!("{:?}", ret)[0..42],
339            "Err(failed to get file list\n\nCaused by:\n  "
340        );
341    }
342
343    #[test]
344    fn test_run_completion() {
345        let args = vec!["ptags", "--completion", "bash"];
346        let opt = Opt::from_iter(args.iter());
347        let ret = run_opt(&opt);
348        assert!(ret.is_ok());
349        let args = vec!["ptags", "--completion", "fish"];
350        let opt = Opt::from_iter(args.iter());
351        let ret = run_opt(&opt);
352        assert!(ret.is_ok());
353        let args = vec!["ptags", "--completion", "zsh"];
354        let opt = Opt::from_iter(args.iter());
355        let ret = run_opt(&opt);
356        assert!(ret.is_ok());
357        let args = vec!["ptags", "--completion", "powershell"];
358        let opt = Opt::from_iter(args.iter());
359        let ret = run_opt(&opt);
360        assert!(ret.is_ok());
361
362        assert!(Path::new("ptags.bash").exists());
363        assert!(Path::new("ptags.fish").exists());
364        assert!(Path::new("_ptags").exists());
365        assert!(Path::new("_ptags.ps1").exists());
366        let _ = fs::remove_file("ptags.bash");
367        let _ = fs::remove_file("ptags.fish");
368        let _ = fs::remove_file("_ptags");
369        let _ = fs::remove_file("_ptags.ps1");
370    }
371
372    #[test]
373    fn test_run_config() {
374        let args = vec!["ptags", "--config"];
375        let opt = Opt::from_iter(args.iter());
376        let ret = run_opt(&opt);
377        assert!(ret.is_ok());
378    }
379}