[augeas-devel] Apt.conf lens

Raphaël Pinson raphink at gmail.com
Fri Nov 18 12:32:45 UTC 2011


Hello,


A few months ago, I started working on a lens for apt.conf(.d/*)?
files. This is a recursive lens, and it took me quite a lot of time to
get it working.
The working lens (and the test file) are pasted at the end of this email.

One problem I've had with this lens is that apt.conf has two ways of
defining subnodes, so that

APT::Clean-Installed "true";

is equivalent to

APT {
  Clean-Installed "true";
}

The first approach I tried was to not use "::" as a separator, and use
"APT::Clean-Installed" as a node label in the first case. This works,
but doesn't really help with parsing, since you never know which
syntax the configuration uses (and there can be more than 2 levels, so
it can get _really_ messy), so the first case is parsed as:

{ "APT::Clean-Installed" = "true" }

while the second syntax is parsed as:

 { "APT" { "Clean-Installed" = "true" } }


Another approach is to consider "::" as a separator, and map the two
syntaxes in the exact same fashion. Obviously, that leads to an
ambiguity in the put direction, since the { "APT" { "Clean-Installed"
= "true" } } tree can be mapped back to two different syntaxes.
Since this is a recursive lens, this kind of ambiguity is not raised
however, and the lens works fine this way, defaulting to the second
syntax because it is the one that supports all of the options (and for
that reason, I list it first in the union). There is just one little
detail: it modifies the existing syntax when the tree is changed.

So, for example,

APT::Clean-Installed "true";

is mapped to

 { "APT" { "Clean-Installed" = "true" } }

but if we then do a set /APT/Clean-Installed "false" and save, we get:

APT {
  Clean-Installed "false";
};

which is perfectly correct, but modifies the logic that was previously
chosen. I can't think of a way to avoid this, and I'm very reluctant
on giving up on parsing the "::" syntax as subnodes since it really
improves the user interface.


What do you guys think?




The lens:
===============================

(*
Module: AptConf
  Parses /etc/apt/apt.conf and /etc/apt/apt.conf.d/*

Author: Raphael Pinson <raphink at gmail.com>

About: Reference
  This lens tries to keep as close as possible to `man 5 apt.conf`
where possible.

About: License
   This file is licenced under the LGPLv2+, like the rest of Augeas.

About: Lens Usage
   To be documented

About: Configuration files
   This lens applies to /etc/apt/apt.conf and /etc/apt/apt.conf.d/*.
See <filter>.
*)


module AptConf =
  autoload xfm

(************************************************************************
 * Group:                 USEFUL PRIMITIVES
 *************************************************************************)

(* View: eol
    And <Util.eol> end of line *)
let eol = Util.eol

(* View: empty
    A C-style empty line *)
let empty = Util.empty_c_style

(* View: indent
    An indentation *)
let indent = Util.indent

(* View: comment_simple
    A one-line comment, C-style *)
let comment_simple = Util.comment_c_style

(* View: comment_multi
    A multiline comment, C-style *)
let comment_multi = Util.comment_multiline

(* View: comment
    A comment, either <comment_simple> or <comment_multi> *)
let comment = comment_simple | comment_multi


(************************************************************************
 * Group:                 ENTRIES
 *************************************************************************)

(* View: name_re
    Regex for entry names *)
let name_re = /[A-Za-z][A-Za-z-]*/

(* View: name_re_colons
    Regex for entry names with colons *)
let name_re_colons = /[A-Za-z][A-Za-z:-]*/


(* View: entry
    An apt.conf entry, recursive *)
let rec entry_noeol =
  let value =
     Util.del_str "\"" . store /[^"\n]+/
                       . del /";?/ "\";" in
  let opt_eol = del /[ \t\n]*/ "\n" in
  let long_eol = del /[ \t]*\n+/ "\n" in
  let list_elem = [ opt_eol . label "@elem" . value ] in
  let eol_comment = del /([ \t\n]*\n)?/ "" . comment in
      [ key name_re . Sep.space . value ]
    | [ key name_re . del /[ \t\n]*\{/ " {" .
          ( (opt_eol . entry_noeol) |
            list_elem |
            eol_comment
            )* .
          del /[ \t\n]*\};?/ "\n};" ]
    | [ key name_re . Util.del_str "::" . entry_noeol ]

let entry = indent . entry_noeol . eol


(* View: include
    A file inclusion
    /!\ The manpage is not clear on the syntax *)
let include =
  [ indent . key "#include" . Sep.space
           . store Rx.fspath . eol ]


(* View: clear
    A list of variables to clear
    /!\ The manpage is not clear on the syntax *)
let clear =
  let name = [ label "name" . store name_re_colons ] in
  [ indent . key "#clear" . Sep.space
           . Build.opt_list name Sep.space
           . eol ]


(************************************************************************
 * Group:                 LENS AND FILTER
 *************************************************************************)

(* View: lns
     The apt.conf lens *)
let lns = (empty|comment|entry|include|clear)*


(* View: filter *)
let filter = incl "/etc/apt/apt.conf"
    . incl "/etc/apt/apt.conf.d/*"
    . Util.stdexcl

let xfm = transform lns filter

===============================

and the test file:
===============================

module Test_aptconf =

  (* Test multiline C-style comments *)
  let comment_multiline = "/* This is a long
/* multiline
comment
*/
"

   test AptConf.comment get comment_multiline =
      { "#mcomment"
         { "1" = "This is a long" }
         { "2" = "/* multiline" }
         { "3" = "comment" } }


   (* Test empty multiline C-style comments *)
   let comment_multiline_empty = "/* */\n"

   test AptConf.empty get comment_multiline_empty = { }


   (* Test a simple entry *)
   let simple_entry = "APT::Clean-Installed \"true\";\n"

   test AptConf.entry get simple_entry =
      { "APT" { "Clean-Installed" = "true" } }

   (* Test simple recursivity *)
   let simple_recursion = "APT { Clean-Installed \"true\"; };\n"

   test AptConf.entry get simple_recursion =
      { "APT" { "Clean-Installed" = "true" } }

   (* Test simple recursivity with several entries *)
   let simple_recursion_multi =
     "APT {
          Clean-Installed \"true\";
          Get::Assume-Yes \"true\";
      }\n"

   test AptConf.entry get simple_recursion_multi =
      { "APT"
         { "Clean-Installed" = "true" }
         { "Get" { "Assume-Yes" = "true" } } }

   (* Test multiple recursivity *)
   let multiple_recursion =
     "APT { Get { Assume-Yes \"true\"; } };\n"

   test AptConf.entry get multiple_recursion =
      { "APT" { "Get" { "Assume-Yes" = "true" } } }

   (* Test simple list *)
   let simple_list = "DPKG::options { \"--force-confold\"; }\n"

   test AptConf.entry get simple_list =
      { "DPKG" { "options" { "@elem" = "--force-confold" } } }


   (* Test list elements with spaces *)
   let list_spaces = "Unattended-Upgrade::Allowed-Origins {
	\"Ubuntu lucid-security\"; };\n"

   test AptConf.entry get list_spaces =
      { "Unattended-Upgrade" { "Allowed-Origins"
        { "@elem" = "Ubuntu lucid-security" } } }

   (* Test recursive list *)
   let recursive_list =
     "DPKG {
          options {
              \"--force-confold\";
              \"--nocheck\";
          } };\n"

   test AptConf.entry get recursive_list =
      { "DPKG"
         { "options"
            { "@elem" = "--force-confold" }
            { "@elem" = "--nocheck" } } }

   (* Test empty group *)
   let empty_group =
    "APT\n{\n};\n"

   test AptConf.entry get empty_group = { "APT" }

   (* Test #include *)
   let include = "  #include /path/to/file\n"

   test AptConf.include get include =
      { "#include" = "/path/to/file" }

   (* Test #clear *)
   let clear = "#clear Dpkg::options Apt::Get::Assume-Yes\n"

   test AptConf.clear get clear =
      { "#clear"
         { "name" = "Dpkg::options" }
         { "name" = "Apt::Get::Assume-Yes" } }


   (* Test put simple value *)
   test AptConf.entry put "APT::Clean-Installed \"true\";\n"
      after set "/APT/Clean-Installed" "false" =
      "APT {\nClean-Installed \"false\";\n};\n"

   (* Test rm everything *)
   test AptConf.entry put "APT { Clean-Installed \"true\"; }\n"
      after rm "/APT" = ""

   (* Test rm on recursive value *)
   test AptConf.entry put "APT { Clean-Installed \"true\"; }\n"
      after rm "/APT/Clean-Installed" = "APT { }\n"

   (* Test put recursive value *)
   test AptConf.entry put "APT { Clean-Installed \"true\"; }\n"
      after set "/APT/Clean-Installed" "false" =
      "APT { Clean-Installed \"false\"; }\n"

   (* Test multiple lens *)
   let multiple_entries =
      "APT { Clean-Installed \"true\"; }\n
       APT::Clean-Installed \"true\";\n"

   test AptConf.lns get multiple_entries =
      { "APT" { "Clean-Installed" = "true" } }
      {}
      { "APT" { "Clean-Installed" = "true" } }

   (* Test with full lens *)
   test AptConf.lns put "APT { Clean-Installed \"true\"; }\n"
      after set "/APT/Clean-Installed" "false" =
      "APT { Clean-Installed \"false\"; }\n"

   (* Test single commented entry *)
   let commented_entry =
       "Unattended-Upgrade::Allowed-Origins {
	\"Ubuntu lucid-security\";
//	\"Ubuntu lucid-updates\";
        };\n"

   test AptConf.lns get commented_entry =
      { "Unattended-Upgrade" { "Allowed-Origins"
        { "@elem" = "Ubuntu lucid-security" }
        { "#comment" = "\"Ubuntu lucid-updates\";" } } }

   (* Test multiple commented entries *)
   let commented_entries =
       "// List of packages to not update
Unattended-Upgrade::Package-Blacklist {
//      \"vim\";
//      \"libc6\";
//      \"libc6-dev\";
//      \"libc6-i686\"
};
"

   test AptConf.lns get commented_entries =
      { "#comment" = "List of packages to not update" }
      { "Unattended-Upgrade" { "Package-Blacklist"
        { "#comment" = "\"vim\";" }
        { "#comment" = "\"libc6\";" }
        { "#comment" = "\"libc6-dev\";" }
        { "#comment" = "\"libc6-i686\"" }
      } }

   (* Test complex elem *)
   let complex_elem = "DPkg::Post-Invoke {\"if [ -d
/var/lib/update-notifier ]; then touch
/var/lib/update-notifier/dpkg-run-stamp; fi; if [ -e
/var/lib/update-notifier/updates-available ]; then echo >
/var/lib/update-notifier/updates-available; fi \"};\n"

   test AptConf.lns get complex_elem =
      { "DPkg" { "Post-Invoke"
        { "@elem" = "if [ -d /var/lib/update-notifier ]; then touch
/var/lib/update-notifier/dpkg-run-stamp; fi; if [ -e
/var/lib/update-notifier/updates-available ]; then echo >
/var/lib/update-notifier/updates-available; fi " } } }

===============================

Raphaël




More information about the augeas-devel mailing list