regast-cli 0.1.0

Command-line interface for regast
use std::process::ExitCode;

use clap::{Parser, Subcommand, ValueEnum};
use regast::{Backend, Disambiguation, JsonShape, Regast};

#[derive(Debug, Parser)]
#[command(
    name = "regast",
    version,
    about = "Inspect regular-expression syntax and derivations"
)]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    /// Print the lossless pattern AST.
    Ast {
        pattern: String,
        #[arg(long, value_enum, default_value_t = Format::Json)]
        format: Format,
        #[arg(long, value_enum, default_value_t = Shape::Tree)]
        shape: Shape,
    },
    /// Parse an entire input and print its derivation tree.
    Parse {
        pattern: String,
        input: String,
        #[arg(long, value_enum, default_value_t = Format::Json)]
        format: Format,
        #[arg(long, conflicts_with = "greedy")]
        posix: bool,
        #[arg(long)]
        greedy: bool,
        #[arg(long, value_enum, default_value_t = BackendArg::Derivative)]
        backend: BackendArg,
    },
    /// Find the leftmost-longest match and print its derivation tree.
    Find {
        pattern: String,
        input: String,
        #[arg(long, value_enum, default_value_t = Format::Json)]
        format: Format,
        #[arg(long)]
        greedy: bool,
        #[arg(long, value_enum, default_value_t = BackendArg::Derivative)]
        backend: BackendArg,
    },
    /// Check an entire input, reporting the result by exit status.
    Check {
        pattern: String,
        input: String,
        #[arg(long, value_enum, default_value_t = BackendArg::Derivative)]
        backend: BackendArg,
    },
    /// Dump the forward derivative trace.
    Trace { pattern: String, input: String },
}

#[derive(Clone, Copy, Debug, Default, ValueEnum)]
enum Format {
    #[default]
    Json,
    Sexpr,
    Dot,
}

#[derive(Clone, Copy, Debug, Default, ValueEnum)]
enum Shape {
    #[default]
    Tree,
    Arena,
}

#[derive(Clone, Copy, Debug, Default, ValueEnum)]
enum BackendArg {
    #[default]
    Derivative,
    Antimirov,
    TaggedNfa,
}

impl From<BackendArg> for Backend {
    fn from(value: BackendArg) -> Self {
        match value {
            BackendArg::Derivative => Self::Derivative,
            BackendArg::Antimirov => Self::Antimirov,
            BackendArg::TaggedNfa => Self::TaggedNfa,
        }
    }
}

fn main() -> ExitCode {
    match run(Cli::parse()) {
        Ok(code) => code,
        Err(error) => {
            eprintln!("{error}");
            ExitCode::from(2)
        }
    }
}

fn run(cli: Cli) -> Result<ExitCode, Box<dyn std::error::Error>> {
    match cli.command {
        Command::Ast {
            pattern,
            format,
            shape,
        } => {
            let regex = Regast::new(&pattern)?;
            let output = match format {
                Format::Json => serde_json::to_string_pretty(&regex.ast().to_json(match shape {
                    Shape::Tree => JsonShape::Tree,
                    Shape::Arena => JsonShape::Arena,
                }))?,
                Format::Sexpr => regex.ast().to_sexpr(),
                Format::Dot => regex.ast().to_dot(),
            };
            println!("{output}");
            Ok(ExitCode::SUCCESS)
        }
        Command::Parse {
            pattern,
            input,
            format,
            posix: _,
            greedy,
            backend,
        } => {
            let disambiguation = if greedy {
                Disambiguation::Greedy
            } else {
                Disambiguation::Posix
            };
            let regex = Regast::builder(&pattern)
                .disambiguation(disambiguation)
                .backend(backend.into())
                .build()?;
            let tree = regex.parse(&input)?;
            let output = match format {
                Format::Json => serde_json::to_string_pretty(&tree.to_json())?,
                Format::Sexpr => tree.to_sexpr(),
                Format::Dot => tree.to_dot(),
            };
            println!("{output}");
            Ok(ExitCode::SUCCESS)
        }
        Command::Check {
            pattern,
            input,
            backend,
        } => {
            let regex = Regast::builder(&pattern).backend(backend.into()).build()?;
            Ok(if regex.is_match(&input) {
                ExitCode::SUCCESS
            } else {
                ExitCode::from(1)
            })
        }
        Command::Find {
            pattern,
            input,
            format,
            greedy,
            backend,
        } => {
            let regex = Regast::builder(&pattern)
                .disambiguation(if greedy {
                    Disambiguation::Greedy
                } else {
                    Disambiguation::Posix
                })
                .backend(backend.into())
                .build()?;
            let Some(tree) = regex.find_parse(&input)? else {
                return Ok(ExitCode::from(1));
            };
            let output = match format {
                Format::Json => serde_json::to_string_pretty(&tree.to_json())?,
                Format::Sexpr => tree.to_sexpr(),
                Format::Dot => tree.to_dot(),
            };
            println!("{output}");
            Ok(ExitCode::SUCCESS)
        }
        Command::Trace { pattern, input } => {
            let regex = Regast::new(&pattern)?;
            for step in regex.trace(&input) {
                println!(
                    "{} {:?}: {} -> {}",
                    step.char_index, step.c, step.before, step.after
                );
            }
            Ok(ExitCode::SUCCESS)
        }
    }
}