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#[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 #[structopt(short = "t", long = "thread", default_value = "8")]
30 pub thread: usize,
31
32 #[structopt(short = "f", long = "file", default_value = "tags", parse(from_os_str))]
34 pub output: PathBuf,
35
36 #[structopt(name = "DIR", default_value = ".", parse(from_os_str))]
38 pub dir: PathBuf,
39
40 #[structopt(short = "s", long = "stat")]
42 pub stat: bool,
43
44 #[structopt(short = "L", long = "list")]
46 pub list: Option<String>,
47
48 #[structopt(long = "bin-ctags", default_value = "ctags", parse(from_os_str))]
50 pub bin_ctags: PathBuf,
51
52 #[structopt(long = "bin-git", default_value = "git", parse(from_os_str))]
54 pub bin_git: PathBuf,
55
56 #[structopt(short = "c", long = "opt-ctags", number_of_values = 1)]
58 pub opt_ctags: Vec<String>,
59
60 #[structopt(short = "g", long = "opt-git", number_of_values = 1)]
62 pub opt_git: Vec<String>,
63
64 #[structopt(long = "opt-git-lfs", number_of_values = 1)]
66 pub opt_git_lfs: Vec<String>,
67
68 #[structopt(short = "v", long = "verbose")]
70 pub verbose: bool,
71
72 #[structopt(long = "exclude-lfs")]
74 pub exclude_lfs: bool,
75
76 #[structopt(long = "include-untracked")]
78 pub include_untracked: bool,
79
80 #[structopt(long = "include-ignored")]
82 pub include_ignored: bool,
83
84 #[structopt(long = "include-submodule")]
86 pub include_submodule: bool,
87
88 #[structopt(long = "validate-utf8")]
90 pub validate_utf8: bool,
91
92 #[structopt(long = "unsorted")]
94 pub unsorted: bool,
95
96 #[structopt(short = "e", long = "exclude", number_of_values = 1)]
98 pub exclude: Vec<String>,
99
100 #[structopt(
102 long = "completion",
103 possible_values = &["bash", "fish", "zsh", "powershell"]
104 )]
105 pub completion: Option<String>,
106
107 #[structopt(long = "config")]
109 pub config: bool,
110}
111
112macro_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
214pub 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#[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}