Repository: google/argh
Pull request #91 (2021-05-05 19:52:52)

Add support for redacting commandline arguments
This is an alternative to #82, which refactors how CLI parsing occurs so it can be also used to redact command line arguments for safe analytics tracking.


Patch:
diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs
index 2eb406d..1603b32 100644
--- a/argh_derive/src/lib.rs
+++ b/argh_derive/src/lib.rs
@@ -238,9 +238,43 @@ fn impl_from_args_struct(
         .collect();
 
     ensure_only_last_positional_is_optional(errors, &fields);
+
+    let impl_span = Span::call_site();
+
+    let from_args_method = impl_from_args_struct_from_args(
+        errors,
+        type_attrs,
+        &fields,
+    );
+
+    let redact_arg_values_method = impl_from_args_struct_redact_arg_values(
+        errors,
+        type_attrs,
+        &fields,
+    );
+
     let top_or_sub_cmd_impl = top_or_sub_cmd_impl(errors, name, type_attrs);
-    let init_fields = declare_local_storage_for_fields(&fields);
-    let unwrap_fields = unwrap_fields(&fields);
+
+    let trait_impl = quote_spanned! { impl_span =>
+        impl argh::FromArgs for #name {
+            #from_args_method
+
+            #redact_arg_values_method
+        }
+
+        #top_or_sub_cmd_impl
+    };
+
+    trait_impl.into()
+}
+
+fn impl_from_args_struct_from_args<'a>(
+    errors: &Errors,
+    type_attrs: &TypeAttrs,
+    fields: &'a [StructField<'a>],
+) -> TokenStream {
+    let init_fields = declare_local_storage_for_from_args_fields(&fields);
+    let unwrap_fields = unwrap_from_args_fields(&fields);
     let positional_fields: Vec<&StructField<'_>> =
         fields.iter().filter(|field| field.kind == FieldKind::Positional).collect();
     let positional_field_idents = positional_fields.iter().map(|field| &field.field.ident);
@@ -253,8 +287,8 @@ fn impl_from_args_struct(
     let flag_output_table = fields.iter().filter_map(|field| {
         let field_name = &field.field.ident;
         match field.kind {
-            FieldKind::Option => Some(quote! { argh::CmdOption::Value(&mut #field_name) }),
-            FieldKind::Switch => Some(quote! { argh::CmdOption::Flag(&mut #field_name) }),
+            FieldKind::Option => Some(quote! { argh::ParseStructOption::Value(&mut #field_name) }),
+            FieldKind::Switch => Some(quote! { argh::ParseStructOption::Flag(&mut #field_name) }),
             FieldKind::SubCommand | FieldKind::Positional => None,
         }
     });
@@ -276,135 +310,181 @@ fn impl_from_args_struct(
     let append_missing_requirements =
         append_missing_requirements(&missing_requirements_ident, &fields);
 
-    let check_subcommands = if let Some(subcommand) = subcommand {
+    let parse_subcommands = if let Some(subcommand) = subcommand {
         let name = subcommand.name;
         let ty = subcommand.ty_without_wrapper;
         quote_spanned! { impl_span =>
-            for __subcommand in <#ty as argh::SubCommands>::COMMANDS {
-                if __subcommand.name == __next_arg {
-                    let mut __command = __cmd_name.to_owned();
-                    __command.push(__subcommand.name);
-                    let __prepended_help;
-                    let __remaining_args = if __help {
-                        __prepended_help = argh::prepend_help(__remaining_args);
-                        &__prepended_help
-                    } else {
-                        __remaining_args
-                    };
-                    #name = Some(<#ty as argh::FromArgs>::from_args(&__command, __remaining_args)?);
-                    // Unset `help`, since we handled it in the subcommand
-                    __help = false;
-                    break 'parse_args;
-                }
-            }
+            Some(argh::ParseStructSubCommand {
+                subcommands: <#ty as argh::SubCommands>::COMMANDS,
+                parse_func: &mut |__command, __remaining_args| {
+                    #name = Some(<#ty as argh::FromArgs>::from_args(__command, __remaining_args)?);
+                    Ok(())
+                },
+            })
         }
     } else {
-        TokenStream::new()
+        quote_spanned! { impl_span => None }
     };
 
     // Identifier referring to a value containing the name of the current command as an `&[&str]`.
     let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span.clone());
     let help = help::help(errors, cmd_name_str_array_ident, type_attrs, &fields, subcommand);
 
-    let trait_impl = quote_spanned! { impl_span =>
-        impl argh::FromArgs for #name {
-            fn from_args(__cmd_name: &[&str], __args: &[&str])
-                -> std::result::Result<Self, argh::EarlyExit>
-            {
-                #( #init_fields )*
-                let __flag_output_table = &mut [
-                    #( #flag_output_table, )*
-                ];
-
-                let __positional_output_table = &mut [
-                    #( (
-                        &mut #positional_field_idents as &mut argh::ParseValueSlot,
-                        #positional_field_names
-                    ), )*
-                ];
-
-                let mut __help = false;
-                let mut __remaining_args = __args;
-                let mut __positional_index = 0;
-                let mut __options_ended = false;
-                'parse_args: while let Some(&__next_arg) = __remaining_args.get(0) {
-                    __remaining_args = &__remaining_args[1..];
-                    if (__next_arg == "--help" || __next_arg == "help") && !__options_ended {
-                        __help = true;
-                        continue;
-                    }
+    let method_impl = quote_spanned! { impl_span =>
+        fn from_args(__cmd_name: &[&str], __args: &[&str])
+            -> std::result::Result<Self, argh::EarlyExit>
+        {
+            #( #init_fields )*
+
+            argh::parse_struct_args(
+                __cmd_name,
+                __args,
+                argh::ParseStructOptions {
+                    arg_to_slot: &[ #( #flag_str_to_output_table_map ,)* ],
+                    slots: &mut [ #( #flag_output_table, )* ],
+                },
+                argh::ParseStructPositionals {
+                    positionals: &mut [
+                        #(
+                            argh::ParseStructPositional {
+                                name: #positional_field_names,
+                                slot: &mut #positional_field_idents as &mut argh::ParseValueSlot,
+                            },
+                        )*
+                    ],
+                    last_is_repeating: #last_positional_is_repeating,
+                },
+                #parse_subcommands,
+                &|| #help,
+            )?;
+
+            let mut #missing_requirements_ident = argh::MissingRequirements::default();
+            #(
+                #append_missing_requirements
+            )*
+            #missing_requirements_ident.err_on_any()?;
+
+            Ok(Self {
+                #( #unwrap_fields, )*
+            })
+        }
+    };
 
-                    if __next_arg.starts_with("-") && !__options_ended {
-                        if __next_arg == "--" {
-                            __options_ended = true;
-                            continue;
-                        }
+    method_impl.into()
+}
 
-                        if __help {
-                            return Err(
-                                "Trailing arguments are not allowed after `help`."
-                                    .to_string()
-                                    .into()
-                            );
-                        }
+fn impl_from_args_struct_redact_arg_values<'a>(
+    errors: &Errors,
+    type_attrs: &TypeAttrs,
+    fields: &'a [StructField<'a>],
+) -> TokenStream {
+    let init_fields = declare_local_storage_for_redacted_fields(&fields);
+    let unwrap_fields = unwrap_redacted_fields(&fields);
 
-                        argh::parse_option(
-                            __next_arg,
-                            &mut __remaining_args,
-                            __flag_output_table,
-                            &[ #( #flag_str_to_output_table_map ,)* ],
-                        )?;
-                        continue;
-                    }
+    let positional_fields: Vec<&StructField<'_>> =
+        fields.iter().filter(|field| field.kind == FieldKind::Positional).collect();
+    let positional_field_idents = positional_fields.iter().map(|field| &field.field.ident);
+    let positional_field_names = positional_fields.iter().map(|field| field.name.to_string());
+    let last_positional_is_repeating = positional_fields
+        .last()
+        .map(|field| field.optionality == Optionality::Repeating)
+        .unwrap_or(false);
 
-                    #check_subcommands
+    let flag_output_table = fields.iter().filter_map(|field| {
+        let field_name = &field.field.ident;
+        match field.kind {
+            FieldKind::Option => Some(quote! { argh::ParseStructOption::Value(&mut #field_name) }),
+            FieldKind::Switch => Some(quote! { argh::ParseStructOption::Flag(&mut #field_name) }),
+            FieldKind::SubCommand | FieldKind::Positional => None,
+        }
+    });
 
-                    if __positional_index < __positional_output_table.len() {
-                        argh::parse_positional(
-                            __next_arg,
-                            &mut __positional_output_table[__positional_index],
-                        )?;
+    let flag_str_to_output_table_map = flag_str_to_output_table_map_entries(&fields);
 
-                        // Don't increment position if we're at the last arg
-                        // *and* the last arg is repeating.
-                        let __skip_increment =
-                            #last_positional_is_repeating &&
-                             __positional_index == __positional_output_table.len() - 1;
+    let mut subcommands_iter =
+        fields.iter().filter(|field| field.kind == FieldKind::SubCommand).fuse();
 
-                        if !__skip_increment {
-                            __positional_index += 1;
-                        }
-                    } else {
-                        return std::result::Result::Err(argh::EarlyExit {
-                            output: argh::unrecognized_arg(__next_arg),
-                            status: std::result::Result::Err(()),
-                        });
-                    }
-                }
+    let subcommand: Option<&StructField<'_>> = subcommands_iter.next();
+    while let Some(dup_subcommand) = subcommands_iter.next() {
+        errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field);
+    }
 
-                if __help {
-                    return std::result::Result::Err(argh::EarlyExit {
-                        output: #help,
-                        status: std::result::Result::Ok(()),
-                    });
-                }
+    let impl_span = Span::call_site();
 
-                let mut #missing_requirements_ident = argh::MissingRequirements::default();
-                #(
-                    #append_missing_requirements
-                )*
-                #missing_requirements_ident.err_on_any()?;
+    let missing_requirements_ident = syn::Ident::new("__missing_requirements", impl_span.clone());
 
-                Ok(Self {
-                    #( #unwrap_fields, )*
-                })
-            }
+    let append_missing_requirements =
+        append_missing_requirements(&missing_requirements_ident, &fields);
+
+    let redact_subcommands = if let Some(subcommand) = subcommand {
+        let name = subcommand.name;
+        let ty = subcommand.ty_without_wrapper;
+        quote_spanned! { impl_span =>
+            Some(argh::ParseStructSubCommand {
+                subcommands: <#ty as argh::SubCommands>::COMMANDS,
+                parse_func: &mut |__command, __remaining_args| {
+                    #name = Some(<#ty as argh::FromArgs>::redact_arg_values(__command, __remaining_args)?);
+                    Ok(())
+                },
+            })
         }
+    } else {
+        quote_spanned! { impl_span => None }
+    };
 
-        #top_or_sub_cmd_impl
+    let cmd_name = if type_attrs.is_subcommand.is_none() {
+        quote! { __cmd_name.last().expect("no command name").to_string() }
+    } else {
+        quote! { __cmd_name.last().expect("no subcommand name").to_string() }
     };
 
-    trait_impl.into()
+    // Identifier referring to a value containing the name of the current command as an `&[&str]`.
+    let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span.clone());
+    let help = help::help(errors, cmd_name_str_array_ident, type_attrs, &fields, subcommand);
+
+    let method_impl = quote_spanned! { impl_span =>
+        fn redact_arg_values(__cmd_name: &[&str], __args: &[&str]) -> Result<Vec<String>, argh::EarlyExit> {
+            #( #init_fields )*
+
+            argh::parse_struct_args(
+                __cmd_name,
+                __args,
+                argh::ParseStructOptions {
+                    arg_to_slot: &[ #( #flag_str_to_output_table_map ,)* ],
+                    slots: &mut [ #( #flag_output_table, )* ],
+                },
+                argh::ParseStructPositionals {
+                    positionals: &mut [
+                        #(
+                            argh::ParseStructPositional {
+                                name: #positional_field_names,
+                                slot: &mut #positional_field_idents as &mut argh::ParseValueSlot,
+                            },
+                        )*
+                    ],
+                    last_is_repeating: #last_positional_is_repeating,
+                },
+                #redact_subcommands,
+                &|| #help,
+            )?;
+
+            let mut #missing_requirements_ident = argh::MissingRequirements::default();
+            #(
+                #append_missing_requirements
+            )*
+            #missing_requirements_ident.err_on_any()?;
+
+            let mut __redacted = vec![
+                #cmd_name,
+            ];
+
+            #( #unwrap_fields )*
+
+            Ok(__redacted)
+        }
+    };
+
+    method_impl.into()
 }
 
 /// Ensures that only the last positional arg is non-required.
@@ -458,7 +538,7 @@ fn top_or_sub_cmd_impl(errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttr
 /// Most fields are stored in `Option<FieldType>` locals.
 /// `argh(option)` fields are stored in a `ParseValueSlotTy` along with a
 /// function that knows how to decode the appropriate value.
-fn declare_local_storage_for_fields<'a>(
+fn declare_local_storage_for_from_args_fields<'a>(
     fields: &'a [StructField<'a>],
 ) -> impl Iterator<Item = TokenStream> + 'a {
     fields.iter().map(|field| {
@@ -488,7 +568,7 @@ fn declare_local_storage_for_fields<'a>(
                     let mut #field_name: argh::ParseValueSlotTy<#field_slot_type, #field_type>
                         = argh::ParseValueSlotTy {
                             slot: std::default::Default::default(),
-                            parse_func: #from_str_fn,
+                            parse_func: |_, value| { #from_str_fn(value) },
                         };
                 }
             }
@@ -503,7 +583,7 @@ fn declare_local_storage_for_fields<'a>(
 }
 
 /// Unwrap non-optional fields and take options out of their tuple slots.
-fn unwrap_fields<'a>(fields: &'a [StructField<'a>]) -> impl Iterator<Item = TokenStream> + 'a {
+fn unwrap_from_args_fields<'a>(fields: &'a [StructField<'a>]) -> impl Iterator<Item = TokenStream> + 'a {
     fields.iter().map(|field| {
         let field_name = field.name;
         match field.kind {
@@ -528,6 +608,90 @@ fn unwrap_fields<'a>(fields: &'a [StructField<'a>]) -> impl Iterator<Item = Toke
     })
 }
 
+/// Declare a local slots to store each field in during parsing.
+///
+/// Most fields are stored in `Option<FieldType>` locals.
+/// `argh(option)` fields are stored in a `ParseValueSlotTy` along with a
+/// function that knows how to decode the appropriate value.
+fn declare_local_storage_for_redacted_fields<'a>(
+    fields: &'a [StructField<'a>],
+) -> impl Iterator<Item = TokenStream> + 'a {
+    fields.iter().map(|field| {
+        let field_name = &field.field.ident;
+
+        match field.kind {
+            FieldKind::Switch => {
+                quote! {
+                    let mut #field_name = argh::RedactFlag {
+                        slot: None,
+                    };
+                }
+            }
+            FieldKind::Option => {
+                quote! {
+                    let mut #field_name: argh::ParseValueSlotTy::<Option<String>, String> =
+                        argh::ParseValueSlotTy {
+                        slot: std::default::Default::default(),
+                        parse_func: |arg, _| { Ok(arg.to_string()) },
+                    };
+                }
+            }
+            FieldKind::Positional => {
+                let field_slot_type = match field.optionality {
+                    Optionality::Repeating => {
+                        quote! { std::vec::Vec<String> }
+                    }
+                    Optionality::None | Optionality::Optional | Optionality::Defaulted(_) => {
+                        quote! { std::option::Option<String> }
+                    }
+                };
+
+                let long_name = field.name.to_string();
+                quote! {
+                    let mut #field_name: argh::ParseValueSlotTy::<#field_slot_type, String> =
+                        argh::ParseValueSlotTy {
+                        slot: std::default::Default::default(),
+                        parse_func: |_, _| { Ok(#long_name.to_string()) },
+                    };
+                }
+
+            }
+            FieldKind::SubCommand => {
+                quote! { let mut #field_name: std::option::Option<std::vec::Vec<String>> = None; }
+            }
+        }
+    })
+}
+
+/// Unwrap non-optional fields and take options out of their tuple slots.
+fn unwrap_redacted_fields<'a>(fields: &'a [StructField<'a>]) -> impl Iterator<Item = TokenStream> + 'a {
+    fields.iter().map(|field| {
+        let field_name = field.name;
+
+        match field.kind {
+            FieldKind::Switch | FieldKind::Option => {
+                quote! {
+                    if let Some(__field_name) = #field_name.slot {
+                        __redacted.push(__field_name);
+                    }
+                }
+            }
+            FieldKind::Positional => {
+                quote! {
+                    __redacted.extend(#field_name.slot.into_iter());
+                }
+            }
+            FieldKind::SubCommand => {
+                quote! {
+                    if let Some(__subcommand_args) = #field_name {
+                        __redacted.extend(__subcommand_args.into_iter());
+                    }
+                }
+            }
+        }
+    })
+}
+
 /// Entries of tokens like `("--some-flag-key", 5)` that map from a flag key string
 /// to an index in the output table.
 fn flag_str_to_output_table_map_entries<'a>(fields: &'a [StructField<'a>]) -> Vec<TokenStream> {
@@ -667,10 +831,8 @@ fn impl_from_args_enum(
         .collect();
 
     let name_repeating = std::iter::repeat(name.clone());
-    let variant_ty_1 = variants.iter().map(|x| x.ty);
-    let variant_ty_2 = variant_ty_1.clone();
-    let variant_ty_3 = variant_ty_1.clone();
-    let variant_names = variants.iter().map(|x| x.name);
+    let variant_ty = variants.iter().map(|x| x.ty).collect::<Vec<_>>();
+    let variant_names = variants.iter().map(|x| x.name).collect::<Vec<_>>();
 
     quote! {
         impl argh::FromArgs for #name {
@@ -679,19 +841,29 @@ fn impl_from_args_enum(
             {
                 let subcommand_name = *command_name.last().expect("no subcommand name");
                 #(
-                    if subcommand_name == <#variant_ty_1 as argh::SubCommand>::COMMAND.name {
+                    if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name {
                         return Ok(#name_repeating::#variant_names(
-                            <#variant_ty_2 as argh::FromArgs>::from_args(command_name, args)?
+                            <#variant_ty as argh::FromArgs>::from_args(command_name, args)?
                         ));
                     }
                 )*
                 unreachable!("no subcommand matched")
             }
+
+            fn redact_arg_values(command_name: &[&str], args: &[&str]) -> std::result::Result<Vec<String>, argh::EarlyExit> {
+                let subcommand_name = *command_name.last().expect("no subcommand name");
+                #(
+                    if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name {
+                        return <#variant_ty as argh::FromArgs>::redact_arg_values(command_name, args);
+                    }
+                )*
+                unreachable!("no subcommand matched")
+            }
         }
 
         impl argh::SubCommands for #name {
             const COMMANDS: &'static [&'static argh::CommandInfo] = &[#(
-                <#variant_ty_3 as argh::SubCommand>::COMMAND,
+                <#variant_ty as argh::SubCommand>::COMMAND,
             )*];
         }
     }
diff --git a/src/lib.rs b/src/lib.rs
index aafa303..984d927 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -182,10 +182,260 @@ pub type CommandInfo = argh_shared::CommandInfo<'static>;
 pub trait FromArgs: Sized {
     /// Construct the type from an input set of arguments.
     ///
-    /// The first argument `command_name` is the identifier for the current
-    /// command, treating each segment as space-separated. This is to be
-    /// used in the output of `--help`, `--version`, and similar flags.
+    /// The first argument `command_name` is the identifier for the current command. In most cases,
+    /// users should only pass in a single item for the command name, which typically comes from
+    /// the first item from `std::env::args()`. Implementations however should append the
+    /// subcommand name in when recursively calling [FromArgs::from_args] for subcommands. This
+    /// allows `argh` to generate correct subcommand help strings.
+    ///
+    /// The second argument `args` is the rest of the command line arguments.
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// # use argh::FromArgs;
+    ///
+    /// /// Command to manage a classroom.
+    /// #[derive(Debug, PartialEq, FromArgs)]
+    /// struct ClassroomCmd {
+    ///     #[argh(subcommand)]
+    ///     subcommands: Subcommands,
+    /// }
+    ///
+    /// #[derive(Debug, PartialEq, FromArgs)]
+    /// #[argh(subcommand)]
+    /// enum Subcommands {
+    ///     List(ListCmd),
+    ///     Add(AddCmd),
+    /// }
+    ///
+    /// /// list all the classes.
+    /// #[derive(Debug, PartialEq, FromArgs)]
+    /// #[argh(subcommand, name = "list")]
+    /// struct ListCmd {
+    ///     /// list classes for only this teacher.
+    ///     #[argh(option)]
+    ///     teacher_name: Option<String>,
+    /// }
+    ///
+    /// /// add students to a class.
+    /// #[derive(Debug, PartialEq, FromArgs)]
+    /// #[argh(subcommand, name = "add")]
+    /// struct AddCmd {
+    ///     /// the name of the class's teacher.
+    ///     #[argh(option)]
+    ///     teacher_name: String,
+    ///
+    ///     /// the name of the class.
+    ///     #[argh(positional)]
+    ///     class_name: String,
+    /// }
+    ///
+    /// let args = ClassroomCmd::from_args(
+    ///     &["classroom"],
+    ///     &["list", "--teacher-name", "Smith"],
+    /// ).unwrap();
+    /// assert_eq!(
+    ///    args,
+    ///     ClassroomCmd {
+    ///         subcommands: Subcommands::List(ListCmd {
+    ///             teacher_name: Some("Smith".to_string()),
+    ///         })
+    ///     },
+    /// );
+    ///
+    /// // Help returns an error, but internally returns an `Ok` status.
+    /// let early_exit = ClassroomCmd::from_args(
+    ///     &["classroom"],
+    ///     &["help"],
+    /// ).unwrap_err();
+    /// assert_eq!(
+    ///     early_exit,
+    ///     argh::EarlyExit {
+    ///        output: r#"Usage: classroom <command> [<args>]
+    ///
+    /// Command to manage a classroom.
+    ///
+    /// Options:
+    ///   --help            display usage information
+    ///
+    /// Commands:
+    ///   list              list all the classes.
+    ///   add               add students to a class.
+    /// "#.to_string(),
+    ///        status: Ok(()),
+    ///     },
+    /// );
+    ///
+    /// // Help works with subcommands.
+    /// let early_exit = ClassroomCmd::from_args(
+    ///     &["classroom"],
+    ///     &["list", "help"],
+    /// ).unwrap_err();
+    /// assert_eq!(
+    ///     early_exit,
+    ///     argh::EarlyExit {
+    ///        output: r#"Usage: classroom list [--teacher-name <teacher-name>]
+    ///
+    /// list all the classes.
+    ///
+    /// Options:
+    ///   --teacher-name    list classes for only this teacher.
+    ///   --help            display usage information
+    /// "#.to_string(),
+    ///        status: Ok(()),
+    ///     },
+    /// );
+    ///
+    /// // Incorrect arguments will error out.
+    /// let err = ClassroomCmd::from_args(
+    ///     &["classroom"],
+    ///     &["lisp"],
+    /// ).unwrap_err();
+    /// assert_eq!(
+    ///    err,
+    ///    argh::EarlyExit {
+    ///        output: "Unrecognized argument: lisp\n".to_string(),
+    ///        status: Err(()),
+    ///     },
+    /// );
+    /// ```
     fn from_args(command_name: &[&str], args: &[&str]) -> Result<Self, EarlyExit>;
+
+    /// Get a String with just the argument names, e.g., options, flags, subcommands, etc, but
+    /// without the values of the options and arguments. This can be useful as a means to capture
+    /// anonymous usage statistics without revealing the content entered by the end user.
+    ///
+    /// The first argument `command_name` is the identifier for the current command. In most cases,
+    /// users should only pass in a single item for the command name, which typically comes from
+    /// the first item from `std::env::args()`. Implementations however should append the
+    /// subcommand name in when recursively calling [FromArgs::from_args] for subcommands. This
+    /// allows `argh` to generate correct subcommand help strings.
+    ///
+    /// The second argument `args` is the rest of the command line arguments.
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// # use argh::FromArgs;
+    ///
+    /// /// Command to manage a classroom.
+    /// #[derive(FromArgs)]
+    /// struct ClassroomCmd {
+    ///     #[argh(subcommand)]
+    ///     subcommands: Subcommands,
+    /// }
+    ///
+    /// #[derive(FromArgs)]
+    /// #[argh(subcommand)]
+    /// enum Subcommands {
+    ///     List(ListCmd),
+    ///     Add(AddCmd),
+    /// }
+    ///
+    /// /// list all the classes.
+    /// #[derive(FromArgs)]
+    /// #[argh(subcommand, name = "list")]
+    /// struct ListCmd {
+    ///     /// list classes for only this teacher.
+    ///     #[argh(option)]
+    ///     teacher_name: Option<String>,
+    /// }
+    ///
+    /// /// add students to a class.
+    /// #[derive(FromArgs)]
+    /// #[argh(subcommand, name = "add")]
+    /// struct AddCmd {
+    ///     /// the name of the class's teacher.
+    ///     #[argh(option)]
+    ///     teacher_name: String,
+    ///
+    ///     /// has the class started yet?
+    ///     #[argh(switch)]
+    ///     started: bool,
+    ///
+    ///     /// the name of the class.
+    ///     #[argh(positional)]
+    ///     class_name: String,
+    ///
+    ///     /// the student names.
+    ///     #[argh(positional)]
+    ///     students: Vec<String>,
+    /// }
+    ///
+    /// let args = ClassroomCmd::redact_arg_values(
+    ///     &["classroom"],
+    ///     &["list"],
+    /// ).unwrap();
+    /// assert_eq!(
+    ///     args,
+    ///     &[
+    ///         "classroom",
+    ///         "list",
+    ///     ],
+    /// );
+    ///
+    /// let args = ClassroomCmd::redact_arg_values(
+    ///     &["classroom"],
+    ///     &["list", "--teacher-name", "Smith"],
+    /// ).unwrap();
+    /// assert_eq!(
+    ///    args,
+    ///    &[
+    ///         "classroom",
+    ///         "list",
+    ///         "--teacher-name",
+    ///     ],
+    /// );
+    ///
+    /// let args = ClassroomCmd::redact_arg_values(
+    ///     &["classroom"],
+    ///     &["add", "--teacher-name", "Smith", "--started", "Math", "Abe", "Sung"],
+    /// ).unwrap();
+    /// assert_eq!(
+    ///     args,
+    ///     &[
+    ///         "classroom",
+    ///         "add",
+    ///         "--teacher-name",
+    ///         "--started",
+    ///         "class_name",
+    ///         "students",
+    ///         "students",
+    ///     ],
+    /// );
+    ///
+    /// // `ClassroomCmd::redact_arg_values` will error out if passed invalid arguments.
+    /// assert_eq!(
+    ///     ClassroomCmd::redact_arg_values(&["classroom"], &["add", "--teacher-name"]),
+    ///     Err(argh::EarlyExit {
+    ///         output: "No value provided for option '--teacher-name'.\n".into(),
+    ///         status: Err(()),
+    ///     }),
+    /// );
+    ///
+    /// // `ClassroomCmd::redact_arg_values` will generate help messages.
+    /// assert_eq!(
+    ///     ClassroomCmd::redact_arg_values(&["classroom"], &["help"]),
+    ///     Err(argh::EarlyExit {
+    ///         output: r#"Usage: classroom <command> [<args>]
+    ///
+    /// Command to manage a classroom.
+    ///
+    /// Options:
+    ///   --help            display usage information
+    ///
+    /// Commands:
+    ///   list              list all the classes.
+    ///   add               add students to a class.
+    /// "#.to_string(),
+    ///         status: Ok(()),
+    ///     }),
+    /// );
+    /// ```
+    fn redact_arg_values(_command_name: &[&str], _args: &[&str]) -> Result<Vec<String>, EarlyExit> {
+        Ok(vec!["<<REDACTED>>".into()])
+    }
 }
 
 /// A top-level `FromArgs` implementation that is not a subcommand.
@@ -309,6 +559,28 @@ where
 // The following items are all used by the generated code, and should not be considered part
 // of this library's public API surface.
 
+#[doc(hidden)]
+pub trait ParseFlag {
+    fn set_flag(&mut self, arg: &str);
+}
+
+impl<T: Flag> ParseFlag for T {
+    fn set_flag(&mut self, _arg: &str) {
+        <T as Flag>::set_flag(self);
+    }
+}
+
+#[doc(hidden)]
+pub struct RedactFlag {
+    pub slot: Option<String>,
+}
+
+impl ParseFlag for RedactFlag {
+    fn set_flag(&mut self, arg: &str) {
+        self.slot = Some(arg.to_string());
+    }
+}
+
 // A trait for for slots that reserve space for a value and know how to parse that value
 // from a command-line `&str` argument.
 //
@@ -317,7 +589,7 @@ where
 // generic parameters.
 #[doc(hidden)]
 pub trait ParseValueSlot {
-    fn fill_slot(&mut self, value: &str) -> Result<(), String>;
+    fn fill_slot(&mut self, arg: &str, value: &str) -> Result<(), String>;
 }
 
 // The concrete type implementing the `ParseValueSlot` trait.
@@ -329,25 +601,25 @@ pub struct ParseValueSlotTy<Slot, T> {
     // The slot for a parsed value.
     pub slot: Slot,
     // The function to parse the value from a string
-    pub parse_func: fn(&str) -> Result<T, String>,
+    pub parse_func: fn(&str, &str) -> Result<T, String>,
 }
 
 // `ParseValueSlotTy<Option<T>, T>` is used as the slot for all non-repeating
 // arguments, both optional and required.
 impl<T> ParseValueSlot for ParseValueSlotTy<Option<T>, T> {
-    fn fill_slot(&mut self, value: &str) -> Result<(), String> {
+    fn fill_slot(&mut self, arg: &str, value: &str) -> Result<(), String> {
         if self.slot.is_some() {
             return Err("duplicate values provided".to_string());
         }
-        self.slot = Some((self.parse_func)(value)?);
+        self.slot = Some((self.parse_func)(arg, value)?);
         Ok(())
     }
 }
 
 // `ParseValueSlotTy<Vec<T>, T>` is used as the slot for repeating arguments.
 impl<T> ParseValueSlot for ParseValueSlotTy<Vec<T>, T> {
-    fn fill_slot(&mut self, value: &str) -> Result<(), String> {
-        self.slot.push((self.parse_func)(value)?);
+    fn fill_slot(&mut self, arg: &str, value: &str) -> Result<(), String> {
+        self.slot.push((self.parse_func)(arg, value)?);
         Ok(())
     }
 }
@@ -358,6 +630,7 @@ pub trait Flag {
     fn default() -> Self
     where
         Self: Sized;
+
     /// Sets the flag. This function is called when the flag is provided.
     fn set_flag(&mut self);
 }
@@ -388,79 +661,229 @@ macro_rules! impl_flag_for_integers {
 
 impl_flag_for_integers![u8, u16, u32, u64, u128, i8, i16, i32, i64, i128,];
 
+/// This function implements argument parsing for structs.
+///
+/// `cmd_name`: The identifier for the current command.
+/// `args`: The command line arguments.
+/// `parse_options`: Helper to parse optional arguments.
+/// `parse_positionals`: Helper to parse positional arguments.
+/// `parse_subcommand`: Helper to parse a subcommand.
+/// `help_func`: Generate a help message.
+#[doc(hidden)]
+pub fn parse_struct_args(
+    cmd_name: &[&str],
+    args: &[&str],
+    mut parse_options: ParseStructOptions<'_>,
+    mut parse_positionals: ParseStructPositionals<'_>,
+    mut parse_subcommand: Option<ParseStructSubCommand<'_>>,
+    help_func: &dyn Fn() -> String,
+) -> Result<(), EarlyExit> {
+    let mut help = false;
+    let mut remaining_args = args;
+    let mut positional_index = 0;
+    let mut options_ended = false;
+
+    'parse_args: while let Some(&next_arg) = remaining_args.get(0) {
+        remaining_args = &remaining_args[1..];
+        if (next_arg == "--help" || next_arg == "help") && !options_ended {
+            help = true;
+            continue;
+        }
+
+        if next_arg.starts_with("-") && !options_ended {
+            if next_arg == "--" {
+                options_ended = true;
+                continue;
+            }
+
+            if help {
+                return Err("Trailing arguments are not allowed after `help`.".to_string().into());
+            }
+
+            parse_options.parse(next_arg, &mut remaining_args)?;
+            continue;
+        }
+
+        if let Some(ref mut parse_subcommand) = parse_subcommand {
+            if parse_subcommand.parse(help, cmd_name, next_arg, remaining_args)? {
+                // Unset `help`, since we handled it in the subcommand
+                help = false;
+                break 'parse_args;
+            }
+        }
+
+        parse_positionals.parse(&mut positional_index, next_arg)?;
+    }
+
+    if help {
+        Err(EarlyExit { output: help_func(), status: Ok(()) })
+    } else {
+        Ok(())
+    }
+}
+
+#[doc(hidden)]
+pub struct ParseStructOptions<'a> {
+    /// A mapping from option string literals to the entry
+    /// in the output table. This may contain multiple entries mapping to
+    /// the same location in the table if both a short and long version
+    /// of the option exist (`-z` and `--zoo`).
+    pub arg_to_slot: &'static [(&'static str, usize)],
+
+    /// The storage for argument output data.
+    pub slots: &'a mut [ParseStructOption<'a>],
+}
+
+impl<'a> ParseStructOptions<'a> {
+    /// Parse a commandline option.
+    ///
+    /// `arg`: the current option argument being parsed (e.g. `--foo`).
+    /// `remaining_args`: the remaining command line arguments. This slice
+    /// will be advanced forwards if the option takes a value argument.
+    fn parse(&mut self, arg: &str, remaining_args: &mut &[&str]) -> Result<(), String> {
+        let pos = self
+            .arg_to_slot
+            .iter()
+            .find_map(|&(name, pos)| if name == arg { Some(pos) } else { None })
+            .ok_or_else(|| unrecognized_argument(arg))?;
+
+        match self.slots[pos] {
+            ParseStructOption::Flag(ref mut b) => b.set_flag(arg),
+            ParseStructOption::Value(ref mut pvs) => {
+                let value = remaining_args
+                    .get(0)
+                    .ok_or_else(|| ["No value provided for option '", arg, "'.\n"].concat())?;
+                *remaining_args = &remaining_args[1..];
+                pvs.fill_slot(arg, value).map_err(|s| {
+                    ["Error parsing option '", arg, "' with value '", value, "': ", &s, "\n"]
+                        .concat()
+                })?;
+            }
+        }
+
+        Ok(())
+    }
+}
+
+fn unrecognized_argument(x: &str) -> String {
+    ["Unrecognized argument: ", x, "\n"].concat()
+}
+
 // `--` or `-` options, including a mutable reference to their value.
 #[doc(hidden)]
-pub enum CmdOption<'a> {
+pub enum ParseStructOption<'a> {
     // A flag which is set to `true` when provided.
-    Flag(&'a mut dyn Flag),
+    Flag(&'a mut dyn ParseFlag),
     // A value which is parsed from the string following the `--` argument,
     // e.g. `--foo bar`.
     Value(&'a mut dyn ParseValueSlot),
 }
 
 #[doc(hidden)]
-pub fn unrecognized_argument(x: &str) -> String {
-    ["Unrecognized argument: ", x, "\n"].concat()
+pub struct ParseStructPositionals<'a> {
+    pub positionals: &'a mut [ParseStructPositional<'a>],
+    pub last_is_repeating: bool,
 }
 
-/// Parse a commandline option.
-///
-/// `arg`: the current option argument being parsed (e.g. `--foo`).
-/// `remaining_args`: the remaining command line arguments. This slice
-/// will be advanced forwards if the option takes a value argument.
-/// `output_table`: the storage for output data.
-/// `arg_to_input`: a mapping from option string literals to the entry
-/// in the output table. This may contain multiple entries mapping to
-/// the same location in the table if both a short and long version
-/// of the option exist (`-z` and `--zoo`).
-#[doc(hidden)]
-pub fn parse_option(
-    arg: &str,
-    remaining_args: &mut &[&str],
-    output_table: &mut [CmdOption<'_>],
-    arg_to_output: &[(&str, usize)],
-) -> Result<(), String> {
-    let pos = arg_to_output
-        .iter()
-        .find_map(|&(name, pos)| if name == arg { Some(pos) } else { None })
-        .ok_or_else(|| unrecognized_argument(arg))?;
-
-    match &mut output_table[pos] {
-        CmdOption::Flag(b) => b.set_flag(),
-        CmdOption::Value(pvs) => {
-            let value = remaining_args
-                .get(0)
-                .ok_or_else(|| ["No value provided for option '", arg, "'.\n"].concat())?;
-            *remaining_args = &remaining_args[1..];
-            pvs.fill_slot(value).map_err(|s| {
-                ["Error parsing option '", arg, "' with value '", value, "': ", &s, "\n"].concat()
-            })?;
+impl<'a> ParseStructPositionals<'a> {
+    /// Parse the next positional argument.
+    ///
+    /// `arg`: the argument supplied by the user.
+    fn parse(&mut self, index: &mut usize, arg: &str) -> Result<(), EarlyExit> {
+        if *index < self.positionals.len() {
+            self.positionals[*index].parse(arg)?;
+
+            // Don't increment position if we're at the last arg
+            // *and* the last arg is repeating.
+            let skip_increment = self.last_is_repeating && *index == self.positionals.len() - 1;
+
+            if !skip_increment {
+                *index += 1;
+            }
+
+            Ok(())
+        } else {
+            Err(EarlyExit { output: unrecognized_arg(arg), status: Err(()) })
         }
     }
-
-    Ok(())
 }
 
-/// Parse a positional argument.
-///
-/// arg: the argument supplied by the user
-/// positional: a tuple containing slot to parse into and the name of the argument
 #[doc(hidden)]
-pub fn parse_positional(
-    arg: &str,
-    positional: &mut (&mut dyn ParseValueSlot, &'static str),
-) -> Result<(), String> {
-    let (slot, name) = positional;
-    slot.fill_slot(arg).map_err(|s| {
-        ["Error parsing positional argument '", name, "' with value '", arg, "': ", &s, "\n"]
+pub struct ParseStructPositional<'a> {
+    // The positional's name
+    pub name: &'static str,
+
+    // The function to parse the positional.
+    pub slot: &'a mut dyn ParseValueSlot,
+}
+
+impl<'a> ParseStructPositional<'a> {
+    /// Parse a positional argument.
+    ///
+    /// `arg`: the argument supplied by the user.
+    fn parse(&mut self, arg: &str) -> Result<(), EarlyExit> {
+        self.slot.fill_slot("", arg).map_err(|s| {
+            [
+                "Error parsing positional argument '",
+                self.name,
+                "' with value '",
+                arg,
+                "': ",
+                &s,
+                "\n",
+            ]
             .concat()
-    })
+            .into()
+        })
+    }
+}
+
+// A type to simplify parsing struct subcommands.
+//
+// This indirection is necessary to allow abstracting over `FromArgs` instances with different
+// generic parameters.
+#[doc(hidden)]
+pub struct ParseStructSubCommand<'a> {
+    // The subcommand commands
+    pub subcommands: &'static [&'static CommandInfo],
+
+    // The function to parse the subcommand arguments.
+    pub parse_func: &'a mut dyn FnMut(&[&str], &[&str]) -> Result<(), EarlyExit>,
+}
+
+impl<'a> ParseStructSubCommand<'a> {
+    fn parse(
+        &mut self,
+        help: bool,
+        cmd_name: &[&str],
+        arg: &str,
+        remaining_args: &[&str],
+    ) -> Result<bool, EarlyExit> {
+        for subcommand in self.subcommands {
+            if subcommand.name == arg {
+                let mut command = cmd_name.to_owned();
+                command.push(subcommand.name);
+                let prepended_help;
+                let remaining_args = if help {
+                    prepended_help = prepend_help(remaining_args);
+                    &prepended_help
+                } else {
+                    remaining_args
+                };
+
+                (self.parse_func)(&command, remaining_args)?;
+
+                return Ok(true);
+            }
+        }
+
+        return Ok(false);
+    }
 }
 
 // Prepend `help` to a list of arguments.
 // This is used to pass the `help` argument on to subcommands.
-#[doc(hidden)]
-pub fn prepend_help<'a>(args: &[&'a str]) -> Vec<&'a str> {
+fn prepend_help<'a>(args: &[&'a str]) -> Vec<&'a str> {
     [&["help"], args].concat()
 }
 
@@ -473,13 +896,7 @@ pub fn print_subcommands(commands: &[&CommandInfo]) -> String {
     out
 }
 
-#[doc(hidden)]
-pub fn expected_subcommand(commands: &[&str]) -> String {
-    ["Expected one of the following subcommands: ", &commands.join(", "), "\n"].concat()
-}
-
-#[doc(hidden)]
-pub fn unrecognized_arg(arg: &str) -> String {
+fn unrecognized_arg(arg: &str) -> String {
     ["Unrecognized argument: ", arg, "\n"].concat()
 }
 


Test patch:
diff --git a/tests/lib.rs b/tests/lib.rs
index 6b66c0a..9369bad 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -808,7 +808,7 @@ Options:
     #[test]
     fn example_parses_correctly() {
         let help_example = HelpExample::from_args(
-            &["<<<arg0>>>"],
+            &["program-name"],
             &["-f", "--scribble", "fooey", "blow-up", "--safely"],
         )
         .unwrap();
@@ -827,7 +827,7 @@ Options:
 
     #[test]
     fn example_errors_on_missing_required_option_and_missing_required_subcommand() {
-        let exit = HelpExample::from_args(&["<<<arg0>>>"], &[]).unwrap_err();
+        let exit = HelpExample::from_args(&["program-name"], &[]).unwrap_err();
         exit.status.unwrap_err();
         assert_eq!(
             exit.output,
@@ -876,3 +876,326 @@ Error codes:
         );
     }
 }
+
+#[test]
+fn redact_arg_values_no_args() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(option)]
+        /// a msg param
+        msg: Option<String>,
+    }
+
+    let actual = Cmd::redact_arg_values(&["program-name"], &[]).unwrap();
+    assert_eq!(actual, &["program-name"]);
+}
+
+#[test]
+fn redact_arg_values_optional_arg() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(option)]
+        /// a msg param
+        msg: Option<String>,
+    }
+
+    let actual = Cmd::redact_arg_values(&["program-name"], &["--msg", "hello"]).unwrap();
+    assert_eq!(actual, &["program-name", "--msg"]);
+}
+
+#[test]
+fn redact_arg_values_two_option_args() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(option)]
+        /// a msg param
+        msg: String,
+
+        #[argh(option)]
+        /// a delivery param
+        delivery: String,
+    }
+
+    let actual =
+        Cmd::redact_arg_values(&["program-name"], &["--msg", "hello", "--delivery", "next day"])
+            .unwrap();
+    assert_eq!(actual, &["program-name", "--msg", "--delivery"]);
+}
+
+#[test]
+fn redact_arg_values_option_one_optional_args() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(option)]
+        /// a msg param
+        msg: String,
+
+        #[argh(option)]
+        /// a delivery param
+        delivery: Option<String>,
+    }
+
+    let actual =
+        Cmd::redact_arg_values(&["program-name"], &["--msg", "hello", "--delivery", "next day"])
+            .unwrap();
+    assert_eq!(actual, &["program-name", "--msg", "--delivery"]);
+
+    let actual = Cmd::redact_arg_values(&["program-name"], &["--msg", "hello"]).unwrap();
+    assert_eq!(actual, &["program-name", "--msg"]);
+}
+
+#[test]
+fn redact_arg_values_switch() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(switch, short = 'f')]
+        /// speed of cmd
+        faster: bool,
+    }
+
+    let actual = Cmd::redact_arg_values(&["program-name"], &["--faster"]).unwrap();
+    assert_eq!(actual, &["program-name", "--faster"]);
+
+    let actual = Cmd::redact_arg_values(&["program-name"], &["-f"]).unwrap();
+    assert_eq!(actual, &["program-name", "-f"]);
+}
+
+#[test]
+fn redact_arg_values_positional() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(positional)]
+        /// speed of cmd
+        speed: u8,
+    }
+
+    let actual = Cmd::redact_arg_values(&["program-name"], &["5"]).unwrap();
+    assert_eq!(actual, &["program-name", "speed"]);
+}
+
+#[test]
+fn redact_arg_values_positional_repeating() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(positional)]
+        /// speed of cmd
+        speed: Vec<u8>,
+    }
+
+    let actual = Cmd::redact_arg_values(&["program-name"], &["5", "6"]).unwrap();
+    assert_eq!(actual, &["program-name", "speed", "speed"]);
+}
+
+#[test]
+fn redact_arg_values_positional_err() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(positional)]
+        /// speed of cmd
+        speed: u8,
+    }
+
+    let actual = Cmd::redact_arg_values(&["program-name"], &[]).unwrap_err();
+    assert_eq!(
+        actual,
+        argh::EarlyExit {
+            output: "Required positional arguments not provided:\n    speed\n".into(),
+            status: Err(()),
+        }
+    );
+}
+
+#[test]
+fn redact_arg_values_two_positional() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(positional)]
+        /// speed of cmd
+        speed: u8,
+
+        #[argh(positional)]
+        /// direction
+        direction: String,
+    }
+
+    let actual = Cmd::redact_arg_values(&["program-name"], &["5", "north"]).unwrap();
+    assert_eq!(actual, &["program-name", "speed", "direction"]);
+}
+
+#[test]
+fn redact_arg_values_positional_option() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(positional)]
+        /// speed of cmd
+        speed: u8,
+
+        #[argh(option)]
+        /// direction
+        direction: String,
+    }
+
+    let actual = Cmd::redact_arg_values(&["program-name"], &["5", "--direction", "north"]).unwrap();
+    assert_eq!(actual, &["program-name", "speed", "--direction"]);
+}
+
+#[test]
+fn redact_arg_values_positional_optional_option() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(positional)]
+        /// speed of cmd
+        speed: u8,
+
+        #[argh(option)]
+        /// direction
+        direction: Option<String>,
+    }
+
+    let actual = Cmd::redact_arg_values(&["program-name"], &["5"]).unwrap();
+    assert_eq!(actual, &["program-name", "speed"]);
+}
+
+#[test]
+fn redact_arg_values_subcommand() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(positional)]
+        /// speed of cmd
+        speed: u8,
+
+        #[argh(subcommand)]
+        /// means of transportation
+        means: MeansSubcommand,
+    }
+
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    #[argh(subcommand)]
+    enum MeansSubcommand {
+        Walking(WalkingSubcommand),
+        Biking(BikingSubcommand),
+        Driving(DrivingSubcommand),
+    }
+
+    #[derive(FromArgs, Debug)]
+    #[argh(subcommand, name = "walking")]
+    /// Short description
+    struct WalkingSubcommand {
+        #[argh(option)]
+        /// a song to listen to
+        music: String,
+    }
+
+    #[derive(FromArgs, Debug)]
+    #[argh(subcommand, name = "biking")]
+    /// Short description
+    struct BikingSubcommand {}
+    #[derive(FromArgs, Debug)]
+    #[argh(subcommand, name = "driving")]
+    /// short description
+    struct DrivingSubcommand {}
+
+    let actual =
+        Cmd::redact_arg_values(&["program-name"], &["5", "walking", "--music", "Bach"]).unwrap();
+    assert_eq!(actual, &["program-name", "speed", "walking", "--music"]);
+}
+
+#[test]
+fn redact_arg_values_subcommand_with_space_in_name() {
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    struct Cmd {
+        #[argh(positional)]
+        /// speed of cmd
+        speed: u8,
+
+        #[argh(subcommand)]
+        /// means of transportation
+        means: MeansSubcommand,
+    }
+
+    #[derive(FromArgs, Debug)]
+    /// Short description
+    #[argh(subcommand)]
+    enum MeansSubcommand {
+        Walking(WalkingSubcommand),
+        Biking(BikingSubcommand),
+    }
+
+    #[derive(FromArgs, Debug)]
+    #[argh(subcommand, name = "has space")]
+    /// Short description
+    struct WalkingSubcommand {
+        #[argh(option)]
+        /// a song to listen to
+        music: String,
+    }
+
+    #[derive(FromArgs, Debug)]
+    #[argh(subcommand, name = "biking")]
+    /// Short description
+    struct BikingSubcommand {}
+
+    let actual =
+        Cmd::redact_arg_values(&["program-name"], &["5", "has space", "--music", "Bach"]).unwrap();
+    assert_eq!(actual, &["program-name", "speed", "has space", "--music"]);
+}
+
+#[test]
+fn redact_arg_values_produces_help() {
+    #[derive(argh::FromArgs, Debug, PartialEq)]
+    /// Woot
+    struct Repeating {
+        #[argh(option, short = 'n')]
+        /// fooey
+        n: Vec<String>,
+    }
+
+    assert_eq!(
+        Repeating::redact_arg_values(&["program-name"], &["--help"]),
+        Err(argh::EarlyExit {
+            output: r###"Usage: program-name [-n <n...>]
+
+Woot
+
+Options:
+  -n, --n           fooey
+  --help            display usage information
+"###
+            .to_string(),
+            status: Ok(()),
+        }),
+    );
+}
+
+#[test]
+fn redact_arg_values_produces_errors_with_bad_arguments() {
+    #[derive(argh::FromArgs, Debug, PartialEq)]
+    /// Woot
+    struct Cmd {
+        #[argh(option, short = 'n')]
+        /// fooey
+        n: String,
+    }
+
+    assert_eq!(
+        Cmd::redact_arg_values(&["program-name"], &["--n"]),
+        Err(argh::EarlyExit {
+            output: "No value provided for option '--n'.\n".to_string(),
+            status: Err(()),
+        }),
+    );
+}