erg_common 0.4.2

A common components library of Erg
Documentation
//! defines a command-line parser for `ergc`.
//!
//! コマンドオプション(パーサー)を定義する
use std::env;
use std::fs::File;
use std::io::{stdin, BufRead, BufReader, Read};
use std::process;

use crate::stdin::GLOBAL_STDIN;
use crate::Str;
use crate::{power_assert, read_file};

pub const SEMVER: &str = env!("CARGO_PKG_VERSION");
pub const GIT_HASH_SHORT: &str = env!("GIT_HASH_SHORT");
pub const BUILD_DATE: &str = env!("BUILD_DATE");

/// 入力はファイルからだけとは限らないので
/// Inputで操作を一本化する
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Input {
    /// filename
    File(Str),
    REPL,
    /// same content as cfg.command
    Pipe(Str),
    /// from command option | eval
    Str(Str),
    Dummy,
}

impl Input {
    pub fn is_repl(&self) -> bool {
        matches!(self, Input::REPL)
    }

    pub fn enclosed_name(&self) -> &str {
        match self {
            Self::File(filename) => &filename[..],
            Self::REPL | Self::Pipe(_) => "<stdin>",
            Self::Str(_) => "<string>",
            Self::Dummy => "<dummy>",
        }
    }

    /// ファイルに書き出すとき使う
    pub fn filename(&self) -> &str {
        match self {
            Self::File(filename) => &filename[..],
            Self::REPL | Self::Pipe(_) => "stdin",
            Self::Str(_) => "string",
            Self::Dummy => "dummy",
        }
    }

    pub fn read(&self) -> Str {
        match self {
            Self::File(filename) => {
                let file = match File::open(&filename[..]) {
                    Ok(f) => f,
                    Err(e) => {
                        let code = e.raw_os_error().unwrap_or(1);
                        println!("cannot open '{filename}': [Errno {code}] {e}");
                        process::exit(code);
                    }
                };
                let src = match read_file(file) {
                    Ok(s) => s,
                    Err(e) => {
                        let code = e.raw_os_error().unwrap_or(1);
                        println!("cannot read '{filename}': [Errno {code}] {e}");
                        process::exit(code);
                    }
                };
                Str::from(src)
            }
            Self::Pipe(s) | Self::Str(s) => s.clone(),
            Self::REPL => GLOBAL_STDIN.read(),
            Self::Dummy => panic!("cannot read from a dummy file"),
        }
    }

    pub fn reread_lines(&self, ln_begin: usize, ln_end: usize) -> Vec<Str> {
        power_assert!(ln_begin, >=, 1);
        match self {
            Self::File(filename) => match File::open(&filename[..]) {
                Ok(file) => {
                    let mut codes = vec![];
                    let mut lines = BufReader::new(file).lines().skip(ln_begin - 1);
                    for _ in ln_begin..=ln_end {
                        codes.push(Str::from(lines.next().unwrap().unwrap()));
                    }
                    codes
                }
                Err(_) => vec!["<file not found>".into()],
            },
            Self::Pipe(s) | Self::Str(s) => s.split('\n').collect::<Vec<_>>()
                [ln_begin - 1..=ln_end - 1]
                .iter()
                .map(|s| Str::rc(*s))
                .collect(),
            Self::REPL => GLOBAL_STDIN.reread_lines(ln_begin, ln_end),
            Self::Dummy => panic!("cannot read lines from a dummy file"),
        }
    }

    pub fn reread(&self) -> Str {
        match self {
            Self::File(_filename) => todo!(),
            Self::Pipe(s) | Self::Str(s) => s.clone(),
            Self::REPL => Str::from(GLOBAL_STDIN.reread().trim_end().to_owned()),
            Self::Dummy => panic!("cannot read from a dummy file"),
        }
    }
}

#[derive(Debug, Clone)]
pub struct ErgConfig {
    /// options: lex | parse | compile | exec
    pub mode: &'static str,
    /// optimization level.
    /// * 0: no optimization
    /// * 1 (default): e.g. constant folding, dead code elimination
    /// * 2: e.g. static dispatching, inlining, peephole
    /// * 3: e.g. JIT compiling
    pub opt_level: u8,
    pub dump_as_pyc: bool,
    pub python_ver: Option<u32>,
    pub py_server_timeout: u64,
    pub input: Input,
    pub module: &'static str,
    /// verbosity level for system messages.
    /// * 0: display errors
    /// * 1: display errors and warns
    /// * 2 (default): display errors, warnings and hints
    pub verbose: u8,
}

impl Default for ErgConfig {
    #[inline]
    fn default() -> Self {
        let is_stdin_piped: bool = atty::isnt(atty::Stream::Stdin);
        let input = if is_stdin_piped {
            let mut buffer = String::new();
            stdin().read_to_string(&mut buffer).unwrap();
            Input::Pipe(Str::from(buffer))
        } else {
            Input::REPL
        };
        Self {
            mode: "exec",
            opt_level: 1,
            dump_as_pyc: false,
            python_ver: None,
            py_server_timeout: 10,
            input,
            module: "<module>",
            verbose: 2,
        }
    }
}

impl ErgConfig {
    /// cloneのエイリアス(実際のcloneコストは低いので)
    #[inline]
    pub fn copy(&self) -> Self {
        self.clone()
    }

    pub fn parse() -> Self {
        let mut args = env::args();
        args.next(); // "ergc"
        let mut cfg = Self::default();
        // ループ内でnextするのでforにしないこと
        while let Some(arg) = args.next() {
            match &arg[..] {
                "-c" => {
                    cfg.input = Input::Str(Str::from(args.next().unwrap()));
                }
                "--dump-as-pyc" => {
                    cfg.dump_as_pyc = true;
                }
                "-?" | "-h" | "--help" => {
                    println!("erg [option] ... [-c cmd | -m mod | file | -] [arg] ...");
                    println!("-c cmd : program passed in as string");
                    println!("-m mod : module to be executed");
                    println!("-?/-h  : show this help");
                    println!("--dump-as-pyc: dump as .pyc file");
                    println!("--mode lex|parse|compile|exec: execution mode");
                    println!("--opt-level/-o 0|1|2|3: optimization level");
                    println!("--python-version/-p (uint 32 number): Python version");
                    println!(
                        "--py-server-timeout (uint 64 number): timeout for the Python REPL server"
                    );
                    println!("--verbose 0|1|2: verbosity level");
                    println!("--version/-V: show version");
                    println!("file : program read from script file");
                    // TODO:
                    process::exit(0);
                }
                "-m" => {
                    cfg.module = Box::leak(args.next().unwrap().into_boxed_str());
                }
                "--mode" => {
                    cfg.mode = Box::leak(args.next().unwrap().into_boxed_str());
                }
                "-o" | "--opt-level" | "--optimization-level" => {
                    cfg.opt_level = args.next().unwrap().parse::<u8>().unwrap();
                }
                "-p" | "--py-ver" | "--python-version" => {
                    cfg.python_ver = Some(args.next().unwrap().parse::<u32>().unwrap());
                }
                "--py-server-timeout" => {
                    cfg.py_server_timeout = args.next().unwrap().parse::<u64>().unwrap();
                }
                "--verbose" => {
                    cfg.verbose = args.next().unwrap().parse::<u8>().unwrap();
                }
                "-V" | "--version" => {
                    println!("Erg {}", env!("CARGO_PKG_VERSION"));
                    process::exit(0);
                }
                other if other.starts_with('-') => {
                    panic!("invalid option: {other}");
                }
                _ => {
                    cfg.input = Input::File(Str::from(arg));
                    break;
                }
            }
        }
        cfg
    }
}