rcshell 0.0.1-alpha.3

plan9 rc in rust
//
// rcshell: Plan9 rc shell in Rust
// src/main.rs: Main entry point
//
// Copyright (c) 2024 Ali Polatel <alip@exherbo.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later

mod lexer;

mod built_info {
    // The file has been placed there by the build script.
    include!(concat!(env!("OUT_DIR"), "/built.rs"));
}

use std::io::{stdin, Read};

use anyhow::{bail, Result};
use clap::Arg;
use is_terminal::IsTerminal;
use lexer::*;
use rustyline::{error::ReadlineError, DefaultEditor};

fn main() -> anyhow::Result<()> {
    let matches = clap::Command::new(built_info::PKG_NAME)
        .about(built_info::PKG_DESCRIPTION)
        .author(built_info::PKG_AUTHORS)
        .version(built_info::PKG_VERSION)
        .help_expected(true)
        .next_line_help(false)
        .max_term_width(72)
        .help_template(
            r#"
{before-help}{name} {version}
{about}
Copyright (c) 2023, 2024 {author}
SPDX-License-Identifier: GPL-3.0-or-later

{usage-heading} {usage}

{all-args}{after-help}
"#,
        )
        .after_help(format!(
            "\
Hey you, out there beyond the wall,
Breaking bottles in the hall,
Can you help me?

Send bug reports to {}
Attaching poems encourages consideration tremendously.

License: {}
Homepage: {}
Repository: {}
",
            built_info::PKG_AUTHORS,
            built_info::PKG_LICENSE,
            built_info::PKG_HOMEPAGE,
            built_info::PKG_REPOSITORY,
        ))
        .arg(
            Arg::new("command")
                .short('c')
                .help("Commands are read from string")
                .num_args(1),
        )
        .arg(
            Arg::new("login")
                .short('l')
                .help("Ignored (not implemented yet)")
                .num_args(0),
        )
        .get_matches();

    if let Some(command) = matches.try_get_one::<String>("command").expect("command") {
        eval(command)?;
        return Ok(());
    }

    if !stdin().is_terminal() {
        let mut buffer = String::new();
        stdin().read_to_string(&mut buffer)?;
        eval(&buffer)?;
        return Ok(());
    }

    let mut rl = match DefaultEditor::new() {
        Ok(editor) => editor,
        Err(error) => {
            bail!("! {error:?}");
        }
    };
    let mut prompt = "; ";
    loop {
        let mut command = String::new();
        loop {
            let readline = rl.readline(prompt);
            match readline {
                Ok(line) => {
                    if line.ends_with('\\') {
                        // Remove the trailing backslash and continue reading the next line
                        command.push_str(&line[..line.len() - 1]);
                        command.push(' '); // Replace backslash with a space
                        prompt = ""; // Change prompt.
                        continue;
                    } else {
                        command.push_str(&line);
                        prompt = "; "; // Restore prompt.
                        break;
                    }
                }
                Err(ReadlineError::Eof) | Err(ReadlineError::Interrupted) => {
                    return Ok(());
                }
                Err(error) => return Err(error.into()),
            }
        }

        if !command.trim().is_empty() {
            println!("< {command}");
            if let Err(error) = rl.add_history_entry(command.as_str()) {
                eprintln!("? {error:?}");
            }
            if let Err(error) = eval(&command) {
                eprintln!("? {error:?}");
            }
        }
    }
}

fn eval(input: &str) -> Result<()> {
    match tokenize(&input) {
        Ok(token) => {
            println!("> {token:?}");
            Ok(())
        }
        Err(error) => {
            println!("! {error:?}");
            Err(error)
        }
    }
}