{"action":"created","comment":{"url":"https://api.github.com/repos/quattor/CAF/pulls/comments/62254502","id":62254502,"diff_hunk":"@@ -0,0 +1,1175 @@\n+# ${license-info}\n+# ${developer-info}\n+# ${author-info}\n+# ${build-info}\n+\n+=pod\n+\n+=head1 DESCRIPTION\n+\n+This module implements a rule-based editor that is used to modify the content\n+of an existing file. Each rule driving the editing process is applied to all \n+lines wose \"keyword\" is matching the one specified in the rule. The input for\n+updating the file is a hash typically built from the Quattor configuration when\n+the rule-based editor is called from a configuration module. Conditions can be defined\n+based on the contents of this configuration. Lines in the configuration file\n+that don't match any rule are kept unmodified.\n+\n+This module is a subclass of the L<CAF::FileEditor>: it extends the base methods of \n+the L<CAF::FileEditor>. It has only one public method (it uses the L<CAF::FileEditor> constructor).\n+The methods provided in this module can be combined with L<CAF::FileEditor>\n+methods to edit a file.\n+\n+Rules used to edit the file are defined in a hash: each entry (key/value pair) defines a rule.\n+Multiple rules can be applied to the same file: it is important that they are\n+orthogonal, else the result is unpredictable. The order used to apply rules is the alphabetical\n+order of keywords. Applying the rules to the same configuration always give the same result\n+but the changes are not necessarily idempotent (order in which successive edits occured\n+may matter, depending on the actual rules).\n+\n+The hash entry key represents the line keyword in configuration file and\n+hash value is the parsing rule for the keyword value. Parsing rule format is :\n+\n+      [condition->]option_name:option_set[,option_set,...];line_fmt[;value_fmt[:value_fmt_opt]]\n+\n+If the line keyword (hash key) starts with a '-', the matching\n+configuration line will be removed/commented out (instead of added/updated) from the\n+configuration file if present. If it starts with a '?', the\n+matching line will be removed/commented out if the option is undefined.\n+\n+=over\n+\n+=item condition\n+\n+An option or an option set (see below) that must exist for the rule to be applied.\n+Both C<option_set> and C<option_name:option_set> are accepted.\n+One option set only is allowed and only its existence (not its value) is tested.\n+It is possible to negate the condition (option or option_set must not exist) \n+by prepending it with '!'.\n+\n+=item option_name \n+\n+The name of an option that will be retrieved from the configuration. An option is\n+a key in the option set hash.\n+\n+=item option_set \n+\n+The name of an option set where the option is located in (for example 'dpnsHost:dpm'\n+means C<dpnsHost> option of C<dpm> option set). An option set is a sub-hash in the configuration\n+hash. C<GLOBAL> is a special value for C<option_set> indicating that the option is a global option,\n+instead of belonging to a specific option set (global options are at the top level of the configuration\n+hash).\n+\n+=item line_fmt \n+\n+Defines the format used to represent the key/value pair. The following formats are\n+supported (see LINE_FORMAT_xxx constants below):\n+\n+=over\n+\n+=item *\n+\n+A SH shell environment variable definition (export key=val).\n+\n+=item *\n+\n+A SH shell variable definition (key=val).\n+\n+=item *\n+\n+A 'keyword value' line, as used by Xrootd or Apache config files.\n+\n+=item *\n+\n+A 'setenv keyword value' line, as used by Xrootd config files mainly. It can also be used in a CSH shell script.\n+\n+=item *\n+\n+A 'set keyword value' line, as used by Xrootd config files mainly. It doesn't work in a CSH shell script (C<=> missing).\n+\n+=back\n+\n+Inline comments are not supported in 'keyword value' family of formats.\n+\n+=item value_fmt \n+\n+used to indicate how to interpret the configuration value. It is used mainly for\n+boolean values, list and hashes. See LINE_VALUE_xxx constants below for the possible values.\n+\n+=item value_fmt \n+\n+used to indicate how to interpret the configuration value. It is used mainly for\n+boolean values, list and hashes. See LINE_VALUE_xxx constants below for the possible values.\n+\n+=back \n+\n+An example of rule declaration is:\n+\n+    my %dpm_config_rules_2 = (\n+        \"ALLOW_COREDUMP\" => \"allowCoreDump:dpm;\".LINE_FORMAT_SH_VAR.\";\".LINE_VALUE_BOOLEAN,\n+        \"GLOBUS_THREAD_MODEL\" => \"globusThreadModel:dpm;\".LINE_FORMAT_ENV_VAR,\n+        \"DISKFLAGS\" =>\"DiskFlags:dpm;\".LINE_FORMAT_SH_VAR.\";\".LINE_VALUE_ARRAY,\n+       );\n+\n+For more comprehensive examples of rules, look at L<ncm-dpmlfc> or L<ncm-xrootd> source code in\n+configuration-modules-grid repository.\n+\n+=cut\n+\n+\n+package CAF::RuleBasedEditor;\n+\n+use strict;\n+use warnings;\n+use vars qw($EC);\n+$EC = LC::Exception::Context->new->will_store_all;\n+\n+use parent qw(CAF::FileEditor Exporter);\n+\n+use EDG::WP4::CCM::Element;\n+\n+use Readonly;\n+\n+use Encode qw(encode_utf8);\n+\n+# Constants from FileEditor\n+use CAF::FileEditor qw(BEGINNING_OF_FILE ENDING_OF_FILE);\n+\n+\n+=pod\n+\n+=head2 Rule Constants\n+\n+The constants described here are used to build the rules. All these\n+constants are exported. Add the following to use them:\n+\n+    use RuleBasedEditor qw(:rule_constants);\n+\n+There is a different group of constants for each part of the rule.\n+\n+\n+=head3  LINE_FORMAT_xxx: general syntax of the line\n+\n+=over\n+\n+=item *\n+\n+LINE_FORMAT_SH_VAR:         key=val (e.g. SH shell family)\n+\n+=item *\n+\n+LINE_FORMAT_ENV_VAR:        export key=val (e.g. SH shell family)\n+\n+=item *\n+\n+LINE_FORMAT_KEY_VAL:        key val (e.g. Xrootd, Apache)\n+\n+=item *\n+LINE_FORMAT_KEY_VAL_SETENV: setenv key val  (used by Xrootd in particular)\n+\n+=item *\n+\n+LINE_FORMAT_KEY_VAL_SET:    set key val  (used by Xrootd in particular)\n+\n+=back\n+\n+=cut\n+\n+use enum qw(\n+  LINE_FORMAT_SH_VAR=1\n+  LINE_FORMAT_ENV_VAR\n+  LINE_FORMAT_KEY_VAL\n+  LINE_FORMAT_KEY_VAL_SETENV\n+  LINE_FORMAT_KEY_VAL_SET\n+  );\n+\n+=pod\n+\n+=head3 \n+\n+LINE_VALUE_xxx: how to interpret the configuration value\n+\n+=over\n+\n+=item\n+\n+LINE_VALUE_AS_IS: take the value as it is, do not attempt any conversion\n+\n+=item\n+\n+LINE_VALUE_BOOLEAN: interpret the value as a boolean rendered as C<yes> or C<no>\n+\n+=item\n+\n+LINE_VALUE_ARRAY: the value is an array. Rendering controlled by LINE_VALUE_OPT_xxx constants.\n+\n+=item\n+\n+LINE_VALUE_HASH_KEYS: the value is hash whose keys are the value. Rendering similar to arrays.\n+\n+=item\n+\n+LINE_VALUE_STRING_HASH: the value is a hash of string. Rendering controlled by LINE_VALUE_OPT_xxx constants.\n+\n+=item\n+\n+LINE_VALUE_INSTANCE_PARAMS: specific to L<ncm-xrootd>\n+\n+=back\n+\n+=cut\n+\n+use enum qw(\n+  LINE_VALUE_AS_IS\n+  LINE_VALUE_BOOLEAN\n+  LINE_VALUE_ARRAY\n+  LINE_VALUE_HASH_KEYS\n+  LINE_VALUE_STRING_HASH\n+  LINE_VALUE_INSTANCE_PARAMS\n+  );\n+\n+=pod\n+\n+=head3 LINE_VALUE_OPT_xxx: options for rendering the value\n+\n+These options mainly apply to lists and hashes and are interpreted as a bitmask.\n+\n+=over\n+\n+=item\n+\n+LINE_VALUE_OPT_SINGLE: each value must be a separate instance of the keyword (multiple lines)\n+\n+=item\n+\n+LINE_VALUE_OPT_UNIQUE: each values are concataneted as a space-separated string\n+\n+=item\n+\n+LINE_VALUE_OPT_SORTED: values are sorted\n+\n+=back\n+\n+=cut\n+\n+use enum qw(\n+  BITMASK: LINE_VALUE_OPT_SINGLE\n+  LINE_VALUE_OPT_UNIQUE\n+  LINE_VALUE_OPT_SORTED\n+  );\n+\n+# Internal constants\n+Readonly my $LINE_FORMAT_DEFAULT            => LINE_FORMAT_SH_VAR;\n+Readonly my $LINE_QUATTOR_COMMENT           => \"\\t\\t# Line generated by Quattor\";\n+Readonly my $LINE_OPT_DEF_REMOVE_IF_UNDEF   => 0;\n+Readonly my $LINE_OPT_DEF_ALWAYS_RULES_ONLY => 0;\n+Readonly my $RULE_CONDITION_ALWAYS          => 'ALWAYS';\n+Readonly my $RULE_OPTION_SET_GLOBAL         => 'GLOBAL';\n+\n+\n+# Export constants used to build rules\n+# Needs to be updated when a constant is added or removed\n+Readonly my @RULE_CONSTANTS => qw(\n+  LINE_FORMAT_SH_VAR\n+  LINE_FORMAT_ENV_VAR\n+  LINE_FORMAT_KEY_VAL\n+  LINE_FORMAT_KEY_VAL_SETENV\n+  LINE_FORMAT_KEY_VAL_SET\n+  LINE_VALUE_AS_IS\n+  LINE_VALUE_BOOLEAN\n+  LINE_VALUE_INSTANCE_PARAMS\n+  LINE_VALUE_ARRAY\n+  LINE_VALUE_HASH_KEYS\n+  LINE_VALUE_STRING_HASH\n+  LINE_VALUE_OPT_SINGLE\n+  LINE_VALUE_OPT_UNIQUE\n+  LINE_VALUE_OPT_SORTED\n+  );\n+\n+\n+our @EXPORT_OK;\n+our %EXPORT_TAGS;\n+push @EXPORT_OK, @RULE_CONSTANTS;\n+$EXPORT_TAGS{rule_constants} = \\@RULE_CONSTANTS;\n+\n+\n+# Backup file extension\n+Readonly my $BACKUP_FILE_EXT => \".old\";\n+\n+=pod\n+\n+=head2 Public methods\n+\n+=over\n+\n+=item updateFile\n+\n+Update configuration file contents,  applying configuration rules.\n+\n+Arguments :\n+    config_rules: config rules corresponding to the file to build\n+    config_options: configuration parameters used to build actual configuration\n+    options: a hash setting options to modify the behaviour of this function\n+\n+Supported entries for options hash:\n+    always_rules_only: if true, apply only rules with ALWAYS condition (D: false)\n+    remove_if_undef: if true, remove matching configuration line is rule condition is not met (D: false)\n+\n+Return value\n+    sucess: 1\n+    argument error: undef\n+    \n+=cut\n+\n+sub updateFile\n+{\n+    my $function_name = \"updateConfigFile\";\n+    my ($self, $config_rules, $config_options, $parser_options) = @_;\n+\n+    unless ($config_rules) {\n+        *$self->{LOG}->error(\"$function_name: 'config_rules' argument missing (internal error)\");\n+        return;\n+    }\n+    unless ($config_options) {\n+        *$self->{LOG}->error(\"$function_name: 'config_options' argument missing (internal error)\");\n+        return;\n+    }\n+    unless (defined($parser_options)) {\n+        *$self->{LOG}->debug(2, \"$function_name: 'parser_options' undefined\");\n+        $parser_options = {};\n+    }\n+\n+    $self->seek_begin();\n+\n+    # Check that config file has an appropriate header\n+    Readonly my $INTRO_PATTERN => \"# This file is managed by Quattor\";\n+    my $intro = \"# This file is managed by Quattor - DO NOT EDIT lines generated by Quattor\";\n+    $self->add_or_replace_lines(\n+                                qr/^$INTRO_PATTERN/,\n+                                qr/^$intro$/,\n+                                $intro . \"\\n#\\n\",\n+                                BEGINNING_OF_FILE,\n+                               );\n+\n+    $self->_apply_rules(\n+                        $config_rules,\n+                        $config_options,\n+                        $parser_options\n+                       );\n+\n+    return 1;\n+}\n+\n+\n+=pod\n+\n+=back\n+\n+=head2 Private methods\n+\n+=over\n+\n+=item formatAttrValue\n+\n+This function formats an attribute value based on the value format specified.\n+\n+Arguments:\n+    attr_value : attribue value\n+    line_fmt : line format (see LINE_FORMAT_xxx constants)\n+    value_fmt : value format (see LINE_VALUE_xxx constants)\n+    value_opt : value interpretation/formatting options (bitmask, see LINE_VALUE_OPT_xxx constants)\n+\n+Return value:\n+    A string corresponding to the value formatted according to the format specified by arguments\n+    or undef in case of an internal error (missing arguments)\n+\n+=cut\n+\n+sub _formatAttributeValue\n+{\n+    my $function_name = \"_formatAttributeValue\";\n+    my ($self, $attr_value, $line_fmt, $value_fmt, $value_opt) = @_;\n+\n+    unless (defined($attr_value)) {\n+        *$self->{LOG}->error(\"$function_name: 'attr_value' argument missing (internal error)\");\n+        return undef;\n+    }\n+    unless (defined($line_fmt)) {\n+        *$self->{LOG}->error(\"$function_name: 'list_fmt' argument missing (internal error)\");\n+        return undef;\n+    }\n+    unless (defined($value_fmt)) {\n+        *$self->{LOG}->error(\"$function_name: 'value_fmt' argument missing (internal error)\");\n+        return undef;\n+    }\n+    unless (defined($value_opt)) {\n+        *$self->{LOG}->error(\"$function_name: 'value_opt' argument missing (internal error)\");\n+        return undef;\n+    }\n+\n+    *$self->{LOG}->debug(2,\n+                         \"$function_name: formatting attribute value >>>$attr_value<<< (line fmt=$line_fmt, value fmt=$value_fmt, value_opt=$value_opt)\"\n+                        );\n+\n+    my $formatted_value;\n+    if ($value_fmt == LINE_VALUE_BOOLEAN) {\n+        $formatted_value = $attr_value ? 'yes' : 'no';\n+\n+    } elsif ($value_fmt == LINE_VALUE_INSTANCE_PARAMS) {\n+        $formatted_value = '';    # Don't return undef if no matching attributes is found\n+                                  # Instance parameters are described in a nlist\n+        $formatted_value .= \" -l $attr_value->{logFile}\"    if $attr_value->{logFile};\n+        $formatted_value .= \" -c $attr_value->{configFile}\" if $attr_value->{configFile};\n+        $formatted_value .= \" -k $attr_value->{logKeep}\"    if $attr_value->{logKeep};\n+\n+    } elsif ($value_fmt == LINE_VALUE_ARRAY) {\n+        *$self->{LOG}->debug(2, \"$function_name: array values received: \", join(\",\", @$attr_value));\n+        if ($value_opt & LINE_VALUE_OPT_UNIQUE) {\n+            my %values = map(($_ => 1), @$attr_value);\n+            $attr_value = [keys(%values)];\n+            *$self->{LOG}->debug(2, \"$function_name: array values made unique: \", join(\",\", @$attr_value));\n+        }\n+        # LINE_VALUE_OPT_UNIQUE implies LINE_VALUE_OPT_SORTED\n+        if ($value_opt & (LINE_VALUE_OPT_UNIQUE | LINE_VALUE_OPT_SORTED)) {\n+            $attr_value = [sort(@$attr_value)] if $value_opt & (LINE_VALUE_OPT_UNIQUE | LINE_VALUE_OPT_SORTED);\n+            *$self->{LOG}->debug(2, \"$function_name: array values sorted: \", join(\",\", @$attr_value));\n+        }\n+        $formatted_value = join \" \", @$attr_value;\n+\n+    } elsif ($value_fmt == LINE_VALUE_HASH_KEYS) {\n+        $formatted_value = join \" \", sort keys %$attr_value;\n+\n+    } elsif (($value_fmt == LINE_VALUE_AS_IS) || ($value_fmt == LINE_VALUE_STRING_HASH)) {\n+        $formatted_value = $attr_value;\n+\n+    } else {\n+        *$self->{LOG}->error(\"$function_name: invalid value format ($value_fmt) (internal error)\");\n+    }\n+\n+    # Quote value if necessary\n+    if (($line_fmt == LINE_FORMAT_SH_VAR) || ($line_fmt == LINE_FORMAT_ENV_VAR)) {\n+        if (   (($formatted_value =~ /\\s+/) && ($formatted_value !~ /^([\"']).*\\g1$/))\n+            || ($value_fmt == LINE_VALUE_BOOLEAN)\n+            || ($formatted_value eq ''))\n+        {\n+            *$self->{LOG}->debug(2, \"$function_name: quoting value '$formatted_value'\");\n+            $formatted_value = '\"' . $formatted_value . '\"';\n+        }\n+    }\n+\n+    *$self->{LOG}->debug(2, \"$function_name: formatted value >>>$formatted_value<<<\");\n+    return $formatted_value;\n+}\n+\n+\n+=pod\n+\n+=item _formatConfigLine\n+\n+This function formats a configuration line using keyword and value,\n+according to the line format requested. Values containing spaces are\n+quoted if the line format is not LINE_FORMAT_KEY_VAL.\n+\n+Arguments :\n+    keyword : line keyword\n+    value : keyword value (can be empty)\n+    line_fmt : line format (see LINE_FORMAT_xxx constants)\n+\n+Return value:\n+    A string corresponding to the line formatted according to line_fmt\n+    or undef in case of an internal error (missing arguments)\n+\n+=cut\n+\n+sub _formatConfigLine\n+{\n+    my $function_name = \"_formatConfigLine\";\n+    my ($self, $keyword, $value, $line_fmt) = @_;\n+\n+    unless ($keyword) {\n+        *$self->{LOG}->error(\"$function_name: 'keyword' argument missing (internal error)\");\n+        return undef;\n+    }\n+    unless (defined($value)) {\n+        *$self->{LOG}->error(\"$function_name: 'value' argument missing (internal error)\");\n+        return undef;\n+    }\n+    unless (defined($line_fmt)) {\n+        *$self->{LOG}->error(\"$function_name: 'line_fmt' argument missing (internal error)\");\n+        return undef;\n+    }\n+\n+    my $config_line = \"\";\n+\n+    if ($line_fmt == LINE_FORMAT_SH_VAR) {\n+        $config_line = \"$keyword=$value\";\n+    } elsif ($line_fmt == LINE_FORMAT_ENV_VAR) {\n+        $config_line = \"export $keyword=$value\";\n+    } elsif ($line_fmt == LINE_FORMAT_KEY_VAL_SETENV) {\n+        $config_line = \"setenv $keyword = $value\";\n+    } elsif ($line_fmt == LINE_FORMAT_KEY_VAL_SET) {\n+        $config_line = \"set $keyword = $value\";\n+    } elsif ($line_fmt == LINE_FORMAT_KEY_VAL) {\n+        $config_line = $keyword;\n+        $config_line .= \" $value\" if $value;\n+        # In trust (shift.conf) format, there should be only one blank between\n+        # tokens and no trailing spaces.\n+        $config_line =~ s/\\s\\s+/ /g;\n+        $config_line =~ s/\\s+$//;\n+    } else {\n+        *$self->{LOG}->error(\"$function_name: invalid line format ($line_fmt). Internal inconsistency.\");\n+    }\n+\n+    *$self->{LOG}->debug(2, \"$function_name: Configuration line : >>$config_line<<\");\n+    return $config_line;\n+}\n+\n+\n+=pod\n+\n+=item _buildLinePattern\n+\n+This function builds a pattern that will match an existing configuration line for\n+the configuration parameter specified. The pattern built takes into account the line format.\n+Every whitespace in the pattern (configuration parameter) are replaced by \\s+.\n+If the line format is LINE_FORMAT_KEY_VAL, no whitespace is\n+imposed at the end of the pattern, as these format can be used to write a configuration\n+directive as a keyword with no value.\n+\n+Arguments :\n+    config_param: parameter to update\n+    line_fmt: line format (see LINE_FORMAT_xxx constants)\n+    config_value: when defined, make it part of the pattern (used when multiple lines\n+                  with the same keyword are allowed)\n+\n+Return value:\n+    A string containing the pattern to use to match the line in the file or undef\n+    in case of an internal error (missing argument).\n+\n+=cut\n+\n+sub _buildLinePattern\n+{\n+    my $function_name = \"_buildLinePattern\";\n+    my ($self, $config_param, $line_fmt, $config_value) = @_;\n+\n+    unless ($config_param) {\n+        *$self->{LOG}->error(\"$function_name: 'config_param' argument missing (internal error)\");\n+        return undef;\n+    }\n+    unless (defined($line_fmt)) {\n+        *$self->{LOG}->error(\"$function_name: 'line_fmt' argument missing (internal error)\");\n+        return undef;\n+    }\n+    if (defined($config_value)) {\n+        *$self->{LOG}->debug(2, \"$function_name: configuration value '$config_value' will be added to the pattern\");\n+        $config_value =~ s/\\\\/\\\\\\\\/g;\n+        $config_value =~ s/([\\-\\+\\?\\.\\*\\[\\]()\\^\\$])/\\\\$1/g;\n+        $config_value =~ s/\\s+/\\\\s+/g;\n+    } else {\n+        $config_value = \"\";\n+    }\n+\n+    # config_param is generally a keyword and in this case it contains no whitespace.\n+    # A special case is when config_param (the rule keyword) is used to match a line\n+    # without specifying a rule: in this case it may contains whitespaces. Remove strict\n+    # matching of them (match any type/number of whitespaces at the same position).\n+    # Look at %trust_config_rules in ncm-dpmlfc Perl module for an example.\n+    $config_param =~ s/\\s+/\\\\s+/g;\n+\n+    my $config_param_pattern;\n+    if ($line_fmt == LINE_FORMAT_SH_VAR) {\n+        $config_param_pattern = \"#?\\\\s*$config_param=\" . $config_value;\n+    } elsif ($line_fmt == LINE_FORMAT_ENV_VAR) {\n+        $config_param_pattern = \"#?\\\\s*export $config_param=\" . $config_value;\n+    } elsif ($line_fmt == LINE_FORMAT_KEY_VAL_SETENV) {\n+        $config_param_pattern = \"#?\\\\s*setenv\\\\s+$config_param\\\\s*=\\\\s*\" . $config_value;\n+    } elsif ($line_fmt == LINE_FORMAT_KEY_VAL_SET) {\n+        $config_param_pattern = \"#?\\\\s*set\\\\s+$config_param\\\\s*=\\\\s*\" . $config_value;\n+    } elsif ($line_fmt == LINE_FORMAT_KEY_VAL) {\n+        $config_param_pattern = \"#?\\\\s*$config_param\";\n+        # Avoid adding a whitespace requirement if there is no config_value\n+        if ($config_value ne \"\") {\n+            $config_param_pattern .= \"\\\\s+\" . $config_value;\n+        }\n+    } else {\n+        *$self->{LOG}->error(\"$function_name: invalid line format ($line_fmt). Internal inconsistency.\");\n+        return undef;\n+    }\n+\n+    return $config_param_pattern;\n+}\n+\n+\n+=pod\n+\n+=item _removeConfigLine\n+\n+This function comments out a configuration line matching the configuration parameter.\n+Match operation takes into account the line format.\n+\n+Arguments :\n+    config_param: parameter to update\n+    line_fmt : line format (see LINE_FORMAT_xxx constants)\n+\n+Return value:\n+    None or 1 in case of an internal error (missing argument)\n+\n+=cut\n+\n+sub _removeConfigLine\n+{\n+    my $function_name = \"_removeConfigLine\";\n+    my ($self, $config_param, $line_fmt) = @_;\n+\n+    unless ($config_param) {\n+        *$self->{LOG}->error(\"$function_name: 'config_param' argument missing (internal error)\");\n+        return 1;\n+    }\n+    unless (defined($line_fmt)) {\n+        *$self->{LOG}->error(\"$function_name: 'line_fmt' argument missing (internal error)\");\n+        return 1;\n+    }\n+\n+    # Build a pattern to look for.\n+    my $config_param_pattern = $self->_buildLinePattern($config_param, $line_fmt);\n+\n+    *$self->{LOG}->debug(1, \"$function_name: commenting out lines matching pattern >>>\" . $config_param_pattern . \"<<<\");\n+    # All matching lines must be commented out, except if they are already commented out.\n+    # The code used is a customized version of FileEditor::replace() that lacks support for backreferences\n+    # in the replacement value (here we want to rewrite the same line commented out but we don't know the\n+    # current line contents, only a regexp matching it).\n+    my @lns;\n+    my $line_count = 0;\n+    $self->seek_begin();\n+    while (my $l = <$self>) {\n+        if ($l =~ qr/^$config_param_pattern/ && $l !~ qr/^\\s*#/) {\n+            *$self->{LOG}->debug(2, \"$function_name: commenting out matching line >>>\" . $l . \"<<<\");\n+            $line_count++;\n+            push(@lns, '#' . $l);\n+        } else {\n+            push(@lns, $l);\n+        }\n+    }\n+    if ($line_count == 0) {\n+        *$self->{LOG}->debug(1, \"$function_name: No line found matching the pattern\");\n+    } else {\n+        *$self->{LOG}->debug(1, \"$function_name: $line_count lines commented out\");\n+    }\n+    $self->set_contents(join(\"\", @lns));\n+\n+}\n+\n+\n+=pod\n+\n+=item _updateConfigLine\n+\n+This function does the actual update of a configuration line after doing the final\n+line formatting based on the line format.\n+\n+Arguments :\n+    config_param: parameter to update\n+    config_value : parameter value (can be empty)\n+    line_fmt : line format (see LINE_FORMAT_xxx constants)\n+    multiple : if true, multiple lines with the same keyword can exist (D: false)\n+\n+Return value:\n+    None or 1 in case of an internal error (missing argument)\n+\n+=cut\n+\n+sub _updateConfigLine\n+{\n+    my $function_name = \"_updateConfigLine\";\n+    my ($self, $config_param, $config_value, $line_fmt, $multiple) = @_;\n+\n+    unless ($config_param) {\n+        *$self->{LOG}->error(\"$function_name: 'config_param' argument missing (internal error)\");\n+        return 1;\n+    }\n+    unless (defined($config_value)) {\n+        *$self->{LOG}->error(\"$function_name: 'config_value' argument missing (internal error)\");\n+        return 1;\n+    }\n+    unless (defined($line_fmt)) {\n+        *$self->{LOG}->error(\"$function_name: 'line_fmt' argument missing (internal error)\");\n+        return 1;\n+    }\n+    unless (defined($multiple)) {\n+        $multiple = 0;\n+    }\n+\n+    my $config_param_pattern;\n+    my $new_line = $self->_formatConfigLine($config_param, $config_value, $line_fmt);","path":"src/main/perl/RuleBasedEditor.pm","position":704,"original_position":704,"commit_id":"80ff77dcc325db9d5d3b97d625b0b7dd7e3f6870","original_commit_id":"80ff77dcc325db9d5d3b97d625b0b7dd7e3f6870","user":{"login":"stdweird","id":1517606,"avatar_url":"https://avatars.githubusercontent.com/u/1517606?v=3","gravatar_id":"","url":"https://api.github.com/users/stdweird","html_url":"https://github.com/stdweird","followers_url":"https://api.github.com/users/stdweird/followers","following_url":"https://api.github.com/users/stdweird/following{/other_user}","gists_url":"https://api.github.com/users/stdweird/gists{/gist_id}","starred_url":"https://api.github.com/users/stdweird/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stdweird/subscriptions","organizations_url":"https://api.github.com/users/stdweird/orgs","repos_url":"https://api.github.com/users/stdweird/repos","events_url":"https://api.github.com/users/stdweird/events{/privacy}","received_events_url":"https://api.github.com/users/stdweird/received_events","type":"User","site_admin":false},"body":"handle undef return value","created_at":"2016-05-05T21:01:04Z","updated_at":"2016-05-05T21:01:04Z","html_url":"https://github.com/quattor/CAF/pull/151#discussion_r62254502","pull_request_url":"https://api.github.com/repos/quattor/CAF/pulls/151","_links":{"self":{"href":"https://api.github.com/repos/quattor/CAF/pulls/comments/62254502"},"html":{"href":"https://github.com/quattor/CAF/pull/151#discussion_r62254502"},"pull_request":{"href":"https://api.github.com/repos/quattor/CAF/pulls/151"}}},"pull_request":{"url":"https://api.github.com/repos/quattor/CAF/pulls/151","id":67614137,"html_url":"https://github.com/quattor/CAF/pull/151","diff_url":"https://github.com/quattor/CAF/pull/151.diff","patch_url":"https://github.com/quattor/CAF/pull/151.patch","issue_url":"https://api.github.com/repos/quattor/CAF/issues/151","number":151,"state":"open","locked":false,"title":"Rule based editor","user":{"login":"jouvin","id":1511937,"avatar_url":"https://avatars.githubusercontent.com/u/1511937?v=3","gravatar_id":"","url":"https://api.github.com/users/jouvin","html_url":"https://github.com/jouvin","followers_url":"https://api.github.com/users/jouvin/followers","following_url":"https://api.github.com/users/jouvin/following{/other_user}","gists_url":"https://api.github.com/users/jouvin/gists{/gist_id}","starred_url":"https://api.github.com/users/jouvin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jouvin/subscriptions","organizations_url":"https://api.github.com/users/jouvin/orgs","repos_url":"https://api.github.com/users/jouvin/repos","events_url":"https://api.github.com/users/jouvin/events{/privacy}","received_events_url":"https://api.github.com/users/jouvin/received_events","type":"User","site_admin":false},"body":"This PR imports into `CAF` the rule-based file editor originally developed as part of `ncm-dpmlfc` and `ncm-xrootd`. It intends to address #123.\r\n\r\nI'll try to complete the import and constant name cleanup during the week-end. I'd appreciate if this code could go into 16.4 release to help further developments, in particular its use in `ncm-dpmlfc` and `ncm-xrootd`. Except if I missed something, it should have no impact on current FileEditor usage as it just adds new methods but don't change anything in the existing code (apart including the module containing the new code).","created_at":"2016-04-23T18:13:57Z","updated_at":"2016-05-05T21:01:04Z","closed_at":null,"merged_at":null,"merge_commit_sha":"18e15fffeb40206c76968d0c6d5fd4a984bc51b0","assignee":null,"milestone":{"url":"https://api.github.com/repos/quattor/CAF/milestones/11","html_url":"https://github.com/quattor/CAF/milestones/16.4","labels_url":"https://api.github.com/repos/quattor/CAF/milestones/11/labels","id":1147834,"number":11,"title":"16.4","description":null,"creator":{"login":"jrha","id":323309,"avatar_url":"https://avatars.githubusercontent.com/u/323309?v=3","gravatar_id":"","url":"https://api.github.com/users/jrha","html_url":"https://github.com/jrha","followers_url":"https://api.github.com/users/jrha/followers","following_url":"https://api.github.com/users/jrha/following{/other_user}","gists_url":"https://api.github.com/users/jrha/gists{/gist_id}","starred_url":"https://api.github.com/users/jrha/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jrha/subscriptions","organizations_url":"https://api.github.com/users/jrha/orgs","repos_url":"https://api.github.com/users/jrha/repos","events_url":"https://api.github.com/users/jrha/events{/privacy}","received_events_url":"https://api.github.com/users/jrha/received_events","type":"User","site_admin":false},"open_issues":7,"closed_issues":9,"state":"open","created_at":"2015-06-03T16:16:32Z","updated_at":"2016-05-05T15:07:03Z","due_on":"2016-05-13T07:00:00Z","closed_at":null},"commits_url":"https://api.github.com/repos/quattor/CAF/pulls/151/commits","review_comments_url":"https://api.github.com/repos/quattor/CAF/pulls/151/comments","review_comment_url":"https://api.github.com/repos/quattor/CAF/pulls/comments{/number}","comments_url":"https://api.github.com/repos/quattor/CAF/issues/151/comments","statuses_url":"https://api.github.com/repos/quattor/CAF/statuses/80ff77dcc325db9d5d3b97d625b0b7dd7e3f6870","head":{"label":"jouvin:rule-based-editor","ref":"rule-based-editor","sha":"80ff77dcc325db9d5d3b97d625b0b7dd7e3f6870","user":{"login":"jouvin","id":1511937,"avatar_url":"https://avatars.githubusercontent.com/u/1511937?v=3","gravatar_id":"","url":"https://api.github.com/users/jouvin","html_url":"https://github.com/jouvin","followers_url":"https://api.github.com/users/jouvin/followers","following_url":"https://api.github.com/users/jouvin/following{/other_user}","gists_url":"https://api.github.com/users/jouvin/gists{/gist_id}","starred_url":"https://api.github.com/users/jouvin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jouvin/subscriptions","organizations_url":"https://api.github.com/users/jouvin/orgs","repos_url":"https://api.github.com/users/jouvin/repos","events_url":"https://api.github.com/users/jouvin/events{/privacy}","received_events_url":"https://api.github.com/users/jouvin/received_events","type":"User","site_admin":false},"repo":{"id":21414660,"name":"CAF","full_name":"jouvin/CAF","owner":{"login":"jouvin","id":1511937,"avatar_url":"https://avatars.githubusercontent.com/u/1511937?v=3","gravatar_id":"","url":"https://api.github.com/users/jouvin","html_url":"https://github.com/jouvin","followers_url":"https://api.github.com/users/jouvin/followers","following_url":"https://api.github.com/users/jouvin/following{/other_user}","gists_url":"https://api.github.com/users/jouvin/gists{/gist_id}","starred_url":"https://api.github.com/users/jouvin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jouvin/subscriptions","organizations_url":"https://api.github.com/users/jouvin/orgs","repos_url":"https://api.github.com/users/jouvin/repos","events_url":"https://api.github.com/users/jouvin/events{/privacy}","received_events_url":"https://api.github.com/users/jouvin/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/jouvin/CAF","description":"Perl Common Application Framework","fork":true,"url":"https://api.github.com/repos/jouvin/CAF","forks_url":"https://api.github.com/repos/jouvin/CAF/forks","keys_url":"https://api.github.com/repos/jouvin/CAF/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jouvin/CAF/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jouvin/CAF/teams","hooks_url":"https://api.github.com/repos/jouvin/CAF/hooks","issue_events_url":"https://api.github.com/repos/jouvin/CAF/issues/events{/number}","events_url":"https://api.github.com/repos/jouvin/CAF/events","assignees_url":"https://api.github.com/repos/jouvin/CAF/assignees{/user}","branches_url":"https://api.github.com/repos/jouvin/CAF/branches{/branch}","tags_url":"https://api.github.com/repos/jouvin/CAF/tags","blobs_url":"https://api.github.com/repos/jouvin/CAF/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jouvin/CAF/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jouvin/CAF/git/refs{/sha}","trees_url":"https://api.github.com/repos/jouvin/CAF/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jouvin/CAF/statuses/{sha}","languages_url":"https://api.github.com/repos/jouvin/CAF/languages","stargazers_url":"https://api.github.com/repos/jouvin/CAF/stargazers","contributors_url":"https://api.github.com/repos/jouvin/CAF/contributors","subscribers_url":"https://api.github.com/repos/jouvin/CAF/subscribers","subscription_url":"https://api.github.com/repos/jouvin/CAF/subscription","commits_url":"https://api.github.com/repos/jouvin/CAF/commits{/sha}","git_commits_url":"https://api.github.com/repos/jouvin/CAF/git/commits{/sha}","comments_url":"https://api.github.com/repos/jouvin/CAF/comments{/number}","issue_comment_url":"https://api.github.com/repos/jouvin/CAF/issues/comments{/number}","contents_url":"https://api.github.com/repos/jouvin/CAF/contents/{+path}","compare_url":"https://api.github.com/repos/jouvin/CAF/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jouvin/CAF/merges","archive_url":"https://api.github.com/repos/jouvin/CAF/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jouvin/CAF/downloads","issues_url":"https://api.github.com/repos/jouvin/CAF/issues{/number}","pulls_url":"https://api.github.com/repos/jouvin/CAF/pulls{/number}","milestones_url":"https://api.github.com/repos/jouvin/CAF/milestones{/number}","notifications_url":"https://api.github.com/repos/jouvin/CAF/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jouvin/CAF/labels{/name}","releases_url":"https://api.github.com/repos/jouvin/CAF/releases{/id}","deployments_url":"https://api.github.com/repos/jouvin/CAF/deployments","created_at":"2014-07-02T07:05:57Z","updated_at":"2016-04-23T15:26:31Z","pushed_at":"2016-05-04T16:50:31Z","git_url":"git://github.com/jouvin/CAF.git","ssh_url":"git@github.com:jouvin/CAF.git","clone_url":"https://github.com/jouvin/CAF.git","svn_url":"https://github.com/jouvin/CAF","homepage":"www.quattor.org","size":614,"stargazers_count":0,"watchers_count":0,"language":"Perl","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"quattor:master","ref":"master","sha":"6b587a4e7b19234d1924b09fe30d6543e40245df","user":{"login":"quattor","id":1557169,"avatar_url":"https://avatars.githubusercontent.com/u/1557169?v=3","gravatar_id":"","url":"https://api.github.com/users/quattor","html_url":"https://github.com/quattor","followers_url":"https://api.github.com/users/quattor/followers","following_url":"https://api.github.com/users/quattor/following{/other_user}","gists_url":"https://api.github.com/users/quattor/gists{/gist_id}","starred_url":"https://api.github.com/users/quattor/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/quattor/subscriptions","organizations_url":"https://api.github.com/users/quattor/orgs","repos_url":"https://api.github.com/users/quattor/repos","events_url":"https://api.github.com/users/quattor/events{/privacy}","received_events_url":"https://api.github.com/users/quattor/received_events","type":"Organization","site_admin":false},"repo":{"id":3835363,"name":"CAF","full_name":"quattor/CAF","owner":{"login":"quattor","id":1557169,"avatar_url":"https://avatars.githubusercontent.com/u/1557169?v=3","gravatar_id":"","url":"https://api.github.com/users/quattor","html_url":"https://github.com/quattor","followers_url":"https://api.github.com/users/quattor/followers","following_url":"https://api.github.com/users/quattor/following{/other_user}","gists_url":"https://api.github.com/users/quattor/gists{/gist_id}","starred_url":"https://api.github.com/users/quattor/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/quattor/subscriptions","organizations_url":"https://api.github.com/users/quattor/orgs","repos_url":"https://api.github.com/users/quattor/repos","events_url":"https://api.github.com/users/quattor/events{/privacy}","received_events_url":"https://api.github.com/users/quattor/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/quattor/CAF","description":"Perl Common Application Framework","fork":false,"url":"https://api.github.com/repos/quattor/CAF","forks_url":"https://api.github.com/repos/quattor/CAF/forks","keys_url":"https://api.github.com/repos/quattor/CAF/keys{/key_id}","collaborators_url":"https://api.github.com/repos/quattor/CAF/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/quattor/CAF/teams","hooks_url":"https://api.github.com/repos/quattor/CAF/hooks","issue_events_url":"https://api.github.com/repos/quattor/CAF/issues/events{/number}","events_url":"https://api.github.com/repos/quattor/CAF/events","assignees_url":"https://api.github.com/repos/quattor/CAF/assignees{/user}","branches_url":"https://api.github.com/repos/quattor/CAF/branches{/branch}","tags_url":"https://api.github.com/repos/quattor/CAF/tags","blobs_url":"https://api.github.com/repos/quattor/CAF/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/quattor/CAF/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/quattor/CAF/git/refs{/sha}","trees_url":"https://api.github.com/repos/quattor/CAF/git/trees{/sha}","statuses_url":"https://api.github.com/repos/quattor/CAF/statuses/{sha}","languages_url":"https://api.github.com/repos/quattor/CAF/languages","stargazers_url":"https://api.github.com/repos/quattor/CAF/stargazers","contributors_url":"https://api.github.com/repos/quattor/CAF/contributors","subscribers_url":"https://api.github.com/repos/quattor/CAF/subscribers","subscription_url":"https://api.github.com/repos/quattor/CAF/subscription","commits_url":"https://api.github.com/repos/quattor/CAF/commits{/sha}","git_commits_url":"https://api.github.com/repos/quattor/CAF/git/commits{/sha}","comments_url":"https://api.github.com/repos/quattor/CAF/comments{/number}","issue_comment_url":"https://api.github.com/repos/quattor/CAF/issues/comments{/number}","contents_url":"https://api.github.com/repos/quattor/CAF/contents/{+path}","compare_url":"https://api.github.com/repos/quattor/CAF/compare/{base}...{head}","merges_url":"https://api.github.com/repos/quattor/CAF/merges","archive_url":"https://api.github.com/repos/quattor/CAF/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/quattor/CAF/downloads","issues_url":"https://api.github.com/repos/quattor/CAF/issues{/number}","pulls_url":"https://api.github.com/repos/quattor/CAF/pulls{/number}","milestones_url":"https://api.github.com/repos/quattor/CAF/milestones{/number}","notifications_url":"https://api.github.com/repos/quattor/CAF/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/quattor/CAF/labels{/name}","releases_url":"https://api.github.com/repos/quattor/CAF/releases{/id}","deployments_url":"https://api.github.com/repos/quattor/CAF/deployments","created_at":"2012-03-26T17:26:06Z","updated_at":"2015-05-08T10:10:08Z","pushed_at":"2016-05-05T14:36:01Z","git_url":"git://github.com/quattor/CAF.git","ssh_url":"git@github.com:quattor/CAF.git","clone_url":"https://github.com/quattor/CAF.git","svn_url":"https://github.com/quattor/CAF","homepage":"www.quattor.org","size":996,"stargazers_count":4,"watchers_count":4,"language":"Perl","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":10,"mirror_url":null,"open_issues_count":26,"forks":10,"open_issues":26,"watchers":4,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/quattor/CAF/pulls/151"},"html":{"href":"https://github.com/quattor/CAF/pull/151"},"issue":{"href":"https://api.github.com/repos/quattor/CAF/issues/151"},"comments":{"href":"https://api.github.com/repos/quattor/CAF/issues/151/comments"},"review_comments":{"href":"https://api.github.com/repos/quattor/CAF/pulls/151/comments"},"review_comment":{"href":"https://api.github.com/repos/quattor/CAF/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/quattor/CAF/pulls/151/commits"},"statuses":{"href":"https://api.github.com/repos/quattor/CAF/statuses/80ff77dcc325db9d5d3b97d625b0b7dd7e3f6870"}}}}