use crate::{
engine::{Call, Command, CommandType, EngineState, Stack},
BlockId, PipelineData, ShellError, SyntaxShape, Type, Value, VarId,
};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Flag {
pub long: String,
pub short: Option<char>,
pub arg: Option<SyntaxShape>,
pub required: bool,
pub desc: String,
pub var_id: Option<VarId>,
pub default_value: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PositionalArg {
pub name: String,
pub desc: String,
pub shape: SyntaxShape,
pub var_id: Option<VarId>,
pub default_value: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Category {
Bits,
Bytes,
Chart,
Conversions,
Core,
Custom(String),
Database,
Date,
Debug,
Default,
Removed,
Env,
Experimental,
FileSystem,
Filters,
Formats,
Generators,
Hash,
History,
Math,
Misc,
Network,
Path,
Platform,
Plugin,
Random,
Shells,
Strings,
System,
Viewers,
}
impl std::fmt::Display for Category {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
Category::Bits => "bits",
Category::Bytes => "bytes",
Category::Chart => "chart",
Category::Conversions => "conversions",
Category::Core => "core",
Category::Custom(name) => name,
Category::Database => "database",
Category::Date => "date",
Category::Debug => "debug",
Category::Default => "default",
Category::Removed => "removed",
Category::Env => "env",
Category::Experimental => "experimental",
Category::FileSystem => "filesystem",
Category::Filters => "filters",
Category::Formats => "formats",
Category::Generators => "generators",
Category::Hash => "hash",
Category::History => "history",
Category::Math => "math",
Category::Misc => "misc",
Category::Network => "network",
Category::Path => "path",
Category::Platform => "platform",
Category::Plugin => "plugin",
Category::Random => "random",
Category::Shells => "shells",
Category::Strings => "strings",
Category::System => "system",
Category::Viewers => "viewers",
};
write!(f, "{msg}")
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Signature {
pub name: String,
pub description: String,
pub extra_description: String,
pub search_terms: Vec<String>,
pub required_positional: Vec<PositionalArg>,
pub optional_positional: Vec<PositionalArg>,
pub rest_positional: Option<PositionalArg>,
pub named: Vec<Flag>,
pub input_output_types: Vec<(Type, Type)>,
pub allow_variants_without_examples: bool,
pub is_filter: bool,
pub creates_scope: bool,
pub allows_unknown_args: bool,
pub category: Category,
}
impl PartialEq for Signature {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.description == other.description
&& self.required_positional == other.required_positional
&& self.optional_positional == other.optional_positional
&& self.rest_positional == other.rest_positional
&& self.is_filter == other.is_filter
}
}
impl Eq for Signature {}
impl Signature {
pub fn new(name: impl Into<String>) -> Signature {
Signature {
name: name.into(),
description: String::new(),
extra_description: String::new(),
search_terms: vec![],
required_positional: vec![],
optional_positional: vec![],
rest_positional: None,
input_output_types: vec![],
allow_variants_without_examples: false,
named: vec![],
is_filter: false,
creates_scope: false,
category: Category::Default,
allows_unknown_args: false,
}
}
pub fn get_input_type(&self) -> Type {
match self.input_output_types.len() {
0 => Type::Any,
1 => self.input_output_types[0].0.clone(),
_ => {
let first = &self.input_output_types[0].0;
if self
.input_output_types
.iter()
.all(|(input, _)| input == first)
{
first.clone()
} else {
Type::Any
}
}
}
}
pub fn get_output_type(&self) -> Type {
match self.input_output_types.len() {
0 => Type::Any,
1 => self.input_output_types[0].1.clone(),
_ => {
let first = &self.input_output_types[0].1;
if self
.input_output_types
.iter()
.all(|(_, output)| output == first)
{
first.clone()
} else {
Type::Any
}
}
}
}
pub fn add_help(mut self) -> Signature {
let flag = Flag {
long: "help".into(),
short: Some('h'),
arg: None,
desc: "Display the help message for this command".into(),
required: false,
var_id: None,
default_value: None,
};
self.named.push(flag);
self
}
pub fn build(name: impl Into<String>) -> Signature {
Signature::new(name.into()).add_help()
}
pub fn description(mut self, msg: impl Into<String>) -> Signature {
self.description = msg.into();
self
}
pub fn extra_description(mut self, msg: impl Into<String>) -> Signature {
self.extra_description = msg.into();
self
}
pub fn search_terms(mut self, terms: Vec<String>) -> Signature {
self.search_terms = terms;
self
}
pub fn update_from_command(mut self, command: &dyn Command) -> Signature {
self.search_terms = command
.search_terms()
.into_iter()
.map(|term| term.to_string())
.collect();
self.extra_description = command.extra_description().to_string();
self.description = command.description().to_string();
self
}
pub fn allows_unknown_args(mut self) -> Signature {
self.allows_unknown_args = true;
self
}
pub fn required(
mut self,
name: impl Into<String>,
shape: impl Into<SyntaxShape>,
desc: impl Into<String>,
) -> Signature {
self.required_positional.push(PositionalArg {
name: name.into(),
desc: desc.into(),
shape: shape.into(),
var_id: None,
default_value: None,
});
self
}
pub fn optional(
mut self,
name: impl Into<String>,
shape: impl Into<SyntaxShape>,
desc: impl Into<String>,
) -> Signature {
self.optional_positional.push(PositionalArg {
name: name.into(),
desc: desc.into(),
shape: shape.into(),
var_id: None,
default_value: None,
});
self
}
pub fn rest(
mut self,
name: &str,
shape: impl Into<SyntaxShape>,
desc: impl Into<String>,
) -> Signature {
self.rest_positional = Some(PositionalArg {
name: name.into(),
desc: desc.into(),
shape: shape.into(),
var_id: None,
default_value: None,
});
self
}
pub fn operates_on_cell_paths(&self) -> bool {
self.required_positional
.iter()
.chain(self.rest_positional.iter())
.any(|pos| {
matches!(
pos,
PositionalArg {
shape: SyntaxShape::CellPath,
..
}
)
})
}
pub fn named(
mut self,
name: impl Into<String>,
shape: impl Into<SyntaxShape>,
desc: impl Into<String>,
short: Option<char>,
) -> Signature {
let (name, s) = self.check_names(name, short);
self.named.push(Flag {
long: name,
short: s,
arg: Some(shape.into()),
required: false,
desc: desc.into(),
var_id: None,
default_value: None,
});
self
}
pub fn required_named(
mut self,
name: impl Into<String>,
shape: impl Into<SyntaxShape>,
desc: impl Into<String>,
short: Option<char>,
) -> Signature {
let (name, s) = self.check_names(name, short);
self.named.push(Flag {
long: name,
short: s,
arg: Some(shape.into()),
required: true,
desc: desc.into(),
var_id: None,
default_value: None,
});
self
}
pub fn switch(
mut self,
name: impl Into<String>,
desc: impl Into<String>,
short: Option<char>,
) -> Signature {
let (name, s) = self.check_names(name, short);
self.named.push(Flag {
long: name,
short: s,
arg: None,
required: false,
desc: desc.into(),
var_id: None,
default_value: None,
});
self
}
pub fn input_output_type(mut self, input_type: Type, output_type: Type) -> Signature {
self.input_output_types.push((input_type, output_type));
self
}
pub fn input_output_types(mut self, input_output_types: Vec<(Type, Type)>) -> Signature {
self.input_output_types = input_output_types;
self
}
pub fn category(mut self, category: Category) -> Signature {
self.category = category;
self
}
pub fn creates_scope(mut self) -> Signature {
self.creates_scope = true;
self
}
pub fn allow_variants_without_examples(mut self, allow: bool) -> Signature {
self.allow_variants_without_examples = allow;
self
}
pub fn call_signature(&self) -> String {
let mut one_liner = String::new();
one_liner.push_str(&self.name);
one_liner.push(' ');
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));
}
for positional in &self.optional_positional {
one_liner.push_str(&get_positional_short_name(positional, false));
}
if let Some(rest) = &self.rest_positional {
let _ = write!(one_liner, "...{}", get_positional_short_name(rest, false));
}
one_liner
}
pub fn get_shorts(&self) -> Vec<char> {
self.named.iter().filter_map(|f| f.short).collect()
}
pub fn get_names(&self) -> Vec<&str> {
self.named.iter().map(|f| f.long.as_str()).collect()
}
fn check_names(&self, name: impl Into<String>, short: Option<char>) -> (String, Option<char>) {
let s = short.inspect(|c| {
assert!(
!self.get_shorts().contains(c),
"There may be duplicate short flags for '-{}'",
c
);
});
let name = {
let name: String = name.into();
assert!(
!self.get_names().contains(&name.as_str()),
"There may be duplicate name flags for '--{}'",
name
);
name
};
(name, s)
}
pub fn get_positional(&self, position: usize) -> Option<&PositionalArg> {
if position < self.required_positional.len() {
self.required_positional.get(position)
} else if position < (self.required_positional.len() + self.optional_positional.len()) {
self.optional_positional
.get(position - self.required_positional.len())
} else {
self.rest_positional.as_ref()
}
}
pub fn num_positionals(&self) -> usize {
let mut total = self.required_positional.len() + self.optional_positional.len();
for positional in &self.required_positional {
if let SyntaxShape::Keyword(..) = positional.shape {
total += 1;
}
}
for positional in &self.optional_positional {
if let SyntaxShape::Keyword(..) = positional.shape {
total += 1;
}
}
total
}
pub fn get_long_flag(&self, name: &str) -> Option<Flag> {
for flag in &self.named {
if flag.long == name {
return Some(flag.clone());
}
}
None
}
pub fn get_short_flag(&self, short: char) -> Option<Flag> {
for flag in &self.named {
if let Some(short_flag) = &flag.short {
if *short_flag == short {
return Some(flag.clone());
}
}
}
None
}
pub fn filter(mut self) -> Signature {
self.is_filter = true;
self
}
pub fn predeclare(self) -> Box<dyn Command> {
Box::new(Predeclaration { signature: self })
}
pub fn into_block_command(self, block_id: BlockId) -> Box<dyn Command> {
Box::new(BlockCommand {
signature: self,
block_id,
})
}
pub fn formatted_flags(self) -> String {
if self.named.len() < 11 {
let mut s = "Available flags:".to_string();
for flag in self.named {
if let Some(short) = flag.short {
let _ = write!(s, " --{}(-{}),", flag.long, short);
} else {
let _ = write!(s, " --{},", flag.long);
}
}
s.remove(s.len() - 1);
let _ = write!(s, ". Use `--help` for more information.");
s
} else {
let mut s = "Some available flags:".to_string();
for flag in self.named {
if let Some(short) = flag.short {
let _ = write!(s, " --{}(-{}),", flag.long, short);
} else {
let _ = write!(s, " --{},", flag.long);
}
}
s.remove(s.len() - 1);
let _ = write!(
s,
"... Use `--help` for a full list of flags and more information."
);
s
}
}
}
#[derive(Clone)]
struct Predeclaration {
signature: Signature,
}
impl Command for Predeclaration {
fn name(&self) -> &str {
&self.signature.name
}
fn signature(&self) -> Signature {
self.signature.clone()
}
fn description(&self) -> &str {
&self.signature.description
}
fn extra_description(&self) -> &str {
&self.signature.extra_description
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
_call: &Call,
_input: PipelineData,
) -> Result<PipelineData, crate::ShellError> {
panic!("Internal error: can't run a predeclaration without a body")
}
}
fn get_positional_short_name(arg: &PositionalArg, is_required: bool) -> String {
match &arg.shape {
SyntaxShape::Keyword(name, ..) => {
if is_required {
format!("{} <{}> ", String::from_utf8_lossy(name), arg.name)
} else {
format!("({} <{}>) ", String::from_utf8_lossy(name), arg.name)
}
}
_ => {
if is_required {
format!("<{}> ", arg.name)
} else {
format!("({}) ", arg.name)
}
}
}
}
#[derive(Clone)]
struct BlockCommand {
signature: Signature,
block_id: BlockId,
}
impl Command for BlockCommand {
fn name(&self) -> &str {
&self.signature.name
}
fn signature(&self) -> Signature {
self.signature.clone()
}
fn description(&self) -> &str {
&self.signature.description
}
fn extra_description(&self) -> &str {
&self.signature.extra_description
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
_call: &Call,
_input: PipelineData,
) -> Result<crate::PipelineData, crate::ShellError> {
Err(ShellError::GenericError {
error: "Internal error: can't run custom command with 'run', use block_id".into(),
msg: "".into(),
span: None,
help: None,
inner: vec![],
})
}
fn command_type(&self) -> CommandType {
CommandType::Custom
}
fn block_id(&self) -> Option<BlockId> {
Some(self.block_id)
}
}