1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.cli;
19
20 import static org.junit.jupiter.api.Assertions.assertEquals;
21 import static org.junit.jupiter.api.Assertions.assertNull;
22 import static org.junit.jupiter.api.Assertions.assertThrows;
23
24 import java.io.ByteArrayOutputStream;
25 import java.io.PrintWriter;
26 import java.io.StringWriter;
27
28 import org.apache.commons.cli.HelpFormatter.Builder;
29 import org.junit.jupiter.api.Test;
30
31
32
33
34 public class HelpFormatterTest {
35 private static final String EOL = System.lineSeparator();
36
37 @Test
38 public void testAccessors() {
39 final HelpFormatter formatter = new HelpFormatter();
40
41 formatter.setArgName("argname");
42 assertEquals("argname", formatter.getArgName(), "arg name");
43
44 formatter.setDescPadding(3);
45 assertEquals(3, formatter.getDescPadding(), "desc padding");
46
47 formatter.setLeftPadding(7);
48 assertEquals(7, formatter.getLeftPadding(), "left padding");
49
50 formatter.setLongOptPrefix("~~");
51 assertEquals("~~", formatter.getLongOptPrefix(), "long opt prefix");
52
53 formatter.setNewLine("\n");
54 assertEquals("\n", formatter.getNewLine(), "new line");
55
56 formatter.setOptPrefix("~");
57 assertEquals("~", formatter.getOptPrefix(), "opt prefix");
58
59 formatter.setSyntaxPrefix("-> ");
60 assertEquals("-> ", formatter.getSyntaxPrefix(), "syntax prefix");
61
62 formatter.setWidth(80);
63 assertEquals(80, formatter.getWidth(), "width");
64 }
65
66 @Test
67 public void testAutomaticUsage() {
68 final HelpFormatter hf = new HelpFormatter();
69 Options options;
70 String expected = "usage: app [-a]";
71 final ByteArrayOutputStream out = new ByteArrayOutputStream();
72 final PrintWriter pw = new PrintWriter(out);
73
74 options = new Options().addOption("a", false, "aaaa aaaa aaaa aaaa aaaa");
75 hf.printUsage(pw, 60, "app", options);
76 pw.flush();
77 assertEquals(expected, out.toString().trim(), "simple auto usage");
78 out.reset();
79
80 expected = "usage: app [-a] [-b]";
81 options = new Options().addOption("a", false, "aaaa aaaa aaaa aaaa aaaa").addOption("b", false, "bbb");
82 hf.printUsage(pw, 60, "app", options);
83 pw.flush();
84 assertEquals(expected, out.toString().trim(), "simple auto usage");
85 out.reset();
86 }
87
88 @Test
89 public void testDefaultArgName() {
90 final Option option = Option.builder("f").hasArg().required(true).build();
91
92 final Options options = new Options();
93 options.addOption(option);
94
95 final StringWriter out = new StringWriter();
96
97 final HelpFormatter formatter = new HelpFormatter();
98 formatter.setArgName("argument");
99 formatter.printUsage(new PrintWriter(out), 80, "app", options);
100
101 assertEquals("usage: app -f <argument>" + EOL, out.toString());
102 }
103
104 @Test
105 public void testFindWrapPos() {
106 final HelpFormatter hf = new HelpFormatter();
107
108 String text = "This is a test.";
109
110 assertEquals(7, hf.findWrapPos(text, 8, 0), "wrap position");
111
112
113 assertEquals(-1, hf.findWrapPos(text, 8, 8), "wrap position 2");
114
115
116 text = "aaaa aa";
117 assertEquals(3, hf.findWrapPos(text, 3, 0), "wrap position 3");
118
119
120 text = "aaaaaa aaaaaa";
121 assertEquals(6, hf.findWrapPos(text, 6, 0), "wrap position 4");
122 assertEquals(-1, hf.findWrapPos(text, 6, 7), "wrap position 4");
123
124 text = "aaaaaa\n aaaaaa";
125 assertEquals(7, hf.findWrapPos(text, 6, 0), "wrap position 5");
126
127 text = "aaaaaa\t aaaaaa";
128 assertEquals(7, hf.findWrapPos(text, 6, 0), "wrap position 6");
129 }
130
131 @Test
132 public void testHeaderStartingWithLineSeparator0() {
133
134 final Options options = new Options();
135 final HelpFormatter formatter = new HelpFormatter();
136 final String header = EOL + "Header";
137 final String footer = "Footer";
138 final StringWriter out = new StringWriter();
139 formatter.printHelp(new PrintWriter(out), 80, "foobar", header, options, 2, 2, footer, true);
140
141 assertEquals(
142 "usage: foobar" + EOL +
143 "" + EOL +
144 "Header" + EOL +
145 "" + EOL +
146 "Footer" + EOL,
147 out.toString());
148
149 }
150
151 @Test
152 public void testHeaderStartingWithLineSeparator1() {
153
154 final Options options = new Options();
155 final String header = EOL + "Header";
156 final String footer = "Footer";
157 final Builder builder = HelpFormatter.builder();
158 StringWriter out = new StringWriter();
159 builder.setPrintWriter(new PrintWriter(out)).get().printHelp(new PrintWriter(out), 80, "foobar", header, options, 2, 2, footer, true);
160
161 assertEquals(
162 "usage: foobar" + EOL +
163 "" + EOL +
164 "Header" + EOL +
165 "" + EOL +
166 "Footer" + EOL,
167 out.toString());
168
169 out = new StringWriter();
170 builder.setPrintWriter(new PrintWriter(out)).get().printHelp("foobar", header, options, footer);
171
172 assertEquals(
173 "usage: foobar" + EOL +
174 "" + EOL +
175 "Header" + EOL +
176 "" + EOL +
177 "Footer" + EOL,
178 out.toString());
179
180 out = new StringWriter();
181 builder.setPrintWriter(new PrintWriter(out)).get().printHelp(80, "foobar", header, options, footer);
182
183 assertEquals(
184 "usage: foobar" + EOL +
185 "" + EOL +
186 "Header" + EOL +
187 "" + EOL +
188 "Footer" + EOL,
189 out.toString());
190
191 out = new StringWriter();
192 builder.setPrintWriter(new PrintWriter(out)).get().printHelp("foobar", header, options, footer, false);
193
194 assertEquals(
195 "usage: foobar" + EOL +
196 "" + EOL +
197 "Header" + EOL +
198 "" + EOL +
199 "Footer" + EOL,
200 out.toString());
201
202 out = new StringWriter();
203 builder.setPrintWriter(new PrintWriter(out)).get().printHelp("foobar", header, options, footer, true);
204
205 assertEquals(
206 "usage: foobar" + EOL +
207 "" + EOL +
208 "Header" + EOL +
209 "" + EOL +
210 "Footer" + EOL,
211 out.toString());
212
213 out = new StringWriter();
214 builder.setPrintWriter(new PrintWriter(out)).get().printHelp("foobar", options, false);
215
216 assertEquals(
217 "usage: foobar" + EOL +
218 "" + EOL,
219 out.toString());
220
221 }
222
223 @Test
224 public void testHelpWithLongOptSeparator() {
225 final Options options = new Options();
226 options.addOption("f", true, "the file");
227 options.addOption(Option.builder("s").longOpt("size").desc("the size").hasArg().argName("SIZE").build());
228 options.addOption(Option.builder().longOpt("age").desc("the age").hasArg().build());
229
230 final HelpFormatter formatter = new HelpFormatter();
231 assertEquals(HelpFormatter.DEFAULT_LONG_OPT_SEPARATOR, formatter.getLongOptSeparator());
232 formatter.setLongOptSeparator("=");
233 assertEquals("=", formatter.getLongOptSeparator());
234
235 final StringWriter out = new StringWriter();
236
237 formatter.printHelp(new PrintWriter(out), 80, "create", "header", options, 2, 2, "footer");
238
239
240 assertEquals(
241 "usage: create" + EOL +
242 "header" + EOL +
243 " --age=<arg> the age" + EOL +
244 " -f <arg> the file" + EOL +
245 " -s,--size=<SIZE> the size" + EOL +
246 "footer" + EOL,
247 out.toString());
248
249 }
250
251 @Test
252 public void testIndentedHeaderAndFooter() {
253
254 final Options options = new Options();
255 final HelpFormatter formatter = new HelpFormatter();
256 final String header = " Header1\n Header2";
257 final String footer = " Footer1\n Footer2";
258 final StringWriter out = new StringWriter();
259 formatter.printHelp(new PrintWriter(out), 80, "foobar", header, options, 2, 2, footer, true);
260
261 assertEquals(
262 "usage: foobar" + EOL +
263 " Header1" + EOL +
264 " Header2" + EOL +
265 "" + EOL +
266 " Footer1" + EOL +
267 " Footer2" + EOL,
268 out.toString());
269
270 }
271
272 @Test
273 public void testOptionWithoutShortFormat() {
274
275 final Options options = new Options();
276 options.addOption(new Option("a", "aaa", false, "aaaaaaa"));
277 options.addOption(new Option(null, "bbb", false, "bbbbbbb"));
278 options.addOption(new Option("c", null, false, "ccccccc"));
279
280 final HelpFormatter formatter = new HelpFormatter();
281 final StringWriter out = new StringWriter();
282 formatter.printHelp(new PrintWriter(out), 80, "foobar", "", options, 2, 2, "", true);
283
284 assertEquals(
285 "usage: foobar [-a] [--bbb] [-c]" + EOL +
286 " -a,--aaa aaaaaaa" + EOL +
287 " --bbb bbbbbbb" + EOL +
288 " -c ccccccc" + EOL,
289 out.toString());
290
291 }
292
293 @Test
294 public void testOptionWithoutShortFormat2() {
295
296 final Option help = new Option("h", "help", false, "print this message");
297 final Option version = new Option("v", "version", false, "print version information");
298 final Option newRun = new Option("n", "new", false, "Create NLT cache entries only for new items");
299 final Option trackerRun = new Option("t", "tracker", false, "Create NLT cache entries only for tracker items");
300
301 final Option timeLimit = Option.builder("l")
302 .longOpt("limit")
303 .hasArg()
304 .valueSeparator()
305 .desc("Set time limit for execution, in mintues")
306 .build();
307 final Option age = Option.builder("a").longOpt("age")
308 .hasArg()
309 .valueSeparator()
310 .desc("Age (in days) of cache item before being recomputed")
311 .build();
312 final Option server = Option.builder("s").longOpt("server")
313 .hasArg()
314 .valueSeparator()
315 .desc("The NLT server address")
316 .build();
317 final Option numResults = Option.builder("r").longOpt("results")
318 .hasArg()
319 .valueSeparator()
320 .desc("Number of results per item")
321 .build();
322 final Option configFile = Option.builder().longOpt("config")
323 .hasArg()
324 .valueSeparator()
325 .desc("Use the specified configuration file")
326 .build();
327
328
329 final Options mOptions = new Options();
330 mOptions.addOption(help);
331 mOptions.addOption(version);
332 mOptions.addOption(newRun);
333 mOptions.addOption(trackerRun);
334 mOptions.addOption(timeLimit);
335 mOptions.addOption(age);
336 mOptions.addOption(server);
337 mOptions.addOption(numResults);
338 mOptions.addOption(configFile);
339
340 final HelpFormatter formatter = new HelpFormatter();
341 final String eol = System.lineSeparator();
342 final StringWriter out = new StringWriter();
343 formatter.printHelp(new PrintWriter(out), 80, "commandline", "header", mOptions, 2, 2, "footer", true);
344
345 assertEquals(
346 "usage: commandline [-a <arg>] [--config <arg>] [-h] [-l <arg>] [-n] [-r <arg>]" + eol +
347 " [-s <arg>] [-t] [-v]" + eol +
348 "header" + eol +
349 " -a,--age <arg> Age (in days) of cache item before being recomputed" + eol +
350 " --config <arg> Use the specified configuration file" + eol +
351 " -h,--help print this message" + eol +
352 " -l,--limit <arg> Set time limit for execution, in mintues" + eol +
353 " -n,--new Create NLT cache entries only for new items" + eol +
354 " -r,--results <arg> Number of results per item" + eol +
355 " -s,--server <arg> The NLT server address" + eol +
356 " -t,--tracker Create NLT cache entries only for tracker items" + eol +
357 " -v,--version print version information" + eol +
358 "footer" + eol,
359 out.toString());
360
361 }
362
363 @Test
364 public void testPrintHelpNewlineFooter() {
365 final HelpFormatter formatter = new HelpFormatter();
366 final ByteArrayOutputStream out = new ByteArrayOutputStream();
367 final PrintWriter pw = new PrintWriter(out);
368
369 final Options options = new Options();
370 options.addOption("a", "b");
371
372 formatter.printHelp(
373 pw,
374 80,
375 "test" + EOL,
376 "header" + EOL,
377 options,
378 0,
379 0,
380 EOL
381 );
382 final String expected = "usage: test" + EOL +
383 "header" + EOL +
384 "-ab" + EOL +
385 EOL;
386 pw.flush();
387 assertEquals(expected, out.toString(), "footer newline");
388 }
389
390 @Test
391 public void testPrintHelpNewlineHeader() {
392 final HelpFormatter formatter = new HelpFormatter();
393 final ByteArrayOutputStream out = new ByteArrayOutputStream();
394 final PrintWriter pw = new PrintWriter(out);
395
396 final Options options = new Options();
397 options.addOption("a", "b");
398
399 formatter.printHelp(
400 pw,
401 80,
402 "test" + EOL,
403 EOL,
404 options,
405 0,
406 0,
407 "footer" + EOL
408 );
409 final String expected = "usage: test" + EOL +
410 EOL +
411 "-ab" + EOL +
412 "footer" + EOL;
413 pw.flush();
414 assertEquals(expected, out.toString(), "header newline");
415 }
416
417 @Test
418 public void testPrintHelpWithEmptySyntax() {
419 final HelpFormatter formatter = new HelpFormatter();
420 assertThrows(IllegalArgumentException.class, () -> formatter.printHelp(null, new Options()), "null command line syntax should be rejected");
421 assertThrows(IllegalArgumentException.class, () -> formatter.printHelp(null, new Options(), true), "null command line syntax should be rejected");
422 assertThrows(IllegalArgumentException.class, () -> formatter.printHelp(null, new Options(), false), "null command line syntax should be rejected");
423 assertThrows(IllegalArgumentException.class, () -> formatter.printHelp("", new Options(), true), "null command line syntax should be rejected");
424 assertThrows(IllegalArgumentException.class, () -> formatter.printHelp("", new Options(), false), "null command line syntax should be rejected");
425 }
426
427 @Test
428 public void testPrintOptionGroupUsage() {
429 final OptionGroup group = new OptionGroup();
430 group.addOption(Option.builder("a").build());
431 group.addOption(Option.builder("b").build());
432 group.addOption(Option.builder("c").build());
433
434 final Options options = new Options();
435 options.addOptionGroup(group);
436
437 final StringWriter out = new StringWriter();
438
439 final HelpFormatter formatter = new HelpFormatter();
440 formatter.printUsage(new PrintWriter(out), 80, "app", options);
441
442 assertEquals("usage: app [-a | -b | -c]" + EOL, out.toString());
443 }
444
445 @Test
446 public void testPrintOptions() {
447 final StringBuffer sb = new StringBuffer();
448 final HelpFormatter hf = new HelpFormatter();
449 final int leftPad = 1;
450 final int descPad = 3;
451 final String lpad = hf.createPadding(leftPad);
452 final String dpad = hf.createPadding(descPad);
453 Options options;
454 String expected;
455
456 options = new Options().addOption("a", false, "aaaa aaaa aaaa aaaa aaaa");
457 expected = lpad + "-a" + dpad + "aaaa aaaa aaaa aaaa aaaa";
458 hf.renderOptions(sb, 60, options, leftPad, descPad);
459 assertEquals(expected, sb.toString(), "simple non-wrapped option");
460
461 int nextLineTabStop = leftPad + descPad + "-a".length();
462 expected = lpad + "-a" + dpad + "aaaa aaaa aaaa" + EOL + hf.createPadding(nextLineTabStop) + "aaaa aaaa";
463 sb.setLength(0);
464 hf.renderOptions(sb, nextLineTabStop + 17, options, leftPad, descPad);
465 assertEquals(expected, sb.toString(), "simple wrapped option");
466
467 options = new Options().addOption("a", "aaa", false, "dddd dddd dddd dddd");
468 expected = lpad + "-a,--aaa" + dpad + "dddd dddd dddd dddd";
469 sb.setLength(0);
470 hf.renderOptions(sb, 60, options, leftPad, descPad);
471 assertEquals(expected, sb.toString(), "long non-wrapped option");
472
473 nextLineTabStop = leftPad + descPad + "-a,--aaa".length();
474 expected = lpad + "-a,--aaa" + dpad + "dddd dddd" + EOL + hf.createPadding(nextLineTabStop) + "dddd dddd";
475 sb.setLength(0);
476 hf.renderOptions(sb, 25, options, leftPad, descPad);
477 assertEquals(expected, sb.toString(), "long wrapped option");
478
479 options = new Options().addOption("a", "aaa", false, "dddd dddd dddd dddd").addOption("b", false, "feeee eeee eeee eeee");
480 expected = lpad + "-a,--aaa" + dpad + "dddd dddd" + EOL + hf.createPadding(nextLineTabStop) + "dddd dddd" + EOL + lpad + "-b " + dpad
481 + "feeee eeee" + EOL + hf.createPadding(nextLineTabStop) + "eeee eeee";
482 sb.setLength(0);
483 hf.renderOptions(sb, 25, options, leftPad, descPad);
484 assertEquals(expected, sb.toString(), "multiple wrapped options");
485 }
486
487 @Test
488 public void testPrintOptionWithEmptyArgNameUsage() {
489 final Option option = new Option("f", true, null);
490 option.setArgName("");
491 option.setRequired(true);
492
493 final Options options = new Options();
494 options.addOption(option);
495
496 final StringWriter out = new StringWriter();
497
498 final HelpFormatter formatter = new HelpFormatter();
499 formatter.printUsage(new PrintWriter(out), 80, "app", options);
500
501 assertEquals("usage: app -f" + EOL, out.toString());
502 }
503
504 @Test
505 public void testPrintRequiredOptionGroupUsage() {
506 final OptionGroup group = new OptionGroup();
507 group.addOption(Option.builder("a").build());
508 group.addOption(Option.builder("b").build());
509 group.addOption(Option.builder("c").build());
510 group.setRequired(true);
511
512 final Options options = new Options();
513 options.addOptionGroup(group);
514
515 final StringWriter out = new StringWriter();
516
517 final HelpFormatter formatter = new HelpFormatter();
518 formatter.printUsage(new PrintWriter(out), 80, "app", options);
519
520 assertEquals("usage: app -a | -b | -c" + EOL, out.toString());
521 }
522
523
524 @Test
525 public void testPrintSortedUsage() {
526 final Options opts = new Options();
527 opts.addOption(new Option("a", "first"));
528 opts.addOption(new Option("b", "second"));
529 opts.addOption(new Option("c", "third"));
530
531 final HelpFormatter helpFormatter = new HelpFormatter();
532 helpFormatter.setOptionComparator((opt1, opt2) -> opt2.getKey().compareToIgnoreCase(opt1.getKey()));
533
534 final StringWriter out = new StringWriter();
535 helpFormatter.printUsage(new PrintWriter(out), 80, "app", opts);
536
537 assertEquals("usage: app [-c] [-b] [-a]" + EOL, out.toString());
538 }
539
540 @Test
541 public void testPrintSortedUsageWithNullComparator() {
542 final Options opts = new Options();
543 opts.addOption(new Option("c", "first"));
544 opts.addOption(new Option("b", "second"));
545 opts.addOption(new Option("a", "third"));
546
547 final HelpFormatter helpFormatter = new HelpFormatter();
548 helpFormatter.setOptionComparator(null);
549
550 final StringWriter out = new StringWriter();
551 helpFormatter.printUsage(new PrintWriter(out), 80, "app", opts);
552
553 assertEquals("usage: app [-c] [-b] [-a]" + EOL, out.toString());
554 }
555
556
557
558 @Test
559 public void testPrintUsage() {
560 final Option optionA = new Option("a", "first");
561 final Option optionB = new Option("b", "second");
562 final Option optionC = new Option("c", "third");
563 final Options opts = new Options();
564 opts.addOption(optionA);
565 opts.addOption(optionB);
566 opts.addOption(optionC);
567 final HelpFormatter helpFormatter = new HelpFormatter();
568 final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
569 try (PrintWriter printWriter = new PrintWriter(bytesOut)) {
570 helpFormatter.printUsage(printWriter, 80, "app", opts);
571 }
572 assertEquals("usage: app [-a] [-b] [-c]" + EOL, bytesOut.toString());
573 }
574
575 @Test
576 public void testRenderWrappedTextMultiLine() {
577
578 final int width = 16;
579 final int padding = 0;
580
581 final String expected = "aaaa aaaa aaaa" + EOL +
582 "aaaaaa" + EOL +
583 "aaaaa";
584
585
586 final StringBuffer sb = new StringBuffer();
587 new HelpFormatter().renderWrappedText(sb, width, padding, expected);
588 assertEquals(expected, sb.toString(), "multi line text");
589 }
590
591 @Test
592 public void testRenderWrappedTextMultiLinePadded() {
593
594 final int width = 16;
595 final int padding = 4;
596
597 final String text = "aaaa aaaa aaaa" + EOL +
598 "aaaaaa" + EOL +
599 "aaaaa";
600 final String expected = "aaaa aaaa aaaa" + EOL +
601 " aaaaaa" + EOL +
602 " aaaaa";
603
604
605 final StringBuffer sb = new StringBuffer();
606 new HelpFormatter().renderWrappedText(sb, width, padding, text);
607 assertEquals(expected, sb.toString(), "multi-line padded text");
608 }
609
610 @Test
611 public void testRenderWrappedTextSingleLine() {
612
613 final int width = 12;
614 final int padding = 0;
615 final String text = "This is a test.";
616 final String expected = "This is a" + EOL + "test.";
617
618 final StringBuffer sb = new StringBuffer();
619 new HelpFormatter().renderWrappedText(sb, width, padding, text);
620 assertEquals(expected, sb.toString(), "single line text");
621 }
622
623 @Test
624 public void testRenderWrappedTextSingleLinePadded() {
625
626 final int width = 12;
627 final int padding = 4;
628 final String text = "This is a test.";
629 final String expected = "This is a" + EOL + " test.";
630
631 final StringBuffer sb = new StringBuffer();
632 new HelpFormatter().renderWrappedText(sb, width, padding, text);
633 assertEquals(expected, sb.toString(), "single line padded text");
634 }
635
636 @Test
637 public void testRenderWrappedTextSingleLinePadded2() {
638
639 final int width = 53;
640 final int padding = 24;
641
642 final String text = " -p,--period <PERIOD> PERIOD is time duration of form " +
643 "DATE[-DATE] where DATE has form YYYY[MM[DD]]";
644 final String expected = " -p,--period <PERIOD> PERIOD is time duration of" + EOL +
645 " form DATE[-DATE] where DATE" + EOL +
646 " has form YYYY[MM[DD]]";
647
648
649 final StringBuffer sb = new StringBuffer();
650 new HelpFormatter().renderWrappedText(sb, width, padding, text);
651 assertEquals(expected, sb.toString(), "single line padded text 2");
652 }
653
654 @Test
655 public void testRenderWrappedTextWordCut() {
656 final int width = 7;
657 final int padding = 0;
658 final String text = "Thisisatest.";
659 final String expected = "Thisisa" + EOL + "test.";
660
661 final StringBuffer sb = new StringBuffer();
662 new HelpFormatter().renderWrappedText(sb, width, padding, text);
663 assertEquals(expected, sb.toString(), "cut and wrap");
664 }
665
666 @Test
667 public void testRtrim() {
668 final HelpFormatter formatter = new HelpFormatter();
669
670 assertNull(formatter.rtrim(null));
671 assertEquals("", formatter.rtrim(""));
672 assertEquals(" foo", formatter.rtrim(" foo "));
673 }
674
675 @Test
676 public void testUsageWithLongOptSeparator() {
677 final Options options = new Options();
678 options.addOption("f", true, "the file");
679 options.addOption(Option.builder("s").longOpt("size").desc("the size").hasArg().argName("SIZE").build());
680 options.addOption(Option.builder().longOpt("age").desc("the age").hasArg().build());
681
682 final HelpFormatter formatter = new HelpFormatter();
683 formatter.setLongOptSeparator("=");
684
685 final StringWriter out = new StringWriter();
686
687 formatter.printUsage(new PrintWriter(out), 80, "create", options);
688
689 assertEquals("usage: create [--age=<arg>] [-f <arg>] [-s <SIZE>]", out.toString().trim());
690 }
691 }