【问题标题】:Using picoCLI to recursively parse configuration files?使用 picoCLI 递归解析配置文件?
【发布时间】:2022-07-06 07:00:48
【问题描述】:

我在下面回答这个问题;我打开它是因为它比我最初在 earlier question 中询问的更通用,所以它不适合在那里。我花了很多时间修修补补,所以我想我会在这里分享解决方案。

我的情况:

我使用 picoCLI 解析多个配置文件,这些配置文件又可以“包含”其他配置文件,达到任意深度。不幸的是,对于我的一些选项,它们的解析顺序也很重要。

在我的应用程序中,有像 section=A:teacher 这样的“部分”选项,请求部分 A 并导致它为教师、学生或其他群体处理(我将省略其确切含义)。在许多其他选项中,还有一个名为configfile= 的选项“包含”另一个选项文件。这种情况可以用配置细节的“树”来描述:

# options given on actual command line ("root of tree")
    section=A:teacher
    configfile=cf-1.txt  # include options from cf-1.txt
        section=A:student # this indentation: options read from cf-1.txt
        section=B:principal
        configfile=cf-2.txt  # read options from cf-2.txt
            section=A:parent # this indentation: options read from cf-2.txt
            section=C:parent
        section=C:teacher  # back in cf-1.txt
    section=D:admin  # back to actual command line

我希望深度优先遍历这棵树,如果它们引用相同的部分名称,“稍后”选项会覆盖“早期”选项:最后,部分 A 应该得到 parent 和 C应该得到teacher。

为了解析configfile= 选项,我不能使用picoCLI 的@-syntax,因为这些文件不一定在“当前”文件夹中,所以我想控制应用程序在哪里寻找它们。这是问题#1。通过下面列出的parseConfigfile方法解决。

不幸的是,当一个选项在同一个文件中多次出现时,picoCLI 有一个特殊的怪癖(就像section 对A、B 和C 所做的那样):它每次都会调用带注释的setter 方法但在该方法的列表参数中累积选项值。第一个电话只得到(A:student),第二个(A:student),(B:prof),第三个(A:student),(B:prof),(C:teacher)`等等。

我了解到here 这种行为是有意为之的,但对我来说这是问题#2,因为对section=A:student 的重复评估弄乱了我的 later-options-overwrite-earlier-ones 语义:最后,A 被错误地配置为student。对于许多选项(具有“一维”值的选项)来说,这不是问题,但它适用于section=,有点讽刺的是,也适用于configfile=。

【问题讨论】:

    标签: java picocli


    【解决方案1】:

    这是我的解决方案;也许它对某人有用。基本方法是为每个嵌套的配置文件使用一个新的CommandLine 实例。

    以下所有代码 sn-ps 均来自带注释的类;删除了特定于项目的内务管理、错误检查、路径构建等。

    问题#1通过parseConfigFile(...)方法解决:

    @Option(names = { "configfile", "cf" } )
    public void parseConfigfile(final List<String> accumulatedCfgFiles) {
        // prune the list, keeping only "new" entries that haven't been seen yet:
        List<String> newCfgFiles = this.cfgfileHelper.retainNewOptionValuesOnly(
            accumulatedCfgFiles, "configfile");
        if(newCfgFiles.isEmpty()) {
            // another picoCLI quirk: this happens even if there always are values 
            return; 
        } else if(newCfgFiles.size() > 1) {
            // loop over the files if you want to allow this, or report an error
        }
        // some path tinkering left out
        File cfgFile = new File(newCfgFiles.get(0));
        if(this.stackOfConfigFiles.contains(cfgFile)) {
            // report error because of cyclic reference
        } else {
            this.stackOfConfigFiles.push(cfgFile);
            // task a new CommandLine instance with processing that file:
            CommandLine cmd = new CommandLine(this);
            String[] optionsFromFile = FileUtils.readLines(cfgFile); // Apache Commons               
            this.cfgfileHelper.wrapParseArgs(cmd, optionsFromFile);
            this.stackOfConfigFiles.pop();
        }
    }
    

    该方法使用NestedCfgfileHelper 的实例(参见下面的源代码),它执行所有嵌套配置特定的内务管理以解决问题#2。每个带注释的类的该帮助器类的一个实例跟踪当前在您的带注释的类上工作的CommandLine 实例。它的构造函数需要助手类应该处理的所有选项的名称:

    ...
    public final NestedCfgfileHelper cfgfileHelper = 
        new NestedCfgfileHelper(new String[] { "configfile", "section" });
    ...
    

    以下步骤使所有这些工作:

    • 识别那些对“虚假的 setter 方法调用”敏感的选项(大多数不是);
    • 如果有,请将NestedCfgfileHelper 的源代码作为内部类粘贴到带注释的类中;
    • 创建一个NestedCfgfileHelper 的实例作为注释类的公共成员,告诉构造函数所有这些“有问题的”选项的名称;
    • 永远不要直接调用yourInstanceOfCommandLine.parseArgs(...),而是使用instanceOfYourAnnotatedClass.cfgfileHelper.wrapParseArgs(...)将它传递给助手
    • 让那些“困难”选项的设置方法...
      • ... 首先通过调用retainNewOptionValuesOnly 来消除先前调用中的“旧”值
      • ...然后正常处理剩余的选项值

    最后,这是NestedCfgfileHelper的来源:

    /** NestedCfgfileHelper ensures that the values of certain options are 
     * processed just once, despite the picoCLI quirk.     */
    public final class NestedCfgfileHelper {
        /** Maps an (option name|CommandLine instance) pair to the number of 
         * option values that instance has so far passed for that option. 
         * Because Java doesn't have Maps with two keys, it's implemented as 
         * a Map of Maps:         */
        private Map<String, Map<CommandLine, Integer>> mapOptionAndCLToCount =
                new HashMap<>();
    
        /** Constructs a helper instance and prepares it to handle the options 
         * given as parameters. 
         * 
         * @param optionNames any number of Strings, with each String denoting
         * one option whose values should be protected against being processed
         * multiple times */
        public NestedCfgfileHelper(String... optionNames) {
            // make one mapping for each option name given:
            for(String optionName: optionNames) {
                mapOptionAndCLToCount.put(optionName, new HashMap<CommandLine, Integer>());
            }
        }
            
        /** This stack keeps track of CommandLine instances that are currently 
         * working on this TFConfig instance. A stack is needed because config files
         * can be nested. Entries in the CommandLine stack correspond to entries in
         * the config file stack (maintained by the parseConfigfile() method),
         * except the lowest (first) one that gets fed the String[] coming directly
         * from the main(String[] args) call.     */
       private Stack<CommandLine> stackOfCmdLineInstances = new Stack<>();
    
        /** Wraps the call to {@link CommandLine#parseArgs(String...)} with some
         * housekeeping so that when an annotated setter method is being called
         * during option parsing, the helper method can look up from which 
         * CommandLine instance the call is coming.
         * Because parseArg invocations will be nested recursively for nested config
         * files, the respective CommandLine instances are kept on a stack.
         * @param cl CommandLine instance that's been about to start parsing
         * @param args options that are to be parsed     */
        public void wrapParseArgsCall(final CommandLine cl, final String[] args) {
            // the brand new CommandLine instance hasn't passed any values yet,
            // so put 0 in all maps: 
            mapOptionAndCLToCount.forEach(
                (String s, Map<CommandLine, Integer> m) -> m.put(cl, 0));
            this.stackOfCmdLineInstances.push(cl);
            cl.parseArgs(args);
            this.stackOfCmdLineInstances.pop();
        }
    
        /** This method filters its list parameter, discarding the first n 
         * entries (assuming they've already been processed), where n is retrieved
         * from a Map instance kept for each option name. This method is intended
         * to be called by annotated setter methods.
         * 
         * @param accumulated List containing all values (old and new ones 
         * accumulated) of the option named in the other parameter.
         * @param optionName describes the option that's being parsed.
         * @return pruned list containing only the "new" values that haven't
         * been seen before.     */
        private List<String> retainNewOptionValuesOnly(
            final List<String> accumulated, 
            final String optionName) {
    
            // get the CommandLine instance currently working on this TFConfig instance:
            CommandLine currentCL = this.stackOfCmdLineInstances.peek();
    
            // get the CommandLine->int map for the option name passed:
            Map<CommandLine, Integer> map = mapOptionAndCLToCount.get(optionName);
            if(map == null) {
                throw new IllegalArgumentException("unknown option: " + optionName);
            }
                
            /* Find out how many option values it has already passed to the setter.
             * For each CL, that number is stored in the map passed to this method.*/
            int n = map.get(currentCL);
                
            /* discard the first n entries (they have already been processed) of
             * accumulated, keeping only the "new" ones: */ 
            List<String> optionValuesNewThisTime = 
                accumulated.subList(n, accumulated.size());
            // associate the new number of patterns with the current CommandLine:
            int newNumber = n + optionValuesNewThisTime.size();
            map.put(currentCL, newNumber);
            return optionValuesNewThisTime;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-01
      • 2017-10-17
      • 1970-01-01
      • 2018-05-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多