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