Skip to content

Commit

Permalink
Prototype out a help trait
Browse files Browse the repository at this point in the history
  • Loading branch information
erickt committed Feb 8, 2022
1 parent b2854ad commit 85bdb1d
Show file tree
Hide file tree
Showing 3 changed files with 495 additions and 4 deletions.
330 changes: 330 additions & 0 deletions argh/src/help.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
// Copyright (c) 2022 Google LLC All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//! TODO
use {
argh_shared::{write_description, CommandInfo, INDENT},
std::fmt,
};

const SECTION_SEPARATOR: &str = "\n\n";

const HELP_FLAG: HelpFlagInfo = HelpFlagInfo {
short: None,
long: "--help",
description: "display usage information",
optionality: HelpOptionality::Optional,
kind: HelpFieldKind::Switch,
};

/// TODO
pub trait Help {
/// TODO
const HELP_INFO: HelpInfo<'static>;
}

/// TODO
pub trait HelpSubCommands {
/// TODO
const HELP_INFO: HelpSubCommandsInfo<'static>;
}

/// TODO
pub trait HelpSubCommand {
/// TODO
const HELP_INFO: HelpSubCommandInfo<'static>;
}

impl<T: HelpSubCommand> HelpSubCommands for T {
/// TODO
const HELP_INFO: HelpSubCommandsInfo<'static> =
HelpSubCommandsInfo { optional: false, commands: &[<T as HelpSubCommand>::HELP_INFO] };
}

/// TODO
pub struct HelpInfo<'a> {
/// TODO
pub description: &'a str,
/// TODO
pub examples: &'a [fn(&[&str]) -> String],
/// TODO
pub notes: &'a [fn(&[&str]) -> String],
/// TODO
pub flags: &'a [HelpFlagInfo<'a>],
/// TODO
pub positionals: &'a [HelpPositionalInfo<'a>],
/// TODO
pub subcommand: Option<HelpSubCommandsInfo<'a>>,
/// TODO
pub error_codes: &'a [(isize, &'a str)],
}

fn help_section(
out: &mut String,
command_name: &[&str],
heading: &str,
sections: &[fn(&[&str]) -> String],
) {
if !sections.is_empty() {
out.push_str(SECTION_SEPARATOR);
for section_fn in sections {
let section = section_fn(command_name);

out.push_str(heading);
for line in section.split('\n') {
out.push('\n');
out.push_str(INDENT);
out.push_str(line);
}
}
}
}

impl<'a> HelpInfo<'a> {
/// TODO
pub fn help(&self, command_name: &[&str]) -> String {
let mut out = format!("Usage: {}", command_name.join(" "));

for positional in self.positionals {
out.push(' ');
positional.help_usage(&mut out);
}

for flag in self.flags {
out.push(' ');
flag.help_usage(&mut out);
}

if let Some(subcommand) = &self.subcommand {
out.push(' ');
if subcommand.optional {
out.push('[');
}
out.push_str("<command>");
if subcommand.optional {
out.push(']');
}
out.push_str(" [<args>]");
}

out.push_str(SECTION_SEPARATOR);

out.push_str(self.description);

if !self.positionals.is_empty() {
out.push_str(SECTION_SEPARATOR);
out.push_str("Positional Arguments:");
for positional in self.positionals {
positional.help_description(&mut out);
}
}

out.push_str(SECTION_SEPARATOR);
out.push_str("Options:");
for flag in self.flags {
flag.help_description(&mut out);
}

// Also include "help"
HELP_FLAG.help_description(&mut out);

if let Some(subcommand) = &self.subcommand {
out.push_str(SECTION_SEPARATOR);
out.push_str("Commands:");
for cmd in subcommand.commands {
let info = CommandInfo { name: cmd.name, description: cmd.info.description };
write_description(&mut out, &info);
}
}

help_section(&mut out, command_name, "Examples:", self.examples);

help_section(&mut out, command_name, "Notes:", self.notes);

if !self.error_codes.is_empty() {
out.push_str(SECTION_SEPARATOR);
out.push_str("Error codes:");
write_error_codes(&mut out, self.error_codes);
}

out.push('\n');

out
}
}

fn write_error_codes(out: &mut String, error_codes: &[(isize, &str)]) {
for (code, text) in error_codes {
out.push('\n');
out.push_str(INDENT);
out.push_str(&format!("{} {}", code, text));
}
}

impl<'a> fmt::Debug for HelpInfo<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let examples = self.examples.iter().map(|f| f(&["{command_name}"])).collect::<Vec<_>>();
let notes = self.notes.iter().map(|f| f(&["{command_name}"])).collect::<Vec<_>>();
f.debug_struct("HelpInfo")
.field("description", &self.description)
.field("examples", &examples)
.field("notes", &notes)
.field("flags", &self.flags)
.field("positionals", &self.positionals)
.field("subcommand", &self.subcommand)
.field("error_codes", &self.error_codes)
.finish()
}
}

/// TODO
#[derive(Debug)]
pub struct HelpSubCommandsInfo<'a> {
/// TODO
pub optional: bool,
/// TODO
pub commands: &'a [HelpSubCommandInfo<'a>],
}

/// TODO
#[derive(Debug)]
pub struct HelpSubCommandInfo<'a> {
/// TODO
pub name: &'a str,
/// TODO
pub info: HelpInfo<'a>,
}

/// TODO
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum HelpOptionality {
/// TODO
None,
/// TODO
Optional,
/// TODO
Repeating,
}

impl HelpOptionality {
/// TODO
fn is_required(&self) -> bool {
matches!(self, HelpOptionality::None)
}
}

/// TODO
#[derive(Debug)]
pub struct HelpPositionalInfo<'a> {
/// TODO
pub name: &'a str,
/// TODO
pub description: &'a str,
/// TODO
pub optionality: HelpOptionality,
}

impl<'a> HelpPositionalInfo<'a> {
/// TODO
pub fn help_usage(&self, out: &mut String) {
if !self.optionality.is_required() {
out.push('[');
}

out.push('<');
out.push_str(self.name);

if self.optionality == HelpOptionality::Repeating {
out.push_str("...");
}

out.push('>');

if !self.optionality.is_required() {
out.push(']');
}
}

/// TODO
pub fn help_description(&self, out: &mut String) {
let info = CommandInfo { name: self.name, description: self.description };
write_description(out, &info);
}
}

/// TODO
#[derive(Debug)]
pub struct HelpFlagInfo<'a> {
/// TODO
pub short: Option<char>,
/// TODO
pub long: &'a str,
/// TODO
pub description: &'a str,
/// TODO
pub optionality: HelpOptionality,
/// TODO
pub kind: HelpFieldKind<'a>,
}

/// TODO
#[derive(Debug)]
pub enum HelpFieldKind<'a> {
/// TODO
Switch,
/// TODO
Option {
/// TODO
arg_name: &'a str,
},
}

impl<'a> HelpFlagInfo<'a> {
/// TODO
pub fn help_usage(&self, out: &mut String) {
if !self.optionality.is_required() {
out.push('[');
}

if let Some(short) = self.short {
out.push('-');
out.push(short);
} else {
out.push_str(self.long);
}

match self.kind {
HelpFieldKind::Switch => {}
HelpFieldKind::Option { arg_name } => {
out.push_str(" <");
out.push_str(arg_name);

if self.optionality == HelpOptionality::Repeating {
out.push_str("...");
}

out.push('>');
}
}

if !self.optionality.is_required() {
out.push(']');
}
}

/// TODO
pub fn help_description(&self, out: &mut String) {
let mut name = String::new();
if let Some(short) = self.short {
name.push('-');
name.push(short);
name.push_str(", ");
}
name.push_str(self.long);

let info = CommandInfo { name: &name, description: self.description };
write_description(out, &info);
}
}
10 changes: 9 additions & 1 deletion argh/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,15 @@

use std::str::FromStr;

pub use argh_derive::FromArgs;
mod help;

pub use {
crate::help::{
Help, HelpFieldKind, HelpFlagInfo, HelpInfo, HelpOptionality, HelpPositionalInfo,
HelpSubCommand, HelpSubCommandInfo, HelpSubCommands, HelpSubCommandsInfo,
},
argh_derive::FromArgs,
};

/// Information about a particular command used for output.
pub type CommandInfo = argh_shared::CommandInfo<'static>;
Expand Down
Loading

0 comments on commit 85bdb1d

Please sign in to comment.