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 final List<String> unmatchedArguments = new ArrayList<String>();
143 private CommandLine parent;
144 private boolean usageHelpRequested;
145 private boolean versionHelpRequested;
146 private final List<String> versionLines = new ArrayList<String>();
147
148
149
150
151
152
153
154
155 public CommandLine(final 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(final String name, final Object command) {
201 final 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(final boolean newValue) {
263 this.overwrittenOptionsAllowed = newValue;
264 for (final 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(final boolean newValue) {
292 this.unmatchedArgumentsAllowed = newValue;
293 for (final 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(final T command, final String... args) {
329 final 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(final 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(final ParameterException ex, final PrintStream out, final Help.Ansi ansi, final 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(final List<CommandLine> parsedCommands, final PrintStream out, final Help.Ansi ansi) {
433 for (final 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(final CommandLine parsed) {
445 final Object command = parsed.getCommand();
446 if (command instanceof Runnable) {
447 try {
448 ((Runnable) command).run();
449 return null;
450 } catch (final 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 (final 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(final List<CommandLine> parsedCommands, final PrintStream out, final 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(final List<CommandLine> parsedCommands, final PrintStream out, final Help.Ansi ansi) {
537 if (printHelpIfRequested(parsedCommands, out, ansi)) { return Collections.emptyList(); }
538 final 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(final List<CommandLine> parsedCommands, final PrintStream out, final Help.Ansi ansi) {
563 if (printHelpIfRequested(parsedCommands, out, ansi)) {
564 return null;
565 }
566 final List<Object> result = new ArrayList<Object>();
567 for (final 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(final IParseResultHandler handler, final PrintStream out, final 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(final IParseResultHandler handler, final PrintStream out, final Help.Ansi ansi, final IExceptionHandler exceptionHandler, final String... args) {
656 try {
657 final List<CommandLine> result = parse(args);
658 return handler.handleParseResult(result, out, ansi);
659 } catch (final ParameterException ex) {
660 return exceptionHandler.handleException(ex, out, ansi, args);
661 }
662 }
663
664
665
666
667
668
669 public static void usage(final Object command, final PrintStream out) {
670 toCommandLine(command).usage(out);
671 }
672
673
674
675
676
677
678
679
680
681 public static void usage(final Object command, final PrintStream out, final Help.Ansi ansi) {
682 toCommandLine(command).usage(out, ansi);
683 }
684
685
686
687
688
689
690
691
692
693 public static void usage(final Object command, final PrintStream out, final Help.ColorScheme colorScheme) {
694 toCommandLine(command).usage(out, colorScheme);
695 }
696
697
698
699
700
701
702 public void usage(final PrintStream out) {
703 usage(out, Help.Ansi.AUTO);
704 }
705
706
707
708
709
710
711
712 public void usage(final PrintStream out, final 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(final PrintStream out, final Help.ColorScheme colorScheme) {
748 final 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 final 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(final PrintStream out) { printVersionHelp(out, Help.Ansi.AUTO); }
781
782
783
784
785
786
787
788
789
790
791
792
793 public void printVersionHelp(final PrintStream out, final Help.Ansi ansi) {
794 for (final 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(final PrintStream out, final Help.Ansi ansi, final Object... params) {
812 for (final 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(final C callable, final PrintStream out, final 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(final C callable, final PrintStream out, final Help.Ansi ansi, final String... args) {
884 final CommandLine cmd = new CommandLine(callable);
885 final 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(final R runnable, final PrintStream out, final 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(final R runnable, final PrintStream out, final Help.Ansi ansi, final String... args) {
952 final 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(final Class<K> cls, final ITypeConverter<K> converter) {
1000 interpreter.converterRegistry.put(Assert.notNull(cls, "class"), Assert.notNull(converter, "converter"));
1001 for (final 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(final 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(final String commandName) {
1034 this.commandName = Assert.notNull(commandName, "commandName");
1035 return this;
1036 }
1037 private static boolean empty(final String str) { return str == null || str.trim().length() == 0; }
1038 private static boolean empty(final Object[] array) { return array == null || array.length == 0; }
1039 private static boolean empty(final Text txt) { return txt == null || txt.plain.toString().trim().length() == 0; }
1040 private static String str(final String[] arr, final int i) { return (arr == null || arr.length == 0) ? "" : arr[i]; }
1041 private static boolean isBoolean(final Class<?> type) { return type == Boolean.class || type == Boolean.TYPE; }
1042 private static CommandLine toCommandLine(final Object obj) { return obj instanceof CommandLine ? (CommandLine) obj : new CommandLine(obj);}
1043 private static boolean isMultiValue(final Field field) { return isMultiValue(field.getType()); }
1044 private static boolean isMultiValue(final Class<?> cls) { return cls.isArray() || Collection.class.isAssignableFrom(cls) || Map.class.isAssignableFrom(cls); }
1045 private static Class<?>[] getTypeAttribute(final Field field) {
1046 final 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 final Type type = field.getGenericType();
1051 if (type instanceof ParameterizedType) {
1052 final ParameterizedType parameterizedType = (ParameterizedType) type;
1053 final Type[] paramTypes = parameterizedType.getActualTypeArguments();
1054 final 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 final WildcardType wildcardType = (WildcardType) paramTypes[i];
1059 final Type[] lower = wildcardType.getLowerBounds();
1060 if (lower.length > 0 && lower[0] instanceof Class) { result[i] = (Class<?>) lower[0]; continue; }
1061 final 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(final int min, final int max, final boolean variable, final boolean unspecified, final 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(final 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(final 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(final 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(final Range result, final Field field) {
1693 return result.isUnspecified ? defaultArity(field) : result;
1694 }
1695
1696
1697
1698
1699
1700
1701 public static Range defaultArity(final Field field) {
1702 final 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(final 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(final Field field) {
1719 final Range arity = parameterArity(field);
1720 if (!isMultiValue(field)) { return arity; }
1721 final 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 final 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 final Range result = new Range(min, max, variable, unspecified, range);
1751 return result;
1752 }
1753 private static int parseInt(final String str, final int defaultValue) {
1754 try {
1755 return Integer.parseInt(str);
1756 } catch (final Exception ex) {
1757 return defaultValue;
1758 }
1759 }
1760
1761
1762
1763
1764 public Range min(final int newMin) { return new Range(newMin, Math.max(newMin, max), isVariable, isUnspecified, originalValue); }
1765
1766
1767
1768
1769
1770 public Range max(final int newMax) { return new Range(Math.min(min, newMax), newMax, isVariable, isUnspecified, originalValue); }
1771
1772
1773
1774
1775
1776
1777 public boolean contains(final int value) { return min <= value && max >= value; }
1778
1779 @Override
1780 public boolean equals(final Object object) {
1781 if (!(object instanceof Range)) { return false; }
1782 final 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(final Range other) {
1795 final int result = min - other.min;
1796 return (result == 0) ? max - other.max : result;
1797 }
1798 }
1799 static void init(final Class<?> cls,
1800 final List<Field> requiredFields,
1801 final Map<String, Field> optionName2Field,
1802 final Map<Character, Field> singleCharOption2Field,
1803 final List<Field> positionalParametersFields) {
1804 final Field[] declaredFields = cls.getDeclaredFields();
1805 for (final Field field : declaredFields) {
1806 field.setAccessible(true);
1807 if (field.isAnnotationPresent(Option.class)) {
1808 final Option option = field.getAnnotation(Option.class);
1809 if (option.required()) {
1810 requiredFields.add(field);
1811 }
1812 for (final String name : option.names()) {
1813 final 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 final char flag = name.charAt(1);
1819 final 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 final Range arity = Range.parameterArity(field);
1833 if (arity.min > 0) {
1834 requiredFields.add(field);
1835 }
1836 }
1837 }
1838 }
1839 static void validatePositionalParameters(final List<Field> positionalParametersFields) {
1840 int min = 0;
1841 for (final Field field : positionalParametersFields) {
1842 final 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(final 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(final 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 final 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 (final Class<?> sub : cmd.subcommands()) {
1919 final 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 final Constructor<?> constructor = sub.getDeclaredConstructor();
1926 constructor.setAccessible(true);
1927 final CommandLine commandLine = toCommandLine(constructor.newInstance());
1928 commandLine.parent = CommandLine.this;
1929 commands.put(subCommand.name(), commandLine);
1930 }
1931 catch (final InitializationException ex) { throw ex; }
1932 catch (final NoSuchMethodException ex) { throw new InitializationException("Cannot instantiate subcommand " +
1933 sub.getName() + ": the class has no constructor", ex); }
1934 catch (final 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(final 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 final Stack<String> arguments = new Stack<String>();
1963 for (int i = args.length - 1; i >= 0; i--) {
1964 arguments.push(args[i]);
1965 }
1966 final List<CommandLine> result = new ArrayList<CommandLine>();
1967 parse(result, arguments, args);
1968 return result;
1969 }
1970
1971 private void parse(final List<CommandLine> parsedCommands, final Stack<String> argumentStack, final String[] originalArgs) {
1972
1973 isHelpRequested = false;
1974 CommandLine.this.versionHelpRequested = false;
1975 CommandLine.this.usageHelpRequested = false;
1976
1977 final 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 final List<Field> required = new ArrayList<Field>(requiredFields);
1981 final Set<Field> initialized = new HashSet<Field>();
1982 Collections.sort(required, new PositionalParametersSorter());
1983 try {
1984 processArguments(parsedCommands, argumentStack, required, initialized, originalArgs);
1985 } catch (final ParameterException ex) {
1986 throw ex;
1987 } catch (final Exception ex) {
1988 final int offendingArgIndex = originalArgs.length - argumentStack.size() - 1;
1989 final 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 (final 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(final List<CommandLine> parsedCommands,
2008 final Stack<String> args,
2009 final Collection<Field> required,
2010 final Set<Field> initialized,
2011 final 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 final int separatorIndex = arg.indexOf(separator);
2049 if (separatorIndex > 0) {
2050 final String key = arg.substring(0, separatorIndex);
2051
2052 if (optionName2Field.containsKey(key) && !optionName2Field.containsKey(arg)) {
2053 paramAttachedToOption = true;
2054 final 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(final String arg) {
2085 int count = 0;
2086 for (final 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 final 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(final String arg) {final Stack<String> args = new Stack<String>(); args.add(arg); handleUnmatchedArguments(args);}
2096 private void handleUnmatchedArguments(final Stack<String> args) {
2097 while (!args.isEmpty()) { unmatchedArguments.add(args.pop()); }
2098 }
2099
2100 private void processRemainderAsPositionalParameters(final Collection<Field> required, final Set<Field> initialized, final Stack<String> args) throws Exception {
2101 while (!args.empty()) {
2102 processPositionalParameter(required, initialized, args);
2103 }
2104 }
2105 private void processPositionalParameter(final Collection<Field> required, final Set<Field> initialized, final 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 (final Field positionalParam : positionalParametersFields) {
2109 final Range indexRange = Range.parameterIndex(positionalParam);
2110 if (!indexRange.contains(position)) {
2111 continue;
2112 }
2113 @SuppressWarnings("unchecked")
2114 final
2115 Stack<String> argsCopy = (Stack<String>) args.clone();
2116 final Range arity = Range.parameterArity(positionalParam);
2117 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);}
2118 assertNoMissingParameters(positionalParam, arity.min, argsCopy);
2119 final int originalSize = argsCopy.size();
2120 applyOption(positionalParam, Parameters.class, arity, false, argsCopy, initialized, "args[" + indexRange + "] at position " + position);
2121 final int count = originalSize - argsCopy.size();
2122 if (count > 0) { required.remove(positionalParam); }
2123 consumed = Math.max(consumed, count);
2124 }
2125
2126 for (int i = 0; i < consumed; i++) { args.pop(); }
2127 position += consumed;
2128 if (tracer.isDebug()) {tracer.debug("Consumed %d arguments, moving position to index %d.%n", consumed, position);}
2129 if (consumed == 0 && !args.isEmpty()) {
2130 handleUnmatchedArguments(args.pop());
2131 }
2132 }
2133
2134 private void processStandaloneOption(final Collection<Field> required,
2135 final Set<Field> initialized,
2136 final String arg,
2137 final Stack<String> args,
2138 final boolean paramAttachedToKey) throws Exception {
2139 final Field field = optionName2Field.get(arg);
2140 required.remove(field);
2141 Range arity = Range.optionArity(field);
2142 if (paramAttachedToKey) {
2143 arity = arity.min(Math.max(1, arity.min));
2144 }
2145 if (tracer.isDebug()) {tracer.debug("Found option named '%s': field %s, arity=%s%n", arg, field, arity);}
2146 applyOption(field, Option.class, arity, paramAttachedToKey, args, initialized, "option " + arg);
2147 }
2148
2149 private void processClusteredShortOptions(final Collection<Field> required,
2150 final Set<Field> initialized,
2151 final String arg,
2152 final Stack<String> args)
2153 throws Exception {
2154 final String prefix = arg.substring(0, 1);
2155 String cluster = arg.substring(1);
2156 boolean paramAttachedToOption = true;
2157 do {
2158 if (cluster.length() > 0 && singleCharOption2Field.containsKey(cluster.charAt(0))) {
2159 final Field field = singleCharOption2Field.get(cluster.charAt(0));
2160 Range arity = Range.optionArity(field);
2161 final String argDescription = "option " + prefix + cluster.charAt(0);
2162 if (tracer.isDebug()) {tracer.debug("Found option '%s%s' in %s: field %s, arity=%s%n", prefix, cluster.charAt(0), arg, field, arity);}
2163 required.remove(field);
2164 cluster = cluster.length() > 0 ? cluster.substring(1) : "";
2165 paramAttachedToOption = cluster.length() > 0;
2166 if (cluster.startsWith(separator)) {
2167 cluster = cluster.substring(separator.length());
2168 arity = arity.min(Math.max(1, arity.min));
2169 }
2170 if (arity.min > 0 && !empty(cluster)) {
2171 if (tracer.isDebug()) {tracer.debug("Trying to process '%s' as option parameter%n", cluster);}
2172 }
2173
2174
2175
2176 if (!empty(cluster)) {
2177 args.push(cluster);
2178 }
2179 final int consumed = applyOption(field, Option.class, arity, paramAttachedToOption, args, initialized, argDescription);
2180
2181 if (empty(cluster) || consumed > 0 || args.isEmpty()) {
2182 return;
2183 }
2184 cluster = args.pop();
2185 } else {
2186 if (cluster.length() == 0) {
2187 return;
2188 }
2189
2190
2191 if (arg.endsWith(cluster)) {
2192 args.push(paramAttachedToOption ? prefix + cluster : cluster);
2193 if (args.peek().equals(arg)) {
2194 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);}
2195 if (resemblesOption(arg)) { handleUnmatchedArguments(args.pop()); return; }
2196 processPositionalParameter(required, initialized, args);
2197 return;
2198 }
2199
2200 if (tracer.isDebug()) {tracer.debug("No option found for %s in %s%n", cluster, arg);}
2201 handleUnmatchedArguments(args.pop());
2202 } else {
2203 args.push(cluster);
2204 if (tracer.isDebug()) {tracer.debug("%s is not an option parameter for %s%n", cluster, arg);}
2205 processPositionalParameter(required, initialized, args);
2206 }
2207 return;
2208 }
2209 } while (true);
2210 }
2211
2212 private int applyOption(final Field field,
2213 final Class<?> annotation,
2214 final Range arity,
2215 final boolean valueAttachedToOption,
2216 final Stack<String> args,
2217 final Set<Field> initialized,
2218 final String argDescription) throws Exception {
2219 updateHelpRequested(field);
2220 final int length = args.size();
2221 assertNoMissingParameters(field, arity.min, args);
2222
2223 Class<?> cls = field.getType();
2224 if (cls.isArray()) {
2225 return applyValuesToArrayField(field, annotation, arity, args, cls, argDescription);
2226 }
2227 if (Collection.class.isAssignableFrom(cls)) {
2228 return applyValuesToCollectionField(field, annotation, arity, args, cls, argDescription);
2229 }
2230 if (Map.class.isAssignableFrom(cls)) {
2231 return applyValuesToMapField(field, annotation, arity, args, cls, argDescription);
2232 }
2233 cls = getTypeAttribute(field)[0];
2234 return applyValueToSingleValuedField(field, arity, args, cls, initialized, argDescription);
2235 }
2236
2237 private int applyValueToSingleValuedField(final Field field,
2238 final Range arity,
2239 final Stack<String> args,
2240 final Class<?> cls,
2241 final Set<Field> initialized,
2242 final String argDescription) throws Exception {
2243 final boolean noMoreValues = args.isEmpty();
2244 String value = args.isEmpty() ? null : trim(args.pop());
2245 int result = arity.min;
2246
2247
2248 if ((cls == Boolean.class || cls == Boolean.TYPE) && arity.min <= 0) {
2249
2250
2251 if (arity.max > 0 && ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
2252 result = 1;
2253 } else {
2254 if (value != null) {
2255 args.push(value);
2256 }
2257 final Boolean currentValue = (Boolean) field.get(command);
2258 value = String.valueOf(currentValue == null ? true : !currentValue);
2259 }
2260 }
2261 if (noMoreValues && value == null) {
2262 return 0;
2263 }
2264 final ITypeConverter<?> converter = getTypeConverter(cls, field);
2265 final Object newValue = tryConvert(field, -1, converter, value, cls);
2266 final Object oldValue = field.get(command);
2267 TraceLevel level = TraceLevel.INFO;
2268 String traceMessage = "Setting %s field '%s.%s' to '%5$s' (was '%4$s') for %6$s%n";
2269 if (initialized != null) {
2270 if (initialized.contains(field)) {
2271 if (!isOverwrittenOptionsAllowed()) {
2272 throw new OverwrittenOptionException(CommandLine.this, optionDescription("", field, 0) + " should be specified only once");
2273 }
2274 level = TraceLevel.WARN;
2275 traceMessage = "Overwriting %s field '%s.%s' value '%s' with '%s' for %s%n";
2276 }
2277 initialized.add(field);
2278 }
2279 if (tracer.level.isEnabled(level)) { level.print(tracer, traceMessage, field.getType().getSimpleName(),
2280 field.getDeclaringClass().getSimpleName(), field.getName(), String.valueOf(oldValue), String.valueOf(newValue), argDescription);
2281 }
2282 field.set(command, newValue);
2283 return result;
2284 }
2285 private int applyValuesToMapField(final Field field,
2286 final Class<?> annotation,
2287 final Range arity,
2288 final Stack<String> args,
2289 final Class<?> cls,
2290 final String argDescription) throws Exception {
2291 final Class<?>[] classes = getTypeAttribute(field);
2292 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."); }
2293 final ITypeConverter<?> keyConverter = getTypeConverter(classes[0], field);
2294 final ITypeConverter<?> valueConverter = getTypeConverter(classes[1], field);
2295 Map<Object, Object> result = (Map<Object, Object>) field.get(command);
2296 if (result == null) {
2297 result = createMap(cls);
2298 field.set(command, result);
2299 }
2300 final int originalSize = result.size();
2301 consumeMapArguments(field, arity, args, classes, keyConverter, valueConverter, result, argDescription);
2302 return result.size() - originalSize;
2303 }
2304
2305 private void consumeMapArguments(final Field field,
2306 final Range arity,
2307 final Stack<String> args,
2308 final Class<?>[] classes,
2309 final ITypeConverter<?> keyConverter,
2310 final ITypeConverter<?> valueConverter,
2311 final Map<Object, Object> result,
2312 final String argDescription) throws Exception {
2313
2314 for (int i = 0; i < arity.min; i++) {
2315 consumeOneMapArgument(field, arity, args, classes, keyConverter, valueConverter, result, i, argDescription);
2316 }
2317
2318 for (int i = arity.min; i < arity.max && !args.isEmpty(); i++) {
2319 if (!field.isAnnotationPresent(Parameters.class)) {
2320 if (commands.containsKey(args.peek()) || isOption(args.peek())) {
2321 return;
2322 }
2323 }
2324 consumeOneMapArgument(field, arity, args, classes, keyConverter, valueConverter, result, i, argDescription);
2325 }
2326 }
2327
2328 private void consumeOneMapArgument(final Field field,
2329 final Range arity,
2330 final Stack<String> args,
2331 final Class<?>[] classes,
2332 final ITypeConverter<?> keyConverter, final ITypeConverter<?> valueConverter,
2333 final Map<Object, Object> result,
2334 final int index,
2335 final String argDescription) throws Exception {
2336 final String[] values = split(trim(args.pop()), field);
2337 for (final String value : values) {
2338 final String[] keyValue = value.split("=");
2339 if (keyValue.length < 2) {
2340 final String splitRegex = splitRegex(field);
2341 if (splitRegex.length() == 0) {
2342 throw new ParameterException(CommandLine.this, "Value for option " + optionDescription("", field,
2343 0) + " should be in KEY=VALUE format but was " + value);
2344 } else {
2345 throw new ParameterException(CommandLine.this, "Value for option " + optionDescription("", field,
2346 0) + " should be in KEY=VALUE[" + splitRegex + "KEY=VALUE]... format but was " + value);
2347 }
2348 }
2349 final Object mapKey = tryConvert(field, index, keyConverter, keyValue[0], classes[0]);
2350 final Object mapValue = tryConvert(field, index, valueConverter, keyValue[1], classes[1]);
2351 result.put(mapKey, mapValue);
2352 if (tracer.isInfo()) {tracer.info("Putting [%s : %s] in %s<%s, %s> field '%s.%s' for %s%n", String.valueOf(mapKey), String.valueOf(mapValue),
2353 result.getClass().getSimpleName(), classes[0].getSimpleName(), classes[1].getSimpleName(), field.getDeclaringClass().getSimpleName(), field.getName(), argDescription);}
2354 }
2355 }
2356
2357 private void checkMaxArityExceeded(final Range arity, final int remainder, final Field field, final String[] values) {
2358 if (values.length <= remainder) { return; }
2359 final String desc = arity.max == remainder ? "" + remainder : arity + ", remainder=" + remainder;
2360 throw new MaxValuesforFieldExceededException(CommandLine.this, optionDescription("", field, -1) +
2361 " max number of values (" + arity.max + ") exceeded: remainder is " + remainder + " but " +
2362 values.length + " values were specified: " + Arrays.toString(values));
2363 }
2364
2365 private int applyValuesToArrayField(final Field field,
2366 final Class<?> annotation,
2367 final Range arity,
2368 final Stack<String> args,
2369 final Class<?> cls,
2370 final String argDescription) throws Exception {
2371 final Object existing = field.get(command);
2372 final int length = existing == null ? 0 : Array.getLength(existing);
2373 final Class<?> type = getTypeAttribute(field)[0];
2374 final List<Object> converted = consumeArguments(field, annotation, arity, args, type, length, argDescription);
2375 final List<Object> newValues = new ArrayList<Object>();
2376 for (int i = 0; i < length; i++) {
2377 newValues.add(Array.get(existing, i));
2378 }
2379 for (final Object obj : converted) {
2380 if (obj instanceof Collection<?>) {
2381 newValues.addAll((Collection<?>) obj);
2382 } else {
2383 newValues.add(obj);
2384 }
2385 }
2386 final Object array = Array.newInstance(type, newValues.size());
2387 field.set(command, array);
2388 for (int i = 0; i < newValues.size(); i++) {
2389 Array.set(array, i, newValues.get(i));
2390 }
2391 return converted.size();
2392 }
2393
2394 @SuppressWarnings("unchecked")
2395 private int applyValuesToCollectionField(final Field field,
2396 final Class<?> annotation,
2397 final Range arity,
2398 final Stack<String> args,
2399 final Class<?> cls,
2400 final String argDescription) throws Exception {
2401 Collection<Object> collection = (Collection<Object>) field.get(command);
2402 final Class<?> type = getTypeAttribute(field)[0];
2403 final int length = collection == null ? 0 : collection.size();
2404 final List<Object> converted = consumeArguments(field, annotation, arity, args, type, length, argDescription);
2405 if (collection == null) {
2406 collection = createCollection(cls);
2407 field.set(command, collection);
2408 }
2409 for (final Object element : converted) {
2410 if (element instanceof Collection<?>) {
2411 collection.addAll((Collection<?>) element);
2412 } else {
2413 collection.add(element);
2414 }
2415 }
2416 return converted.size();
2417 }
2418
2419 private List<Object> consumeArguments(final Field field,
2420 final Class<?> annotation,
2421 final Range arity,
2422 final Stack<String> args,
2423 final Class<?> type,
2424 final int originalSize,
2425 final String argDescription) throws Exception {
2426 final List<Object> result = new ArrayList<Object>();
2427
2428
2429 for (int i = 0; i < arity.min; i++) {
2430 consumeOneArgument(field, arity, args, type, result, i, originalSize, argDescription);
2431 }
2432
2433 for (int i = arity.min; i < arity.max && !args.isEmpty(); i++) {
2434 if (annotation != Parameters.class) {
2435 if (commands.containsKey(args.peek()) || isOption(args.peek())) {
2436 return result;
2437 }
2438 }
2439 consumeOneArgument(field, arity, args, type, result, i, originalSize, argDescription);
2440 }
2441 return result;
2442 }
2443
2444 private int consumeOneArgument(final Field field,
2445 final Range arity,
2446 final Stack<String> args,
2447 final Class<?> type,
2448 final List<Object> result,
2449 int index,
2450 final int originalSize,
2451 final String argDescription) throws Exception {
2452 final String[] values = split(trim(args.pop()), field);
2453 final ITypeConverter<?> converter = getTypeConverter(type, field);
2454
2455 for (int j = 0; j < values.length; j++) {
2456 result.add(tryConvert(field, index, converter, values[j], type));
2457 if (tracer.isInfo()) {
2458 if (field.getType().isArray()) {
2459 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);
2460 } else {
2461 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);
2462 }
2463 }
2464 }
2465
2466 return ++index;
2467 }
2468
2469 private String splitRegex(final Field field) {
2470 if (field.isAnnotationPresent(Option.class)) { return field.getAnnotation(Option.class).split(); }
2471 if (field.isAnnotationPresent(Parameters.class)) { return field.getAnnotation(Parameters.class).split(); }
2472 return "";
2473 }
2474 private String[] split(final String value, final Field field) {
2475 final String regex = splitRegex(field);
2476 return regex.length() == 0 ? new String[] {value} : value.split(regex);
2477 }
2478
2479
2480
2481
2482
2483
2484
2485 private boolean isOption(final String arg) {
2486 if ("--".equals(arg)) {
2487 return true;
2488 }
2489
2490 if (optionName2Field.containsKey(arg)) {
2491 return true;
2492 }
2493 final int separatorIndex = arg.indexOf(separator);
2494 if (separatorIndex > 0) {
2495 if (optionName2Field.containsKey(arg.substring(0, separatorIndex))) {
2496 return true;
2497 }
2498 }
2499 return (arg.length() > 2 && arg.startsWith("-") && singleCharOption2Field.containsKey(arg.charAt(1)));
2500 }
2501 private Object tryConvert(final Field field, final int index, final ITypeConverter<?> converter, final String value, final Class<?> type)
2502 throws Exception {
2503 try {
2504 return converter.convert(value);
2505 } catch (final TypeConversionException ex) {
2506 throw new ParameterException(CommandLine.this, ex.getMessage() + optionDescription(" for ", field, index));
2507 } catch (final Exception other) {
2508 final String desc = optionDescription(" for ", field, index) + ": " + other;
2509 throw new ParameterException(CommandLine.this, "Could not convert '" + value + "' to " + type.getSimpleName() + desc, other);
2510 }
2511 }
2512
2513 private String optionDescription(final String prefix, final Field field, final int index) {
2514 final Help.IParamLabelRenderer labelRenderer = Help.createMinimalParamLabelRenderer();
2515 String desc = "";
2516 if (field.isAnnotationPresent(Option.class)) {
2517 desc = prefix + "option '" + field.getAnnotation(Option.class).names()[0] + "'";
2518 if (index >= 0) {
2519 final Range arity = Range.optionArity(field);
2520 if (arity.max > 1) {
2521 desc += " at index " + index;
2522 }
2523 desc += " (" + labelRenderer.renderParameterLabel(field, Help.Ansi.OFF, Collections.<IStyle>emptyList()) + ")";
2524 }
2525 } else if (field.isAnnotationPresent(Parameters.class)) {
2526 final Range indexRange = Range.parameterIndex(field);
2527 final Text label = labelRenderer.renderParameterLabel(field, Help.Ansi.OFF, Collections.<IStyle>emptyList());
2528 desc = prefix + "positional parameter at index " + indexRange + " (" + label + ")";
2529 }
2530 return desc;
2531 }
2532
2533 private boolean isAnyHelpRequested() { return isHelpRequested || versionHelpRequested || usageHelpRequested; }
2534
2535 private void updateHelpRequested(final Field field) {
2536 if (field.isAnnotationPresent(Option.class)) {
2537 isHelpRequested |= is(field, "help", field.getAnnotation(Option.class).help());
2538 CommandLine.this.versionHelpRequested |= is(field, "versionHelp", field.getAnnotation(Option.class).versionHelp());
2539 CommandLine.this.usageHelpRequested |= is(field, "usageHelp", field.getAnnotation(Option.class).usageHelp());
2540 }
2541 }
2542 private boolean is(final Field f, final String description, final boolean value) {
2543 if (value) { if (tracer.isInfo()) {tracer.info("Field '%s.%s' has '%s' annotation: not validating required fields%n", f.getDeclaringClass().getSimpleName(), f.getName(), description); }}
2544 return value;
2545 }
2546 @SuppressWarnings("unchecked")
2547 private Collection<Object> createCollection(final Class<?> collectionClass) throws Exception {
2548 if (collectionClass.isInterface()) {
2549 if (List.class.isAssignableFrom(collectionClass)) {
2550 return new ArrayList<Object>();
2551 } else if (SortedSet.class.isAssignableFrom(collectionClass)) {
2552 return new TreeSet<Object>();
2553 } else if (Set.class.isAssignableFrom(collectionClass)) {
2554 return new LinkedHashSet<Object>();
2555 } else if (Queue.class.isAssignableFrom(collectionClass)) {
2556 return new LinkedList<Object>();
2557 }
2558 return new ArrayList<Object>();
2559 }
2560
2561 return (Collection<Object>) collectionClass.newInstance();
2562 }
2563 private Map<Object, Object> createMap(final Class<?> mapClass) throws Exception {
2564 try {
2565 return (Map<Object, Object>) mapClass.newInstance();
2566 } catch (final Exception ignored) {}
2567 return new LinkedHashMap<Object, Object>();
2568 }
2569 private ITypeConverter<?> getTypeConverter(final Class<?> type, final Field field) {
2570 final ITypeConverter<?> result = converterRegistry.get(type);
2571 if (result != null) {
2572 return result;
2573 }
2574 if (type.isEnum()) {
2575 return new ITypeConverter<Object>() {
2576 @Override
2577 @SuppressWarnings("unchecked")
2578 public Object convert(final String value) throws Exception {
2579 return Enum.valueOf((Class<Enum>) type, value);
2580 }
2581 };
2582 }
2583 throw new MissingTypeConverterException(CommandLine.this, "No TypeConverter registered for " + type.getName() + " of field " + field);
2584 }
2585
2586 private void assertNoMissingParameters(final Field field, final int arity, final Stack<String> args) {
2587 if (arity > args.size()) {
2588 if (arity == 1) {
2589 if (field.isAnnotationPresent(Option.class)) {
2590 throw new MissingParameterException(CommandLine.this, "Missing required parameter for " +
2591 optionDescription("", field, 0));
2592 }
2593 final Range indexRange = Range.parameterIndex(field);
2594 final Help.IParamLabelRenderer labelRenderer = Help.createMinimalParamLabelRenderer();
2595 String sep = "";
2596 String names = "";
2597 int count = 0;
2598 for (int i = indexRange.min; i < positionalParametersFields.size(); i++) {
2599 if (Range.parameterArity(positionalParametersFields.get(i)).min > 0) {
2600 names += sep + labelRenderer.renderParameterLabel(positionalParametersFields.get(i),
2601 Help.Ansi.OFF, Collections.<IStyle>emptyList());
2602 sep = ", ";
2603 count++;
2604 }
2605 }
2606 String msg = "Missing required parameter";
2607 final Range paramArity = Range.parameterArity(field);
2608 if (paramArity.isVariable) {
2609 msg += "s at positions " + indexRange + ": ";
2610 } else {
2611 msg += (count > 1 ? "s: " : ": ");
2612 }
2613 throw new MissingParameterException(CommandLine.this, msg + names);
2614 }
2615 if (args.isEmpty()) {
2616 throw new MissingParameterException(CommandLine.this, optionDescription("", field, 0) +
2617 " requires at least " + arity + " values, but none were specified.");
2618 }
2619 throw new MissingParameterException(CommandLine.this, optionDescription("", field, 0) +
2620 " requires at least " + arity + " values, but only " + args.size() + " were specified: " + reverse(args));
2621 }
2622 }
2623 private String trim(final String value) {
2624 return unquote(value);
2625 }
2626
2627 private String unquote(final String value) {
2628 return value == null
2629 ? null
2630 : (value.length() > 1 && value.startsWith("\"") && value.endsWith("\""))
2631 ? value.substring(1, value.length() - 1)
2632 : value;
2633 }
2634 }
2635 private static class PositionalParametersSorter implements Comparator<Field> {
2636 @Override
2637 public int compare(final Field o1, final Field o2) {
2638 final int result = Range.parameterIndex(o1).compareTo(Range.parameterIndex(o2));
2639 return (result == 0) ? Range.parameterArity(o1).compareTo(Range.parameterArity(o2)) : result;
2640 }
2641 }
2642
2643
2644
2645 private static class BuiltIn {
2646 static class PathConverter implements ITypeConverter<Path> {
2647 @Override public Path convert(final String value) { return Paths.get(value); }
2648 }
2649 static class StringConverter implements ITypeConverter<String> {
2650 @Override
2651 public String convert(final String value) { return value; }
2652 }
2653 static class StringBuilderConverter implements ITypeConverter<StringBuilder> {
2654 @Override
2655 public StringBuilder convert(final String value) { return new StringBuilder(value); }
2656 }
2657 static class CharSequenceConverter implements ITypeConverter<CharSequence> {
2658 @Override
2659 public String convert(final String value) { return value; }
2660 }
2661
2662 static class ByteConverter implements ITypeConverter<Byte> {
2663 @Override
2664 public Byte convert(final String value) { return Byte.valueOf(value); }
2665 }
2666
2667 static class BooleanConverter implements ITypeConverter<Boolean> {
2668 @Override
2669 public Boolean convert(final String value) {
2670 if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
2671 return Boolean.parseBoolean(value);
2672 } else {
2673 throw new TypeConversionException("'" + value + "' is not a boolean");
2674 }
2675 }
2676 }
2677 static class CharacterConverter implements ITypeConverter<Character> {
2678 @Override
2679 public Character convert(final String value) {
2680 if (value.length() > 1) {
2681 throw new TypeConversionException("'" + value + "' is not a single character");
2682 }
2683 return value.charAt(0);
2684 }
2685 }
2686
2687 static class ShortConverter implements ITypeConverter<Short> {
2688 @Override
2689 public Short convert(final String value) { return Short.valueOf(value); }
2690 }
2691
2692 static class IntegerConverter implements ITypeConverter<Integer> {
2693 @Override
2694 public Integer convert(final String value) { return Integer.valueOf(value); }
2695 }
2696
2697 static class LongConverter implements ITypeConverter<Long> {
2698 @Override
2699 public Long convert(final String value) { return Long.valueOf(value); }
2700 }
2701 static class FloatConverter implements ITypeConverter<Float> {
2702 @Override
2703 public Float convert(final String value) { return Float.valueOf(value); }
2704 }
2705 static class DoubleConverter implements ITypeConverter<Double> {
2706 @Override
2707 public Double convert(final String value) { return Double.valueOf(value); }
2708 }
2709 static class FileConverter implements ITypeConverter<File> {
2710 @Override
2711 public File convert(final String value) { return new File(value); }
2712 }
2713 static class URLConverter implements ITypeConverter<URL> {
2714 @Override
2715 public URL convert(final String value) throws MalformedURLException { return new URL(value); }
2716 }
2717 static class URIConverter implements ITypeConverter<URI> {
2718 @Override
2719 public URI convert(final String value) throws URISyntaxException { return new URI(value); }
2720 }
2721
2722 static class ISO8601DateConverter implements ITypeConverter<Date> {
2723 @Override
2724 public Date convert(final String value) {
2725 try {
2726 return new SimpleDateFormat("yyyy-MM-dd").parse(value);
2727 } catch (final ParseException e) {
2728 throw new TypeConversionException("'" + value + "' is not a yyyy-MM-dd date");
2729 }
2730 }
2731 }
2732
2733
2734 static class ISO8601TimeConverter implements ITypeConverter<Time> {
2735 @Override
2736 public Time convert(final String value) {
2737 try {
2738 if (value.length() <= 5) {
2739 return new Time(new SimpleDateFormat("HH:mm").parse(value).getTime());
2740 } else if (value.length() <= 8) {
2741 return new Time(new SimpleDateFormat("HH:mm:ss").parse(value).getTime());
2742 } else if (value.length() <= 12) {
2743 try {
2744 return new Time(new SimpleDateFormat("HH:mm:ss.SSS").parse(value).getTime());
2745 } catch (final ParseException e2) {
2746 return new Time(new SimpleDateFormat("HH:mm:ss,SSS").parse(value).getTime());
2747 }
2748 }
2749 } catch (final ParseException ignored) {
2750
2751 }
2752 throw new TypeConversionException("'" + value + "' is not a HH:mm[:ss[.SSS]] time");
2753 }
2754 }
2755 static class BigDecimalConverter implements ITypeConverter<BigDecimal> {
2756 @Override
2757 public BigDecimal convert(final String value) { return new BigDecimal(value); }
2758 }
2759 static class BigIntegerConverter implements ITypeConverter<BigInteger> {
2760 @Override
2761 public BigInteger convert(final String value) { return new BigInteger(value); }
2762 }
2763 static class CharsetConverter implements ITypeConverter<Charset> {
2764 @Override
2765 public Charset convert(final String s) { return Charset.forName(s); }
2766 }
2767
2768 static class InetAddressConverter implements ITypeConverter<InetAddress> {
2769 @Override
2770 public InetAddress convert(final String s) throws Exception { return InetAddress.getByName(s); }
2771 }
2772 static class PatternConverter implements ITypeConverter<Pattern> {
2773 @Override
2774 public Pattern convert(final String s) { return Pattern.compile(s); }
2775 }
2776 static class UUIDConverter implements ITypeConverter<UUID> {
2777 @Override
2778 public UUID convert(final String s) throws Exception { return UUID.fromString(s); }
2779 }
2780 private BuiltIn() {}
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
2816 public static class Help {
2817
2818 protected static final String DEFAULT_COMMAND_NAME = "<main class>";
2819
2820
2821 protected static final String DEFAULT_SEPARATOR = "=";
2822
2823 private final static int usageHelpWidth = 80;
2824 private final static int optionsColumnWidth = 2 + 2 + 1 + 24;
2825 private final Object command;
2826 private final Map<String, Help> commands = new LinkedHashMap<String, Help>();
2827 final ColorScheme colorScheme;
2828
2829
2830 public final List<Field> optionFields;
2831
2832
2833 public final List<Field> positionalParametersFields;
2834
2835
2836
2837
2838 public String separator;
2839
2840
2841
2842 public String commandName = DEFAULT_COMMAND_NAME;
2843
2844
2845
2846
2847
2848 public String[] description = {};
2849
2850
2851
2852
2853
2854 public String[] customSynopsis = {};
2855
2856
2857
2858
2859
2860 public String[] header = {};
2861
2862
2863
2864
2865
2866 public String[] footer = {};
2867
2868
2869
2870
2871
2872 public IParamLabelRenderer parameterLabelRenderer;
2873
2874
2875 public Boolean abbreviateSynopsis;
2876
2877
2878 public Boolean sortOptions;
2879
2880
2881 public Boolean showDefaultValues;
2882
2883
2884 public Character requiredOptionMarker;
2885
2886
2887 public String headerHeading;
2888
2889
2890 public String synopsisHeading;
2891
2892
2893 public String descriptionHeading;
2894
2895
2896 public String parameterListHeading;
2897
2898
2899 public String optionListHeading;
2900
2901
2902 public String commandListHeading;
2903
2904
2905 public String footerHeading;
2906
2907
2908
2909
2910 public Help(final Object command) {
2911 this(command, Ansi.AUTO);
2912 }
2913
2914
2915
2916
2917
2918 public Help(final Object command, final Ansi ansi) {
2919 this(command, defaultColorScheme(ansi));
2920 }
2921
2922
2923
2924
2925
2926 public Help(final Object command, final ColorScheme colorScheme) {
2927 this.command = Assert.notNull(command, "command");
2928 this.colorScheme = Assert.notNull(colorScheme, "colorScheme").applySystemProperties();
2929 final List<Field> options = new ArrayList<Field>();
2930 final List<Field> operands = new ArrayList<Field>();
2931 Class<?> cls = command.getClass();
2932 while (cls != null) {
2933 for (final Field field : cls.getDeclaredFields()) {
2934 field.setAccessible(true);
2935 if (field.isAnnotationPresent(Option.class)) {
2936 final Option option = field.getAnnotation(Option.class);
2937 if (!option.hidden()) {
2938
2939 options.add(field);
2940 }
2941 }
2942 if (field.isAnnotationPresent(Parameters.class)) {
2943 operands.add(field);
2944 }
2945 }
2946
2947 if (cls.isAnnotationPresent(Command.class)) {
2948 final Command cmd = cls.getAnnotation(Command.class);
2949 if (DEFAULT_COMMAND_NAME.equals(commandName)) {
2950 commandName = cmd.name();
2951 }
2952 separator = (separator == null) ? cmd.separator() : separator;
2953 abbreviateSynopsis = (abbreviateSynopsis == null) ? cmd.abbreviateSynopsis() : abbreviateSynopsis;
2954 sortOptions = (sortOptions == null) ? cmd.sortOptions() : sortOptions;
2955 requiredOptionMarker = (requiredOptionMarker == null) ? cmd.requiredOptionMarker() : requiredOptionMarker;
2956 showDefaultValues = (showDefaultValues == null) ? cmd.showDefaultValues() : showDefaultValues;
2957 customSynopsis = empty(customSynopsis) ? cmd.customSynopsis() : customSynopsis;
2958 description = empty(description) ? cmd.description() : description;
2959 header = empty(header) ? cmd.header() : header;
2960 footer = empty(footer) ? cmd.footer() : footer;
2961 headerHeading = empty(headerHeading) ? cmd.headerHeading() : headerHeading;
2962 synopsisHeading = empty(synopsisHeading) || "Usage: ".equals(synopsisHeading) ? cmd.synopsisHeading() : synopsisHeading;
2963 descriptionHeading = empty(descriptionHeading) ? cmd.descriptionHeading() : descriptionHeading;
2964 parameterListHeading = empty(parameterListHeading) ? cmd.parameterListHeading() : parameterListHeading;
2965 optionListHeading = empty(optionListHeading) ? cmd.optionListHeading() : optionListHeading;
2966 commandListHeading = empty(commandListHeading) || "Commands:%n".equals(commandListHeading) ? cmd.commandListHeading() : commandListHeading;
2967 footerHeading = empty(footerHeading) ? cmd.footerHeading() : footerHeading;
2968 }
2969 cls = cls.getSuperclass();
2970 }
2971 sortOptions = (sortOptions == null) ? true : sortOptions;
2972 abbreviateSynopsis = (abbreviateSynopsis == null) ? false : abbreviateSynopsis;
2973 requiredOptionMarker = (requiredOptionMarker == null) ? ' ' : requiredOptionMarker;
2974 showDefaultValues = (showDefaultValues == null) ? false : showDefaultValues;
2975 synopsisHeading = (synopsisHeading == null) ? "Usage: " : synopsisHeading;
2976 commandListHeading = (commandListHeading == null) ? "Commands:%n" : commandListHeading;
2977 separator = (separator == null) ? DEFAULT_SEPARATOR : separator;
2978 parameterLabelRenderer = createDefaultParamLabelRenderer();
2979 Collections.sort(operands, new PositionalParametersSorter());
2980 positionalParametersFields = Collections.unmodifiableList(operands);
2981 optionFields = Collections.unmodifiableList(options);
2982 }
2983
2984
2985
2986
2987
2988
2989 public Help addAllSubcommands(final Map<String, CommandLine> commands) {
2990 if (commands != null) {
2991 for (final Map.Entry<String, CommandLine> entry : commands.entrySet()) {
2992 addSubcommand(entry.getKey(), entry.getValue().getCommand());
2993 }
2994 }
2995 return this;
2996 }
2997
2998
2999
3000
3001
3002
3003 public Help addSubcommand(final String commandName, final Object command) {
3004 commands.put(commandName, new Help(command));
3005 return this;
3006 }
3007
3008
3009
3010
3011
3012
3013
3014 @Deprecated
3015 public String synopsis() { return synopsis(0); }
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025 public String synopsis(final int synopsisHeadingLength) {
3026 if (!empty(customSynopsis)) { return customSynopsis(); }
3027 return abbreviateSynopsis ? abbreviatedSynopsis()
3028 : detailedSynopsis(synopsisHeadingLength, createShortOptionArityAndNameComparator(), true);
3029 }
3030
3031
3032
3033
3034 public String abbreviatedSynopsis() {
3035 final StringBuilder sb = new StringBuilder();
3036 if (!optionFields.isEmpty()) {
3037 sb.append(" [OPTIONS]");
3038 }
3039
3040 for (final Field positionalParam : positionalParametersFields) {
3041 if (!positionalParam.getAnnotation(Parameters.class).hidden()) {
3042 sb.append(' ').append(parameterLabelRenderer.renderParameterLabel(positionalParam, ansi(), colorScheme.parameterStyles));
3043 }
3044 }
3045 return colorScheme.commandText(commandName).toString()
3046 + (sb.toString()) + System.getProperty("line.separator");
3047 }
3048
3049
3050
3051
3052
3053
3054 @Deprecated
3055 public String detailedSynopsis(final Comparator<Field> optionSort, final boolean clusterBooleanOptions) {
3056 return detailedSynopsis(0, optionSort, clusterBooleanOptions);
3057 }
3058
3059
3060
3061
3062
3063
3064
3065 public String detailedSynopsis(final int synopsisHeadingLength, final Comparator<Field> optionSort, final boolean clusterBooleanOptions) {
3066 Text optionText = ansi().new Text(0);
3067 final List<Field> fields = new ArrayList<Field>(optionFields);
3068 if (optionSort != null) {
3069 Collections.sort(fields, optionSort);
3070 }
3071 if (clusterBooleanOptions) {
3072 final List<Field> booleanOptions = new ArrayList<Field>();
3073 final StringBuilder clusteredRequired = new StringBuilder("-");
3074 final StringBuilder clusteredOptional = new StringBuilder("-");
3075 for (final Field field : fields) {
3076 if (field.getType() == boolean.class || field.getType() == Boolean.class) {
3077 final Option option = field.getAnnotation(Option.class);
3078 final String shortestName = ShortestFirst.sort(option.names())[0];
3079 if (shortestName.length() == 2 && shortestName.startsWith("-")) {
3080 booleanOptions.add(field);
3081 if (option.required()) {
3082 clusteredRequired.append(shortestName.substring(1));
3083 } else {
3084 clusteredOptional.append(shortestName.substring(1));
3085 }
3086 }
3087 }
3088 }
3089 fields.removeAll(booleanOptions);
3090 if (clusteredRequired.length() > 1) {
3091 optionText = optionText.append(" ").append(colorScheme.optionText(clusteredRequired.toString()));
3092 }
3093 if (clusteredOptional.length() > 1) {
3094 optionText = optionText.append(" [").append(colorScheme.optionText(clusteredOptional.toString())).append("]");
3095 }
3096 }
3097 for (final Field field : fields) {
3098 final Option option = field.getAnnotation(Option.class);
3099 if (!option.hidden()) {
3100 if (option.required()) {
3101 optionText = appendOptionSynopsis(optionText, field, ShortestFirst.sort(option.names())[0], " ", "");
3102 if (isMultiValue(field)) {
3103 optionText = appendOptionSynopsis(optionText, field, ShortestFirst.sort(option.names())[0], " [", "]...");
3104 }
3105 } else {
3106 optionText = appendOptionSynopsis(optionText, field, ShortestFirst.sort(option.names())[0], " [", "]");
3107 if (isMultiValue(field)) {
3108 optionText = optionText.append("...");
3109 }
3110 }
3111 }
3112 }
3113 for (final Field positionalParam : positionalParametersFields) {
3114 if (!positionalParam.getAnnotation(Parameters.class).hidden()) {
3115 optionText = optionText.append(" ");
3116 final Text label = parameterLabelRenderer.renderParameterLabel(positionalParam, colorScheme.ansi(), colorScheme.parameterStyles);
3117 optionText = optionText.append(label);
3118 }
3119 }
3120
3121 final int firstColumnLength = commandName.length() + synopsisHeadingLength;
3122
3123
3124 final TextTable textTable = new TextTable(ansi(), firstColumnLength, usageHelpWidth - firstColumnLength);
3125 textTable.indentWrappedLines = 1;
3126
3127
3128 final Text PADDING = Ansi.OFF.new Text(stringOf('X', synopsisHeadingLength));
3129 textTable.addRowValues(new Text[] {PADDING.append(colorScheme.commandText(commandName)), optionText});
3130 return textTable.toString().substring(synopsisHeadingLength);
3131 }
3132
3133 private Text appendOptionSynopsis(final Text optionText, final Field field, final String optionName, final String prefix, final String suffix) {
3134 final Text optionParamText = parameterLabelRenderer.renderParameterLabel(field, colorScheme.ansi(), colorScheme.optionParamStyles);
3135 return optionText.append(prefix)
3136 .append(colorScheme.optionText(optionName))
3137 .append(optionParamText)
3138 .append(suffix);
3139 }
3140
3141
3142
3143
3144
3145 public int synopsisHeadingLength() {
3146 final String[] lines = Ansi.OFF.new Text(synopsisHeading).toString().split("\\r?\\n|\\r|%n", -1);
3147 return lines[lines.length - 1].length();
3148 }
3149
3150
3151
3152
3153
3154
3155
3156
3157 public String optionList() {
3158 final Comparator<Field> sortOrder = sortOptions == null || sortOptions.booleanValue()
3159 ? createShortOptionNameComparator()
3160 : null;
3161 return optionList(createDefaultLayout(), sortOrder, parameterLabelRenderer);
3162 }
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172 public String optionList(final Layout layout, final Comparator<Field> optionSort, final IParamLabelRenderer valueLabelRenderer) {
3173 final List<Field> fields = new ArrayList<Field>(optionFields);
3174 if (optionSort != null) {
3175 Collections.sort(fields, optionSort);
3176 }
3177 layout.addOptions(fields, valueLabelRenderer);
3178 return layout.toString();
3179 }
3180
3181
3182
3183
3184
3185 public String parameterList() {
3186 return parameterList(createDefaultLayout(), parameterLabelRenderer);
3187 }
3188
3189
3190
3191
3192
3193
3194 public String parameterList(final Layout layout, final IParamLabelRenderer paramLabelRenderer) {
3195 layout.addPositionalParameters(positionalParametersFields, paramLabelRenderer);
3196 return layout.toString();
3197 }
3198
3199 private static String heading(final Ansi ansi, final String values, final Object... params) {
3200 final StringBuilder sb = join(ansi, new String[] {values}, new StringBuilder(), params);
3201 String result = sb.toString();
3202 result = result.endsWith(System.getProperty("line.separator"))
3203 ? result.substring(0, result.length() - System.getProperty("line.separator").length()) : result;
3204 return result + new String(spaces(countTrailingSpaces(values)));
3205 }
3206 private static char[] spaces(final int length) { final char[] result = new char[length]; Arrays.fill(result, ' '); return result; }
3207 private static int countTrailingSpaces(final String str) {
3208 if (str == null) {return 0;}
3209 int trailingSpaces = 0;
3210 for (int i = str.length() - 1; i >= 0 && str.charAt(i) == ' '; i--) { trailingSpaces++; }
3211 return trailingSpaces;
3212 }
3213
3214
3215
3216
3217
3218
3219
3220 public static StringBuilder join(final Ansi ansi, final String[] values, final StringBuilder sb, final Object... params) {
3221 if (values != null) {
3222 final TextTable table = new TextTable(ansi, usageHelpWidth);
3223 table.indentWrappedLines = 0;
3224 for (final String summaryLine : values) {
3225 final Text[] lines = ansi.new Text(format(summaryLine, params)).splitLines();
3226 for (final Text line : lines) { table.addRowValues(line); }
3227 }
3228 table.toString(sb);
3229 }
3230 return sb;
3231 }
3232 private static String format(final String formatString, final Object... params) {
3233 return formatString == null ? "" : String.format(formatString, params);
3234 }
3235
3236
3237
3238
3239
3240
3241 public String customSynopsis(final Object... params) {
3242 return join(ansi(), customSynopsis, new StringBuilder(), params).toString();
3243 }
3244
3245
3246
3247
3248
3249
3250 public String description(final Object... params) {
3251 return join(ansi(), description, new StringBuilder(), params).toString();
3252 }
3253
3254
3255
3256
3257
3258
3259 public String header(final Object... params) {
3260 return join(ansi(), header, new StringBuilder(), params).toString();
3261 }
3262
3263
3264
3265
3266
3267
3268 public String footer(final Object... params) {
3269 return join(ansi(), footer, new StringBuilder(), params).toString();
3270 }
3271
3272
3273
3274
3275 public String headerHeading(final Object... params) {
3276 return heading(ansi(), headerHeading, params);
3277 }
3278
3279
3280
3281
3282 public String synopsisHeading(final Object... params) {
3283 return heading(ansi(), synopsisHeading, params);
3284 }
3285
3286
3287
3288
3289
3290 public String descriptionHeading(final Object... params) {
3291 return empty(descriptionHeading) ? "" : heading(ansi(), descriptionHeading, params);
3292 }
3293
3294
3295
3296
3297
3298 public String parameterListHeading(final Object... params) {
3299 return positionalParametersFields.isEmpty() ? "" : heading(ansi(), parameterListHeading, params);
3300 }
3301
3302
3303
3304
3305
3306 public String optionListHeading(final Object... params) {
3307 return optionFields.isEmpty() ? "" : heading(ansi(), optionListHeading, params);
3308 }
3309
3310
3311
3312
3313
3314 public String commandListHeading(final Object... params) {
3315 return commands.isEmpty() ? "" : heading(ansi(), commandListHeading, params);
3316 }
3317
3318
3319
3320
3321 public String footerHeading(final Object... params) {
3322 return heading(ansi(), footerHeading, params);
3323 }
3324
3325
3326 public String commandList() {
3327 if (commands.isEmpty()) { return ""; }
3328 final int commandLength = maxLength(commands.keySet());
3329 final Help.TextTable textTable = new Help.TextTable(ansi(),
3330 new Help.Column(commandLength + 2, 2, Help.Column.Overflow.SPAN),
3331 new Help.Column(usageHelpWidth - (commandLength + 2), 2, Help.Column.Overflow.WRAP));
3332
3333 for (final Map.Entry<String, Help> entry : commands.entrySet()) {
3334 final Help command = entry.getValue();
3335 final String header = command.header != null && command.header.length > 0 ? command.header[0]
3336 : (command.description != null && command.description.length > 0 ? command.description[0] : "");
3337 textTable.addRowValues(colorScheme.commandText(entry.getKey()), ansi().new Text(header));
3338 }
3339 return textTable.toString();
3340 }
3341 private static int maxLength(final Collection<String> any) {
3342 final List<String> strings = new ArrayList<String>(any);
3343 Collections.sort(strings, Collections.reverseOrder(Help.shortestFirst()));
3344 return strings.get(0).length();
3345 }
3346 private static String join(final String[] names, final int offset, final int length, final String separator) {
3347 if (names == null) { return ""; }
3348 final StringBuilder result = new StringBuilder();
3349 for (int i = offset; i < offset + length; i++) {
3350 result.append((i > offset) ? separator : "").append(names[i]);
3351 }
3352 return result.toString();
3353 }
3354 private static String stringOf(final char chr, final int length) {
3355 final char[] buff = new char[length];
3356 Arrays.fill(buff, chr);
3357 return new String(buff);
3358 }
3359
3360
3361
3362 public Layout createDefaultLayout() {
3363 return new Layout(colorScheme, new TextTable(colorScheme.ansi()), createDefaultOptionRenderer(), createDefaultParameterRenderer());
3364 }
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379 public IOptionRenderer createDefaultOptionRenderer() {
3380 final DefaultOptionRenderer result = new DefaultOptionRenderer();
3381 result.requiredMarker = String.valueOf(requiredOptionMarker);
3382 if (showDefaultValues != null && showDefaultValues.booleanValue()) {
3383 result.command = this.command;
3384 }
3385 return result;
3386 }
3387
3388
3389
3390 public static IOptionRenderer createMinimalOptionRenderer() {
3391 return new MinimalOptionRenderer();
3392 }
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407 public IParameterRenderer createDefaultParameterRenderer() {
3408 final DefaultParameterRenderer result = new DefaultParameterRenderer();
3409 result.requiredMarker = String.valueOf(requiredOptionMarker);
3410 return result;
3411 }
3412
3413
3414
3415 public static IParameterRenderer createMinimalParameterRenderer() {
3416 return new MinimalParameterRenderer();
3417 }
3418
3419
3420
3421 public static IParamLabelRenderer createMinimalParamLabelRenderer() {
3422 return new IParamLabelRenderer() {
3423 @Override
3424 public Text renderParameterLabel(final Field field, final Ansi ansi, final List<IStyle> styles) {
3425 final String text = DefaultParamLabelRenderer.renderParameterName(field);
3426 return ansi.apply(text, styles);
3427 }
3428 @Override
3429 public String separator() { return ""; }
3430 };
3431 }
3432
3433
3434
3435
3436
3437 public IParamLabelRenderer createDefaultParamLabelRenderer() {
3438 return new DefaultParamLabelRenderer(separator);
3439 }
3440
3441
3442
3443 public static Comparator<Field> createShortOptionNameComparator() {
3444 return new SortByShortestOptionNameAlphabetically();
3445 }
3446
3447
3448
3449 public static Comparator<Field> createShortOptionArityAndNameComparator() {
3450 return new SortByOptionArityAndNameAlphabetically();
3451 }
3452
3453
3454 public static Comparator<String> shortestFirst() {
3455 return new ShortestFirst();
3456 }
3457
3458
3459
3460
3461 public Ansi ansi() {
3462 return colorScheme.ansi;
3463 }
3464
3465
3466
3467
3468
3469 public interface IOptionRenderer {
3470
3471
3472
3473
3474
3475
3476
3477
3478 Text[][] render(Option option, Field field, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme);
3479 }
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492 static class DefaultOptionRenderer implements IOptionRenderer {
3493 public String requiredMarker = " ";
3494 public Object command;
3495 private String sep;
3496 private boolean showDefault;
3497 @Override
3498 public Text[][] render(final Option option, final Field field, final IParamLabelRenderer paramLabelRenderer, final ColorScheme scheme) {
3499 final String[] names = ShortestFirst.sort(option.names());
3500 final int shortOptionCount = names[0].length() == 2 ? 1 : 0;
3501 final String shortOption = shortOptionCount > 0 ? names[0] : "";
3502 sep = shortOptionCount > 0 && names.length > 1 ? "," : "";
3503
3504 final String longOption = join(names, shortOptionCount, names.length - shortOptionCount, ", ");
3505 final Text longOptionText = createLongOptionText(field, paramLabelRenderer, scheme, longOption);
3506
3507 showDefault = command != null && !option.help() && !isBoolean(field.getType());
3508 final Object defaultValue = createDefaultValue(field);
3509
3510 final String requiredOption = option.required() ? requiredMarker : "";
3511 return renderDescriptionLines(option, scheme, requiredOption, shortOption, longOptionText, defaultValue);
3512 }
3513
3514 private Object createDefaultValue(final Field field) {
3515 Object defaultValue = null;
3516 try {
3517 defaultValue = field.get(command);
3518 if (defaultValue == null) { showDefault = false; }
3519 else if (field.getType().isArray()) {
3520 final StringBuilder sb = new StringBuilder();
3521 for (int i = 0; i < Array.getLength(defaultValue); i++) {
3522 sb.append(i > 0 ? ", " : "").append(Array.get(defaultValue, i));
3523 }
3524 defaultValue = sb.insert(0, "[").append("]").toString();
3525 }
3526 } catch (final Exception ex) {
3527 showDefault = false;
3528 }
3529 return defaultValue;
3530 }
3531
3532 private Text createLongOptionText(final Field field, final IParamLabelRenderer renderer, final ColorScheme scheme, final String longOption) {
3533 Text paramLabelText = renderer.renderParameterLabel(field, scheme.ansi(), scheme.optionParamStyles);
3534
3535
3536 if (paramLabelText.length > 0 && longOption.length() == 0) {
3537 sep = renderer.separator();
3538
3539 final int sepStart = paramLabelText.plainString().indexOf(sep);
3540 final Text prefix = paramLabelText.substring(0, sepStart);
3541 paramLabelText = prefix.append(paramLabelText.substring(sepStart + sep.length()));
3542 }
3543 Text longOptionText = scheme.optionText(longOption);
3544 longOptionText = longOptionText.append(paramLabelText);
3545 return longOptionText;
3546 }
3547
3548 private Text[][] renderDescriptionLines(final Option option,
3549 final ColorScheme scheme,
3550 final String requiredOption,
3551 final String shortOption,
3552 final Text longOptionText,
3553 final Object defaultValue) {
3554 final Text EMPTY = Ansi.EMPTY_TEXT;
3555 final List<Text[]> result = new ArrayList<Text[]>();
3556 Text[] descriptionFirstLines = scheme.ansi().new Text(str(option.description(), 0)).splitLines();
3557 if (descriptionFirstLines.length == 0) {
3558 if (showDefault) {
3559 descriptionFirstLines = new Text[]{scheme.ansi().new Text(" Default: " + defaultValue)};
3560 showDefault = false;
3561 } else {
3562 descriptionFirstLines = new Text[]{ EMPTY };
3563 }
3564 }
3565 result.add(new Text[] { scheme.optionText(requiredOption), scheme.optionText(shortOption),
3566 scheme.ansi().new Text(sep), longOptionText, descriptionFirstLines[0] });
3567 for (int i = 1; i < descriptionFirstLines.length; i++) {
3568 result.add(new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, descriptionFirstLines[i] });
3569 }
3570 for (int i = 1; i < option.description().length; i++) {
3571 final Text[] descriptionNextLines = scheme.ansi().new Text(option.description()[i]).splitLines();
3572 for (final Text line : descriptionNextLines) {
3573 result.add(new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, line });
3574 }
3575 }
3576 if (showDefault) {
3577 result.add(new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, scheme.ansi().new Text(" Default: " + defaultValue) });
3578 }
3579 return result.toArray(new Text[result.size()][]);
3580 }
3581 }
3582
3583
3584 static class MinimalOptionRenderer implements IOptionRenderer {
3585 @Override
3586 public Text[][] render(final Option option, final Field field, final IParamLabelRenderer parameterLabelRenderer, final ColorScheme scheme) {
3587 Text optionText = scheme.optionText(option.names()[0]);
3588 final Text paramLabelText = parameterLabelRenderer.renderParameterLabel(field, scheme.ansi(), scheme.optionParamStyles);
3589 optionText = optionText.append(paramLabelText);
3590 return new Text[][] {{ optionText,
3591 scheme.ansi().new Text(option.description().length == 0 ? "" : option.description()[0]) }};
3592 }
3593 }
3594
3595
3596 static class MinimalParameterRenderer implements IParameterRenderer {
3597 @Override
3598 public Text[][] render(final Parameters param, final Field field, final IParamLabelRenderer parameterLabelRenderer, final ColorScheme scheme) {
3599 return new Text[][] {{ parameterLabelRenderer.renderParameterLabel(field, scheme.ansi(), scheme.parameterStyles),
3600 scheme.ansi().new Text(param.description().length == 0 ? "" : param.description()[0]) }};
3601 }
3602 }
3603
3604
3605
3606
3607 public interface IParameterRenderer {
3608
3609
3610
3611
3612
3613
3614
3615
3616 Text[][] render(Parameters parameters, Field field, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme);
3617 }
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630 static class DefaultParameterRenderer implements IParameterRenderer {
3631 public String requiredMarker = " ";
3632 @Override
3633 public Text[][] render(final Parameters params, final Field field, final IParamLabelRenderer paramLabelRenderer, final ColorScheme scheme) {
3634 final Text label = paramLabelRenderer.renderParameterLabel(field, scheme.ansi(), scheme.parameterStyles);
3635 final Text requiredParameter = scheme.parameterText(Range.parameterArity(field).min > 0 ? requiredMarker : "");
3636
3637 final Text EMPTY = Ansi.EMPTY_TEXT;
3638 final List<Text[]> result = new ArrayList<Text[]>();
3639 Text[] descriptionFirstLines = scheme.ansi().new Text(str(params.description(), 0)).splitLines();
3640 if (descriptionFirstLines.length == 0) { descriptionFirstLines = new Text[]{ EMPTY }; }
3641 result.add(new Text[] { requiredParameter, EMPTY, EMPTY, label, descriptionFirstLines[0] });
3642 for (int i = 1; i < descriptionFirstLines.length; i++) {
3643 result.add(new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, descriptionFirstLines[i] });
3644 }
3645 for (int i = 1; i < params.description().length; i++) {
3646 final Text[] descriptionNextLines = scheme.ansi().new Text(params.description()[i]).splitLines();
3647 for (final Text line : descriptionNextLines) {
3648 result.add(new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, line });
3649 }
3650 }
3651 return result.toArray(new Text[result.size()][]);
3652 }
3653 }
3654
3655
3656 public interface IParamLabelRenderer {
3657
3658
3659
3660
3661
3662
3663
3664 Text renderParameterLabel(Field field, Ansi ansi, List<IStyle> styles);
3665
3666
3667
3668 String separator();
3669 }
3670
3671
3672
3673
3674
3675
3676 static class DefaultParamLabelRenderer implements IParamLabelRenderer {
3677
3678 public final String separator;
3679
3680 public DefaultParamLabelRenderer(final String separator) {
3681 this.separator = Assert.notNull(separator, "separator");
3682 }
3683 @Override
3684 public String separator() { return separator; }
3685 @Override
3686 public Text renderParameterLabel(final Field field, final Ansi ansi, final List<IStyle> styles) {
3687 final boolean isOptionParameter = field.isAnnotationPresent(Option.class);
3688 final Range arity = isOptionParameter ? Range.optionArity(field) : Range.parameterCapacity(field);
3689 final String split = isOptionParameter ? field.getAnnotation(Option.class).split() : field.getAnnotation(Parameters.class).split();
3690 Text result = ansi.new Text("");
3691 String sep = isOptionParameter ? separator : "";
3692 Text paramName = ansi.apply(renderParameterName(field), styles);
3693 if (!empty(split)) { paramName = paramName.append("[" + split).append(paramName).append("]..."); }
3694 for (int i = 0; i < arity.min; i++) {
3695 result = result.append(sep).append(paramName);
3696 sep = " ";
3697 }
3698 if (arity.isVariable) {
3699 if (result.length == 0) {
3700 result = result.append(sep + "[").append(paramName).append("]...");
3701 } else if (!result.plainString().endsWith("...")) {
3702 result = result.append("...");
3703 }
3704 } else {
3705 sep = result.length == 0 ? (isOptionParameter ? separator : "") : " ";
3706 for (int i = arity.min; i < arity.max; i++) {
3707 if (sep.trim().length() == 0) {
3708 result = result.append(sep + "[").append(paramName);
3709 } else {
3710 result = result.append("[" + sep).append(paramName);
3711 }
3712 sep = " ";
3713 }
3714 for (int i = arity.min; i < arity.max; i++) { result = result.append("]"); }
3715 }
3716 return result;
3717 }
3718 private static String renderParameterName(final Field field) {
3719 String result = null;
3720 if (field.isAnnotationPresent(Option.class)) {
3721 result = field.getAnnotation(Option.class).paramLabel();
3722 } else if (field.isAnnotationPresent(Parameters.class)) {
3723 result = field.getAnnotation(Parameters.class).paramLabel();
3724 }
3725 if (result != null && result.trim().length() > 0) {
3726 return result.trim();
3727 }
3728 String name = field.getName();
3729 if (Map.class.isAssignableFrom(field.getType())) {
3730 final Class<?>[] paramTypes = getTypeAttribute(field);
3731 if (paramTypes.length < 2 || paramTypes[0] == null || paramTypes[1] == null) {
3732 name = "String=String";
3733 } else { name = paramTypes[0].getSimpleName() + "=" + paramTypes[1].getSimpleName(); }
3734 }
3735 return "<" + name + ">";
3736 }
3737 }
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747 public static class Layout {
3748 protected final ColorScheme colorScheme;
3749 protected final TextTable table;
3750 protected IOptionRenderer optionRenderer;
3751 protected IParameterRenderer parameterRenderer;
3752
3753
3754
3755
3756
3757 public Layout(final ColorScheme colorScheme) { this(colorScheme, new TextTable(colorScheme.ansi())); }
3758
3759
3760
3761
3762
3763
3764 public Layout(final ColorScheme colorScheme, final TextTable textTable) {
3765 this(colorScheme, textTable, new DefaultOptionRenderer(), new DefaultParameterRenderer());
3766 }
3767
3768
3769
3770
3771
3772
3773 public Layout(final ColorScheme colorScheme, final TextTable textTable, final IOptionRenderer optionRenderer, final IParameterRenderer parameterRenderer) {
3774 this.colorScheme = Assert.notNull(colorScheme, "colorScheme");
3775 this.table = Assert.notNull(textTable, "textTable");
3776 this.optionRenderer = Assert.notNull(optionRenderer, "optionRenderer");
3777 this.parameterRenderer = Assert.notNull(parameterRenderer, "parameterRenderer");
3778 }
3779
3780
3781
3782
3783
3784
3785
3786 public void layout(final Field field, final Text[][] cellValues) {
3787 for (final Text[] oneRow : cellValues) {
3788 table.addRowValues(oneRow);
3789 }
3790 }
3791
3792
3793
3794 public void addOptions(final List<Field> fields, final IParamLabelRenderer paramLabelRenderer) {
3795 for (final Field field : fields) {
3796 final Option option = field.getAnnotation(Option.class);
3797 if (!option.hidden()) {
3798 addOption(field, paramLabelRenderer);
3799 }
3800 }
3801 }
3802
3803
3804
3805
3806
3807
3808
3809 public void addOption(final Field field, final IParamLabelRenderer paramLabelRenderer) {
3810 final Option option = field.getAnnotation(Option.class);
3811 final Text[][] values = optionRenderer.render(option, field, paramLabelRenderer, colorScheme);
3812 layout(field, values);
3813 }
3814
3815
3816
3817 public void addPositionalParameters(final List<Field> fields, final IParamLabelRenderer paramLabelRenderer) {
3818 for (final Field field : fields) {
3819 final Parameters parameters = field.getAnnotation(Parameters.class);
3820 if (!parameters.hidden()) {
3821 addPositionalParameter(field, paramLabelRenderer);
3822 }
3823 }
3824 }
3825
3826
3827
3828
3829
3830
3831
3832 public void addPositionalParameter(final Field field, final IParamLabelRenderer paramLabelRenderer) {
3833 final Parameters option = field.getAnnotation(Parameters.class);
3834 final Text[][] values = parameterRenderer.render(option, field, paramLabelRenderer, colorScheme);
3835 layout(field, values);
3836 }
3837
3838 @Override public String toString() {
3839 return table.toString();
3840 }
3841 }
3842
3843 static class ShortestFirst implements Comparator<String> {
3844 @Override
3845 public int compare(final String o1, final String o2) {
3846 return o1.length() - o2.length();
3847 }
3848
3849 public static String[] sort(final String[] names) {
3850 Arrays.sort(names, new ShortestFirst());
3851 return names;
3852 }
3853 }
3854
3855
3856 static class SortByShortestOptionNameAlphabetically implements Comparator<Field> {
3857 @Override
3858 public int compare(final Field f1, final Field f2) {
3859 final Option o1 = f1.getAnnotation(Option.class);
3860 final Option o2 = f2.getAnnotation(Option.class);
3861 if (o1 == null) { return 1; } else if (o2 == null) { return -1; }
3862 final String[] names1 = ShortestFirst.sort(o1.names());
3863 final String[] names2 = ShortestFirst.sort(o2.names());
3864 int result = names1[0].toUpperCase().compareTo(names2[0].toUpperCase());
3865 result = result == 0 ? -names1[0].compareTo(names2[0]) : result;
3866 return o1.help() == o2.help() ? result : o2.help() ? -1 : 1;
3867 }
3868 }
3869
3870 static class SortByOptionArityAndNameAlphabetically extends SortByShortestOptionNameAlphabetically {
3871 @Override
3872 public int compare(final Field f1, final Field f2) {
3873 final Option o1 = f1.getAnnotation(Option.class);
3874 final Option o2 = f2.getAnnotation(Option.class);
3875 final Range arity1 = Range.optionArity(f1);
3876 final Range arity2 = Range.optionArity(f2);
3877 int result = arity1.max - arity2.max;
3878 if (result == 0) {
3879 result = arity1.min - arity2.min;
3880 }
3881 if (result == 0) {
3882 if (isMultiValue(f1) && !isMultiValue(f2)) { result = 1; }
3883 if (!isMultiValue(f1) && isMultiValue(f2)) { result = -1; }
3884 }
3885 return result == 0 ? super.compare(f1, f2) : result;
3886 }
3887 }
3888
3889
3890
3891
3892
3893 public static class TextTable {
3894
3895
3896
3897
3898 public static class Cell {
3899
3900 public final int column;
3901
3902 public final int row;
3903
3904
3905
3906 public Cell(final int column, final int row) { this.column = column; this.row = row; }
3907 }
3908
3909
3910 public final Column[] columns;
3911
3912
3913 protected final List<Text> columnValues = new ArrayList<Text>();
3914
3915
3916 public int indentWrappedLines = 2;
3917
3918 private final Ansi ansi;
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930 public TextTable(final Ansi ansi) {
3931
3932 this(ansi, new Column[] {
3933 new Column(2, 0, TRUNCATE),
3934 new Column(2, 0, TRUNCATE),
3935 new Column(1, 0, TRUNCATE),
3936 new Column(optionsColumnWidth - 2 - 2 - 1 , 1, SPAN),
3937 new Column(usageHelpWidth - optionsColumnWidth, 1, WRAP)
3938 });
3939 }
3940
3941
3942
3943
3944
3945
3946 public TextTable(final Ansi ansi, final int... columnWidths) {
3947 this.ansi = Assert.notNull(ansi, "ansi");
3948 columns = new Column[columnWidths.length];
3949 for (int i = 0; i < columnWidths.length; i++) {
3950 columns[i] = new Column(columnWidths[i], 0, i == columnWidths.length - 1 ? SPAN: WRAP);
3951 }
3952 }
3953
3954
3955
3956 public TextTable(final Ansi ansi, final Column... columns) {
3957 this.ansi = Assert.notNull(ansi, "ansi");
3958 this.columns = Assert.notNull(columns, "columns");
3959 if (columns.length == 0) { throw new IllegalArgumentException("At least one column is required"); }
3960 }
3961
3962
3963
3964
3965
3966 public Text textAt(final int row, final int col) { return columnValues.get(col + (row * columns.length)); }
3967
3968
3969
3970
3971
3972
3973 @Deprecated
3974 public Text cellAt(final int row, final int col) { return textAt(row, col); }
3975
3976
3977
3978 public int rowCount() { return columnValues.size() / columns.length; }
3979
3980
3981 public void addEmptyRow() {
3982 for (int i = 0; i < columns.length; i++) {
3983 columnValues.add(ansi.new Text(columns[i].width));
3984 }
3985 }
3986
3987
3988
3989 public void addRowValues(final String... values) {
3990 final Text[] array = new Text[values.length];
3991 for (int i = 0; i < array.length; i++) {
3992 array[i] = values[i] == null ? Ansi.EMPTY_TEXT : ansi.new Text(values[i]);
3993 }
3994 addRowValues(array);
3995 }
3996
3997
3998
3999
4000
4001
4002
4003 public void addRowValues(final Text... values) {
4004 if (values.length > columns.length) {
4005 throw new IllegalArgumentException(values.length + " values don't fit in " +
4006 columns.length + " columns");
4007 }
4008 addEmptyRow();
4009 for (int col = 0; col < values.length; col++) {
4010 final int row = rowCount() - 1;
4011 final Cell cell = putValue(row, col, values[col]);
4012
4013
4014 if ((cell.row != row || cell.column != col) && col != values.length - 1) {
4015 addEmptyRow();
4016 }
4017 }
4018 }
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031 public Cell putValue(int row, int col, Text value) {
4032 if (row > rowCount() - 1) {
4033 throw new IllegalArgumentException("Cannot write to row " + row + ": rowCount=" + rowCount());
4034 }
4035 if (value == null || value.plain.length() == 0) { return new Cell(col, row); }
4036 final Column column = columns[col];
4037 int indent = column.indent;
4038 switch (column.overflow) {
4039 case TRUNCATE:
4040 copy(value, textAt(row, col), indent);
4041 return new Cell(col, row);
4042 case SPAN:
4043 final int startColumn = col;
4044 do {
4045 final boolean lastColumn = col == columns.length - 1;
4046 final int charsWritten = lastColumn
4047 ? copy(BreakIterator.getLineInstance(), value, textAt(row, col), indent)
4048 : copy(value, textAt(row, col), indent);
4049 value = value.substring(charsWritten);
4050 indent = 0;
4051 if (value.length > 0) {
4052 ++col;
4053 }
4054 if (value.length > 0 && col >= columns.length) {
4055 addEmptyRow();
4056 row++;
4057 col = startColumn;
4058 indent = column.indent + indentWrappedLines;
4059 }
4060 } while (value.length > 0);
4061 return new Cell(col, row);
4062 case WRAP:
4063 final BreakIterator lineBreakIterator = BreakIterator.getLineInstance();
4064 do {
4065 final int charsWritten = copy(lineBreakIterator, value, textAt(row, col), indent);
4066 value = value.substring(charsWritten);
4067 indent = column.indent + indentWrappedLines;
4068 if (value.length > 0) {
4069 ++row;
4070 addEmptyRow();
4071 }
4072 } while (value.length > 0);
4073 return new Cell(col, row);
4074 }
4075 throw new IllegalStateException(column.overflow.toString());
4076 }
4077 private static int length(final Text str) {
4078 return str.length;
4079 }
4080
4081 private int copy(final BreakIterator line, final Text text, final Text columnValue, final int offset) {
4082
4083 line.setText(text.plainString().replace("-", "\u00ff"));
4084 int done = 0;
4085 for (int start = line.first(), end = line.next(); end != BreakIterator.DONE; start = end, end = line.next()) {
4086 final Text word = text.substring(start, end);
4087 if (columnValue.maxLength >= offset + done + length(word)) {
4088 done += copy(word, columnValue, offset + done);
4089 } else {
4090 break;
4091 }
4092 }
4093 if (done == 0 && length(text) > columnValue.maxLength) {
4094
4095 done = copy(text, columnValue, offset);
4096 }
4097 return done;
4098 }
4099 private static int copy(final Text value, final Text destination, final int offset) {
4100 final int length = Math.min(value.length, destination.maxLength - offset);
4101 value.getStyledChars(value.from, length, destination, offset);
4102 return length;
4103 }
4104
4105
4106
4107
4108 public StringBuilder toString(final StringBuilder text) {
4109 final int columnCount = this.columns.length;
4110 final StringBuilder row = new StringBuilder(usageHelpWidth);
4111 for (int i = 0; i < columnValues.size(); i++) {
4112 final Text column = columnValues.get(i);
4113 row.append(column.toString());
4114 row.append(new String(spaces(columns[i % columnCount].width - column.length)));
4115 if (i % columnCount == columnCount - 1) {
4116 int lastChar = row.length() - 1;
4117 while (lastChar >= 0 && row.charAt(lastChar) == ' ') {lastChar--;}
4118 row.setLength(lastChar + 1);
4119 text.append(row.toString()).append(System.getProperty("line.separator"));
4120 row.setLength(0);
4121 }
4122 }
4123
4124 return text;
4125 }
4126 @Override
4127 public String toString() { return toString(new StringBuilder()).toString(); }
4128 }
4129
4130
4131 public static class Column {
4132
4133
4134
4135 public enum Overflow { TRUNCATE, SPAN, WRAP }
4136
4137
4138 public final int width;
4139
4140
4141 public final int indent;
4142
4143
4144 public final Overflow overflow;
4145 public Column(final int width, final int indent, final Overflow overflow) {
4146 this.width = width;
4147 this.indent = indent;
4148 this.overflow = Assert.notNull(overflow, "overflow");
4149 }
4150 }
4151
4152
4153
4154
4155
4156
4157
4158
4159 public static class ColorScheme {
4160 public final List<IStyle> commandStyles = new ArrayList<IStyle>();
4161 public final List<IStyle> optionStyles = new ArrayList<IStyle>();
4162 public final List<IStyle> parameterStyles = new ArrayList<IStyle>();
4163 public final List<IStyle> optionParamStyles = new ArrayList<IStyle>();
4164 private final Ansi ansi;
4165
4166
4167 public ColorScheme() { this(Ansi.AUTO); }
4168
4169
4170
4171
4172 public ColorScheme(final Ansi ansi) {this.ansi = Assert.notNull(ansi, "ansi"); }
4173
4174
4175
4176
4177 public ColorScheme commands(final IStyle... styles) { return addAll(commandStyles, styles); }
4178
4179
4180
4181 public ColorScheme options(final IStyle... styles) { return addAll(optionStyles, styles);}
4182
4183
4184
4185 public ColorScheme parameters(final IStyle... styles) { return addAll(parameterStyles, styles);}
4186
4187
4188
4189 public ColorScheme optionParams(final IStyle... styles) { return addAll(optionParamStyles, styles);}
4190
4191
4192
4193 public Ansi.Text commandText(final String command) { return ansi().apply(command, commandStyles); }
4194
4195
4196
4197 public Ansi.Text optionText(final String option) { return ansi().apply(option, optionStyles); }
4198
4199
4200
4201 public Ansi.Text parameterText(final String parameter) { return ansi().apply(parameter, parameterStyles); }
4202
4203
4204
4205 public Ansi.Text optionParamText(final String optionParam) { return ansi().apply(optionParam, optionParamStyles); }
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216 public ColorScheme applySystemProperties() {
4217 replace(commandStyles, System.getProperty("picocli.color.commands"));
4218 replace(optionStyles, System.getProperty("picocli.color.options"));
4219 replace(parameterStyles, System.getProperty("picocli.color.parameters"));
4220 replace(optionParamStyles, System.getProperty("picocli.color.optionParams"));
4221 return this;
4222 }
4223 private void replace(final List<IStyle> styles, final String property) {
4224 if (property != null) {
4225 styles.clear();
4226 addAll(styles, Style.parse(property));
4227 }
4228 }
4229 private ColorScheme addAll(final List<IStyle> styles, final IStyle... add) {
4230 styles.addAll(Arrays.asList(add));
4231 return this;
4232 }
4233
4234 public Ansi ansi() {
4235 return ansi;
4236 }
4237 }
4238
4239
4240
4241
4242
4243
4244 public static ColorScheme defaultColorScheme(final Ansi ansi) {
4245 return new ColorScheme(ansi)
4246 .commands(Style.bold)
4247 .options(Style.fg_yellow)
4248 .parameters(Style.fg_yellow)
4249 .optionParams(Style.italic);
4250 }
4251
4252
4253 public enum Ansi {
4254
4255
4256 AUTO,
4257
4258 ON,
4259
4260 OFF;
4261 static Text EMPTY_TEXT = OFF.new Text(0);
4262 static final boolean isWindows = System.getProperty("os.name").startsWith("Windows");
4263 static final boolean isXterm = System.getenv("TERM") != null && System.getenv("TERM").startsWith("xterm");
4264 static final boolean ISATTY = calcTTY();
4265
4266
4267 static final boolean calcTTY() {
4268 if (isWindows && isXterm) { return true; }
4269 try { return System.class.getDeclaredMethod("console").invoke(null) != null; }
4270 catch (final Throwable reflectionFailed) { return true; }
4271 }
4272 private static boolean ansiPossible() { return ISATTY && (!isWindows || isXterm); }
4273
4274
4275
4276
4277 public boolean enabled() {
4278 if (this == ON) { return true; }
4279 if (this == OFF) { return false; }
4280 return (System.getProperty("picocli.ansi") == null ? ansiPossible() : Boolean.getBoolean("picocli.ansi"));
4281 }
4282
4283
4284 public interface IStyle {
4285
4286
4287 String CSI = "\u001B[";
4288
4289
4290
4291 String on();
4292
4293
4294
4295 String off();
4296 }
4297
4298
4299
4300
4301
4302
4303 public enum Style implements IStyle {
4304 reset(0, 0), bold(1, 21), faint(2, 22), italic(3, 23), underline(4, 24), blink(5, 25), reverse(7, 27),
4305 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),
4306 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),
4307 ;
4308 private final int startCode;
4309 private final int endCode;
4310
4311 Style(final int startCode, final int endCode) {this.startCode = startCode; this.endCode = endCode; }
4312 @Override
4313 public String on() { return CSI + startCode + "m"; }
4314 @Override
4315 public String off() { return CSI + endCode + "m"; }
4316
4317
4318
4319
4320 public static String on(final IStyle... styles) {
4321 final StringBuilder result = new StringBuilder();
4322 for (final IStyle style : styles) {
4323 result.append(style.on());
4324 }
4325 return result.toString();
4326 }
4327
4328
4329
4330 public static String off(final IStyle... styles) {
4331 final StringBuilder result = new StringBuilder();
4332 for (final IStyle style : styles) {
4333 result.append(style.off());
4334 }
4335 return result.toString();
4336 }
4337
4338
4339
4340
4341
4342
4343
4344 public static IStyle fg(final String str) {
4345 try { return Style.valueOf(str.toLowerCase(ENGLISH)); } catch (final Exception ignored) {}
4346 try { return Style.valueOf("fg_" + str.toLowerCase(ENGLISH)); } catch (final Exception ignored) {}
4347 return new Palette256Color(true, str);
4348 }
4349
4350
4351
4352
4353
4354
4355
4356 public static IStyle bg(final String str) {
4357 try { return Style.valueOf(str.toLowerCase(ENGLISH)); } catch (final Exception ignored) {}
4358 try { return Style.valueOf("bg_" + str.toLowerCase(ENGLISH)); } catch (final Exception ignored) {}
4359 return new Palette256Color(false, str);
4360 }
4361
4362
4363
4364
4365
4366
4367 public static IStyle[] parse(final String commaSeparatedCodes) {
4368 final String[] codes = commaSeparatedCodes.split(",");
4369 final IStyle[] styles = new IStyle[codes.length];
4370 for(int i = 0; i < codes.length; ++i) {
4371 if (codes[i].toLowerCase(ENGLISH).startsWith("fg(")) {
4372 final int end = codes[i].indexOf(')');
4373 styles[i] = Style.fg(codes[i].substring(3, end < 0 ? codes[i].length() : end));
4374 } else if (codes[i].toLowerCase(ENGLISH).startsWith("bg(")) {
4375 final int end = codes[i].indexOf(')');
4376 styles[i] = Style.bg(codes[i].substring(3, end < 0 ? codes[i].length() : end));
4377 } else {
4378 styles[i] = Style.fg(codes[i]);
4379 }
4380 }
4381 return styles;
4382 }
4383 }
4384
4385
4386
4387 static class Palette256Color implements IStyle {
4388 private final int fgbg;
4389 private final int color;
4390
4391 Palette256Color(final boolean foreground, final String color) {
4392 this.fgbg = foreground ? 38 : 48;
4393 final String[] rgb = color.split(";");
4394 if (rgb.length == 3) {
4395 this.color = 16 + 36 * Integer.decode(rgb[0]) + 6 * Integer.decode(rgb[1]) + Integer.decode(rgb[2]);
4396 } else {
4397 this.color = Integer.decode(color);
4398 }
4399 }
4400 @Override
4401 public String on() { return String.format(CSI + "%d;5;%dm", fgbg, color); }
4402 @Override
4403 public String off() { return CSI + (fgbg + 1) + "m"; }
4404 }
4405 private static class StyledSection {
4406 int startIndex, length;
4407 String startStyles, endStyles;
4408 StyledSection(final int start, final int len, final String style1, final String style2) {
4409 startIndex = start; length = len; startStyles = style1; endStyles = style2;
4410 }
4411 StyledSection withStartIndex(final int newStart) {
4412 return new StyledSection(newStart, length, startStyles, endStyles);
4413 }
4414 }
4415
4416
4417
4418
4419
4420
4421
4422
4423 public Text apply(final String plainText, final List<IStyle> styles) {
4424 if (plainText.length() == 0) { return new Text(0); }
4425 final Text result = new Text(plainText.length());
4426 final IStyle[] all = styles.toArray(new IStyle[styles.size()]);
4427 result.sections.add(new StyledSection(
4428 0, plainText.length(), Style.on(all), Style.off(reverse(all)) + Style.reset.off()));
4429 result.plain.append(plainText);
4430 result.length = result.plain.length();
4431 return result;
4432 }
4433
4434 private static <T> T[] reverse(final T[] all) {
4435 for (int i = 0; i < all.length / 2; i++) {
4436 final T temp = all[i];
4437 all[i] = all[all.length - i - 1];
4438 all[all.length - i - 1] = temp;
4439 }
4440 return all;
4441 }
4442
4443
4444
4445
4446
4447
4448 public class Text implements Cloneable {
4449 private final int maxLength;
4450 private int from;
4451 private int length;
4452 private StringBuilder plain = new StringBuilder();
4453 private List<StyledSection> sections = new ArrayList<StyledSection>();
4454
4455
4456
4457 public Text(final int maxLength) { this.maxLength = maxLength; }
4458
4459
4460
4461
4462
4463
4464 public Text(final String input) {
4465 maxLength = -1;
4466 plain.setLength(0);
4467 int i = 0;
4468
4469 while (true) {
4470 int j = input.indexOf("@|", i);
4471 if (j == -1) {
4472 if (i == 0) {
4473 plain.append(input);
4474 length = plain.length();
4475 return;
4476 }
4477 plain.append(input.substring(i, input.length()));
4478 length = plain.length();
4479 return;
4480 }
4481 plain.append(input.substring(i, j));
4482 final int k = input.indexOf("|@", j);
4483 if (k == -1) {
4484 plain.append(input);
4485 length = plain.length();
4486 return;
4487 }
4488
4489 j += 2;
4490 final String spec = input.substring(j, k);
4491 final String[] items = spec.split(" ", 2);
4492 if (items.length == 1) {
4493 plain.append(input);
4494 length = plain.length();
4495 return;
4496 }
4497
4498 final IStyle[] styles = Style.parse(items[0]);
4499 addStyledSection(plain.length(), items[1].length(),
4500 Style.on(styles), Style.off(reverse(styles)) + Style.reset.off());
4501 plain.append(items[1]);
4502 i = k + 2;
4503 }
4504 }
4505 private void addStyledSection(final int start, final int length, final String startStyle, final String endStyle) {
4506 sections.add(new StyledSection(start, length, startStyle, endStyle));
4507 }
4508 @Override
4509 public Object clone() {
4510 try { return super.clone(); } catch (final CloneNotSupportedException e) { throw new IllegalStateException(e); }
4511 }
4512
4513 public Text[] splitLines() {
4514 final List<Text> result = new ArrayList<Text>();
4515 boolean trailingEmptyString = false;
4516 int start = 0, end = 0;
4517 for (int i = 0; i < plain.length(); i++, end = i) {
4518 final char c = plain.charAt(i);
4519 boolean eol = c == '\n';
4520 eol |= (c == '\r' && i + 1 < plain.length() && plain.charAt(i + 1) == '\n' && ++i > 0);
4521 eol |= c == '\r';
4522 if (eol) {
4523 result.add(this.substring(start, end));
4524 trailingEmptyString = i == plain.length() - 1;
4525 start = i + 1;
4526 }
4527 }
4528 if (start < plain.length() || trailingEmptyString) {
4529 result.add(this.substring(start, plain.length()));
4530 }
4531 return result.toArray(new Text[result.size()]);
4532 }
4533
4534
4535
4536
4537 public Text substring(final int start) {
4538 return substring(start, length);
4539 }
4540
4541
4542
4543
4544
4545 public Text substring(final int start, final int end) {
4546 final Text result = (Text) clone();
4547 result.from = from + start;
4548 result.length = end - start;
4549 return result;
4550 }
4551
4552
4553
4554 public Text append(final String string) {
4555 return append(new Text(string));
4556 }
4557
4558
4559
4560
4561 public Text append(final Text other) {
4562 final Text result = (Text) clone();
4563 result.plain = new StringBuilder(plain.toString().substring(from, from + length));
4564 result.from = 0;
4565 result.sections = new ArrayList<StyledSection>();
4566 for (final StyledSection section : sections) {
4567 result.sections.add(section.withStartIndex(section.startIndex - from));
4568 }
4569 result.plain.append(other.plain.toString().substring(other.from, other.from + other.length));
4570 for (final StyledSection section : other.sections) {
4571 final int index = result.length + section.startIndex - other.from;
4572 result.sections.add(section.withStartIndex(index));
4573 }
4574 result.length = result.plain.length();
4575 return result;
4576 }
4577
4578
4579
4580
4581
4582
4583
4584
4585 public void getStyledChars(final int from, final int length, final Text destination, final int offset) {
4586 if (destination.length < offset) {
4587 for (int i = destination.length; i < offset; i++) {
4588 destination.plain.append(' ');
4589 }
4590 destination.length = offset;
4591 }
4592 for (final StyledSection section : sections) {
4593 destination.sections.add(section.withStartIndex(section.startIndex - from + destination.length));
4594 }
4595 destination.plain.append(plain.toString().substring(from, from + length));
4596 destination.length = destination.plain.length();
4597 }
4598
4599
4600 public String plainString() { return plain.toString().substring(from, from + length); }
4601
4602 @Override
4603 public boolean equals(final Object obj) { return toString().equals(String.valueOf(obj)); }
4604 @Override
4605 public int hashCode() { return toString().hashCode(); }
4606
4607
4608
4609
4610 @Override
4611 public String toString() {
4612 if (!Ansi.this.enabled()) {
4613 return plain.toString().substring(from, from + length);
4614 }
4615 if (length == 0) { return ""; }
4616 final StringBuilder sb = new StringBuilder(plain.length() + 20 * sections.size());
4617 StyledSection current = null;
4618 final int end = Math.min(from + length, plain.length());
4619 for (int i = from; i < end; i++) {
4620 final StyledSection section = findSectionContaining(i);
4621 if (section != current) {
4622 if (current != null) { sb.append(current.endStyles); }
4623 if (section != null) { sb.append(section.startStyles); }
4624 current = section;
4625 }
4626 sb.append(plain.charAt(i));
4627 }
4628 if (current != null) { sb.append(current.endStyles); }
4629 return sb.toString();
4630 }
4631
4632 private StyledSection findSectionContaining(final int index) {
4633 for (final StyledSection section : sections) {
4634 if (index >= section.startIndex && index < section.startIndex + section.length) {
4635 return section;
4636 }
4637 }
4638 return null;
4639 }
4640 }
4641 }
4642 }
4643
4644
4645
4646
4647 private static final class Assert {
4648
4649
4650
4651
4652
4653
4654
4655 static <T> T notNull(final T object, final String description) {
4656 if (object == null) {
4657 throw new NullPointerException(description);
4658 }
4659 return object;
4660 }
4661 private Assert() {}
4662 }
4663 private enum TraceLevel { OFF, WARN, INFO, DEBUG;
4664 public boolean isEnabled(final TraceLevel other) { return ordinal() >= other.ordinal(); }
4665 private void print(final Tracer tracer, final String msg, final Object... params) {
4666 if (tracer.level.isEnabled(this)) { tracer.stream.printf(prefix(msg), params); }
4667 }
4668 private String prefix(final String msg) { return "[picocli " + this + "] " + msg; }
4669 static TraceLevel lookup(final String key) { return key == null ? WARN : empty(key) || "true".equalsIgnoreCase(key) ? INFO : valueOf(key); }
4670 }
4671 private static class Tracer {
4672 TraceLevel level = TraceLevel.lookup(System.getProperty("picocli.trace"));
4673 PrintStream stream = System.err;
4674 void warn (final String msg, final Object... params) { TraceLevel.WARN.print(this, msg, params); }
4675 void info (final String msg, final Object... params) { TraceLevel.INFO.print(this, msg, params); }
4676 void debug(final String msg, final Object... params) { TraceLevel.DEBUG.print(this, msg, params); }
4677 boolean isWarn() { return level.isEnabled(TraceLevel.WARN); }
4678 boolean isInfo() { return level.isEnabled(TraceLevel.INFO); }
4679 boolean isDebug() { return level.isEnabled(TraceLevel.DEBUG); }
4680 }
4681
4682
4683 public static class PicocliException extends RuntimeException {
4684 private static final long serialVersionUID = -2574128880125050818L;
4685 public PicocliException(final String msg) { super(msg); }
4686 public PicocliException(final String msg, final Exception ex) { super(msg, ex); }
4687 }
4688
4689
4690 public static class InitializationException extends PicocliException {
4691 private static final long serialVersionUID = 8423014001666638895L;
4692 public InitializationException(final String msg) { super(msg); }
4693 public InitializationException(final String msg, final Exception ex) { super(msg, ex); }
4694 }
4695
4696
4697 public static class ExecutionException extends PicocliException {
4698 private static final long serialVersionUID = 7764539594267007998L;
4699 private final CommandLine commandLine;
4700 public ExecutionException(final CommandLine commandLine, final String msg) {
4701 super(msg);
4702 this.commandLine = Assert.notNull(commandLine, "commandLine");
4703 }
4704 public ExecutionException(final CommandLine commandLine, final String msg, final Exception ex) {
4705 super(msg, ex);
4706 this.commandLine = Assert.notNull(commandLine, "commandLine");
4707 }
4708
4709
4710
4711 public CommandLine getCommandLine() { return commandLine; }
4712 }
4713
4714
4715 public static class TypeConversionException extends PicocliException {
4716 private static final long serialVersionUID = 4251973913816346114L;
4717 public TypeConversionException(final String msg) { super(msg); }
4718 }
4719
4720 public static class ParameterException extends PicocliException {
4721 private static final long serialVersionUID = 1477112829129763139L;
4722 private final CommandLine commandLine;
4723
4724
4725
4726
4727
4728 public ParameterException(final CommandLine commandLine, final String msg) {
4729 super(msg);
4730 this.commandLine = Assert.notNull(commandLine, "commandLine");
4731 }
4732
4733
4734
4735
4736
4737 public ParameterException(final CommandLine commandLine, final String msg, final Exception ex) {
4738 super(msg, ex);
4739 this.commandLine = Assert.notNull(commandLine, "commandLine");
4740 }
4741
4742
4743
4744
4745
4746 public CommandLine getCommandLine() { return commandLine; }
4747
4748 private static ParameterException create(final CommandLine cmd, final Exception ex, final String arg, final int i, final String[] args) {
4749 final String msg = ex.getClass().getSimpleName() + ": " + ex.getLocalizedMessage()
4750 + " while processing argument at or before arg[" + i + "] '" + arg + "' in " + Arrays.toString(args) + ": " + ex.toString();
4751 return new ParameterException(cmd, msg, ex);
4752 }
4753 }
4754
4755
4756
4757 public static class MissingParameterException extends ParameterException {
4758 private static final long serialVersionUID = 5075678535706338753L;
4759 public MissingParameterException(final CommandLine commandLine, final String msg) {
4760 super(commandLine, msg);
4761 }
4762
4763 private static MissingParameterException create(final CommandLine cmd, final Collection<Field> missing, final String separator) {
4764 if (missing.size() == 1) {
4765 return new MissingParameterException(cmd, "Missing required option '"
4766 + describe(missing.iterator().next(), separator) + "'");
4767 }
4768 final List<String> names = new ArrayList<String>(missing.size());
4769 for (final Field field : missing) {
4770 names.add(describe(field, separator));
4771 }
4772 return new MissingParameterException(cmd, "Missing required options " + names.toString());
4773 }
4774 private static String describe(final Field field, final String separator) {
4775 final String prefix = (field.isAnnotationPresent(Option.class))
4776 ? field.getAnnotation(Option.class).names()[0] + separator
4777 : "params[" + field.getAnnotation(Parameters.class).index() + "]" + separator;
4778 return prefix + Help.DefaultParamLabelRenderer.renderParameterName(field);
4779 }
4780 }
4781
4782
4783
4784
4785 public static class DuplicateOptionAnnotationsException extends InitializationException {
4786 private static final long serialVersionUID = -3355128012575075641L;
4787 public DuplicateOptionAnnotationsException(final String msg) { super(msg); }
4788
4789 private static DuplicateOptionAnnotationsException create(final String name, final Field field1, final Field field2) {
4790 return new DuplicateOptionAnnotationsException("Option name '" + name + "' is used by both " +
4791 field1.getDeclaringClass().getName() + "." + field1.getName() + " and " +
4792 field2.getDeclaringClass().getName() + "." + field2.getName());
4793 }
4794 }
4795
4796 public static class ParameterIndexGapException extends InitializationException {
4797 private static final long serialVersionUID = -1520981133257618319L;
4798 public ParameterIndexGapException(final String msg) { super(msg); }
4799 }
4800
4801
4802 public static class UnmatchedArgumentException extends ParameterException {
4803 private static final long serialVersionUID = -8700426380701452440L;
4804 public UnmatchedArgumentException(final CommandLine commandLine, final String msg) { super(commandLine, msg); }
4805 public UnmatchedArgumentException(final CommandLine commandLine, final Stack<String> args) { this(commandLine, new ArrayList<String>(reverse(args))); }
4806 public UnmatchedArgumentException(final CommandLine commandLine, final List<String> args) { this(commandLine, "Unmatched argument" + (args.size() == 1 ? " " : "s ") + args); }
4807 }
4808
4809 public static class MaxValuesforFieldExceededException extends ParameterException {
4810 private static final long serialVersionUID = 6536145439570100641L;
4811 public MaxValuesforFieldExceededException(final CommandLine commandLine, final String msg) { super(commandLine, msg); }
4812 }
4813
4814 public static class OverwrittenOptionException extends ParameterException {
4815 private static final long serialVersionUID = 1338029208271055776L;
4816 public OverwrittenOptionException(final CommandLine commandLine, final String msg) { super(commandLine, msg); }
4817 }
4818
4819
4820
4821
4822 public static class MissingTypeConverterException extends ParameterException {
4823 private static final long serialVersionUID = -6050931703233083760L;
4824 public MissingTypeConverterException(final CommandLine commandLine, final String msg) { super(commandLine, msg); }
4825 }
4826 }