cliparser/
help.rs

1//! # help
2//!
3//! Generates help/version text based on given spec.
4//!
5
6#[cfg(test)]
7#[path = "./help_test.rs"]
8mod help_test;
9
10use crate::types::{
11    Argument, ArgumentHelp, ArgumentValueType, CliSpec, Command, PositionalArgument,
12};
13
14/// Generates and returns the spec version text
15pub(crate) fn version(spec: &CliSpec) -> String {
16    let mut buffer = String::new();
17
18    match spec.meta_info {
19        Some(ref meta_info) => {
20            if let Some(ref project) = meta_info.project {
21                buffer.push_str(&project);
22            }
23
24            if let Some(ref version) = meta_info.version {
25                if !buffer.is_empty() {
26                    buffer.push_str(" ");
27                }
28                buffer.push_str(&version);
29            }
30        }
31        None => (),
32    }
33
34    buffer
35}
36
37/// Generates and returns the spec help text
38pub(crate) fn help(spec: &CliSpec) -> String {
39    let mut buffer = version(spec);
40
41    match spec.meta_info {
42        Some(ref meta_info) => {
43            if let Some(ref author) = meta_info.author {
44                if !buffer.is_empty() {
45                    buffer.push_str("\n");
46                }
47                buffer.push_str(&author);
48            }
49
50            if let Some(ref description) = meta_info.description {
51                if !buffer.is_empty() {
52                    buffer.push_str("\n");
53                }
54                buffer.push_str(&description);
55            }
56        }
57        None => (),
58    }
59
60    if !buffer.is_empty() {
61        buffer.push_str("\n\n");
62    }
63
64    if append_usage_line(spec, &mut buffer) {
65        buffer.push_str("\n\n");
66    }
67    if append_args_line(spec, &mut buffer) {
68        buffer.push_str("\n\n");
69    }
70    if append_options_block(spec, &mut buffer) {
71        buffer.push_str("\n\n");
72    }
73
74    match spec.meta_info {
75        Some(ref meta_info) => match meta_info.help_post_text {
76            Some(ref text) => buffer.push_str(text),
77            None => (),
78        },
79        None => (),
80    }
81
82    buffer = buffer.trim().to_string();
83    buffer.push_str("\n");
84
85    buffer
86}
87
88fn append_usage_line(spec: &CliSpec, buffer: &mut String) -> bool {
89    if spec.command.is_empty() && spec.arguments.is_empty() && spec.positional_argument.is_none() {
90        return false;
91    }
92
93    buffer.push_str("USAGE:\n    ");
94    let mut sub_buffer = String::new();
95    let mut multiple = false;
96    let mut added_content = false;
97    for command in &spec.command {
98        if !sub_buffer.is_empty() {
99            sub_buffer.push_str(" | ");
100            multiple = true;
101            added_content = true;
102        }
103
104        match command {
105            Command::Command(value) => sub_buffer.push_str(value),
106            Command::SubCommand(values) => sub_buffer.push_str(values.join(" ").as_str()),
107        }
108    }
109    if multiple {
110        buffer.push_str("[");
111    }
112    buffer.push_str(&sub_buffer);
113    if multiple {
114        buffer.push_str("]");
115    }
116
117    if !spec.arguments.is_empty() {
118        if added_content {
119            buffer.push_str(" ");
120        }
121        added_content = true;
122        buffer.push_str("[OPTIONS]");
123    }
124
125    if let Some(ref positional_argument_spec) = spec.positional_argument {
126        let name = get_positional_argument_value_name(positional_argument_spec);
127
128        if added_content {
129            buffer.push_str(" ");
130        }
131        buffer.push_str("[--] [<");
132        buffer.push_str(&name);
133        buffer.push_str(">...]");
134    }
135
136    true
137}
138
139fn append_args_line(spec: &CliSpec, buffer: &mut String) -> bool {
140    if let Some(ref positional_argument_spec) = spec.positional_argument {
141        let name = get_positional_argument_value_name(positional_argument_spec);
142
143        buffer.push_str("ARGS:\n    <");
144        buffer.push_str(&name);
145        buffer.push_str(">");
146
147        if let Some(ref help) = positional_argument_spec.help {
148            buffer.push_str("    ");
149
150            match help {
151                ArgumentHelp::Text(text) => buffer.push_str(text),
152                ArgumentHelp::TextAndParam(text, _) => buffer.push_str(text),
153            }
154        }
155
156        true
157    } else {
158        false
159    }
160}
161
162fn append_options_block(spec: &CliSpec, buffer: &mut String) -> bool {
163    if !spec.arguments.is_empty() {
164        let mut names = vec![];
165        let mut max_width = 0;
166
167        for argument in &spec.arguments {
168            let mut sub_buffer = String::new();
169            sub_buffer.push_str("    ");
170            let mut added = false;
171            for key in &argument.key {
172                if added {
173                    sub_buffer.push_str(", ");
174                }
175                added = true;
176                sub_buffer.push_str(&key);
177            }
178
179            let value_name = get_argument_value_name(argument);
180            if let Some(name) = value_name {
181                sub_buffer.push_str(" <");
182                sub_buffer.push_str(&name);
183                sub_buffer.push_str(">");
184            }
185
186            let name_len = sub_buffer.len();
187            names.push(sub_buffer);
188            if max_width < name_len {
189                max_width = name_len;
190            }
191        }
192
193        buffer.push_str("OPTIONS:\n");
194
195        let mut index = 0;
196        let help_offset = max_width + 4;
197        for argument in &spec.arguments {
198            if index > 0 {
199                buffer.push_str("\n");
200            }
201
202            let help_text = match argument.help {
203                Some(ref help) => {
204                    let mut text = match help {
205                        ArgumentHelp::Text(ref text) => text.to_string(),
206                        ArgumentHelp::TextAndParam(ref text, _) => text.to_string(),
207                    };
208                    if !text.is_empty() {
209                        text.push_str(" ");
210                    }
211
212                    text
213                }
214                None => "".to_string(),
215            };
216
217            let default_text = match argument.value_type {
218                ArgumentValueType::None => "".to_string(),
219                _ => match argument.default_value {
220                    Some(ref value) => format!("[default: {}]", value),
221                    None => "".to_string(),
222                },
223            };
224
225            let line = format!(
226                "{:<help_offset$}{}{}",
227                &names[index],
228                &help_text,
229                &default_text,
230                help_offset = help_offset
231            );
232            buffer.push_str(&line.trim_end());
233            index = index + 1;
234        }
235
236        true
237    } else {
238        false
239    }
240}
241
242fn get_positional_argument_value_name(positional_argument_spec: &PositionalArgument) -> String {
243    match positional_argument_spec.help {
244        Some(ref help) => match help {
245            ArgumentHelp::TextAndParam(_, value) => value.to_string(),
246            ArgumentHelp::Text(_) => positional_argument_spec.name.clone(),
247        },
248        None => positional_argument_spec.name.clone(),
249    }
250}
251
252fn get_argument_value_name(argument_spec: &Argument) -> Option<String> {
253    if argument_spec.value_type == ArgumentValueType::None {
254        None
255    } else {
256        let name = match argument_spec.help {
257            Some(ref help) => match help {
258                ArgumentHelp::TextAndParam(_, value) => value.to_string(),
259                ArgumentHelp::Text(_) => argument_spec.name.clone(),
260            },
261            None => argument_spec.name.clone(),
262        };
263
264        Some(name)
265    }
266}