use std::fs::File;
use std::io::{BufRead, BufReader};
use pest::Parser;
use crate::errors::HakuError;
use crate::feature::process_feature;
use crate::ops::{
build_assign, build_cd, build_def_assign, build_either_assign, build_either_def_assign, build_elseif, build_error,
build_for, build_func, build_if, build_include, build_recipe, build_shell_cmd, build_while, Op,
};
use crate::vm::RunOpts;
#[derive(Parser)]
#[grammar = "haku.pest"]
pub struct TaskParser;
#[derive(Clone, Debug)]
pub struct DisabledRecipe {
pub name: String,
pub desc: String,
pub feat: String,
pub line: usize,
}
#[derive(Clone, Debug)]
pub(crate) struct OpItem {
pub(crate) op: Op,
pub(crate) line: usize,
}
pub(crate) struct HakuFile {
pub(crate) ops: Vec<OpItem>,
pub(crate) disabled: Vec<DisabledRecipe>,
pub(crate) user_feats: Vec<String>,
pub(crate) orig_lines: Vec<String>,
}
#[derive(Debug, PartialEq)]
enum Skip {
None,
Command,
Recipe,
}
struct DeadState {
pass: bool,
desc: String,
fstr: String,
f_list: Vec<OpItem>,
next_pass: bool,
next_desc: String,
next_fstr: String,
next_f_list: Vec<OpItem>,
}
impl DeadState {
fn new() -> Self {
DeadState {
pass: true,
desc: String::new(),
fstr: String::new(),
f_list: Vec::new(),
next_pass: true,
next_desc: String::new(),
next_fstr: String::new(),
next_f_list: Vec::new(),
}
}
fn reset(&mut self) {
self.pass = true;
self.desc.clear();
self.fstr.clear();
self.f_list.clear();
self.next_pass = true;
self.next_desc.clear();
self.next_fstr.clear();
self.next_f_list.clear();
}
}
impl HakuFile {
pub(crate) fn new() -> Self {
HakuFile { ops: Vec::new(), disabled: Vec::new(), user_feats: Vec::new(), orig_lines: Vec::new() }
}
fn process_line(&mut self, line: &str, idx: usize, opts: &RunOpts) -> Result<(), HakuError> {
let res = TaskParser::parse(Rule::expression, line);
let pairs = match res {
Err(e) => {
let msg = format!("'{}': {}", line, e.to_string());
return Err(HakuError::ParseError(msg, HakuError::error_extra("", line, idx)));
}
Ok(p) => p,
};
let mut feat_list: Vec<String> = Vec::new();
for pair in pairs {
match pair.as_rule() {
Rule::shell_stmt => {
self.ops.push(OpItem { op: build_shell_cmd(pair.into_inner())?, line: idx });
}
Rule::comment => {
let mut inner = pair.into_inner();
let s = inner.next().unwrap().as_str();
self.ops.push(OpItem { op: Op::Comment(s.to_owned()), line: idx });
}
Rule::doc_comment => {
let mut inner = pair.into_inner();
let s = inner.next().unwrap().as_str();
self.ops.push(OpItem { op: Op::DocComment(s.to_owned()), line: idx });
}
Rule::include_stmt => {
self.ops.push(OpItem { op: build_include(pair.into_inner())?, line: idx });
}
Rule::cd_stmt => {
self.ops.push(OpItem { op: build_cd(pair.into_inner())?, line: idx });
}
Rule::error_stmt => {
self.ops.push(OpItem { op: build_error(pair.into_inner())?, line: idx });
}
Rule::func => {
self.ops.push(OpItem { op: build_func(pair.into_inner())?, line: idx });
}
Rule::stmt_close => {
self.ops.push(OpItem { op: Op::StmtClose, line: idx });
}
Rule::break_stmt => {
self.ops.push(OpItem { op: Op::Break, line: idx });
}
Rule::cont_stmt => {
self.ops.push(OpItem { op: Op::Continue, line: idx });
}
Rule::either_def_assign => {
self.ops.push(OpItem { op: build_either_def_assign(pair.into_inner())?, line: idx });
}
Rule::either_assign => {
self.ops.push(OpItem { op: build_either_assign(pair.into_inner())?, line: idx });
}
Rule::def_assign => {
self.ops.push(OpItem { op: build_def_assign(pair.into_inner())?, line: idx });
}
Rule::assign => {
self.ops.push(OpItem { op: build_assign(pair.into_inner())?, line: idx });
}
Rule::while_stmt => {
self.ops.push(OpItem { op: build_while(pair.into_inner())?, line: idx });
}
Rule::for_stmt => {
self.ops.push(OpItem { op: build_for(pair.into_inner())?, line: idx });
}
Rule::if_stmt => {
self.ops.push(OpItem { op: build_if(pair.into_inner())?, line: idx });
}
Rule::elseif_stmt => {
self.ops.push(OpItem { op: build_elseif(pair.into_inner())?, line: idx });
}
Rule::else_stmt => {
self.ops.push(OpItem { op: Op::Else, line: idx });
}
Rule::return_stmt => {
self.ops.push(OpItem { op: Op::Return, line: idx });
}
Rule::recipe => {
self.ops.push(OpItem { op: build_recipe(pair.into_inner())?, line: idx });
}
Rule::feature_list => {
let txt = pair.as_str();
let pass = match process_feature(pair.into_inner(), opts, &mut feat_list) {
Ok(b) => b,
Err(s) => return Err(HakuError::InvalidFeatureName(s, HakuError::error_extra("", line, idx))),
};
self.ops.push(OpItem { op: Op::Feature(pass, txt.to_string()), line: idx });
}
Rule::pause_stmt => {
self.ops.push(OpItem { op: Op::Pause, line: idx });
}
_ => {
return Err(HakuError::ParseError(line.to_string(), HakuError::error_extra("", line, idx)));
}
}
}
if !feat_list.is_empty() {
for f in feat_list.drain(..) {
let mut unique = true;
for fe in self.user_feats.iter() {
if &f == fe {
unique = false;
break;
}
}
if unique {
self.user_feats.push(f);
}
}
}
Ok(())
}
pub fn load_from_file(path: &str, opts: &RunOpts) -> Result<HakuFile, HakuError> {
const BOM: [u8; 3] = [0xef, 0xbb, 0xbf];
let bom = if let Ok(s) = String::from_utf8(BOM.to_vec()) { s } else { "".to_string() };
let mut hk = HakuFile::new();
let input = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(HakuError::FileOpenFailure(path.to_string(), e.to_string())),
};
let buffered = BufReader::new(input);
let mut full_line = String::new();
hk.ops.clear();
for (idx, line) in buffered.lines().enumerate() {
if let Ok(l) = line {
let l = l.trim_start_matches(&bom);
hk.orig_lines.push(l.trim_end().to_string());
let l = l.trim();
full_line += l;
if full_line == "" {
continue;
}
if full_line.ends_with('\\') {
let stripped = full_line.trim_end_matches('\\');
full_line = format!("{} ", stripped);
continue;
}
} else {
return Err(HakuError::FileReadFailure(path.to_string()));
}
if full_line != "" {
hk.process_line(&full_line, idx, opts)?;
full_line.clear();
}
}
hk.remove_dead_code();
Ok(hk)
}
pub fn load_from_str(src: &str, opts: &RunOpts) -> Result<HakuFile, HakuError> {
let mut hk = HakuFile::new();
let mut full_line = String::new();
hk.ops.clear();
let mut idx: usize = 0;
for l in src.lines() {
hk.orig_lines.push(l.trim_end().to_string());
let l = l.trim();
full_line += l;
if full_line.ends_with('\\') || full_line == "" {
idx += 1;
continue;
}
if full_line != "" {
hk.process_line(&full_line, idx, opts)?;
full_line.clear();
}
idx += 1;
}
hk.remove_dead_code();
Ok(hk)
}
pub fn remove_dead_code(&mut self) {
let mut skip = Skip::None;
let mut ds = DeadState::new();
let mut op_list: Vec<OpItem> = Vec::new();
let mut nesting = 0;
for o in self.ops.iter().cloned() {
match o.op {
Op::Comment(_) => continue,
Op::DocComment(ref s) => {
if skip == Skip::Recipe {
ds.next_desc = s.to_string();
ds.next_f_list.push(o);
} else {
ds.desc = s.to_string();
ds.f_list.push(o);
}
}
Op::Feature(b, ref s) => {
if skip == Skip::Recipe {
ds.next_pass &= b;
ds.next_fstr += s;
} else {
ds.pass &= b;
ds.fstr += s;
}
}
Op::Recipe(ref name, _, _, _) => {
if skip == Skip::Recipe && !ds.next_pass {
self.disabled.push(DisabledRecipe {
name: name.to_string(),
desc: ds.next_desc.clone(),
feat: ds.next_fstr.clone(),
line: o.line,
});
} else if skip != Skip::None || ds.pass {
skip = Skip::None;
op_list.append(&mut ds.f_list);
if !ds.next_desc.is_empty() {
op_list.push(OpItem { op: Op::DocComment(ds.next_desc.clone()), line: o.line });
}
op_list.push(o);
} else if !ds.pass {
self.disabled.push(DisabledRecipe {
name: name.to_string(),
desc: ds.desc.clone(),
feat: ds.fstr.clone(),
line: o.line,
});
skip = Skip::Recipe;
}
ds.reset();
}
Op::If(_) | Op::While(_) | Op::For(_, _) => {
if skip != Skip::None {
nesting += 1;
} else if ds.pass {
op_list.push(o);
} else {
skip = Skip::Command;
nesting = 1;
}
ds.reset();
}
Op::StmtClose => {
if skip == Skip::None {
if ds.pass {
op_list.push(o);
}
} else if skip == Skip::Command {
nesting -= 1;
if nesting == 0 {
skip = Skip::None;
}
}
ds.reset();
}
_ => {
if skip == Skip::None && ds.pass {
op_list.push(o);
}
ds.reset();
}
}
}
self.ops = op_list;
}
}