#[macro_use]
extern crate lazy_static;
use std::fs::File;
use std::io::prelude::*;
use std::io::{self, ErrorKind, Write};
use std::process::{Command, Output, Stdio};
use difference::Changeset;
use mdsh::cli::{FileArg, Opt, Parent};
use regex::{Captures, Regex};
use structopt::StructOpt;
fn run_command(command: &str, work_dir: &Parent) -> Output {
let mut cli = Command::new("bash");
cli.arg("-c")
.arg(command)
.stdin(Stdio::null()) .current_dir(work_dir.as_path_buf())
.output()
.expect(
format!(
"fatal: failed to execute command `{:?}` in {}",
cli,
work_dir.as_path_buf().display()
)
.as_str(),
)
}
fn die<A>(msg: String) -> A {
std::io::stderr()
.write_all(format!("fatal: {}\n", msg).as_bytes())
.unwrap();
std::process::exit(1)
}
fn read_file(f: &FileArg) -> String {
let mut buffer = String::new();
match f {
FileArg::StdHandle => {
let stdin = io::stdin();
let mut handle = stdin.lock();
handle
.read_to_string(&mut buffer)
.unwrap_or_else(|err| die(format!("failed to read from stdin: {}", err)));
}
FileArg::File(path_buf) => {
File::open(path_buf)
.and_then(|mut file| file.read_to_string(&mut buffer))
.unwrap_or_else(|err| {
die(format!(
"failed to read from {}: {}",
path_buf.display(),
err
))
});
}
}
buffer
}
fn write_file(f: &FileArg, contents: String) {
match f {
FileArg::StdHandle => {
let stdout = io::stdout();
let mut handle = stdout.lock();
write!(handle, "{}", contents)
.unwrap_or_else(|err| die(format!("failed to write to stdout: {}", err)));
}
FileArg::File(path_buf) => {
File::create(path_buf)
.and_then(|mut file| {
write!(file, "{}", contents)?;
file.sync_all()
})
.unwrap_or_else(|err| {
die(format!(
"failed to write to {}: {}",
path_buf.display(),
err
))
});
}
}
}
fn trail_nl<T: AsRef<str>>(s: T) -> String {
let r = s.as_ref();
if r.ends_with('\n') {
r.to_string()
} else {
format!("{}\n", r)
}
}
fn wrap_nl(s: String) -> String {
if s.starts_with('\n') {
trail_nl(s)
} else if s.ends_with('\n') {
format!("\n{}", s)
} else {
format!("\n{}\n", s)
}
}
fn filter_ansi(s: String) -> String {
RE_ANSI_FILTER.replace_all(&s, "").to_string()
}
static RE_FENCE_LINK_STR: &str = r"\[\$ [^\]]+\]\((?P<link>[^\)]+)\)";
static RE_MD_LINK_STR: &str = r"\[> [^\]]+\]\((?P<link>[^\)]+)\)";
static RE_FENCE_COMMAND_STR: &str = r"`\$ (?P<command>[^`]+)`";
static RE_MD_COMMAND_STR: &str = r"`> (?P<command>[^`]+)`";
static RE_VAR_COMMAND_STR: &str = r"`! (?P<key>[\w_]+)=(?P<raw_value>[^`]+)`";
static RE_FENCE_BLOCK_STR: &str = r"^```.+?^```";
static RE_MD_BLOCK_STR: &str = r"^<!-- BEGIN mdsh -->.+?^<!-- END mdsh -->";
static RE_COMMENT_BEGIN_STR: &str = r"(?:<!-- +)?";
static RE_COMMENT_END_STR: &str = r"(?: +-->)?";
static RE_FENCE_TYPE_STR: &str = r"(?: as (?P<fence_type>\w+))?";
lazy_static! {
static ref RE_MATCH_FENCE_BLOCK_STR: String = format!(
r"(?sm)(^{}(?:{}|{}){}{} *$)\n+({}|{})",
RE_COMMENT_BEGIN_STR, RE_FENCE_COMMAND_STR, RE_FENCE_LINK_STR, RE_FENCE_TYPE_STR, RE_COMMENT_END_STR,
RE_FENCE_BLOCK_STR, RE_MD_BLOCK_STR,
);
static ref RE_MATCH_MD_BLOCK_STR: String = format!(
r"(?sm)(^{}(?:{}|{}){} *$)\n+({}|{})",
RE_COMMENT_BEGIN_STR, RE_MD_COMMAND_STR, RE_MD_LINK_STR, RE_COMMENT_END_STR,
RE_MD_BLOCK_STR, RE_FENCE_BLOCK_STR,
);
static ref RE_MATCH_ANY_COMMAND_STR: String = format!(r"(?sm)^{}(`[^`]+`){}{} *$", RE_COMMENT_BEGIN_STR, RE_FENCE_TYPE_STR, RE_COMMENT_END_STR);
static ref RE_MATCH_FENCE_COMMAND_STR: String = format!(r"(?sm)^{}$", RE_FENCE_COMMAND_STR);
static ref RE_MATCH_MD_COMMAND_STR: String = format!(r"(?sm)^{}$", RE_MD_COMMAND_STR);
static ref RE_MATCH_VAR_COMMAND_STR: String = format!(r"(?sm)^{}$", RE_VAR_COMMAND_STR);
static ref RE_MATCH_FENCE_LINK_STR: String = format!(r"(?sm)^{}{}{}{} *$", RE_COMMENT_BEGIN_STR, RE_FENCE_LINK_STR, RE_FENCE_TYPE_STR, RE_COMMENT_END_STR);
static ref RE_MATCH_MD_LINK_STR: String = format!(r"(?sm)^{}{}{} *$", RE_COMMENT_BEGIN_STR, RE_MD_LINK_STR, RE_COMMENT_END_STR);
static ref RE_MATCH_ANY_COMMAND: Regex = Regex::new(&RE_MATCH_ANY_COMMAND_STR).unwrap();
static ref RE_MATCH_FENCE_BLOCK: Regex = Regex::new(&RE_MATCH_FENCE_BLOCK_STR).unwrap();
static ref RE_MATCH_MD_BLOCK: Regex = Regex::new(&RE_MATCH_MD_BLOCK_STR).unwrap();
static ref RE_MATCH_FENCE_COMMAND: Regex = Regex::new(&RE_MATCH_FENCE_COMMAND_STR).unwrap();
static ref RE_MATCH_MD_COMMAND: Regex = Regex::new(&RE_MATCH_MD_COMMAND_STR).unwrap();
static ref RE_MATCH_VAR_COMMAND: Regex = Regex::new(&RE_MATCH_VAR_COMMAND_STR).unwrap();
static ref RE_MATCH_FENCE_LINK: Regex = Regex::new(&RE_MATCH_FENCE_LINK_STR).unwrap();
static ref RE_MATCH_MD_LINK: Regex = Regex::new(&RE_MATCH_MD_LINK_STR).unwrap();
static ref RE_ANSI_FILTER: Regex = Regex::new(r"\x1b\[[0-9;]*[mGKH]").unwrap();
}
struct FailingCommand {
output: Output,
command: String,
command_char: char,
}
fn main() -> std::io::Result<()> {
let opt = Opt::from_args();
let clean = opt.clean;
let frozen = opt.frozen;
let inputs = opt.inputs;
if inputs.len() == 0 {
return Ok(());
} else if inputs.len() == 1 {
let input = inputs.first().unwrap();
let output = opt.output.unwrap_or_else(|| input.clone());
let work_dir: Parent = opt.work_dir.map_or_else(
|| {
input
.clone()
.parent()
.expect("fatal: your input file has no parent directory.")
},
|buf| Parent::from_parent_path_buf(buf),
);
process_file(&input, &output, &work_dir, clean, frozen)?;
} else {
if opt.output.is_some() {
return Err(std::io::Error::new(
ErrorKind::Other,
"--output is not compatible with multiple inputs",
));
}
if opt.work_dir.is_some() {
return Err(std::io::Error::new(
ErrorKind::Other,
"--work-dir is not compatible with multiple inputs",
));
}
for input in inputs {
let work_dir = input
.clone()
.parent()
.expect("fatal: your input file has no parent directory.");
let output = input.clone();
process_file(&input, &output, &work_dir, clean, frozen)?;
}
}
Ok(())
}
fn process_file(
input: &FileArg,
output: &FileArg,
work_dir: &Parent,
clean: bool,
frozen: bool,
) -> std::io::Result<()> {
let original_contents = read_file(&input);
let mut contents = original_contents.clone();
eprintln!(
"Using input={:?} output={:?} work_dir={:?} clean={:?} frozen={:?}",
&input, output, work_dir, clean, frozen
);
fn clean_blocks(file: &mut String, block_regex: &Regex) {
*file = block_regex
.replace_all(file, |caps: &Captures| {
caps[1].to_string()
})
.into_owned()
}
clean_blocks(&mut contents, &RE_MATCH_FENCE_BLOCK);
clean_blocks(&mut contents, &RE_MATCH_MD_BLOCK);
if clean {
write_file(&output, contents.to_string());
return Ok(());
}
fn get_fence_type(caps: &Captures) -> String {
if let Some(name) = &caps.name("fence_type") {
format!("{}", name.as_str())
} else {
format!("")
}
}
let mut failures = Vec::new();
let fill_commands =
|data: &mut String, command_regex: &Regex| -> Result<(), Vec<FailingCommand>> {
*data = command_regex
.replace_all(data, |caps: &Captures| {
let original_line = &caps[0];
let command_line = &caps[1];
let fence_type = get_fence_type(caps);
eprintln!("{}", command_line);
if let Some(caps) = RE_MATCH_FENCE_COMMAND.captures(command_line) {
let command = &caps["command"];
let start_delimiter = "```";
let end_delimiter = "```";
let command_char = '$';
let result = run_command(command, &work_dir);
if result.status.success() {
let stdout = String::from_utf8_lossy(&result.stdout);
let stdout = filter_ansi(stdout.to_string());
if stdout.trim().is_empty() {
format!("{}", trail_nl(&original_line))
} else {
format!(
"{}{}{}{}{}",
trail_nl(&original_line),
start_delimiter,
fence_type,
wrap_nl(stdout.to_string()),
end_delimiter
)
}
} else {
failures.push(FailingCommand {
output: result,
command: command.to_string(),
command_char: command_char,
});
original_line.to_string()
}
} else if let Some(caps) = RE_MATCH_MD_COMMAND.captures(command_line) {
let command = &caps["command"];
let start_delimiter = "<!-- BEGIN mdsh -->";
let end_delimiter = "<!-- END mdsh -->";
let command_char = '>';
let result = run_command(command, &work_dir);
if result.status.success() {
let stdout = String::from_utf8_lossy(&result.stdout);
let stdout = filter_ansi(stdout.to_string());
if stdout.trim().is_empty() {
format!("{}", trail_nl(&original_line))
} else {
format!(
"{}{}{}{}{}",
trail_nl(&original_line),
start_delimiter,
fence_type,
wrap_nl(stdout.to_string()),
end_delimiter
)
}
} else {
failures.push(FailingCommand {
output: result,
command: command.to_string(),
command_char: command_char,
});
original_line.to_string()
}
} else if let Some(caps) = RE_MATCH_VAR_COMMAND.captures(command_line) {
let key = &caps["key"];
let raw_value = &caps["raw_value"];
let command = format!("echo {}", raw_value.trim());
let result = run_command(&command, &work_dir);
if result.status.success() {
let stdout = String::from_utf8_lossy(&result.stdout);
let stdout = filter_ansi(stdout.to_string());
std::env::set_var(key, stdout.trim());
} else {
failures.push(FailingCommand {
output: result,
command: command.to_string(),
command_char: '!',
});
};
original_line.to_string()
} else {
panic!("WTF, not supported")
}
})
.into_owned();
if failures.is_empty() {
Ok(())
} else {
Err(failures)
}
};
fn print_failures(fs: Vec<FailingCommand>) {
eprintln!("\nERROR: some commands failed:\n");
for f in fs {
let stderr = match String::from_utf8_lossy(&f.output.stderr)
.into_owned()
.as_str()
{
"" => String::from(""),
s => String::from("\nIts stderr was:\n") + s.trim_end(),
};
eprintln!(
"`{} {}` failed with status {}.{}\n",
f.command_char, f.command, f.output.status, stderr
);
}
}
fill_commands(&mut contents, &RE_MATCH_ANY_COMMAND).or_else(
|failures| -> std::io::Result<()> {
print_failures(failures);
std::process::exit(1);
},
)?;
fn fill_includes(
file: &mut String,
link_regex: &Regex,
link_char: char,
start_delimiter: &str,
end_delimiter: &str,
) {
*file = link_regex
.replace_all(file, |caps: &Captures| {
let link = &caps["link"];
let fence_type = get_fence_type(caps);
eprintln!("[{} {}]", link_char, link);
let result = read_file(&FileArg::from_str_unsafe(link));
format!(
"{}{}{}{}{}",
trail_nl(&caps[0]),
start_delimiter,
fence_type,
wrap_nl(result.to_owned()),
end_delimiter
)
})
.into_owned()
}
fill_includes(&mut contents, &RE_MATCH_FENCE_LINK, '$', "```", "```");
fill_includes(
&mut contents,
&RE_MATCH_MD_LINK,
'>',
"<!-- BEGIN mdsh -->",
"<!-- END mdsh -->",
);
if original_contents == contents {
return Ok(());
}
let changeset = Changeset::new(&original_contents, &contents, "\n");
eprintln!("{}", changeset);
if frozen {
return Err(std::io::Error::new(
ErrorKind::Other,
"--frozen: output is not the same",
));
}
write_file(&output, contents.to_string());
return Ok(());
}