nushell/crates/nu-cli/src/cli.rs

531 lines
16 KiB
Rust
Raw Normal View History

use crate::line_editor::configure_ctrl_c;
use nu_ansi_term::Color;
use nu_engine::{maybe_print_errors, run_block, script::run_script_standalone, EvaluationContext};
#[allow(unused_imports)]
pub(crate) use nu_engine::script::{process_script, LineResult};
#[cfg(feature = "rustyline-support")]
use crate::line_editor::{
configure_rustyline_editor, convert_rustyline_result_to_string,
default_rustyline_editor_configuration, nu_line_editor_helper,
};
#[allow(unused_imports)]
use nu_data::config;
use nu_source::{Tag, Text};
use nu_stream::InputStream;
use std::ffi::{OsStr, OsString};
#[allow(unused_imports)]
use std::sync::atomic::Ordering;
#[cfg(feature = "rustyline-support")]
use rustyline::{self, error::ReadlineError};
use nu_errors::ShellError;
use nu_parser::ParserScope;
use nu_protocol::{hir::ExternalRedirection, ConfigPath, UntaggedValue, Value};
use log::trace;
use std::error::Error;
2019-05-24 09:29:16 +02:00
use std::iter::Iterator;
use std::path::PathBuf;
2019-05-23 06:30:43 +02:00
pub struct Options {
pub config: Option<OsString>,
pub stdin: bool,
pub scripts: Vec<NuScript>,
pub save_history: bool,
}
impl Default for Options {
fn default() -> Self {
Self::new()
}
}
impl Options {
pub fn new() -> Self {
Self {
config: None,
stdin: false,
scripts: vec![],
save_history: true,
}
}
}
pub struct NuScript {
pub filepath: Option<OsString>,
pub contents: String,
}
impl NuScript {
pub fn code<'a>(content: impl Iterator<Item = &'a str>) -> Result<Self, ShellError> {
let text = content
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join("\n");
Ok(Self {
filepath: None,
contents: text,
})
}
pub fn get_code(&self) -> &str {
&self.contents
}
pub fn source_file(path: &OsStr) -> Result<Self, ShellError> {
use std::fs::File;
use std::io::Read;
let path = path.to_os_string();
let mut file = File::open(&path)?;
let mut buffer = String::new();
file.read_to_string(&mut buffer)?;
Ok(Self {
filepath: Some(path),
contents: buffer,
})
}
}
2020-08-27 09:57:40 +02:00
pub fn search_paths() -> Vec<std::path::PathBuf> {
use std::env;
2019-07-04 05:06:43 +02:00
let mut search_paths = Vec::new();
// Automatically add path `nu` is in as a search path
if let Ok(exe_path) = env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
search_paths.push(exe_dir.to_path_buf());
}
}
2019-07-03 19:37:09 +02:00
if let Ok(config) = nu_data::config::config(Tag::unknown()) {
2021-02-12 11:13:14 +01:00
if let Some(Value {
value: UntaggedValue::Table(pipelines),
..
}) = config.get("plugin_dirs")
{
for pipeline in pipelines {
if let Ok(plugin_dir) = pipeline.as_string() {
search_paths.push(PathBuf::from(plugin_dir));
}
}
}
2019-09-12 05:20:42 +02:00
}
search_paths
}
pub fn run_script_file(context: EvaluationContext, options: Options) -> Result<(), Box<dyn Error>> {
if let Some(cfg) = options.config {
load_cfg_as_global_cfg(&context, PathBuf::from(cfg));
} else {
load_global_cfg(&context);
}
let _ = register_plugins(&context);
let _ = configure_ctrl_c(&context);
let script = options
.scripts
.get(0)
.ok_or_else(|| ShellError::unexpected("Nu source code not available"))?;
run_script_standalone(script.get_code().to_string(), options.stdin, &context, true)?;
Ok(())
}
#[cfg(feature = "rustyline-support")]
pub fn cli(context: EvaluationContext, options: Options) -> Result<(), Box<dyn Error>> {
let _ = configure_ctrl_c(&context);
// start time for running startup scripts (this metric includes loading of the cfg, but w/e)
let startup_commands_start_time = std::time::Instant::now();
if let Some(cfg) = options.config {
load_cfg_as_global_cfg(&context, PathBuf::from(cfg));
} else {
load_global_cfg(&context);
}
// Store cmd duration in an env var
context.scope.add_env_var(
"CMD_DURATION",
format!("{:?}", startup_commands_start_time.elapsed()),
);
trace!(
"startup commands took {:?}",
startup_commands_start_time.elapsed()
);
2020-08-27 13:06:25 +02:00
//Configure rustyline
let mut rl = default_rustyline_editor_configuration();
let history_path = if let Some(cfg) = &context.configs.lock().global_config {
let _ = configure_rustyline_editor(&mut rl, cfg);
let helper = Some(nu_line_editor_helper(&context, cfg));
rl.set_helper(helper);
nu_data::config::path::history_path_or_default(cfg)
} else {
nu_data::config::path::default_history_path()
};
// Don't load history if it's not necessary
if options.save_history {
let _ = rl.load_history(&history_path);
}
//set vars from cfg if present
let (skip_welcome_message, prompt) = if let Some(cfg) = &context.configs.lock().global_config {
(
cfg.var("skip_welcome_message")
.map(|x| x.is_true())
.unwrap_or(false),
cfg.var("prompt"),
)
} else {
(false, None)
};
//Check whether dir we start in contains local cfg file and if so load it.
load_local_cfg_if_present(&context);
// Give ourselves a scope to work in
context.scope.enter_scope();
let mut session_text = String::new();
let mut line_start: usize = 0;
if !skip_welcome_message {
println!(
"Welcome to Nushell {} (type 'help' for more info)",
clap::crate_version!()
);
}
2019-05-26 08:54:41 +02:00
#[cfg(windows)]
{
2021-02-22 19:33:34 +01:00
let _ = nu_ansi_term::enable_ansi_support();
2019-05-26 08:54:41 +02:00
}
2019-06-15 20:36:17 +02:00
let mut ctrlcbreak = false;
2019-05-23 06:30:43 +02:00
loop {
if context.ctrl_c.load(Ordering::SeqCst) {
context.ctrl_c.store(false, Ordering::SeqCst);
2019-06-07 02:31:22 +02:00
continue;
}
Restructure and streamline token expansion (#1123) Restructure and streamline token expansion The purpose of this commit is to streamline the token expansion code, by removing aspects of the code that are no longer relevant, removing pointless duplication, and eliminating the need to pass the same arguments to `expand_syntax`. The first big-picture change in this commit is that instead of a handful of `expand_` functions, which take a TokensIterator and ExpandContext, a smaller number of methods on the `TokensIterator` do the same job. The second big-picture change in this commit is fully eliminating the coloring traits, making coloring a responsibility of the base expansion implementations. This also means that the coloring tracer is merged into the expansion tracer, so you can follow a single expansion and see how the expansion process produced colored tokens. One side effect of this change is that the expander itself is marginally more error-correcting. The error correction works by switching from structured expansion to `BackoffColoringMode` when an unexpected token is found, which guarantees that all spans of the source are colored, but may not be the most optimal error recovery strategy. That said, because `BackoffColoringMode` only extends as far as a closing delimiter (`)`, `]`, `}`) or pipe (`|`), it does result in fairly granular correction strategy. The current code still produces an `Err` (plus a complete list of colored shapes) from the parsing process if any errors are encountered, but this could easily be addressed now that the underlying expansion is error-correcting. This commit also colors any spans that are syntax errors in red, and causes the parser to include some additional information about what tokens were expected at any given point where an error was encountered, so that completions and hinting could be more robust in the future. Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com> Co-authored-by: Andrés N. Robalino <andres@androbtech.com>
2020-01-21 23:45:03 +01:00
let cwd = context.shell_manager.path();
2019-08-07 19:49:11 +02:00
let colored_prompt = {
if let Some(prompt) = &prompt {
let prompt_line = prompt.as_string()?;
context.scope.enter_scope();
let (prompt_block, err) = nu_parser::parse(&prompt_line, 0, &context.scope);
if err.is_some() {
context.scope.exit_scope();
format!(
"{}{}{}{}{}{}> ",
Color::Green.bold().prefix().to_string(),
cwd,
nu_ansi_term::ansi::RESET,
Color::Cyan.bold().prefix().to_string(),
current_branch(),
nu_ansi_term::ansi::RESET
)
} else {
let run_result = run_block(
&prompt_block,
&context,
InputStream::empty(),
ExternalRedirection::Stdout,
);
context.scope.exit_scope();
2020-06-28 23:06:05 +02:00
match run_result {
Ok(result) => match result.collect_string(Tag::unknown()) {
Ok(string_result) => {
let errors = context.get_errors();
maybe_print_errors(&context, Text::from(prompt_line));
context.clear_errors();
if !errors.is_empty() {
2020-06-28 23:06:05 +02:00
"> ".to_string()
} else {
string_result.item
}
}
2020-06-28 23:06:05 +02:00
Err(e) => {
context.host.lock().print_err(e, &Text::from(prompt_line));
2020-06-28 23:06:05 +02:00
context.clear_errors();
"> ".to_string()
}
},
Err(e) => {
context.host.lock().print_err(e, &Text::from(prompt_line));
context.clear_errors();
"> ".to_string()
}
}
}
} else {
format!(
"{}{}{}{}{}{}> ",
Color::Green.bold().prefix().to_string(),
cwd,
nu_ansi_term::ansi::RESET,
Color::Cyan.bold().prefix().to_string(),
current_branch(),
nu_ansi_term::ansi::RESET
)
}
};
let prompt = {
if let Ok(bytes) = strip_ansi_escapes::strip(&colored_prompt) {
String::from_utf8_lossy(&bytes).to_string()
} else {
"> ".to_string()
2020-01-01 21:45:32 +01:00
}
2019-11-16 21:42:35 +01:00
};
if let Some(helper) = rl.helper_mut() {
helper.colored_prompt = colored_prompt;
}
2019-09-18 00:21:39 +02:00
let mut initial_command = Some(String::new());
let mut readline = Err(ReadlineError::Eof);
while let Some(ref cmd) = initial_command {
readline = rl.readline_with_initial(&prompt, (&cmd, ""));
2019-11-22 09:25:09 +01:00
initial_command = None;
2019-09-18 00:21:39 +02:00
}
2019-05-23 06:30:43 +02:00
if let Ok(line) = &readline {
line_start = session_text.len();
session_text.push_str(line);
session_text.push('\n');
}
// start time for command duration
let cmd_start_time = std::time::Instant::now();
let line = match convert_rustyline_result_to_string(readline) {
LineResult::Success(_) => process_script(
&session_text[line_start..],
&context,
false,
line_start,
true,
),
x => x,
};
2019-11-04 16:47:03 +01:00
// Store cmd duration in an env var
context
.scope
.add_env_var("CMD_DURATION", format!("{:?}", cmd_start_time.elapsed()));
2019-11-04 16:47:03 +01:00
match line {
2019-05-23 06:30:43 +02:00
LineResult::Success(line) => {
if options.save_history && !line.trim().is_empty() {
rl.add_history_entry(&line);
let _ = rl.append_history(&history_path);
}
maybe_print_errors(&context, Text::from(session_text.clone()));
2019-11-04 16:47:03 +01:00
}
2020-11-09 17:23:41 +01:00
LineResult::ClearHistory => {
if options.save_history {
rl.clear_history();
let _ = rl.append_history(&history_path);
}
2020-11-09 17:23:41 +01:00
}
LineResult::Error(line, err) => {
if options.save_history && !line.trim().is_empty() {
rl.add_history_entry(&line);
let _ = rl.append_history(&history_path);
}
2019-11-04 16:47:03 +01:00
context
.host
.lock()
.print_err(err, &Text::from(session_text.clone()));
// I am not so sure, we don't need maybe_print_errors here (as we printed an err
// above), because maybe_print_errors also clears the errors.
// TODO Analyze where above err comes from, and whether we need to clear
// context.errors here
// Or just be consistent and return errors always in context.errors...
maybe_print_errors(&context, Text::from(session_text.clone()));
2019-05-23 06:30:43 +02:00
}
2019-06-15 20:36:17 +02:00
LineResult::CtrlC => {
let config_ctrlc_exit = context
.configs
.lock()
.global_config
.as_ref()
.map(|cfg| cfg.var("ctrlc_exit"))
.flatten()
.map(|ctrl_c| ctrl_c.is_true())
.unwrap_or(false); // default behavior is to allow CTRL-C spamming similar to other shells
if !config_ctrlc_exit {
continue;
}
2019-06-15 20:36:17 +02:00
if ctrlcbreak {
if options.save_history {
let _ = rl.append_history(&history_path);
}
2019-06-15 20:36:17 +02:00
std::process::exit(0);
} else {
Restructure and streamline token expansion (#1123) Restructure and streamline token expansion The purpose of this commit is to streamline the token expansion code, by removing aspects of the code that are no longer relevant, removing pointless duplication, and eliminating the need to pass the same arguments to `expand_syntax`. The first big-picture change in this commit is that instead of a handful of `expand_` functions, which take a TokensIterator and ExpandContext, a smaller number of methods on the `TokensIterator` do the same job. The second big-picture change in this commit is fully eliminating the coloring traits, making coloring a responsibility of the base expansion implementations. This also means that the coloring tracer is merged into the expansion tracer, so you can follow a single expansion and see how the expansion process produced colored tokens. One side effect of this change is that the expander itself is marginally more error-correcting. The error correction works by switching from structured expansion to `BackoffColoringMode` when an unexpected token is found, which guarantees that all spans of the source are colored, but may not be the most optimal error recovery strategy. That said, because `BackoffColoringMode` only extends as far as a closing delimiter (`)`, `]`, `}`) or pipe (`|`), it does result in fairly granular correction strategy. The current code still produces an `Err` (plus a complete list of colored shapes) from the parsing process if any errors are encountered, but this could easily be addressed now that the underlying expansion is error-correcting. This commit also colors any spans that are syntax errors in red, and causes the parser to include some additional information about what tokens were expected at any given point where an error was encountered, so that completions and hinting could be more robust in the future. Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com> Co-authored-by: Andrés N. Robalino <andres@androbtech.com>
2020-01-21 23:45:03 +01:00
context.with_host(|host| host.stdout("CTRL-C pressed (again to quit)"));
2019-06-15 20:36:17 +02:00
ctrlcbreak = true;
continue;
}
}
2020-09-21 09:56:10 +02:00
LineResult::CtrlD => {
context.shell_manager.remove_at_current();
if context.shell_manager.is_empty() {
break;
}
}
2019-05-23 06:30:43 +02:00
LineResult::Break => {
break;
}
}
2019-06-15 20:36:17 +02:00
ctrlcbreak = false;
2019-05-23 06:30:43 +02:00
}
2019-08-27 00:41:57 +02:00
// we are ok if we can not save history
if options.save_history {
let _ = rl.append_history(&history_path);
}
2019-05-23 06:30:43 +02:00
Ok(())
}
pub fn load_local_cfg_if_present(context: &EvaluationContext) {
trace!("Loading local cfg if present");
match config::loadable_cfg_exists_in_dir(PathBuf::from(context.shell_manager.path())) {
Ok(Some(cfg_path)) => {
if let Err(err) = context.load_config(&ConfigPath::Local(cfg_path)) {
context.host.lock().print_err(err, &Text::from(""))
}
}
Err(e) => {
//Report error while checking for local cfg file
context.host.lock().print_err(e, &Text::from(""))
}
Ok(None) => {
//No local cfg file present in start dir
}
}
}
fn load_cfg_as_global_cfg(context: &EvaluationContext, path: PathBuf) {
if let Err(err) = context.load_config(&ConfigPath::Global(path)) {
context.host.lock().print_err(err, &Text::from(""));
}
}
pub fn load_global_cfg(context: &EvaluationContext) {
match config::default_path() {
Ok(path) => {
load_cfg_as_global_cfg(context, path);
}
Err(e) => {
context.host.lock().print_err(e, &Text::from(""));
}
}
}
pub fn register_plugins(context: &EvaluationContext) -> Result<(), ShellError> {
if let Ok(plugins) = nu_engine::plugin::build_plugin::scan(search_paths()) {
context.add_commands(
plugins
.into_iter()
.filter(|p| !context.is_command_registered(p.name()))
.collect(),
);
}
Ok(())
}
pub fn parse_and_eval(line: &str, ctx: &EvaluationContext) -> Result<String, ShellError> {
// FIXME: do we still need this?
let line = if let Some(s) = line.strip_suffix('\n') {
s
} else {
line
};
// TODO ensure the command whose examples we're testing is actually in the pipeline
ctx.scope.enter_scope();
let (classified_block, err) = nu_parser::parse(&line, 0, &ctx.scope);
if let Some(err) = err {
ctx.scope.exit_scope();
return Err(err.into());
}
let input_stream = InputStream::empty();
let result = run_block(
&classified_block,
ctx,
input_stream,
ExternalRedirection::Stdout,
);
ctx.scope.exit_scope();
result?.collect_string(Tag::unknown()).map(|x| x.item)
}
#[allow(dead_code)]
fn current_branch() -> String {
#[cfg(feature = "shadow-rs")]
{
Some(shadow_rs::branch())
.map(|x| x.trim().to_string())
.filter(|x| !x.is_empty())
.map(|x| format!("({})", x))
.unwrap_or_default()
}
#[cfg(not(feature = "shadow-rs"))]
{
"".to_string()
}
}
#[cfg(test)]
mod tests {
use nu_engine::EvaluationContext;
#[quickcheck]
fn quickcheck_parse(data: String) -> bool {
let (tokens, err) = nu_parser::lex(&data, 0);
let (lite_block, err2) = nu_parser::parse_block(tokens);
if err.is_none() && err2.is_none() {
let context = EvaluationContext::basic();
let _ = nu_parser::classify_block(&lite_block, &context.scope);
}
true
}
}