ptagslib/
cmd_ctags.rs

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// ---------------------------------------------------------------------------------------------------------------------
19// Error
20// ---------------------------------------------------------------------------------------------------------------------
21
22#[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
34// ---------------------------------------------------------------------------------------------------------------------
35// CmdCtags
36// ---------------------------------------------------------------------------------------------------------------------
37
38pub 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::piped()) // Stdio::piped is x2 slow to wait_with_output() completion
76                    .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        // In windiws environment, write access by ctags to the opened tmp_tags fails.
134        // So the tmp_tags must be closed and deleted.
135        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// ---------------------------------------------------------------------------------------------------------------------
186// Test
187// ---------------------------------------------------------------------------------------------------------------------
188
189#[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        // Exuberant Ctags doesn't support Rust ( *.rs ).
238        // So the result becomes empty when 'Makefile' is excluded.
239        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}