diff --git a/zebrad/src/application.rs b/zebrad/src/application.rs index 2a2c0936..de46dc73 100644 --- a/zebrad/src/application.rs +++ b/zebrad/src/application.rs @@ -79,6 +79,7 @@ impl Application for ZebradApp { &mut self.state } + /// Returns the framework components used by this application. fn framework_components( &mut self, command: &Self::Cmd, @@ -135,10 +136,29 @@ impl Application for ZebradApp { impl ZebradApp { fn level(&self, command: &EntryPoint) -> String { - if let Ok(level) = std::env::var("ZEBRAD_LOG") { - level - } else if command.verbose { + // `None` outputs zebrad usage information to stdout + let command_uses_stdout = match &command.command { + None => true, + Some(c) => c.uses_stdout(), + }; + + // Allow users to: + // - override all other configs and defaults using the command line + // - see command outputs without spurious log messages, by default + // - override the config file using an environmental variable + if command.verbose { "debug".to_string() + } else if command_uses_stdout { + // Tracing sends output to stdout, so we disable info-level logs for + // some commands. + // + // TODO: send tracing output to stderr. This change requires an abscissa + // update, because `abscissa_core::component::Tracing` uses + // `tracing_subscriber::fmt::Formatter`, which has `Stdout` as a + // type parameter. We need `MakeWriter` or a similar type. + "warn".to_string() + } else if let Ok(level) = std::env::var("ZEBRAD_LOG") { + level } else if let Some(ZebradConfig { tracing: crate::config::TracingSection { diff --git a/zebrad/src/commands.rs b/zebrad/src/commands.rs index 6310478e..dbe4e0a8 100644 --- a/zebrad/src/commands.rs +++ b/zebrad/src/commands.rs @@ -52,6 +52,24 @@ pub enum ZebradCmd { Version(VersionCmd), } +impl ZebradCmd { + /// Returns true if this command sends output to stdout. + /// + /// For example, `Generate` sends a default config file to stdout. + /// + /// Usage note: `abscissa_core::EntryPoint` stores an `Option`. + /// If the command is `None`, then abscissa writes zebrad usage information + /// to stdout. + pub(crate) fn uses_stdout(&self) -> bool { + use ZebradCmd::*; + match self { + // List all the commands, so new commands have to make a choice here + Generate(_) | Help(_) | Revhex(_) | Version(_) => true, + Connect(_) | Seed(_) | Start(_) => false, + } + } +} + /// This trait allows you to define how application configuration is loaded. impl Configurable for ZebradCmd { /// Location of the configuration file diff --git a/zebrad/src/commands/revhex.rs b/zebrad/src/commands/revhex.rs index 1e925865..419e0829 100644 --- a/zebrad/src/commands/revhex.rs +++ b/zebrad/src/commands/revhex.rs @@ -3,31 +3,44 @@ #![allow(clippy::never_loop)] use abscissa_core::{Command, Options, Runnable}; +use std::io::stdin; /// `revhex` subcommand #[derive(Command, Debug, Default, Options)] pub struct RevhexCmd { /// The hex string whose endianness will be reversed. + /// + /// When input is "-" or empty, reads lines from standard input, and + /// reverses each line. #[options(free)] input: String, } +/// Returns the hexadecimal-encoded string `s` in byte-reversed order. +fn byte_reverse_hex(s: &str) -> String { + String::from_utf8( + s.as_bytes() + .chunks(2) + .rev() + .map(|c| c.iter()) + .flatten() + .cloned() + .collect::>(), + ) + .expect("input should be ascii") +} + impl Runnable for RevhexCmd { /// Print endian-reversed hex string. fn run(&self) { - println!( - "{}", - String::from_utf8( - self.input - .as_bytes() - .chunks(2) - .rev() - .map(|c| c.iter()) - .flatten() - .cloned() - .collect::>(), - ) - .expect("input should be ascii") - ); + if self.input.is_empty() || self.input == "-" { + // "-" is a typical command-line argument for "read standard input" + let mut input = String::new(); + while stdin().read_line(&mut input).is_ok() { + println!("{}", byte_reverse_hex(&input.trim())); + } + } else { + println!("{}", byte_reverse_hex(&self.input)); + } } }