View Javadoc
1   /*
2     Licensed to the Apache Software Foundation (ASF) under one or more
3     contributor license agreements.  See the NOTICE file distributed with
4     this work for additional information regarding copyright ownership.
5     The ASF licenses this file to You under the Apache License, Version 2.0
6     (the "License"); you may not use this file except in compliance with
7     the License.  You may obtain a copy of the License at
8   
9         https://www.apache.org/licenses/LICENSE-2.0
10  
11    Unless required by applicable law or agreed to in writing, software
12    distributed under the License is distributed on an "AS IS" BASIS,
13    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14    See the License for the specific language governing permissions and
15    limitations under the License.
16   */
17  package org.apache.commons.cli.help;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertThrows;
21  import static org.junit.jupiter.api.Assertions.assertTrue;
22  
23  import java.io.IOException;
24  import java.io.StringReader;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.List;
28  
29  import org.apache.commons.cli.Option;
30  import org.apache.commons.cli.OptionGroup;
31  import org.apache.commons.cli.Options;
32  import org.apache.commons.cli.example.XhtmlHelpAppendable;
33  import org.apache.commons.io.IOUtils;
34  import org.junit.jupiter.api.Test;
35  
36  /**
37   * Tests {@link HelpFormatter}.
38   */
39  class HelpFormatterTest {
40  
41      private Options getTestGroups() {
42          // @formatter:off
43          return new Options()
44              .addOptionGroup(new OptionGroup()
45                  .addOption(Option.builder("1").longOpt("one").hasArg().desc("English one").get())
46                  .addOption(Option.builder().longOpt("aon").hasArg().desc("Irish one").get())
47                  .addOption(Option.builder().longOpt("uno").hasArg().desc("Spanish one").get())
48              )
49              .addOptionGroup(new OptionGroup()
50                  .addOption(Option.builder().longOpt("two").hasArg().desc("English two").get())
51                  .addOption(Option.builder().longOpt("dó").hasArg().desc("Irish twp").get())
52                  .addOption(Option.builder().longOpt("dos").hasArg().desc("Spanish two").get())
53              )
54              .addOptionGroup(new OptionGroup()
55                  .addOption(Option.builder().longOpt("three").hasArg().desc("English three").get())
56                  .addOption(Option.builder().longOpt("trí").hasArg().desc("Irish three").get())
57                  .addOption(Option.builder().longOpt("tres").hasArg().desc("Spanish three").get())
58              );
59          // @formatter:on
60      }
61  
62      @Test
63      void testDefault() {
64          final StringBuilder sb = new StringBuilder();
65          final TextHelpAppendable serializer = new TextHelpAppendable(sb);
66          final HelpFormatter formatter = HelpFormatter.builder().setHelpAppendable(serializer).get();
67          assertEquals(serializer, formatter.getSerializer(), "Unexpected helpAppendable tests may fail unexpectedly");
68          assertEquals(AbstractHelpFormatter.DEFAULT_COMPARATOR, formatter.getComparator(), "Unexpected comparator tests may fail unexpectedly");
69          assertEquals(AbstractHelpFormatter.DEFAULT_SYNTAX_PREFIX, formatter.getSyntaxPrefix(), "Unexpected syntax prefix tests may fail unexpectedly");
70      }
71  
72      @Test
73      void testPrintHelp() throws IOException {
74          final StringBuilder sb = new StringBuilder();
75          final TextHelpAppendable serializer = new TextHelpAppendable(sb);
76          HelpFormatter formatter = HelpFormatter.builder().setHelpAppendable(serializer).get();
77  
78          final Options options = new Options().addOption(Option.builder("a").since("1853").hasArg().desc("aaaa aaaa aaaa aaaa aaaa").get());
79  
80          List<String> expected = new ArrayList<>();
81          expected.add(" usage:  commandSyntax [-a <arg>]");
82          expected.add("");
83          expected.add(" header");
84          expected.add("");
85          expected.add(" Options      Since           Description       ");
86          expected.add(" -a <arg>     1853      aaaa aaaa aaaa aaaa aaaa");
87          expected.add("");
88          expected.add(" footer");
89          expected.add("");
90  
91          formatter.printHelp("commandSyntax", "header", options, "footer", true);
92          List<String> actual = IOUtils.readLines(new StringReader(sb.toString()));
93          assertEquals(expected, actual);
94  
95          formatter = HelpFormatter.builder().setShowSince(false).setHelpAppendable(serializer).get();
96          expected = new ArrayList<>();
97          expected.add(" usage:  commandSyntax [-a <arg>]");
98          expected.add("");
99          expected.add(" header");
100         expected.add("");
101         expected.add(" Options            Description       ");
102         expected.add(" -a <arg>     aaaa aaaa aaaa aaaa aaaa");
103         expected.add("");
104         expected.add(" footer");
105         expected.add("");
106 
107         sb.setLength(0);
108         formatter.printHelp("commandSyntax", "header", options, "footer", true);
109         actual = IOUtils.readLines(new StringReader(sb.toString()));
110         assertEquals(expected, actual);
111 
112         sb.setLength(0);
113         formatter.printHelp("commandSyntax", "header", options, "footer", false);
114         expected.set(0, " usage:  commandSyntax");
115         actual = IOUtils.readLines(new StringReader(sb.toString()));
116         assertEquals(expected, actual);
117 
118         sb.setLength(0);
119         formatter.printHelp("commandSyntax", "", options, "footer", false);
120         expected.remove(3);
121         expected.remove(2);
122         actual = IOUtils.readLines(new StringReader(sb.toString()));
123         assertEquals(expected, actual);
124 
125         sb.setLength(0);
126         formatter.printHelp("commandSyntax", null, options, "footer", false);
127         actual = IOUtils.readLines(new StringReader(sb.toString()));
128         assertEquals(expected, actual);
129 
130         sb.setLength(0);
131         formatter.printHelp("commandSyntax", null, options, "", false);
132         expected.remove(6);
133         expected.remove(5);
134         actual = IOUtils.readLines(new StringReader(sb.toString()));
135         assertEquals(expected, actual);
136 
137         sb.setLength(0);
138         formatter.printHelp("commandSyntax", null, options, null, false);
139         actual = IOUtils.readLines(new StringReader(sb.toString()));
140         assertEquals(expected, actual);
141 
142         sb.setLength(0);
143         final HelpFormatter fHelp = formatter;
144         assertThrows(IllegalArgumentException.class, () -> fHelp.printHelp("", "header", options, "footer", true));
145         assertEquals(0, sb.length(), "Should not write to output");
146         assertThrows(IllegalArgumentException.class, () -> fHelp.printHelp(null, "header", options, "footer", true));
147         assertEquals(0, sb.length(), "Should not write to output");
148     }
149 
150     /**
151      * Tests example from the mailing list that caused an infinite loop.
152      *
153      * @see <a href="https://issues.apache.org/jira/browse/CLI-351">[CLI-351] Multiple traililng BREAK_CHAR_SET characters cause infinite loop in
154      *      HelpFormatter</a>
155      */
156     @Test
157     void testPrintHelpHeader() throws IOException {
158         HelpFormatter.builder().get().printHelp("CL syntax", "Header", Collections.emptyList(), "Footer", true);
159         HelpFormatter.builder().get().printHelp("CL syntax", "Header\n\n", // This makes printHelp enter into an infinite loop
160                 Collections.emptyList(), "Footer", true);
161     }
162 
163     @Test
164     public void testPrintHelpWithIterableOptions() throws IOException {
165         final StringBuilder sb = new StringBuilder();
166         final TextHelpAppendable serializer = new TextHelpAppendable(sb);
167         HelpFormatter formatter = HelpFormatter.builder().setHelpAppendable(serializer).get();
168 
169         final List<Option> options = new ArrayList<>();
170         options.add(Option.builder("a").since("1853").hasArg().desc("aaaa aaaa aaaa aaaa aaaa").build());
171 
172         List<String> expected = new ArrayList<>();
173         expected.add(" usage:  commandSyntax [-a <arg>]");
174         expected.add("");
175         expected.add(" header");
176         expected.add("");
177         expected.add(" Options      Since           Description       ");
178         expected.add(" -a <arg>     1853      aaaa aaaa aaaa aaaa aaaa");
179         expected.add("");
180         expected.add(" footer");
181         expected.add("");
182 
183         formatter.printHelp("commandSyntax", "header", options, "footer", true);
184         List<String> actual = IOUtils.readLines(new StringReader(sb.toString()));
185         assertEquals(expected, actual);
186 
187         formatter = HelpFormatter.builder().setShowSince(false).setHelpAppendable(serializer).get();
188         expected = new ArrayList<>();
189         expected.add(" usage:  commandSyntax [-a <arg>]");
190         expected.add("");
191         expected.add(" header");
192         expected.add("");
193         expected.add(" Options            Description       ");
194         expected.add(" -a <arg>     aaaa aaaa aaaa aaaa aaaa");
195         expected.add("");
196         expected.add(" footer");
197         expected.add("");
198 
199         sb.setLength(0);
200         formatter.printHelp("commandSyntax", "header", options, "footer", true);
201         actual = IOUtils.readLines(new StringReader(sb.toString()));
202         assertEquals(expected, actual);
203 
204         sb.setLength(0);
205         formatter.printHelp("commandSyntax", "header", options, "footer", false);
206         expected.set(0, " usage:  commandSyntax");
207         actual = IOUtils.readLines(new StringReader(sb.toString()));
208         assertEquals(expected, actual);
209 
210         sb.setLength(0);
211         formatter.printHelp("commandSyntax", "", options, "footer", false);
212         expected.remove(3);
213         expected.remove(2);
214         actual = IOUtils.readLines(new StringReader(sb.toString()));
215         assertEquals(expected, actual);
216 
217         sb.setLength(0);
218         formatter.printHelp("commandSyntax", null, options, "footer", false);
219         actual = IOUtils.readLines(new StringReader(sb.toString()));
220         assertEquals(expected, actual);
221 
222         sb.setLength(0);
223         formatter.printHelp("commandSyntax", null, options, "", false);
224         expected.remove(6);
225         expected.remove(5);
226         actual = IOUtils.readLines(new StringReader(sb.toString()));
227         assertEquals(expected, actual);
228 
229         sb.setLength(0);
230         formatter.printHelp("commandSyntax", null, options, null, false);
231         actual = IOUtils.readLines(new StringReader(sb.toString()));
232         assertEquals(expected, actual);
233 
234         sb.setLength(0);
235         final HelpFormatter fHelp = formatter;
236         assertThrows(IllegalArgumentException.class, () -> fHelp.printHelp("", "header", options, "footer", true));
237         assertEquals(0, sb.length(), "Should not write to output");
238         assertThrows(IllegalArgumentException.class, () -> fHelp.printHelp(null, "header", options, "footer", true));
239         assertEquals(0, sb.length(), "Should not write to output");
240     }
241 
242     @Test
243     void testPrintHelpXML() throws IOException {
244         final StringBuilder sb = new StringBuilder();
245         final XhtmlHelpAppendable serializer = new XhtmlHelpAppendable(sb);
246         final HelpFormatter formatter = HelpFormatter.builder().setHelpAppendable(serializer).get();
247 
248         final Options options = new Options().addOption("a", false, "aaaa aaaa aaaa aaaa aaaa");
249 
250         final List<String> expected = new ArrayList<>();
251         expected.add("<p>usage:  commandSyntax [-a]</p>");
252         expected.add("<p>header</p>");
253         expected.add("<table class='commons_cli_table'>");
254         expected.add("  <tr>");
255         expected.add("    <th>Options</th>");
256         expected.add("    <th>Since</th>");
257         expected.add("    <th>Description</th>");
258         expected.add("  </tr>");
259         expected.add("  <tr>");
260         expected.add("    <td>-a</td>");
261         expected.add("    <td>--</td>");
262         expected.add("    <td>aaaa aaaa aaaa aaaa aaaa</td>");
263         expected.add("  </tr>");
264         expected.add("</table>");
265         expected.add("<p>footer</p>");
266 
267         formatter.printHelp("commandSyntax", "header", options, "footer", true);
268         final List<String> actual = IOUtils.readLines(new StringReader(sb.toString()));
269 
270         assertEquals(expected, actual);
271     }
272 
273     @Test
274     void testPrintOptions() throws IOException {
275         final StringBuilder sb = new StringBuilder();
276         final TextHelpAppendable serializer = new TextHelpAppendable(sb);
277         final HelpFormatter formatter = HelpFormatter.builder().setHelpAppendable(serializer).setShowSince(false).get();
278 
279         // help format default column styles
280         // col options description helpAppendable
281         // styl FIXED VARIABLE VARIABLE
282         // LPad 0 5 1
283         // indent 1 1 3
284         //
285         // default helpAppendable
286 
287         Options options;
288         List<String> expected = new ArrayList<>();
289         expected.add(" Options           Description       ");
290         expected.add(" -a          aaaa aaaa aaaa aaaa aaaa");
291         expected.add("");
292 
293         options = new Options().addOption("a", false, "aaaa aaaa aaaa aaaa aaaa");
294 
295         formatter.printOptions(options);
296         List<String> actual = IOUtils.readLines(new StringReader(sb.toString()));
297         assertEquals(expected, actual);
298 
299         sb.setLength(0);
300         serializer.setMaxWidth(30);
301         expected = new ArrayList<>();
302         expected.add(" Options        Description    ");
303         expected.add(" -a          aaaa aaaa aaaa    ");
304         expected.add("              aaaa aaaa        ");
305         expected.add("");
306         formatter.printOptions(options);
307         actual = IOUtils.readLines(new StringReader(sb.toString()));
308         assertEquals(31, actual.get(0).length());
309         assertEquals(expected, actual);
310 
311         sb.setLength(0);
312         serializer.setLeftPad(5);
313         expected = new ArrayList<>();
314         expected.add("     Options        Description    ");
315         expected.add("     -a          aaaa aaaa aaaa    ");
316         expected.add("                  aaaa aaaa        ");
317         expected.add("");
318         formatter.printOptions(options);
319         actual = IOUtils.readLines(new StringReader(sb.toString()));
320         assertEquals(expected, actual);
321     }
322 
323     @Test
324     void testSetOptionFormatBuilderTest() {
325         final HelpFormatter.Builder underTest = HelpFormatter.builder();
326         final OptionFormatter.Builder ofBuilder = OptionFormatter.builder().setOptPrefix("Just Another ");
327         underTest.setOptionFormatBuilder(ofBuilder);
328         final HelpFormatter formatter = underTest.get();
329         final OptionFormatter oFormatter = formatter.getOptionFormatter(Option.builder("thing").get());
330         assertEquals("Just Another thing", oFormatter.getOpt());
331 
332     }
333 
334     @Test
335     void testSetOptionGroupSeparatorTest() {
336         final HelpFormatter.Builder underTest = HelpFormatter.builder().setOptionGroupSeparator(" and ");
337         final HelpFormatter formatter = underTest.get();
338         final String result = formatter.toSyntaxOptions(new OptionGroup().addOption(Option.builder("this").get()).addOption(Option.builder("that").get()));
339         assertTrue(result.contains("-that and -this"));
340     }
341 
342     @Test
343     void testSortOptionGroupsTest() {
344         final Options options = getTestGroups();
345         final List<Option> optList = new ArrayList<>(options.getOptions());
346         final HelpFormatter underTest = HelpFormatter.builder().get();
347         final List<Option> expected = new ArrayList<>();
348         expected.add(optList.get(0)); // because 1 sorts before all long values
349         expected.add(optList.get(1));
350         expected.add(optList.get(5));
351         expected.add(optList.get(4));
352         expected.add(optList.get(6));
353         expected.add(optList.get(8));
354         expected.add(optList.get(7));
355         expected.add(optList.get(3));
356         expected.add(optList.get(2));
357         assertEquals(expected, underTest.sort(options));
358     }
359 
360     @Test
361     void testSortOptionsTest() {
362         // @formatter:off
363         final Options options = new Options()
364             .addOption(Option.builder("a").longOpt("optA").hasArg().desc("The description of A").get())
365             .addOption(Option.builder("b").longOpt("BOpt").hasArg().desc("B description").get())
366             .addOption(Option.builder().longOpt("COpt").hasArg().desc("A COpt description").get());
367         // @formatter:on
368 
369         HelpFormatter underTest = HelpFormatter.builder().get();
370         final List<Option> expected = new ArrayList<>();
371         expected.add(options.getOption("a"));
372         expected.add(options.getOption("b"));
373         expected.add(options.getOption("COpt"));
374 
375         assertEquals(expected, underTest.sort(options));
376 
377         expected.set(0, expected.get(2));
378         expected.set(2, options.getOption("a"));
379         underTest = HelpFormatter.builder().setComparator(AbstractHelpFormatter.DEFAULT_COMPARATOR.reversed()).get();
380         assertEquals(expected, underTest.sort(options));
381 
382         assertEquals(0, underTest.sort(Collections.emptyList()).size(), "empty colleciton should return empty list");
383         assertEquals(0, underTest.sort((Iterable<Option>) null).size(), "null iterable should return empty list");
384         assertEquals(0, underTest.sort((Options) null).size(), "null Options should return empty list");
385     }
386 
387     @Test
388     void testSyntaxPrefix() {
389         final StringBuilder sb = new StringBuilder();
390         final TextHelpAppendable serializer = new TextHelpAppendable(sb);
391         final HelpFormatter formatter = HelpFormatter.builder().setHelpAppendable(serializer).get();
392         formatter.setSyntaxPrefix("Something new");
393         assertEquals("Something new", formatter.getSyntaxPrefix());
394         assertEquals(0, sb.length(), "Should not write to output");
395     }
396 
397     @Test
398     void testToArgNameTest() {
399         final StringBuilder sb = new StringBuilder();
400         final TextHelpAppendable serializer = new TextHelpAppendable(sb);
401         final HelpFormatter formatter = HelpFormatter.builder().setHelpAppendable(serializer).get();
402 
403         assertEquals("<some Arg>", formatter.toArgName("some Arg"));
404         assertEquals("<>", formatter.toArgName(""));
405         assertEquals("<>", formatter.toArgName(null));
406     }
407 
408     @Test
409     void testToSyntaxOptionGroupTest() {
410         final HelpFormatter underTest = HelpFormatter.builder().get();
411         // @formatter:off
412         final OptionGroup optionGroup = new OptionGroup()
413             .addOption(Option.builder().option("o").longOpt("one").hasArg().get())
414             .addOption(Option.builder().option("t").longOpt("two").hasArg().required().argName("other").get())
415             .addOption(Option.builder().option("th").longOpt("three").required().argName("other").get())
416             .addOption(Option.builder().option("f").argName("other").get())
417             .addOption(Option.builder().longOpt("five").hasArg().argName("other").get())
418             .addOption(Option.builder().longOpt("six").required().hasArg().argName("other").get())
419             .addOption(Option.builder().option("s").longOpt("sevem").hasArg().get());
420         // @formatter:on
421         assertEquals("[-f | --five <other> | -o <arg> | -s <arg> | --six <other> | -t <other> | -th]", underTest.toSyntaxOptions(optionGroup));
422 
423         optionGroup.setRequired(true);
424         assertEquals("-f | --five <other> | -o <arg> | -s <arg> | --six <other> | -t <other> | -th", underTest.toSyntaxOptions(optionGroup));
425 
426         assertEquals("", underTest.toSyntaxOptions(new OptionGroup()), "empty group should return empty string");
427     }
428 
429     @Test
430     void testToSyntaxOptionIterableTest() {
431         final HelpFormatter underTest = HelpFormatter.builder().get();
432         final List<Option> options = new ArrayList<>();
433 
434         options.add(Option.builder().option("o").longOpt("one").hasArg().get());
435         options.add(Option.builder().option("t").longOpt("two").hasArg().required().argName("other").get());
436         options.add(Option.builder().option("th").longOpt("three").required().argName("other").get());
437         options.add(Option.builder().option("f").argName("other").get());
438         options.add(Option.builder().longOpt("five").hasArg().argName("other").get());
439         options.add(Option.builder().longOpt("six").required().hasArg().argName("other").get());
440         options.add(Option.builder().option("s").longOpt("sevem").hasArg().get());
441         assertEquals("[-f] [--five <other>] [-o <arg>] [-s <arg>] --six <other> -t <other> -th", underTest.toSyntaxOptions(options));
442 
443     }
444 
445     @Test
446     void testToSyntaxOptionOptionsTest() {
447         final HelpFormatter underTest = HelpFormatter.builder().get();
448         Options options = getTestGroups();
449         assertEquals("[-1 <arg> | --aon <arg> | --uno <arg>] [--dos <arg> | --dó <arg> | --two <arg>] [--three <arg> | --tres <arg> | --trí <arg>]",
450                 underTest.toSyntaxOptions(options), "getTestGroup options failed");
451 
452         // @formatter:off
453         options = new Options()
454             .addOption(Option.builder().option("o").longOpt("one").hasArg().get())
455             .addOption(Option.builder().option("t").longOpt("two").hasArg().required().argName("other").get())
456             .addOption(Option.builder().option("th").longOpt("three").required().argName("other").get())
457             .addOption(Option.builder().option("f").argName("other").get())
458             .addOption(Option.builder().longOpt("five").hasArg().argName("other").get())
459             .addOption(Option.builder().longOpt("six").required().hasArg().argName("other").get())
460             .addOption(Option.builder().option("s").longOpt("seven").hasArg().get());
461         // @formatter:on
462         assertEquals("[-f] [--five <other>] [-o <arg>] [-s <arg>] --six <other> -t <other> -th", underTest.toSyntaxOptions(options), "assorted options failed");
463         // @formatter:off
464         options = new Options()
465             .addOption(Option.builder().option("o").longOpt("one").hasArg().get())
466             .addOptionGroup(
467                 new OptionGroup()
468                     .addOption(Option.builder().option("t").longOpt("two").hasArg().required().argName("other").get())
469                     .addOption(Option.builder().option("th").longOpt("three").required().argName("other").get()))
470             .addOption(Option.builder().option("f").argName("other").get())
471             .addOption(Option.builder().longOpt("five").hasArg().argName("other").get())
472             .addOption(Option.builder().longOpt("six").required().hasArg().argName("other").get())
473             .addOption(Option.builder().option("s").longOpt("seven").hasArg().get());
474         // @formatter:on
475         assertEquals("[-f] [--five <other>] [-o <arg>] [-s <arg>] --six <other> [-t <other> | -th]", underTest.toSyntaxOptions(options),
476                 "option with group failed");
477 
478         // @formatter:off
479         final OptionGroup group1 = new OptionGroup()
480             .addOption(Option.builder().option("t").longOpt("two").hasArg().required().argName("other").get())
481             .addOption(Option.builder().option("th").longOpt("three").required().argName("other").get());
482         // @formatter:on
483         group1.setRequired(true);
484         // @formatter:off
485         options = new Options()
486             .addOption(Option.builder().option("o").longOpt("one").hasArg().get())
487             .addOptionGroup(group1)
488             .addOption(Option.builder().option("f").argName("other").get())
489             .addOption(Option.builder().longOpt("five").hasArg().argName("other").get())
490             .addOption(Option.builder().longOpt("six").required().hasArg().argName("other").get())
491             .addOption(Option.builder().option("s").longOpt("seven").hasArg().get());
492         // @formatter:on
493         assertEquals("[-f] [--five <other>] [-o <arg>] [-s <arg>] --six <other> -t <other> | -th", underTest.toSyntaxOptions(options),
494                 "options with required group failed");
495     }
496 
497     @Test
498     void verifyOptionGroupingOutput() throws IOException {
499          // create options and groups
500          final Option o1 = new Option("o1", "Descr");
501          final Option o2 = new Option("o2", "Descr");
502 
503          final Options options = new Options();
504          options.addOption(o1);
505          options.addOption(o2);
506 
507          final OptionGroup group = new OptionGroup();
508          group.addOption(o1);
509          group.addOption(o2);
510          options.addOptionGroup(group);
511 
512          final StringBuilder output = new StringBuilder();
513          //format options with new formatter
514          final org.apache.commons.cli.help.HelpFormatter newFormatter =
515                  org.apache.commons.cli.help.HelpFormatter.builder().setShowSince(false)
516         .setHelpAppendable(new TextHelpAppendable(output)).get();
517          newFormatter.printHelp("Command", null, options, null, true);
518          assertTrue(output.toString().contains("[-o1 | -o2]"));
519      }
520 
521 }