mdbook/preprocess/
cmd.rs

1use super::{Preprocessor, PreprocessorContext};
2use crate::book::Book;
3use crate::errors::*;
4use log::{debug, trace, warn};
5use shlex::Shlex;
6use std::io::{self, Read, Write};
7use std::process::{Child, Command, Stdio};
8
9/// A custom preprocessor which will shell out to a 3rd-party program.
10///
11/// # Preprocessing Protocol
12///
13/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
14/// execute the shell command `$cmd supports $renderer`. If the renderer is
15/// supported, custom preprocessors should exit with a exit code of `0`,
16/// any other exit code be considered as unsupported.
17///
18/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
19/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
20/// should then "return" a processed book by printing it to `stdout` as JSON.
21/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
22/// to parse the input provided by `mdbook`.
23///
24/// Exiting with a non-zero exit code while preprocessing is considered an
25/// error. `stderr` is passed directly through to the user, so it can be used
26/// for logging or emitting warnings if desired.
27///
28/// # Examples
29///
30/// An example preprocessor is available in this project's `examples/`
31/// directory.
32#[derive(Debug, Clone, PartialEq)]
33pub struct CmdPreprocessor {
34    name: String,
35    cmd: String,
36}
37
38impl CmdPreprocessor {
39    /// Create a new `CmdPreprocessor`.
40    pub fn new(name: String, cmd: String) -> CmdPreprocessor {
41        CmdPreprocessor { name, cmd }
42    }
43
44    /// A convenience function custom preprocessors can use to parse the input
45    /// written to `stdin` by a `CmdRenderer`.
46    pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
47        serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
48    }
49
50    fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
51        let stdin = child.stdin.take().expect("Child has stdin");
52
53        if let Err(e) = self.write_input(stdin, book, ctx) {
54            // Looks like the backend hung up before we could finish
55            // sending it the render context. Log the error and keep going
56            warn!("Error writing the RenderContext to the backend, {}", e);
57        }
58    }
59
60    fn write_input<W: Write>(
61        &self,
62        writer: W,
63        book: &Book,
64        ctx: &PreprocessorContext,
65    ) -> Result<()> {
66        serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
67    }
68
69    /// The command this `Preprocessor` will invoke.
70    pub fn cmd(&self) -> &str {
71        &self.cmd
72    }
73
74    fn command(&self) -> Result<Command> {
75        let mut words = Shlex::new(&self.cmd);
76        let executable = match words.next() {
77            Some(e) => e,
78            None => bail!("Command string was empty"),
79        };
80
81        let mut cmd = Command::new(executable);
82
83        for arg in words {
84            cmd.arg(arg);
85        }
86
87        Ok(cmd)
88    }
89}
90
91impl Preprocessor for CmdPreprocessor {
92    fn name(&self) -> &str {
93        &self.name
94    }
95
96    fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
97        let mut cmd = self.command()?;
98
99        let mut child = cmd
100            .stdin(Stdio::piped())
101            .stdout(Stdio::piped())
102            .stderr(Stdio::inherit())
103            .spawn()
104            .with_context(|| {
105                format!(
106                    "Unable to start the \"{}\" preprocessor. Is it installed?",
107                    self.name()
108                )
109            })?;
110
111        self.write_input_to_child(&mut child, &book, ctx);
112
113        let output = child.wait_with_output().with_context(|| {
114            format!(
115                "Error waiting for the \"{}\" preprocessor to complete",
116                self.name
117            )
118        })?;
119
120        trace!("{} exited with output: {:?}", self.cmd, output);
121        ensure!(
122            output.status.success(),
123            format!(
124                "The \"{}\" preprocessor exited unsuccessfully with {} status",
125                self.name, output.status
126            )
127        );
128
129        serde_json::from_slice(&output.stdout).with_context(|| {
130            format!(
131                "Unable to parse the preprocessed book from \"{}\" processor",
132                self.name
133            )
134        })
135    }
136
137    fn supports_renderer(&self, renderer: &str) -> bool {
138        debug!(
139            "Checking if the \"{}\" preprocessor supports \"{}\"",
140            self.name(),
141            renderer
142        );
143
144        let mut cmd = match self.command() {
145            Ok(c) => c,
146            Err(e) => {
147                warn!(
148                    "Unable to create the command for the \"{}\" preprocessor, {}",
149                    self.name(),
150                    e
151                );
152                return false;
153            }
154        };
155
156        let outcome = cmd
157            .arg("supports")
158            .arg(renderer)
159            .stdin(Stdio::null())
160            .stdout(Stdio::inherit())
161            .stderr(Stdio::inherit())
162            .status()
163            .map(|status| status.code() == Some(0));
164
165        if let Err(ref e) = outcome {
166            if e.kind() == io::ErrorKind::NotFound {
167                warn!(
168                    "The command wasn't found, is the \"{}\" preprocessor installed?",
169                    self.name
170                );
171                warn!("\tCommand: {}", self.cmd);
172            }
173        }
174
175        outcome.unwrap_or(false)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::MDBook;
183    use std::path::Path;
184
185    fn guide() -> MDBook {
186        let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
187        MDBook::load(example).unwrap()
188    }
189
190    #[test]
191    fn round_trip_write_and_parse_input() {
192        let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
193        let md = guide();
194        let ctx = PreprocessorContext::new(
195            md.root.clone(),
196            md.config.clone(),
197            "some-renderer".to_string(),
198        );
199
200        let mut buffer = Vec::new();
201        cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
202
203        let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
204
205        assert_eq!(got_book, md.book);
206        assert_eq!(got_ctx, ctx);
207    }
208}