jimtcl 0.5.0-beta4

Embed Jim Tcl in Rust.
Documentation
//! Command-line shells.

use std::env::current_exe;
use std::ffi::{OsStr, OsString};
use std::io::{self, IsTerminal};
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;

use clap::Parser;
use log::*;

use crate::error::ExitCode;
use crate::{Interp, JimObject, JimResult};
use crate::{JimError, sys};

/// Identifier for types of Tcl input sources.
pub enum TclInput<'cli> {
    Stdin,
    Interactive,
    Script(&'cli str),
    File(&'cli OsStr),
}

/// Rust wrapper to implement jimsh
#[derive(Parser, Debug)]
#[command(disable_version_flag = true)]
pub struct Jimsh {
    /// Print version information.
    #[arg(long = "version")]
    pub print_version: bool,

    /// Tcl expression to execute.
    #[arg(short = 'e')]
    pub expression: Option<String>,

    /// Tcl script to execute, optionally with arguments.
    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
    pub script_and_args: Vec<OsString>,
}

impl Jimsh {
    /// Get the input that should be used to run Tcl.
    pub fn script_input(&self) -> TclInput<'_> {
        if let Some(e) = &self.expression {
            TclInput::Script(e)
        } else if self.script_and_args.is_empty() {
            if io::stdin().is_terminal() {
                TclInput::Interactive
            } else {
                TclInput::Stdin
            }
        } else if self.script_and_args[0].as_bytes() == b"-" {
            TclInput::Stdin
        } else {
            TclInput::File(&self.script_and_args[0])
        }
    }

    /// Obtain the script file to run, if one is specified.
    pub fn script_file(&self) -> Option<&OsStr> {
        if let TclInput::File(path) = self.script_input() {
            Some(path)
        } else {
            None
        }
    }

    /// Get the arguments to be passed to the script.
    pub fn script_args(&self) -> &[OsString] {
        if !self.script_and_args.is_empty() && self.expression.is_none() {
            &self.script_and_args[1..]
        } else {
            &self.script_and_args
        }
    }

    /// Set up an interpreter environment to run the script.
    pub fn setup_env(&self, interp: &Interp, argv0: Option<OsString>) -> JimResult<()> {
        debug!("initializing interpreter");
        interp.eval_source("initjimsh.tcl", 1, sys::tclexts::initjimsh)?;
        if let TclInput::File(path) = self.script_input() {
            interp.set_variable("argv0", path.as_bytes())?;
        }
        let argv = JimObject::empty(interp);
        for arg in self.script_args() {
            argv.list_append(arg.as_bytes());
        }
        interp.set_variable("argv", argv)?;
        interp.set_variable("jim::lineedit", 0)?;

        let exe = current_exe()?;
        debug!("setting executable from {}", exe.display());
        interp.set_variable("jim::exe", exe.as_os_str().as_bytes())?;

        if let Some(arg) = argv0 {
            debug!("initializing argv0 to {:?}", arg);
            interp.set_variable("jim::argv0", arg.as_bytes())?;
        } else {
            interp.set_variable("jim::argv0", exe.as_os_str().as_bytes())?;
        }

        Ok(())
    }

    /// Run the command line in the specified interpreter.  Returns the Jim
    /// exit code on success.
    ///
    /// **Note:** you must call [setup_env] before this function to properly
    /// initialize the interpreter variables. The calls are not automatically
    /// linked, to allow embedding code to perform additional post-setup
    /// actions.
    pub fn run(&self, interp: &Interp) -> JimResult<i32> {
        let result = match self.script_input() {
            TclInput::Interactive => {
                info!("running interactive prompt");
                interp.interactive_prompt()
            }
            TclInput::Stdin => {
                info!("evaluating standard input");
                interp
                    .eval("eval [info source [stdin read] stdin 1]")
                    .map(|_| ())
            }
            TclInput::Script(c) => {
                info!("evaluating command");
                interp.eval(c).map(|_| ())
            }
            TclInput::File(f) => {
                let path = PathBuf::from(f);
                info!("evaluating file {}", path.display());
                interp.eval_file(&path)
            }
        };

        // special-case unhandled errors to get stacktrace
        if let Err(JimError::Error(msg)) = &result {
            eprintln!("{}", msg);
            let stack = interp.stack_trace();
            let obj = interp.new_string("stackdump");
            obj.list_append(stack);
            let result = interp.eval_object(&obj)?;
            eprintln!("{}", result);
            Ok(1)
        } else if let Err(JimError::OtherCode(ExitCode::Break)) = &result {
            // break is fine
            Ok(0)
        } else {
            interp.resolve_exit(result)
        }
    }
}