1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.logging.log4j.core.tools.picocli;
18
19 import java.io.File;
20 import java.io.PrintStream;
21 import java.lang.annotation.ElementType;
22 import java.lang.annotation.Retention;
23 import java.lang.annotation.RetentionPolicy;
24 import java.lang.annotation.Target;
25 import java.lang.reflect.Array;
26 import java.lang.reflect.Constructor;
27 import java.lang.reflect.Field;
28 import java.lang.reflect.ParameterizedType;
29 import java.lang.reflect.Type;
30 import java.lang.reflect.WildcardType;
31 import java.math.BigDecimal;
32 import java.math.BigInteger;
33 import java.net.InetAddress;
34 import java.net.MalformedURLException;
35 import java.net.URI;
36 import java.net.URISyntaxException;
37 import java.net.URL;
38 import java.nio.charset.Charset;
39 import java.nio.file.Path;
40 import java.nio.file.Paths;
41 import java.sql.Time;
42 import java.text.BreakIterator;
43 import java.text.ParseException;
44 import java.text.SimpleDateFormat;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.Collection;
48 import java.util.Collections;
49 import java.util.Comparator;
50 import java.util.Date;
51 import java.util.HashMap;
52 import java.util.HashSet;
53 import java.util.LinkedHashMap;
54 import java.util.LinkedHashSet;
55 import java.util.LinkedList;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.Queue;
59 import java.util.Set;
60 import java.util.SortedSet;
61 import java.util.Stack;
62 import java.util.TreeSet;
63 import java.util.UUID;
64 import java.util.concurrent.Callable;
65 import java.util.regex.Pattern;
66
67 import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Ansi.IStyle;
68 import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Ansi.Style;
69 import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Ansi.Text;
70
71 import static java.util.Locale.ENGLISH;
72 import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Column.Overflow.SPAN;
73 import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Column.Overflow.TRUNCATE;
74 import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Column.Overflow.WRAP;
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133 public class CommandLine {
134
135 public static final String VERSION = "2.0.3";
136
137 private final Tracer tracer = new Tracer();
138 private final Interpreter interpreter;
139 private String commandName = Help.DEFAULT_COMMAND_NAME;
140 private boolean overwrittenOptionsAllowed = false;
141 private boolean unmatchedArgumentsAllowed = false;
142 private List<String> unmatchedArguments = new ArrayList<String>();
143 private CommandLine parent;
144 private boolean usageHelpRequested;
145 private boolean versionHelpRequested;
146 private List<String> versionLines = new ArrayList<String>();
147
148
149
150
151
152
153
154
155 public CommandLine(Object command) {
156 interpreter = new Interpreter(command);
157 }
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200 public CommandLine addSubcommand(String name, Object command) {
201 CommandLine commandLine = toCommandLine(command);
202 commandLine.parent = this;
203 interpreter.commands.put(name, commandLine);
204 return this;
205 }
206
207
208
209
210 public Map<String, CommandLine> getSubcommands() {
211 return new LinkedHashMap<String, CommandLine>(interpreter.commands);
212 }
213
214
215
216
217
218
219
220 public CommandLine getParent() {
221 return parent;
222 }
223
224
225
226
227
228
229 public <T> T getCommand() {
230 return (T) interpreter.command;
231 }
232
233
234
235
236 public boolean isUsageHelpRequested() { return usageHelpRequested; }
237
238
239
240
241 public boolean isVersionHelpRequested() { return versionHelpRequested; }
242
243
244
245
246
247
248
249 public boolean isOverwrittenOptionsAllowed() {
250 return overwrittenOptionsAllowed;
251 }
252
253
254
255
256
257
258
259
260
261
262 public CommandLine setOverwrittenOptionsAllowed(boolean newValue) {
263 this.overwrittenOptionsAllowed = newValue;
264 for (CommandLine command : interpreter.commands.values()) {
265 command.setOverwrittenOptionsAllowed(newValue);
266 }
267 return this;
268 }
269
270
271
272
273
274
275
276
277 public boolean isUnmatchedArgumentsAllowed() {
278 return unmatchedArgumentsAllowed;
279 }
280
281
282
283
284
285
286
287
288
289
290
291 public CommandLine setUnmatchedArgumentsAllowed(boolean newValue) {
292 this.unmatchedArgumentsAllowed = newValue;
293 for (CommandLine command : interpreter.commands.values()) {
294 command.setUnmatchedArgumentsAllowed(newValue);
295 }
296 return this;
297 }
298
299
300
301
302
303
304 public List<String> getUnmatchedArguments() {
305 return unmatchedArguments;
306 }
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328 public static <T> T populateCommand(T command, String... args) {
329 CommandLine cli = toCommandLine(command);
330 cli.parse(args);
331 return command;
332 }
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348 public List<CommandLine> parse(String... args) {
349 return interpreter.parse(args);
350 }
351
352
353
354
355
356
357
358
359
360
361
362
363
364 public static interface IParseResultHandler {
365
366
367
368
369
370
371
372
373
374 List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) throws ExecutionException;
375 }
376
377
378
379
380
381
382
383
384
385
386
387 public static interface IExceptionHandler {
388
389
390
391
392
393
394
395
396
397 List<Object> handleException(ParameterException ex, PrintStream out, Help.Ansi ansi, String... args);
398 }
399
400
401
402
403
404
405
406
407
408 public static class DefaultExceptionHandler implements IExceptionHandler {
409 @Override
410 public List<Object> handleException(ParameterException ex, PrintStream out, Help.Ansi ansi, String... args) {
411 out.println(ex.getMessage());
412 ex.getCommandLine().usage(out, ansi);
413 return Collections.emptyList();
414 }
415 }
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432 public static boolean printHelpIfRequested(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
433 for (CommandLine parsed : parsedCommands) {
434 if (parsed.isUsageHelpRequested()) {
435 parsed.usage(out, ansi);
436 return true;
437 } else if (parsed.isVersionHelpRequested()) {
438 parsed.printVersionHelp(out, ansi);
439 return true;
440 }
441 }
442 return false;
443 }
444 private static Object execute(CommandLine parsed) {
445 Object command = parsed.getCommand();
446 if (command instanceof Runnable) {
447 try {
448 ((Runnable) command).run();
449 return null;
450 } catch (Exception ex) {
451 throw new ExecutionException(parsed, "Error while running command (" + command + ")", ex);
452 }
453 } else if (command instanceof Callable) {
454 try {
455 return ((Callable<Object>) command).call();
456 } catch (Exception ex) {
457 throw new ExecutionException(parsed, "Error while calling command (" + command + ")", ex);
458 }
459 }
460 throw new ExecutionException(parsed, "Parsed command (" + command + ") is not Runnable or Callable");
461 }
462
463
464
465
466
467
468
469
470
471 public static class RunFirst implements IParseResultHandler {
472
473
474
475
476
477
478
479
480
481
482
483
484 @Override
485 public List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
486 if (printHelpIfRequested(parsedCommands, out, ansi)) { return Collections.emptyList(); }
487 return Arrays.asList(execute(parsedCommands.get(0)));
488 }
489 }
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522 public static class RunLast implements IParseResultHandler {
523
524
525
526
527
528
529
530
531
532
533
534
535 @Override
536 public List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
537 if (printHelpIfRequested(parsedCommands, out, ansi)) { return Collections.emptyList(); }
538 CommandLine last = parsedCommands.get(parsedCommands.size() - 1);
539 return Arrays.asList(execute(last));
540 }
541 }
542
543
544
545
546
547 public static class RunAll implements IParseResultHandler {
548
549
550
551
552
553
554
555
556
557
558
559
560
561 @Override
562 public List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
563 if (printHelpIfRequested(parsedCommands, out, ansi)) {
564 return null;
565 }
566 List<Object> result = new ArrayList<Object>();
567 for (CommandLine parsed : parsedCommands) {
568 result.add(execute(parsed));
569 }
570 return result;
571 }
572 }
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610 public List<Object> parseWithHandler(IParseResultHandler handler, PrintStream out, String... args) {
611 return parseWithHandlers(handler, out, Help.Ansi.AUTO, new DefaultExceptionHandler(), args);
612 }
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655 public List<Object> parseWithHandlers(IParseResultHandler handler, PrintStream out, Help.Ansi ansi, IExceptionHandler exceptionHandler, String... args) {
656 try {
657 List<CommandLine> result = parse(args);
658 return handler.handleParseResult(result, out, ansi);
659 } catch (ParameterException ex) {
660 return exceptionHandler.handleException(ex, out, ansi, args);
661 }
662 }
663
664
665
666
667
668
669 public static void usage(Object command, PrintStream out) {
670 toCommandLine(command).usage(out);
671 }
672
673
674
675
676
677
678
679
680
681 public static void usage(Object command, PrintStream out, Help.Ansi ansi) {
682 toCommandLine(command).usage(out, ansi);
683 }
684
685
686
687
688
689
690
691
692
693 public static void usage(Object command, PrintStream out, Help.ColorScheme colorScheme) {
694 toCommandLine(command).usage(out, colorScheme);
695 }
696
697
698
699
700
701
702 public void usage(PrintStream out) {
703 usage(out, Help.Ansi.AUTO);
704 }
705
706
707
708
709
710
711
712 public void usage(PrintStream out, Help.Ansi ansi) {
713 usage(out, Help.defaultColorScheme(ansi));
714 }
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747 public void usage(PrintStream out, Help.ColorScheme colorScheme) {
748 Help help = new Help(interpreter.command, colorScheme).addAllSubcommands(getSubcommands());
749 if (!Help.DEFAULT_SEPARATOR.equals(getSeparator())) {
750 help.separator = getSeparator();
751 help.parameterLabelRenderer = help.createDefaultParamLabelRenderer();
752 }
753 if (!Help.DEFAULT_COMMAND_NAME.equals(getCommandName())) {
754 help.commandName = getCommandName();
755 }
756 StringBuilder sb = new StringBuilder()
757 .append(help.headerHeading())
758 .append(help.header())
759 .append(help.synopsisHeading())
760 .append(help.synopsis(help.synopsisHeadingLength()))
761 .append(help.descriptionHeading())
762 .append(help.description())
763 .append(help.parameterListHeading())
764 .append(help.parameterList())
765 .append(help.optionListHeading())
766 .append(help.optionList())
767 .append(help.commandListHeading())
768 .append(help.commandList())
769 .append(help.footerHeading())
770 .append(help.footer());
771 out.print(sb);
772 }
773
774
775
776
777
778
779
780 public void printVersionHelp(PrintStream out) { printVersionHelp(out, Help.Ansi.AUTO); }
781
782
783
784
785
786
787
788
789
790
791
792
793 public void printVersionHelp(PrintStream out, Help.Ansi ansi) {
794 for (String versionInfo : versionLines) {
795 out.println(ansi.new Text(versionInfo));
796 }
797 }
798
799
800
801
802
803
804
805
806
807
808
809
810
811 public void printVersionHelp(PrintStream out, Help.Ansi ansi, Object... params) {
812 for (String versionInfo : versionLines) {
813 out.println(ansi.new Text(String.format(versionInfo, params)));
814 }
815 }
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835 public static <C extends Callable<T>, T> T call(C callable, PrintStream out, String... args) {
836 return call(callable, out, Help.Ansi.AUTO, args);
837 }
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883 public static <C extends Callable<T>, T> T call(C callable, PrintStream out, Help.Ansi ansi, String... args) {
884 CommandLine cmd = new CommandLine(callable);
885 List<Object> results = cmd.parseWithHandlers(new RunLast(), out, ansi, new DefaultExceptionHandler(), args);
886 return results == null || results.isEmpty() ? null : (T) results.get(0);
887 }
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905 public static <R extends Runnable> void run(R runnable, PrintStream out, String... args) {
906 run(runnable, out, Help.Ansi.AUTO, args);
907 }
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951 public static <R extends Runnable> void run(R runnable, PrintStream out, Help.Ansi ansi, String... args) {
952 CommandLine cmd = new CommandLine(runnable);
953 cmd.parseWithHandlers(new RunLast(), out, ansi, new DefaultExceptionHandler(), args);
954 }
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999 public <K> CommandLine registerConverter(Class<K> cls, ITypeConverter<K> converter) {
1000 interpreter.converterRegistry.put(Assert.notNull(cls, "class"), Assert.notNull(converter, "converter"));
1001 for (CommandLine command : interpreter.commands.values()) {
1002 command.registerConverter(cls, converter);
1003 }
1004 return this;
1005 }
1006
1007
1008
1009 public String getSeparator() {
1010 return interpreter.separator;
1011 }
1012
1013
1014
1015
1016
1017 public CommandLine setSeparator(String separator) {
1018 interpreter.separator = Assert.notNull(separator, "separator");
1019 return this;
1020 }
1021
1022
1023
1024 public String getCommandName() {
1025 return commandName;
1026 }
1027
1028
1029
1030
1031
1032
1033 public CommandLine setCommandName(String commandName) {
1034 this.commandName = Assert.notNull(commandName, "commandName");
1035 return this;
1036 }
1037 private static boolean empty(String str) { return str == null || str.trim().length() == 0; }
1038 private static boolean empty(Object[] array) { return array == null || array.length == 0; }
1039 private static boolean empty(Text txt) { return txt == null || txt.plain.toString().trim().length() == 0; }
1040 private static String str(String[] arr, int i) { return (arr == null || arr.length == 0) ? "" : arr[i]; }
1041 private static boolean isBoolean(Class<?> type) { return type == Boolean.class || type == Boolean.TYPE; }
1042 private static CommandLine toCommandLine(Object obj) { return obj instanceof CommandLine ? (CommandLine) obj : new CommandLine(obj);}
1043 private static boolean isMultiValue(Field field) { return isMultiValue(field.getType()); }
1044 private static boolean isMultiValue(Class<?> cls) { return cls.isArray() || Collection.class.isAssignableFrom(cls) || Map.class.isAssignableFrom(cls); }
1045 private static Class<?>[] getTypeAttribute(Field field) {
1046 Class<?>[] explicit = field.isAnnotationPresent(Parameters.class) ? field.getAnnotation(Parameters.class).type() : field.getAnnotation(Option.class).type();
1047 if (explicit.length > 0) { return explicit; }
1048 if (field.getType().isArray()) { return new Class<?>[] { field.getType().getComponentType() }; }
1049 if (isMultiValue(field)) {
1050 Type type = field.getGenericType();
1051 if (type instanceof ParameterizedType) {
1052 ParameterizedType parameterizedType = (ParameterizedType) type;
1053 Type[] paramTypes = parameterizedType.getActualTypeArguments();
1054 Class<?>[] result = new Class<?>[paramTypes.length];
1055 for (int i = 0; i < paramTypes.length; i++) {
1056 if (paramTypes[i] instanceof Class) { result[i] = (Class<?>) paramTypes[i]; continue; }
1057 if (paramTypes[i] instanceof WildcardType) {
1058 WildcardType wildcardType = (WildcardType) paramTypes[i];
1059 Type[] lower = wildcardType.getLowerBounds();
1060 if (lower.length > 0 && lower[0] instanceof Class) { result[i] = (Class<?>) lower[0]; continue; }
1061 Type[] upper = wildcardType.getUpperBounds();
1062 if (upper.length > 0 && upper[0] instanceof Class) { result[i] = (Class<?>) upper[0]; continue; }
1063 }
1064 Arrays.fill(result, String.class); return result;
1065 }
1066 return result;
1067 }
1068 return new Class<?>[] {String.class, String.class};
1069 }
1070 return new Class<?>[] {field.getType()};
1071 }
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103 @Retention(RetentionPolicy.RUNTIME)
1104 @Target(ElementType.FIELD)
1105 public @interface Option {
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148 String[] names();
1149
1150
1151
1152
1153
1154
1155
1156 boolean required() default false;
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174 boolean help() default false;
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192 boolean usageHelp() default false;
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210 boolean versionHelp() default false;
1211
1212
1213
1214
1215
1216 String[] description() default {};
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257 String arity() default "";
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275 String paramLabel() default "";
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304 Class<?>[] type() default {};
1305
1306
1307
1308
1309
1310
1311
1312 String split() default "";
1313
1314
1315
1316
1317
1318 boolean hidden() default false;
1319 }
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345 @Retention(RetentionPolicy.RUNTIME)
1346 @Target(ElementType.FIELD)
1347 public @interface Parameters {
1348
1349
1350
1351
1352
1353 String index() default "*";
1354
1355
1356
1357
1358 String[] description() default {};
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368 String arity() default "";
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383 String paramLabel() default "";
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413 Class<?>[] type() default {};
1414
1415
1416
1417
1418
1419
1420
1421 String split() default "";
1422
1423
1424
1425
1426
1427 boolean hidden() default false;
1428 }
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454 @Retention(RetentionPolicy.RUNTIME)
1455 @Target({ElementType.TYPE, ElementType.LOCAL_VARIABLE, ElementType.PACKAGE})
1456 public @interface Command {
1457
1458
1459
1460
1461
1462 String name() default "<main class>";
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489 Class<?>[] subcommands() default {};
1490
1491
1492
1493
1494
1495 String separator() default "=";
1496
1497
1498
1499
1500
1501
1502
1503
1504 String[] version() default {};
1505
1506
1507
1508
1509 String headerHeading() default "";
1510
1511
1512
1513
1514
1515 String[] header() default {};
1516
1517
1518
1519
1520
1521
1522 String synopsisHeading() default "Usage: ";
1523
1524
1525
1526
1527
1528
1529
1530 boolean abbreviateSynopsis() default false;
1531
1532
1533
1534
1535
1536 String[] customSynopsis() default {};
1537
1538
1539
1540
1541 String descriptionHeading() default "";
1542
1543
1544
1545
1546
1547 String[] description() default {};
1548
1549
1550
1551
1552 String parameterListHeading() default "";
1553
1554
1555
1556
1557 String optionListHeading() default "";
1558
1559
1560
1561
1562 boolean sortOptions() default true;
1563
1564
1565
1566
1567
1568 char requiredOptionMarker() default ' ';
1569
1570
1571
1572
1573
1574 boolean showDefaultValues() default false;
1575
1576
1577
1578
1579
1580 String commandListHeading() default "Commands:%n";
1581
1582
1583
1584
1585 String footerHeading() default "";
1586
1587
1588
1589
1590
1591 String[] footer() default {};
1592 }
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631 public interface ITypeConverter<K> {
1632
1633
1634
1635
1636
1637
1638 K convert(String value) throws Exception;
1639 }
1640
1641
1642
1643 public static class Range implements Comparable<Range> {
1644
1645 public final int min;
1646
1647 public final int max;
1648 public final boolean isVariable;
1649 private final boolean isUnspecified;
1650 private final String originalValue;
1651
1652
1653
1654
1655
1656
1657
1658
1659 public Range(int min, int max, boolean variable, boolean unspecified, String originalValue) {
1660 this.min = min;
1661 this.max = max;
1662 this.isVariable = variable;
1663 this.isUnspecified = unspecified;
1664 this.originalValue = originalValue;
1665 }
1666
1667
1668
1669
1670 public static Range optionArity(Field field) {
1671 return field.isAnnotationPresent(Option.class)
1672 ? adjustForType(Range.valueOf(field.getAnnotation(Option.class).arity()), field)
1673 : new Range(0, 0, false, true, "0");
1674 }
1675
1676
1677
1678
1679 public static Range parameterArity(Field field) {
1680 return field.isAnnotationPresent(Parameters.class)
1681 ? adjustForType(Range.valueOf(field.getAnnotation(Parameters.class).arity()), field)
1682 : new Range(0, 0, false, true, "0");
1683 }
1684
1685
1686
1687 public static Range parameterIndex(Field field) {
1688 return field.isAnnotationPresent(Parameters.class)
1689 ? Range.valueOf(field.getAnnotation(Parameters.class).index())
1690 : new Range(0, 0, false, true, "0");
1691 }
1692 static Range adjustForType(Range result, Field field) {
1693 return result.isUnspecified ? defaultArity(field) : result;
1694 }
1695
1696
1697
1698
1699
1700
1701 public static Range defaultArity(Field field) {
1702 Class<?> type = field.getType();
1703 if (field.isAnnotationPresent(Option.class)) {
1704 return defaultArity(type);
1705 }
1706 if (isMultiValue(type)) {
1707 return Range.valueOf("0..1");
1708 }
1709 return Range.valueOf("1");
1710 }
1711
1712
1713
1714 public static Range defaultArity(Class<?> type) {
1715 return isBoolean(type) ? Range.valueOf("0") : Range.valueOf("1");
1716 }
1717 private int size() { return 1 + max - min; }
1718 static Range parameterCapacity(Field field) {
1719 Range arity = parameterArity(field);
1720 if (!isMultiValue(field)) { return arity; }
1721 Range index = parameterIndex(field);
1722 if (arity.max == 0) { return arity; }
1723 if (index.size() == 1) { return arity; }
1724 if (index.isVariable) { return Range.valueOf(arity.min + "..*"); }
1725 if (arity.size() == 1) { return Range.valueOf(arity.min * index.size() + ""); }
1726 if (arity.isVariable) { return Range.valueOf(arity.min * index.size() + "..*"); }
1727 return Range.valueOf(arity.min * index.size() + ".." + arity.max * index.size());
1728 }
1729
1730
1731
1732
1733
1734
1735 public static Range valueOf(String range) {
1736 range = range.trim();
1737 boolean unspecified = range.length() == 0 || range.startsWith("..");
1738 int min = -1, max = -1;
1739 boolean variable = false;
1740 int dots = -1;
1741 if ((dots = range.indexOf("..")) >= 0) {
1742 min = parseInt(range.substring(0, dots), 0);
1743 max = parseInt(range.substring(dots + 2), Integer.MAX_VALUE);
1744 variable = max == Integer.MAX_VALUE;
1745 } else {
1746 max = parseInt(range, Integer.MAX_VALUE);
1747 variable = max == Integer.MAX_VALUE;
1748 min = variable ? 0 : max;
1749 }
1750 Range result = new Range(min, max, variable, unspecified, range);
1751 return result;
1752 }
1753 private static int parseInt(String str, int defaultValue) {
1754 try {
1755 return Integer.parseInt(str);
1756 } catch (Exception ex) {
1757 return defaultValue;
1758 }
1759 }
1760
1761
1762
1763
1764 public Range min(int newMin) { return new Range(newMin, Math.max(newMin, max), isVariable, isUnspecified, originalValue); }
1765
1766
1767
1768
1769
1770 public Range max(int newMax) { return new Range(Math.min(min, newMax), newMax, isVariable, isUnspecified, originalValue); }
1771
1772
1773
1774
1775
1776
1777 public boolean contains(int value) { return min <= value && max >= value; }
1778
1779 @Override
1780 public boolean equals(Object object) {
1781 if (!(object instanceof Range)) { return false; }
1782 Range other = (Range) object;
1783 return other.max == this.max && other.min == this.min && other.isVariable == this.isVariable;
1784 }
1785 @Override
1786 public int hashCode() {
1787 return ((17 * 37 + max) * 37 + min) * 37 + (isVariable ? 1 : 0);
1788 }
1789 @Override
1790 public String toString() {
1791 return min == max ? String.valueOf(min) : min + ".." + (isVariable ? "*" : max);
1792 }
1793 @Override
1794 public int compareTo(Range other) {
1795 int result = min - other.min;
1796 return (result == 0) ? max - other.max : result;
1797 }
1798 }
1799 static void init(Class<?> cls,
1800 List<Field> requiredFields,
1801 Map<String, Field> optionName2Field,
1802 Map<Character, Field> singleCharOption2Field,
1803 List<Field> positionalParametersFields) {
1804 Field[] declaredFields = cls.getDeclaredFields();
1805 for (Field field : declaredFields) {
1806 field.setAccessible(true);
1807 if (field.isAnnotationPresent(Option.class)) {
1808 Option option = field.getAnnotation(Option.class);
1809 if (option.required()) {
1810 requiredFields.add(field);
1811 }
1812 for (String name : option.names()) {
1813 Field existing = optionName2Field.put(name, field);
1814 if (existing != null && existing != field) {
1815 throw DuplicateOptionAnnotationsException.create(name, field, existing);
1816 }
1817 if (name.length() == 2 && name.startsWith("-")) {
1818 char flag = name.charAt(1);
1819 Field existing2 = singleCharOption2Field.put(flag, field);
1820 if (existing2 != null && existing2 != field) {
1821 throw DuplicateOptionAnnotationsException.create(name, field, existing2);
1822 }
1823 }
1824 }
1825 }
1826 if (field.isAnnotationPresent(Parameters.class)) {
1827 if (field.isAnnotationPresent(Option.class)) {
1828 throw new DuplicateOptionAnnotationsException("A field can be either @Option or @Parameters, but '"
1829 + field.getName() + "' is both.");
1830 }
1831 positionalParametersFields.add(field);
1832 Range arity = Range.parameterArity(field);
1833 if (arity.min > 0) {
1834 requiredFields.add(field);
1835 }
1836 }
1837 }
1838 }
1839 static void validatePositionalParameters(List<Field> positionalParametersFields) {
1840 int min = 0;
1841 for (Field field : positionalParametersFields) {
1842 Range index = Range.parameterIndex(field);
1843 if (index.min > min) {
1844 throw new ParameterIndexGapException("Missing field annotated with @Parameter(index=" + min +
1845 "). Nearest field '" + field.getName() + "' has index=" + index.min);
1846 }
1847 min = Math.max(min, index.max);
1848 min = min == Integer.MAX_VALUE ? min : min + 1;
1849 }
1850 }
1851 private static <T> Stack<T> reverse(Stack<T> stack) {
1852 Collections.reverse(stack);
1853 return stack;
1854 }
1855
1856
1857
1858 private class Interpreter {
1859 private final Map<String, CommandLine> commands = new LinkedHashMap<String, CommandLine>();
1860 private final Map<Class<?>, ITypeConverter<?>> converterRegistry = new HashMap<Class<?>, ITypeConverter<?>>();
1861 private final Map<String, Field> optionName2Field = new HashMap<String, Field>();
1862 private final Map<Character, Field> singleCharOption2Field = new HashMap<Character, Field>();
1863 private final List<Field> requiredFields = new ArrayList<Field>();
1864 private final List<Field> positionalParametersFields = new ArrayList<Field>();
1865 private final Object command;
1866 private boolean isHelpRequested;
1867 private String separator = Help.DEFAULT_SEPARATOR;
1868 private int position;
1869
1870 Interpreter(Object command) {
1871 converterRegistry.put(Path.class, new BuiltIn.PathConverter());
1872 converterRegistry.put(Object.class, new BuiltIn.StringConverter());
1873 converterRegistry.put(String.class, new BuiltIn.StringConverter());
1874 converterRegistry.put(StringBuilder.class, new BuiltIn.StringBuilderConverter());
1875 converterRegistry.put(CharSequence.class, new BuiltIn.CharSequenceConverter());
1876 converterRegistry.put(Byte.class, new BuiltIn.ByteConverter());
1877 converterRegistry.put(Byte.TYPE, new BuiltIn.ByteConverter());
1878 converterRegistry.put(Boolean.class, new BuiltIn.BooleanConverter());
1879 converterRegistry.put(Boolean.TYPE, new BuiltIn.BooleanConverter());
1880 converterRegistry.put(Character.class, new BuiltIn.CharacterConverter());
1881 converterRegistry.put(Character.TYPE, new BuiltIn.CharacterConverter());
1882 converterRegistry.put(Short.class, new BuiltIn.ShortConverter());
1883 converterRegistry.put(Short.TYPE, new BuiltIn.ShortConverter());
1884 converterRegistry.put(Integer.class, new BuiltIn.IntegerConverter());
1885 converterRegistry.put(Integer.TYPE, new BuiltIn.IntegerConverter());
1886 converterRegistry.put(Long.class, new BuiltIn.LongConverter());
1887 converterRegistry.put(Long.TYPE, new BuiltIn.LongConverter());
1888 converterRegistry.put(Float.class, new BuiltIn.FloatConverter());
1889 converterRegistry.put(Float.TYPE, new BuiltIn.FloatConverter());
1890 converterRegistry.put(Double.class, new BuiltIn.DoubleConverter());
1891 converterRegistry.put(Double.TYPE, new BuiltIn.DoubleConverter());
1892 converterRegistry.put(File.class, new BuiltIn.FileConverter());
1893 converterRegistry.put(URI.class, new BuiltIn.URIConverter());
1894 converterRegistry.put(URL.class, new BuiltIn.URLConverter());
1895 converterRegistry.put(Date.class, new BuiltIn.ISO8601DateConverter());
1896 converterRegistry.put(Time.class, new BuiltIn.ISO8601TimeConverter());
1897 converterRegistry.put(BigDecimal.class, new BuiltIn.BigDecimalConverter());
1898 converterRegistry.put(BigInteger.class, new BuiltIn.BigIntegerConverter());
1899 converterRegistry.put(Charset.class, new BuiltIn.CharsetConverter());
1900 converterRegistry.put(InetAddress.class, new BuiltIn.InetAddressConverter());
1901 converterRegistry.put(Pattern.class, new BuiltIn.PatternConverter());
1902 converterRegistry.put(UUID.class, new BuiltIn.UUIDConverter());
1903
1904 this.command = Assert.notNull(command, "command");
1905 Class<?> cls = command.getClass();
1906 String declaredName = null;
1907 String declaredSeparator = null;
1908 boolean hasCommandAnnotation = false;
1909 while (cls != null) {
1910 init(cls, requiredFields, optionName2Field, singleCharOption2Field, positionalParametersFields);
1911 if (cls.isAnnotationPresent(Command.class)) {
1912 hasCommandAnnotation = true;
1913 Command cmd = cls.getAnnotation(Command.class);
1914 declaredSeparator = (declaredSeparator == null) ? cmd.separator() : declaredSeparator;
1915 declaredName = (declaredName == null) ? cmd.name() : declaredName;
1916 CommandLine.this.versionLines.addAll(Arrays.asList(cmd.version()));
1917
1918 for (Class<?> sub : cmd.subcommands()) {
1919 Command subCommand = sub.getAnnotation(Command.class);
1920 if (subCommand == null || Help.DEFAULT_COMMAND_NAME.equals(subCommand.name())) {
1921 throw new InitializationException("Subcommand " + sub.getName() +
1922 " is missing the mandatory @Command annotation with a 'name' attribute");
1923 }
1924 try {
1925 Constructor<?> constructor = sub.getDeclaredConstructor();
1926 constructor.setAccessible(true);
1927 CommandLine commandLine = toCommandLine(constructor.newInstance());
1928 commandLine.parent = CommandLine.this;
1929 commands.put(subCommand.name(), commandLine);
1930 }
1931 catch (InitializationException ex) { throw ex; }
1932 catch (NoSuchMethodException ex) { throw new InitializationException("Cannot instantiate subcommand " +
1933 sub.getName() + ": the class has no constructor", ex); }
1934 catch (Exception ex) {
1935 throw new InitializationException("Could not instantiate and add subcommand " +
1936 sub.getName() + ": " + ex, ex);
1937 }
1938 }
1939 }
1940 cls = cls.getSuperclass();
1941 }
1942 separator = declaredSeparator != null ? declaredSeparator : separator;
1943 CommandLine.this.commandName = declaredName != null ? declaredName : CommandLine.this.commandName;
1944 Collections.sort(positionalParametersFields, new PositionalParametersSorter());
1945 validatePositionalParameters(positionalParametersFields);
1946
1947 if (positionalParametersFields.isEmpty() && optionName2Field.isEmpty() && !hasCommandAnnotation) {
1948 throw new InitializationException(command + " (" + command.getClass() +
1949 ") is not a command: it has no @Command, @Option or @Parameters annotations");
1950 }
1951 }
1952
1953
1954
1955
1956
1957
1958
1959 List<CommandLine> parse(String... args) {
1960 Assert.notNull(args, "argument array");
1961 if (tracer.isInfo()) {tracer.info("Parsing %d command line args %s%n", args.length, Arrays.toString(args));}
1962 Stack<String> arguments = new Stack<String>();
1963 for (int i = args.length - 1; i >= 0; i--) {
1964 arguments.push(args[i]);
1965 }
1966 List<CommandLine> result = new ArrayList<CommandLine>();
1967 parse(result, arguments, args);
1968 return result;
1969 }
1970
1971 private void parse(List<CommandLine> parsedCommands, Stack<String> argumentStack, String[] originalArgs) {
1972
1973 isHelpRequested = false;
1974 CommandLine.this.versionHelpRequested = false;
1975 CommandLine.this.usageHelpRequested = false;
1976
1977 Class<?> cmdClass = this.command.getClass();
1978 if (tracer.isDebug()) {tracer.debug("Initializing %s: %d options, %d positional parameters, %d required, %d subcommands.%n", cmdClass.getName(), new HashSet<Field>(optionName2Field.values()).size(), positionalParametersFields.size(), requiredFields.size(), commands.size());}
1979 parsedCommands.add(CommandLine.this);
1980 List<Field> required = new ArrayList<Field>(requiredFields);
1981 Set<Field> initialized = new HashSet<Field>();
1982 Collections.sort(required, new PositionalParametersSorter());
1983 try {
1984 processArguments(parsedCommands, argumentStack, required, initialized, originalArgs);
1985 } catch (ParameterException ex) {
1986 throw ex;
1987 } catch (Exception ex) {
1988 int offendingArgIndex = originalArgs.length - argumentStack.size() - 1;
1989 String arg = offendingArgIndex >= 0 && offendingArgIndex < originalArgs.length ? originalArgs[offendingArgIndex] : "?";
1990 throw ParameterException.create(CommandLine.this, ex, arg, offendingArgIndex, originalArgs);
1991 }
1992 if (!isAnyHelpRequested() && !required.isEmpty()) {
1993 for (Field missing : required) {
1994 if (missing.isAnnotationPresent(Option.class)) {
1995 throw MissingParameterException.create(CommandLine.this, required, separator);
1996 } else {
1997 assertNoMissingParameters(missing, Range.parameterArity(missing).min, argumentStack);
1998 }
1999 }
2000 }
2001 if (!unmatchedArguments.isEmpty()) {
2002 if (!isUnmatchedArgumentsAllowed()) { throw new UnmatchedArgumentException(CommandLine.this, unmatchedArguments); }
2003 if (tracer.isWarn()) { tracer.warn("Unmatched arguments: %s%n", unmatchedArguments); }
2004 }
2005 }
2006
2007 private void processArguments(List<CommandLine> parsedCommands,
2008 Stack<String> args,
2009 Collection<Field> required,
2010 Set<Field> initialized,
2011 String[] originalArgs) throws Exception {
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021 while (!args.isEmpty()) {
2022 String arg = args.pop();
2023 if (tracer.isDebug()) {tracer.debug("Processing argument '%s'. Remainder=%s%n", arg, reverse((Stack<String>) args.clone()));}
2024
2025
2026
2027 if ("--".equals(arg)) {
2028 tracer.info("Found end-of-options delimiter '--'. Treating remainder as positional parameters.%n");
2029 processRemainderAsPositionalParameters(required, initialized, args);
2030 return;
2031 }
2032
2033
2034 if (commands.containsKey(arg)) {
2035 if (!isHelpRequested && !required.isEmpty()) {
2036 throw MissingParameterException.create(CommandLine.this, required, separator);
2037 }
2038 if (tracer.isDebug()) {tracer.debug("Found subcommand '%s' (%s)%n", arg, commands.get(arg).interpreter.command.getClass().getName());}
2039 commands.get(arg).interpreter.parse(parsedCommands, args, originalArgs);
2040 return;
2041 }
2042
2043
2044
2045
2046
2047 boolean paramAttachedToOption = false;
2048 int separatorIndex = arg.indexOf(separator);
2049 if (separatorIndex > 0) {
2050 String key = arg.substring(0, separatorIndex);
2051
2052 if (optionName2Field.containsKey(key) && !optionName2Field.containsKey(arg)) {
2053 paramAttachedToOption = true;
2054 String optionParam = arg.substring(separatorIndex + separator.length());
2055 args.push(optionParam);
2056 arg = key;
2057 if (tracer.isDebug()) {tracer.debug("Separated '%s' option from '%s' option parameter%n", key, optionParam);}
2058 } else {
2059 if (tracer.isDebug()) {tracer.debug("'%s' contains separator '%s' but '%s' is not a known option%n", arg, separator, key);}
2060 }
2061 } else {
2062 if (tracer.isDebug()) {tracer.debug("'%s' cannot be separated into <option>%s<option-parameter>%n", arg, separator);}
2063 }
2064 if (optionName2Field.containsKey(arg)) {
2065 processStandaloneOption(required, initialized, arg, args, paramAttachedToOption);
2066 }
2067
2068
2069 else if (arg.length() > 2 && arg.startsWith("-")) {
2070 if (tracer.isDebug()) {tracer.debug("Trying to process '%s' as clustered short options%n", arg, args);}
2071 processClusteredShortOptions(required, initialized, arg, args);
2072 }
2073
2074
2075 else {
2076 args.push(arg);
2077 if (tracer.isDebug()) {tracer.debug("Could not find option '%s', deciding whether to treat as unmatched option or positional parameter...%n", arg);}
2078 if (resemblesOption(arg)) { handleUnmatchedArguments(args.pop()); continue; }
2079 if (tracer.isDebug()) {tracer.debug("No option named '%s' found. Processing remainder as positional parameters%n", arg);}
2080 processPositionalParameter(required, initialized, args);
2081 }
2082 }
2083 }
2084 private boolean resemblesOption(String arg) {
2085 int count = 0;
2086 for (String optionName : optionName2Field.keySet()) {
2087 for (int i = 0; i < arg.length(); i++) {
2088 if (optionName.length() > i && arg.charAt(i) == optionName.charAt(i)) { count++; } else { break; }
2089 }
2090 }
2091 boolean result = count > 0 && count * 10 >= optionName2Field.size() * 9;
2092 if (tracer.isDebug()) {tracer.debug("%s %s an option: %d matching prefix chars out of %d option names%n", arg, (result ? "resembles" : "doesn't resemble"), count, optionName2Field.size());}
2093 return result;
2094 }
2095 private void handleUnmatchedArguments(String arg) {Stack<String> args = new Stack<String>(); args.add(arg); handleUnmatchedArguments(args);}
2096 private void handleUnmatchedArguments(Stack<String> args) {
2097 while (!args.isEmpty()) { unmatchedArguments.add(args.pop()); }
2098 }
2099
2100 private void processRemainderAsPositionalParameters(Collection<Field> required, Set<Field> initialized, Stack<String> args) throws Exception {
2101 while (!args.empty()) {
2102 processPositionalParameter(required, initialized, args);
2103 }
2104 }
2105 private void processPositionalParameter(Collection<Field> required, Set<Field> initialized, Stack<String> args) throws Exception {
2106 if (tracer.isDebug()) {tracer.debug("Processing next arg as a positional parameter at index=%d. Remainder=%s%n", position, reverse((Stack<String>) args.clone()));}
2107 int consumed = 0;
2108 for (Field positionalParam : positionalParametersFields) {
2109 Range indexRange = Range.parameterIndex(positionalParam);
2110 if (!indexRange.contains(position)) {
2111 continue;
2112 }
2113 @SuppressWarnings("unchecked")
2114 Stack<String> argsCopy = (Stack<String>) args.clone();
2115 Range arity = Range.parameterArity(positionalParam);
2116 if (tracer.isDebug()) {tracer.debug("Position %d is in index range %s. Trying to assign args to %s, arity=%s%n", position, indexRange, positionalParam, arity);}
2117 assertNoMissingParameters(positionalParam, arity.min, argsCopy);
2118 int originalSize = argsCopy.size();
2119 applyOption(positionalParam, Parameters.class, arity, false, argsCopy, initialized, "args[" + indexRange + "] at position " + position);
2120 int count = originalSize - argsCopy.size();
2121 if (count > 0) { required.remove(positionalParam); }
2122 consumed = Math.max(consumed, count);
2123 }
2124
2125 for (int i = 0; i < consumed; i++) { args.pop(); }
2126 position += consumed;
2127 if (tracer.isDebug()) {tracer.debug("Consumed %d arguments, moving position to index %d.%n", consumed, position);}
2128 if (consumed == 0 && !args.isEmpty()) {
2129 handleUnmatchedArguments(args.pop());
2130 }
2131 }
2132
2133 private void processStandaloneOption(Collection<Field> required,
2134 Set<Field> initialized,
2135 String arg,
2136 Stack<String> args,
2137 boolean paramAttachedToKey) throws Exception {
2138 Field field = optionName2Field.get(arg);
2139 required.remove(field);
2140 Range arity = Range.optionArity(field);
2141 if (paramAttachedToKey) {
2142 arity = arity.min(Math.max(1, arity.min));
2143 }
2144 if (tracer.isDebug()) {tracer.debug("Found option named '%s': field %s, arity=%s%n", arg, field, arity);}
2145 applyOption(field, Option.class, arity, paramAttachedToKey, args, initialized, "option " + arg);
2146 }
2147
2148 private void processClusteredShortOptions(Collection<Field> required,
2149 Set<Field> initialized,
2150 String arg,
2151 Stack<String> args)
2152 throws Exception {
2153 String prefix = arg.substring(0, 1);
2154 String cluster = arg.substring(1);
2155 boolean paramAttachedToOption = true;
2156 do {
2157 if (cluster.length() > 0 && singleCharOption2Field.containsKey(cluster.charAt(0))) {
2158 Field field = singleCharOption2Field.get(cluster.charAt(0));
2159 Range arity = Range.optionArity(field);
2160 String argDescription = "option " + prefix + cluster.charAt(0);
2161 if (tracer.isDebug()) {tracer.debug("Found option '%s%s' in %s: field %s, arity=%s%n", prefix, cluster.charAt(0), arg, field, arity);}
2162 required.remove(field);
2163 cluster = cluster.length() > 0 ? cluster.substring(1) : "";
2164 paramAttachedToOption = cluster.length() > 0;
2165 if (cluster.startsWith(separator)) {
2166 cluster = cluster.substring(separator.length());
2167 arity = arity.min(Math.max(1, arity.min));
2168 }
2169 if (arity.min > 0 && !empty(cluster)) {
2170 if (tracer.isDebug()) {tracer.debug("Trying to process '%s' as option parameter%n", cluster);}
2171 }
2172
2173
2174
2175 if (!empty(cluster)) {
2176 args.push(cluster);
2177 }
2178 int consumed = applyOption(field, Option.class, arity, paramAttachedToOption, args, initialized, argDescription);
2179
2180 if (empty(cluster) || consumed > 0 || args.isEmpty()) {
2181 return;
2182 }
2183 cluster = args.pop();
2184 } else {
2185 if (cluster.length() == 0) {
2186 return;
2187 }
2188
2189
2190 if (arg.endsWith(cluster)) {
2191 args.push(paramAttachedToOption ? prefix + cluster : cluster);
2192 if (args.peek().equals(arg)) {
2193 if (tracer.isDebug()) {tracer.debug("Could not match any short options in %s, deciding whether to treat as unmatched option or positional parameter...%n", arg);}
2194 if (resemblesOption(arg)) { handleUnmatchedArguments(args.pop()); return; }
2195 processPositionalParameter(required, initialized, args);
2196 return;
2197 }
2198
2199 if (tracer.isDebug()) {tracer.debug("No option found for %s in %s%n", cluster, arg);}
2200 handleUnmatchedArguments(args.pop());
2201 } else {
2202 args.push(cluster);
2203 if (tracer.isDebug()) {tracer.debug("%s is not an option parameter for %s%n", cluster, arg);}
2204 processPositionalParameter(required, initialized, args);
2205 }
2206 return;
2207 }
2208 } while (true);
2209 }
2210
2211 private int applyOption(Field field,
2212 Class<?> annotation,
2213 Range arity,
2214 boolean valueAttachedToOption,
2215 Stack<String> args,
2216 Set<Field> initialized,
2217 String argDescription) throws Exception {
2218 updateHelpRequested(field);
2219 int length = args.size();
2220 assertNoMissingParameters(field, arity.min, args);
2221
2222 Class<?> cls = field.getType();
2223 if (cls.isArray()) {
2224 return applyValuesToArrayField(field, annotation, arity, args, cls, argDescription);
2225 }
2226 if (Collection.class.isAssignableFrom(cls)) {
2227 return applyValuesToCollectionField(field, annotation, arity, args, cls, argDescription);
2228 }
2229 if (Map.class.isAssignableFrom(cls)) {
2230 return applyValuesToMapField(field, annotation, arity, args, cls, argDescription);
2231 }
2232 cls = getTypeAttribute(field)[0];
2233 return applyValueToSingleValuedField(field, arity, args, cls, initialized, argDescription);
2234 }
2235
2236 private int applyValueToSingleValuedField(Field field,
2237 Range arity,
2238 Stack<String> args,
2239 Class<?> cls,
2240 Set<Field> initialized,
2241 String argDescription) throws Exception {
2242 boolean noMoreValues = args.isEmpty();
2243 String value = args.isEmpty() ? null : trim(args.pop());
2244 int result = arity.min;
2245
2246
2247 if ((cls == Boolean.class || cls == Boolean.TYPE) && arity.min <= 0) {
2248
2249
2250 if (arity.max > 0 && ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
2251 result = 1;
2252 } else {
2253 if (value != null) {
2254 args.push(value);
2255 }
2256 Boolean currentValue = (Boolean) field.get(command);
2257 value = String.valueOf(currentValue == null ? true : !currentValue);
2258 }
2259 }
2260 if (noMoreValues && value == null) {
2261 return 0;
2262 }
2263 ITypeConverter<?> converter = getTypeConverter(cls, field);
2264 Object newValue = tryConvert(field, -1, converter, value, cls);
2265 Object oldValue = field.get(command);
2266 TraceLevel level = TraceLevel.INFO;
2267 String traceMessage = "Setting %s field '%s.%s' to '%5$s' (was '%4$s') for %6$s%n";
2268 if (initialized != null) {
2269 if (initialized.contains(field)) {
2270 if (!isOverwrittenOptionsAllowed()) {
2271 throw new OverwrittenOptionException(CommandLine.this, optionDescription("", field, 0) + " should be specified only once");
2272 }
2273 level = TraceLevel.WARN;
2274 traceMessage = "Overwriting %s field '%s.%s' value '%s' with '%s' for %s%n";
2275 }
2276 initialized.add(field);
2277 }
2278 if (tracer.level.isEnabled(level)) { level.print(tracer, traceMessage, field.getType().getSimpleName(),
2279 field.getDeclaringClass().getSimpleName(), field.getName(), String.valueOf(oldValue), String.valueOf(newValue), argDescription);
2280 }
2281 field.set(command, newValue);
2282 return result;
2283 }
2284 private int applyValuesToMapField(Field field,
2285 Class<?> annotation,
2286 Range arity,
2287 Stack<String> args,
2288 Class<?> cls,
2289 String argDescription) throws Exception {
2290 Class<?>[] classes = getTypeAttribute(field);
2291 if (classes.length < 2) { throw new ParameterException(CommandLine.this, "Field " + field + " needs two types (one for the map key, one for the value) but only has " + classes.length + " types configured."); }
2292 ITypeConverter<?> keyConverter = getTypeConverter(classes[0], field);
2293 ITypeConverter<?> valueConverter = getTypeConverter(classes[1], field);
2294 Map<Object, Object> result = (Map<Object, Object>) field.get(command);
2295 if (result == null) {
2296 result = createMap(cls);
2297 field.set(command, result);
2298 }
2299 int originalSize = result.size();
2300 consumeMapArguments(field, arity, args, classes, keyConverter, valueConverter, result, argDescription);
2301 return result.size() - originalSize;
2302 }
2303
2304 private void consumeMapArguments(Field field,
2305 Range arity,
2306 Stack<String> args,
2307 Class<?>[] classes,
2308 ITypeConverter<?> keyConverter,
2309 ITypeConverter<?> valueConverter,
2310 Map<Object, Object> result,
2311 String argDescription) throws Exception {
2312
2313 for (int i = 0; i < arity.min; i++) {
2314 consumeOneMapArgument(field, arity, args, classes, keyConverter, valueConverter, result, i, argDescription);
2315 }
2316
2317 for (int i = arity.min; i < arity.max && !args.isEmpty(); i++) {
2318 if (!field.isAnnotationPresent(Parameters.class)) {
2319 if (commands.containsKey(args.peek()) || isOption(args.peek())) {
2320 return;
2321 }
2322 }
2323 consumeOneMapArgument(field, arity, args, classes, keyConverter, valueConverter, result, i, argDescription);
2324 }
2325 }
2326
2327 private void consumeOneMapArgument(Field field,
2328 Range arity,
2329 Stack<String> args,
2330 Class<?>[] classes,
2331 ITypeConverter<?> keyConverter, ITypeConverter<?> valueConverter,
2332 Map<Object, Object> result,
2333 int index,
2334 String argDescription) throws Exception {
2335 String[] values = split(trim(args.pop()), field);
2336 for (String value : values) {
2337 String[] keyValue = value.split("=");
2338 if (keyValue.length < 2) {
2339 String splitRegex = splitRegex(field);
2340 if (splitRegex.length() == 0) {
2341 throw new ParameterException(CommandLine.this, "Value for option " + optionDescription("", field,
2342 0) + " should be in KEY=VALUE format but was " + value);
2343 } else {
2344 throw new ParameterException(CommandLine.this, "Value for option " + optionDescription("", field,
2345 0) + " should be in KEY=VALUE[" + splitRegex + "KEY=VALUE]... format but was " + value);
2346 }
2347 }
2348 Object mapKey = tryConvert(field, index, keyConverter, keyValue[0], classes[0]);
2349 Object mapValue = tryConvert(field, index, valueConverter, keyValue[1], classes[1]);
2350 result.put(mapKey, mapValue);
2351 if (tracer.isInfo()) {tracer.info("Putting [%s : %s] in %s<%s, %s> field '%s.%s' for %s%n", String.valueOf(mapKey), String.valueOf(mapValue),
2352 result.getClass().getSimpleName(), classes[0].getSimpleName(), classes[1].getSimpleName(), field.getDeclaringClass().getSimpleName(), field.getName(), argDescription);}
2353 }
2354 }
2355
2356 private void checkMaxArityExceeded(Range arity, int remainder, Field field, String[] values) {
2357 if (values.length <= remainder) { return; }
2358 String desc = arity.max == remainder ? "" + remainder : arity + ", remainder=" + remainder;
2359 throw new MaxValuesforFieldExceededException(CommandLine.this, optionDescription("", field, -1) +
2360 " max number of values (" + arity.max + ") exceeded: remainder is " + remainder + " but " +
2361 values.length + " values were specified: " + Arrays.toString(values));
2362 }
2363
2364 private int applyValuesToArrayField(Field field,
2365 Class<?> annotation,
2366 Range arity,
2367 Stack<String> args,
2368 Class<?> cls,
2369 String argDescription) throws Exception {
2370 Object existing = field.get(command);
2371 int length = existing == null ? 0 : Array.getLength(existing);
2372 Class<?> type = getTypeAttribute(field)[0];
2373 List<Object> converted = consumeArguments(field, annotation, arity, args, type, length, argDescription);
2374 List<Object> newValues = new ArrayList<Object>();
2375 for (int i = 0; i < length; i++) {
2376 newValues.add(Array.get(existing, i));
2377 }
2378 for (Object obj : converted) {
2379 if (obj instanceof Collection<?>) {
2380 newValues.addAll((Collection<?>) obj);
2381 } else {
2382 newValues.add(obj);
2383 }
2384 }
2385 Object array = Array.newInstance(type, newValues.size());
2386 field.set(command, array);
2387 for (int i = 0; i < newValues.size(); i++) {
2388 Array.set(array, i, newValues.get(i));
2389 }
2390 return converted.size();
2391 }
2392
2393 @SuppressWarnings("unchecked")
2394 private int applyValuesToCollectionField(Field field,
2395 Class<?> annotation,
2396 Range arity,
2397 Stack<String> args,
2398 Class<?> cls,
2399 String argDescription) throws Exception {
2400 Collection<Object> collection = (Collection<Object>) field.get(command);
2401 Class<?> type = getTypeAttribute(field)[0];
2402 int length = collection == null ? 0 : collection.size();
2403 List<Object> converted = consumeArguments(field, annotation, arity, args, type, length, argDescription);
2404 if (collection == null) {
2405 collection = createCollection(cls);
2406 field.set(command, collection);
2407 }
2408 for (Object element : converted) {
2409 if (element instanceof Collection<?>) {
2410 collection.addAll((Collection<?>) element);
2411 } else {
2412 collection.add(element);
2413 }
2414 }
2415 return converted.size();
2416 }
2417
2418 private List<Object> consumeArguments(Field field,
2419 Class<?> annotation,
2420 Range arity,
2421 Stack<String> args,
2422 Class<?> type,
2423 int originalSize,
2424 String argDescription) throws Exception {
2425 List<Object> result = new ArrayList<Object>();
2426
2427
2428 for (int i = 0; i < arity.min; i++) {
2429 consumeOneArgument(field, arity, args, type, result, i, originalSize, argDescription);
2430 }
2431
2432 for (int i = arity.min; i < arity.max && !args.isEmpty(); i++) {
2433 if (annotation != Parameters.class) {
2434 if (commands.containsKey(args.peek()) || isOption(args.peek())) {
2435 return result;
2436 }
2437 }
2438 consumeOneArgument(field, arity, args, type, result, i, originalSize, argDescription);
2439 }
2440 return result;
2441 }
2442
2443 private int consumeOneArgument(Field field,
2444 Range arity,
2445 Stack<String> args,
2446 Class<?> type,
2447 List<Object> result,
2448 int index,
2449 int originalSize,
2450 String argDescription) throws Exception {
2451 String[] values = split(trim(args.pop()), field);
2452 ITypeConverter<?> converter = getTypeConverter(type, field);
2453
2454 for (int j = 0; j < values.length; j++) {
2455 result.add(tryConvert(field, index, converter, values[j], type));
2456 if (tracer.isInfo()) {
2457 if (field.getType().isArray()) {
2458 tracer.info("Adding [%s] to %s[] field '%s.%s' for %s%n", String.valueOf(result.get(result.size() - 1)), type.getSimpleName(), field.getDeclaringClass().getSimpleName(), field.getName(), argDescription);
2459 } else {
2460 tracer.info("Adding [%s] to %s<%s> field '%s.%s' for %s%n", String.valueOf(result.get(result.size() - 1)), field.getType().getSimpleName(), type.getSimpleName(), field.getDeclaringClass().getSimpleName(), field.getName(), argDescription);
2461 }
2462 }
2463 }
2464
2465 return ++index;
2466 }
2467
2468 private String splitRegex(Field field) {
2469 if (field.isAnnotationPresent(Option.class)) { return field.getAnnotation(Option.class).split(); }
2470 if (field.isAnnotationPresent(Parameters.class)) { return field.getAnnotation(Parameters.class).split(); }
2471 return "";
2472 }
2473 private String[] split(String value, Field field) {
2474 String regex = splitRegex(field);
2475 return regex.length() == 0 ? new String[] {value} : value.split(regex);
2476 }
2477
2478
2479
2480
2481
2482
2483
2484 private boolean isOption(String arg) {
2485 if ("--".equals(arg)) {
2486 return true;
2487 }
2488
2489 if (optionName2Field.containsKey(arg)) {
2490 return true;
2491 }
2492 int separatorIndex = arg.indexOf(separator);
2493 if (separatorIndex > 0) {
2494 if (optionName2Field.containsKey(arg.substring(0, separatorIndex))) {
2495 return true;
2496 }
2497 }
2498 return (arg.length() > 2 && arg.startsWith("-") && singleCharOption2Field.containsKey(arg.charAt(1)));
2499 }
2500 private Object tryConvert(Field field, int index, ITypeConverter<?> converter, String value, Class<?> type)
2501 throws Exception {
2502 try {
2503 return converter.convert(value);
2504 } catch (TypeConversionException ex) {
2505 throw new ParameterException(CommandLine.this, ex.getMessage() + optionDescription(" for ", field, index));
2506 } catch (Exception other) {
2507 String desc = optionDescription(" for ", field, index) + ": " + other;
2508 throw new ParameterException(CommandLine.this, "Could not convert '" + value + "' to " + type.getSimpleName() + desc, other);
2509 }
2510 }
2511
2512 private String optionDescription(String prefix, Field field, int index) {
2513 Help.IParamLabelRenderer labelRenderer = Help.createMinimalParamLabelRenderer();
2514 String desc = "";
2515 if (field.isAnnotationPresent(Option.class)) {
2516 desc = prefix + "option '" + field.getAnnotation(Option.class).names()[0] + "'";
2517 if (index >= 0) {
2518 Range arity = Range.optionArity(field);
2519 if (arity.max > 1) {
2520 desc += " at index " + index;
2521 }
2522 desc += " (" + labelRenderer.renderParameterLabel(field, Help.Ansi.OFF, Collections.<IStyle>emptyList()) + ")";
2523 }
2524 } else if (field.isAnnotationPresent(Parameters.class)) {
2525 Range indexRange = Range.parameterIndex(field);
2526 Text label = labelRenderer.renderParameterLabel(field, Help.Ansi.OFF, Collections.<IStyle>emptyList());
2527 desc = prefix + "positional parameter at index " + indexRange + " (" + label + ")";
2528 }
2529 return desc;
2530 }
2531
2532 private boolean isAnyHelpRequested() { return isHelpRequested || versionHelpRequested || usageHelpRequested; }
2533
2534 private void updateHelpRequested(Field field) {
2535 if (field.isAnnotationPresent(Option.class)) {
2536 isHelpRequested |= is(field, "help", field.getAnnotation(Option.class).help());
2537 CommandLine.this.versionHelpRequested |= is(field, "versionHelp", field.getAnnotation(Option.class).versionHelp());
2538 CommandLine.this.usageHelpRequested |= is(field, "usageHelp", field.getAnnotation(Option.class).usageHelp());
2539 }
2540 }
2541 private boolean is(Field f, String description, boolean value) {
2542 if (value) { if (tracer.isInfo()) {tracer.info("Field '%s.%s' has '%s' annotation: not validating required fields%n", f.getDeclaringClass().getSimpleName(), f.getName(), description); }}
2543 return value;
2544 }
2545 @SuppressWarnings("unchecked")
2546 private Collection<Object> createCollection(Class<?> collectionClass) throws Exception {
2547 if (collectionClass.isInterface()) {
2548 if (List.class.isAssignableFrom(collectionClass)) {
2549 return new ArrayList<Object>();
2550 } else if (SortedSet.class.isAssignableFrom(collectionClass)) {
2551 return new TreeSet<Object>();
2552 } else if (Set.class.isAssignableFrom(collectionClass)) {
2553 return new LinkedHashSet<Object>();
2554 } else if (Queue.class.isAssignableFrom(collectionClass)) {
2555 return new LinkedList<Object>();
2556 }
2557 return new ArrayList<Object>();
2558 }
2559
2560 return (Collection<Object>) collectionClass.newInstance();
2561 }
2562 private Map<Object, Object> createMap(Class<?> mapClass) throws Exception {
2563 try {
2564 return (Map<Object, Object>) mapClass.newInstance();
2565 } catch (Exception ignored) {}
2566 return new LinkedHashMap<Object, Object>();
2567 }
2568 private ITypeConverter<?> getTypeConverter(final Class<?> type, Field field) {
2569 ITypeConverter<?> result = converterRegistry.get(type);
2570 if (result != null) {
2571 return result;
2572 }
2573 if (type.isEnum()) {
2574 return new ITypeConverter<Object>() {
2575 @Override
2576 @SuppressWarnings("unchecked")
2577 public Object convert(String value) throws Exception {
2578 return Enum.valueOf((Class<Enum>) type, value);
2579 }
2580 };
2581 }
2582 throw new MissingTypeConverterException(CommandLine.this, "No TypeConverter registered for " + type.getName() + " of field " + field);
2583 }
2584
2585 private void assertNoMissingParameters(Field field, int arity, Stack<String> args) {
2586 if (arity > args.size()) {
2587 if (arity == 1) {
2588 if (field.isAnnotationPresent(Option.class)) {
2589 throw new MissingParameterException(CommandLine.this, "Missing required parameter for " +
2590 optionDescription("", field, 0));
2591 }
2592 Range indexRange = Range.parameterIndex(field);
2593 Help.IParamLabelRenderer labelRenderer = Help.createMinimalParamLabelRenderer();
2594 String sep = "";
2595 String names = "";
2596 int count = 0;
2597 for (int i = indexRange.min; i < positionalParametersFields.size(); i++) {
2598 if (Range.parameterArity(positionalParametersFields.get(i)).min > 0) {
2599 names += sep + labelRenderer.renderParameterLabel(positionalParametersFields.get(i),
2600 Help.Ansi.OFF, Collections.<IStyle>emptyList());
2601 sep = ", ";
2602 count++;
2603 }
2604 }
2605 String msg = "Missing required parameter";
2606 Range paramArity = Range.parameterArity(field);
2607 if (paramArity.isVariable) {
2608 msg += "s at positions " + indexRange + ": ";
2609 } else {
2610 msg += (count > 1 ? "s: " : ": ");
2611 }
2612 throw new MissingParameterException(CommandLine.this, msg + names);
2613 }
2614 if (args.isEmpty()) {
2615 throw new MissingParameterException(CommandLine.this, optionDescription("", field, 0) +
2616 " requires at least " + arity + " values, but none were specified.");
2617 }
2618 throw new MissingParameterException(CommandLine.this, optionDescription("", field, 0) +
2619 " requires at least " + arity + " values, but only " + args.size() + " were specified: " + reverse(args));
2620 }
2621 }
2622 private String trim(String value) {
2623 return unquote(value);
2624 }
2625
2626 private String unquote(String value) {
2627 return value == null
2628 ? null
2629 : (value.length() > 1 && value.startsWith("\"") && value.endsWith("\""))
2630 ? value.substring(1, value.length() - 1)
2631 : value;
2632 }
2633 }
2634 private static class PositionalParametersSorter implements Comparator<Field> {
2635 @Override
2636 public int compare(Field o1, Field o2) {
2637 int result = Range.parameterIndex(o1).compareTo(Range.parameterIndex(o2));
2638 return (result == 0) ? Range.parameterArity(o1).compareTo(Range.parameterArity(o2)) : result;
2639 }
2640 }
2641
2642
2643
2644 private static class BuiltIn {
2645 static class PathConverter implements ITypeConverter<Path> {
2646 @Override public Path convert(final String value) { return Paths.get(value); }
2647 }
2648 static class StringConverter implements ITypeConverter<String> {
2649 @Override
2650 public String convert(String value) { return value; }
2651 }
2652 static class StringBuilderConverter implements ITypeConverter<StringBuilder> {
2653 @Override
2654 public StringBuilder convert(String value) { return new StringBuilder(value); }
2655 }
2656 static class CharSequenceConverter implements ITypeConverter<CharSequence> {
2657 @Override
2658 public String convert(String value) { return value; }
2659 }
2660
2661 static class ByteConverter implements ITypeConverter<Byte> {
2662 @Override
2663 public Byte convert(String value) { return Byte.valueOf(value); }
2664 }
2665
2666 static class BooleanConverter implements ITypeConverter<Boolean> {
2667 @Override
2668 public Boolean convert(String value) {
2669 if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
2670 return Boolean.parseBoolean(value);
2671 } else {
2672 throw new TypeConversionException("'" + value + "' is not a boolean");
2673 }
2674 }
2675 }
2676 static class CharacterConverter implements ITypeConverter<Character> {
2677 @Override
2678 public Character convert(String value) {
2679 if (value.length() > 1) {
2680 throw new TypeConversionException("'" + value + "' is not a single character");
2681 }
2682 return value.charAt(0);
2683 }
2684 }
2685
2686 static class ShortConverter implements ITypeConverter<Short> {
2687 @Override
2688 public Short convert(String value) { return Short.valueOf(value); }
2689 }
2690
2691 static class IntegerConverter implements ITypeConverter<Integer> {
2692 @Override
2693 public Integer convert(String value) { return Integer.valueOf(value); }
2694 }
2695
2696 static class LongConverter implements ITypeConverter<Long> {
2697 @Override
2698 public Long convert(String value) { return Long.valueOf(value); }
2699 }
2700 static class FloatConverter implements ITypeConverter<Float> {
2701 @Override
2702 public Float convert(String value) { return Float.valueOf(value); }
2703 }
2704 static class DoubleConverter implements ITypeConverter<Double> {
2705 @Override
2706 public Double convert(String value) { return Double.valueOf(value); }
2707 }
2708 static class FileConverter implements ITypeConverter<File> {
2709 @Override
2710 public File convert(String value) { return new File(value); }
2711 }
2712 static class URLConverter implements ITypeConverter<URL> {
2713 @Override
2714 public URL convert(String value) throws MalformedURLException { return new URL(value); }
2715 }
2716 static class URIConverter implements ITypeConverter<URI> {
2717 @Override
2718 public URI convert(String value) throws URISyntaxException { return new URI(value); }
2719 }
2720
2721 static class ISO8601DateConverter implements ITypeConverter<Date> {
2722 @Override
2723 public Date convert(String value) {
2724 try {
2725 return new SimpleDateFormat("yyyy-MM-dd").parse(value);
2726 } catch (ParseException e) {
2727 throw new TypeConversionException("'" + value + "' is not a yyyy-MM-dd date");
2728 }
2729 }
2730 }
2731
2732
2733 static class ISO8601TimeConverter implements ITypeConverter<Time> {
2734 @Override
2735 public Time convert(String value) {
2736 try {
2737 if (value.length() <= 5) {
2738 return new Time(new SimpleDateFormat("HH:mm").parse(value).getTime());
2739 } else if (value.length() <= 8) {
2740 return new Time(new SimpleDateFormat("HH:mm:ss").parse(value).getTime());
2741 } else if (value.length() <= 12) {
2742 try {
2743 return new Time(new SimpleDateFormat("HH:mm:ss.SSS").parse(value).getTime());
2744 } catch (ParseException e2) {
2745 return new Time(new SimpleDateFormat("HH:mm:ss,SSS").parse(value).getTime());
2746 }
2747 }
2748 } catch (ParseException ignored) {
2749
2750 }
2751 throw new TypeConversionException("'" + value + "' is not a HH:mm[:ss[.SSS]] time");
2752 }
2753 }
2754 static class BigDecimalConverter implements ITypeConverter<BigDecimal> {
2755 @Override
2756 public BigDecimal convert(String value) { return new BigDecimal(value); }
2757 }
2758 static class BigIntegerConverter implements ITypeConverter<BigInteger> {
2759 @Override
2760 public BigInteger convert(String value) { return new BigInteger(value); }
2761 }
2762 static class CharsetConverter implements ITypeConverter<Charset> {
2763 @Override
2764 public Charset convert(String s) { return Charset.forName(s); }
2765 }
2766
2767 static class InetAddressConverter implements ITypeConverter<InetAddress> {
2768 @Override
2769 public InetAddress convert(String s) throws Exception { return InetAddress.getByName(s); }
2770 }
2771 static class PatternConverter implements ITypeConverter<Pattern> {
2772 @Override
2773 public Pattern convert(String s) { return Pattern.compile(s); }
2774 }
2775 static class UUIDConverter implements ITypeConverter<UUID> {
2776 @Override
2777 public UUID convert(String s) throws Exception { return UUID.fromString(s); }
2778 }
2779 private BuiltIn() {}
2780 }
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815 public static class Help {
2816
2817 protected static final String DEFAULT_COMMAND_NAME = "<main class>";
2818
2819
2820 protected static final String DEFAULT_SEPARATOR = "=";
2821
2822 private final static int usageHelpWidth = 80;
2823 private final static int optionsColumnWidth = 2 + 2 + 1 + 24;
2824 private final Object command;
2825 private final Map<String, Help> commands = new LinkedHashMap<String, Help>();
2826 final ColorScheme colorScheme;
2827
2828
2829 public final List<Field> optionFields;
2830
2831
2832 public final List<Field> positionalParametersFields;
2833
2834
2835
2836
2837 public String separator;
2838
2839
2840
2841 public String commandName = DEFAULT_COMMAND_NAME;
2842
2843
2844
2845
2846
2847 public String[] description = {};
2848
2849
2850
2851
2852
2853 public String[] customSynopsis = {};
2854
2855
2856
2857
2858
2859 public String[] header = {};
2860
2861
2862
2863
2864
2865 public String[] footer = {};
2866
2867
2868
2869
2870
2871 public IParamLabelRenderer parameterLabelRenderer;
2872
2873
2874 public Boolean abbreviateSynopsis;
2875
2876
2877 public Boolean sortOptions;
2878
2879
2880 public Boolean showDefaultValues;
2881
2882
2883 public Character requiredOptionMarker;
2884
2885
2886 public String headerHeading;
2887
2888
2889 public String synopsisHeading;
2890
2891
2892 public String descriptionHeading;
2893
2894
2895 public String parameterListHeading;
2896
2897
2898 public String optionListHeading;
2899
2900
2901 public String commandListHeading;
2902
2903
2904 public String footerHeading;
2905
2906
2907
2908
2909 public Help(Object command) {
2910 this(command, Ansi.AUTO);
2911 }
2912
2913
2914
2915
2916
2917 public Help(Object command, Ansi ansi) {
2918 this(command, defaultColorScheme(ansi));
2919 }
2920
2921
2922
2923
2924
2925 public Help(Object command, ColorScheme colorScheme) {
2926 this.command = Assert.notNull(command, "command");
2927 this.colorScheme = Assert.notNull(colorScheme, "colorScheme").applySystemProperties();
2928 List<Field> options = new ArrayList<Field>();
2929 List<Field> operands = new ArrayList<Field>();
2930 Class<?> cls = command.getClass();
2931 while (cls != null) {
2932 for (Field field : cls.getDeclaredFields()) {
2933 field.setAccessible(true);
2934 if (field.isAnnotationPresent(Option.class)) {
2935 Option option = field.getAnnotation(Option.class);
2936 if (!option.hidden()) {
2937
2938 options.add(field);
2939 }
2940 }
2941 if (field.isAnnotationPresent(Parameters.class)) {
2942 operands.add(field);
2943 }
2944 }
2945
2946 if (cls.isAnnotationPresent(Command.class)) {
2947 Command cmd = cls.getAnnotation(Command.class);
2948 if (DEFAULT_COMMAND_NAME.equals(commandName)) {
2949 commandName = cmd.name();
2950 }
2951 separator = (separator == null) ? cmd.separator() : separator;
2952 abbreviateSynopsis = (abbreviateSynopsis == null) ? cmd.abbreviateSynopsis() : abbreviateSynopsis;
2953 sortOptions = (sortOptions == null) ? cmd.sortOptions() : sortOptions;
2954 requiredOptionMarker = (requiredOptionMarker == null) ? cmd.requiredOptionMarker() : requiredOptionMarker;
2955 showDefaultValues = (showDefaultValues == null) ? cmd.showDefaultValues() : showDefaultValues;
2956 customSynopsis = empty(customSynopsis) ? cmd.customSynopsis() : customSynopsis;
2957 description = empty(description) ? cmd.description() : description;
2958 header = empty(header) ? cmd.header() : header;
2959 footer = empty(footer) ? cmd.footer() : footer;
2960 headerHeading = empty(headerHeading) ? cmd.headerHeading() : headerHeading;
2961 synopsisHeading = empty(synopsisHeading) || "Usage: ".equals(synopsisHeading) ? cmd.synopsisHeading() : synopsisHeading;
2962 descriptionHeading = empty(descriptionHeading) ? cmd.descriptionHeading() : descriptionHeading;
2963 parameterListHeading = empty(parameterListHeading) ? cmd.parameterListHeading() : parameterListHeading;
2964 optionListHeading = empty(optionListHeading) ? cmd.optionListHeading() : optionListHeading;
2965 commandListHeading = empty(commandListHeading) || "Commands:%n".equals(commandListHeading) ? cmd.commandListHeading() : commandListHeading;
2966 footerHeading = empty(footerHeading) ? cmd.footerHeading() : footerHeading;
2967 }
2968 cls = cls.getSuperclass();
2969 }
2970 sortOptions = (sortOptions == null) ? true : sortOptions;
2971 abbreviateSynopsis = (abbreviateSynopsis == null) ? false : abbreviateSynopsis;
2972 requiredOptionMarker = (requiredOptionMarker == null) ? ' ' : requiredOptionMarker;
2973 showDefaultValues = (showDefaultValues == null) ? false : showDefaultValues;
2974 synopsisHeading = (synopsisHeading == null) ? "Usage: " : synopsisHeading;
2975 commandListHeading = (commandListHeading == null) ? "Commands:%n" : commandListHeading;
2976 separator = (separator == null) ? DEFAULT_SEPARATOR : separator;
2977 parameterLabelRenderer = createDefaultParamLabelRenderer();
2978 Collections.sort(operands, new PositionalParametersSorter());
2979 positionalParametersFields = Collections.unmodifiableList(operands);
2980 optionFields = Collections.unmodifiableList(options);
2981 }
2982
2983
2984
2985
2986
2987
2988 public Help addAllSubcommands(Map<String, CommandLine> commands) {
2989 if (commands != null) {
2990 for (Map.Entry<String, CommandLine> entry : commands.entrySet()) {
2991 addSubcommand(entry.getKey(), entry.getValue().getCommand());
2992 }
2993 }
2994 return this;
2995 }
2996
2997
2998
2999
3000
3001
3002 public Help addSubcommand(String commandName, Object command) {
3003 commands.put(commandName, new Help(command));
3004 return this;
3005 }
3006
3007
3008
3009
3010
3011
3012
3013 @Deprecated
3014 public String synopsis() { return synopsis(0); }
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024 public String synopsis(int synopsisHeadingLength) {
3025 if (!empty(customSynopsis)) { return customSynopsis(); }
3026 return abbreviateSynopsis ? abbreviatedSynopsis()
3027 : detailedSynopsis(synopsisHeadingLength, createShortOptionArityAndNameComparator(), true);
3028 }
3029
3030
3031
3032
3033 public String abbreviatedSynopsis() {
3034 StringBuilder sb = new StringBuilder();
3035 if (!optionFields.isEmpty()) {
3036 sb.append(" [OPTIONS]");
3037 }
3038
3039 for (Field positionalParam : positionalParametersFields) {
3040 if (!positionalParam.getAnnotation(Parameters.class).hidden()) {
3041 sb.append(' ').append(parameterLabelRenderer.renderParameterLabel(positionalParam, ansi(), colorScheme.parameterStyles));
3042 }
3043 }
3044 return colorScheme.commandText(commandName).toString()
3045 + (sb.toString()) + System.getProperty("line.separator");
3046 }
3047
3048
3049
3050
3051
3052
3053 @Deprecated
3054 public String detailedSynopsis(Comparator<Field> optionSort, boolean clusterBooleanOptions) {
3055 return detailedSynopsis(0, optionSort, clusterBooleanOptions);
3056 }
3057
3058
3059
3060
3061
3062
3063
3064 public String detailedSynopsis(int synopsisHeadingLength, Comparator<Field> optionSort, boolean clusterBooleanOptions) {
3065 Text optionText = ansi().new Text(0);
3066 List<Field> fields = new ArrayList<Field>(optionFields);
3067 if (optionSort != null) {
3068 Collections.sort(fields, optionSort);
3069 }
3070 if (clusterBooleanOptions) {
3071 List<Field> booleanOptions = new ArrayList<Field>();
3072 StringBuilder clusteredRequired = new StringBuilder("-");
3073 StringBuilder clusteredOptional = new StringBuilder("-");
3074 for (Field field : fields) {
3075 if (field.getType() == boolean.class || field.getType() == Boolean.class) {
3076 Option option = field.getAnnotation(Option.class);
3077 String shortestName = ShortestFirst.sort(option.names())[0];
3078 if (shortestName.length() == 2 && shortestName.startsWith("-")) {
3079 booleanOptions.add(field);
3080 if (option.required()) {
3081 clusteredRequired.append(shortestName.substring(1));
3082 } else {
3083 clusteredOptional.append(shortestName.substring(1));
3084 }
3085 }
3086 }
3087 }
3088 fields.removeAll(booleanOptions);
3089 if (clusteredRequired.length() > 1) {
3090 optionText = optionText.append(" ").append(colorScheme.optionText(clusteredRequired.toString()));
3091 }
3092 if (clusteredOptional.length() > 1) {
3093 optionText = optionText.append(" [").append(colorScheme.optionText(clusteredOptional.toString())).append("]");
3094 }
3095 }
3096 for (Field field : fields) {
3097 Option option = field.getAnnotation(Option.class);
3098 if (!option.hidden()) {
3099 if (option.required()) {
3100 optionText = appendOptionSynopsis(optionText, field, ShortestFirst.sort(option.names())[0], " ", "");
3101 if (isMultiValue(field)) {
3102 optionText = appendOptionSynopsis(optionText, field, ShortestFirst.sort(option.names())[0], " [", "]...");
3103 }
3104 } else {
3105 optionText = appendOptionSynopsis(optionText, field, ShortestFirst.sort(option.names())[0], " [", "]");
3106 if (isMultiValue(field)) {
3107 optionText = optionText.append("...");
3108 }
3109 }
3110 }
3111 }
3112 for (Field positionalParam : positionalParametersFields) {
3113 if (!positionalParam.getAnnotation(Parameters.class).hidden()) {
3114 optionText = optionText.append(" ");
3115 Text label = parameterLabelRenderer.renderParameterLabel(positionalParam, colorScheme.ansi(), colorScheme.parameterStyles);
3116 optionText = optionText.append(label);
3117 }
3118 }
3119
3120 int firstColumnLength = commandName.length() + synopsisHeadingLength;
3121
3122
3123 TextTable textTable = new TextTable(ansi(), firstColumnLength, usageHelpWidth - firstColumnLength);
3124 textTable.indentWrappedLines = 1;
3125
3126
3127 Text PADDING = Ansi.OFF.new Text(stringOf('X', synopsisHeadingLength));
3128 textTable.addRowValues(new Text[] {PADDING.append(colorScheme.commandText(commandName)), optionText});
3129 return textTable.toString().substring(synopsisHeadingLength);
3130 }
3131
3132 private Text appendOptionSynopsis(Text optionText, Field field, String optionName, String prefix, String suffix) {
3133 Text optionParamText = parameterLabelRenderer.renderParameterLabel(field, colorScheme.ansi(), colorScheme.optionParamStyles);
3134 return optionText.append(prefix)
3135 .append(colorScheme.optionText(optionName))
3136 .append(optionParamText)
3137 .append(suffix);
3138 }
3139
3140
3141
3142
3143
3144 public int synopsisHeadingLength() {
3145 String[] lines = Ansi.OFF.new Text(synopsisHeading).toString().split("\\r?\\n|\\r|%n", -1);
3146 return lines[lines.length - 1].length();
3147 }
3148
3149
3150
3151
3152
3153
3154
3155
3156 public String optionList() {
3157 Comparator<Field> sortOrder = sortOptions == null || sortOptions.booleanValue()
3158 ? createShortOptionNameComparator()
3159 : null;
3160 return optionList(createDefaultLayout(), sortOrder, parameterLabelRenderer);
3161 }
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171 public String optionList(Layout layout, Comparator<Field> optionSort, IParamLabelRenderer valueLabelRenderer) {
3172 List<Field> fields = new ArrayList<Field>(optionFields);
3173 if (optionSort != null) {
3174 Collections.sort(fields, optionSort);
3175 }
3176 layout.addOptions(fields, valueLabelRenderer);
3177 return layout.toString();
3178 }
3179
3180
3181
3182
3183
3184 public String parameterList() {
3185 return parameterList(createDefaultLayout(), parameterLabelRenderer);
3186 }
3187
3188
3189
3190
3191
3192
3193 public String parameterList(Layout layout, IParamLabelRenderer paramLabelRenderer) {
3194 layout.addPositionalParameters(positionalParametersFields, paramLabelRenderer);
3195 return layout.toString();
3196 }
3197
3198 private static String heading(Ansi ansi, String values, Object... params) {
3199 StringBuilder sb = join(ansi, new String[] {values}, new StringBuilder(), params);
3200 String result = sb.toString();
3201 result = result.endsWith(System.getProperty("line.separator"))
3202 ? result.substring(0, result.length() - System.getProperty("line.separator").length()) : result;
3203 return result + new String(spaces(countTrailingSpaces(values)));
3204 }
3205 private static char[] spaces(int length) { char[] result = new char[length]; Arrays.fill(result, ' '); return result; }
3206 private static int countTrailingSpaces(String str) {
3207 if (str == null) {return 0;}
3208 int trailingSpaces = 0;
3209 for (int i = str.length() - 1; i >= 0 && str.charAt(i) == ' '; i--) { trailingSpaces++; }
3210 return trailingSpaces;
3211 }
3212
3213
3214
3215
3216
3217
3218
3219 public static StringBuilder join(Ansi ansi, String[] values, StringBuilder sb, Object... params) {
3220 if (values != null) {
3221 TextTable table = new TextTable(ansi, usageHelpWidth);
3222 table.indentWrappedLines = 0;
3223 for (String summaryLine : values) {
3224 Text[] lines = ansi.new Text(format(summaryLine, params)).splitLines();
3225 for (Text line : lines) { table.addRowValues(line); }
3226 }
3227 table.toString(sb);
3228 }
3229 return sb;
3230 }
3231 private static String format(String formatString, Object... params) {
3232 return formatString == null ? "" : String.format(formatString, params);
3233 }
3234
3235
3236
3237
3238
3239
3240 public String customSynopsis(Object... params) {
3241 return join(ansi(), customSynopsis, new StringBuilder(), params).toString();
3242 }
3243
3244
3245
3246
3247
3248
3249 public String description(Object... params) {
3250 return join(ansi(), description, new StringBuilder(), params).toString();
3251 }
3252
3253
3254
3255
3256
3257
3258 public String header(Object... params) {
3259 return join(ansi(), header, new StringBuilder(), params).toString();
3260 }
3261
3262
3263
3264
3265
3266
3267 public String footer(Object... params) {
3268 return join(ansi(), footer, new StringBuilder(), params).toString();
3269 }
3270
3271
3272
3273
3274 public String headerHeading(Object... params) {
3275 return heading(ansi(), headerHeading, params);
3276 }
3277
3278
3279
3280
3281 public String synopsisHeading(Object... params) {
3282 return heading(ansi(), synopsisHeading, params);
3283 }
3284
3285
3286
3287
3288
3289 public String descriptionHeading(Object... params) {
3290 return empty(descriptionHeading) ? "" : heading(ansi(), descriptionHeading, params);
3291 }
3292
3293
3294
3295
3296
3297 public String parameterListHeading(Object... params) {
3298 return positionalParametersFields.isEmpty() ? "" : heading(ansi(), parameterListHeading, params);
3299 }
3300
3301
3302
3303
3304
3305 public String optionListHeading(Object... params) {
3306 return optionFields.isEmpty() ? "" : heading(ansi(), optionListHeading, params);
3307 }
3308
3309
3310
3311
3312
3313 public String commandListHeading(Object... params) {
3314 return commands.isEmpty() ? "" : heading(ansi(), commandListHeading, params);
3315 }
3316
3317
3318
3319
3320 public String footerHeading(Object... params) {
3321 return heading(ansi(), footerHeading, params);
3322 }
3323
3324
3325 public String commandList() {
3326 if (commands.isEmpty()) { return ""; }
3327 int commandLength = maxLength(commands.keySet());
3328 Help.TextTable textTable = new Help.TextTable(ansi(),
3329 new Help.Column(commandLength + 2, 2, Help.Column.Overflow.SPAN),
3330 new Help.Column(usageHelpWidth - (commandLength + 2), 2, Help.Column.Overflow.WRAP));
3331
3332 for (Map.Entry<String, Help> entry : commands.entrySet()) {
3333 Help command = entry.getValue();
3334 String header = command.header != null && command.header.length > 0 ? command.header[0]
3335 : (command.description != null && command.description.length > 0 ? command.description[0] : "");
3336 textTable.addRowValues(colorScheme.commandText(entry.getKey()), ansi().new Text(header));
3337 }
3338 return textTable.toString();
3339 }
3340 private static int maxLength(Collection<String> any) {
3341 List<String> strings = new ArrayList<String>(any);
3342 Collections.sort(strings, Collections.reverseOrder(Help.shortestFirst()));
3343 return strings.get(0).length();
3344 }
3345 private static String join(String[] names, int offset, int length, String separator) {
3346 if (names == null) { return ""; }
3347 StringBuilder result = new StringBuilder();
3348 for (int i = offset; i < offset + length; i++) {
3349 result.append((i > offset) ? separator : "").append(names[i]);
3350 }
3351 return result.toString();
3352 }
3353 private static String stringOf(char chr, int length) {
3354 char[] buff = new char[length];
3355 Arrays.fill(buff, chr);
3356 return new String(buff);
3357 }
3358
3359
3360
3361 public Layout createDefaultLayout() {
3362 return new Layout(colorScheme, new TextTable(colorScheme.ansi()), createDefaultOptionRenderer(), createDefaultParameterRenderer());
3363 }
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378 public IOptionRenderer createDefaultOptionRenderer() {
3379 DefaultOptionRenderer result = new DefaultOptionRenderer();
3380 result.requiredMarker = String.valueOf(requiredOptionMarker);
3381 if (showDefaultValues != null && showDefaultValues.booleanValue()) {
3382 result.command = this.command;
3383 }
3384 return result;
3385 }
3386
3387
3388
3389 public static IOptionRenderer createMinimalOptionRenderer() {
3390 return new MinimalOptionRenderer();
3391 }
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406 public IParameterRenderer createDefaultParameterRenderer() {
3407 DefaultParameterRenderer result = new DefaultParameterRenderer();
3408 result.requiredMarker = String.valueOf(requiredOptionMarker);
3409 return result;
3410 }
3411
3412
3413
3414 public static IParameterRenderer createMinimalParameterRenderer() {
3415 return new MinimalParameterRenderer();
3416 }
3417
3418
3419
3420 public static IParamLabelRenderer createMinimalParamLabelRenderer() {
3421 return new IParamLabelRenderer() {
3422 @Override
3423 public Text renderParameterLabel(Field field, Ansi ansi, List<IStyle> styles) {
3424 String text = DefaultParamLabelRenderer.renderParameterName(field);
3425 return ansi.apply(text, styles);
3426 }
3427 @Override
3428 public String separator() { return ""; }
3429 };
3430 }
3431
3432
3433
3434
3435
3436 public IParamLabelRenderer createDefaultParamLabelRenderer() {
3437 return new DefaultParamLabelRenderer(separator);
3438 }
3439
3440
3441
3442 public static Comparator<Field> createShortOptionNameComparator() {
3443 return new SortByShortestOptionNameAlphabetically();
3444 }
3445
3446
3447
3448 public static Comparator<Field> createShortOptionArityAndNameComparator() {
3449 return new SortByOptionArityAndNameAlphabetically();
3450 }
3451
3452
3453 public static Comparator<String> shortestFirst() {
3454 return new ShortestFirst();
3455 }
3456
3457
3458
3459
3460 public Ansi ansi() {
3461 return colorScheme.ansi;
3462 }
3463
3464
3465
3466
3467
3468 public interface IOptionRenderer {
3469
3470
3471
3472
3473
3474
3475
3476
3477 Text[][] render(Option option, Field field, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme);
3478 }
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491 static class DefaultOptionRenderer implements IOptionRenderer {
3492 public String requiredMarker = " ";
3493 public Object command;
3494 private String sep;
3495 private boolean showDefault;
3496 @Override
3497 public Text[][] render(Option option, Field field, IParamLabelRenderer paramLabelRenderer, ColorScheme scheme) {
3498 String[] names = ShortestFirst.sort(option.names());
3499 int shortOptionCount = names[0].length() == 2 ? 1 : 0;
3500 String shortOption = shortOptionCount > 0 ? names[0] : "";
3501 sep = shortOptionCount > 0 && names.length > 1 ? "," : "";
3502
3503 String longOption = join(names, shortOptionCount, names.length - shortOptionCount, ", ");
3504 Text longOptionText = createLongOptionText(field, paramLabelRenderer, scheme, longOption);
3505
3506 showDefault = command != null && !option.help() && !isBoolean(field.getType());
3507 Object defaultValue = createDefaultValue(field);
3508
3509 String requiredOption = option.required() ? requiredMarker : "";
3510 return renderDescriptionLines(option, scheme, requiredOption, shortOption, longOptionText, defaultValue);
3511 }
3512
3513 private Object createDefaultValue(Field field) {
3514 Object defaultValue = null;
3515 try {
3516 defaultValue = field.get(command);
3517 if (defaultValue == null) { showDefault = false; }
3518 else if (field.getType().isArray()) {
3519 StringBuilder sb = new StringBuilder();
3520 for (int i = 0; i < Array.getLength(defaultValue); i++) {
3521 sb.append(i > 0 ? ", " : "").append(Array.get(defaultValue, i));
3522 }
3523 defaultValue = sb.insert(0, "[").append("]").toString();
3524 }
3525 } catch (Exception ex) {
3526 showDefault = false;
3527 }
3528 return defaultValue;
3529 }
3530
3531 private Text createLongOptionText(Field field, IParamLabelRenderer renderer, ColorScheme scheme, String longOption) {
3532 Text paramLabelText = renderer.renderParameterLabel(field, scheme.ansi(), scheme.optionParamStyles);
3533
3534
3535 if (paramLabelText.length > 0 && longOption.length() == 0) {
3536 sep = renderer.separator();
3537
3538 int sepStart = paramLabelText.plainString().indexOf(sep);
3539 Text prefix = paramLabelText.substring(0, sepStart);
3540 paramLabelText = prefix.append(paramLabelText.substring(sepStart + sep.length()));
3541 }
3542 Text longOptionText = scheme.optionText(longOption);
3543 longOptionText = longOptionText.append(paramLabelText);
3544 return longOptionText;
3545 }
3546
3547 private Text[][] renderDescriptionLines(Option option,
3548 ColorScheme scheme,
3549 String requiredOption,
3550 String shortOption,
3551 Text longOptionText,
3552 Object defaultValue) {
3553 Text EMPTY = Ansi.EMPTY_TEXT;
3554 List<Text[]> result = new ArrayList<Text[]>();
3555 Text[] descriptionFirstLines = scheme.ansi().new Text(str(option.description(), 0)).splitLines();
3556 if (descriptionFirstLines.length == 0) {
3557 if (showDefault) {
3558 descriptionFirstLines = new Text[]{scheme.ansi().new Text(" Default: " + defaultValue)};
3559 showDefault = false;
3560 } else {
3561 descriptionFirstLines = new Text[]{ EMPTY };
3562 }
3563 }
3564 result.add(new Text[] { scheme.optionText(requiredOption), scheme.optionText(shortOption),
3565 scheme.ansi().new Text(sep), longOptionText, descriptionFirstLines[0] });
3566 for (int i = 1; i < descriptionFirstLines.length; i++) {
3567 result.add(new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, descriptionFirstLines[i] });
3568 }
3569 for (int i = 1; i < option.description().length; i++) {
3570 Text[] descriptionNextLines = scheme.ansi().new Text(option.description()[i]).splitLines();
3571 for (Text line : descriptionNextLines) {
3572 result.add(new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, line });
3573 }
3574 }
3575 if (showDefault) {
3576 result.add(new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, scheme.ansi().new Text(" Default: " + defaultValue) });
3577 }
3578 return result.toArray(new Text[result.size()][]);
3579 }
3580 }
3581
3582
3583 static class MinimalOptionRenderer implements IOptionRenderer {
3584 @Override
3585 public Text[][] render(Option option, Field field, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme) {
3586 Text optionText = scheme.optionText(option.names()[0]);
3587 Text paramLabelText = parameterLabelRenderer.renderParameterLabel(field, scheme.ansi(), scheme.optionParamStyles);
3588 optionText = optionText.append(paramLabelText);
3589 return new Text[][] {{ optionText,
3590 scheme.ansi().new Text(option.description().length == 0 ? "" : option.description()[0]) }};
3591 }
3592 }
3593
3594
3595 static class MinimalParameterRenderer implements IParameterRenderer {
3596 @Override
3597 public Text[][] render(Parameters param, Field field, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme) {
3598 return new Text[][] {{ parameterLabelRenderer.renderParameterLabel(field, scheme.ansi(), scheme.parameterStyles),
3599 scheme.ansi().new Text(param.description().length == 0 ? "" : param.description()[0]) }};
3600 }
3601 }
3602
3603
3604
3605
3606 public interface IParameterRenderer {
3607
3608
3609
3610
3611
3612
3613
3614
3615 Text[][] render(Parameters parameters, Field field, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme);
3616 }
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629 static class DefaultParameterRenderer implements IParameterRenderer {
3630 public String requiredMarker = " ";
3631 @Override
3632 public Text[][] render(Parameters params, Field field, IParamLabelRenderer paramLabelRenderer, ColorScheme scheme) {
3633 Text label = paramLabelRenderer.renderParameterLabel(field, scheme.ansi(), scheme.parameterStyles);
3634 Text requiredParameter = scheme.parameterText(Range.parameterArity(field).min > 0 ? requiredMarker : "");
3635
3636 Text EMPTY = Ansi.EMPTY_TEXT;
3637 List<Text[]> result = new ArrayList<Text[]>();
3638 Text[] descriptionFirstLines = scheme.ansi().new Text(str(params.description(), 0)).splitLines();
3639 if (descriptionFirstLines.length == 0) { descriptionFirstLines = new Text[]{ EMPTY }; }
3640 result.add(new Text[] { requiredParameter, EMPTY, EMPTY, label, descriptionFirstLines[0] });
3641 for (int i = 1; i < descriptionFirstLines.length; i++) {
3642 result.add(new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, descriptionFirstLines[i] });
3643 }
3644 for (int i = 1; i < params.description().length; i++) {
3645 Text[] descriptionNextLines = scheme.ansi().new Text(params.description()[i]).splitLines();
3646 for (Text line : descriptionNextLines) {
3647 result.add(new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, line });
3648 }
3649 }
3650 return result.toArray(new Text[result.size()][]);
3651 }
3652 }
3653
3654
3655 public interface IParamLabelRenderer {
3656
3657
3658
3659
3660
3661
3662
3663 Text renderParameterLabel(Field field, Ansi ansi, List<IStyle> styles);
3664
3665
3666
3667 String separator();
3668 }
3669
3670
3671
3672
3673
3674
3675 static class DefaultParamLabelRenderer implements IParamLabelRenderer {
3676
3677 public final String separator;
3678
3679 public DefaultParamLabelRenderer(String separator) {
3680 this.separator = Assert.notNull(separator, "separator");
3681 }
3682 @Override
3683 public String separator() { return separator; }
3684 @Override
3685 public Text renderParameterLabel(Field field, Ansi ansi, List<IStyle> styles) {
3686 boolean isOptionParameter = field.isAnnotationPresent(Option.class);
3687 Range arity = isOptionParameter ? Range.optionArity(field) : Range.parameterCapacity(field);
3688 String split = isOptionParameter ? field.getAnnotation(Option.class).split() : field.getAnnotation(Parameters.class).split();
3689 Text result = ansi.new Text("");
3690 String sep = isOptionParameter ? separator : "";
3691 Text paramName = ansi.apply(renderParameterName(field), styles);
3692 if (!empty(split)) { paramName = paramName.append("[" + split).append(paramName).append("]..."); }
3693 for (int i = 0; i < arity.min; i++) {
3694 result = result.append(sep).append(paramName);
3695 sep = " ";
3696 }
3697 if (arity.isVariable) {
3698 if (result.length == 0) {
3699 result = result.append(sep + "[").append(paramName).append("]...");
3700 } else if (!result.plainString().endsWith("...")) {
3701 result = result.append("...");
3702 }
3703 } else {
3704 sep = result.length == 0 ? (isOptionParameter ? separator : "") : " ";
3705 for (int i = arity.min; i < arity.max; i++) {
3706 if (sep.trim().length() == 0) {
3707 result = result.append(sep + "[").append(paramName);
3708 } else {
3709 result = result.append("[" + sep).append(paramName);
3710 }
3711 sep = " ";
3712 }
3713 for (int i = arity.min; i < arity.max; i++) { result = result.append("]"); }
3714 }
3715 return result;
3716 }
3717 private static String renderParameterName(Field field) {
3718 String result = null;
3719 if (field.isAnnotationPresent(Option.class)) {
3720 result = field.getAnnotation(Option.class).paramLabel();
3721 } else if (field.isAnnotationPresent(Parameters.class)) {
3722 result = field.getAnnotation(Parameters.class).paramLabel();
3723 }
3724 if (result != null && result.trim().length() > 0) {
3725 return result.trim();
3726 }
3727 String name = field.getName();
3728 if (Map.class.isAssignableFrom(field.getType())) {
3729 Class<?>[] paramTypes = getTypeAttribute(field);
3730 if (paramTypes.length < 2 || paramTypes[0] == null || paramTypes[1] == null) {
3731 name = "String=String";
3732 } else { name = paramTypes[0].getSimpleName() + "=" + paramTypes[1].getSimpleName(); }
3733 }
3734 return "<" + name + ">";
3735 }
3736 }
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746 public static class Layout {
3747 protected final ColorScheme colorScheme;
3748 protected final TextTable table;
3749 protected IOptionRenderer optionRenderer;
3750 protected IParameterRenderer parameterRenderer;
3751
3752
3753
3754
3755
3756 public Layout(ColorScheme colorScheme) { this(colorScheme, new TextTable(colorScheme.ansi())); }
3757
3758
3759
3760
3761
3762
3763 public Layout(ColorScheme colorScheme, TextTable textTable) {
3764 this(colorScheme, textTable, new DefaultOptionRenderer(), new DefaultParameterRenderer());
3765 }
3766
3767
3768
3769
3770
3771
3772 public Layout(ColorScheme colorScheme, TextTable textTable, IOptionRenderer optionRenderer, IParameterRenderer parameterRenderer) {
3773 this.colorScheme = Assert.notNull(colorScheme, "colorScheme");
3774 this.table = Assert.notNull(textTable, "textTable");
3775 this.optionRenderer = Assert.notNull(optionRenderer, "optionRenderer");
3776 this.parameterRenderer = Assert.notNull(parameterRenderer, "parameterRenderer");
3777 }
3778
3779
3780
3781
3782
3783
3784
3785 public void layout(Field field, Text[][] cellValues) {
3786 for (Text[] oneRow : cellValues) {
3787 table.addRowValues(oneRow);
3788 }
3789 }
3790
3791
3792
3793 public void addOptions(List<Field> fields, IParamLabelRenderer paramLabelRenderer) {
3794 for (Field field : fields) {
3795 Option option = field.getAnnotation(Option.class);
3796 if (!option.hidden()) {
3797 addOption(field, paramLabelRenderer);
3798 }
3799 }
3800 }
3801
3802
3803
3804
3805
3806
3807
3808 public void addOption(Field field, IParamLabelRenderer paramLabelRenderer) {
3809 Option option = field.getAnnotation(Option.class);
3810 Text[][] values = optionRenderer.render(option, field, paramLabelRenderer, colorScheme);
3811 layout(field, values);
3812 }
3813
3814
3815
3816 public void addPositionalParameters(List<Field> fields, IParamLabelRenderer paramLabelRenderer) {
3817 for (Field field : fields) {
3818 Parameters parameters = field.getAnnotation(Parameters.class);
3819 if (!parameters.hidden()) {
3820 addPositionalParameter(field, paramLabelRenderer);
3821 }
3822 }
3823 }
3824
3825
3826
3827
3828
3829
3830
3831 public void addPositionalParameter(Field field, IParamLabelRenderer paramLabelRenderer) {
3832 Parameters option = field.getAnnotation(Parameters.class);
3833 Text[][] values = parameterRenderer.render(option, field, paramLabelRenderer, colorScheme);
3834 layout(field, values);
3835 }
3836
3837 @Override public String toString() {
3838 return table.toString();
3839 }
3840 }
3841
3842 static class ShortestFirst implements Comparator<String> {
3843 @Override
3844 public int compare(String o1, String o2) {
3845 return o1.length() - o2.length();
3846 }
3847
3848 public static String[] sort(String[] names) {
3849 Arrays.sort(names, new ShortestFirst());
3850 return names;
3851 }
3852 }
3853
3854
3855 static class SortByShortestOptionNameAlphabetically implements Comparator<Field> {
3856 @Override
3857 public int compare(Field f1, Field f2) {
3858 Option o1 = f1.getAnnotation(Option.class);
3859 Option o2 = f2.getAnnotation(Option.class);
3860 if (o1 == null) { return 1; } else if (o2 == null) { return -1; }
3861 String[] names1 = ShortestFirst.sort(o1.names());
3862 String[] names2 = ShortestFirst.sort(o2.names());
3863 int result = names1[0].toUpperCase().compareTo(names2[0].toUpperCase());
3864 result = result == 0 ? -names1[0].compareTo(names2[0]) : result;
3865 return o1.help() == o2.help() ? result : o2.help() ? -1 : 1;
3866 }
3867 }
3868
3869 static class SortByOptionArityAndNameAlphabetically extends SortByShortestOptionNameAlphabetically {
3870 @Override
3871 public int compare(Field f1, Field f2) {
3872 Option o1 = f1.getAnnotation(Option.class);
3873 Option o2 = f2.getAnnotation(Option.class);
3874 Range arity1 = Range.optionArity(f1);
3875 Range arity2 = Range.optionArity(f2);
3876 int result = arity1.max - arity2.max;
3877 if (result == 0) {
3878 result = arity1.min - arity2.min;
3879 }
3880 if (result == 0) {
3881 if (isMultiValue(f1) && !isMultiValue(f2)) { result = 1; }
3882 if (!isMultiValue(f1) && isMultiValue(f2)) { result = -1; }
3883 }
3884 return result == 0 ? super.compare(f1, f2) : result;
3885 }
3886 }
3887
3888
3889
3890
3891
3892 public static class TextTable {
3893
3894
3895
3896
3897 public static class Cell {
3898
3899 public final int column;
3900
3901 public final int row;
3902
3903
3904
3905 public Cell(int column, int row) { this.column = column; this.row = row; }
3906 }
3907
3908
3909 public final Column[] columns;
3910
3911
3912 protected final List<Text> columnValues = new ArrayList<Text>();
3913
3914
3915 public int indentWrappedLines = 2;
3916
3917 private final Ansi ansi;
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929 public TextTable(Ansi ansi) {
3930
3931 this(ansi, new Column[] {
3932 new Column(2, 0, TRUNCATE),
3933 new Column(2, 0, TRUNCATE),
3934 new Column(1, 0, TRUNCATE),
3935 new Column(optionsColumnWidth - 2 - 2 - 1 , 1, SPAN),
3936 new Column(usageHelpWidth - optionsColumnWidth, 1, WRAP)
3937 });
3938 }
3939
3940
3941
3942
3943
3944
3945 public TextTable(Ansi ansi, int... columnWidths) {
3946 this.ansi = Assert.notNull(ansi, "ansi");
3947 columns = new Column[columnWidths.length];
3948 for (int i = 0; i < columnWidths.length; i++) {
3949 columns[i] = new Column(columnWidths[i], 0, i == columnWidths.length - 1 ? SPAN: WRAP);
3950 }
3951 }
3952
3953
3954
3955 public TextTable(Ansi ansi, Column... columns) {
3956 this.ansi = Assert.notNull(ansi, "ansi");
3957 this.columns = Assert.notNull(columns, "columns");
3958 if (columns.length == 0) { throw new IllegalArgumentException("At least one column is required"); }
3959 }
3960
3961
3962
3963
3964
3965 public Text textAt(int row, int col) { return columnValues.get(col + (row * columns.length)); }
3966
3967
3968
3969
3970
3971
3972 @Deprecated
3973 public Text cellAt(int row, int col) { return textAt(row, col); }
3974
3975
3976
3977 public int rowCount() { return columnValues.size() / columns.length; }
3978
3979
3980 public void addEmptyRow() {
3981 for (int i = 0; i < columns.length; i++) {
3982 columnValues.add(ansi.new Text(columns[i].width));
3983 }
3984 }
3985
3986
3987
3988 public void addRowValues(String... values) {
3989 Text[] array = new Text[values.length];
3990 for (int i = 0; i < array.length; i++) {
3991 array[i] = values[i] == null ? Ansi.EMPTY_TEXT : ansi.new Text(values[i]);
3992 }
3993 addRowValues(array);
3994 }
3995
3996
3997
3998
3999
4000
4001
4002 public void addRowValues(Text... values) {
4003 if (values.length > columns.length) {
4004 throw new IllegalArgumentException(values.length + " values don't fit in " +
4005 columns.length + " columns");
4006 }
4007 addEmptyRow();
4008 for (int col = 0; col < values.length; col++) {
4009 int row = rowCount() - 1;
4010 Cell cell = putValue(row, col, values[col]);
4011
4012
4013 if ((cell.row != row || cell.column != col) && col != values.length - 1) {
4014 addEmptyRow();
4015 }
4016 }
4017 }
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030 public Cell putValue(int row, int col, Text value) {
4031 if (row > rowCount() - 1) {
4032 throw new IllegalArgumentException("Cannot write to row " + row + ": rowCount=" + rowCount());
4033 }
4034 if (value == null || value.plain.length() == 0) { return new Cell(col, row); }
4035 Column column = columns[col];
4036 int indent = column.indent;
4037 switch (column.overflow) {
4038 case TRUNCATE:
4039 copy(value, textAt(row, col), indent);
4040 return new Cell(col, row);
4041 case SPAN:
4042 int startColumn = col;
4043 do {
4044 boolean lastColumn = col == columns.length - 1;
4045 int charsWritten = lastColumn
4046 ? copy(BreakIterator.getLineInstance(), value, textAt(row, col), indent)
4047 : copy(value, textAt(row, col), indent);
4048 value = value.substring(charsWritten);
4049 indent = 0;
4050 if (value.length > 0) {
4051 ++col;
4052 }
4053 if (value.length > 0 && col >= columns.length) {
4054 addEmptyRow();
4055 row++;
4056 col = startColumn;
4057 indent = column.indent + indentWrappedLines;
4058 }
4059 } while (value.length > 0);
4060 return new Cell(col, row);
4061 case WRAP:
4062 BreakIterator lineBreakIterator = BreakIterator.getLineInstance();
4063 do {
4064 int charsWritten = copy(lineBreakIterator, value, textAt(row, col), indent);
4065 value = value.substring(charsWritten);
4066 indent = column.indent + indentWrappedLines;
4067 if (value.length > 0) {
4068 ++row;
4069 addEmptyRow();
4070 }
4071 } while (value.length > 0);
4072 return new Cell(col, row);
4073 }
4074 throw new IllegalStateException(column.overflow.toString());
4075 }
4076 private static int length(Text str) {
4077 return str.length;
4078 }
4079
4080 private int copy(BreakIterator line, Text text, Text columnValue, int offset) {
4081
4082 line.setText(text.plainString().replace("-", "\u00ff"));
4083 int done = 0;
4084 for (int start = line.first(), end = line.next(); end != BreakIterator.DONE; start = end, end = line.next()) {
4085 Text word = text.substring(start, end);
4086 if (columnValue.maxLength >= offset + done + length(word)) {
4087 done += copy(word, columnValue, offset + done);
4088 } else {
4089 break;
4090 }
4091 }
4092 if (done == 0 && length(text) > columnValue.maxLength) {
4093
4094 done = copy(text, columnValue, offset);
4095 }
4096 return done;
4097 }
4098 private static int copy(Text value, Text destination, int offset) {
4099 int length = Math.min(value.length, destination.maxLength - offset);
4100 value.getStyledChars(value.from, length, destination, offset);
4101 return length;
4102 }
4103
4104
4105
4106
4107 public StringBuilder toString(StringBuilder text) {
4108 int columnCount = this.columns.length;
4109 StringBuilder row = new StringBuilder(usageHelpWidth);
4110 for (int i = 0; i < columnValues.size(); i++) {
4111 Text column = columnValues.get(i);
4112 row.append(column.toString());
4113 row.append(new String(spaces(columns[i % columnCount].width - column.length)));
4114 if (i % columnCount == columnCount - 1) {
4115 int lastChar = row.length() - 1;
4116 while (lastChar >= 0 && row.charAt(lastChar) == ' ') {lastChar--;}
4117 row.setLength(lastChar + 1);
4118 text.append(row.toString()).append(System.getProperty("line.separator"));
4119 row.setLength(0);
4120 }
4121 }
4122
4123 return text;
4124 }
4125 @Override
4126 public String toString() { return toString(new StringBuilder()).toString(); }
4127 }
4128
4129
4130 public static class Column {
4131
4132
4133
4134 public enum Overflow { TRUNCATE, SPAN, WRAP }
4135
4136
4137 public final int width;
4138
4139
4140 public final int indent;
4141
4142
4143 public final Overflow overflow;
4144 public Column(int width, int indent, Overflow overflow) {
4145 this.width = width;
4146 this.indent = indent;
4147 this.overflow = Assert.notNull(overflow, "overflow");
4148 }
4149 }
4150
4151
4152
4153
4154
4155
4156
4157
4158 public static class ColorScheme {
4159 public final List<IStyle> commandStyles = new ArrayList<IStyle>();
4160 public final List<IStyle> optionStyles = new ArrayList<IStyle>();
4161 public final List<IStyle> parameterStyles = new ArrayList<IStyle>();
4162 public final List<IStyle> optionParamStyles = new ArrayList<IStyle>();
4163 private final Ansi ansi;
4164
4165
4166 public ColorScheme() { this(Ansi.AUTO); }
4167
4168
4169
4170
4171 public ColorScheme(Ansi ansi) {this.ansi = Assert.notNull(ansi, "ansi"); }
4172
4173
4174
4175
4176 public ColorScheme commands(IStyle... styles) { return addAll(commandStyles, styles); }
4177
4178
4179
4180 public ColorScheme options(IStyle... styles) { return addAll(optionStyles, styles);}
4181
4182
4183
4184 public ColorScheme parameters(IStyle... styles) { return addAll(parameterStyles, styles);}
4185
4186
4187
4188 public ColorScheme optionParams(IStyle... styles) { return addAll(optionParamStyles, styles);}
4189
4190
4191
4192 public Ansi.Text commandText(String command) { return ansi().apply(command, commandStyles); }
4193
4194
4195
4196 public Ansi.Text optionText(String option) { return ansi().apply(option, optionStyles); }
4197
4198
4199
4200 public Ansi.Text parameterText(String parameter) { return ansi().apply(parameter, parameterStyles); }
4201
4202
4203
4204 public Ansi.Text optionParamText(String optionParam) { return ansi().apply(optionParam, optionParamStyles); }
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215 public ColorScheme applySystemProperties() {
4216 replace(commandStyles, System.getProperty("picocli.color.commands"));
4217 replace(optionStyles, System.getProperty("picocli.color.options"));
4218 replace(parameterStyles, System.getProperty("picocli.color.parameters"));
4219 replace(optionParamStyles, System.getProperty("picocli.color.optionParams"));
4220 return this;
4221 }
4222 private void replace(List<IStyle> styles, String property) {
4223 if (property != null) {
4224 styles.clear();
4225 addAll(styles, Style.parse(property));
4226 }
4227 }
4228 private ColorScheme addAll(List<IStyle> styles, IStyle... add) {
4229 styles.addAll(Arrays.asList(add));
4230 return this;
4231 }
4232
4233 public Ansi ansi() {
4234 return ansi;
4235 }
4236 }
4237
4238
4239
4240
4241
4242
4243 public static ColorScheme defaultColorScheme(Ansi ansi) {
4244 return new ColorScheme(ansi)
4245 .commands(Style.bold)
4246 .options(Style.fg_yellow)
4247 .parameters(Style.fg_yellow)
4248 .optionParams(Style.italic);
4249 }
4250
4251
4252 public enum Ansi {
4253
4254
4255 AUTO,
4256
4257 ON,
4258
4259 OFF;
4260 static Text EMPTY_TEXT = OFF.new Text(0);
4261 static final boolean isWindows = System.getProperty("os.name").startsWith("Windows");
4262 static final boolean isXterm = System.getenv("TERM") != null && System.getenv("TERM").startsWith("xterm");
4263 static final boolean ISATTY = calcTTY();
4264
4265
4266 static final boolean calcTTY() {
4267 if (isWindows && isXterm) { return true; }
4268 try { return System.class.getDeclaredMethod("console").invoke(null) != null; }
4269 catch (Throwable reflectionFailed) { return true; }
4270 }
4271 private static boolean ansiPossible() { return ISATTY && (!isWindows || isXterm); }
4272
4273
4274
4275
4276 public boolean enabled() {
4277 if (this == ON) { return true; }
4278 if (this == OFF) { return false; }
4279 return (System.getProperty("picocli.ansi") == null ? ansiPossible() : Boolean.getBoolean("picocli.ansi"));
4280 }
4281
4282
4283 public interface IStyle {
4284
4285
4286 String CSI = "\u001B[";
4287
4288
4289
4290 String on();
4291
4292
4293
4294 String off();
4295 }
4296
4297
4298
4299
4300
4301
4302 public enum Style implements IStyle {
4303 reset(0, 0), bold(1, 21), faint(2, 22), italic(3, 23), underline(4, 24), blink(5, 25), reverse(7, 27),
4304 fg_black(30, 39), fg_red(31, 39), fg_green(32, 39), fg_yellow(33, 39), fg_blue(34, 39), fg_magenta(35, 39), fg_cyan(36, 39), fg_white(37, 39),
4305 bg_black(40, 49), bg_red(41, 49), bg_green(42, 49), bg_yellow(43, 49), bg_blue(44, 49), bg_magenta(45, 49), bg_cyan(46, 49), bg_white(47, 49),
4306 ;
4307 private final int startCode;
4308 private final int endCode;
4309
4310 Style(int startCode, int endCode) {this.startCode = startCode; this.endCode = endCode; }
4311 @Override
4312 public String on() { return CSI + startCode + "m"; }
4313 @Override
4314 public String off() { return CSI + endCode + "m"; }
4315
4316
4317
4318
4319 public static String on(IStyle... styles) {
4320 StringBuilder result = new StringBuilder();
4321 for (IStyle style : styles) {
4322 result.append(style.on());
4323 }
4324 return result.toString();
4325 }
4326
4327
4328
4329 public static String off(IStyle... styles) {
4330 StringBuilder result = new StringBuilder();
4331 for (IStyle style : styles) {
4332 result.append(style.off());
4333 }
4334 return result.toString();
4335 }
4336
4337
4338
4339
4340
4341
4342
4343 public static IStyle fg(String str) {
4344 try { return Style.valueOf(str.toLowerCase(ENGLISH)); } catch (Exception ignored) {}
4345 try { return Style.valueOf("fg_" + str.toLowerCase(ENGLISH)); } catch (Exception ignored) {}
4346 return new Palette256Color(true, str);
4347 }
4348
4349
4350
4351
4352
4353
4354
4355 public static IStyle bg(String str) {
4356 try { return Style.valueOf(str.toLowerCase(ENGLISH)); } catch (Exception ignored) {}
4357 try { return Style.valueOf("bg_" + str.toLowerCase(ENGLISH)); } catch (Exception ignored) {}
4358 return new Palette256Color(false, str);
4359 }
4360
4361
4362
4363
4364
4365
4366 public static IStyle[] parse(String commaSeparatedCodes) {
4367 String[] codes = commaSeparatedCodes.split(",");
4368 IStyle[] styles = new IStyle[codes.length];
4369 for(int i = 0; i < codes.length; ++i) {
4370 if (codes[i].toLowerCase(ENGLISH).startsWith("fg(")) {
4371 int end = codes[i].indexOf(')');
4372 styles[i] = Style.fg(codes[i].substring(3, end < 0 ? codes[i].length() : end));
4373 } else if (codes[i].toLowerCase(ENGLISH).startsWith("bg(")) {
4374 int end = codes[i].indexOf(')');
4375 styles[i] = Style.bg(codes[i].substring(3, end < 0 ? codes[i].length() : end));
4376 } else {
4377 styles[i] = Style.fg(codes[i]);
4378 }
4379 }
4380 return styles;
4381 }
4382 }
4383
4384
4385
4386 static class Palette256Color implements IStyle {
4387 private final int fgbg;
4388 private final int color;
4389
4390 Palette256Color(boolean foreground, String color) {
4391 this.fgbg = foreground ? 38 : 48;
4392 String[] rgb = color.split(";");
4393 if (rgb.length == 3) {
4394 this.color = 16 + 36 * Integer.decode(rgb[0]) + 6 * Integer.decode(rgb[1]) + Integer.decode(rgb[2]);
4395 } else {
4396 this.color = Integer.decode(color);
4397 }
4398 }
4399 @Override
4400 public String on() { return String.format(CSI + "%d;5;%dm", fgbg, color); }
4401 @Override
4402 public String off() { return CSI + (fgbg + 1) + "m"; }
4403 }
4404 private static class StyledSection {
4405 int startIndex, length;
4406 String startStyles, endStyles;
4407 StyledSection(int start, int len, String style1, String style2) {
4408 startIndex = start; length = len; startStyles = style1; endStyles = style2;
4409 }
4410 StyledSection withStartIndex(int newStart) {
4411 return new StyledSection(newStart, length, startStyles, endStyles);
4412 }
4413 }
4414
4415
4416
4417
4418
4419
4420
4421
4422 public Text apply(String plainText, List<IStyle> styles) {
4423 if (plainText.length() == 0) { return new Text(0); }
4424 Text result = new Text(plainText.length());
4425 IStyle[] all = styles.toArray(new IStyle[styles.size()]);
4426 result.sections.add(new StyledSection(
4427 0, plainText.length(), Style.on(all), Style.off(reverse(all)) + Style.reset.off()));
4428 result.plain.append(plainText);
4429 result.length = result.plain.length();
4430 return result;
4431 }
4432
4433 private static <T> T[] reverse(T[] all) {
4434 for (int i = 0; i < all.length / 2; i++) {
4435 T temp = all[i];
4436 all[i] = all[all.length - i - 1];
4437 all[all.length - i - 1] = temp;
4438 }
4439 return all;
4440 }
4441
4442
4443
4444
4445
4446
4447 public class Text implements Cloneable {
4448 private final int maxLength;
4449 private int from;
4450 private int length;
4451 private StringBuilder plain = new StringBuilder();
4452 private List<StyledSection> sections = new ArrayList<StyledSection>();
4453
4454
4455
4456 public Text(int maxLength) { this.maxLength = maxLength; }
4457
4458
4459
4460
4461
4462
4463 public Text(String input) {
4464 maxLength = -1;
4465 plain.setLength(0);
4466 int i = 0;
4467
4468 while (true) {
4469 int j = input.indexOf("@|", i);
4470 if (j == -1) {
4471 if (i == 0) {
4472 plain.append(input);
4473 length = plain.length();
4474 return;
4475 }
4476 plain.append(input.substring(i, input.length()));
4477 length = plain.length();
4478 return;
4479 }
4480 plain.append(input.substring(i, j));
4481 int k = input.indexOf("|@", j);
4482 if (k == -1) {
4483 plain.append(input);
4484 length = plain.length();
4485 return;
4486 }
4487
4488 j += 2;
4489 String spec = input.substring(j, k);
4490 String[] items = spec.split(" ", 2);
4491 if (items.length == 1) {
4492 plain.append(input);
4493 length = plain.length();
4494 return;
4495 }
4496
4497 IStyle[] styles = Style.parse(items[0]);
4498 addStyledSection(plain.length(), items[1].length(),
4499 Style.on(styles), Style.off(reverse(styles)) + Style.reset.off());
4500 plain.append(items[1]);
4501 i = k + 2;
4502 }
4503 }
4504 private void addStyledSection(int start, int length, String startStyle, String endStyle) {
4505 sections.add(new StyledSection(start, length, startStyle, endStyle));
4506 }
4507 @Override
4508 public Object clone() {
4509 try { return super.clone(); } catch (CloneNotSupportedException e) { throw new IllegalStateException(e); }
4510 }
4511
4512 public Text[] splitLines() {
4513 List<Text> result = new ArrayList<Text>();
4514 boolean trailingEmptyString = false;
4515 int start = 0, end = 0;
4516 for (int i = 0; i < plain.length(); i++, end = i) {
4517 char c = plain.charAt(i);
4518 boolean eol = c == '\n';
4519 eol |= (c == '\r' && i + 1 < plain.length() && plain.charAt(i + 1) == '\n' && ++i > 0);
4520 eol |= c == '\r';
4521 if (eol) {
4522 result.add(this.substring(start, end));
4523 trailingEmptyString = i == plain.length() - 1;
4524 start = i + 1;
4525 }
4526 }
4527 if (start < plain.length() || trailingEmptyString) {
4528 result.add(this.substring(start, plain.length()));
4529 }
4530 return result.toArray(new Text[result.size()]);
4531 }
4532
4533
4534
4535
4536 public Text substring(int start) {
4537 return substring(start, length);
4538 }
4539
4540
4541
4542
4543
4544 public Text substring(int start, int end) {
4545 Text result = (Text) clone();
4546 result.from = from + start;
4547 result.length = end - start;
4548 return result;
4549 }
4550
4551
4552
4553 public Text append(String string) {
4554 return append(new Text(string));
4555 }
4556
4557
4558
4559
4560 public Text append(Text other) {
4561 Text result = (Text) clone();
4562 result.plain = new StringBuilder(plain.toString().substring(from, from + length));
4563 result.from = 0;
4564 result.sections = new ArrayList<StyledSection>();
4565 for (StyledSection section : sections) {
4566 result.sections.add(section.withStartIndex(section.startIndex - from));
4567 }
4568 result.plain.append(other.plain.toString().substring(other.from, other.from + other.length));
4569 for (StyledSection section : other.sections) {
4570 int index = result.length + section.startIndex - other.from;
4571 result.sections.add(section.withStartIndex(index));
4572 }
4573 result.length = result.plain.length();
4574 return result;
4575 }
4576
4577
4578
4579
4580
4581
4582
4583
4584 public void getStyledChars(int from, int length, Text destination, int offset) {
4585 if (destination.length < offset) {
4586 for (int i = destination.length; i < offset; i++) {
4587 destination.plain.append(' ');
4588 }
4589 destination.length = offset;
4590 }
4591 for (StyledSection section : sections) {
4592 destination.sections.add(section.withStartIndex(section.startIndex - from + destination.length));
4593 }
4594 destination.plain.append(plain.toString().substring(from, from + length));
4595 destination.length = destination.plain.length();
4596 }
4597
4598
4599 public String plainString() { return plain.toString().substring(from, from + length); }
4600
4601 @Override
4602 public boolean equals(Object obj) { return toString().equals(String.valueOf(obj)); }
4603 @Override
4604 public int hashCode() { return toString().hashCode(); }
4605
4606
4607
4608
4609 @Override
4610 public String toString() {
4611 if (!Ansi.this.enabled()) {
4612 return plain.toString().substring(from, from + length);
4613 }
4614 if (length == 0) { return ""; }
4615 StringBuilder sb = new StringBuilder(plain.length() + 20 * sections.size());
4616 StyledSection current = null;
4617 int end = Math.min(from + length, plain.length());
4618 for (int i = from; i < end; i++) {
4619 StyledSection section = findSectionContaining(i);
4620 if (section != current) {
4621 if (current != null) { sb.append(current.endStyles); }
4622 if (section != null) { sb.append(section.startStyles); }
4623 current = section;
4624 }
4625 sb.append(plain.charAt(i));
4626 }
4627 if (current != null) { sb.append(current.endStyles); }
4628 return sb.toString();
4629 }
4630
4631 private StyledSection findSectionContaining(int index) {
4632 for (StyledSection section : sections) {
4633 if (index >= section.startIndex && index < section.startIndex + section.length) {
4634 return section;
4635 }
4636 }
4637 return null;
4638 }
4639 }
4640 }
4641 }
4642
4643
4644
4645
4646 private static final class Assert {
4647
4648
4649
4650
4651
4652
4653
4654 static <T> T notNull(T object, String description) {
4655 if (object == null) {
4656 throw new NullPointerException(description);
4657 }
4658 return object;
4659 }
4660 private Assert() {}
4661 }
4662 private enum TraceLevel { OFF, WARN, INFO, DEBUG;
4663 public boolean isEnabled(TraceLevel other) { return ordinal() >= other.ordinal(); }
4664 private void print(Tracer tracer, String msg, Object... params) {
4665 if (tracer.level.isEnabled(this)) { tracer.stream.printf(prefix(msg), params); }
4666 }
4667 private String prefix(String msg) { return "[picocli " + this + "] " + msg; }
4668 static TraceLevel lookup(String key) { return key == null ? WARN : empty(key) || "true".equalsIgnoreCase(key) ? INFO : valueOf(key); }
4669 }
4670 private static class Tracer {
4671 TraceLevel level = TraceLevel.lookup(System.getProperty("picocli.trace"));
4672 PrintStream stream = System.err;
4673 void warn (String msg, Object... params) { TraceLevel.WARN.print(this, msg, params); }
4674 void info (String msg, Object... params) { TraceLevel.INFO.print(this, msg, params); }
4675 void debug(String msg, Object... params) { TraceLevel.DEBUG.print(this, msg, params); }
4676 boolean isWarn() { return level.isEnabled(TraceLevel.WARN); }
4677 boolean isInfo() { return level.isEnabled(TraceLevel.INFO); }
4678 boolean isDebug() { return level.isEnabled(TraceLevel.DEBUG); }
4679 }
4680
4681
4682 public static class PicocliException extends RuntimeException {
4683 private static final long serialVersionUID = -2574128880125050818L;
4684 public PicocliException(String msg) { super(msg); }
4685 public PicocliException(String msg, Exception ex) { super(msg, ex); }
4686 }
4687
4688
4689 public static class InitializationException extends PicocliException {
4690 private static final long serialVersionUID = 8423014001666638895L;
4691 public InitializationException(String msg) { super(msg); }
4692 public InitializationException(String msg, Exception ex) { super(msg, ex); }
4693 }
4694
4695
4696 public static class ExecutionException extends PicocliException {
4697 private static final long serialVersionUID = 7764539594267007998L;
4698 private final CommandLine commandLine;
4699 public ExecutionException(CommandLine commandLine, String msg) {
4700 super(msg);
4701 this.commandLine = Assert.notNull(commandLine, "commandLine");
4702 }
4703 public ExecutionException(CommandLine commandLine, String msg, Exception ex) {
4704 super(msg, ex);
4705 this.commandLine = Assert.notNull(commandLine, "commandLine");
4706 }
4707
4708
4709
4710 public CommandLine getCommandLine() { return commandLine; }
4711 }
4712
4713
4714 public static class TypeConversionException extends PicocliException {
4715 private static final long serialVersionUID = 4251973913816346114L;
4716 public TypeConversionException(String msg) { super(msg); }
4717 }
4718
4719 public static class ParameterException extends PicocliException {
4720 private static final long serialVersionUID = 1477112829129763139L;
4721 private final CommandLine commandLine;
4722
4723
4724
4725
4726
4727 public ParameterException(CommandLine commandLine, String msg) {
4728 super(msg);
4729 this.commandLine = Assert.notNull(commandLine, "commandLine");
4730 }
4731
4732
4733
4734
4735
4736 public ParameterException(CommandLine commandLine, String msg, Exception ex) {
4737 super(msg, ex);
4738 this.commandLine = Assert.notNull(commandLine, "commandLine");
4739 }
4740
4741
4742
4743
4744
4745 public CommandLine getCommandLine() { return commandLine; }
4746
4747 private static ParameterException create(CommandLine cmd, Exception ex, String arg, int i, String[] args) {
4748 String msg = ex.getClass().getSimpleName() + ": " + ex.getLocalizedMessage()
4749 + " while processing argument at or before arg[" + i + "] '" + arg + "' in " + Arrays.toString(args) + ": " + ex.toString();
4750 return new ParameterException(cmd, msg, ex);
4751 }
4752 }
4753
4754
4755
4756 public static class MissingParameterException extends ParameterException {
4757 private static final long serialVersionUID = 5075678535706338753L;
4758 public MissingParameterException(CommandLine commandLine, String msg) {
4759 super(commandLine, msg);
4760 }
4761
4762 private static MissingParameterException create(CommandLine cmd, Collection<Field> missing, String separator) {
4763 if (missing.size() == 1) {
4764 return new MissingParameterException(cmd, "Missing required option '"
4765 + describe(missing.iterator().next(), separator) + "'");
4766 }
4767 List<String> names = new ArrayList<String>(missing.size());
4768 for (Field field : missing) {
4769 names.add(describe(field, separator));
4770 }
4771 return new MissingParameterException(cmd, "Missing required options " + names.toString());
4772 }
4773 private static String describe(Field field, String separator) {
4774 String prefix = (field.isAnnotationPresent(Option.class))
4775 ? field.getAnnotation(Option.class).names()[0] + separator
4776 : "params[" + field.getAnnotation(Parameters.class).index() + "]" + separator;
4777 return prefix + Help.DefaultParamLabelRenderer.renderParameterName(field);
4778 }
4779 }
4780
4781
4782
4783
4784 public static class DuplicateOptionAnnotationsException extends InitializationException {
4785 private static final long serialVersionUID = -3355128012575075641L;
4786 public DuplicateOptionAnnotationsException(String msg) { super(msg); }
4787
4788 private static DuplicateOptionAnnotationsException create(String name, Field field1, Field field2) {
4789 return new DuplicateOptionAnnotationsException("Option name '" + name + "' is used by both " +
4790 field1.getDeclaringClass().getName() + "." + field1.getName() + " and " +
4791 field2.getDeclaringClass().getName() + "." + field2.getName());
4792 }
4793 }
4794
4795 public static class ParameterIndexGapException extends InitializationException {
4796 private static final long serialVersionUID = -1520981133257618319L;
4797 public ParameterIndexGapException(String msg) { super(msg); }
4798 }
4799
4800
4801 public static class UnmatchedArgumentException extends ParameterException {
4802 private static final long serialVersionUID = -8700426380701452440L;
4803 public UnmatchedArgumentException(CommandLine commandLine, String msg) { super(commandLine, msg); }
4804 public UnmatchedArgumentException(CommandLine commandLine, Stack<String> args) { this(commandLine, new ArrayList<String>(reverse(args))); }
4805 public UnmatchedArgumentException(CommandLine commandLine, List<String> args) { this(commandLine, "Unmatched argument" + (args.size() == 1 ? " " : "s ") + args); }
4806 }
4807
4808 public static class MaxValuesforFieldExceededException extends ParameterException {
4809 private static final long serialVersionUID = 6536145439570100641L;
4810 public MaxValuesforFieldExceededException(CommandLine commandLine, String msg) { super(commandLine, msg); }
4811 }
4812
4813 public static class OverwrittenOptionException extends ParameterException {
4814 private static final long serialVersionUID = 1338029208271055776L;
4815 public OverwrittenOptionException(CommandLine commandLine, String msg) { super(commandLine, msg); }
4816 }
4817
4818
4819
4820
4821 public static class MissingTypeConverterException extends ParameterException {
4822 private static final long serialVersionUID = -6050931703233083760L;
4823 public MissingTypeConverterException(CommandLine commandLine, String msg) { super(commandLine, msg); }
4824 }
4825 }