Start support for commandline args to nu itself (#851)

* cmdline args wip

* WIP

* redirect working

* Add help and examples

* Only show flags in signature of more than help
This commit is contained in:
JT 2022-01-26 09:42:39 -05:00 committed by GitHub
parent 285f65ba34
commit 8ee619954d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 307 additions and 97 deletions

View File

@ -14,7 +14,7 @@ pub use cp::Cp;
pub use ls::Ls;
pub use mkdir::Mkdir;
pub use mv::Mv;
pub use open::Open;
pub use open::{BufferedReader, Open};
pub use rm::Rm;
pub use save::Save;
pub use touch::Touch;

View File

@ -184,6 +184,12 @@ pub struct BufferedReader<R: Read> {
input: BufReader<R>,
}
impl<R: Read> BufferedReader<R> {
pub fn new(input: BufReader<R>) -> Self {
Self { input }
}
}
impl<R: Read> Iterator for BufferedReader<R> {
type Item = Result<Vec<u8>, ShellError>;

View File

@ -186,6 +186,10 @@ pub fn get_documentation(
long_desc.push('\n');
}
if !sig.named.is_empty() {
long_desc.push_str(&get_flags_section(sig))
}
if !sig.required_positional.is_empty()
|| !sig.optional_positional.is_empty()
|| sig.rest_positional.is_some()
@ -205,13 +209,11 @@ pub fn get_documentation(
long_desc.push_str(&format!(" ...args: {}\n", rest_positional.desc));
}
}
if !sig.named.is_empty() {
long_desc.push_str(&get_flags_section(sig))
}
if !examples.is_empty() {
long_desc.push_str("\nExamples:");
}
for example in examples {
long_desc.push('\n');
long_desc.push_str(" ");
@ -222,7 +224,7 @@ pub fn get_documentation(
} else if let Some(highlighter) = engine_state.find_decl(b"nu-highlight") {
let decl = engine_state.get_decl(highlighter);
if let Ok(output) = decl.run(
match decl.run(
engine_state,
stack,
&Call::new(),
@ -232,16 +234,23 @@ pub fn get_documentation(
}
.into_pipeline_data(),
) {
let result = output.into_value(Span { start: 0, end: 0 });
match result.as_string() {
Ok(s) => {
long_desc.push_str(&format!("\n > {}\n", s));
}
_ => {
long_desc.push_str(&format!("\n > {}\n", example.example));
Ok(output) => {
let result = output.into_value(Span { start: 0, end: 0 });
match result.as_string() {
Ok(s) => {
long_desc.push_str(&format!("\n > {}\n", s));
}
_ => {
long_desc.push_str(&format!("\n > {}\n", example.example));
}
}
}
Err(_) => {
long_desc.push_str(&format!("\n > {}\n", example.example));
}
}
} else {
long_desc.push_str(&format!("\n > {}\n", example.example));
}
}

View File

@ -12,9 +12,7 @@ pub use flatten::{
};
pub use lex::{lex, Token, TokenContents};
pub use lite_parse::{lite_parse, LiteBlock};
pub use parse_keywords::{
parse_alias, parse_def, parse_def_predecl, parse_let, parse_module, parse_use,
};
pub use parser::{find_captures_in_expr, parse, trim_quotes, Import};
#[cfg(feature = "plugin")]

View File

@ -1,5 +1,6 @@
use crate::Value;
#[derive(Debug)]
pub struct Example {
pub example: &'static str,
pub description: &'static str,

View File

@ -279,6 +279,14 @@ impl Signature {
one_liner.push_str(&self.name);
one_liner.push(' ');
// Note: the call signature needs flags first because on the nu commandline,
// flags will precede the script file name. Flags for internal commands can come
// either before or after (or around) positional parameters, so there isn't a strong
// preference, so we default to the more constrained example.
if self.named.len() > 1 {
one_liner.push_str("{flags} ");
}
for positional in &self.required_positional {
one_liner.push_str(&get_positional_short_name(positional, true));
}
@ -286,18 +294,14 @@ impl Signature {
one_liner.push_str(&get_positional_short_name(positional, false));
}
if self.rest_positional.is_some() {
one_liner.push_str("...args ");
if let Some(rest) = &self.rest_positional {
one_liner.push_str(&format!("...{}", get_positional_short_name(rest, false)));
}
// if !self.subcommands.is_empty() {
// one_liner.push_str("<subcommand> ");
// }
if !self.named.is_empty() {
one_liner.push_str("{flags} ");
}
one_liner
}

View File

@ -13,8 +13,10 @@ use crate::utils::{gather_parent_env_vars, report_error};
/// Main function used when a file path is found as argument for nu
pub(crate) fn evaluate(
path: String,
args: &[String],
init_cwd: PathBuf,
engine_state: &mut EngineState,
input: PipelineData,
) -> Result<()> {
// First, set up env vars as strings only
gather_parent_env_vars(engine_state);
@ -82,84 +84,71 @@ pub(crate) fn evaluate(
std::process::exit(1);
}
match eval_block(
engine_state,
&mut stack,
&block,
PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored
) {
Ok(pipeline_data) => {
for item in pipeline_data {
if let Value::Error { error } = item {
let working_set = StateWorkingSet::new(engine_state);
// Next, let's check if there are any flags we want to pass to the main function
if args.is_empty() && engine_state.find_decl(b"main").is_none() {
// We don't have a main, so evaluate the whole file
match eval_block(engine_state, &mut stack, &block, input) {
Ok(pipeline_data) => {
for item in pipeline_data {
if let Value::Error { error } = item {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &error);
report_error(&working_set, &error);
std::process::exit(1);
}
println!("{}", item.into_string("\n", &config));
}
// Next, let's check if there are any flags we want to pass to the main function
let args: Vec<String> = std::env::args().skip(2).collect();
if args.is_empty() && engine_state.find_decl(b"main").is_none() {
return Ok(());
}
let args = format!("main {}", args.join(" ")).as_bytes().to_vec();
let (block, delta) = {
let mut working_set = StateWorkingSet::new(engine_state);
let (output, err) = parse(&mut working_set, Some("<cmdline>"), &args, false);
if let Some(err) = err {
report_error(&working_set, &err);
std::process::exit(1);
}
(output, working_set.render())
};
let cwd = nu_engine::env::current_dir_str(engine_state, &stack)?;
if let Err(err) = engine_state.merge_delta(delta, Some(&mut stack), &cwd) {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
}
match eval_block(
engine_state,
&mut stack,
&block,
PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored
) {
Ok(pipeline_data) => {
for item in pipeline_data {
if let Value::Error { error } = item {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &error);
std::process::exit(1);
}
println!("{}", item.into_string("\n", &config));
std::process::exit(1);
}
println!("{}", item.into_string("\n", &config));
}
Err(err) => {
let working_set = StateWorkingSet::new(engine_state);
}
Err(err) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
report_error(&working_set, &err);
std::process::exit(1);
}
std::process::exit(1);
}
}
Err(err) => {
} else {
let args = format!("main {}", args.join(" ")).as_bytes().to_vec();
let (block, delta) = {
let mut working_set = StateWorkingSet::new(engine_state);
let (output, err) = parse(&mut working_set, Some("<cmdline>"), &args, false);
if let Some(err) = err {
report_error(&working_set, &err);
std::process::exit(1);
}
(output, working_set.render())
};
let cwd = nu_engine::env::current_dir_str(engine_state, &stack)?;
if let Err(err) = engine_state.merge_delta(delta, Some(&mut stack), &cwd) {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
}
std::process::exit(1);
match eval_block(engine_state, &mut stack, &block, input) {
Ok(pipeline_data) => {
for item in pipeline_data {
if let Value::Error { error } = item {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &error);
std::process::exit(1);
}
println!("{}", item.into_string("\n", &config));
}
}
Err(err) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
std::process::exit(1);
}
}
}

View File

@ -10,11 +10,24 @@ mod utils;
mod tests;
use miette::Result;
use nu_command::create_default_context;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
use nu_command::{create_default_context, BufferedReader};
use nu_engine::get_full_help;
use nu_parser::parse;
use nu_protocol::{
ast::{Call, Expr, Expression, Pipeline, Statement},
engine::{Command, EngineState, Stack, StateWorkingSet},
ByteStream, Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span,
Spanned, SyntaxShape, Value, CONFIG_VARIABLE_ID,
};
use std::{
io::BufReader,
path::Path,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use utils::report_error;
fn main() -> Result<()> {
// miette::set_panic_hook();
@ -51,9 +64,199 @@ fn main() -> Result<()> {
engine_state.ctrlc = Some(engine_state_ctrlc);
// End ctrl-c protection section
if let Some(path) = std::env::args().nth(1) {
eval_file::evaluate(path, init_cwd, &mut engine_state)
} else {
repl::evaluate(ctrlc, &mut engine_state)
let mut args_to_nushell = vec![];
let mut script_name = String::new();
let mut args_to_script = vec![];
// Would be nice if we had a way to parse this. The first flags we see will be going to nushell
// then it'll be the script name
// then the args to the script
let mut collect_arg_nushell = false;
for arg in std::env::args().skip(1) {
if !script_name.is_empty() {
args_to_script.push(arg);
} else if collect_arg_nushell {
args_to_nushell.push(arg);
collect_arg_nushell = false;
} else if arg.starts_with('-') {
// Cool, it's a flag
if arg == "-c"
|| arg == "--commands"
|| arg == "--develop"
|| arg == "--debug"
|| arg == "--loglevel"
|| arg == "--config-file"
{
collect_arg_nushell = true;
}
args_to_nushell.push(arg);
} else {
// Our script file
script_name = arg;
}
}
args_to_nushell.insert(0, "nu".into());
let nushell_commandline_args = args_to_nushell.join(" ");
let nushell_config =
parse_commandline_args(&nushell_commandline_args, &init_cwd, &mut engine_state);
match nushell_config {
Ok(nushell_config) => {
if !script_name.is_empty() {
let input = if let Some(redirect_stdin) = &nushell_config.redirect_stdin {
let stdin = std::io::stdin();
let buf_reader = BufReader::new(stdin);
PipelineData::ByteStream(
ByteStream {
stream: Box::new(BufferedReader::new(buf_reader)),
ctrlc: Some(ctrlc),
},
redirect_stdin.span,
None,
)
} else {
PipelineData::new(Span::new(0, 0))
};
eval_file::evaluate(
script_name,
&args_to_script,
init_cwd,
&mut engine_state,
input,
)
} else {
repl::evaluate(ctrlc, &mut engine_state)
}
}
Err(_) => std::process::exit(1),
}
}
fn parse_commandline_args(
commandline_args: &str,
init_cwd: &Path,
engine_state: &mut EngineState,
) -> Result<NushellConfig, ShellError> {
let (block, delta) = {
let mut working_set = StateWorkingSet::new(engine_state);
working_set.add_decl(Box::new(Nu));
let (output, err) = parse(&mut working_set, None, commandline_args.as_bytes(), false);
if let Some(err) = err {
report_error(&working_set, &err);
std::process::exit(1);
}
working_set.hide_decl(b"nu");
(output, working_set.render())
};
let _ = engine_state.merge_delta(delta, None, init_cwd);
let mut stack = Stack::new();
stack.add_var(
CONFIG_VARIABLE_ID,
Value::Record {
cols: vec![],
vals: vec![],
span: Span::new(0, 0),
},
);
// We should have a successful parse now
if let Some(Statement::Pipeline(Pipeline { expressions })) = block.stmts.get(0) {
if let Some(Expression {
expr: Expr::Call(call),
..
}) = expressions.get(0)
{
let redirect_stdin = call.get_named_arg("stdin");
let help = call.has_flag("help");
if help {
let full_help =
get_full_help(&Nu.signature(), &Nu.examples(), engine_state, &mut stack);
print!("{}", full_help);
std::process::exit(1);
}
return Ok(NushellConfig { redirect_stdin });
}
}
// Just give the help and exit if the above fails
let full_help = get_full_help(&Nu.signature(), &Nu.examples(), engine_state, &mut stack);
print!("{}", full_help);
std::process::exit(1);
}
struct NushellConfig {
redirect_stdin: Option<Spanned<String>>,
}
#[derive(Clone)]
struct Nu;
impl Command for Nu {
fn name(&self) -> &str {
"nu"
}
fn signature(&self) -> Signature {
Signature::build("nu")
.desc("The nushell language and shell.")
.switch("stdin", "redirect the stdin", None)
.optional(
"script file",
SyntaxShape::Filepath,
"name of the optional script file to run",
)
.rest(
"script args",
SyntaxShape::String,
"parameters to the script file",
)
.category(Category::System)
}
fn usage(&self) -> &str {
"The nushell language and shell."
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Ok(Value::String {
val: get_full_help(&Nu.signature(), &Nu.examples(), engine_state, stack),
span: call.head,
}
.into_pipeline_data())
}
fn examples(&self) -> Vec<nu_protocol::Example> {
vec![
Example {
description: "Run a script",
example: "nu myfile.nu",
result: None,
},
Example {
description: "Run nushell interactively (as a shell or REPL)",
example: "nu",
result: None,
},
]
}
}

View File

@ -317,7 +317,7 @@ pub(crate) fn eval_source(
engine_state,
stack,
&block,
PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored
PipelineData::new(Span::new(0, 0)),
) {
Ok(pipeline_data) => {
if let Err(err) = print_pipeline_data(pipeline_data, engine_state, stack) {