use std::convert::From;
use std::env;
use std::fmt;
use std::io::{self, Write};
use std::iter::FromIterator;
use std::mem;
use std::path::PathBuf;
use std::process::Command;
use std::time::{Duration, Instant};
use std::usize;
use crate::errors::HakuError;
use crate::func::{run_func, FuncResult};
use crate::ops::{is_flag_on, Op, Seq, FLAG_PASS, FLAG_QUIET};
use crate::parse::{DisabledRecipe, HakuFile};
use crate::var::{ExecResult, VarMgr, VarValue};
const DEFAULT_RECIPE: &str = "_default";
#[macro_export]
macro_rules! output {
($v:expr, $lvl:literal, $fmt:literal) => {
if $v >= $lvl {
println!($fmt);
}
};
($v:expr, $lvl:literal, $fmt:literal, $vals:expr) => {
if $v >= $lvl {
println!($fmt, $vals);
}
};
($v:expr, $lvl:literal, $fmt:literal, $($vals:expr),+) => {
if $v >= $lvl {
println!($fmt, $($vals), +);
}
};
}
fn human_duration(dur: Duration) -> String {
let sec = dur.as_secs();
if sec >= 60 {
let min = sec / 60;
let sec = sec - min * 60;
return format!("{}m{}s", min, sec);
}
let milli = dur.subsec_millis();
if milli == 0 {
return "0ms".to_string();
}
if sec > 0 {
return format!("{}s{}ms", sec, milli);
}
format!("{}ms", milli)
}
#[derive(Clone)]
pub struct RunOpts {
pub(crate) feats: Vec<String>,
verbosity: usize,
dry_run: bool,
show_time: bool,
}
impl Default for RunOpts {
fn default() -> Self {
RunOpts { dry_run: false, feats: Vec::new(), verbosity: 0, show_time: false }
}
}
impl RunOpts {
pub fn new() -> Self {
Default::default()
}
pub fn with_dry_run(mut self, dry_run: bool) -> Self {
self.dry_run = dry_run;
self
}
pub fn with_features(mut self, feats: Vec<String>) -> Self {
self.feats = feats;
self
}
pub fn with_verbosity(mut self, verbosity: usize) -> Self {
self.verbosity = verbosity;
self
}
pub fn with_time(mut self, show: bool) -> Self {
self.show_time = show;
self
}
}
#[derive(Clone, Debug)]
pub struct RecipeDesc {
pub name: String,
pub desc: String,
pub depends: Vec<String>,
pub system: bool,
pub loc: RecipeLoc,
pub flags: u32,
pub vars: Vec<String>,
}
impl fmt::Display for RecipeDesc {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)?;
if !self.vars.is_empty() {
write!(f, " (")?;
for v in self.vars.iter() {
write!(f, "{},", v)?;
}
write!(f, ") ")?;
}
if !self.depends.is_empty() {
write!(f, "[")?;
for dep in self.depends.iter() {
write!(f, "{},", dep)?;
}
write!(f, "]")?;
}
if !self.desc.is_empty() {
write!(f, " #{}", self.desc)?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
enum Condition {
If(bool),
While(Vec<Op>),
ForInt(String, i64, i64, i64),
ForList(String, Vec<String>),
}
#[derive(Clone, Debug)]
struct CondItem {
line: usize,
cond: Condition,
}
pub struct Engine {
files: Vec<HakuFile>,
included: Vec<String>,
recipes: Vec<RecipeDesc>,
varmgr: VarMgr,
shell: Vec<String>,
opts: RunOpts,
cond_stack: Vec<CondItem>,
real_line: usize,
file_idx: usize,
pub(crate) cwd: PathBuf,
pub(crate) cwd_history: Vec<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct RecipeLoc {
pub file: usize,
pub line: usize,
pub script_line: usize,
}
#[derive(Debug)]
struct RecipeItem {
name: String,
loc: RecipeLoc,
vars: Vec<String>,
flags: u32,
}
pub struct RecipeContent {
pub filename: String,
pub content: Vec<String>,
pub enabled: bool,
}
impl Engine {
pub fn new(opts: RunOpts) -> Self {
#[cfg(windows)]
let shell = vec!["powershell".to_string(), "-c".to_string()];
#[cfg(not(windows))]
let shell = vec!["sh".to_string(), "-cu".to_string()];
let cwd = match env::current_dir() {
Ok(p) => p,
Err(e) => {
eprintln!("Failed to detect current working directory: {:?}", e);
PathBuf::new()
}
};
Engine {
files: Vec::new(),
included: Vec::new(),
recipes: Vec::new(),
varmgr: VarMgr::new(opts.verbosity),
cond_stack: Vec::new(),
real_line: usize::MAX,
file_idx: usize::MAX,
opts,
shell,
cwd,
cwd_history: Vec::new(),
}
}
fn line_desc(&self, fidx: usize, lidx: usize) -> (String, String) {
if fidx == usize::MAX || fidx >= self.files.len() {
return (String::new(), String::new());
}
let fname = if self.files.len() == 1 { String::new() } else { self.included[fidx].clone() };
if lidx == usize::MAX || lidx >= self.files[fidx].orig_lines.len() {
return (fname, String::new());
}
(fname, self.files[fidx].orig_lines[lidx].clone())
}
fn error_extra(&self) -> String {
let (filename, line) = self.line_desc(self.file_idx, self.real_line);
HakuError::error_extra(&filename, &line, self.real_line)
}
pub fn load_from_file(&mut self, filepath: &str) -> Result<(), HakuError> {
output!(self.opts.verbosity, 2, "Loading file: {}", filepath);
for s in &self.included {
if s == filepath {
return Err(HakuError::IncludeRecursionError(filepath.to_string()));
}
}
let hk = HakuFile::load_from_file(filepath, &self.opts)?;
self.files.push(hk);
self.included.push(filepath.to_string());
self.run_header(self.files.len() - 1)?;
self.detect_recipes();
Ok(())
}
pub fn load_from_str(&mut self, src: &str) -> Result<(), HakuError> {
output!(self.opts.verbosity, 2, "Executing string: {}", src);
let hk = HakuFile::load_from_str(src, &self.opts)?;
self.files.push(hk);
self.run_header(self.files.len() - 1)?;
self.detect_recipes();
Ok(())
}
fn run_header(&mut self, idx: usize) -> Result<(), HakuError> {
output!(self.opts.verbosity, 3, "RUN HEADER: {}: {}", idx, self.files[idx].ops.len());
let mut to_include: Vec<String> = Vec::new();
let mut to_include_flags: Vec<u32> = Vec::new();
for op in &self.files[idx].ops {
self.real_line = op.line;
self.file_idx = idx;
match &op.op {
Op::Feature(_, _) => { }
Op::Recipe(_, _, _, _) => break,
Op::Comment(_) | Op::DocComment(_) => { }
Op::Include(flags, path) => {
let inc_path = self.varmgr.interpolate(&path, true);
output!(self.opts.verbosity, 3, " !!INCLUDE - {}", inc_path);
to_include.push(inc_path);
to_include_flags.push(*flags);
}
_ => { }
}
}
output!(self.opts.verbosity, 3, "TO INCLUDE: {}", to_include.len());
for (i, path) in to_include.iter().enumerate() {
let f = to_include_flags[i];
let res = self.load_from_file(path);
if res.is_err() {
output!(self.opts.verbosity, 2, "ERROR: {:?}", res);
}
if res.is_err() && !is_flag_on(f, FLAG_PASS) {
return res;
}
eprintln!("Skipping included file: {:?}", res);
}
Ok(())
}
fn is_system_recipe(name: &str) -> bool {
name == "_default" || name == "_before" || name == "_after"
}
fn detect_recipes(&mut self) {
for (file_idx, hk) in self.files.iter().enumerate() {
let mut desc = String::new();
for (line_idx, op) in hk.ops.iter().enumerate() {
match op.op {
Op::Feature(_, _) => {}
Op::DocComment(ref s) => desc = self.varmgr.interpolate(s, true),
Op::Recipe(ref nm, flags, ref vars, ref deps) => {
let mut recipe = RecipeDesc {
name: nm.clone(),
desc: desc.clone(),
loc: RecipeLoc { line: line_idx, file: file_idx, script_line: op.line },
depends: Vec::new(),
system: Engine::is_system_recipe(&nm),
vars: vars.clone(),
flags,
};
if !deps.is_empty() {
for d in deps.iter() {
recipe.depends.push(d.to_string());
}
}
self.recipes.push(recipe);
desc.clear();
}
Op::Comment(_) => { }
_ => {
desc.clear();
}
}
}
}
self.recipes.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap());
}
pub fn file_name(&self, file_idx: usize) -> Result<&str, HakuError> {
if file_idx >= self.files.len() {
return Err(HakuError::FileNotLoaded(file_idx));
}
Ok(&self.included[file_idx])
}
pub fn recipes(&self) -> &[RecipeDesc] {
&self.recipes
}
pub fn disabled_recipes(&self) -> Vec<DisabledRecipe> {
let mut v = Vec::new();
for file in self.files.iter() {
for ds in file.disabled.iter() {
v.push(ds.clone());
}
}
v
}
fn next_recipe(&self, file: usize, line: usize) -> usize {
let mut min = usize::MAX;
for r in self.recipes.iter() {
if r.loc.file != file || r.loc.script_line <= line {
continue;
}
if min > r.loc.script_line {
min = r.loc.script_line;
}
}
for d in self.files[file].disabled.iter() {
if d.line > line && d.line < min {
min = d.line;
}
}
if min == usize::MAX {
min = self.files[file].orig_lines.len();
}
min
}
pub fn recipe_content(&self, name: &str) -> Result<RecipeContent, HakuError> {
if let Ok(desc) = self.find_recipe(name) {
let fidx = desc.loc.file;
let sidx = desc.loc.script_line;
let mut eidx = self.next_recipe(fidx, sidx);
let mut content = Vec::new();
while eidx > sidx
&& (self.files[fidx].orig_lines[eidx - 1].trim_start().starts_with("#[")
|| self.files[fidx].orig_lines[eidx - 1].trim_start().starts_with("##")
|| self.files[fidx].orig_lines[eidx - 1].trim_start().starts_with("//"))
{
eidx -= 1;
}
for lidx in sidx..eidx {
content.push(self.files[fidx].orig_lines[lidx].clone());
}
return Ok(RecipeContent {
filename: self.file_name(fidx).unwrap_or("").to_string(),
content,
enabled: true,
});
}
for (fidx, f) in self.files.iter().enumerate() {
let mut sidx = usize::MAX;
for r in f.disabled.iter() {
if r.name == name {
sidx = r.line;
break;
}
}
if sidx != usize::MAX {
let mut eidx = self.next_recipe(fidx, sidx);
while eidx > sidx
&& (self.files[fidx].orig_lines[eidx - 1].trim_start().starts_with("#[")
|| self.files[fidx].orig_lines[eidx - 1].trim_start().starts_with("##")
|| self.files[fidx].orig_lines[eidx - 1].trim_start().starts_with("//"))
{
eidx -= 1;
}
let mut content = Vec::new();
for lidx in sidx..eidx {
content.push(f.orig_lines[lidx].clone());
}
return Ok(RecipeContent {
filename: self.file_name(fidx).unwrap_or("").to_string(),
content,
enabled: false,
});
}
}
Err(HakuError::RecipeNotFoundError(name.to_string()))
}
pub fn user_features(&self) -> Vec<String> {
let mut v: Vec<String> = Vec::new();
for file in self.files.iter() {
for feat in file.user_feats.iter() {
let mut unique = true;
for ex in v.iter() {
if ex == feat {
unique = false;
break;
}
}
if unique {
v.push(feat.clone());
}
}
}
v
}
fn find_recipe(&self, name: &str) -> Result<RecipeDesc, HakuError> {
for sec in &self.recipes {
if sec.name == name {
return Ok(sec.clone());
}
}
Err(HakuError::RecipeNotFoundError(name.to_string()))
}
pub fn set_free_args(&mut self, args: &[String]) {
self.varmgr.free = Vec::from_iter(args.iter().cloned());
}
pub fn run_recipe(&mut self, name: &str) -> Result<(), HakuError> {
output!(self.opts.verbosity, 1, "Running SECTION '{}'", name);
let sec_res = if name.is_empty() {
match self.find_recipe(DEFAULT_RECIPE) {
Ok(loc) => Some(loc),
_ => None,
}
} else {
Some(self.find_recipe(name)?)
};
self.exec_init()?;
if let Some(sec) = sec_res {
return self.exec_recipe(sec.loc);
}
Err(HakuError::DefaultRecipeError)
}
pub(crate) fn set_shell(&mut self, new_shell: Vec<String>) -> FuncResult {
if new_shell.is_empty() {
return Err("shell cannot be empty".to_string());
}
output!(self.opts.verbosity, 1, "Setting new shell: {:?}", new_shell);
self.shell = new_shell;
Ok(VarValue::from(1))
}
pub(crate) fn set_env_var(&mut self, name: String, value: String) -> FuncResult {
if name.is_empty() {
return Err("variable name missing".to_string());
}
output!(self.opts.verbosity, 1, "Change env var {} to '{}'", name, value);
self.varmgr.env.insert(name, value);
Ok(VarValue::from(1))
}
pub(crate) fn del_env_var(&mut self, name: String) -> FuncResult {
if name.is_empty() {
return Err("variable name missing".to_string());
}
output!(self.opts.verbosity, 1, "Delete env var {}", name);
self.varmgr.env.remove(&name);
Ok(VarValue::from(1))
}
pub(crate) fn clear_env_vars(&mut self) -> FuncResult {
output!(self.opts.verbosity, 1, "Remove all env vars");
self.varmgr.env.clear();
Ok(VarValue::from(1))
}
fn exec_file_init(&mut self, file: usize) -> Result<(), HakuError> {
let cnt = self.files[file].ops.len();
let mut i = 0;
while i < cnt {
let op = self.files[file].ops[i].clone();
self.real_line = op.line;
self.file_idx = file;
match op.op {
Op::Recipe(_, _, _, _) | Op::Return => return Ok(()),
Op::Include(_, _) => {
i += 1;
}
Op::Error(msg) => return Err(HakuError::UserError(format!("{} at line {}", msg, op.line))),
Op::DocComment(_) | Op::Comment(_) => {
i += 1;
}
Op::Shell(flags, cmd) => {
self.exec_cmd_shell(flags, &cmd)?;
i += 1;
}
Op::EitherAssign(chk, name, ops) => {
self.exec_either_assign(chk, &name, &ops)?;
i += 1;
}
Op::DefAssign(name, ops) => {
self.exec_assign_or(&name, &ops)?;
i += 1;
}
Op::Assign(name, ops) => {
self.exec_assign(&name, &ops)?;
i += 1;
}
Op::Func(name, ops) => {
self.exec_func(&name, &ops)?;
i += 1;
} Op::StmtClose => {
let next = self.exec_end()?;
if next == 0 {
i += 1;
} else {
i = next;
}
}
Op::For(name, seq) => {
let ok = self.exec_for(&name, seq, i)?;
if ok {
i += 1;
} else {
i = self.find_end(file, i + 1, "for")?;
}
}
Op::While(ops) => {
let ok = self.exec_while(&ops, i)?;
if ok {
i += 1;
} else {
i = self.find_end(file, i + 1, "while")?;
}
}
Op::Break => {
i = self.exec_break(file)?;
}
Op::Continue => {
i = self.exec_continue(file)?;
}
Op::If(ops) => {
i = self.exec_if(&ops, file, i)?;
}
Op::Else => {
i = self.exec_else(file, i)?;
}
Op::ElseIf(ops) => {
i = self.exec_elseif(&ops, file, i)?;
} Op::Cd(flags, p) => {
self.exec_cd(flags, &p)?;
i += 1;
}
Op::Pause => {
self.exec_pause()?;
i += 1;
}
_ => {
eprintln!("UNIMPLEMENTED: {:?}", op);
i += 1;
}
}
}
Ok(())
}
fn exec_init(&mut self) -> Result<(), HakuError> {
let cnt = self.files.len();
for i in 0..cnt {
self.exec_file_init(cnt - i - 1)?;
}
Ok(())
}
fn push_recipe(
&mut self,
loc: RecipeLoc,
found: Option<&[RecipeItem]>,
parent: Option<&[String]>,
) -> Result<Vec<RecipeItem>, HakuError> {
let op = self.files[loc.file].ops[loc.line].clone();
let mut sec_item: RecipeItem = RecipeItem {
name: String::new(),
loc: RecipeLoc { file: 0, line: 0, script_line: 0 },
vars: Vec::new(),
flags: 0,
};
output!(self.opts.verbosity, 2, "Checking recipe: {:?}", op);
let mut vc: Vec<RecipeItem> = Vec::new();
let mut parents: Vec<String> = match parent {
None => Vec::new(),
Some(p) => p.iter().map(|a| a.to_string()).collect(),
};
match op.op {
Op::Recipe(name, flags, vars, deps) => {
if vc.iter().any(|s| s.name == name) || deps.iter().any(|d| d == &name) {
return Err(HakuError::RecipeRecursionError(name, self.error_extra()));
}
for dep in deps {
if let Some(ps) = parent {
if ps.iter().any(|p| p == &dep) {
return Err(HakuError::RecipeRecursionError(dep, self.error_extra()));
}
}
if let Some(fnd) = found {
if fnd.iter().any(|f| f.name == dep) {
return Err(HakuError::RecipeRecursionError(dep, self.error_extra()));
}
}
let next_s = self.find_recipe(&dep)?;
parents.push(name.clone());
let mut slist = self.push_recipe(next_s.loc, Some(&vc), Some(&parents))?;
vc.append(&mut slist);
}
sec_item.name = name;
sec_item.loc = loc;
sec_item.vars = vars;
sec_item.flags = flags;
}
_ => unreachable!(),
}
vc.push(sec_item);
Ok(vc)
}
fn exec_recipe(&mut self, loc: RecipeLoc) -> Result<(), HakuError> {
output!(self.opts.verbosity, 2, "Start recipe [{}:{}]", loc.file, loc.line);
self.real_line = loc.script_line;
self.file_idx = loc.file;
let sec = self.push_recipe(loc, None, None)?;
output!(self.opts.verbosity, 2, "recipe call stack: {:?}", sec);
let mut idx = 0;
while idx < sec.len() {
let now = Instant::now();
let op = &sec[idx];
output!(self.opts.verbosity, 1, "Starting recipe: {}", op.name);
self.enter_recipe(op);
self.exec_from(op.loc.file, op.loc.line + 1, op.flags)?;
self.leave_recipe();
let dur = now.elapsed();
if self.opts.show_time {
println!("Section {} finished in {}", op.name, human_duration(dur));
} else {
output!(self.opts.verbosity, 1, "Section {} finished in {}", op.name, human_duration(dur));
}
idx += 1;
}
Ok(())
}
fn exec_from(&mut self, file: usize, line: usize, sec_flags: u32) -> Result<(), HakuError> {
let mut idx = line;
let l = self.files[file].ops.len();
while idx < l {
let op = (self.files[file].ops[idx]).clone();
self.real_line = op.line;
self.file_idx = file;
match op.op {
Op::Return | Op::Recipe(_, _, _, _) => return Ok(()),
Op::Include(_, _) => return Err(HakuError::IncludeInRecipeError(self.error_extra())),
Op::Error(msg) => return Err(HakuError::UserError(format!("{} at line {}", msg, op.line))),
Op::Shell(flags, cmd) => {
let cmd_flags = sec_flags ^ flags;
self.exec_cmd_shell(cmd_flags, &cmd)?;
idx += 1;
}
Op::EitherAssign(chk, name, ops) => {
self.exec_either_assign(chk, &name, &ops)?;
idx += 1;
}
Op::DefAssign(name, ops) => {
self.exec_assign_or(&name, &ops)?;
idx += 1;
}
Op::Assign(name, ops) => {
self.exec_assign(&name, &ops)?;
idx += 1;
}
Op::Func(name, ops) => {
self.exec_func(&name, &ops)?;
idx += 1;
} Op::StmtClose => {
let next = self.exec_end()?;
if next == 0 {
idx += 1
} else {
idx = next;
}
}
Op::For(name, seq) => {
let ok = self.exec_for(&name, seq, idx)?;
if ok {
idx += 1;
} else {
idx = self.find_end(file, idx + 1, "for")?;
}
}
Op::While(ops) => {
let ok = self.exec_while(&ops, idx)?;
if ok {
idx += 1;
} else {
idx = self.find_end(file, idx + 1, "while")?;
}
}
Op::Break => {
idx = self.exec_break(file)?;
}
Op::Continue => {
idx = self.exec_continue(file)?;
}
Op::If(ops) => {
idx = self.exec_if(&ops, file, idx)?;
} Op::Else => {
idx = self.exec_else(file, idx)?;
}
Op::ElseIf(ops) => {
idx = self.exec_elseif(&ops, file, idx)?;
} Op::Cd(flags, p) => {
let cmd_flags = sec_flags ^ flags;
self.exec_cd(cmd_flags, &p)?;
idx += 1;
}
Op::Pause => {
self.exec_pause()?;
idx += 1;
}
_ => {
idx += 1;
}
}
}
Ok(())
}
fn find_end(&self, file: usize, line: usize, tp: &str) -> Result<usize, HakuError> {
let mut idx = line;
let l = self.files[file].ops.len();
let mut nesting = 1;
while idx < l {
let op = (self.files[file].ops[idx]).clone();
match op.op {
Op::StmtClose => {
nesting -= 1;
if nesting == 0 {
return Ok(idx + 1);
}
}
Op::If(_) | Op::While(_) | Op::For(_, _) => nesting += 1,
_ => {}
}
idx += 1;
}
Err(HakuError::NoMatchingEndError(tp.to_string(), self.error_extra()))
}
fn find_else(&self, file: usize, line: usize, tp: &str) -> Result<(bool, usize), HakuError> {
let mut idx = line;
let l = self.files[file].ops.len();
let mut nesting = 1;
while idx < l {
let op = (self.files[file].ops[idx]).clone();
match op.op {
Op::StmtClose => {
nesting -= 1;
if nesting == 0 {
return Ok((true, idx + 1));
}
}
Op::If(_) | Op::While(_) | Op::For(_, _) => nesting += 1,
Op::ElseIf(_) | Op::Else => {
if nesting == 1 {
return Ok((false, idx));
}
}
_ => {}
}
idx += 1;
}
Err(HakuError::NoMatchingEndError(tp.to_string(), self.error_extra()))
}
fn exec_cmd(&mut self, cmdline: &str) -> Result<ExecResult, HakuError> {
let cmdline = self.varmgr.interpolate(&cmdline, true);
let mut eres = ExecResult { code: 0, stdout: String::new() };
let mut cmd = Command::new(&self.shell[0]);
for arg in self.shell[1..].iter() {
cmd.arg(arg);
}
cmd.arg(&cmdline);
self.augment_cmd(&mut cmd);
let out = match cmd.output() {
Ok(o) => o,
Err(e) => return Err(HakuError::ExecFailureError(cmdline, e.to_string(), self.error_extra())),
};
if !out.status.success() {
if let Ok(s) = String::from_utf8(out.stderr) {
eprint!("{}", s);
}
return Err(HakuError::ExecFailureError(
cmdline,
format!("exit code {}", out.status.code().unwrap_or(0)),
self.error_extra(),
));
}
if let Ok(s) = String::from_utf8(out.stdout) {
eres.stdout = s.trim_end().to_string();
} else {
eres.stdout = String::from("[Non-UTF-8 Output]");
}
Ok(eres)
}
fn augment_cmd(&self, cmd: &mut Command) {
if !self.cwd_history.is_empty() {
cmd.current_dir(&self.cwd);
}
if !self.varmgr.env.is_empty() {
cmd.envs(&self.varmgr.env);
}
}
fn exec_cmd_shell(&mut self, flags: u32, cmdline: &str) -> Result<(), HakuError> {
let no_fail = is_flag_on(flags, FLAG_PASS);
let cmdline = self.varmgr.interpolate(&cmdline, true);
output!(self.opts.verbosity, 2, "ExecShell[{}]: {}", no_fail, cmdline);
if !is_flag_on(flags, FLAG_QUIET) {
println!("{}", cmdline);
}
let mut cmd = Command::new(&self.shell[0]);
for arg in self.shell[1..].iter() {
cmd.arg(arg);
}
cmd.arg(&cmdline);
self.augment_cmd(&mut cmd);
let result = cmd.status();
let st = match result {
Ok(exit_status) => exit_status,
Err(e) => {
if is_flag_on(flags, FLAG_PASS) {
return Ok(());
}
return Err(HakuError::ExecFailureError(cmdline, e.to_string(), self.error_extra()));
}
};
if !st.success() && !is_flag_on(flags, FLAG_PASS) {
let code = match st.code() {
None => "(unknown exit code)".to_string(),
Some(c) => format!("(exit code: {})", c),
};
return Err(HakuError::ExecFailureError(cmdline, code, self.error_extra()));
}
Ok(())
}
fn exec_either_assign(&mut self, chk: bool, name: &str, ops: &[Op]) -> Result<(), HakuError> {
if chk && self.varmgr.var(name).is_true() {
return Ok(());
}
for op in ops.iter() {
let v = self.exec_op(op)?;
if v.is_true() {
self.varmgr.set_var(name, v);
return Ok(());
}
}
Ok(())
}
fn exec_assign_generic(&mut self, chk: bool, name: &str, ops: &[Op]) -> Result<(), HakuError> {
if chk && self.varmgr.var(name).is_true() {
return Ok(());
}
let cnt = ops.len(); let mut val = false;
for op in ops.iter() {
let v = self.exec_op(op)?;
if cnt == 1 {
self.varmgr.set_var(name, v);
return Ok(());
}
if v.is_true() {
val = true;
break;
}
}
if val {
let v = VarValue::Int(1);
self.varmgr.set_var(name, v);
} else {
let v = VarValue::Int(0);
self.varmgr.set_var(name, v);
}
Ok(())
}
fn exec_assign_or(&mut self, name: &str, ops: &[Op]) -> Result<(), HakuError> {
self.exec_assign_generic(true, name, ops)
}
fn exec_assign(&mut self, name: &str, ops: &[Op]) -> Result<(), HakuError> {
self.exec_assign_generic(false, name, ops)
}
fn exec_and_expr(&mut self, ops: &[Op]) -> Result<VarValue, HakuError> {
let cnt = ops.len();
let mut val = true;
for op in ops.iter() {
let v = self.exec_op(op)?;
if cnt == 1 {
return Ok(v);
}
if !v.is_true() {
val = false;
break;
}
}
if val {
Ok(VarValue::Int(1))
} else {
Ok(VarValue::Int(0))
}
}
fn exec_func(&mut self, name: &str, ops: &[Op]) -> Result<VarValue, HakuError> {
output!(self.opts.verbosity, 2, "Exec func {}, args: {:?}", name, ops);
let mut args: Vec<VarValue> = Vec::new();
for op in ops.iter() {
let v = self.exec_op(op)?;
args.push(v);
}
let r = run_func(name, self, &args);
output!(self.opts.verbosity, 3, "func {} with {} args returned {:?}", name, ops.len(), r);
r.map_err(|s| HakuError::FunctionError(format!("{}: {}", s, self.error_extra())))
}
fn exec_if(&mut self, ops: &[Op], file: usize, idx: usize) -> Result<usize, HakuError> {
output!(self.opts.verbosity, 3, "Exec if");
assert!(ops.len() == 1);
let v = self.exec_op(&ops[0])?;
if v.is_true() {
output!(self.opts.verbosity, 3, " if == true");
self.cond_stack.push(CondItem { line: idx, cond: Condition::If(true) });
Ok(idx + 1)
} else {
output!(self.opts.verbosity, 3, " if == false -> look for else/end");
let (is_end, else_idx) = self.find_else(file, idx + 1, "if")?;
if !is_end {
self.cond_stack.push(CondItem { line: idx, cond: Condition::If(false) });
}
Ok(else_idx)
}
}
fn exec_else(&mut self, file: usize, idx: usize) -> Result<usize, HakuError> {
output!(self.opts.verbosity, 3, "Exec else");
if self.cond_stack.is_empty() {
return Err(HakuError::StrayElseError(self.error_extra()));
}
let op = self.cond_stack[self.cond_stack.len() - 1].clone();
output!(self.opts.verbosity, 3, "else op: {:?}", op);
match op.cond {
Condition::If(c) => {
if c {
self.cond_stack.pop();
Ok(self.find_end(file, idx + 1, "else")?)
} else {
Ok(idx + 1)
}
}
_ => Err(HakuError::StrayElseError(self.error_extra())),
}
}
fn exec_elseif(&mut self, ops: &[Op], file: usize, idx: usize) -> Result<usize, HakuError> {
output!(self.opts.verbosity, 3, "Exec elseif");
if self.cond_stack.is_empty() {
return Err(HakuError::StrayElseIfError(self.error_extra()));
}
assert!(ops.len() == 1);
let op = self.cond_stack[self.cond_stack.len() - 1].clone();
match op.cond {
Condition::If(c) => {
if c {
self.cond_stack.pop();
return Ok(self.find_end(file, idx + 1, "else")?);
}
let v = self.exec_op(&ops[0])?;
if v.is_true() {
let mut cnd = match self.cond_stack.pop() {
Some(cc) => cc,
None => return Err(HakuError::InternalError(self.error_extra())),
};
cnd.cond = Condition::If(true);
self.cond_stack.push(cnd);
Ok(idx + 1)
} else {
let (_, else_idx) = self.find_else(file, idx + 1, "elseif")?;
Ok(else_idx)
}
}
_ => Err(HakuError::StrayElseIfError(self.error_extra())),
}
}
fn exec_while(&mut self, ops: &[Op], idx: usize) -> Result<bool, HakuError> {
output!(self.opts.verbosity, 3, "Exec while {:?}", ops);
assert!(ops.len() == 1);
let v = self.exec_op(&ops[0])?;
if v.is_true() {
let lst: Vec<Op> = ops.to_vec();
self.cond_stack.push(CondItem { line: idx, cond: Condition::While(lst) });
}
Ok(v.is_true())
}
fn exec_end(&mut self) -> Result<usize, HakuError> {
output!(self.opts.verbosity, 3, "Exec end");
if let Some(op) = self.cond_stack.pop() {
output!(self.opts.verbosity, 3, "END OP >> {:?}", op);
match op.cond {
Condition::If(_) => Ok(0), Condition::While(ref ops) => {
assert!(ops.len() == 1);
let val = self.exec_op(&ops[0])?;
if val.is_true() {
let ln = op.line + 1;
self.cond_stack.push(op);
Ok(ln)
} else {
Ok(0)
}
}
Condition::ForList(var, mut vals) => {
output!(self.opts.verbosity, 3, "END FOR LIST: {} = {:?}", var, vals);
if vals.is_empty() {
return Ok(0);
}
let val = vals[0].clone();
vals.remove(0);
self.varmgr.set_var(&var, VarValue::Str(val));
self.cond_stack.push(CondItem { line: op.line, cond: Condition::ForList(var, vals) });
Ok(op.line + 1)
}
Condition::ForInt(var, mut curr, end, step) => {
curr += step;
output!(self.opts.verbosity, 3, "END FOR INT: {} of {}", curr, end);
if (step > 0 && curr >= end) || (step < 0 && curr <= end) {
return Ok(0);
}
self.varmgr.set_var(&var, VarValue::Int(curr));
self.cond_stack.push(CondItem { line: op.line, cond: Condition::ForInt(var, curr, end, step) });
Ok(op.line + 1)
}
}
} else {
Err(HakuError::StrayEndError(self.error_extra()))
}
}
fn exec_break(&mut self, file: usize) -> Result<usize, HakuError> {
output!(self.opts.verbosity, 3, "Exec break");
while let Some(cnd) = self.cond_stack.pop() {
match cnd.cond {
Condition::If(_) => continue,
_ => {
return Ok(self.find_end(file, cnd.line + 1, "break")?);
}
}
}
Err(HakuError::NoMatchingForWhileError(self.error_extra()))
}
fn exec_continue(&mut self, file: usize) -> Result<usize, HakuError> {
output!(self.opts.verbosity, 3, "Exec continue");
let mut next: usize = usize::MAX;
while let Some(cnd) = self.cond_stack.pop() {
match cnd.cond {
Condition::If(_) => continue,
_ => {
next = self.find_end(file, cnd.line + 1, "continue")?;
self.cond_stack.push(cnd);
break;
}
}
}
if next == usize::MAX {
Err(HakuError::NoMatchingForWhileError(self.error_extra()))
} else {
Ok(next - 1)
}
}
fn exec_for(&mut self, name: &str, seq: Seq, idx: usize) -> Result<bool, HakuError> {
output!(self.opts.verbosity, 3, "Exec for");
match seq {
Seq::Int(start, end, step) => {
output!(self.opts.verbosity, 3, " FOR: from {} to {} step {}", start, end, step);
if (step > 0 && end <= start) || (step < 0 && end >= start) {
return Ok(false);
}
if step == 0 {
return Err(HakuError::ForeverForError(self.error_extra()));
}
self.varmgr.set_var(name, VarValue::Int(start));
self.cond_stack
.push(CondItem { line: idx, cond: Condition::ForInt(name.to_string(), start, end, step) });
return Ok(true);
}
Seq::Str(s) => {
output!(self.opts.verbosity, 3, " FOR: whitespace-delimited string {}", s);
let s = self.varmgr.interpolate(&s, false);
let mut v: Vec<String> = if s.find('\n').is_some() {
s.trim_end().split('\n').map(|s| s.trim_end().to_string()).collect()
} else {
s.split_ascii_whitespace().map(|s| s.to_string()).collect()
};
output!(self.opts.verbosity, 3, " FOR whitespace: {:?}", v);
if v.is_empty() {
return Ok(false);
}
self.varmgr.set_var(name, VarValue::Str(v[0].clone()));
v.remove(0);
self.cond_stack.push(CondItem { line: idx, cond: Condition::ForList(name.to_string(), v) });
return Ok(true);
}
Seq::Idents(ids) => {
output!(self.opts.verbosity, 3, " FOR idents: {:?}", ids);
if ids.is_empty() {
return Ok(false);
}
let first = self.varmgr.interpolate(&ids[0], false);
self.varmgr.set_var(name, VarValue::Str(first));
let v: Vec<String> = ids.iter().skip(1).map(|s| self.varmgr.interpolate(s, false)).collect();
self.cond_stack.push(CondItem { line: idx, cond: Condition::ForList(name.to_string(), v) });
return Ok(true);
}
Seq::Exec(s) => match self.exec_cmd(&s) {
Ok(res) => {
if res.code == 0 {
let mut v: Vec<String> = res.stdout.lines().map(|s| s.trim_end().to_string()).collect();
output!(self.opts.verbosity, 3, " FOR exec: {:?}", v);
if v.is_empty() {
return Ok(false);
}
self.varmgr.set_var(name, VarValue::Str(v[0].clone()));
v.remove(0);
self.cond_stack.push(CondItem { line: idx, cond: Condition::ForList(name.to_string(), v) });
return Ok(true);
} else {
output!(self.opts.verbosity, 3, " FOR exec: FAILURE");
};
}
Err(_) => {
output!(self.opts.verbosity, 3, " FOR exec: FAILURE[2]");
}
},
Seq::Var(s) => {
output!(self.opts.verbosity, 3, " FOR var ${}", s);
let val = self.varmgr.var(&s);
match val {
VarValue::Undefined => {}
VarValue::Int(start) => {
output!(self.opts.verbosity, 3, " FOR var int ${} = {}", s, start);
self.varmgr.set_var(name, VarValue::Int(start));
self.cond_stack
.push(CondItem { line: idx, cond: Condition::ForInt(name.to_string(), start, start, 1) });
return Ok(true);
}
VarValue::List(vc) => {
output!(self.opts.verbosity, 3, " FOR var list ${} = {:?}", s, vc);
let mut first = true;
let mut v: Vec<String> = Vec::new();
for s in vc.iter() {
if first {
self.varmgr.set_var(name, VarValue::from(s.to_string()));
first = false;
} else {
v.push(s.to_string());
}
}
self.cond_stack.push(CondItem { line: idx, cond: Condition::ForList(name.to_string(), v) });
return Ok(true);
}
VarValue::Exec(ex) => {
output!(self.opts.verbosity, 3, " FOR var exec ${} = {:?}", s, ex);
if ex.code != 0 || ex.stdout.is_empty() {
return Ok(false);
}
let mut v: Vec<String> =
ex.stdout.trim_end().split('\n').map(|s| s.trim_end().to_string()).collect();
self.varmgr.set_var(name, VarValue::Str(v[0].clone()));
v.remove(0);
self.cond_stack.push(CondItem { line: idx, cond: Condition::ForList(name.to_string(), v) });
return Ok(true);
}
VarValue::Str(st) => {
output!(self.opts.verbosity, 3, " FOR var str ${} = {}", s, st);
if st.is_empty() {
return Ok(false);
}
let mut v: Vec<String> = if st.find('\n').is_some() {
st.trim_end().split('\n').map(|s| s.trim_end().to_string()).collect()
} else {
st.trim_end().split_whitespace().map(|s| s.to_string()).collect()
};
self.varmgr.set_var(name, VarValue::Str(v[0].clone()));
v.remove(0);
self.cond_stack.push(CondItem { line: idx, cond: Condition::ForList(name.to_string(), v) });
return Ok(true);
}
}
}
}
Ok(false)
}
fn interpolate_path(&self, path: &str) -> String {
if path != "~" && !path.starts_with("~/") && !path.starts_with("~\\") {
return path.to_string();
}
let home = match dirs::home_dir() {
None => {
eprintln!("Failed to get user's home directory");
return path.to_string();
}
Some(p) => p.to_string_lossy().to_string(),
};
if path == "~" {
return home;
}
let rest = path.trim_start_matches("~/").trim_start_matches("~\\");
let pb = PathBuf::from(home);
pb.join(PathBuf::from(rest)).to_string_lossy().to_string()
}
fn exec_cd(&mut self, flags: u32, path: &str) -> Result<(), HakuError> {
output!(self.opts.verbosity, 3, "Exec cd");
let path = self.varmgr.interpolate(&path, true);
let path = self.interpolate_path(&path);
if !is_flag_on(flags, FLAG_QUIET) {
println!("cd {}", path);
}
if path == "-" {
if !self.cwd_history.is_empty() {
self.cwd = self.cwd_history.pop().unwrap_or_else(|| self.cwd.clone());
}
return Ok(());
}
if path == ".." {
let mut p = self.cwd.clone();
p.pop();
mem::swap(&mut self.cwd, &mut p);
self.cwd_history.push(p);
return Ok(());
}
let fspath = PathBuf::from(path);
let mut full_path = if fspath.is_absolute() {
fspath
} else {
let mut p = self.cwd.clone();
p.push(fspath);
p
};
if !full_path.is_dir() {
return Err(HakuError::CdError(full_path.to_string_lossy().to_string(), self.error_extra()));
}
mem::swap(&mut self.cwd, &mut full_path);
self.cwd_history.push(full_path);
Ok(())
}
fn exec_compare(&mut self, cmp_op: &str, args: &[Op]) -> Result<VarValue, HakuError> {
assert!(args.len() == 2);
let v1 = self.exec_op(&args[0])?;
let v2 = self.exec_op(&args[1])?;
if v1.cmp(&v2, cmp_op) {
Ok(VarValue::Int(1))
} else {
Ok(VarValue::Int(0))
}
}
fn exec_op(&mut self, op: &Op) -> Result<VarValue, HakuError> {
match op {
Op::Int(i) => Ok(VarValue::Int(*i)),
Op::Str(s) => {
let s = self.varmgr.interpolate(&s, false);
Ok(VarValue::Str(s))
}
Op::Var(name) => Ok(self.varmgr.var(name)),
Op::Exec(s) => match self.exec_cmd(s) {
Err(_) => Ok(VarValue::Undefined),
Ok(er) => Ok(VarValue::Exec(er)),
},
Op::Not(ops) => {
if let Some(o) = ops.iter().next() {
let v = self.exec_op(o)?;
if v.is_true() {
return Ok(VarValue::Int(0));
} else {
return Ok(VarValue::Int(1));
}
}
unreachable!()
}
Op::AndExpr(ops) => self.exec_and_expr(ops),
Op::Func(name, ops) => self.exec_func(&name, &ops),
Op::Compare(cmp_op, ops) => self.exec_compare(cmp_op, ops),
_ => unreachable!(),
}
}
fn exec_pause(&mut self) -> Result<(), HakuError> {
output!(self.opts.verbosity, 3, "Exec pause");
{
let stdout = io::stdout();
let mut handle = stdout.lock();
if let Err(_e) = handle.write_all(b"Press Enter to continue...") {
return Err(HakuError::InternalError(self.error_extra()));
}
if let Err(_e) = handle.flush() {
return Err(HakuError::InternalError(self.error_extra()));
};
}
let mut _input = String::new();
match io::stdin().read_line(&mut _input) {
Ok(_) => Ok(()),
Err(_) => Err(HakuError::InternalError(self.error_extra())),
}
}
fn enter_recipe(&mut self, recipe: &RecipeItem) {
output!(self.opts.verbosity, 2, "enter recipe. Vars {:?}, Free {:?}", recipe.vars, self.varmgr.free);
if recipe.vars.is_empty() || self.varmgr.free.is_empty() {
return;
}
let mut idx = 0usize;
for v in recipe.vars.iter() {
if v.starts_with('+') {
let nm = v.trim_start_matches('+');
let mut out = Vec::new();
while idx < self.varmgr.free.len() {
out.push(self.varmgr.free[idx].clone());
idx += 1;
}
self.varmgr.set_recipe_var(nm, VarValue::List(out));
return;
} else {
self.varmgr.set_recipe_var(v, VarValue::Str(self.varmgr.free[idx].clone()));
idx += 1;
if idx >= self.varmgr.free.len() {
return;
}
}
}
}
fn leave_recipe(&mut self) {
self.varmgr.recipe_vars.clear();
self.cond_stack.clear();
}
}
#[cfg(test)]
mod vm_test {
use super::*;
use std::mem;
struct Prs {
expr: &'static str,
tp: Op,
}
#[test]
fn load() {
let opts = RunOpts::new();
let mut vm = Engine::new(opts);
let res = vm.load_from_str("recipe: deps");
assert!(res.is_ok());
assert_eq!(vm.files.len(), 1);
assert_eq!(vm.recipes.len(), 1);
assert_eq!(vm.files[0].ops.len(), 1);
assert_eq!(vm.files[0].disabled.len(), 0);
assert_eq!(
mem::discriminant(&vm.files[0].ops[0].op),
mem::discriminant(&Op::Recipe(String::new(), 0, Vec::new(), Vec::new()))
);
}
#[test]
fn ops() {
let parses: Vec<Prs> = vec![
Prs { expr: "run('cmd')", tp: Op::Func(String::new(), Vec::new()) },
Prs { expr: "run('cmd', `abs`, inner(10,2,3))", tp: Op::Func(String::new(), Vec::new()) },
Prs { expr: "END", tp: Op::StmtClose },
Prs { expr: "Return", tp: Op::Return },
Prs { expr: "ELse", tp: Op::Else },
Prs { expr: "brEAk", tp: Op::Break },
Prs { expr: "continuE", tp: Op::Continue },
Prs { expr: "a = `ls` || `dir` && 12 == 'zcv'", tp: Op::Assign(String::new(), Vec::new()) },
Prs { expr: "a = `ls` || !`dir` && 12 || $ui != 'zcv'", tp: Op::Assign(String::new(), Vec::new()) },
Prs { expr: "a ?= `ls` || `dir` || 'default'", tp: Op::DefAssign(String::new(), Vec::new()) },
Prs { expr: "a = `ls` ? `dir` ? 'default'", tp: Op::EitherAssign(false, String::new(), Vec::new()) },
Prs { expr: "a ?= `ls` ? `dir` ? 'default'", tp: Op::EitherAssign(false, String::new(), Vec::new()) },
Prs { expr: "if $a > `dir | wc -l` || $b == 'test${zef}':", tp: Op::If(Vec::new()) },
Prs { expr: "if $a > `dir | wc -l` || $b == 'test${zef}' ; do", tp: Op::If(Vec::new()) },
Prs { expr: "if $a > `dir | wc -l` || $b == 'test${zef}' then", tp: Op::If(Vec::new()) },
Prs { expr: "while `ping ${ip}`:", tp: Op::While(Vec::new()) },
Prs { expr: "while `ping ${ip}` && $b == 90 do", tp: Op::While(Vec::new()) },
Prs { expr: "for a in 1..2:", tp: Op::For(String::new(), Seq::Int(0, 0, 0)) },
Prs { expr: "for a in 1..2..8 do", tp: Op::For(String::new(), Seq::Int(0, 0, 0)) },
Prs { expr: "for a in 'a b c d' then", tp: Op::For(String::new(), Seq::Int(0, 0, 0)) },
Prs { expr: "for a in a b c d :", tp: Op::For(String::new(), Seq::Int(0, 0, 0)) },
Prs { expr: "for a in `dir *.*`", tp: Op::For(String::new(), Seq::Int(0, 0, 0)) },
Prs { expr: "for a in \"acd def\" \"fgh er\"", tp: Op::For(String::new(), Seq::Int(0, 0, 0)) },
Prs { expr: "for a in ${var}", tp: Op::For(String::new(), Seq::Int(0, 0, 0)) },
];
for p in parses {
let opts = RunOpts::new();
let mut vm = Engine::new(opts);
let res = vm.load_from_str(p.expr);
assert!(res.is_ok());
println!("{}", p.expr);
assert_eq!(mem::discriminant(&vm.files[0].ops[0].op), mem::discriminant(&p.tp));
}
}
}