diff --git a/src/error.rs b/src/error.rs index 036b842..9dfb2f1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,13 +6,16 @@ use indextree::NodeId; +use crate::pipe::PipeError; + #[derive(Debug)] pub enum Error { Parser(ParserError), EditConfig(yang4::Error), ValidateConfig(yang4::Error), - Callback(String), + Callback(CallbackError), Backend(tonic::Status), + Pipe(PipeError), } #[derive(Debug)] @@ -22,6 +25,12 @@ pub enum ParserError { Ambiguous(Vec), } +#[derive(Debug)] +pub enum CallbackError { + BrokenPipe, + Other(String), +} + // ===== impl Error ===== impl std::fmt::Display for Error { @@ -40,6 +49,9 @@ impl std::fmt::Display for Error { Error::Backend(error) => { write!(f, "{}", error) } + Error::Pipe(error) => { + write!(f, "{}", error) + } } } } @@ -61,3 +73,30 @@ impl std::fmt::Display for ParserError { } impl std::error::Error for ParserError {} + +// ===== impl CallbackError ===== + +impl std::fmt::Display for CallbackError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CallbackError::BrokenPipe => write!(f, "broken pipe"), + CallbackError::Other(msg) => write!(f, "{}", msg), + } + } +} + +impl From for CallbackError { + fn from(s: String) -> Self { + CallbackError::Other(s) + } +} + +impl From for CallbackError { + fn from(e: std::io::Error) -> Self { + if e.kind() == std::io::ErrorKind::BrokenPipe { + CallbackError::BrokenPipe + } else { + CallbackError::Other(e.to_string()) + } + } +} diff --git a/src/internal_commands.rs b/src/internal_commands.rs index d3e4d0f..9aa2645 100644 --- a/src/internal_commands.rs +++ b/src/internal_commands.rs @@ -5,8 +5,7 @@ // use std::collections::BTreeMap; -use std::fmt::Write; -use std::process::{Child, Command, Stdio}; +use std::fmt::Write as _; use chrono::prelude::*; use indextree::NodeId; @@ -19,6 +18,7 @@ use yang4::data::{ use yang4::schema::SchemaNodeKind; use crate::YANG_CTX; +use crate::error::CallbackError; use crate::grpc::proto; use crate::parser::ParsedArgs; use crate::session::{CommandMode, ConfigurationType, Session}; @@ -198,7 +198,7 @@ impl<'a> YangTableBuilder<'a> { } // Builds and displays the table. - pub fn show(self) -> Result<(), String> { + pub fn show(self) -> Result<(), CallbackError> { let xpath_req = "/ietf-routing:routing/control-plane-protocols"; // Fetch data. @@ -222,9 +222,11 @@ impl<'a> YangTableBuilder<'a> { let values = Vec::new(); Self::show_path(&mut table, dnode, &self.paths, values); - // Print the table to stdout. - if let Err(error) = page_table(self.session, &table) { - println!("% failed to display data: {}", error); + // Print the table. + if !table.is_empty() { + let writer = self.session.writer(); + table.print(writer)?; + writeln!(writer)?; } Ok(()) @@ -246,63 +248,13 @@ fn get_opt_arg(args: &mut ParsedArgs, name: &str) -> Option { None } -fn pager() -> Result { - Command::new("less") - // Exit immediately if the data fits on one screen. - .arg("-F") - // Do not clear the screen on exit. - .arg("-X") - .stdin(Stdio::piped()) - .spawn() -} - -fn page_output(session: &Session, data: &str) -> Result<(), std::io::Error> { - if session.use_pager() { - use std::io::Write; - - // Spawn the pager process. - let mut pager = pager()?; - - // Feed the data to the pager. - pager.stdin.as_mut().unwrap().write_all(data.as_bytes())?; - - // Wait for the pager process to finish. - pager.wait()?; - } else { - // Print the data directly to the console. - println!("{}", data); - } - - Ok(()) -} - -fn page_table(session: &Session, table: &Table) -> Result<(), std::io::Error> { - if table.is_empty() { - return Ok(()); - } - - if session.use_pager() { - use std::io::Write; - - // Spawn the pager process. - let mut pager = pager()?; - - // Print the table. - let mut output = Vec::new(); - table.print(&mut output)?; - writeln!(output)?; - - // Feed the data to the pager. - pager.stdin.as_mut().unwrap().write_all(&output)?; - - // Wait for the pager process to finish. - pager.wait()?; - } else { - // Print the table directly to the console. - table.printstd(); - println!(); - } - +fn write_output( + session: &mut Session, + data: &str, +) -> Result<(), std::io::Error> { + let w = session.writer(); + w.write_all(data.as_bytes())?; + writeln!(w)?; Ok(()) } @@ -365,7 +317,7 @@ pub fn cmd_config( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { let mode = CommandMode::Configure { nodes: vec![] }; session.mode_set(mode); Ok(false) @@ -377,7 +329,7 @@ pub fn cmd_exit_exec( _commands: &Commands, _session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { // Do nothing. Ok(true) } @@ -386,7 +338,7 @@ pub fn cmd_exit_config( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { session.mode_config_exit(); Ok(false) } @@ -397,7 +349,7 @@ pub fn cmd_end( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { session.mode_set(CommandMode::Operational); Ok(false) } @@ -408,27 +360,32 @@ pub fn cmd_list( commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { match session.mode() { CommandMode::Operational => { // List EXEC-level commands. - cmd_list_root(commands, &commands.exec_root); + cmd_list_root(commands, session, &commands.exec_root); } CommandMode::Configure { .. } => { // List internal configuration commands first. - cmd_list_root(commands, &commands.config_dflt_internal); - println!("---"); - cmd_list_root(commands, &commands.config_root_internal); - println!("---"); + cmd_list_root(commands, session, &commands.config_dflt_internal); + writeln!(session.writer(), "---")?; + cmd_list_root(commands, session, &commands.config_root_internal); + writeln!(session.writer(), "---")?; // List YANG configuration commands. - cmd_list_root(commands, &session.mode().token(commands)); + let yang_root = session.mode().token(commands); + cmd_list_root(commands, session, &yang_root); } } Ok(false) } -pub fn cmd_list_root(commands: &Commands, top_token_id: &NodeId) { +pub fn cmd_list_root( + commands: &Commands, + session: &mut Session, + top_token_id: &NodeId, +) { for token_id in top_token_id .descendants(&commands.arena) @@ -454,7 +411,7 @@ pub fn cmd_list_root(commands: &Commands, top_token_id: &NodeId) { cmd_string.push(' '); } - println!("{}", cmd_string); + let _ = writeln!(session.writer(), "{}", cmd_string); } } @@ -464,7 +421,7 @@ pub fn cmd_pwd( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { println!( "{}", session.mode().data_path().unwrap_or_else(|| "/".to_owned()) @@ -478,7 +435,7 @@ pub fn cmd_top( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { session.mode_config_top(); Ok(false) } @@ -489,7 +446,7 @@ pub fn cmd_discard( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { session.candidate_discard(); Ok(false) } @@ -500,7 +457,7 @@ pub fn cmd_commit( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let comment = get_opt_arg(&mut args, "comment"); match session.candidate_commit(comment) { Ok(_) => { @@ -520,7 +477,7 @@ pub fn cmd_validate( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { match session.candidate_validate() { Ok(_) => println!("% candidate configuration validated successfully"), Err(error) => { @@ -621,7 +578,7 @@ pub fn cmd_show_config( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { // Parse parameters. let config_type = get_arg(&mut args, "configuration"); let config_type = match config_type.as_str() { @@ -646,9 +603,7 @@ pub fn cmd_show_config( Some(_) => panic!("unknown format"), None => cmd_show_config_cmds(config, with_defaults), }; - if let Err(error) = page_output(session, &data) { - println!("% failed to print configuration: {}", error) - } + write_output(session, &data)?; Ok(false) } @@ -657,7 +612,7 @@ pub fn cmd_show_config_changes( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { let running = session.get_configuration(ConfigurationType::Running); let running = cmd_show_config_cmds(running, false); let candidate = session.get_configuration(ConfigurationType::Candidate); @@ -680,7 +635,7 @@ pub fn cmd_show_state( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let xpath = get_opt_arg(&mut args, "xpath"); let format = get_opt_arg(&mut args, "format"); let format = match format.as_deref() { @@ -693,9 +648,7 @@ pub fn cmd_show_state( match session.get(proto::get_request::DataType::State, format, false, xpath) { Ok(proto::data_tree::Data::DataString(data)) => { - if let Err(error) = page_output(session, &data) { - println!("% failed to print state data: {}", error) - } + write_output(session, &data)?; } Ok(proto::data_tree::Data::DataBytes(_)) => unreachable!(), Err(error) => println!("% failed to fetch state data: {}", error), @@ -710,7 +663,7 @@ pub fn cmd_show_yang_modules( _commands: &Commands, _session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { // Create the table let mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); @@ -756,7 +709,7 @@ pub fn cmd_show_isis_interface( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { YangTableBuilder::new(session, proto::get_request::DataType::All) .xpath(XPATH_PROTOCOL) .filter_list_key("type", Some(PROTOCOL_ISIS)) @@ -776,7 +729,7 @@ pub fn cmd_show_isis_adjacency( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let hostnames = isis_hostnames(session)?; YangTableBuilder::new(session, proto::get_request::DataType::State) .xpath(XPATH_PROTOCOL) @@ -806,7 +759,7 @@ pub fn cmd_show_isis_database( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { let hostnames = isis_hostnames(session)?; YangTableBuilder::new(session, proto::get_request::DataType::State) .xpath(XPATH_PROTOCOL) @@ -838,7 +791,7 @@ pub fn cmd_show_isis_route( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { YangTableBuilder::new(session, proto::get_request::DataType::State) .xpath(XPATH_PROTOCOL) .filter_list_key("type", Some(PROTOCOL_ISIS)) @@ -907,7 +860,7 @@ pub fn cmd_show_ospf_interface( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let protocol = match get_arg(&mut args, "protocol").as_str() { "ospfv2" => PROTOCOL_OSPFV2, "ospfv3" => PROTOCOL_OSPFV3, @@ -946,9 +899,7 @@ pub fn cmd_show_ospf_interface_detail( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { - let mut output = String::new(); - +) -> Result { // Parse arguments. let protocol = match get_arg(&mut args, "protocol").as_str() { "ospfv2" => PROTOCOL_OSPFV2, @@ -980,6 +931,7 @@ pub fn cmd_show_ospf_interface_detail( let area = dnode.child_value("area-id"); // Iterate over OSPF interfaces. + let output = session.writer(); for dnode in dnode.find_xpath(&xpath_iface).unwrap() { writeln!(output, "{}", dnode.child_value("name")).unwrap(); writeln!(output, " instance: {}", instance).unwrap(); @@ -1009,10 +961,6 @@ pub fn cmd_show_ospf_interface_detail( } } - if let Err(error) = page_output(session, &output) { - println!("% failed to print data: {}", error) - } - Ok(false) } @@ -1020,7 +968,7 @@ pub fn cmd_show_ospf_vlink( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let protocol = match get_arg(&mut args, "protocol").as_str() { "ospfv2" => PROTOCOL_OSPFV2, "ospfv3" => PROTOCOL_OSPFV3, @@ -1061,7 +1009,7 @@ pub fn cmd_show_ospf_neighbor( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let protocol = match get_arg(&mut args, "protocol").as_str() { "ospfv2" => PROTOCOL_OSPFV2, "ospfv3" => PROTOCOL_OSPFV3, @@ -1107,9 +1055,7 @@ pub fn cmd_show_ospf_neighbor_detail( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { - let mut output = String::new(); - +) -> Result { // Parse arguments. let protocol = match get_arg(&mut args, "protocol").as_str() { "ospfv2" => PROTOCOL_OSPFV2, @@ -1135,6 +1081,7 @@ pub fn cmd_show_ospf_neighbor_detail( fetch_data(session, proto::get_request::DataType::All, xpath_req)?; // Iterate over OSPF instances. + let output = session.writer(); for dnode in data.find_xpath(&xpath_instance).unwrap() { let instance = dnode.child_value("name"); @@ -1190,10 +1137,6 @@ pub fn cmd_show_ospf_neighbor_detail( } } - if let Err(error) = page_output(session, &output) { - println!("% failed to print data: {}", error) - } - Ok(false) } @@ -1201,7 +1144,7 @@ pub fn cmd_show_ospf_database_as( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let protocol = match get_arg(&mut args, "protocol").as_str() { "ospfv2" => PROTOCOL_OSPFV2, "ospfv3" => PROTOCOL_OSPFV3, @@ -1244,7 +1187,7 @@ pub fn cmd_show_ospf_database_area( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let protocol = match get_arg(&mut args, "protocol").as_str() { "ospfv2" => PROTOCOL_OSPFV2, "ospfv3" => PROTOCOL_OSPFV3, @@ -1289,7 +1232,7 @@ pub fn cmd_show_ospf_database_link( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let protocol = match get_arg(&mut args, "protocol").as_str() { "ospfv2" => PROTOCOL_OSPFV2, "ospfv3" => PROTOCOL_OSPFV3, @@ -1336,7 +1279,7 @@ pub fn cmd_show_ospf_route( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let protocol = match get_arg(&mut args, "protocol").as_str() { "ospfv2" => PROTOCOL_OSPFV2, "ospfv3" => PROTOCOL_OSPFV3, @@ -1364,7 +1307,7 @@ pub fn cmd_show_ospf_hostnames( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let protocol = match get_arg(&mut args, "protocol").as_str() { "ospfv2" => PROTOCOL_OSPFV2, "ospfv3" => PROTOCOL_OSPFV3, @@ -1425,7 +1368,7 @@ pub fn cmd_show_rip_interface( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { // Parse arguments. let protocol = match get_arg(&mut args, "protocol").as_str() { "ripv2" => PROTOCOL_RIPV2, @@ -1449,9 +1392,7 @@ pub fn cmd_show_rip_interface_detail( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { - let mut output = String::new(); - +) -> Result { // Parse arguments. let protocol = match get_arg(&mut args, "protocol").as_str() { "ripv2" => PROTOCOL_RIPV2, @@ -1479,6 +1420,7 @@ pub fn cmd_show_rip_interface_detail( fetch_data(session, proto::get_request::DataType::State, xpath_req)?; // Iterate over RIP instances. + let output = session.writer(); for dnode in data.find_xpath(&xpath_instance).unwrap() { let instance = dnode.child_value("name"); @@ -1511,10 +1453,6 @@ pub fn cmd_show_rip_interface_detail( } } - if let Err(error) = page_output(session, &output) { - println!("% failed to print data: {}", error) - } - Ok(false) } @@ -1522,7 +1460,7 @@ pub fn cmd_show_rip_neighbor( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { // Parse arguments. let (protocol, afi, address) = match get_arg(&mut args, "protocol").as_str() { @@ -1550,9 +1488,7 @@ pub fn cmd_show_rip_neighbor_detail( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { - let mut output = String::new(); - +) -> Result { // Parse arguments. let (protocol, afi, address) = match get_arg(&mut args, "protocol").as_str() { @@ -1581,6 +1517,7 @@ pub fn cmd_show_rip_neighbor_detail( fetch_data(session, proto::get_request::DataType::State, xpath_req)?; // Iterate over RIP instances. + let output = session.writer(); for dnode in data.find_xpath(&xpath_instance).unwrap() { let instance = dnode.child_value("name"); @@ -1603,10 +1540,6 @@ pub fn cmd_show_rip_neighbor_detail( } } - if let Err(error) = page_output(session, &output) { - println!("% failed to print data: {}", error) - } - Ok(false) } @@ -1614,7 +1547,7 @@ pub fn cmd_show_rip_route( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { // Parse arguments. let (protocol, afi, prefix) = match get_arg(&mut args, "protocol").as_str() { @@ -1662,7 +1595,7 @@ pub fn cmd_show_mpls_ldp_discovery( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { YangTableBuilder::new(session, proto::get_request::DataType::State) .xpath(XPATH_PROTOCOL) .filter_list_key("type", Some(PROTOCOL_MPLS_LDP)) @@ -1683,9 +1616,7 @@ pub fn cmd_show_mpls_ldp_discovery_detail( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { - let mut output = String::new(); - +) -> Result { // Parse arguments. let name = get_opt_arg(&mut args, "name"); @@ -1709,6 +1640,7 @@ pub fn cmd_show_mpls_ldp_discovery_detail( fetch_data(session, proto::get_request::DataType::State, xpath_req)?; // Iterate over MPLS LDP instances. + let output = session.writer(); for dnode in data.find_xpath(&xpath_instance).unwrap() { let instance = dnode.child_value("name"); @@ -1766,10 +1698,6 @@ pub fn cmd_show_mpls_ldp_discovery_detail( } } - if let Err(error) = page_output(session, &output) { - println!("% failed to print data: {}", error) - } - Ok(false) } @@ -1777,7 +1705,7 @@ pub fn cmd_show_mpls_ldp_peer( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { YangTableBuilder::new(session, proto::get_request::DataType::State) .xpath(XPATH_PROTOCOL) .filter_list_key("type", Some(PROTOCOL_MPLS_LDP)) @@ -1799,9 +1727,7 @@ pub fn cmd_show_mpls_ldp_peer_detail( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { - let mut output = String::new(); - +) -> Result { // Parse arguments. let lsr_id = get_opt_arg(&mut args, "lsr-id"); @@ -1825,6 +1751,7 @@ pub fn cmd_show_mpls_ldp_peer_detail( fetch_data(session, proto::get_request::DataType::State, xpath_req)?; // Iterate over MPLS LDP instances. + let output = session.writer(); for dnode in data.find_xpath(&xpath_instance).unwrap() { let instance = dnode.child_value("name"); @@ -1943,10 +1870,6 @@ pub fn cmd_show_mpls_ldp_peer_detail( } } - if let Err(error) = page_output(session, &output) { - println!("% failed to print data: {}", error) - } - Ok(false) } @@ -1954,7 +1877,7 @@ pub fn cmd_show_mpls_ldp_binding_address( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { YangTableBuilder::new(session, proto::get_request::DataType::State) .xpath(XPATH_PROTOCOL) .filter_list_key("type", Some(PROTOCOL_MPLS_LDP)) @@ -1988,7 +1911,7 @@ pub fn cmd_show_mpls_ldp_binding_fec( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { YangTableBuilder::new(session, proto::get_request::DataType::State) .xpath(XPATH_PROTOCOL) .filter_list_key("type", Some(PROTOCOL_MPLS_LDP)) @@ -2060,13 +1983,13 @@ pub fn cmd_show_bgp_summary( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let afi = get_opt_arg(&mut args, "afi").unwrap_or("ipv4".to_owned()); let afi = match afi.as_str() { "ipv4" => "iana-bgp-types:ipv4-unicast", "ipv6" => "iana-bgp-types:ipv6-unicast", - _ => return Err(format!("Unsupported address family: {}", afi)), + _ => return Err(format!("Unsupported address family: {}", afi).into()), }; let afi_xpath = format!("afi-safis/afi-safi[name='{}']/prefixes", afi); @@ -2178,11 +2101,9 @@ pub fn cmd_show_bgp_neighbor( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let attrs = bgp_get_attrs(session).unwrap(); - let mut output = String::new(); - let neighbor = get_arg(&mut args, "neighbor"); let rt_type = get_arg(&mut args, "type"); let afi = get_opt_arg(&mut args, "afi").unwrap_or("ipv4".to_owned()); @@ -2190,7 +2111,7 @@ pub fn cmd_show_bgp_neighbor( let afi = match afi.as_str() { "ipv4" => "ipv4-unicast", "ipv6" => "ipv6-unicast", - _ => return Err(format!("Unsupported address family: {}", afi)), + _ => return Err(format!("Unsupported address family: {}", afi).into()), }; let rt_type = match rt_type.as_str() { @@ -2216,6 +2137,8 @@ pub fn cmd_show_bgp_neighbor( let xpath_routes = format!("{}/route", &xpath_req); + let output = session.writer(); + writeln!(output, "\nAddress family: {afi}").unwrap(); writeln!( output, @@ -2227,11 +2150,7 @@ pub fn cmd_show_bgp_neighbor( let prefix = route.child_opt_value("prefix").unwrap(); let index = route.child_opt_value("attr-index").unwrap(); let route_attrs = attrs.get(&index).unwrap(); - writeln!(output, "{:>20} {}", prefix, route_attrs).unwrap(); - } - - if let Err(error) = page_output(session, &output) { - println!("% failed to print data: {}", error) + writeln!(output, "{:>20} {}", prefix, route_attrs)?; } Ok(false) @@ -2248,8 +2167,7 @@ pub fn cmd_show_bgp_neighbor_detail( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { - let mut output = String::new(); +) -> Result { let neighbor_addr = get_opt_arg(&mut args, "neighbor"); let xpath_bgp_instance = format!( @@ -2269,6 +2187,7 @@ pub fn cmd_show_bgp_neighbor_detail( &xpath_bgp_instance, )?; + let output = session.writer(); for dnode_inst in data.find_xpath(&xpath_bgp_instance).unwrap() { let local_as = dnode_inst.relative_value("ietf-bgp:bgp/global/as"); let local_rid = @@ -2469,10 +2388,6 @@ pub fn cmd_show_bgp_neighbor_detail( } } - if let Err(error) = page_output(session, &output) { - println!("% failed to print data: {}", error) - } - Ok(false) } @@ -2481,7 +2396,7 @@ pub fn cmd_clear_isis_adjacency( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { let yang_ctx = YANG_CTX.get().unwrap(); let data = r#"{"ietf-isis:clear-adjacency": {}}"#; let data = DataTree::parse_op_string( @@ -2503,7 +2418,7 @@ pub fn cmd_clear_isis_database( _commands: &Commands, session: &mut Session, _args: ParsedArgs, -) -> Result { +) -> Result { let yang_ctx = YANG_CTX.get().unwrap(); let data = r#"{"ietf-isis:clear-database": {}}"#; let data = DataTree::parse_op_string( @@ -2528,7 +2443,7 @@ pub fn cmd_clear_bgp_neighbor( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let neighbor = get_opt_arg(&mut args, "neighbor"); let clear_type = get_opt_arg(&mut args, "type"); let yang_ctx = YANG_CTX.get().unwrap(); @@ -2599,7 +2514,7 @@ pub fn cmd_show_route( _commands: &Commands, session: &mut Session, mut args: ParsedArgs, -) -> Result { +) -> Result { let rib_name = get_opt_arg(&mut args, "afi").unwrap_or("ipv4".to_owned()); let fetch_xpath = format!("{}[name='{}']", XPATH_RIB, rib_name); let route_xpath = format!("{}/routes/route", fetch_xpath); @@ -2611,7 +2526,7 @@ pub fn cmd_show_route( return Ok(false); }; - let mut output = String::new(); + let output = session.writer(); for route in dnode.find_xpath(&route_xpath).unwrap() { let prefix = route.child_value("destination-prefix"); @@ -2656,10 +2571,5 @@ pub fn cmd_show_route( } } - if !output.is_empty() { - page_output(session, &output) - .map_err(|e| format!("% failed to display data: {}", e))?; - } - Ok(false) } diff --git a/src/internal_commands.xml b/src/internal_commands.xml index 27c2800..7b58935 100644 --- a/src/internal_commands.xml +++ b/src/internal_commands.xml @@ -4,8 +4,8 @@ - - + + @@ -216,7 +216,7 @@ - + diff --git a/src/main.rs b/src/main.rs index 635143e..d07b4fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod error; mod grpc; mod internal_commands; mod parser; +mod pipe; mod session; mod terminal; mod token; @@ -20,11 +21,11 @@ use clap::{App, Arg}; use reedline::Signal; use yang4::context::{Context, ContextFlags}; -use crate::error::Error; +use crate::error::{CallbackError, Error}; use crate::grpc::GrpcClient; use crate::session::{CommandMode, Session}; use crate::terminal::CliPrompt; -use crate::token::{Action, Commands}; +use crate::token::{Action, Commands, is_pipeable}; // Global YANG context. pub static YANG_CTX: OnceLock> = OnceLock::new(); @@ -58,28 +59,87 @@ impl Cli { None => return Ok(false), }; - // Parse command. + // Split on pipe characters. + let (base_line, pipe_segments) = pipe::split_on_pipes(&line); + + // Parse pipe commands (validated early for fast error + // reporting). + let parsed_pipes = self + .commands + .pipe_registry + .parse_pipes(&pipe_segments) + .map_err(Error::Pipe)?; + + // Parse base command. let pcmd = - parser::parse_command(&mut self.session, &self.commands, &line) + parser::parse_command(&mut self.session, &self.commands, base_line) .map_err(Error::Parser)?; let token = self.commands.get_token(pcmd.token_id); let negate = pcmd.negate; let args = pcmd.args; + // Validate pipes are allowed for this command. + if !parsed_pipes.is_empty() + && !is_pipeable(&self.commands, pcmd.token_id) + { + return Err(Error::Pipe(pipe::PipeError::NotAllowed)); + } + // Process command. let mut exit = false; if let Some(action) = &token.action { match action { Action::ConfigEdit(snode) => { - // Edit configuration & update CLI node if necessary. + // Edit configuration & update CLI node if + // necessary. self.session .edit_candidate(negate, snode, args) .map_err(Error::EditConfig)?; } Action::Callback(callback) => { + // Set up output: pipe chain (with + // optional pager), pager only, or stdout. + let pipe_chain = match pipe::PipeChain::spawn( + &self.commands.pipe_registry, + &parsed_pipes, + self.session.use_pager(), + ) { + Ok(mut chain) => { + let writer = chain.take_writer(); + self.session.set_writer(writer); + Some(chain) + } + Err(e) if parsed_pipes.is_empty() => { + // Pager-only failure — fall back to + // stdout. + eprintln!("% {}", e); + None + } + Err(e) => return Err(Error::Pipe(e)), + }; + // Execute callback. - exit = (callback)(&self.commands, &mut self.session, args) - .map_err(Error::Callback)?; + let result = + (callback)(&self.commands, &mut self.session, args); + + // Clean up: reset writer and finish pipe + // chain / pager. + self.session.set_writer(None); + if let Some(chain) = pipe_chain { + let _ = chain.finish(); + } + + // Handle callback result, treating + // BrokenPipe as non-fatal. + match result { + Ok(should_exit) => exit = should_exit, + Err(CallbackError::BrokenPipe) => { + // Pipe closed early — not an error. + } + Err(e) => { + return Err(Error::Callback(e)); + } + } } } } diff --git a/src/pipe.rs b/src/pipe.rs new file mode 100644 index 0000000..9b96a2c --- /dev/null +++ b/src/pipe.rs @@ -0,0 +1,517 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// + +use std::fmt; +use std::io::{BufRead, BufReader, BufWriter, Read, Write}; +use std::process::{Child, Command, Stdio}; +use std::thread::JoinHandle; + +// ===== type aliases ===== + +type BuiltinFn = fn( + args: &[String], + reader: Box, + writer: Box, +) -> Result<(), String>; + +// ===== data types ===== + +pub enum PipeAction { + External { binary: &'static str }, + Builtin(BuiltinFn), +} + +pub struct PipeCommand { + pub name: &'static str, + pub help: &'static str, + pub args: &'static [&'static str], + pub action: PipeAction, +} + +pub struct ParsedPipe { + pub command_idx: usize, + pub args: Vec, +} + +pub struct PipeRegistry { + commands: Vec, +} + +#[derive(Debug)] +pub enum PipeError { + NotFound(String), + Ambiguous(String, Vec), + WrongArgCount { + command: String, + expected: usize, + got: usize, + }, + NotAllowed, + Io(std::io::Error), + Spawn { + command: String, + source: std::io::Error, + }, + ThreadPanicked, + Filter(String), +} + +enum PipeStage { + Thread(JoinHandle>), + Process(Child), +} + +pub struct PipeChain { + writer: Option>, + stages: Vec, + pager: Option, +} + +// ===== impl PipeRegistry ===== + +impl PipeRegistry { + pub fn new() -> Self { + Self { + commands: Vec::new(), + } + } + + pub fn builtin( + mut self, + name: &'static str, + help: &'static str, + args: &'static [&'static str], + func: BuiltinFn, + ) -> Self { + self.commands.push(PipeCommand { + name, + help, + args, + action: PipeAction::Builtin(func), + }); + self + } + + pub fn external( + mut self, + name: &'static str, + help: &'static str, + binary: &'static str, + args: &'static [&'static str], + ) -> Self { + self.commands.push(PipeCommand { + name, + help, + args, + action: PipeAction::External { binary }, + }); + self + } + + pub fn build(self) -> Self { + self + } + + pub fn commands(&self) -> &[PipeCommand] { + &self.commands + } + + pub fn find(&self, name: &str) -> Result { + let matches: Vec = self + .commands + .iter() + .enumerate() + .filter(|(_, cmd)| cmd.name.starts_with(name)) + .map(|(i, _)| i) + .collect(); + + match matches.len() { + 0 => Err(PipeError::NotFound(name.to_owned())), + 1 => Ok(matches[0]), + _ => { + // Check for exact match. + for &idx in &matches { + if self.commands[idx].name == name { + return Ok(idx); + } + } + let names = matches + .iter() + .map(|&i| self.commands[i].name.to_owned()) + .collect(); + Err(PipeError::Ambiguous(name.to_owned(), names)) + } + } + } + + pub fn parse_pipe(&self, segment: &str) -> Result { + let mut words = split_words(segment); + let name = if words.is_empty() { + String::new() + } else { + words.remove(0) + }; + let idx = self.find(&name)?; + let args = words; + let cmd = &self.commands[idx]; + let expected = cmd.args.len(); + if matches!(cmd.action, PipeAction::External { .. }) { + // External: args specifies minimum required arguments. + if args.len() < expected { + return Err(PipeError::WrongArgCount { + command: cmd.name.to_owned(), + expected, + got: args.len(), + }); + } + } else if args.len() != expected { + return Err(PipeError::WrongArgCount { + command: cmd.name.to_owned(), + expected, + got: args.len(), + }); + } + Ok(ParsedPipe { + command_idx: idx, + args, + }) + } + + pub fn parse_pipes( + &self, + segments: &[&str], + ) -> Result, PipeError> { + segments.iter().map(|seg| self.parse_pipe(seg)).collect() + } +} + +// ===== impl PipeError ===== + +impl fmt::Display for PipeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PipeError::NotFound(name) => { + write!(f, "unknown pipe command: '{}'", name) + } + PipeError::Ambiguous(name, matches) => { + write!( + f, + "ambiguous pipe command '{}': {}", + name, + matches.join(", ") + ) + } + PipeError::WrongArgCount { + command, + expected, + got, + } => { + write!( + f, + "pipe command '{}' expects {} argument(s), \ + got {}", + command, expected, got + ) + } + PipeError::NotAllowed => { + write!(f, "pipes are not supported for this command") + } + PipeError::Io(e) => { + write!(f, "pipe I/O error: {}", e) + } + PipeError::Spawn { command, source } => { + write!(f, "failed to spawn '{}': {}", command, source) + } + PipeError::ThreadPanicked => { + write!(f, "pipe thread panicked") + } + PipeError::Filter(e) => { + write!(f, "pipe filter error: {}", e) + } + } + } +} + +// ===== builtin filters ===== + +pub fn filter_include( + args: &[String], + reader: Box, + writer: Box, +) -> Result<(), String> { + let pattern = &args[0]; + let reader = BufReader::new(reader); + let mut writer = BufWriter::new(writer); + for line in reader.lines() { + let line = line.map_err(|e| e.to_string())?; + if line.contains(pattern.as_str()) { + writeln!(writer, "{}", line).map_err(|e| e.to_string())?; + } + } + Ok(()) +} + +pub fn filter_exclude( + args: &[String], + reader: Box, + writer: Box, +) -> Result<(), String> { + let pattern = &args[0]; + let reader = BufReader::new(reader); + let mut writer = BufWriter::new(writer); + for line in reader.lines() { + let line = line.map_err(|e| e.to_string())?; + if !line.contains(pattern.as_str()) { + writeln!(writer, "{}", line).map_err(|e| e.to_string())?; + } + } + Ok(()) +} + +pub fn filter_count( + _args: &[String], + reader: Box, + writer: Box, +) -> Result<(), String> { + let reader = BufReader::new(reader); + let count = reader.lines().count(); + let mut writer = BufWriter::new(writer); + writeln!(writer, "{}", count).map_err(|e| e.to_string())?; + Ok(()) +} + +pub fn filter_no_more( + _args: &[String], + reader: Box, + mut writer: Box, +) -> Result<(), String> { + let mut reader = reader; + std::io::copy(&mut reader, &mut writer).map_err(|e| e.to_string())?; + Ok(()) +} + +// ===== pager ===== + +fn spawn_pager() -> Result { + Command::new("less") + .arg("-F") + .arg("-X") + .stdin(Stdio::piped()) + .spawn() +} + +// ===== default registry ===== + +pub fn default_registry() -> PipeRegistry { + PipeRegistry::new() + .builtin( + "include", + "Filter lines matching pattern", + &["pattern"], + filter_include, + ) + .builtin( + "exclude", + "Remove lines matching pattern", + &["pattern"], + filter_exclude, + ) + .builtin("count", "Count output lines", &[], filter_count) + .builtin("no-more", "Disable pager", &[], filter_no_more) + .external("grep", "Filter lines using grep", "grep", &["PATTERN"]) + .build() +} + +// ===== pipe chain ===== + +/// Output sink that can be converted to either `Stdio` (for external +/// processes) or `Box` (for builtin threads). +enum ChainOutput { + ChildStdin(std::process::ChildStdin), + PipeWriter(std::io::PipeWriter), + Terminal, +} + +impl ChainOutput { + fn into_stdio(self) -> Stdio { + match self { + ChainOutput::ChildStdin(s) => Stdio::from(s), + ChainOutput::PipeWriter(w) => Stdio::from(w), + ChainOutput::Terminal => Stdio::inherit(), + } + } + + fn into_writer(self) -> Box { + match self { + ChainOutput::ChildStdin(s) => Box::new(s), + ChainOutput::PipeWriter(w) => Box::new(w), + ChainOutput::Terminal => Box::new(std::io::stdout()), + } + } +} + +impl PipeChain { + pub fn spawn( + registry: &PipeRegistry, + pipes: &[ParsedPipe], + use_pager: bool, + ) -> Result { + let has_no_more = pipes + .iter() + .any(|p| registry.commands()[p.command_idx].name == "no-more"); + let should_page = use_pager && !has_no_more; + + let mut stages: Vec = Vec::new(); + + // Determine the final output destination. + let (mut next_output, pager) = if should_page { + let mut pager = spawn_pager().map_err(|e| PipeError::Spawn { + command: "less".to_owned(), + source: e, + })?; + let stdin = pager.stdin.take().ok_or_else(|| { + PipeError::Io(std::io::Error::other("pager has no stdin")) + })?; + (ChainOutput::ChildStdin(stdin), Some(pager)) + } else { + (ChainOutput::Terminal, None) + }; + + // Build the chain backwards (last pipe first). + for parsed in pipes.iter().rev() { + let cmd = ®istry.commands()[parsed.command_idx]; + + // Skip no-more — it's handled by the pager logic. + if cmd.name == "no-more" { + continue; + } + + match &cmd.action { + PipeAction::External { binary } => { + let mut child = Command::new(binary) + .args(&parsed.args) + .stdin(Stdio::piped()) + .stdout(next_output.into_stdio()) + .spawn() + .map_err(|e| PipeError::Spawn { + command: binary.to_string(), + source: e, + })?; + let child_stdin = child.stdin.take().ok_or_else(|| { + PipeError::Io(std::io::Error::other(format!( + "'{}' process has no stdin", + binary + ))) + })?; + next_output = ChainOutput::ChildStdin(child_stdin); + stages.push(PipeStage::Process(child)); + } + PipeAction::Builtin(func) => { + let (pipe_reader, pipe_writer) = + std::io::pipe().map_err(PipeError::Io)?; + let func = *func; + let args = parsed.args.clone(); + let writer_out = next_output.into_writer(); + let handle = std::thread::spawn(move || { + func(&args, Box::new(pipe_reader), writer_out) + }); + next_output = ChainOutput::PipeWriter(pipe_writer); + stages.push(PipeStage::Thread(handle)); + } + } + } + + Ok(PipeChain { + writer: Some(next_output.into_writer()), + stages, + pager, + }) + } + + pub fn take_writer(&mut self) -> Option> { + self.writer.take() + } + + pub fn finish(mut self) -> Result<(), PipeError> { + // Drop writer to signal EOF to the first pipe stage. + drop(self.writer.take()); + + // Wait for all stages (in reverse order — first spawned + // last). + for stage in self.stages.drain(..).rev() { + match stage { + PipeStage::Thread(handle) => match handle.join() { + Ok(result) => result.map_err(PipeError::Filter)?, + Err(_) => return Err(PipeError::ThreadPanicked), + }, + PipeStage::Process(mut child) => { + child.wait().map_err(PipeError::Io)?; + } + } + } + + // Wait for pager if present. + if let Some(mut pager) = self.pager.take() { + pager.wait().map_err(PipeError::Io)?; + } + + Ok(()) + } +} + +// ===== helper functions ===== + +/// Split a string on a delimiter, respecting double-quoted segments. +/// Quotes are preserved in the output slices. +fn split_unquoted(s: &str, delim: char) -> Vec<&str> { + let mut parts = Vec::new(); + let mut start = 0; + let mut in_quote = false; + for (i, c) in s.char_indices() { + if c == '"' { + in_quote = !in_quote; + } else if c == delim && !in_quote { + parts.push(&s[start..i]); + start = i + c.len_utf8(); + } + } + parts.push(&s[start..]); + parts +} + +/// Split words on whitespace, respecting double-quoted segments. +/// Quotes are stripped from the returned strings. +fn split_words(s: &str) -> Vec { + let mut words = Vec::new(); + let mut current = String::new(); + let mut in_quote = false; + for c in s.chars() { + if c == '"' { + in_quote = !in_quote; + } else if c.is_whitespace() && !in_quote { + if !current.is_empty() { + words.push(std::mem::take(&mut current)); + } + } else { + current.push(c); + } + } + if !current.is_empty() { + words.push(current); + } + words +} + +pub fn split_on_pipes(line: &str) -> (&str, Vec<&str>) { + let parts = split_unquoted(line, '|'); + let base = parts[0].trim(); + if parts.len() > 1 { + let pipes: Vec<&str> = parts[1..].iter().map(|s| s.trim()).collect(); + (base, pipes) + } else { + (base, vec![]) + } +} diff --git a/src/session.rs b/src/session.rs index 809445a..ba36591 100644 --- a/src/session.rs +++ b/src/session.rs @@ -20,7 +20,6 @@ use crate::{YANG_CTX, token_yang}; static DEFAULT_HOSTNAME: &str = "holo"; -#[derive(Debug)] pub struct Session { hostname: String, prompt: String, @@ -29,6 +28,7 @@ pub struct Session { running: DataTree<'static>, candidate: Option>, grpc_client: GrpcClient, + writer: Box, } #[derive(Clone, Debug, Eq, PartialEq, EnumAsInner)] @@ -81,6 +81,7 @@ impl Session { running, candidate: None, grpc_client, + writer: Box::new(std::io::stdout()), } } @@ -102,6 +103,14 @@ impl Session { self.use_pager } + pub fn set_writer(&mut self, w: Option>) { + self.writer = w.unwrap_or_else(|| Box::new(std::io::stdout())); + } + + pub fn writer(&mut self) -> &mut dyn std::io::Write { + self.writer.as_mut() + } + fn update_prompt(&mut self) { self.prompt = match &self.mode { CommandMode::Operational => self.hostname.clone(), diff --git a/src/terminal.rs b/src/terminal.rs index 875e576..561433e 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -19,7 +19,8 @@ use reedline::{ use crate::Cli; use crate::error::ParserError; use crate::parser::{self, ParsedCommand}; -use crate::token::{Commands, TokenKind}; +use crate::pipe::PipeRegistry; +use crate::token::{Commands, TokenKind, is_pipeable}; static DEFAULT_PROMPT_INDICATOR: &str = "# "; static DEFAULT_MULTILINE_INDICATOR: &str = "::: "; @@ -85,6 +86,32 @@ impl Completer for CliCompleter { fn complete(&mut self, line: &str, pos: usize) -> Vec { let cli = self.0.lock().unwrap(); + // Check if we're completing after a pipe character. + let line_to_pos = &line[..pos]; + if let Some(pipe_pos) = line_to_pos.rfind('|') { + // Parse the base command (before the first pipe) to + // check if it supports pipes. + let base_cmd = line_to_pos.split('|').next().unwrap_or("").trim(); + let wd = cli.session.mode().token(&cli.commands); + let pipeable = match parser::parse_command_try( + &cli.session, + &cli.commands, + wd, + base_cmd, + ) { + Ok(parsed) => is_pipeable(&cli.commands, parsed.token_id), + Err(ParserError::Incomplete(tid)) => { + is_pipeable(&cli.commands, tid) + } + _ => false, + }; + if !pipeable { + return vec![]; + } + let after_pipe = line_to_pos[pipe_pos + 1..].trim_start(); + return complete_pipe(&cli.commands.pipe_registry, after_pipe, pos); + } + let last_word = line.split_whitespace().last().unwrap_or(line); let partial = line .chars() @@ -192,6 +219,78 @@ pub fn reedline_init( .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) } +fn complete_pipe( + registry: &PipeRegistry, + after_pipe: &str, + pos: usize, +) -> Vec { + let words: Vec<&str> = after_pipe.split_whitespace().collect(); + let first_word = words.first().copied().unwrap_or(""); + + // Check if cursor is at a partial word or after whitespace. + let partial = after_pipe + .chars() + .last() + .map(|c| !c.is_whitespace()) + .unwrap_or(false); + + let exact_match = + registry.commands().iter().any(|cmd| cmd.name == first_word); + + if exact_match && (words.len() > 1 || !partial) { + // Command is fully entered — show arg hints if needed. + if let Ok(idx) = registry.find(first_word) { + let cmd = ®istry.commands()[idx]; + // Count args already provided (excluding the command + // word and any partial word being typed). + let provided = if partial { + words.len() - 2 + } else { + words.len() - 1 + }; + // Show remaining arg hints if not typing a value. + if !partial && provided < cmd.args.len() { + return cmd.args[provided..] + .iter() + .map(|arg| Suggestion { + value: arg.to_uppercase(), + description: Some(cmd.help.to_owned()), + extra: None, + span: Span { + start: pos, + end: pos, + }, + append_whitespace: true, + style: None, + }) + .collect(); + } + } + return vec![]; + } + + // Complete pipe command names. + registry + .commands() + .iter() + .filter(|cmd| first_word.is_empty() || cmd.name.starts_with(first_word)) + .map(|cmd| { + let span_start = if partial { pos - first_word.len() } else { pos }; + Suggestion { + value: cmd.name.to_owned(), + description: Some(cmd.help.to_owned()), + extra: None, + span: Span { + start: span_start, + end: pos, + }, + append_whitespace: true, + style: None, + } + }) + .collect() +} + fn complete_add_token( commands: &Commands, token_id: NodeId, diff --git a/src/token.rs b/src/token.rs index 46f53a1..dd7d21f 100644 --- a/src/token.rs +++ b/src/token.rs @@ -7,7 +7,9 @@ use indextree::{Arena, NodeId}; use yang4::schema::SchemaNode; +use crate::error::CallbackError; use crate::parser::ParsedArgs; +use crate::pipe::PipeRegistry; use crate::session::Session; use crate::{token_xml, token_yang}; @@ -17,6 +19,7 @@ pub struct Commands { pub config_root_yang: NodeId, pub config_root_internal: NodeId, pub config_dflt_internal: NodeId, + pub pipe_registry: PipeRegistry, } pub struct Token { @@ -26,6 +29,7 @@ pub struct Token { pub argument: Option, pub action: Option, pub node_update: bool, + pub pipeable: bool, } #[derive(Debug, Eq, PartialEq)] @@ -43,7 +47,7 @@ type Callback = fn( commands: &Commands, session: &mut Session, args: ParsedArgs, -) -> Result; +) -> Result; // ===== impl Commands ===== @@ -61,6 +65,7 @@ impl Commands { config_root_yang, config_root_internal, config_dflt_internal, + pipe_registry: crate::pipe::default_registry(), } } @@ -94,6 +99,7 @@ impl Token { argument: Option, action: Option, node_update: bool, + pipeable: bool, ) -> Token { Token { name: name.into(), @@ -102,6 +108,7 @@ impl Token { argument: argument.map(|s| s.into()), action, node_update, + pipeable, } } @@ -116,3 +123,11 @@ impl Token { } } } + +/// Returns `true` if the token or any of its ancestors has `pipeable = true`. +pub fn is_pipeable(commands: &Commands, token_id: NodeId) -> bool { + std::iter::once(token_id) + .chain(token_id.ancestors(&commands.arena)) + .filter_map(|id| commands.get_opt_token(id)) + .any(|t| t.pipeable) +} diff --git a/src/token_xml.rs b/src/token_xml.rs index ca1dd97..09347ca 100644 --- a/src/token_xml.rs +++ b/src/token_xml.rs @@ -33,11 +33,11 @@ pub fn gen_cmds(commands: &mut Commands) { _ => continue, }; - // Update stack of tokens. + // Update stacks. stack.push(token_id); } Ok(XmlEvent::EndElement { .. }) => { - // Update stack of tokens. + // Update stacks. stack.pop(); } Ok(_) => (), @@ -69,6 +69,7 @@ fn parse_tag_token( let kind = find_opt_attribute(&attributes, "kind"); let argument = find_opt_attribute(&attributes, "argument"); let cmd_name = find_opt_attribute(&attributes, "cmd"); + let pipeable = find_opt_attribute(&attributes, "pipe") == Some("true"); let callback = cmd_name.map(|name| match name { "cmd_config" => internal_commands::cmd_config, "cmd_list" => internal_commands::cmd_list, @@ -156,7 +157,7 @@ fn parse_tag_token( let action = callback.map(|callback| Action::Callback(callback)); // Add new token. - let token = Token::new(name, help, kind, argument, action, false); + let token = Token::new(name, help, kind, argument, action, false, pipeable); // Link new token. commands.add_token(parent, token) diff --git a/src/token_yang.rs b/src/token_yang.rs index 61b7f46..2a6a211 100644 --- a/src/token_yang.rs +++ b/src/token_yang.rs @@ -94,7 +94,8 @@ fn add_token( .then(|| Action::ConfigEdit(snode.clone())); let node_update = snode.kind() == SchemaNodeKind::List; - let token = Token::new(name, help, kind, argument, action, node_update); + let token = + Token::new(name, help, kind, argument, action, node_update, false); *token_id = commands.add_token(*token_id, token); snode_set_token_id(snode, *token_id); }