use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct Program {
pub statements: Vec<Stmt>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Stmt {
Assignment(Assignment),
Command(Command),
Pipeline(Pipeline),
If(IfStmt),
For(ForLoop),
While(WhileLoop),
Case(CaseStmt),
Break(Option<usize>),
Continue(Option<usize>),
Return(Option<Box<Expr>>),
Exit(Option<Box<Expr>>),
ToolDef(ToolDef),
Test(TestExpr),
AndChain { left: Box<Stmt>, right: Box<Stmt> },
OrChain { left: Box<Stmt>, right: Box<Stmt> },
EnvScoped { assignments: Vec<Assignment>, body: Box<Stmt> },
Empty,
}
impl Stmt {
pub fn kind_name(&self) -> &'static str {
match self {
Stmt::Assignment(_) => "assignment",
Stmt::Command(_) => "command",
Stmt::Pipeline(_) => "pipeline",
Stmt::If(_) => "if",
Stmt::For(_) => "for",
Stmt::While(_) => "while",
Stmt::Case(_) => "case",
Stmt::Break(_) => "break",
Stmt::Continue(_) => "continue",
Stmt::Return(_) => "return",
Stmt::Exit(_) => "exit",
Stmt::ToolDef(_) => "tooldef",
Stmt::Test(_) => "test",
Stmt::AndChain { .. } => "and_chain",
Stmt::OrChain { .. } => "or_chain",
Stmt::EnvScoped { .. } => "env_scoped",
Stmt::Empty => "empty",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Assignment {
pub path: VarPath,
pub value: Expr,
pub local: bool,
}
impl Assignment {
pub fn name(&self) -> &str {
match self.path.segments.first() {
Some(VarSegment::Field(name)) => name,
_ => unreachable!("Assignment.path always starts with a root Field segment"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Command {
pub name: String,
pub args: Vec<Arg>,
pub redirects: Vec<Redirect>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum PipelineStage {
Command(Command),
Compound(Box<Stmt>),
}
impl PipelineStage {
pub fn as_command(&self) -> Option<&Command> {
match self {
PipelineStage::Command(cmd) => Some(cmd),
PipelineStage::Compound(_) => None,
}
}
pub fn redirects(&self) -> &[Redirect] {
match self {
PipelineStage::Command(cmd) => &cmd.redirects,
PipelineStage::Compound(_) => &[],
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Pipeline {
pub stages: Vec<PipelineStage>,
pub background: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct IfStmt {
pub condition: Box<Expr>,
pub then_branch: Vec<Stmt>,
pub else_branch: Option<Vec<Stmt>>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ForLoop {
pub variable: String,
pub items: Vec<Expr>,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct WhileLoop {
pub condition: Box<Expr>,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CaseStmt {
pub expr: Expr,
pub branches: Vec<CaseBranch>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CaseBranch {
pub patterns: Vec<String>,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ToolDef {
pub name: String,
pub params: Vec<ParamDef>,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParamDef {
pub name: String,
pub param_type: Option<ParamType>,
pub default: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ParamType {
String,
Int,
Float,
Bool,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Arg {
Positional(Expr),
Named { key: String, value: Expr },
WordAssign { key: String, value: Expr },
ShortFlag(String),
LongFlag(String),
DoubleDash,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Redirect {
pub kind: RedirectKind,
pub target: Expr,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum RedirectKind {
StdoutOverwrite,
StdoutAppend,
Stdin,
HereDoc(HereDocMeta),
HereString,
Stderr,
Both,
MergeStderr,
MergeStdout,
}
#[derive(Debug, Clone, PartialEq)]
pub struct HereDocMeta {
pub delimiter: String,
pub literal: bool,
pub strip_tabs: bool,
pub body: String,
pub body_offset: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpannedPart {
pub part: StringPart,
pub offset: usize,
pub len: usize,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Expr {
Literal(Value),
VarRef(VarPath),
Interpolated(Vec<StringPart>),
HereDocBody {
parts: Vec<SpannedPart>,
strip_tabs: bool,
},
Not(Box<Expr>),
BinaryOp {
left: Box<Expr>,
op: BinaryOp,
right: Box<Expr>,
},
CommandSubst(Vec<Stmt>),
Test(Box<TestExpr>),
Positional(usize),
AllArgs,
ArgCount,
VarLength(VarPath),
VarWithDefault { path: VarPath, default: Vec<StringPart> },
Arithmetic(String),
Command(Command),
LastExitCode,
CurrentPid,
GlobPattern(String),
ListLiteral(Vec<ListElem>),
RecordLiteral(Vec<RecordEntry>),
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ListElem {
Item(Expr),
Spread(Expr),
}
#[derive(Debug, Clone, PartialEq)]
pub struct RecordEntry {
pub key: RecordKey,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum RecordKey {
Bare(String),
Quoted(String),
Interpolated(Vec<StringPart>),
}
pub(crate) fn spread_value_kind(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a bool",
Value::Int(_) => "an int",
Value::Float(_) => "a float",
Value::String(_) => "a string",
Value::Bytes(_) => "bytes",
Value::Json(serde_json::Value::Object(_)) => "a record",
Value::Json(serde_json::Value::Array(_)) => "a list",
Value::Json(_) => "a json scalar",
}
}
pub(crate) fn spread_non_list_message(value: &Value) -> String {
format!(
"cannot spread `...` — value is {}, not a list; spread only flattens a list's elements, e.g. `[...$xs date]`",
spread_value_kind(value)
)
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum TestExpr {
FileTest { op: FileTestOp, path: Box<Expr> },
StringTest { op: StringTestOp, value: Box<Expr> },
Comparison { left: Box<Expr>, op: TestCmpOp, right: Box<Expr> },
And { left: Box<TestExpr>, right: Box<TestExpr> },
Or { left: Box<TestExpr>, right: Box<TestExpr> },
Not { expr: Box<TestExpr> },
In { left: Box<Expr>, right: Box<Expr> },
NotIn { left: Box<Expr>, right: Box<Expr> },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FileTestOp {
Exists,
IsFile,
IsDir,
Readable,
Writable,
Executable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum StringTestOp {
IsEmpty,
IsNonEmpty,
IsList,
IsRecord,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TestCmpOp {
Eq,
NotEq,
Match,
NotMatch,
Gt,
Lt,
GtEq,
LtEq,
NumEq,
NumNotEq,
NumGt,
NumLt,
NumGtEq,
NumLtEq,
}
pub use kaish_types::Value;
#[derive(Debug, Clone, PartialEq)]
pub struct VarPath {
pub segments: Vec<VarSegment>,
}
impl VarPath {
pub fn simple(name: impl Into<String>) -> Self {
Self {
segments: vec![VarSegment::Field(normalize_name(name.into()))],
}
}
}
pub(crate) fn normalize_name(name: String) -> String {
use unicode_normalization::{is_nfc_quick, IsNormalized, UnicodeNormalization};
if name.is_ascii() || is_nfc_quick(name.chars()) == IsNormalized::Yes {
return name;
}
name.nfc().collect()
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum VarSegment {
Field(String),
Index(i64),
Key(String),
Dynamic(String),
Slice(Option<i64>, Option<i64>),
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum StringPart {
Literal(String),
Var(VarPath),
VarWithDefault { path: VarPath, default: Vec<StringPart> },
VarLength(VarPath),
Positional(usize),
AllArgs,
ArgCount,
Arithmetic(String),
CommandSubst(Vec<Stmt>),
LastExitCode,
CurrentPid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinaryOp {
And,
Or,
}
impl fmt::Display for BinaryOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BinaryOp::And => write!(f, "&&"),
BinaryOp::Or => write!(f, "||"),
}
}
}
impl fmt::Display for RedirectKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RedirectKind::StdoutOverwrite => write!(f, ">"),
RedirectKind::StdoutAppend => write!(f, ">>"),
RedirectKind::Stdin => write!(f, "<"),
RedirectKind::HereDoc(_) => write!(f, "<<"),
RedirectKind::HereString => write!(f, "<<<"),
RedirectKind::Stderr => write!(f, "2>"),
RedirectKind::Both => write!(f, "&>"),
RedirectKind::MergeStderr => write!(f, "2>&1"),
RedirectKind::MergeStdout => write!(f, "1>&2"),
}
}
}
impl fmt::Display for FileTestOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileTestOp::Exists => write!(f, "-e"),
FileTestOp::IsFile => write!(f, "-f"),
FileTestOp::IsDir => write!(f, "-d"),
FileTestOp::Readable => write!(f, "-r"),
FileTestOp::Writable => write!(f, "-w"),
FileTestOp::Executable => write!(f, "-x"),
}
}
}
impl fmt::Display for StringTestOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StringTestOp::IsEmpty => write!(f, "-z"),
StringTestOp::IsNonEmpty => write!(f, "-n"),
StringTestOp::IsList => write!(f, "-list"),
StringTestOp::IsRecord => write!(f, "-record"),
}
}
}
impl StringTestOp {
pub fn matches_shape(self, value: &Value) -> bool {
matches!(
(self, value),
(StringTestOp::IsList, Value::Json(serde_json::Value::Array(_)))
| (StringTestOp::IsRecord, Value::Json(serde_json::Value::Object(_)))
)
}
}
impl fmt::Display for TestCmpOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TestCmpOp::Eq => write!(f, "=="),
TestCmpOp::NotEq => write!(f, "!="),
TestCmpOp::Match => write!(f, "=~"),
TestCmpOp::NotMatch => write!(f, "!~"),
TestCmpOp::Gt => write!(f, ">"),
TestCmpOp::Lt => write!(f, "<"),
TestCmpOp::GtEq => write!(f, ">="),
TestCmpOp::LtEq => write!(f, "<="),
TestCmpOp::NumEq => write!(f, "-eq"),
TestCmpOp::NumNotEq => write!(f, "-ne"),
TestCmpOp::NumGt => write!(f, "-gt"),
TestCmpOp::NumLt => write!(f, "-lt"),
TestCmpOp::NumGtEq => write!(f, "-ge"),
TestCmpOp::NumLtEq => write!(f, "-le"),
}
}
}