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