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