JAL-629 More consistent printing of --arguments. Example nf-core argfile
[jalview.git] / src / jalview / bin / argparser / ArgParser.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.bin.argparser;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.nio.file.Files;
26 import java.nio.file.Paths;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.EnumSet;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33
34 import jalview.bin.Console;
35 import jalview.bin.Jalview;
36 import jalview.bin.argparser.Arg.Opt;
37 import jalview.util.FileUtils;
38
39 public class ArgParser
40 {
41   protected static final String DOUBLEDASH = "--";
42
43   protected static final char EQUALS = '=';
44
45   protected static final String NEGATESTRING = "no";
46
47   // the default linked id prefix used for no id (not even square braces)
48   protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
49
50   // the counter added to the default linked id prefix
51   private int defaultLinkedIdCounter = 0;
52
53   // the linked id prefix used for --opennew files
54   protected static final String OPENNEWLINKEDIDPREFIX = "OPENNEW:";
55
56   // the counter added to the default linked id prefix
57   private int opennewLinkedIdCounter = 0;
58
59   // the linked id substitution string used to increment the idCounter (and use
60   // the incremented value)
61   private static final String INCREMENTAUTOCOUNTERLINKEDID = "{++n}";
62
63   // the linked id substitution string used to use the idCounter
64   private static final String AUTOCOUNTERLINKEDID = "{n}";
65
66   // the linked id substitution string used to use the base filename of --open
67   // or --opennew
68   private static final String LINKEDIDBASENAME = "{basename}";
69
70   // the linked id substitution string used to use the dir path of --open
71   // or --opennew
72   private static final String LINKEDIDDIRNAME = "{dirname}";
73
74   // the current argfile
75   private String argFile = null;
76
77   // the linked id substitution string used to use the dir path of the latest
78   // --argfile name
79   private static final String ARGFILEBASENAME = "{argfilebasename}";
80
81   // the linked id substitution string used to use the dir path of the latest
82   // --argfile name
83   private static final String ARGFILEDIRNAME = "{argfiledirname}";
84
85   private int linkedIdAutoCounter = 0;
86
87   // flag to say whether {n} subtitutions in output filenames should be made.
88   // Turn on and off with --subs and --nosubs
89   private boolean substitutions = false;
90
91   protected static final Map<String, Arg> argMap;
92
93   protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
94
95   protected List<String> linkedOrder = null;
96
97   protected List<Arg> argList;
98
99   static
100   {
101     argMap = new HashMap<>();
102     for (Arg a : EnumSet.allOf(Arg.class))
103     {
104       for (String argName : a.getNames())
105       {
106         if (argMap.containsKey(argName))
107         {
108           Console.warn("Trying to add argument name multiple times: '"
109                   + argName + "'"); // RESTORE THIS WHEN
110           // MERGED
111           if (argMap.get(argName) != a)
112           {
113             Console.error(
114                     "Trying to add argument name multiple times for different Args: '"
115                             + argMap.get(argName).getName() + ":" + argName
116                             + "' and '" + a.getName() + ":" + argName
117                             + "'");
118           }
119           continue;
120         }
121         argMap.put(argName, a);
122       }
123     }
124   }
125
126   public ArgParser(String[] args)
127   {
128     this(args, false);
129   }
130
131   public ArgParser(String[] args, boolean initsubstitutions)
132   {
133     // Make a mutable new ArrayList so that shell globbing parser works.
134     // (When shell file globbing is used, there are a sequence of non-Arg
135     // arguments (which are the expanded globbed filenames) that need to be
136     // consumed by the --open/--argfile/etc Arg which is most easily done by
137     // removing these filenames from the list one at a time. This can't be done
138     // with an ArrayList made with only Arrays.asList(String[] args). )
139     this(new ArrayList<>(Arrays.asList(args)), initsubstitutions);
140   }
141
142   public ArgParser(List<String> args, boolean initsubstitutions)
143   {
144     this(args, initsubstitutions, false);
145   }
146
147   public ArgParser(List<String> args, boolean initsubstitutions,
148           boolean allowPrivate)
149   {
150     // do nothing if there are no "--" args and some "-" args
151     boolean d = false;
152     boolean dd = false;
153     for (String arg : args)
154     {
155       if (arg.startsWith(DOUBLEDASH))
156       {
157         dd = true;
158         break;
159       }
160       else if (arg.startsWith("-"))
161       {
162         d = true;
163       }
164     }
165     if (d && !dd)
166     {
167       // leave it to the old style -- parse an empty list
168       parse(new ArrayList<String>(), false, false);
169       return;
170     }
171     parse(args, initsubstitutions, allowPrivate);
172   }
173
174   private void parse(List<String> args, boolean initsubstitutions,
175           boolean allowPrivate)
176   {
177     this.substitutions = initsubstitutions;
178     int argIndex = 0;
179     boolean openEachInitialFilenames = true;
180     for (int i = 0; i < args.size(); i++)
181     {
182       String arg = args.get(i);
183
184       // If the first arguments do not start with "--" or "-" or is "open" and
185       // is a filename that exists it is probably a file/list of files to open
186       // so we fake an Arg.OPEN argument and when adding files only add the
187       // single arg[i] and increment the defaultLinkedIdCounter so that each of
188       // these files is opened separately.
189       if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH)
190               && !arg.startsWith("-") && new File(arg).exists())
191       {
192         arg = Arg.OPENNEW.argString();
193       }
194       else
195       {
196         openEachInitialFilenames = false;
197       }
198
199       String argName = null;
200       String val = null;
201       List<String> globVals = null; // for Opt.GLOB only
202       SubVals globSubVals = null; // also for use by Opt.GLOB only
203       String linkedId = null;
204       if (arg.startsWith(DOUBLEDASH))
205       {
206         int equalPos = arg.indexOf(EQUALS);
207         if (equalPos > -1)
208         {
209           argName = arg.substring(DOUBLEDASH.length(), equalPos);
210           val = arg.substring(equalPos + 1);
211         }
212         else
213         {
214           argName = arg.substring(DOUBLEDASH.length());
215         }
216         int idOpen = argName.indexOf('[');
217         int idClose = argName.indexOf(']');
218
219         if (idOpen > -1 && idClose == argName.length() - 1)
220         {
221           linkedId = argName.substring(idOpen + 1, idClose);
222           argName = argName.substring(0, idOpen);
223         }
224
225         Arg a = argMap.get(argName);
226         // check for boolean prepended by "no"
227         boolean negated = false;
228         if (a == null && argName.startsWith(NEGATESTRING) && argMap
229                 .containsKey(argName.substring(NEGATESTRING.length())))
230         {
231           argName = argName.substring(NEGATESTRING.length());
232           a = argMap.get(argName);
233           negated = true;
234         }
235
236         // check for config errors
237         if (a == null)
238         {
239           // arg not found
240           Console.error("Argument '" + arg + "' not recognised. Ignoring.");
241           continue;
242         }
243         if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
244         {
245           Console.error(
246                   "Argument '" + a.argString() + "' is private. Ignoring.");
247           continue;
248         }
249         if (!a.hasOption(Opt.BOOLEAN) && negated)
250         {
251           // used "no" with a non-boolean option
252           Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
253                   + "' not a boolean option. Ignoring.");
254           continue;
255         }
256         if (!a.hasOption(Opt.STRING) && equalPos > -1)
257         {
258           // set --argname=value when arg does not accept values
259           Console.error("Argument '" + a.argString()
260                   + "' does not expect a value (given as '" + arg
261                   + "').  Ignoring.");
262           continue;
263         }
264         if (!a.hasOption(Opt.LINKED) && linkedId != null)
265         {
266           // set --argname[linkedId] when arg does not use linkedIds
267           Console.error("Argument '" + a.argString()
268                   + "' does not expect a linked id (given as '" + arg
269                   + "'). Ignoring.");
270           continue;
271         }
272
273         // String value(s)
274         if (a.hasOption(Opt.STRING))
275         {
276           if (equalPos >= 0)
277           {
278             if (a.hasOption(Opt.GLOB))
279             {
280               // strip off and save the SubVals to be added individually later
281               globSubVals = ArgParser.getSubVals(val);
282               // make substitutions before looking for files
283               String fileGlob = makeSubstitutions(globSubVals.getContent(),
284                       linkedId);
285               globVals = FileUtils.getFilenamesFromGlob(fileGlob);
286             }
287             else
288             {
289               // val is already set -- will be saved in the ArgValue later in
290               // the normal way
291             }
292           }
293           else
294           {
295             // There is no "=" so value is next arg or args (possibly shell
296             // glob-expanded)
297             if ((openEachInitialFilenames ? i : i + 1) >= args.size())
298             {
299               // no value to take for arg, which wants a value
300               Console.error("Argument '" + a.getName()
301                       + "' requires a value, none given. Ignoring.");
302               continue;
303             }
304             // deal with bash globs here (--arg val* is expanded before reaching
305             // the JVM). Note that SubVals cannot be used in this case.
306             // If using the --arg=val then the glob is preserved and Java globs
307             // will be used later. SubVals can be used.
308             if (a.hasOption(Opt.GLOB))
309             {
310               // if this is the first argument with a file list at the start of
311               // the args we add filenames from index i instead of i+1
312               globVals = getShellGlobbedFilenameValues(a, args,
313                       openEachInitialFilenames ? i : i + 1);
314             }
315             else
316             {
317               val = args.get(i + 1);
318             }
319           }
320         }
321
322         // make NOACTION adjustments
323         // default and auto counter increments
324         if (a == Arg.INCREMENT)
325         {
326           defaultLinkedIdCounter++;
327         }
328         else if (a == Arg.NPP)
329         {
330           linkedIdAutoCounter++;
331         }
332         else if (a == Arg.SUBSTITUTIONS)
333         {
334           substitutions = !negated;
335         }
336         else if (a == Arg.SETARGFILE)
337         {
338           argFile = val;
339         }
340         else if (a == Arg.UNSETARGFILE)
341         {
342           argFile = null;
343         }
344
345         String autoCounterString = null;
346         boolean usingAutoCounterLinkedId = false;
347         String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
348                 .append(Integer.toString(defaultLinkedIdCounter))
349                 .toString();
350         boolean usingDefaultLinkedId = false;
351         if (a.hasOption(Opt.LINKED))
352         {
353           if (linkedId == null)
354           {
355             if (a == Arg.OPENNEW)
356             {
357               // use the next default prefixed OPENNEWLINKEDID
358               linkedId = new StringBuilder(OPENNEWLINKEDIDPREFIX)
359                       .append(Integer.toString(opennewLinkedIdCounter))
360                       .toString();
361               opennewLinkedIdCounter++;
362             }
363             else
364             {
365               // use default linkedId for linked arguments
366               linkedId = defaultLinkedId;
367               usingDefaultLinkedId = true;
368               Console.debug("Changing linkedId to '" + linkedId + "' from "
369                       + arg);
370             }
371           }
372           else if (linkedId.contains(AUTOCOUNTERLINKEDID))
373           {
374             // turn {n} to the autoCounter
375             autoCounterString = Integer.toString(linkedIdAutoCounter);
376             linkedId = linkedId.replace(AUTOCOUNTERLINKEDID,
377                     autoCounterString);
378             usingAutoCounterLinkedId = true;
379             Console.debug(
380                     "Changing linkedId to '" + linkedId + "' from " + arg);
381           }
382           else if (linkedId.contains(INCREMENTAUTOCOUNTERLINKEDID))
383           {
384             // turn {++n} to the incremented autoCounter
385             autoCounterString = Integer.toString(++linkedIdAutoCounter);
386             linkedId = linkedId.replace(INCREMENTAUTOCOUNTERLINKEDID,
387                     autoCounterString);
388             usingAutoCounterLinkedId = true;
389             Console.debug(
390                     "Changing linkedId to '" + linkedId + "' from " + arg);
391           }
392         }
393
394         if (!linkedArgs.containsKey(linkedId))
395           linkedArgs.put(linkedId, new ArgValuesMap());
396
397         // do not continue for NOACTION args
398         if (a.hasOption(Opt.NOACTION))
399           continue;
400
401         ArgValuesMap avm = linkedArgs.get(linkedId);
402
403         // not dealing with both NODUPLICATEVALUES and GLOB
404         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
405         {
406           Console.error("Argument '" + a.argString()
407                   + "' cannot contain a duplicate value ('" + val
408                   + "'). Ignoring this and subsequent occurrences.");
409           continue;
410         }
411
412         // check for unique id
413         SubVals idsv = ArgParser.getSubVals(val);
414         String id = idsv.get(ArgValues.ID);
415         if (id != null && avm.hasId(a, id))
416         {
417           Console.error("Argument '" + a.argString()
418                   + "' has a duplicate id ('" + id + "'). Ignoring.");
419           continue;
420         }
421
422         boolean argIndexIncremented = false;
423         ArgValues avs = avm.getOrCreateArgValues(a);
424
425         // store appropriate String value(s)
426         if (a.hasOption(Opt.STRING))
427         {
428           if (a.hasOption(Opt.GLOB) && globVals != null
429                   && globVals.size() > 0)
430           {
431             for (String v : globVals)
432             {
433               v = makeSubstitutions(v, linkedId);
434               SubVals vsv = new SubVals(globSubVals, v);
435               avs.addValue(vsv, v, argIndex++);
436               argIndexIncremented = true;
437             }
438           }
439           else
440           {
441             avs.addValue(makeSubstitutions(val, linkedId), argIndex);
442           }
443         }
444         else if (a.hasOption(Opt.BOOLEAN))
445         {
446           avs.setBoolean(!negated, argIndex);
447           avs.setNegated(negated);
448         }
449         else if (a.hasOption(Opt.UNARY))
450         {
451           avs.setBoolean(true, argIndex);
452         }
453         avs.incrementCount();
454         if (!argIndexIncremented)
455           argIndex++;
456
457         // store in appropriate place
458         if (a.hasOption(Opt.LINKED))
459         {
460           // store the order of linkedIds
461           if (linkedOrder == null)
462             linkedOrder = new ArrayList<>();
463           if (!linkedOrder.contains(linkedId))
464             linkedOrder.add(linkedId);
465         }
466
467         // store arg in the list of args used
468         if (argList == null)
469           argList = new ArrayList<>();
470         if (!argList.contains(a))
471           argList.add(a);
472
473       }
474     }
475   }
476
477   private String makeSubstitutions(String val, String linkedId)
478   {
479     if (!this.substitutions)
480       return val;
481
482     String subvals;
483     String rest;
484     if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
485     {
486       int closeBracket = val.indexOf(']');
487       if (val.length() == closeBracket)
488         return val;
489       subvals = val.substring(0, closeBracket + 1);
490       rest = val.substring(closeBracket + 1);
491     }
492     else
493     {
494       subvals = "";
495       rest = val;
496     }
497     if (rest.contains(AUTOCOUNTERLINKEDID))
498       rest = rest.replace(AUTOCOUNTERLINKEDID,
499               String.valueOf(linkedIdAutoCounter));
500     if (rest.contains(INCREMENTAUTOCOUNTERLINKEDID))
501       rest = rest.replace(INCREMENTAUTOCOUNTERLINKEDID,
502               String.valueOf(++linkedIdAutoCounter));
503     if (rest.contains("{}"))
504       rest = rest.replace("{}", String.valueOf(defaultLinkedIdCounter));
505     ArgValuesMap avm = linkedArgs.get(linkedId);
506     if (avm != null)
507     {
508       if (rest.contains(LINKEDIDBASENAME))
509       {
510         rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
511       }
512       if (rest.contains(LINKEDIDDIRNAME))
513       {
514         rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
515       }
516     }
517     if (argFile != null)
518     {
519       if (rest.contains(ARGFILEBASENAME))
520       {
521         rest = rest.replace(ARGFILEBASENAME,
522                 FileUtils.getBasename(new File(argFile)));
523       }
524       if (rest.contains(ARGFILEDIRNAME))
525       {
526         rest = rest.replace(ARGFILEDIRNAME,
527                 FileUtils.getDirname(new File(argFile)));
528       }
529     }
530
531     return new StringBuilder(subvals).append(rest).toString();
532   }
533
534   /*
535    * A helper method to take a list of String args where we're expecting
536    * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
537    * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
538    * "file2", "file3"} *and remove these from the original list object* so that
539    * processing can continue from where it has left off, e.g. args has become
540    * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
541    * carries on from the next --arg if available.
542    */
543   protected static List<String> getShellGlobbedFilenameValues(Arg a,
544           List<String> args, int i)
545   {
546     List<String> vals = new ArrayList<>();
547     while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
548     {
549       vals.add(FileUtils.substituteHomeDir(args.remove(i)));
550       if (!a.hasOption(Opt.GLOB))
551         break;
552     }
553     return vals;
554   }
555
556   public boolean isSet(Arg a)
557   {
558     return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
559   }
560
561   public boolean isSet(String linkedId, Arg a)
562   {
563     ArgValuesMap avm = linkedArgs.get(linkedId);
564     return avm == null ? false : avm.containsArg(a);
565   }
566
567   public boolean getBool(Arg a)
568   {
569     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
570     {
571       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
572               + "'.");
573     }
574     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
575   }
576
577   public boolean getBool(String linkedId, Arg a)
578   {
579     ArgValuesMap avm = linkedArgs.get(linkedId);
580     if (avm == null)
581       return a.getDefaultBoolValue();
582     ArgValues avs = avm.getArgValues(a);
583     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
584   }
585
586   public List<String> linkedIds()
587   {
588     return linkedOrder;
589   }
590
591   public ArgValuesMap linkedArgs(String id)
592   {
593     return linkedArgs.get(id);
594   }
595
596   @Override
597   public String toString()
598   {
599     StringBuilder sb = new StringBuilder();
600     sb.append("UNLINKED\n");
601     sb.append(argValuesMapToString(linkedArgs.get(null)));
602     if (linkedIds() != null)
603     {
604       sb.append("LINKED\n");
605       for (String id : linkedIds())
606       {
607         // already listed these as UNLINKED args
608         if (id == null)
609           continue;
610
611         ArgValuesMap avm = linkedArgs(id);
612         sb.append("ID: '").append(id).append("'\n");
613         sb.append(argValuesMapToString(avm));
614       }
615     }
616     return sb.toString();
617   }
618
619   private static String argValuesMapToString(ArgValuesMap avm)
620   {
621     if (avm == null)
622       return null;
623     StringBuilder sb = new StringBuilder();
624     for (Arg a : avm.getArgKeys())
625     {
626       ArgValues v = avm.getArgValues(a);
627       sb.append(v.toString());
628       sb.append("\n");
629     }
630     return sb.toString();
631   }
632
633   public static SubVals getSubVals(String item)
634   {
635     return new SubVals(item);
636   }
637
638   public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
639           boolean initsubstitutions)
640   {
641     List<File> argFiles = new ArrayList<>();
642
643     for (String pattern : argFilenameGlobs)
644     {
645       // I don't think we want to dedup files, making life easier
646       argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
647     }
648
649     return parseArgFileList(argFiles, initsubstitutions);
650   }
651
652   public static ArgParser parseArgFileList(List<File> argFiles,
653           boolean initsubstitutions)
654   {
655     List<String> argsList = new ArrayList<>();
656     for (File argFile : argFiles)
657     {
658       if (!argFile.exists())
659       {
660         String message = Arg.ARGFILE.argString() + EQUALS + "\""
661                 + argFile.getPath() + "\": File does not exist.";
662         Jalview.exit(message, 2);
663       }
664       try
665       {
666         String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
667                 .append(EQUALS).append(argFile.getCanonicalPath())
668                 .toString();
669         argsList.add(setargfile);
670         argsList.addAll(Files.readAllLines(Paths.get(argFile.getPath())));
671         argsList.add(Arg.UNSETARGFILE.argString());
672       } catch (IOException e)
673       {
674         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
675                 + "\": File could not be read.";
676         Jalview.exit(message, 3);
677       }
678     }
679     // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
680     // --unsetargfile
681     return new ArgParser(argsList, initsubstitutions, true);
682   }
683
684 }