1use std::borrow::Cow;
2use std::fs;
3use std::path::Path;
4use std::path::PathBuf;
5use std::process::Command;
6
7use anyhow::Context;
8use anyhow::Result;
9use cfg_if::cfg_if;
10use lazy_static::lazy_static;
11use regex::Captures;
12use regex::Regex;
13use serde::Deserialize;
14
15use mdbook::book::Book;
16use mdbook::book::Chapter;
17use mdbook::preprocess::{Preprocessor, PreprocessorContext};
18
19use crate::utils::map_chapter;
20
21pub struct CmdRun;
22
23lazy_static! {
24 static ref CMDRUN_REG_NEWLINE: Regex = Regex::new(r"<!--[ ]*cmdrun (.*?)-->\r?\n")
25 .expect("Failed to init regex for finding newline pattern");
26 static ref CMDRUN_REG_INLINE: Regex = Regex::new(r"<!--[ ]*cmdrun (.*?)-->")
27 .expect("Failed to init regex for finding inline pattern");
28}
29
30cfg_if! {
31 if #[cfg(target_family = "unix")] {
32 const LAUNCH_SHELL_COMMAND: &str = "sh";
33 const LAUNCH_SHELL_FLAG: &str = "-c";
34 const NEWLINE: &str = "\n";
35 } else if #[cfg(target_family = "windows")] {
36 const LAUNCH_SHELL_COMMAND: &str = "cmd";
37 const LAUNCH_SHELL_FLAG: &str = "/C";
38 const NEWLINE: &str = "\r\n";
39 }
40}
41
42impl Preprocessor for CmdRun {
43 fn name(&self) -> &str {
44 "cmdrun"
45 }
46
47 fn supports_renderer(&self, renderer: &str) -> bool {
48 renderer == "html"
49 }
50
51 fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
52 map_chapter(&mut book, &mut CmdRun::run_on_chapter)?;
53
54 Ok(book)
55 }
56}
57
58lazy_static! {
59 static ref SRC_DIR: String = get_src_dir();
60}
61
62#[derive(Deserialize)]
63struct BookConfig {
64 book: BookField,
65}
66
67#[derive(Deserialize)]
68struct BookField {
69 src: Option<String>,
70}
71
72fn get_src_dir() -> String {
73 fs::read_to_string(Path::new("book.toml"))
74 .map_err(|_| None::<String>)
75 .and_then(|fc| toml::from_str::<BookConfig>(fc.as_str()).map_err(|_| None))
76 .and_then(|bc| bc.book.src.ok_or(None))
77 .unwrap_or_else(|_| String::from("src"))
78}
79
80impl CmdRun {
81 fn run_on_chapter(chapter: &mut Chapter) -> Result<()> {
82 let working_dir = &chapter
83 .path
84 .to_owned()
85 .and_then(|p| {
86 Path::new(SRC_DIR.as_str())
87 .join(p)
88 .parent()
89 .map(PathBuf::from)
90 })
91 .and_then(|p| p.to_str().map(String::from))
92 .unwrap_or_default();
93
94 chapter.content = CmdRun::run_on_content(&chapter.content, working_dir)?;
95
96 Ok(())
97 }
98
99 pub fn run_on_content(content: &str, working_dir: &str) -> Result<String> {
101 let mut err = None;
102
103 let mut result = CMDRUN_REG_NEWLINE
104 .replace_all(content, |caps: &Captures| {
105 Self::run_cmdrun(caps[1].to_string(), working_dir, false).unwrap_or_else(|e| {
106 err = Some(e);
107 String::new()
108 })
109 })
110 .to_string();
111
112 if let Some(e) = err {
113 return Err(e);
114 }
115
116 result = CMDRUN_REG_INLINE
117 .replace_all(result.as_str(), |caps: &Captures| {
118 Self::run_cmdrun(caps[1].to_string(), working_dir, true).unwrap_or_else(|e| {
119 err = Some(e);
120 String::new()
121 })
122 })
123 .to_string();
124
125 match err {
126 None => Ok(result),
127 Some(err) => Err(err),
128 }
129 }
130
131 #[cfg(target_family = "windows")]
138 fn format_whitespace(str: Cow<'_, str>, inline: bool) -> String {
139 let str = match inline {
140 true => str.trim_end(),
142 false => str.as_ref(),
143 };
144
145 let mut res = str.lines().collect::<Vec<_>>().join("\r\n");
146 if !inline && !res.is_empty() {
147 res.push_str("\r\n");
148 }
149
150 return res;
151 }
152
153 #[cfg(target_family = "unix")]
154 fn format_whitespace(str: Cow<'_, str>, inline: bool) -> String {
155 match inline {
156 true => str.trim_end().to_string(),
158 false => str.to_string(),
159 }
160 }
161
162 fn cmdrun_error_message(message: &str, command: &str) -> String {
163 format!("**cmdrun error**: {} in 'cmdrun {}'", message, command)
164 }
165
166 pub fn run_cmdrun(command: String, working_dir: &str, inline: bool) -> Result<String> {
168 let (command, correct_exit_code): (String, Option<i32>) =
173 if let Some(first_word) = command.split_whitespace().next() {
174 if first_word.starts_with('-') {
175 if first_word.starts_with("--") {
176 match first_word {
178 "--strict" => (
179 command
180 .split_whitespace()
181 .skip(1)
182 .collect::<Vec<&str>>()
183 .join(" "),
184 Some(0),
185 ),
186 "--expect-return-code" => {
187 if let Some(second_word) = command.split_whitespace().nth(1) {
188 match second_word.parse::<i32>() {
189 Ok(return_code) => (
190 command
191 .split_whitespace()
192 .skip(2)
193 .collect::<Vec<&str>>()
194 .join(" "),
195 Some(return_code),
196 ),
197 Err(_) => {
198 return Ok(Self::cmdrun_error_message(
199 "No return code after '--expect-return-code'",
200 &command,
201 ));
202 }
203 }
204 } else {
205 return Ok(Self::cmdrun_error_message(
207 "No return code after '--expect-return-code'",
208 &command,
209 ));
210 }
211 }
212 some_other_word => {
213 return Ok(Self::cmdrun_error_message(
215 &format!("Unrecognized cmdrun flag {}", some_other_word),
216 &command,
217 ));
218 }
219 }
220 } else {
221 let (_, exit_code) = first_word.rsplit_once('-').unwrap_or(("", "0"));
223 match exit_code.parse::<i32>() {
224 Ok(return_code) => (
225 command
226 .split_whitespace()
227 .skip(1)
228 .collect::<Vec<&str>>()
229 .join(" "),
230 Some(return_code),
231 ),
232 Err(_) => {
233 return Ok(Self::cmdrun_error_message(
234 &format!(
235 "Unable to interpret short-form exit code {} as a number",
236 first_word
237 ),
238 &command,
239 ));
240 }
241 }
242 }
243 } else {
244 (command, None)
245 }
246 } else {
247 (command, None)
248 };
249
250 let output = Command::new(LAUNCH_SHELL_COMMAND)
251 .args([LAUNCH_SHELL_FLAG, &command])
252 .current_dir(working_dir)
253 .output()
254 .with_context(|| "Fail to run shell")?;
255
256 let stdout = Self::format_whitespace(String::from_utf8_lossy(&output.stdout), inline);
257 match (output.status.code(), correct_exit_code) {
258 (None, _) => Ok(Self::cmdrun_error_message(
259 "Command was ended before completing",
260 &command,
261 )),
262 (Some(code), Some(correct_code)) => {
263 if code != correct_code {
264 Ok(format!(
265 "**cmdrun error**: '{command}' returned exit code {code} instead of {correct_code}.{0}{1}{0}{2}",
266 NEWLINE,
267 String::from_utf8_lossy(&output.stdout),
268 String::from_utf8_lossy(&output.stderr)))
269 } else {
270 Ok(stdout)
271 }
272 }
273 (Some(_code), None) => {
274 Ok(stdout)
278 }
279 }
280 }
281}