JAL-629 Added --structureimage code and formatting args/subvals. Added tests for...
[jalview.git] / src / jalview / bin / argparser / Arg.java
1 package jalview.bin.argparser;
2
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.Collections;
6 import java.util.Comparator;
7 import java.util.EnumSet;
8 import java.util.Iterator;
9 import java.util.List;
10 import java.util.Locale;
11 import java.util.stream.Collectors;
12
13 import jalview.bin.argparser.Arg.Opt;
14 import jalview.util.ChannelProperties;
15 import jalview.util.Platform;
16
17 public enum Arg
18 {
19
20   // Initialising arguments (BOOTSTRAP)
21   HELP(Type.HELP, "h", "Display basic help", Opt.UNARY, Opt.BOOTSTRAP,
22           Opt.HASTYPE, Opt.MULTI),
23   /*
24    * Other --help-type Args will be added by the static block.
25    */
26   VERSION(Type.CONFIG, "v",
27           "Display the version of "
28                   + ChannelProperties.getProperty("app_name"),
29           Opt.UNARY, Opt.BOOTSTRAP),
30   HEADLESS(Type.CONFIG,
31           "Run Jalview in headless mode. No GUI interface will be created and Jalview will quit after all arguments have been processed.",
32           Opt.UNARY, Opt.BOOTSTRAP),
33   JABAWS(Type.CONFIG, "Set a different URL to connect to a JABAWS server.",
34           Opt.STRING, Opt.BOOTSTRAP),
35   NEWS(Type.CONFIG, "Show (or don't show) the news feed.", true,
36           Opt.BOOLEAN, Opt.BOOTSTRAP),
37   SPLASH(Type.CONFIG,
38           "Show (or don't show) the About Jalview splash screen.", true,
39           Opt.BOOLEAN, Opt.BOOTSTRAP),
40   QUESTIONNAIRE(Type.CONFIG,
41           "Show (or don't show) the questionnaire if one is available.",
42           true, Opt.BOOLEAN, Opt.BOOTSTRAP),
43   USAGESTATS(Type.CONFIG,
44           "Send (or don't send) initial launch usage stats.", true,
45           Opt.BOOLEAN, Opt.BOOTSTRAP),
46   WEBSERVICEDISCOVERY(Type.CONFIG,
47           "Attempt (or don't attempt) to connect to JABAWS web services.",
48           true, Opt.BOOLEAN, Opt.BOOTSTRAP),
49   PROPS(Type.CONFIG,
50           "Use a file as the preferences file instead of the usual ~/"
51                   + ChannelProperties.getProperty("preferences.filename")
52                   + " file.",
53           Opt.STRING, Opt.BOOTSTRAP),
54   DEBUG(Type.CONFIG, "d", "Start Jalview in debug log level.", Opt.BOOLEAN,
55           Opt.BOOTSTRAP),
56   TRACE(Type.CONFIG, "Start Jalview in trace log level.", Opt.BOOLEAN,
57           Opt.BOOTSTRAP, Opt.SECRET),
58   QUIET(Type.CONFIG, "q",
59           "Stop all output to STDOUT (after the Java Virtual Machine has started). Use ‑‑quiet a second time to stop all output to STDERR.",
60           Opt.UNARY, Opt.MULTI, Opt.BOOTSTRAP),
61   INITSUBSTITUTIONS(Type.CONFIG,
62           "Set ‑‑substitutions to be initially enabled (or initially disabled).",
63           true, Opt.BOOLEAN, Opt.BOOTSTRAP, Opt.NOACTION, Opt.SECRET),
64
65   // Opening an alignment
66   OPEN(Type.OPENING,
67           "Opens one or more alignment files or URLs in new alignment windows.",
68           Opt.STRING, Opt.LINKED, Opt.INCREMENTDEFAULTCOUNTER, Opt.MULTI,
69           Opt.GLOB, Opt.ALLOWSUBSTITUTIONS, Opt.INPUT, Opt.STORED,
70           Opt.PRIMARY),
71   APPEND(Type.OPENING,
72           "Appends one or more alignment files or URLs to the open alignment window (or opens a new alignment if none already open).",
73           Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.GLOB,
74           Opt.ALLOWSUBSTITUTIONS, Opt.INPUT, Opt.PRIMARY),
75   TITLE(Type.OPENING,
76           "Specifies the title for the open alignment window as string.",
77           Opt.STRING, Opt.LINKED),
78   COLOUR(Type.OPENING, "color", // being a bit soft on the Americans!
79           "Applies the colour scheme to the open alignment window. Valid values are:\n"
80                   + "clustal,\n" + "blosum62,\n" + "pc-identity,\n"
81                   + "zappo,\n" + "taylor,\n" + "gecos-flower,\n"
82                   + "gecos-blossom,\n" + "gecos-sunset,\n"
83                   + "gecos-ocean,\n" + "hydrophobic,\n"
84                   + "helix-propensity,\n" + "strand-propensity,\n"
85                   + "turn-propensity,\n" + "buried-index,\n"
86                   + "nucleotide,\n" + "nucleotide-ambiguity,\n"
87                   + "purine-pyrimidine,\n" + "rna-helices,\n"
88                   + "t-coffee-scores,\n" + "sequence-id.",
89           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
90   FEATURES(Type.OPENING, "Add a feature file or URL to the open alignment.",
91           Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.ALLOWSUBSTITUTIONS),
92   TREE(Type.OPENING, "Add a tree file or URL to the open alignment.",
93           Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.ALLOWSUBSTITUTIONS),
94   SORTBYTREE(Type.OPENING,
95           "Enforces sorting (or not sorting) the open alignment in the order of an attached phylogenetic tree.",
96           true, Opt.LINKED, Opt.BOOLEAN, Opt.ALLOWALL),
97   ANNOTATIONS(Type.OPENING,
98           "Add an annotations file or URL to the open alignment.",
99           Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.ALLOWSUBSTITUTIONS),
100   SHOWANNOTATIONS(Type.OPENING,
101           "Enforces showing (or not showing) alignment annotations.",
102           Opt.BOOLEAN, Opt.LINKED, Opt.ALLOWALL),
103   WRAP(Type.OPENING,
104           "Enforces wrapped (or not wrapped) alignment formatting.",
105           Opt.BOOLEAN, Opt.LINKED, Opt.ALLOWALL),
106   NOSTRUCTURE(Type.OPENING,
107           "Do not open or process any 3D structure in the ‑‑open or ‑‑append files.",
108           Opt.UNARY, Opt.LINKED, Opt.ALLOWALL),
109
110   // Adding a 3D structure
111   STRUCTURE(Type.STRUCTURE,
112           "Load a structure file or URL associated with a sequence in the open alignment.\n"
113                   + "The sequence to be associated with can be specified with a following --seqid argument, or the subval modifier seqid=ID can be used. A subval INDEX can also be used to specify the INDEX-th sequence in the open alignment.",
114           Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.ALLOWSUBSTITUTIONS,
115           Opt.PRIMARY),
116   SEQID(Type.STRUCTURE,
117           "Specify the sequence name for the preceding --structure to be associated with.",
118           Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.ALLOWSUBSTITUTIONS),
119   PAEMATRIX(Type.STRUCTURE,
120           "Add a PAE json matrix file to the preceding --structure.",
121           Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.ALLOWSUBSTITUTIONS),
122   TEMPFAC(Type.STRUCTURE,
123           "Set the type of temperature factor. Possible values are:\n"
124                   + "default,\n" + "plddt.",
125           Opt.STRING, Opt.LINKED),
126   STRUCTUREVIEWER(Type.STRUCTURE,
127           "Set the structure viewer to use to open the 3D structure file specified in previous --structure to name. Possible values of name are:\n"
128                   + "none,\n" + "jmol,\n" + "chimera,\n" + "chimerax,\n"
129                   + "pymol.",
130           Opt.STRING, Opt.LINKED, Opt.MULTI),
131   NOTEMPFAC(Type.STRUCTURE,
132           "Do not show the temperature factor annotation for the preceding --structure.",
133           Opt.UNARY, Opt.LINKED, Opt.ALLOWALL, Opt.SECRET), // keep this secret
134                                                             // until it works!
135   SHOWSSANNOTATIONS(Type.STRUCTURE, null, Opt.BOOLEAN, Opt.LINKED,
136           Opt.ALLOWALL),
137
138   // Outputting files
139   IMAGE(Type.IMAGE,
140           "Output an image of the open alignment window. Format is specified by the subval modifier, a following --type argument or guessed from the file extension. Valid formats/extensions are:\n"
141                   + "svg,\n" + "png,\n" + "eps,\n" + "html,\n" + "biojs.",
142           Opt.STRING, Opt.LINKED, Opt.ALLOWSUBSTITUTIONS, Opt.ALLOWALL,
143           Opt.REQUIREINPUT, Opt.OUTPUT, Opt.PRIMARY),
144   TYPE(Type.IMAGE,
145           "Set the image format for the preceding --image. Valid values are:\n"
146                   + "svg,\n" + "png,\n" + "eps,\n" + "html,\n" + "biojs.",
147           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
148   TEXTRENDERER(Type.IMAGE,
149           "Sets whether text in a vector image format (SVG, HTML, EPS) should be rendered as text or vector line-art. Possible values are:\n"
150                   + "text,\n" + "lineart.",
151           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
152   SCALE(Type.IMAGE,
153           "Sets a scaling for bitmap image format (PNG). Should be given as a floating point number. If used in conjunction with --width and --height then the smallest scaling will be used (scale, width and height provide bounds for the image).",
154           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
155   WIDTH(Type.IMAGE,
156           "Sets a width for bitmap image format (PNG) with the height maintaining the aspect ratio. Should be given as a positive integer. If used in conjunction with --scale and --height then the smallest scaling will be used (scale, width and height provide bounds for the image).",
157           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
158   HEIGHT(Type.IMAGE,
159           "Sets a height for bitmap image format (PNG) with the width maintaining the aspect ratio. Should be given as a positive integer. If used in conjunction with --scale and --width then the smallest scaling will be used (scale, width and height provide bounds for the image).",
160           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
161   STRUCTUREIMAGE(Type.STRUCTUREIMAGE,
162           "Export an image of a 3D structure opened in JMOL", Opt.STRING,
163           Opt.LINKED, Opt.MULTI),
164   STRUCTUREIMAGETYPE(Type.STRUCTUREIMAGE,
165           "Set the structure image format for the preceding --structureimage. Valid values are:\n"
166                   + "svg,\n" + "png,\n" + "eps,\n" + "html,\n" + "biojs.",
167           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
168   STRUCTUREIMAGETEXTRENDERER(Type.STRUCTUREIMAGE,
169           "Sets whether text in a vector structure image format (SVG, EPS) should be rendered as text or vector line-art. Possible values are:\n"
170                   + "text,\n" + "lineart.",
171           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
172   STRUCTUREIMAGESCALE(Type.STRUCTUREIMAGE,
173           "Sets a scaling for bitmap structure image format (PNG). Should be given as a floating point number. If used in conjunction with --structureimagewidth and --structureimageheight then the smallest scaling will be used (structureimagescale, structureimagewidth and structureimageheight provide bounds for the structure image).",
174           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
175   STRUCTUREIMAGEWIDTH(Type.STRUCTUREIMAGE,
176           "Sets a width for bitmap structure image format (PNG) with the height maintaining the aspect ratio. Should be given as a positive integer. If used in conjunction with --structureimagescale and --structureimageheight then the smallest scaling will be used (structureimagescale, structureimagewidth and structureimageheight provide bounds for the structure image).",
177           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
178   STRUCTUREIMAGEHEIGHT(Type.STRUCTUREIMAGE,
179           "Sets a height for bitmap structure image format (PNG) with the width maintaining the aspect ratio. Should be given as a positive integer. If used in conjunction with --structureimagescale and --structureimagewidth then the smallest scaling will be used (structureimagescale, structureimagewidth and structureimageheight provide bounds for the structure image).",
180           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
181
182   OUTPUT(Type.OUTPUT,
183           "Export the open alignment to file filename. The format name is specified by the subval modifier format=name, a following --format name argument or guessed from the file extension. Valid format names (and file extensions) are:\n"
184                   + "fasta (fa, fasta, mfa, fastq),\n" + "pfam (pfam),\n"
185                   + "stockholm (sto, stk),\n" + "pir (pir),\n"
186                   + "blc (blc),\n" + "amsa (amsa),\n" + "json (json),\n"
187                   + "pileup (pileup),\n" + "msf (msf),\n"
188                   + "clustal (aln),\n" + "phylip (phy),\n"
189                   + "jalview (jvp, jar).",
190           Opt.STRING, Opt.LINKED, Opt.ALLOWSUBSTITUTIONS, Opt.ALLOWALL,
191           Opt.REQUIREINPUT, Opt.OUTPUT, Opt.PRIMARY),
192   FORMAT(Type.OUTPUT,
193           "Sets the format for the preceding --output file. Valid formats are:\n"
194                   + "fasta,\n" + "pfam,\n" + "stockholm,\n" + "pir,\n"
195                   + "blc,\n" + "amsa,\n" + "json,\n" + "pileup,\n"
196                   + "msf,\n" + "clustal,\n" + "phylip,\n" + "jalview.",
197           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
198   GROOVY(Type.PROCESS,
199           "Process a groovy script in the file for the open alignment.",
200           Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.ALLOWSUBSTITUTIONS,
201           Opt.ALLOWALL),
202   BACKUPS(Type.OUTPUT,
203           "Enable (or disable) writing backup files when saving an ‑‑output file. This applies to the current open alignment.  To apply to all ‑‑output and ‑‑image files, use after ‑‑all.",
204           true, Opt.BOOLEAN, Opt.LINKED, Opt.ALLOWALL),
205   OVERWRITE(Type.OUTPUT,
206           "Enable (or disable) overwriting of output files without backups enabled. This applies to the current open alignment.  To apply to all ‑‑output and ‑‑image files, use after ‑‑all.",
207           Opt.BOOLEAN, Opt.LINKED, Opt.ALLOWALL),
208   CLOSE(Type.OPENING,
209           "Close the current open alignment window. This occurs after other output arguments. This applies to the current open alignment.  To apply to all ‑‑output and ‑‑image files, use after ‑‑all.",
210           Opt.UNARY, Opt.LINKED, Opt.ALLOWALL),
211
212   // controlling flow of arguments
213   NEW(Type.FLOW,
214           "Move on to a new alignment window. This will ensure --append will start a new alignment window and other linked arguments will apply to the new alignment window.",
215           Opt.UNARY, Opt.MULTI, Opt.NOACTION, Opt.INCREMENTDEFAULTCOUNTER),
216   SUBSTITUTIONS(Type.FLOW,
217           "The following argument values allow (or don't allow) subsituting filename parts. This is initially true. Valid substitutions are:\n"
218                   + "{basename} - the filename-without-extension of the currently --opened file (or first --appended file),\n"
219                   + "{dirname} - the directory (folder) name of the currently --opened file (or first --appended file),\n"
220                   + "{argfilebasename} - the filename-without-extension of the current --argfile,\n"
221                   + "{argfiledirname} - the directory (folder) name of the current --argfile,\n"
222                   + "{n} - the value of the index counter (starting at 0).\n"
223                   + "{++n} - increase and substitute the value of the index counter,\n"
224                   + "{} - the value of the current alignment window default index.",
225           true, Opt.BOOLEAN, Opt.MULTI, Opt.NOACTION),
226   ARGFILE(Type.FLOW,
227           "Open one or more files filename and read, line-by-line, as arguments to Jalview.\n"
228                   + "Values in an argfile should be given with an equals sign (\"=\") separator with no spaces.\n"
229                   + "Note that if you use one or more --argfile arguments then all other non-initialising arguments will be ignored.",
230           Opt.STRING, Opt.MULTI, Opt.BOOTSTRAP, Opt.GLOB,
231           Opt.ALLOWSUBSTITUTIONS),
232   NPP(Type.FLOW, "n++",
233           "Increase the index counter used in argument value substitutions.",
234           Opt.UNARY, Opt.MULTI, Opt.NOACTION),
235   ALL(Type.FLOW,
236           "Apply the following output arguments to all sets of linked arguments.",
237           Opt.BOOLEAN, Opt.MULTI, Opt.NOACTION),
238   OPENED(Type.FLOW,
239           "Apply the following output arguments to all of the last --open'ed set of linked arguments.",
240           Opt.BOOLEAN, Opt.MULTI, Opt.NOACTION),
241   QUIT(Type.FLOW,
242           "After all files have been opened, appended and output, quit Jalview. In ‑‑headless mode this already happens.",
243           Opt.UNARY),
244   NOQUIT(Type.FLOW,
245           "Secret arg to not quit after --headless mode for tests",
246           Opt.UNARY, Opt.SECRET),
247   ALLSTRUCTURES(Type.FLOW,
248           "Apply the following 3D structure formatting arguments to all structures within the open alignment.",
249           Opt.BOOLEAN, Opt.MULTI, Opt.NOACTION),
250
251   // secret options
252   TESTOUTPUT(Type.CONFIG,
253           "Allow specific stdout information.  For testing purposes only.",
254           Opt.UNARY, Opt.BOOTSTRAP, Opt.SECRET), // do not show this to the user
255   SETPROP(Type.CONFIG, "Set an individual Java System property.",
256           Opt.STRING, Opt.MULTI, Opt.BOOTSTRAP, Opt.SECRET), // not in use yet
257   NIL(Type.FLOW,
258           "This argument does nothing on its own, but can be used with linkedIds.",
259           Opt.UNARY, Opt.LINKED, Opt.MULTI, Opt.NOACTION, Opt.SECRET),
260
261   // private options (inserted during arg processing)
262   SETARGFILE(Type.FLOW,
263           "Sets the current value of the argfilename.  Inserted before argfilecontents.",
264           Opt.UNARY, Opt.LINKED, Opt.STRING, Opt.MULTI, Opt.PRIVATE,
265           Opt.NOACTION),
266   UNSETARGFILE(Type.FLOW,
267           "Unsets the current value of the argfilename.  Inserted after argfile contents.",
268           Opt.UNARY, Opt.LINKED, Opt.MULTI, Opt.PRIVATE, Opt.NOACTION),
269
270   // these last two have no purpose in the normal Jalview application but are
271   // used by jalview.bin.Launcher to set memory settings. They are not used by
272   // argparser but are here for Usage statement reasons.
273   JVMMEMPC(Type.CONFIG,
274           "Limit maximum heap size (memory) to PERCENT% of total physical memory detected. This defaults to 90 if total physical memory can be detected.\n"
275                   + "The equals sign (\"=\") separator must be used with no spaces.",
276           Opt.NOACTION, Opt.BOOTSTRAP, Opt.STRING, Opt.LAST),
277   JVMMEMMAX(Type.CONFIG,
278           "Limit maximum heap size (memory) to MAXMEMORY. MAXMEMORY can be specified in bytes, kilobytes(k), megabytes(m), gigabytes(g) or if you're lucky enough, terabytes(t). This defaults to 32g if total physical memory can be detected, or to 8g if total physical memory cannot be detected.\n"
279                   + "The equals sign (\"=\") separator must be used with no spaces.",
280           Opt.NOACTION, Opt.BOOTSTRAP, Opt.STRING, Opt.LAST),
281
282   ;
283
284   public static enum Opt
285   {
286     BOOLEAN, // This Arg can be specified as --arg or --noarg to give true or
287              // false. A default can be given with setOptions(bool, Opt....).
288              // Use ArgParser.isSet(Arg) to see if this arg was not specified.
289     STRING, // This Arg can accept a value either through --arg=value or --arg
290             // value.
291     UNARY, // This Arg is a boolean value, true if present, false if not. Like
292            // BOOLEAN but without the --noarg option.
293     MULTI, // This Arg can be specified multiple times. Multiple values are
294            // stored in the ArgValuesMap (along with their positional index) for
295            // each linkedId.
296     LINKED, // This Arg can be linked to others through a --arg[linkedId] or
297             // --arg[linkedId]=value. If no linkedId is specified then the
298             // current default linkedId will be used.
299     NODUPLICATEVALUES, // This Arg can only have one value (per linkedId). The
300                        // first value will be used and subsequent values ignored
301                        // with a warning.
302     BOOTSTRAP, // This Arg value(s) can be determined at an earlier stage than
303                // non-BOOTSTRAP Args. Substitutions do not happen in BOOTSTRAP
304                // Args and they cannot be linked or contain SubVals. See
305                // jalview.bin.argparser.BootstrapArgs.
306     GLOB, // This Arg can expand wildcard filename "globs" (e.g.
307           // path/*/filename*). If the Arg value is given as --arg filename*
308           // then the shell will have expanded the glob already, but if
309           // specified as --arg=filename* then the Java glob expansion method
310           // will be used (see FileUtils.getFilenamesFromGlob()). Note that this
311           // might be different from the shell expansion rules.
312     NOACTION, // This Arg does not perform a data task, usually used to control
313               // flow in ArgParser.parse(args).
314     ALLOWSUBSTITUTIONS, // This Arg allows substitutions in its linkedId,
315                         // SubVals and values.
316     PRIVATE, // This Arg is used internally, and cannot be specified by the
317              // user.
318     SECRET, // This Arg is used by development processes and although it can be
319             // set by the user, it is not displayed to the user.
320     ALLOWALL, // This Arg can use the '*' linkedId to apply to all known
321               // linkedIds
322     INCREMENTDEFAULTCOUNTER, // If an Arg has this option and the default
323                              // linkedId is used, the defaultLinkedIdCounter is
324                              // incremented *first*.
325     INPUT, // This Arg counts as an input for REQUIREINPUT
326     REQUIREINPUT, // This Arg can only be applied via --all if there is an
327                   // input (i.e. --open or --append)
328     OUTPUT, // This Arg provides an output filename. With Opt.ALLOWALL *.ext is
329             // shorthand for --all --output={basename}.ext
330     STORED, // This Arg resets and creates a new set of "opened" linkedIds
331     HELP, // This Arg is a --help type arg
332     PRIMARY, // This Arg is the main Arg for its type
333     HASTYPE, // This Arg can have an Arg.Type assigned to it (and no value)
334     FIRST, // Move this arg to the first in usage statement (within type)
335     LAST, // Move this arg to the end in usage statement (within type)
336   }
337
338   public static enum Type
339   {
340     // Type restricts argument to certain usage output
341     HELP, // --help
342     CONFIG("arguments used to configure "
343             + ChannelProperties.getProperty("app_name") + " from startup"),
344     OPENING("arguments used to open and format alignments"),
345     STRUCTURE("arguments used to add and format 3D structure data"),
346     PROCESS("arguments used to process an alignment once opened"),
347     OUTPUT("arguments used to save data from a processed alignment"),
348     IMAGE("arguments used to export an image of an alignment"),
349     STRUCTUREIMAGE("arguments used to export an image of an structure"),
350     FLOW("arguments that control processing of the other arguments"), //
351     ALL("all arguments"), // mostly just a place-holder for --help-all
352     NONE, // mostly a place-holder for --help
353     INVALID;
354
355     private String description;
356
357     private Type()
358     {
359       description = null;
360     }
361
362     private Type(String description)
363     {
364       this.description = description;
365     }
366
367     public String description()
368     {
369       return description;
370     }
371   }
372
373   private final String[] argNames;
374
375   private Opt[] argOptions;
376
377   private boolean defaultBoolValue;
378
379   private String description;
380
381   private Type type;
382
383   private Arg(Type type, String description, Opt... options)
384   {
385     this(type, null, description, false, options);
386   }
387
388   private Arg(Type type, String description, boolean defaultBoolean,
389           Opt... options)
390   {
391     this(type, null, description, defaultBoolean, options);
392   }
393
394   private Arg(Type type, String alternativeName, String description,
395           Opt... options)
396   {
397     this(type, alternativeName, description, false, options);
398   }
399
400   private Arg(Type type, String alternativeName, String description,
401           boolean defaultBoolean, Opt... options)
402   {
403     this.argNames = alternativeName != null
404             ? new String[]
405             { this.getName(), alternativeName }
406             : new String[]
407             { this.getName() };
408     this.type = type;
409     this.description = description;
410     this.defaultBoolValue = defaultBoolean;
411     this.setOptions(options);
412   }
413
414   public String argString()
415   {
416     return argString(false);
417   }
418
419   public String negateArgString()
420   {
421     return argString(true);
422   }
423
424   private String argString(boolean negate)
425   {
426     StringBuilder sb = new StringBuilder(ArgParser.DOUBLEDASH);
427     if (negate && hasOption(Opt.BOOLEAN))
428       sb.append(ArgParser.NEGATESTRING);
429     sb.append(getName());
430     return sb.toString();
431   }
432
433   public String toLongString()
434   {
435     StringBuilder sb = new StringBuilder();
436     sb.append(this.getClass().getName()).append('.').append(this.name());
437     sb.append('(');
438     if (getNames().length > 0)
439       sb.append('"');
440     sb.append(String.join("\", \"", getNames()));
441     if (getNames().length > 0)
442       sb.append('"');
443     sb.append(")\n");
444     sb.append("\nType: " + type.name());
445     sb.append("\nOpt: ");
446     // map List<Opt> to List<String> for the String.join
447     List<String> optList = Arrays.asList(argOptions).stream()
448             .map(opt -> opt.name()).collect(Collectors.toList());
449     sb.append(String.join(", ", optList));
450     sb.append("\n");
451     return sb.toString();
452   }
453
454   public String[] getNames()
455   {
456     return argNames;
457   }
458
459   public String getName()
460   {
461     return this.name().toLowerCase(Locale.ROOT).replace('_', '-');
462   }
463
464   @Override
465   public final String toString()
466   {
467     return getName();
468   }
469
470   public boolean hasOption(Opt o)
471   {
472     if (argOptions == null)
473       return false;
474     for (Opt option : argOptions)
475     {
476       if (o == option)
477         return true;
478     }
479     return false;
480   }
481
482   public boolean hasAllOptions(Opt... opts)
483   {
484     for (Opt o : opts)
485     {
486       if (!this.hasOption(o))
487         return false;
488     }
489     return true;
490   }
491
492   protected void setOptions(Opt... options)
493   {
494     this.argOptions = options;
495   }
496
497   protected boolean getDefaultBoolValue()
498   {
499     return defaultBoolValue;
500   }
501
502   public Type getType()
503   {
504     return this.type;
505   }
506
507   protected String getDescription()
508   {
509     return description;
510   }
511
512   public static String booleanArgString(Arg a)
513   {
514     StringBuilder sb = new StringBuilder(a.argString());
515     if (a.hasOption(Opt.BOOLEAN))
516     {
517       sb.append('/');
518       sb.append(a.negateArgString());
519     }
520     return sb.toString();
521   }
522
523   public static final String usage()
524   {
525     return usage(null);
526   }
527
528   public static final void appendUsageGeneral(StringBuilder sb,
529           int maxArgLength)
530   {
531     for (Type t : EnumSet.allOf(Type.class))
532     {
533       if (t.description() != null)
534       {
535         StringBuilder argSb = new StringBuilder();
536         argSb.append(Arg.HELP.argString()).append(ArgParser.SINGLEDASH)
537                 .append(t.name().toLowerCase(Locale.ROOT));
538         appendArgAndDescription(sb, argSb.toString(),
539                 "Help for " + t.description(), null, maxArgLength);
540         sb.append(System.lineSeparator());
541       }
542     }
543   }
544
545   public static final String usage(List<Type> types)
546   {
547     StringBuilder sb = new StringBuilder();
548
549     sb.append("usage: jalview [" + Arg.HEADLESS.argString() + "] [["
550             + Arg.OPEN.argString() + "/" + Arg.APPEND.argString()
551             + "] file(s)] [args]");
552     sb.append(System.lineSeparator());
553     sb.append(System.lineSeparator());
554
555     if (types == null || types.contains(null))
556     {
557       // always show --help
558       appendArgAndDescription(sb, null, "Display this basic help", Arg.HELP,
559               DESCRIPTIONINDENT);
560       sb.append(System.lineSeparator());
561
562       appendUsageGeneral(sb, DESCRIPTIONINDENT);
563     }
564     else
565     {
566       List<Arg> args = argsSortedForDisplay(types);
567
568       /*
569        * just use a set maxArgLength of DESCRIPTIONINDENT
570        
571       int maxArgLength = 0;
572       for (Arg a : args)
573       {
574         if (a.hasOption(Opt.PRIVATE) || a.hasOption(Opt.SECRET))
575           continue;
576       
577         String argS = argDisplayString(a);
578         if (argS.length() > maxArgLength)
579           maxArgLength = argS.length();
580       }
581       */
582       int maxArgLength = DESCRIPTIONINDENT;
583
584       // always show --help
585       appendArgAndDescription(sb, null, null, Arg.HELP, maxArgLength);
586       sb.append(System.lineSeparator());
587
588       if ((args.contains(Arg.HELP) && types.contains(Type.ALL)))
589       {
590         appendUsageGeneral(sb, maxArgLength);
591       }
592
593       Iterator<Arg> argsI = args.iterator();
594       Type typeSection = null;
595       while (argsI.hasNext())
596       {
597         Arg a = argsI.next();
598
599         if (a.hasOption(Opt.PRIVATE) || a.hasOption(Opt.SECRET)
600                 || a == Arg.HELP)
601         {
602           continue;
603         }
604
605         if (a.getType() != typeSection)
606         {
607           typeSection = a.getType();
608           String typeDescription = a.getType().description();
609           if (typeDescription != null && typeDescription.length() > 0)
610           {
611             // typeDescription = typeDescription.substring(0,
612             // 1).toUpperCase(Locale.ROOT) + typeDescription.substring(1);
613             typeDescription = typeDescription.toUpperCase(Locale.ROOT);
614             sb.append(typeDescription);
615             sb.append(System.lineSeparator());
616             sb.append(System.lineSeparator());
617           }
618         }
619
620         appendArgUsage(sb, a, maxArgLength);
621
622         if (argsI.hasNext())
623         {
624           sb.append(System.lineSeparator());
625         }
626       }
627     }
628     return sb.toString();
629   }
630
631   private static void appendArgUsage(StringBuilder sb, Arg a,
632           int maxArgLength)
633   {
634     boolean first = appendArgAndDescription(sb, null, null, a,
635             maxArgLength);
636     List<String> options = new ArrayList<>();
637
638     if (a.hasOption(Opt.BOOLEAN))
639     {
640       options.add("default " + (a.getDefaultBoolValue() ? a.argString()
641               : a.negateArgString()));
642     }
643
644     if (a.hasOption(Opt.MULTI))
645     {
646       options.add("multiple");
647     }
648
649     if (a.hasOption(Opt.LINKED))
650     {
651       options.add("can be linked");
652     }
653
654     if (a.hasOption(Opt.GLOB))
655     {
656       options.add("allows file globs");
657     }
658
659     if (a.hasOption(Opt.ALLOWSUBSTITUTIONS))
660     {
661       options.add("allows substitutions");
662     }
663
664     if (a.hasOption(Opt.ALLOWALL))
665     {
666       options.add("can be applied to all linked arguments");
667     }
668
669     if (a.hasOption(Opt.PRIVATE))
670     {
671       options.add("for internal use only");
672     }
673
674     if (a.hasOption(Opt.SECRET))
675     {
676       options.add("for development use only");
677     }
678
679     if (options.size() > 0)
680     {
681       if (first)
682       {
683         sb.append(ARGDESCRIPTIONSEPARATOR);
684       }
685       else
686       {
687         sb.append(String.format("%-"
688                 + (maxArgLength + ARGDESCRIPTIONSEPARATOR.length()) + "s",
689                 ""));
690       }
691       sb.append("(");
692       sb.append(String.join("; ", options));
693       sb.append(')');
694       sb.append(System.lineSeparator());
695     }
696   }
697
698   public static String argDisplayString(Arg a)
699   {
700     StringBuilder argSb = new StringBuilder();
701     argSb.append(
702             a.hasOption(Opt.BOOLEAN) ? booleanArgString(a) : a.argString());
703     if (a.hasOption(Opt.STRING))
704       argSb.append("=value");
705     return argSb.toString();
706   }
707
708   public static boolean appendArgAndDescription(StringBuilder sb,
709           String aString, String description, Arg a, int maxArgLength)
710   {
711     return appendArgAndDescription(sb, aString, description, a,
712             maxArgLength, Platform.consoleWidth());
713   }
714
715   public static boolean appendArgAndDescription(StringBuilder sb,
716           String aString, String description, Arg a, int maxArgLength,
717           int maxLength)
718   {
719     if (aString == null && a != null)
720     {
721       aString = argDisplayString(a);
722     }
723     if (description == null && a != null)
724     {
725       description = a.getDescription();
726     }
727     sb.append(String.format("%-" + maxArgLength + "s", aString));
728     if (aString.length() > maxArgLength)
729     {
730       sb.append(System.lineSeparator());
731       sb.append(String.format("%-" + maxArgLength + "s", ""));
732     }
733
734     int descLength = maxLength - maxArgLength
735             - ARGDESCRIPTIONSEPARATOR.length();
736     // reformat the descriptions lines to the right width
737     Iterator<String> descLines = null;
738     if (description != null)
739     {
740       descLines = Arrays.stream(description.split("\\n")).iterator();
741     }
742     List<String> splitDescLinesList = new ArrayList<>();
743     while (descLines != null && descLines.hasNext())
744     {
745       String line = descLines.next();
746       while (line.length() > descLength)
747       {
748         int splitIndex = line.lastIndexOf(" ", descLength);
749         splitDescLinesList.add(line.substring(0, splitIndex));
750         line = line.substring(splitIndex + 1);
751       }
752       splitDescLinesList.add(line);
753     }
754
755     Iterator<String> splitDescLines = splitDescLinesList.iterator();
756     boolean first = true;
757     if (splitDescLines != null)
758     {
759       while (splitDescLines.hasNext())
760       {
761         if (first)
762         {
763           sb.append(ARGDESCRIPTIONSEPARATOR);
764         }
765         else
766         {
767           sb.append(String.format("%-"
768                   + (maxArgLength + ARGDESCRIPTIONSEPARATOR.length()) + "s",
769                   ""));
770         }
771         sb.append(splitDescLines.next());
772         sb.append(System.lineSeparator());
773         first = false;
774       }
775     }
776     return first;
777   }
778
779   protected static Iterator<Arg> getAllOfType(Type type)
780   {
781     return getAllOfType(type, new Opt[] {});
782   }
783
784   protected static Iterator<Arg> getAllOfType(Type type, Opt... options)
785   {
786     Opt[] opts = options == null ? new Opt[] {} : options;
787     return EnumSet.allOf(Arg.class).stream().filter(a -> {
788       if (a.getType() != type)
789         return false;
790       for (Opt o : opts)
791       {
792         if (!a.hasOption(o))
793           return false;
794       }
795       return true;
796     }).iterator();
797   }
798
799   private static List<Arg> argsSortedForDisplay(List<Type> types)
800   {
801     List<Arg> argsToSort;
802     // if no types provided, do all
803     if (types == null || types.size() == 0 || types.contains(Type.ALL))
804     {
805       argsToSort = Arrays
806               .asList(EnumSet.allOf(Arg.class).toArray(new Arg[] {}));
807     }
808     else
809     {
810       argsToSort = new ArrayList<>();
811       for (Type type : types)
812       {
813         if (type == null)
814           continue;
815         Arg.getAllOfType(type).forEachRemaining(a -> argsToSort.add(a));
816       }
817     }
818
819     Collections.sort(argsToSort, new ArgDisplayComparator());
820     return argsToSort;
821   }
822
823   private static final String ARGDESCRIPTIONSEPARATOR = " - ";
824
825   private static final int DESCRIPTIONINDENT = 20;
826
827 }
828
829 class ArgDisplayComparator implements Comparator<Arg>
830 {
831   private int compareArgOpts(Arg a, Arg b, Opt o)
832   {
833     int i = a.hasOption(o) ? (b.hasOption(o) ? 0 : -1)
834             : (b.hasOption(o) ? 1 : 0);
835     return i;
836   }
837
838   private int compareForDisplay(Arg a, Arg b)
839   {
840     if (b == null)
841       return -1;
842     // first compare types (in enum order)
843     int i = a.getType().compareTo(b.getType());
844     if (i != 0)
845       return i;
846     // do Opt.LAST next (oddly). Reversed args important!
847     i = compareArgOpts(b, a, Opt.LAST);
848     if (i != 0)
849       return i;
850     // priority order
851     Opt[] optOrder = { Opt.HELP, Opt.FIRST, Opt.PRIMARY, Opt.STRING,
852         Opt.BOOLEAN };
853     for (Opt o : optOrder)
854     {
855       i = compareArgOpts(a, b, o);
856       if (i != 0)
857         return i;
858     }
859     // finally order of appearance in enum declarations
860     return a.compareTo(b);
861   }
862
863   @Override
864   public int compare(Arg a, Arg b)
865   {
866     return compareForDisplay(a, b);
867   }
868 }