1use crate::bin::Opt;
2use anyhow::{bail, Context, Error};
3#[cfg(target_os = "linux")]
4use nix::fcntl::{fcntl, FcntlArg};
5use std::fs;
6use std::fs::File;
7use std::io::{BufReader, Read, Write};
8#[cfg(target_os = "linux")]
9use std::os::unix::io::AsRawFd;
10use std::path::PathBuf;
11use std::process::{ChildStdin, Command, Output, Stdio};
12use std::str;
13use std::sync::mpsc;
14use std::thread;
15use tempfile::NamedTempFile;
16use thiserror::Error;
17
18#[derive(Debug, Error)]
23enum CtagsError {
24 #[error("failed to execute ctags command ({})\n{}", cmd, err)]
25 ExecFailed { cmd: String, err: String },
26
27 #[error("failed to call ctags command ({})", cmd)]
28 CallFailed { cmd: String },
29
30 #[error("failed to convert to UTF-8 ({:?})", s)]
31 ConvFailed { s: Vec<u8> },
32}
33
34pub struct CmdCtags;
39
40impl CmdCtags {
41 pub fn call(opt: &Opt, files: &[String]) -> Result<Vec<Output>, Error> {
42 let mut args = Vec::new();
43 args.push(String::from("-L -"));
44 args.push(String::from("-f -"));
45 if opt.unsorted {
46 args.push(String::from("--sort=no"));
47 }
48 for e in &opt.exclude {
49 args.push(String::from(format!("--exclude={}", e)));
50 }
51 args.append(&mut opt.opt_ctags.clone());
52
53 let cmd = CmdCtags::get_cmd(&opt, &args);
54
55 let (tx, rx) = mpsc::channel::<Result<Output, Error>>();
56
57 for i in 0..opt.thread {
58 let tx = tx.clone();
59 let file = files[i].clone();
60 let dir = opt.dir.clone();
61 let bin_ctags = opt.bin_ctags.clone();
62 let args = args.clone();
63 let cmd = cmd.clone();
64
65 if opt.verbose {
66 eprintln!("Call : {}", cmd);
67 }
68
69 thread::spawn(move || {
70 let child = Command::new(bin_ctags.clone())
71 .args(args)
72 .current_dir(dir)
73 .stdin(Stdio::piped())
74 .stdout(Stdio::piped())
75 .stderr(Stdio::null())
77 .spawn();
78 match child {
79 Ok(mut x) => {
80 {
81 let stdin = x.stdin.as_mut().unwrap();
82 let pipe_size = std::cmp::min(file.len() as i32, 1048576);
83 let _ = CmdCtags::set_pipe_size(&stdin, pipe_size)
84 .or_else(|x| tx.send(Err(x.into())));
85 let _ = stdin.write_all(file.as_bytes());
86 }
87 match x.wait_with_output() {
88 Ok(x) => {
89 let _ = tx.send(Ok(x));
90 }
91 Err(x) => {
92 let _ = tx.send(Err(x.into()));
93 }
94 }
95 }
96 Err(_) => {
97 let _ = tx.send(Err(CtagsError::CallFailed { cmd }.into()));
98 }
99 }
100 });
101 }
102
103 let mut children = Vec::new();
104 for _ in 0..opt.thread {
105 children.push(rx.recv());
106 }
107
108 let mut outputs = Vec::new();
109 for child in children {
110 let output = child??;
111
112 if !output.status.success() {
113 bail!(CtagsError::ExecFailed {
114 cmd: cmd,
115 err: String::from(str::from_utf8(&output.stderr).context(
116 CtagsError::ConvFailed {
117 s: output.stderr.to_vec(),
118 }
119 )?)
120 });
121 }
122
123 outputs.push(output);
124 }
125
126 Ok(outputs)
127 }
128
129 pub fn get_tags_header(opt: &Opt) -> Result<String, Error> {
130 let tmp_empty = NamedTempFile::new()?;
131 let tmp_tags = NamedTempFile::new()?;
132 let tmp_tags_path: PathBuf = tmp_tags.path().into();
133 tmp_tags.close()?;
136
137 let _ = Command::new(&opt.bin_ctags)
138 .arg(format!("-L {}", tmp_empty.path().to_string_lossy()))
139 .arg(format!("-f {}", tmp_tags_path.to_string_lossy()))
140 .args(&opt.opt_ctags)
141 .current_dir(&opt.dir)
142 .status();
143 let mut f = BufReader::new(File::open(&tmp_tags_path)?);
144 let mut s = String::new();
145 f.read_to_string(&mut s)?;
146
147 fs::remove_file(&tmp_tags_path)?;
148
149 Ok(s)
150 }
151
152 fn get_cmd(opt: &Opt, args: &[String]) -> String {
153 let mut cmd = format!(
154 "cd {}; {}",
155 opt.dir.to_string_lossy(),
156 opt.bin_ctags.to_string_lossy()
157 );
158 for arg in args {
159 cmd = format!("{} {}", cmd, arg);
160 }
161 cmd
162 }
163
164 #[allow(dead_code)]
165 fn is_exuberant_ctags(opt: &Opt) -> Result<bool, Error> {
166 let output = Command::new(&opt.bin_ctags)
167 .arg("--version")
168 .current_dir(&opt.dir)
169 .output()?;
170 Ok(str::from_utf8(&output.stdout)?.starts_with("Exuberant Ctags"))
171 }
172
173 #[cfg(target_os = "linux")]
174 fn set_pipe_size(stdin: &ChildStdin, len: i32) -> Result<(), Error> {
175 fcntl(stdin.as_raw_fd(), FcntlArg::F_SETPIPE_SZ(len))?;
176 Ok(())
177 }
178
179 #[cfg(not(target_os = "linux"))]
180 fn set_pipe_size(_stdin: &ChildStdin, _len: i32) -> Result<(), Error> {
181 Ok(())
182 }
183}
184
185#[cfg(test)]
190mod tests {
191 use super::super::bin::{git_files, Opt};
192 use super::CmdCtags;
193 use std::str;
194 use structopt::StructOpt;
195
196 #[test]
197 fn test_call() {
198 let args = vec!["ptags", "-t", "1", "--exclude=README.md"];
199 let opt = Opt::from_iter(args.iter());
200 let files = git_files(&opt).unwrap();
201 let outputs = CmdCtags::call(&opt, &files).unwrap();
202 let mut iter = str::from_utf8(&outputs[0].stdout).unwrap().lines();
203 assert_eq!(
204 iter.next().unwrap_or(""),
205 "BIN_NAME\tMakefile\t/^BIN_NAME = ptags$/;\"\tm"
206 );
207 }
208
209 #[test]
210 fn test_call_with_opt() {
211 let args = vec!["ptags", "-t", "1", "--opt-ctags=-u"];
212 let opt = Opt::from_iter(args.iter());
213 let files = git_files(&opt).unwrap();
214 let outputs = CmdCtags::call(&opt, &files).unwrap();
215 let mut iter = str::from_utf8(&outputs[0].stdout).unwrap().lines();
216 assert_eq!(
217 iter.next().unwrap_or(""),
218 "VERSION\tMakefile\t/^VERSION = $(patsubst \"%\",%, $(word 3, $(shell grep version Cargo.toml)))$/;\"\tm"
219 );
220 }
221
222 #[test]
223 fn test_call_exclude() {
224 let args = vec![
225 "ptags",
226 "-t",
227 "1",
228 "--exclude=Make*",
229 "--exclude=README.md",
230 "-v",
231 ];
232 let opt = Opt::from_iter(args.iter());
233 let files = git_files(&opt).unwrap();
234 let outputs = CmdCtags::call(&opt, &files).unwrap();
235 let mut iter = str::from_utf8(&outputs[0].stdout).unwrap().lines();
236
237 if CmdCtags::is_exuberant_ctags(&opt).unwrap() {
240 assert_eq!(iter.next().unwrap_or(""), "");
241 } else {
242 assert_eq!(
243 iter.next().unwrap_or(""),
244 "CallFailed\tsrc/cmd_ctags.rs\t/^ CallFailed { cmd: String },$/;\"\te\tenum:CtagsError"
245 );
246 }
247 }
248
249 #[test]
250 fn test_command_fail() {
251 let args = vec!["ptags", "--bin-ctags", "aaa"];
252 let opt = Opt::from_iter(args.iter());
253 let files = git_files(&opt).unwrap();
254 let outputs = CmdCtags::call(&opt, &files);
255 assert_eq!(
256 &format!("{:?}", outputs),
257 "Err(failed to call ctags command (cd .; aaa -L - -f -))"
258 );
259 }
260
261 #[test]
262 fn test_ctags_fail() {
263 let args = vec!["ptags", "--opt-ctags=--u"];
264 let opt = Opt::from_iter(args.iter());
265 let files = git_files(&opt).unwrap();
266 let outputs = CmdCtags::call(&opt, &files);
267 assert_eq!(
268 &format!("{:?}", outputs)[0..60],
269 "Err(failed to execute ctags command (cd .; ctags -L - -f - -"
270 );
271 }
272
273 #[test]
274 fn test_get_tags_header() {
275 let args = vec!["ptags"];
276 let opt = Opt::from_iter(args.iter());
277 let output = CmdCtags::get_tags_header(&opt).unwrap();
278 let output = output.lines().next();
279 assert_eq!(&output.unwrap_or("")[0..5], "!_TAG");
280 }
281}