Support for getting help text from a plugin command (#12243)

# Description
There wasn't really a good way to implement a command group style (e.g.
`from`, `query`, etc.) command in the past that just returns the help
text even if `--help` is not passed. This adds a new engine call that
just does that.

This is actually something I ran into before when developing the dbus
plugin, so it's nice to fix it.

# User-Facing Changes


# Tests + Formatting
- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

# After Submitting
- [ ] Document `GetHelp` engine call in proto
This commit is contained in:
Devyn Cairns 2024-03-23 16:30:38 -07:00 committed by GitHub
parent c79c43d2f8
commit 78be67f0c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 79 additions and 36 deletions

1
Cargo.lock generated
View file

@ -3254,7 +3254,6 @@ name = "nu_plugin_query"
version = "0.91.1" version = "0.91.1"
dependencies = [ dependencies = [
"gjson", "gjson",
"nu-engine",
"nu-plugin", "nu-plugin",
"nu-protocol", "nu-protocol",
"scraper", "scraper",

View file

@ -4,7 +4,7 @@ use std::{
sync::{atomic::AtomicBool, Arc}, sync::{atomic::AtomicBool, Arc},
}; };
use nu_engine::get_eval_block_with_early_return; use nu_engine::{get_eval_block_with_early_return, get_full_help};
use nu_protocol::{ use nu_protocol::{
ast::Call, ast::Call,
engine::{Closure, EngineState, Redirection, Stack}, engine::{Closure, EngineState, Redirection, Stack},
@ -32,6 +32,8 @@ pub trait PluginExecutionContext: Send + Sync {
fn get_current_dir(&self) -> Result<Spanned<String>, ShellError>; fn get_current_dir(&self) -> Result<Spanned<String>, ShellError>;
/// Set an environment variable /// Set an environment variable
fn add_env_var(&mut self, name: String, value: Value) -> Result<(), ShellError>; fn add_env_var(&mut self, name: String, value: Value) -> Result<(), ShellError>;
/// Get help for the current command
fn get_help(&self) -> Result<Spanned<String>, ShellError>;
/// Evaluate a closure passed to the plugin /// Evaluate a closure passed to the plugin
fn eval_closure( fn eval_closure(
&self, &self,
@ -137,6 +139,19 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> {
Ok(()) Ok(())
} }
fn get_help(&self) -> Result<Spanned<String>, ShellError> {
let decl = self.engine_state.get_decl(self.call.decl_id);
Ok(get_full_help(
&decl.signature(),
&decl.examples(),
&self.engine_state,
&mut self.stack.clone(),
false,
)
.into_spanned(self.call.head))
}
fn eval_closure( fn eval_closure(
&self, &self,
closure: Spanned<Closure>, closure: Spanned<Closure>,
@ -252,6 +267,12 @@ impl PluginExecutionContext for PluginExecutionBogusContext {
}) })
} }
fn get_help(&self) -> Result<Spanned<String>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_help not implemented on bogus".into(),
})
}
fn eval_closure( fn eval_closure(
&self, &self,
_closure: Spanned<Closure>, _closure: Spanned<Closure>,

View file

@ -636,6 +636,31 @@ impl EngineInterface {
} }
} }
/// Get the help string for the current command.
///
/// This returns the same string as passing `--help` would, and can be used for the top-level
/// command in a command group that doesn't do anything on its own (e.g. `query`).
///
/// # Example
/// ```rust,no_run
/// # use nu_protocol::{Value, ShellError};
/// # use nu_plugin::EngineInterface;
/// # fn example(engine: &EngineInterface) -> Result<(), ShellError> {
/// eprintln!("{}", engine.get_help()?);
/// # Ok(())
/// # }
/// ```
pub fn get_help(&self) -> Result<String, ShellError> {
match self.engine_call(EngineCall::GetHelp)? {
EngineCallResponse::PipelineData(PipelineData::Value(Value::String { val, .. }, _)) => {
Ok(val)
}
_ => Err(ShellError::PluginFailedToDecode {
msg: "Received unexpected response type for EngineCall::GetHelp".into(),
}),
}
}
/// Ask the engine to evaluate a closure. Input to the closure is passed as a stream, and the /// Ask the engine to evaluate a closure. Input to the closure is passed as a stream, and the
/// output is available as a stream. /// output is available as a stream.
/// ///

View file

@ -1005,6 +1005,24 @@ fn interface_add_env_var() -> Result<(), ShellError> {
Ok(()) Ok(())
} }
#[test]
fn interface_get_help() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.engine();
let interface = manager.interface_for_context(0);
start_fake_plugin_call_responder(manager, 1, move |_| {
EngineCallResponse::value(Value::test_string("help string"))
});
let help = interface.get_help()?;
assert_eq!("help string", help);
assert!(test.has_unconsumed_write());
Ok(())
}
#[test] #[test]
fn interface_eval_closure_with_stream() -> Result<(), ShellError> { fn interface_eval_closure_with_stream() -> Result<(), ShellError> {
let test = TestCase::new(); let test = TestCase::new();

View file

@ -1018,6 +1018,12 @@ pub(crate) fn handle_engine_call(
context.add_env_var(name, value)?; context.add_env_var(name, value)?;
Ok(EngineCallResponse::empty()) Ok(EngineCallResponse::empty())
} }
EngineCall::GetHelp => {
let help = context.get_help()?;
Ok(EngineCallResponse::value(Value::string(
help.item, help.span,
)))
}
EngineCall::EvalClosure { EngineCall::EvalClosure {
closure, closure,
positional, positional,

View file

@ -447,6 +447,8 @@ pub enum EngineCall<D> {
GetCurrentDir, GetCurrentDir,
/// Set an environment variable in the caller's scope /// Set an environment variable in the caller's scope
AddEnvVar(String, Value), AddEnvVar(String, Value),
/// Get help for the current command
GetHelp,
/// Evaluate a closure with stream input/output /// Evaluate a closure with stream input/output
EvalClosure { EvalClosure {
/// The closure to call. /// The closure to call.
@ -474,6 +476,7 @@ impl<D> EngineCall<D> {
EngineCall::GetEnvVars => "GetEnvs", EngineCall::GetEnvVars => "GetEnvs",
EngineCall::GetCurrentDir => "GetCurrentDir", EngineCall::GetCurrentDir => "GetCurrentDir",
EngineCall::AddEnvVar(..) => "AddEnvVar", EngineCall::AddEnvVar(..) => "AddEnvVar",
EngineCall::GetHelp => "GetHelp",
EngineCall::EvalClosure { .. } => "EvalClosure", EngineCall::EvalClosure { .. } => "EvalClosure",
} }
} }
@ -491,6 +494,7 @@ impl<D> EngineCall<D> {
EngineCall::GetEnvVars => EngineCall::GetEnvVars, EngineCall::GetEnvVars => EngineCall::GetEnvVars,
EngineCall::GetCurrentDir => EngineCall::GetCurrentDir, EngineCall::GetCurrentDir => EngineCall::GetCurrentDir,
EngineCall::AddEnvVar(name, value) => EngineCall::AddEnvVar(name, value), EngineCall::AddEnvVar(name, value) => EngineCall::AddEnvVar(name, value),
EngineCall::GetHelp => EngineCall::GetHelp,
EngineCall::EvalClosure { EngineCall::EvalClosure {
closure, closure,
positional, positional,

View file

@ -28,11 +28,10 @@ particularly useful.
fn run( fn run(
&self, &self,
_plugin: &Self::Plugin, _plugin: &Self::Plugin,
_engine: &EngineInterface, engine: &EngineInterface,
call: &EvaluatedCall, call: &EvaluatedCall,
_input: &Value, _input: &Value,
) -> Result<Value, LabeledError> { ) -> Result<Value, LabeledError> {
Err(LabeledError::new("No subcommand provided") Ok(Value::string(engine.get_help()?, call.head))
.with_label("add --help to see a list of subcommands", call.head))
} }
} }

View file

@ -18,7 +18,6 @@ bench = false
[dependencies] [dependencies]
nu-plugin = { path = "../nu-plugin", version = "0.91.1" } nu-plugin = { path = "../nu-plugin", version = "0.91.1" }
nu-protocol = { path = "../nu-protocol", version = "0.91.1" } nu-protocol = { path = "../nu-protocol", version = "0.91.1" }
nu-engine = { path = "../nu-engine", version = "0.91.1" }
gjson = "0.8" gjson = "0.8"
scraper = { default-features = false, version = "0.19" } scraper = { default-features = false, version = "0.19" }

View file

@ -2,10 +2,8 @@ use crate::query_json::QueryJson;
use crate::query_web::QueryWeb; use crate::query_web::QueryWeb;
use crate::query_xml::QueryXml; use crate::query_xml::QueryXml;
use nu_engine::documentation::get_flags_section;
use nu_plugin::{EvaluatedCall, Plugin, PluginCommand, SimplePluginCommand}; use nu_plugin::{EvaluatedCall, Plugin, PluginCommand, SimplePluginCommand};
use nu_protocol::{Category, LabeledError, PluginSignature, Value}; use nu_protocol::{Category, LabeledError, PluginSignature, Value};
use std::fmt::Write;
#[derive(Default)] #[derive(Default)]
pub struct Query; pub struct Query;
@ -46,36 +44,10 @@ impl SimplePluginCommand for QueryCommand {
fn run( fn run(
&self, &self,
_plugin: &Query, _plugin: &Query,
_engine: &nu_plugin::EngineInterface, engine: &nu_plugin::EngineInterface,
call: &EvaluatedCall, call: &EvaluatedCall,
_input: &Value, _input: &Value,
) -> Result<Value, LabeledError> { ) -> Result<Value, LabeledError> {
let help = get_brief_subcommand_help(); Ok(Value::string(engine.get_help()?, call.head))
Ok(Value::string(help, call.head))
} }
} }
pub fn get_brief_subcommand_help() -> String {
let sigs: Vec<_> = Query
.commands()
.into_iter()
.map(|cmd| cmd.signature())
.collect();
let mut help = String::new();
let _ = write!(help, "{}\n\n", sigs[0].sig.usage);
let _ = write!(help, "Usage:\n > {}\n\n", sigs[0].sig.name);
help.push_str("Subcommands:\n");
for x in sigs.iter().enumerate() {
if x.0 == 0 {
continue;
}
let _ = writeln!(help, " {} - {}", x.1.sig.name, x.1.sig.usage);
}
help.push_str(&get_flags_section(None, &sigs[0].sig, |v| {
format!("{:#?}", v)
}));
help
}