extern crate dirs;
extern crate gumdrop;
extern crate ketos;
extern crate linefeed;
use std::cell::RefCell;
use std::env::{split_paths, var_os};
use std::io::{self, stderr, Write};
use std::iter::repeat;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use gumdrop::{Options, ParsingStyle};
use ketos::{
Builder, Interpreter, complete_name,
Context, Error, RestrictConfig, ParseError, ParseErrorKind,
};
use linefeed::{
Interface, Prompter, Command, Completer,
Completion, Function, ReadResult, Signal, Suffix, Terminal,
};
fn main() {
let status = run();
std::process::exit(status);
}
#[derive(Options)]
struct KetosOpts {
#[options(free)]
free: Vec<String>,
#[options(help = "Print this help message and exit")]
help: bool,
#[options(short = "V", help = "Print version and exit")]
version: bool,
#[options(no_long, help = "Evaluate one expression and exit", meta = "EXPR")]
expr: Option<String>,
#[options(help = "Run interactively even with a file")]
interactive: bool,
#[options(no_long, help = "Add DIR to list of module search paths", meta = "DIR")]
include: Vec<String>,
#[options(short = "R",
help = "Configure execution restrictions; see `-R help` for more details",
meta = "SPEC")]
restrict: Option<String>,
#[options(no_short, help = "Do not run ~/.ketosrc.ket on startup")]
no_rc: bool,
}
fn run() -> i32 {
let args = std::env::args().collect::<Vec<_>>();
let opts = match KetosOpts::parse_args(&args[1..], ParsingStyle::StopAtFirstFree) {
Ok(opts) => opts,
Err(e) => {
let _ = writeln!(stderr(), "{}: {}", args[0], e);
return 1;
}
};
if opts.version {
print_version();
return 0;
}
if opts.help {
print_usage(&args[0]);
return 0;
}
let mut paths = vec![PathBuf::new()];
if let Some(p) = var_os("KETOS_PATH") {
paths.extend(split_paths(&p));
}
paths.extend(opts.include.into_iter().map(PathBuf::from));
let mut builder = Builder::new()
.search_paths(paths);
if let Some(ref res) = opts.restrict {
if res == "help" {
print_restrict_usage();
return 0;
}
builder = match parse_restrict(res) {
Ok(res) => builder.restrict(res),
Err(e) => {
println!("{}: {}", args[0], e);
return 1;
}
}
}
let interp = builder.finish();
let interactive = opts.interactive ||
(opts.free.is_empty() && opts.expr.is_none());
if let Some(ref expr) = opts.expr {
if !run_expr(&interp, &expr) && !interactive {
return 1;
}
} else if !opts.free.is_empty() {
interp.set_args(&opts.free);
if !run_file(&interp, Path::new(&opts.free[0])) && !interactive {
return 1;
}
}
if interactive {
if !opts.no_rc {
if let Some(p) = dirs::home_dir() {
let rc = p.join(".ketosrc.ket");
if rc.is_file() {
run_file(&interp, &rc);
}
}
}
if let Err(e) = run_repl(&interp) {
eprintln!("terminal device error: {}", e);
}
}
0
}
fn parse_restrict(params: &str) -> Result<RestrictConfig, String> {
let mut res = RestrictConfig::permissive();
for param in params.split(',') {
match param {
"permissive" => res = RestrictConfig::permissive(),
"strict" => res = RestrictConfig::strict(),
_ => {
let (name, value) = match param.find('=') {
Some(pos) => (¶m[..pos], ¶m[pos + 1..]),
None => return Err(format!("unrecognized restrict option: {}", param))
};
match name {
"execution_time" =>
res.execution_time = Some(Duration::from_millis(
parse_param(name, value)?)),
"call_stack_size" =>
res.call_stack_size = parse_param(name, value)?,
"value_stack_size" =>
res.value_stack_size = parse_param(name, value)?,
"namespace_size" =>
res.namespace_size = parse_param(name, value)?,
"memory_limit" =>
res.memory_limit = parse_param(name, value)?,
"max_integer_size" =>
res.max_integer_size = parse_param(name, value)?,
"max_syntax_nesting" =>
res.max_syntax_nesting = parse_param(name, value)?,
_ => return Err(format!("unrecognized parameter: {}", name))
}
}
}
}
Ok(res)
}
fn parse_param<T: FromStr>(name: &str, value: &str) -> Result<T, String> {
value.parse().map_err(|_| format!("invalid `{}` value: {}", name, value))
}
fn display_error(interp: &Interpreter, e: &Error) {
if let Some(trace) = interp.take_traceback() {
interp.display_trace(&trace);
}
interp.display_error(e);
}
fn run_expr(interp: &Interpreter, expr: &str) -> bool {
match interp.run_single_expr(expr, None) {
Ok(value) => {
interp.display_value(&value);
true
}
Err(e) => {
display_error(&interp, &e);
false
}
}
}
fn run_file(interp: &Interpreter, file: &Path) -> bool {
match interp.run_file(file) {
Ok(()) => true,
Err(e) => {
display_error(&interp, &e);
false
}
}
}
fn run_repl(interp: &Interpreter) -> io::Result<()> {
let interface = Interface::new("ketos")?;
set_thread_context(interp.context().clone());
interface.set_completer(Arc::new(KetosCompleter));
interface.set_prompt("ketos=> ")?;
interface.set_report_signal(Signal::Interrupt, true);
interface.define_function("ketos-accept", Arc::new(KetosAccept));
interface.bind_sequence("\r", Command::from_str("ketos-accept"));
interface.bind_sequence("\n", Command::from_str("ketos-accept"));
{
let mut reader = interface.lock_reader();
reader.set_word_break_chars(" \t\n#\"'(),:;@[\\]`{}");
reader.set_string_chars("\"");
reader.set_blink_matching_paren(true);
}
loop {
match interface.read_line()? {
ReadResult::Eof => break,
ReadResult::Input(mut line) => {
if line.chars().all(|c| c.is_whitespace()) {
continue;
}
interface.add_history(line.clone());
line.push('\n');
match interp.compile_exprs(&line) {
Ok(code) => {
if !code.is_empty() {
match interp.execute_program(code) {
Ok(v) => interp.display_value(&v),
Err(e) => display_error(&interp, &e)
}
}
}
Err(e) => {
display_error(&interp, &e);
}
}
interp.clear_codemap();
}
ReadResult::Signal(_) => {
println!("^C");
interface.cancel_read_line()?;
}
}
}
println!();
Ok(())
}
struct KetosCompleter;
thread_local!{
static CONTEXT: RefCell<Option<Context>> = RefCell::new(None);
}
fn set_thread_context(ctx: Context) {
CONTEXT.with(|key| {
*key.borrow_mut() = Some(ctx);
});
}
fn thread_context() -> Context {
CONTEXT.with(|key| {
key.borrow().clone().unwrap_or_else(
|| panic!("no thread-local Context object set"))
})
}
impl<Term: Terminal> Completer<Term> for KetosCompleter {
fn complete(&self, word: &str, prompter: &Prompter<Term>,
start: usize, end: usize) -> Option<Vec<Completion>> {
let line_start = prompter.buffer()[..start].rfind('\n')
.map(|pos| pos + 1).unwrap_or(0);
let is_whitespace = prompter.buffer()[line_start..start]
.chars().all(|ch| ch.is_whitespace());
if is_whitespace && start == end {
let n = 2 - (start - line_start) % 2;
Some(vec![Completion{
completion: repeat(' ').take(n).collect(),
display: None,
suffix: Suffix::None,
}])
} else {
let ctx = thread_context();
complete_name(word, ctx.scope()).map(
|words| words.into_iter().map(Completion::simple).collect())
}
}
}
struct KetosAccept;
impl<Term: Terminal> Function<Term> for KetosAccept {
fn execute(&self, prompter: &mut Prompter<Term>, count: i32, _ch: char) -> io::Result<()> {
let interp = Interpreter::with_context(thread_context());
let r = interp.parse_exprs(prompter.buffer(), None);
interp.clear_codemap();
match r {
Err(Error::ParseError(ParseError{kind: ParseErrorKind::MissingCloseParen, ..})) |
Err(Error::ParseError(ParseError{kind: ParseErrorKind::UnterminatedComment, ..})) |
Err(Error::ParseError(ParseError{kind: ParseErrorKind::UnterminatedString, ..})) |
Err(Error::ParseError(ParseError{kind: ParseErrorKind::DocCommentEof, ..})) => {
if count > 0 {
prompter.insert(count as usize, '\n')
} else {
Ok(())
}
}
Ok(_) | Err(_) => prompter.accept_input(),
}
}
}
fn print_version() {
println!("ketos {}", version());
}
fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
fn print_usage(arg0: &str) {
println!("Usage: {} [OPTIONS] [FILE] [ARGS]", arg0);
println!();
println!("{}", KetosOpts::usage());
}
fn print_restrict_usage() {
print!(
r#"The `-R` / `--restrict` option accepts a comma-separated list of parameters:
permissive
Applies "permissive" restrictions (default)
strict
Applies "strict" restrictions
key=value
Assigns a value to the named restriction configuration parameter.
Accepted keys are:
execution_time Maximum execution time, in milliseconds
call_stack_size Maximum call frames
value_stack_size Maximum values stored on the stack
namespace_size Maximum values stored in global namespace
memory_limit Maximum total held memory, in abstract units
max_integer_size Maximum integer size, in bits
max_syntax_nesting Maximum nested syntax elements
"#);
}