man
man copied to clipboard
Revise exit status section
This PR revises the exit section to allow the user to specify custom status codes and messages to correspond to those codes, implementing the API discussed in #25.
Merged master so that this PR will apply cleanly.
I also reverted deleting the mod.rs
file. I believe this file isn't needed and had deleted it to test, but that's not logically related to this PR and I didn't mean to include the deletion as part of this PR.
Fixed those nits—agreed on both points.
Agreed on both points. I refactored the code and am much happier with how it reads now that the default value is set using an enum instead of with a sentential value. Thanks for the tip!
Nice! Sorry it took me a while to reply. I still don't know what the exact context for this is and how you'll use it, but one thing I'd definitely do is make a single method that allows you to get an iterator of exit status entries, for both cases. After a bit of fiddling I wrote this, that also moves the default list into a static
(a constant value that will be part of the binary):
diff --git a/src/man.rs b/src/man.rs
index 53eda6c..177f0e9 100644
--- a/src/man.rs
+++ b/src/man.rs
@@ -19,33 +19,41 @@ pub struct Manual {
#[derive(Debug, Clone)]
enum ExitStatuses {
- DefaultStatuses([ExitStatus; 3]),
+ DefaultStatuses,
CustomStatuses(Vec<ExitStatus>),
}
+static DEFAULT_STATUSES: [ExitStatus; 3] = [
+ ExitStatus {
+ code: Some(0),
+ description: Some("Successful program execution."),
+ },
+ ExitStatus {
+ code: Some(1),
+ description: Some("Unsuccessful program execution."),
+ },
+ ExitStatus {
+ code: Some(101),
+ description: Some("The program panicked."),
+ },
+];
+
impl ExitStatuses {
fn push(&mut self, new_status: ExitStatus) {
- if let ExitStatuses::CustomStatuses(mut vec) = self.to_owned() {
+ if let ExitStatuses::CustomStatuses(vec) = self {
vec.push(new_status);
- *self = ExitStatuses::CustomStatuses(vec);
}
}
fn set_to_default(&mut self) {
- *self = ExitStatuses::DefaultStatuses([
- ExitStatus {
- code: Some(0),
- description: Some("Successful program execution."),
- },
- ExitStatus {
- code: Some(1),
- description: Some("Unsuccessful program execution."),
- },
- ExitStatus {
- code: Some(101),
- description: Some("The program panicked."),
- },
- ]);
+ *self = ExitStatuses::DefaultStatuses;
+ }
+
+ fn iter(&self) -> impl Iterator<Item = &ExitStatus> {
+ match self {
+ ExitStatuses::CustomStatuses(vec) => vec.iter(),
+ ExitStatuses::DefaultStatuses => DEFAULT_STATUSES.iter(),
+ }
}
}
@@ -397,29 +405,19 @@ fn env(page: Roff, environment: &[Env]) -> Roff {
/// ```
fn exit_status(page: Roff, exit_statuses: &ExitStatuses) -> Roff {
let mut arr = vec![];
- match exit_statuses {
- ExitStatuses::DefaultStatuses(default_statuses) => {
- for status in default_statuses.iter() {
- let code = format!("{}", status.code.expect("Set as part of default"));
- let mut description = String::from(status.description.unwrap());
- description.push_str("\n\n");
- arr.push(list(&[bold(&code)], &[description]));
- }
- }
- ExitStatuses::CustomStatuses(custom_statuses) => {
- if custom_statuses.is_empty() {
- return page;
- }
- for status in custom_statuses.iter() {
- let code =
- format!("{}", status.code.expect("Always set when not default"));
- let mut description = String::from(status.description.unwrap_or(""));
- description.push_str("\n\n");
- arr.push(list(&[bold(&code)], &[description]));
- }
- }
+
+ for status in exit_statuses.iter() {
+ let code = format!("{}", status.code.unwrap());
+ let mut description = String::from(status.description.unwrap());
+ description.push_str("\n\n");
+ arr.push(list(&[bold(&code)], &[description]));
+ }
+
+ if !arr.is_empty() {
+ page.section("exit status", &arr)
+ } else {
+ page
}
- page.section("exit status", &arr)
}
/// Create a custom section.
I'm a bit unsure about the two unwraps in the for loop: It seems that both code and description are optional, but here they are required? What do you want to do? Make sure that all exit codes have code+description, or use fallback values here, or actually have this library crash your program when one of the options is None
? (Not sure if this was a change in this PR, or if it needs to be addressed here, but I wanted to point it out.)
Nice! Sorry it took me a while to reply.
No need to apologize—I thought you replied very promptly so if this was you taking a while to reply, then I can't even imagine what you're like when you're fast :)
one thing I'd definitely do is make a single method that allows you to get an iterator of exit status entries, for both cases
I really like this change! I implemented it in the PR and will keep that pattern in mind for future code.
I'm a bit unsure about the two unwraps in the for loop
I think that the way we have it in the PR now correctly captures the semantics we want. code
is None
if the default
method is called—which is how we can detect that the exit status was initialized with default
rather than new
. If the exit status was initialize with new
, then it's safe to unwrap. (I changed it to expect
to clarify this point).
The description
is None
whenever the user hasn't set a description—and keeping it as None
instead of an empty string clarifies that there isn't a user-supplied description (and is consistent with how we treat other user-supplied optional text, like the help text for flags). But we don't want the program to crash if the user doesn't supply a description, so we substitute in a default value of ''
at the last minute with an unwrap_or
call.
Hmm, all of that is very interesting—I feel like I'm learning a lot from this conversation, so thanks for taking the time.
I'm not entirely sure what API you're suggesting when you said
You call
ExitStatus::default()
to resetExitStatuses
.ExitStatus
should not have that power/responsibility, I think.
The current (pre-PR) API is that the user has no control over the exit status: exit statuses of 0, 1, and 101 are always printed, along with matching descriptions. As @yoshuawuyts and I discussed in #25, we think that most users will want to use those three statuses, so we'd like to keep the process of using them as concise/ergonomic as possible. The API we settled on in #25 was
let page = Manual::new("basic")
.exit_status(ExitStatus::default())
.render();
to display those three exit statuses and
let page = Manual::new("basic")
.exit_status(
ExitStatus::new(1)
.description("Unsuccessful program execution.")
)
.exit_status(
ExitStatus::new(3)
.description("Invalid config.")
)
.render();
to display exit statuses different from those three. Are you suggesting a change from that API?
If we do want to change the API, we could do the following:
let page = Manual::new("basic")
.exit_status(ExitStatus::with_default_values())
.render();
to display those three exit statuses and
let page = Manual::new("basic")
.exit_status(
ExitStatus::with_code(1)
.description("Unsuccessful program execution.")
)
.exit_status(
ExitStatus::with_code(3)
.description("Invalid config.")
)
.render();
However, that change wouldn't address your point when you said:
Off the top of my head I'd make the code field mandatory and add a method
use_default_exit_statuses()
toExitStatuses
But (unless I'm misunderstanding) the ExitStatuses
enum isn't public, so the user wouldn't be about to call the use_default_exit_statuses()
method without other changes to the API.
(Whatever we decide, I agree that the API docs should be improved for this section)
Hmm, all of that is very interesting—I feel like I'm learning a lot from this conversation, so thanks for taking the time.
Cool! I'm happy to!
Are you suggesting a change from that API?
After reading #25: Yes, I disagree with that API.
My main issue is that the .exit_status
method as proposed there (and here) has two modes of operation: Adding one exit status to the list of exit statuses (this makes sense to me), and replacing the currently set exit statuses with three default ones. This latter case feels very weird to me, which made me suggest to add a new method, use_default_exit_statuses
, next to .exit_status
on Man
.
Let me write a comment on #25 with a summary of the API I'd like to suggest.
I like the API you proposed in #25. I added a slight amendment inspired by how clap-rs handles a similar issue and provided details in that thread.