cli: Initial yubikey-cli utility with list command
Adds a `yubikey-cli` crate to the workspace, with a `yubikey` binary, which presently provides a `list` command for listing detected readers. Dependencies: - `env_logger`: logging - `gumdrop`: argument parsing - `termcolor`: colored terminal output As this repo now contains a binary, it also checks in `Cargo.lock`.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
//! `yubikey` command-line utility
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
missing_docs,
|
||||
rust_2018_idioms,
|
||||
unused_lifetimes,
|
||||
unused_qualifications
|
||||
)]
|
||||
|
||||
use gumdrop::Options;
|
||||
use yubikey_cli::commands::YubikeyCli;
|
||||
|
||||
fn main() {
|
||||
YubikeyCli::parse_args_default_or_exit().run();
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Commands of the CLI application
|
||||
|
||||
pub mod list;
|
||||
|
||||
use self::list::ListCmd;
|
||||
use crate::status;
|
||||
use gumdrop::Options;
|
||||
use std::env;
|
||||
use std::process::exit;
|
||||
use termcolor::ColorChoice;
|
||||
|
||||
/// The `yubikey` CLI utility
|
||||
#[derive(Debug, Options)]
|
||||
pub struct YubikeyCli {
|
||||
/// Obtain help about the current command
|
||||
#[options(short = "h", help = "print help message")]
|
||||
pub help: bool,
|
||||
|
||||
/// Subcommand to execute.
|
||||
#[options(command)]
|
||||
pub command: Option<Commands>,
|
||||
}
|
||||
|
||||
impl YubikeyCli {
|
||||
/// Run the underlying command type or print usage info and exit
|
||||
pub fn run(&self) {
|
||||
// TODO(tarcieri): make this more configurable
|
||||
status::set_color_choice(ColorChoice::Auto);
|
||||
|
||||
// Only show logs if `RUST_LOG` is set
|
||||
if env::var("RUST_LOG").is_ok() {
|
||||
env_logger::builder().format_timestamp(None).init();
|
||||
}
|
||||
|
||||
match &self.command {
|
||||
Some(cmd) => cmd.run(),
|
||||
None => println!("{}", Commands::usage()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subcommands of this application
|
||||
#[derive(Debug, Options)]
|
||||
pub enum Commands {
|
||||
/// `help` subcommand
|
||||
#[options(help = "show help for a command")]
|
||||
Help(HelpOpts),
|
||||
|
||||
/// `version` subcommand
|
||||
#[options(help = "display version information")]
|
||||
Version(VersionOpts),
|
||||
|
||||
/// `list` subcommand
|
||||
#[options(help = "list detected readers")]
|
||||
List(ListCmd),
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
/// Run the given command
|
||||
pub fn run(&self) {
|
||||
match self {
|
||||
Commands::Help(help) => help.run(),
|
||||
Commands::Version(version) => version.run(),
|
||||
Commands::List(list) => list.run(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Help options
|
||||
#[derive(Debug, Options)]
|
||||
pub struct HelpOpts {
|
||||
#[options(free, help = "subcommand to get help for")]
|
||||
free: Vec<String>,
|
||||
}
|
||||
|
||||
impl HelpOpts {
|
||||
fn run(&self) {
|
||||
if let Some(command) = self.free.first() {
|
||||
if let Some(usage) = Commands::command_usage(command) {
|
||||
println!("{}", usage);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", Commands::usage());
|
||||
}
|
||||
}
|
||||
|
||||
/// Version options
|
||||
#[derive(Debug, Options)]
|
||||
pub struct VersionOpts {}
|
||||
|
||||
impl VersionOpts {
|
||||
/// Display version information
|
||||
pub fn run(&self) {
|
||||
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! List detected readers
|
||||
|
||||
use gumdrop::Options;
|
||||
use std::process::exit;
|
||||
use yubikey_piv::readers::Readers;
|
||||
|
||||
/// The `list` subcommand
|
||||
#[derive(Debug, Options)]
|
||||
pub struct ListCmd {}
|
||||
|
||||
impl ListCmd {
|
||||
/// Run the `list` subcommand
|
||||
pub fn run(&self) {
|
||||
let mut readers = Readers::open().unwrap_or_else(|e| {
|
||||
status_err!("couldn't open PC/SC context: {}", e);
|
||||
exit(1);
|
||||
});
|
||||
|
||||
let readers_iter = readers.iter().unwrap_or_else(|e| {
|
||||
status_err!("couldn't enumerate PC/SC readers: {}", e);
|
||||
exit(1);
|
||||
});
|
||||
|
||||
if readers_iter.len() == 0 {
|
||||
status_err!("no YubiKeys detected!");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
for (i, reader) in readers_iter.enumerate() {
|
||||
let name = reader.name();
|
||||
let mut yubikey = match reader.open() {
|
||||
Ok(yk) => yk,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let serial = yubikey.serial();
|
||||
println!("{}: {} (serial: {})", i + 1, name, serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! `yubikey` command-line utility
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
missing_docs,
|
||||
rust_2018_idioms,
|
||||
unused_lifetimes,
|
||||
unused_qualifications
|
||||
)]
|
||||
|
||||
#[macro_use]
|
||||
pub mod status;
|
||||
|
||||
pub mod commands;
|
||||
@@ -0,0 +1,165 @@
|
||||
//! Status messages
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::io::{self, Write};
|
||||
use std::sync::Mutex;
|
||||
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
|
||||
|
||||
/// Print a success status message (in green if colors are enabled)
|
||||
#[macro_export]
|
||||
macro_rules! status_ok {
|
||||
($status:expr, $msg:expr) => {
|
||||
$crate::status::Status::new()
|
||||
.justified()
|
||||
.bold()
|
||||
.color(termcolor::Color::Green)
|
||||
.status($status)
|
||||
.print_stdout($msg);
|
||||
};
|
||||
($status:expr, $fmt:expr, $($arg:tt)+) => {
|
||||
$crate::status_ok!($status, format!($fmt, $($arg)+));
|
||||
};
|
||||
}
|
||||
|
||||
/// Print a warning status message (in yellow if colors are enabled)
|
||||
#[macro_export]
|
||||
macro_rules! status_warn {
|
||||
($msg:expr) => {
|
||||
$crate::status::Status::new()
|
||||
.bold()
|
||||
.color(termcolor::Color::Yellow)
|
||||
.status("warning:")
|
||||
.print_stdout($msg);
|
||||
};
|
||||
($fmt:expr, $($arg:tt)+) => {
|
||||
$crate::status_warn!(format!($fmt, $($arg)+));
|
||||
};
|
||||
}
|
||||
|
||||
/// Print an error message (in red if colors are enabled)
|
||||
#[macro_export]
|
||||
macro_rules! status_err {
|
||||
($msg:expr) => {
|
||||
$crate::status::Status::new()
|
||||
.bold()
|
||||
.color(termcolor::Color::Red)
|
||||
.status("error:")
|
||||
.print_stderr($msg);
|
||||
};
|
||||
($fmt:expr, $($arg:tt)+) => {
|
||||
$crate::status_err!(format!($fmt, $($arg)+));
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// Color configuration
|
||||
static ref COLOR_CHOICE: Mutex<Option<ColorChoice>> = Mutex::new(None);
|
||||
|
||||
/// Standard output
|
||||
pub static ref STDOUT: StandardStream = StandardStream::stdout(get_color_choice());
|
||||
|
||||
/// Standard error
|
||||
pub static ref STDERR: StandardStream = StandardStream::stderr(get_color_choice());
|
||||
}
|
||||
|
||||
/// Obtain the color configuration.
|
||||
///
|
||||
/// Panics if no configuration has been provided.
|
||||
fn get_color_choice() -> ColorChoice {
|
||||
let choice = COLOR_CHOICE.lock().unwrap();
|
||||
*choice
|
||||
.as_ref()
|
||||
.expect("terminal stream accessed before initialized!")
|
||||
}
|
||||
|
||||
/// Set the color configuration.
|
||||
///
|
||||
/// Panics if the terminal has already been configured.
|
||||
pub(super) fn set_color_choice(color_choice: ColorChoice) {
|
||||
let mut choice = COLOR_CHOICE.lock().unwrap();
|
||||
assert!(choice.is_none(), "terminal colors already configured!");
|
||||
*choice = Some(color_choice);
|
||||
}
|
||||
|
||||
/// Status message builder
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Status {
|
||||
/// Should the status be justified?
|
||||
justified: bool,
|
||||
|
||||
/// Should colors be bold?
|
||||
bold: bool,
|
||||
|
||||
/// Color in which status should be displayed
|
||||
color: Option<Color>,
|
||||
|
||||
/// Prefix of the status message (e.g. `Success`)
|
||||
status: Option<String>,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
/// Create a new status message with default settings
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Justify status on display
|
||||
pub fn justified(mut self) -> Self {
|
||||
self.justified = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Make colors bold
|
||||
pub fn bold(mut self) -> Self {
|
||||
self.bold = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the colors used to display this message
|
||||
pub fn color(mut self, c: Color) -> Self {
|
||||
self.color = Some(c);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a status message to display
|
||||
pub fn status<S>(mut self, msg: S) -> Self
|
||||
where
|
||||
S: ToString,
|
||||
{
|
||||
self.status = Some(msg.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Print the given message to stdout
|
||||
pub fn print_stdout(self, msg: impl AsRef<str>) {
|
||||
self.print(&*STDOUT, msg)
|
||||
.expect("error printing to stdout!")
|
||||
}
|
||||
|
||||
/// Print the given message to stderr
|
||||
pub fn print_stderr(self, msg: impl AsRef<str>) {
|
||||
self.print(&*STDERR, msg)
|
||||
.expect("error printing to stderr!")
|
||||
}
|
||||
|
||||
/// Print the given message
|
||||
fn print(self, stream: &StandardStream, msg: impl AsRef<str>) -> Result<(), io::Error> {
|
||||
let mut s = stream.lock();
|
||||
s.reset()?;
|
||||
s.set_color(ColorSpec::new().set_fg(self.color).set_bold(self.bold))?;
|
||||
|
||||
if let Some(status) = self.status {
|
||||
if self.justified {
|
||||
write!(s, "{:>12}", status)?;
|
||||
} else {
|
||||
write!(s, "{}", status)?;
|
||||
}
|
||||
}
|
||||
|
||||
s.reset()?;
|
||||
writeln!(s, " {}", msg.as_ref())?;
|
||||
s.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user