Inifile: allow single-character keys

Inifile: allow single-character keys

(*
Module: IniFile
  Generic module to create INI files lenses

Author: Raphael Pinson <raphink@gmail.com>

About: License
  This file is licensed under the LGPL v2+, like the rest of Augeas.

About: TODO
  Things to add in the future
  - Support double quotes in value

About: Lens usage
  This lens is made to provide generic primitives to construct INI File lenses.
  See <Puppet>, <PHP>, <MySQL> or <Dput> for examples of real life lenses using it.

About: Examples
  The <Test_IniFile> file contains various examples and tests.
*)

module IniFile  =

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

(* Group: Internal primitives *)

(*
Variable: eol
  End of line, inherited from <Util.eol>
*)
let eol = Util.doseol

(* Group: Separators *)

(*
Variable: sep
  Generic separator

  Parameters:
    pat:regexp - the pattern to delete
    default:string - the default string to use
*)
let sep (pat:regexp) (default:string)
                       = Sep.opt_space . del pat default

(*
Variable: sep_noindent
  Generic separator, no indentation

  Parameters:
    pat:regexp - the pattern to delete
    default:string - the default string to use
*)
let sep_noindent (pat:regexp) (default:string)
                       = del pat default

(*
Variable: sep_re
  The default regexp for a separator
*)

let sep_re             = /[=:]/

(*
Variable: sep_default
  The default separator value
*)
let sep_default        = "="

(* Group: Stores *)

(*
Variable: sto_to_eol
  Store until end of line
*)
let sto_to_eol         = Sep.opt_space . store Rx.space_in

(*
Variable: to_comment_re
  Regex until comment
*)
let to_comment_re = /[^";# \t\n][^";#\n]*[^";# \t\n]|[^";# \t\n]/

(*
Variable: sto_to_comment
  Store until comment
*)
let sto_to_comment = Sep.opt_space . store to_comment_re

(*
Variable: sto_multiline
  Store multiline values
*)
let sto_multiline = Sep.opt_space
         . store (to_comment_re
               . (/[ \t]*\n/ . Rx.space . to_comment_re)*)

(*
Variable: sto_multiline_nocomment
  Store multiline values without an end-of-line comment
*)
let sto_multiline_nocomment = Sep.opt_space
         . store (Rx.space_in . (/[ \t]*\n/ . Rx.space . Rx.space_in)*)

(* Group: Define comment and defaults *)

(*
View: comment_noindent
  Map comments into "#comment" nodes,
  no indentation allowed

  Parameters:
    pat:regexp - pattern to delete before commented data
    default:string - default pattern before commented data

  Sample Usage:
  (start code)
    let comment  = IniFile.comment_noindent "#" "#"
    let comment  = IniFile.comment_noindent IniFile.comment_re IniFile.comment_default
  (end code)
*)
let comment_noindent (pat:regexp) (default:string) =
  Util.comment_generic_seteol (pat . Rx.opt_space) default eol

(*
View: comment
  Map comments into "#comment" nodes

  Parameters:
    pat:regexp - pattern to delete before commented data
    default:string - default pattern before commented data

  Sample Usage:
  (start code)
    let comment  = IniFile.comment "#" "#"
    let comment  = IniFile.comment IniFile.comment_re IniFile.comment_default
  (end code)
*)
let comment (pat:regexp) (default:string) =
  Util.comment_generic_seteol (Rx.opt_space . pat . Rx.opt_space) default eol

(*
Variable: comment_re
  Default regexp for <comment> pattern
*)

let comment_re         = /[;#]/

(*
Variable: comment_default
  Default value for <comment> pattern
*)
let comment_default    = ";"

(*
View: empty_generic
  Empty line, including empty comments

  Parameters:
    indent:regexp     - the indentation regexp
    comment_re:regexp - the comment separator regexp
*)
let empty_generic (indent:regexp) (comment_re:regexp) =
  Util.empty_generic_dos (indent . comment_re? . Rx.opt_space)

(*
View: empty
  Empty line
*)
let empty = empty_generic Rx.opt_space comment_re

(*
View: empty_noindent
  Empty line, without indentation
*)
let empty_noindent = empty_generic "" comment_re

(************************************************************************
 * Group:                     ENTRY
 *************************************************************************)

(* Group: entry includes comments *)

(*
View: entry_generic_nocomment
  A very generic INI File entry, not including comments
  It allows to set the key lens (to set indentation
  or subnodes linked to the key) as well as the comment
  separator regexp, used to tune the store regexps.

  Parameters:
    kw:lens           - lens to match the key, including optional indentation
    sep:lens          - lens to use as key/value separator
    comment_re:regexp - comment separator regexp
    comment:lens      - lens to use as comment

  Sample Usage:
     > let entry = IniFile.entry_generic (key "setting") sep IniFile.comment_re comment
*)
let entry_generic_nocomment (kw:lens) (sep:lens)
                            (comment_re:regexp) (comment:lens) =
     let bare_re_noquot = (/[^" \t\r\n]/ - comment_re)
  in let bare_re = (/[^\r\n]/ - comment_re)+
  in let no_quot = /[^"\r\n]*/
  in let bare = Quote.do_dquote_opt_nil (store (bare_re_noquot . (bare_re* . bare_re_noquot)?))
  in let quoted = Quote.do_dquote (store (no_quot . comment_re+ . no_quot))
  in [ kw . sep . (Sep.opt_space . bare)? . (comment|eol) ]
   | [ kw . sep . Sep.opt_space . quoted . (comment|eol) ]

(*
View: entry_generic
  A very generic INI File entry
  It allows to set the key lens (to set indentation
  or subnodes linked to the key) as well as the comment
  separator regexp, used to tune the store regexps.

  Parameters:
    kw:lens           - lens to match the key, including optional indentation
    sep:lens          - lens to use as key/value separator
    comment_re:regexp - comment separator regexp
    comment:lens      - lens to use as comment

  Sample Usage:
     > let entry = IniFile.entry_generic (key "setting") sep IniFile.comment_re comment
*)
let entry_generic (kw:lens) (sep:lens) (comment_re:regexp) (comment:lens) =
  entry_generic_nocomment kw sep comment_re comment | comment

(*
View: entry
  Generic INI File entry

  Parameters:
    kw:regexp    - keyword regexp for the label
    sep:lens     - lens to use as key/value separator
    comment:lens - lens to use as comment

  Sample Usage:
     > let entry = IniFile.entry setting sep comment
*)
let entry (kw:regexp) (sep:lens) (comment:lens) =
     entry_generic (key kw) sep comment_re comment

(*
View: indented_entry
  Generic INI File entry that might be indented with an arbitrary
  amount of whitespace

  Parameters:
    kw:regexp    - keyword regexp for the label
    sep:lens     - lens to use as key/value separator
    comment:lens - lens to use as comment

  Sample Usage:
     > let entry = IniFile.indented_entry setting sep comment
*)
let indented_entry (kw:regexp) (sep:lens) (comment:lens) =
     entry_generic (Util.indent . key kw) sep comment_re comment

(*
View: entry_multiline_generic
  A very generic multiline INI File entry
  It allows to set the key lens (to set indentation
  or subnodes linked to the key) as well as the comment
  separator regexp, used to tune the store regexps.

  Parameters:
    kw:lens           - lens to match the key, including optional indentation
    sep:lens          - lens to use as key/value separator
    comment_re:regexp - comment separator regexp
    comment:lens      - lens to use as comment
    eol:lens          - lens for end of line

  Sample Usage:
     > let entry = IniFile.entry_generic (key "setting") sep IniFile.comment_re comment comment_or_eol
*)
let entry_multiline_generic (kw:lens) (sep:lens) (comment_re:regexp)
                            (comment:lens) (eol:lens) =
     let newline = /\r?\n[ \t]+/
  in let bare =
          let word_re_noquot = (/[^" \t\r\n]/ - comment_re)+
       in let word_re = (/[^\r\n]/ - comment_re)+
       in let base_re = (word_re_noquot . (word_re* . word_re_noquot)?)
       in let sto_re = base_re . (newline . base_re)*
                     | (newline . base_re)+
       in Quote.do_dquote_opt_nil (store sto_re)
  in let quoted =
          let no_quot = /[^"\r\n]*/
       in let base_re = (no_quot . comment_re+ . no_quot)
       in let sto_re = base_re . (newline . base_re)*
                     | (newline . base_re)+
       in Quote.do_dquote (store sto_re)
  in [ kw . sep . (Sep.opt_space . bare)? . eol ]
   | [ kw . sep . Sep.opt_space . quoted . eol ]
   | comment

(*
View: entry_multiline
  Generic multiline INI File entry

  Parameters:
    kw:regexp    - keyword regexp for the label
    sep:lens     - lens to use as key/value separator
    comment:lens - lens to use as comment
*)
let entry_multiline (kw:regexp) (sep:lens) (comment:lens) =
  entry_multiline_generic (key kw) sep comment_re comment (comment|eol)

(*
View: entry_multiline_nocomment
  Generic multiline INI File entry without an end-of-line comment

  Parameters:
    kw:regexp    - keyword regexp for the label
    sep:lens     - lens to use as key/value separator
    comment:lens - lens to use as comment
*)
let entry_multiline_nocomment (kw:regexp) (sep:lens) (comment:lens) =
  entry_multiline_generic (key kw) sep comment_re comment eol

(*
View: entry_list
  Generic INI File list entry

  Parameters:
    kw:regexp     - keyword regexp for the label
    sep:lens      - lens to use as key/value separator
    sto:regexp    - store regexp for the values
    list_sep:lens - lens to use as list separator
    comment:lens  - lens to use as comment
*)
let entry_list (kw:regexp) (sep:lens) (sto:regexp) (list_sep:lens) (comment:lens) =
  let list = counter "elem"
      . Build.opt_list [ seq "elem" . store sto ] list_sep
  in Build.key_value_line_comment kw sep (Sep.opt_space . list) comment

(*
View: entry_list_nocomment
  Generic INI File list entry without an end-of-line comment

  Parameters:
    kw:regexp     - keyword regexp for the label
    sep:lens      - lens to use as key/value separator
    sto:regexp    - store regexp for the values
    list_sep:lens - lens to use as list separator
*)
let entry_list_nocomment (kw:regexp) (sep:lens) (sto:regexp) (list_sep:lens) =
  let list = counter "elem"
      . Build.opt_list [ seq "elem" . store sto ] list_sep
  in Build.key_value_line kw sep (Sep.opt_space . list)

(*
Variable: entry_re
  Default regexp for <entry> keyword
*)
let entry_re           = ( /[A-Za-z][A-Za-z0-9._-]*/ )

(************************************************************************
 * Group:                      RECORD
 *************************************************************************)

(* Group: Title definition *)

(*
View: title
  Title for <record>. This maps the title of a record as a node in the abstract tree.

  Parameters:
    kw:regexp - keyword regexp for the label

  Sample Usage:
    > let title   = IniFile.title IniFile.record_re
*)
let title (kw:regexp)
                       = Util.del_str "[" . key kw
                         . Util.del_str "]". eol

(*
View: indented_title
  Title for <record>. This maps the title of a record as a node in the abstract tree. The title may be indented with arbitrary amounts of whitespace

  Parameters:
    kw:regexp - keyword regexp for the label

  Sample Usage:
    > let title   = IniFile.title IniFile.record_re
*)
let indented_title (kw:regexp)
                       = Util.indent . title kw

(*
View: title_label
  Title for <record>. This maps the title of a record as a value in the abstract tree.

  Parameters:
    name:string - name for the title label
    kw:regexp   - keyword regexp for the label

  Sample Usage:
    > let title   = IniFile.title_label "target" IniFile.record_label_re
*)
let title_label (name:string) (kw:regexp)
                       = label name
                         . Util.del_str "[" . store kw
                         . Util.del_str "]". eol

(*
View: indented_title_label
  Title for <record>. This maps the title of a record as a value in the abstract tree. The title may be indented with arbitrary amounts of whitespace

  Parameters:
    name:string - name for the title label
    kw:regexp   - keyword regexp for the label

  Sample Usage:
    > let title   = IniFile.title_label "target" IniFile.record_label_re
*)
let indented_title_label (name:string) (kw:regexp)
                       = Util.indent . title_label name kw

(*
Variable: record_re
  Default regexp for <title> keyword pattern
*)
let record_re          = ( /[^]\r\n\/]+/ - /#comment/ )

(*
Variable: record_label_re
  Default regexp for <title_label> keyword pattern
*)
let record_label_re    = /[^]\r\n]+/

(* Group: Record definition *)

(*
View: record_noempty
  INI File Record with no empty lines allowed.

  Parameters:
    title:lens - lens to use for title. Use either <title> or <title_label>.
    entry:lens - lens to use for entries in the record. See <entry>.
*)
let record_noempty (title:lens) (entry:lens)
                       = [ title
		       . entry* ]

(*
View: record
  Generic INI File record

  Parameters:
    title:lens - lens to use for title. Use either <title> or <title_label>.
    entry:lens - lens to use for entries in the record. See <entry>.

  Sample Usage:
    > let record  = IniFile.record title entry
*)
let record (title:lens) (entry:lens)
                       = record_noempty title ( entry | empty )

(************************************************************************
 * Group:                      GENERIC LENSES
 *************************************************************************)

(*

Group: Lens definition

View: lns_noempty
  Generic INI File lens with no empty lines

  Parameters:
    record:lens  - record lens to use. See <record_noempty>.
    comment:lens - comment lens to use. See <comment>.

  Sample Usage:
    > let lns     = IniFile.lns_noempty record comment
*)
let lns_noempty (record:lens) (comment:lens)
                       = comment* . record*

(*
View: lns
  Generic INI File lens

  Parameters:
    record:lens  - record lens to use. See <record>.
    comment:lens - comment lens to use. See <comment>.

  Sample Usage:
    > let lns     = IniFile.lns record comment
*)
let lns (record:lens) (comment:lens)
                       = lns_noempty record (comment|empty)

(************************************************************************
 * Group:                   READY-TO-USE LENSES
 *************************************************************************)

let record_anon (entry:lens) = [ label "section" . value ".anon" . ( entry | empty )+ ]

(*
View: lns_loose
  A loose, ready-to-use lens, featuring:
    - sections as values (to allow '/' in names)
    - support empty lines and comments
    - support for [#;] as comment, defaulting to ";"
    - .anon sections
    - don't allow multiline values
    - allow indented titles
    - allow indented entries
*)
let lns_loose =
     let l_comment = comment comment_re comment_default
  in let l_sep = sep sep_re sep_default
  in let l_entry = indented_entry entry_re l_sep l_comment
  in let l_title = indented_title_label "section" (record_label_re - ".anon")
  in let l_record = record l_title l_entry
  in (record_anon l_entry)? . l_record*

(*
View: lns_loose_multiline
  A loose, ready-to-use lens, featuring:
    - sections as values (to allow '/' in names)
    - support empty lines and comments
    - support for [#;] as comment, defaulting to ";"
    - .anon sections
    - allow multiline values
*)
let lns_loose_multiline =
     let l_comment = comment comment_re comment_default
  in let l_sep = sep sep_re sep_default
  in let l_entry = entry_multiline entry_re l_sep l_comment
  in let l_title = title_label "section" (record_label_re - ".anon")
  in let l_record = record l_title l_entry
  in (record_anon l_entry)? . l_record*

(*
Module: IniFile
  Generic module to create INI files lenses

Author: Raphael Pinson <raphink@gmail.com>

About: License
  This file is licensed under the LGPL v2+, like the rest of Augeas.

About: TODO
  Things to add in the future
  - Support double quotes in value

About: Lens usage
  This lens is made to provide generic primitives to construct INI File lenses.
  See <Puppet>, <PHP>, <MySQL> or <Dput> for examples of real life lenses using it.

About: Examples
  The <Test_IniFile> file contains various examples and tests.
*)

module IniFile  =

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

(* Group: Internal primitives *)

(*
Variable: eol
  End of line, inherited from <Util.eol>
*)
let eol = Util.doseol

(* Group: Separators *)

(*
Variable: sep
  Generic separator

  Parameters:
    pat:regexp - the pattern to delete
    default:string - the default string to use
*)
let sep (pat:regexp) (default:string)
                       = Sep.opt_space . del pat default

(*
Variable: sep_noindent
  Generic separator, no indentation

  Parameters:
    pat:regexp - the pattern to delete
    default:string - the default string to use
*)
let sep_noindent (pat:regexp) (default:string)
                       = del pat default

(*
Variable: sep_re
  The default regexp for a separator
*)

let sep_re             = /[=:]/

(*
Variable: sep_default
  The default separator value
*)
let sep_default        = "="

(* Group: Stores *)

(*
Variable: sto_to_eol
  Store until end of line
*)
let sto_to_eol         = Sep.opt_space . store Rx.space_in

(*
Variable: to_comment_re
  Regex until comment
*)
let to_comment_re = /[^";# \t\n][^";#\n]*[^";# \t\n]|[^";# \t\n]/

(*
Variable: sto_to_comment
  Store until comment
*)
let sto_to_comment = Sep.opt_space . store to_comment_re

(*
Variable: sto_multiline
  Store multiline values
*)
let sto_multiline = Sep.opt_space
         . store (to_comment_re
               . (/[ \t]*\n/ . Rx.space . to_comment_re)*)

(*
Variable: sto_multiline_nocomment
  Store multiline values without an end-of-line comment
*)
let sto_multiline_nocomment = Sep.opt_space
         . store (Rx.space_in . (/[ \t]*\n/ . Rx.space . Rx.space_in)*)

(* Group: Define comment and defaults *)

(*
View: comment_noindent
  Map comments into "#comment" nodes,
  no indentation allowed

  Parameters:
    pat:regexp - pattern to delete before commented data
    default:string - default pattern before commented data

  Sample Usage:
  (start code)
    let comment  = IniFile.comment_noindent "#" "#"
    let comment  = IniFile.comment_noindent IniFile.comment_re IniFile.comment_default
  (end code)
*)
let comment_noindent (pat:regexp) (default:string) =
  Util.comment_generic_seteol (pat . Rx.opt_space) default eol

(*
View: comment
  Map comments into "#comment" nodes

  Parameters:
    pat:regexp - pattern to delete before commented data
    default:string - default pattern before commented data

  Sample Usage:
  (start code)
    let comment  = IniFile.comment "#" "#"
    let comment  = IniFile.comment IniFile.comment_re IniFile.comment_default
  (end code)
*)
let comment (pat:regexp) (default:string) =
  Util.comment_generic_seteol (Rx.opt_space . pat . Rx.opt_space) default eol

(*
Variable: comment_re
  Default regexp for <comment> pattern
*)

let comment_re         = /[;#]/

(*
Variable: comment_default
  Default value for <comment> pattern
*)
let comment_default    = ";"

(*
View: empty_generic
  Empty line, including empty comments

  Parameters:
    indent:regexp     - the indentation regexp
    comment_re:regexp - the comment separator regexp
*)
let empty_generic (indent:regexp) (comment_re:regexp) =
  Util.empty_generic_dos (indent . comment_re? . Rx.opt_space)

(*
View: empty
  Empty line
*)
let empty = empty_generic Rx.opt_space comment_re

(*
View: empty_noindent
  Empty line, without indentation
*)
let empty_noindent = empty_generic "" comment_re

(************************************************************************
 * Group:                     ENTRY
 *************************************************************************)

(* Group: entry includes comments *)

(*
View: entry_generic_nocomment
  A very generic INI File entry, not including comments
  It allows to set the key lens (to set indentation
  or subnodes linked to the key) as well as the comment
  separator regexp, used to tune the store regexps.

  Parameters:
    kw:lens           - lens to match the key, including optional indentation
    sep:lens          - lens to use as key/value separator
    comment_re:regexp - comment separator regexp
    comment:lens      - lens to use as comment

  Sample Usage:
     > let entry = IniFile.entry_generic (key "setting") sep IniFile.comment_re comment
*)
let entry_generic_nocomment (kw:lens) (sep:lens)
                            (comment_re:regexp) (comment:lens) =
     let bare_re_noquot = (/[^" \t\r\n]/ - comment_re)
  in let bare_re = (/[^\r\n]/ - comment_re)+
  in let no_quot = /[^"\r\n]*/
  in let bare = Quote.do_dquote_opt_nil (store (bare_re_noquot . (bare_re* . bare_re_noquot)?))
  in let quoted = Quote.do_dquote (store (no_quot . comment_re+ . no_quot))
  in [ kw . sep . (Sep.opt_space . bare)? . (comment|eol) ]
   | [ kw . sep . Sep.opt_space . quoted . (comment|eol) ]

(*
View: entry_generic
  A very generic INI File entry
  It allows to set the key lens (to set indentation
  or subnodes linked to the key) as well as the comment
  separator regexp, used to tune the store regexps.

  Parameters:
    kw:lens           - lens to match the key, including optional indentation
    sep:lens          - lens to use as key/value separator
    comment_re:regexp - comment separator regexp
    comment:lens      - lens to use as comment

  Sample Usage:
     > let entry = IniFile.entry_generic (key "setting") sep IniFile.comment_re comment
*)
let entry_generic (kw:lens) (sep:lens) (comment_re:regexp) (comment:lens) =
  entry_generic_nocomment kw sep comment_re comment | comment

(*
View: entry
  Generic INI File entry

  Parameters:
    kw:regexp    - keyword regexp for the label
    sep:lens     - lens to use as key/value separator
    comment:lens - lens to use as comment

  Sample Usage:
     > let entry = IniFile.entry setting sep comment
*)
let entry (kw:regexp) (sep:lens) (comment:lens) =
     entry_generic (key kw) sep comment_re comment

(*
View: indented_entry
  Generic INI File entry that might be indented with an arbitrary
  amount of whitespace

  Parameters:
    kw:regexp    - keyword regexp for the label
    sep:lens     - lens to use as key/value separator
    comment:lens - lens to use as comment

  Sample Usage:
     > let entry = IniFile.indented_entry setting sep comment
*)
let indented_entry (kw:regexp) (sep:lens) (comment:lens) =
     entry_generic (Util.indent . key kw) sep comment_re comment

(*
View: entry_multiline_generic
  A very generic multiline INI File entry
  It allows to set the key lens (to set indentation
  or subnodes linked to the key) as well as the comment
  separator regexp, used to tune the store regexps.

  Parameters:
    kw:lens           - lens to match the key, including optional indentation
    sep:lens          - lens to use as key/value separator
    comment_re:regexp - comment separator regexp
    comment:lens      - lens to use as comment
    eol:lens          - lens for end of line

  Sample Usage:
     > let entry = IniFile.entry_generic (key "setting") sep IniFile.comment_re comment comment_or_eol
*)
let entry_multiline_generic (kw:lens) (sep:lens) (comment_re:regexp)
                            (comment:lens) (eol:lens) =
     let newline = /\r?\n[ \t]+/
  in let bare =
          let word_re_noquot = (/[^" \t\r\n]/ - comment_re)+
       in let word_re = (/[^\r\n]/ - comment_re)+
       in let base_re = (word_re_noquot . (word_re* . word_re_noquot)?)
       in let sto_re = base_re . (newline . base_re)*
                     | (newline . base_re)+
       in Quote.do_dquote_opt_nil (store sto_re)
  in let quoted =
          let no_quot = /[^"\r\n]*/
       in let base_re = (no_quot . comment_re+ . no_quot)
       in let sto_re = base_re . (newline . base_re)*
                     | (newline . base_re)+
       in Quote.do_dquote (store sto_re)
  in [ kw . sep . (Sep.opt_space . bare)? . eol ]
   | [ kw . sep . Sep.opt_space . quoted . eol ]
   | comment

(*
View: entry_multiline
  Generic multiline INI File entry

  Parameters:
    kw:regexp    - keyword regexp for the label
    sep:lens     - lens to use as key/value separator
    comment:lens - lens to use as comment
*)
let entry_multiline (kw:regexp) (sep:lens) (comment:lens) =
  entry_multiline_generic (key kw) sep comment_re comment (comment|eol)

(*
View: entry_multiline_nocomment
  Generic multiline INI File entry without an end-of-line comment

  Parameters:
    kw:regexp    - keyword regexp for the label
    sep:lens     - lens to use as key/value separator
    comment:lens - lens to use as comment
*)
let entry_multiline_nocomment (kw:regexp) (sep:lens) (comment:lens) =
  entry_multiline_generic (key kw) sep comment_re comment eol

(*
View: entry_list
  Generic INI File list entry

  Parameters:
    kw:regexp     - keyword regexp for the label
    sep:lens      - lens to use as key/value separator
    sto:regexp    - store regexp for the values
    list_sep:lens - lens to use as list separator
    comment:lens  - lens to use as comment
*)
let entry_list (kw:regexp) (sep:lens) (sto:regexp) (list_sep:lens) (comment:lens) =
  let list = counter "elem"
      . Build.opt_list [ seq "elem" . store sto ] list_sep
  in Build.key_value_line_comment kw sep (Sep.opt_space . list) comment

(*
View: entry_list_nocomment
  Generic INI File list entry without an end-of-line comment

  Parameters:
    kw:regexp     - keyword regexp for the label
    sep:lens      - lens to use as key/value separator
    sto:regexp    - store regexp for the values
    list_sep:lens - lens to use as list separator
*)
let entry_list_nocomment (kw:regexp) (sep:lens) (sto:regexp) (list_sep:lens) =
  let list = counter "elem"
      . Build.opt_list [ seq "elem" . store sto ] list_sep
  in Build.key_value_line kw sep (Sep.opt_space . list)

(*
Variable: entry_re
  Default regexp for <entry> keyword
*)
let entry_re           = ( /[A-Za-z][A-Za-z0-9._-]+/ )

(************************************************************************
 * Group:                      RECORD
 *************************************************************************)

(* Group: Title definition *)

(*
View: title
  Title for <record>. This maps the title of a record as a node in the abstract tree.

  Parameters:
    kw:regexp - keyword regexp for the label

  Sample Usage:
    > let title   = IniFile.title IniFile.record_re
*)
let title (kw:regexp)
                       = Util.del_str "[" . key kw
                         . Util.del_str "]". eol

(*
View: indented_title
  Title for <record>. This maps the title of a record as a node in the abstract tree. The title may be indented with arbitrary amounts of whitespace

  Parameters:
    kw:regexp - keyword regexp for the label

  Sample Usage:
    > let title   = IniFile.title IniFile.record_re
*)
let indented_title (kw:regexp)
                       = Util.indent . title kw

(*
View: title_label
  Title for <record>. This maps the title of a record as a value in the abstract tree.

  Parameters:
    name:string - name for the title label
    kw:regexp   - keyword regexp for the label

  Sample Usage:
    > let title   = IniFile.title_label "target" IniFile.record_label_re
*)
let title_label (name:string) (kw:regexp)
                       = label name
                         . Util.del_str "[" . store kw
                         . Util.del_str "]". eol

(*
View: indented_title_label
  Title for <record>. This maps the title of a record as a value in the abstract tree. The title may be indented with arbitrary amounts of whitespace

  Parameters:
    name:string - name for the title label
    kw:regexp   - keyword regexp for the label

  Sample Usage:
    > let title   = IniFile.title_label "target" IniFile.record_label_re
*)
let indented_title_label (name:string) (kw:regexp)
                       = Util.indent . title_label name kw

(*
Variable: record_re
  Default regexp for <title> keyword pattern
*)
let record_re          = ( /[^]\r\n\/]+/ - /#comment/ )

(*
Variable: record_label_re
  Default regexp for <title_label> keyword pattern
*)
let record_label_re    = /[^]\r\n]+/

(* Group: Record definition *)

(*
View: record_noempty
  INI File Record with no empty lines allowed.

  Parameters:
    title:lens - lens to use for title. Use either <title> or <title_label>.
    entry:lens - lens to use for entries in the record. See <entry>.
*)
let record_noempty (title:lens) (entry:lens)
                       = [ title
		       . entry* ]

(*
View: record
  Generic INI File record

  Parameters:
    title:lens - lens to use for title. Use either <title> or <title_label>.
    entry:lens - lens to use for entries in the record. See <entry>.

  Sample Usage:
    > let record  = IniFile.record title entry
*)
let record (title:lens) (entry:lens)
                       = record_noempty title ( entry | empty )

(************************************************************************
 * Group:                      GENERIC LENSES
 *************************************************************************)

(*

Group: Lens definition

View: lns_noempty
  Generic INI File lens with no empty lines

  Parameters:
    record:lens  - record lens to use. See <record_noempty>.
    comment:lens - comment lens to use. See <comment>.

  Sample Usage:
    > let lns     = IniFile.lns_noempty record comment
*)
let lns_noempty (record:lens) (comment:lens)
                       = comment* . record*

(*
View: lns
  Generic INI File lens

  Parameters:
    record:lens  - record lens to use. See <record>.
    comment:lens - comment lens to use. See <comment>.

  Sample Usage:
    > let lns     = IniFile.lns record comment
*)
let lns (record:lens) (comment:lens)
                       = lns_noempty record (comment|empty)

(************************************************************************
 * Group:                   READY-TO-USE LENSES
 *************************************************************************)

let record_anon (entry:lens) = [ label "section" . value ".anon" . ( entry | empty )+ ]

(*
View: lns_loose
  A loose, ready-to-use lens, featuring:
    - sections as values (to allow '/' in names)
    - support empty lines and comments
    - support for [#;] as comment, defaulting to ";"
    - .anon sections
    - don't allow multiline values
    - allow indented titles
    - allow indented entries
*)
let lns_loose =
     let l_comment = comment comment_re comment_default
  in let l_sep = sep sep_re sep_default
  in let l_entry = indented_entry entry_re l_sep l_comment
  in let l_title = indented_title_label "section" (record_label_re - ".anon")
  in let l_record = record l_title l_entry
  in (record_anon l_entry)? . l_record*

(*
View: lns_loose_multiline
  A loose, ready-to-use lens, featuring:
    - sections as values (to allow '/' in names)
    - support empty lines and comments
    - support for [#;] as comment, defaulting to ";"
    - .anon sections
    - allow multiline values
*)
let lns_loose_multiline =
     let l_comment = comment comment_re comment_default
  in let l_sep = sep sep_re sep_default
  in let l_entry = entry_multiline entry_re l_sep l_comment
  in let l_title = title_label "section" (record_label_re - ".anon")
  in let l_record = record l_title l_entry
  in (record_anon l_entry)? . l_record*


Add /etc/profile and /etc/byobu to Shellvars.lns

(*
Module: Shellvars
 Generic lens for shell-script config files like the ones found
 in /etc/sysconfig

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

About: Lens Usage
   To be documented
*)

module Shellvars =
  autoload xfm

  (* Delete a blank line, rather than mapping it *)
  let del_empty = del (Util.empty_generic_re . "\n") "\n"

  let empty   = Util.empty
  let empty_part_re = Util.empty_generic_re . /\n+/
  let eol = del (/[ \t]+|[ \t]*[;\n]/ . empty_part_re*) "\n"
  let semicol_eol = del (/[ \t]*[;\n]/ . empty_part_re*) "\n"
  let brace_eol = del /[ \t\n]+/ "\n"

  let key_re = /[A-Za-z0-9_]+(\[[0-9A-Za-z_,]+\])?/ - ("unset" | "export")
  let matching_re = "${!" . key_re . /[\*@]\}/
  let eq = Util.del_str "="

  let eol_for_comment = del /([ \t]*\n)([ \t]*(#[ \t]*)?\n)*/ "\n"
  let comment = Util.comment_generic_seteol /[ \t]*#[ \t]*/ " # " eol_for_comment
  (* comment_eol in shell MUST begin with a space *)
  let comment_eol = Util.comment_generic_seteol /[ \t]+#[ \t]*/ " # " eol_for_comment
  let comment_or_eol = comment_eol | semicol_eol

  let xchgs   = Build.xchgs
  let semicol = del /;?/ ""

  let char  = /[^`;()'"&|\n\\# \t]#*|\\\\./
  let dquot =
       let char = /[^"\\]|\\\\./ | Rx.cl
    in "\"" . char* . "\""                    (* " Emacs, relax *)
  let squot = /'[^']*'/
  let bquot = /`[^`\n]+`/
  (* dbquot don't take spaces or semi-colons *)
  let dbquot = /``[^` \t\n;]+``/
  let dollar_assign = /\$\([^\(\)#\n]*\)/
  let dollar_arithm = /\$\(\([^\)#\n]*\)\)/

  let anyquot = (char|dquot|squot|dollar_assign|dollar_arithm)+ | bquot | dbquot
  let sto_to_semicol = store (anyquot . (Rx.cl_or_space . anyquot)*)

  (* Array values of the form '(val1 val2 val3)'. We do not handle empty *)
  (* arrays here because of typechecking headaches. Instead, they are    *)
  (* treated as a simple value                                           *)
  let array =
    let array_value = store anyquot in
    del /\([ \t]*/ "(" . counter "values" .
      [ seq "values" . array_value ] .
      [ del /[ \t\n]+/ " " . seq "values" . array_value ] *
      . del /[ \t]*\)/ ")"

  (* Treat an empty list () as a value '()'; that's not quite correct *)
  (* but fairly close.                                                *)
  let simple_value =
    let empty_array = /\([ \t]*\)/ in
      store (anyquot | empty_array)?

  let export = [ key "export" . Util.del_ws_spc ]
  let kv = Util.indent . export? . key key_re
           . eq . (simple_value | array)

  let var_action (name:string) =
    Util.indent . del name name . Util.del_ws_spc
    . label ("@" . name) . counter "var_action"
    . Build.opt_list [ seq "var_action" . store (key_re | matching_re) ] Util.del_ws_spc

  let unset = var_action "unset"
  let bare_export = var_action "export"

  let source =
    Util.indent
    . del /\.|source/ "." . label ".source"
    . Util.del_ws_spc . store /[^;=# \t\n]+/

  let shell_builtin_cmds = "ulimit" | "shift" | "exit"

  let eval =
    Util.indent . Util.del_str "eval" . Util.del_ws_spc
    . label "@eval" . store anyquot

  let alias =
    Util.indent . Util.del_str "alias" . Util.del_ws_spc
    . label "@alias" . store key_re . eq
    . [ label "value" . store anyquot ]

  let builtin =
    Util.indent . label "@builtin"
    . store shell_builtin_cmds
    . (Sep.cl_or_space
    . [ label "args" . sto_to_semicol ])?

  let keyword (kw:string) = Util.indent . Util.del_str kw
  let keyword_label (kw:string) (lbl:string) = keyword kw . label lbl

  let return =
    Util.indent . label "@return"
    . Util.del_str "return"
    . ( Util.del_ws_spc . store Rx.integer )?

  let action (operator:string) (lbl:string) (sto:lens) =
       let sp = Rx.cl_or_opt_space | /[ \t\n]+/
    in [ del (sp . operator . sp) (" " . operator . " ")
       . label ("@".lbl) . sto ]

  let action_pipe = action "|" "pipe"
  let action_and = action "&&" "and"
  let action_or = action "||" "or"

  let condition =
    let cond (start:string) (end:string) = [ label "type" . store start ]
                                         . Util.del_ws_spc . sto_to_semicol
                                         . Util.del_ws_spc . Util.del_str end
                                         . ( action_and sto_to_semicol | action_or sto_to_semicol )*
    in Util.indent . label "@condition" . (cond "[" "]" | cond "[[" "]]")

  (* Entry types *)
  let entry_eol_item (item:lens) = [ item . comment_or_eol ]
  let entry_item (item:lens) = [ item ]

  let entry_eol_nocommand =
      entry_eol_item source
        | entry_eol_item kv
        | entry_eol_item unset
        | entry_eol_item bare_export
        | entry_eol_item builtin
        | entry_eol_item return
        | entry_eol_item condition
        | entry_eol_item eval
        | entry_eol_item alias

  let entry_noeol_nocommand =
      entry_item source
        | entry_item kv
        | entry_item unset
        | entry_item bare_export
        | entry_item builtin
        | entry_item return
        | entry_item condition
        | entry_item eval
        | entry_item alias

  (* Command *)
  let rec command =
       let env = [ key key_re . eq . store anyquot . Sep.cl_or_space ]
    in let reserved_key = /exit|shift|return|ulimit|unset|export|source|\.|if|for|select|while|until|then|else|fi|done|case|eval|alias/
    in let word = /[A-Za-z0-9_.-\/]+/
    in let entry_eol = entry_eol_nocommand | entry_eol_item command
    in let entry_noeol = entry_noeol_nocommand | entry_item command
    in let entry = entry_eol | entry_noeol
    in let pipe = action_pipe (entry_eol_item command | entry_item command)
    in let and = action_and entry
    in let or = action_or entry
    in Util.indent . label "@command" . env* . store (word - reserved_key)
     . [ Sep.cl_or_space . label "@arg" . sto_to_semicol]?
     . ( pipe | and | or )?

  let entry_eol = entry_eol_nocommand
                | entry_eol_item command

  let entry_noeol = entry_noeol_nocommand
                  | entry_item command

(************************************************************************
 * Group:                 CONDITIONALS AND LOOPS
 *************************************************************************)

  let generic_cond_start (start_kw:string) (lbl:string)
                         (then_kw:string) (contents:lens) =
      keyword_label start_kw lbl . Sep.space
      . sto_to_semicol . semicol_eol
      . keyword then_kw . eol
      . contents

  let generic_cond (start_kw:string) (lbl:string)
                       (then_kw:string) (contents:lens) (end_kw:string) =
      [ generic_cond_start start_kw lbl then_kw contents
        . keyword end_kw . comment_or_eol ]

  let cond_if (entry:lens) =
    let elif = [ generic_cond_start "elif" "@elif" "then" entry+ ] in
    let else = [ keyword_label "else" "@else" . eol . entry+ ] in
    generic_cond "if" "@if" "then" (entry+ . elif* . else?) "fi"

  let loop_for (entry:lens) =
    generic_cond "for" "@for" "do" entry+ "done"

  let loop_while (entry:lens) =
    generic_cond "while" "@while" "do" entry+ "done"

  let loop_until (entry:lens) =
    generic_cond "until" "@until" "do" entry+ "done"

  let loop_select (entry:lens) =
    generic_cond "select" "@select" "do" entry+ "done"

  let case (entry:lens) (entry_noeol:lens) =
       let pattern = [ label "@pattern" . sto_to_semicol . Sep.opt_space ]
    in let case_entry = [ label "@case_entry"
                       . Util.indent . pattern
                       . (Util.del_str "|" . Sep.opt_space . pattern)*
                       . Util.del_str ")" . eol
                       . entry* . entry_noeol?
                       . Util.indent . Util.del_str ";;" . eol ] in
      [ keyword_label "case" "@case" . Sep.space
        . store (char+ | ("\"" . char+ . "\""))
        . del /[ \t\n]+/ " " . Util.del_str "in" . eol
        . (empty* . comment* . case_entry)*
        . empty* . comment*
        . keyword "esac" . comment_or_eol ]

  let subshell (entry:lens) =
    [ Util.indent . label "@subshell"
    . Util.del_str "{" . brace_eol
    . entry+
    . Util.indent . Util.del_str "}" . eol ]

  let function (entry:lens) =
    [ Util.indent . label "@function"
    . del /(function[ \t]+)?/ ""
    . store Rx.word . del /[ \t]*\(\)/ "()"
    . (comment_eol|brace_eol) . Util.del_str "{" . brace_eol
    . entry+
    . Util.indent . Util.del_str "}" . eol ]

  let rec rec_entry =
    let entry = comment | entry_eol | rec_entry in
        cond_if entry
      | loop_for entry
      | loop_select entry
      | loop_while entry
      | loop_until entry
      | case entry entry_noeol
      | function entry
      | subshell entry

  let lns_norec = del_empty* . (comment | entry_eol) *

  let lns = del_empty* . (comment | entry_eol | rec_entry) *

  let sc_incl (n:string) = (incl ("/etc/sysconfig/" . n))
  let sc_excl (n:string) = (excl ("/etc/sysconfig/" . n))

  let filter_sysconfig =
      sc_incl "*" .
      sc_excl "bootloader" .
      sc_excl "hw-uuid" .
      sc_excl "hwconf" .
      sc_excl "ip*tables" .
      sc_excl "ip*tables.save" .
      sc_excl "kernel" .
      sc_excl "*.pub" .
      sc_excl "sysstat.ioconf" .
      sc_excl "system-config-firewall" .
      sc_excl "system-config-securitylevel" .
      sc_incl "network/config" .
      sc_incl "network/dhcp" .
      sc_incl "network/dhcp6r" .
      sc_incl "network/dhcp6s" .
      sc_incl "network/ifcfg-*" .
      sc_incl "network/if-down.d/*" .
      sc_incl "network/ifroute-*" .
      sc_incl "network/if-up.d/*" .
      sc_excl "network/if-up.d/SuSEfirewall2" .
      sc_incl "network/providers/*" .
      sc_excl "network-scripts" .
      sc_incl "network-scripts/ifcfg-*" .
      sc_excl "rhn" .
      sc_incl "rhn/allowed-actions/*" .
      sc_excl "rhn/allowed-actions/script" .
      sc_incl "rhn/allowed-actions/script/*" .
      sc_incl "rhn/rhnsd" .
      sc_excl "SuSEfirewall2.d" .
      sc_incl "SuSEfirewall2.d/cobbler" .
      sc_incl "SuSEfirewall2.d/services/*" .
      sc_excl "SuSEfirewall2.d/services/TEMPLATE" .
      sc_excl "*.systemd"

  let filter_default = incl "/etc/default/*"
                     . excl "/etc/default/grub_installdevice*"
                     . excl "/etc/default/rmt"
                     . excl "/etc/default/star"
                     . excl "/etc/default/whoopsie"
                     . incl "/etc/profile"
                     . incl "/etc/profile.d/*"
  let filter_misc    = incl "/etc/arno-iptables-firewall/debconf.cfg"
                     . incl "/etc/conf.d/*"
                     . incl "/etc/cron-apt/config"
                     . incl "/etc/environment"
                     . incl "/etc/firewalld/firewalld.conf"
                     . incl "/etc/blkid.conf"
                     . incl "/etc/adduser.conf"
                     . incl "/etc/cowpoke.conf"
                     . incl "/etc/cvs-cron.conf"
                     . incl "/etc/cvs-pserver.conf"
                     . incl "/etc/devscripts.conf"
                     . incl "/etc/kamailio/kamctlrc"
                     . incl "/etc/lintianrc"
                     . incl "/etc/lsb-release"
                     . incl "/etc/os-release"
                     . incl "/etc/periodic.conf"
                     . incl "/etc/popularity-contest.conf"
                     . incl "/etc/rc.conf"
                     . incl "/etc/rc.conf.local"
                     . incl "/etc/selinux/config"
                     . incl "/etc/ucf.conf"
                     . incl "/etc/locale.conf"
                     . incl "/etc/vconsole.conf"
                     . incl "/etc/byobu/*"

  let filter = filter_sysconfig
             . filter_default
             . filter_misc
             . Util.stdexcl

  let xfm = transform lns filter

(* Local Variables: *)
(* mode: caml       *)
(* End:             *)

(*
Module: Shellvars
 Generic lens for shell-script config files like the ones found
 in /etc/sysconfig

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

About: Lens Usage
   To be documented
*)

module Shellvars =
  autoload xfm

  (* Delete a blank line, rather than mapping it *)
  let del_empty = del (Util.empty_generic_re . "\n") "\n"

  let empty   = Util.empty
  let empty_part_re = Util.empty_generic_re . /\n+/
  let eol = del (/[ \t]+|[ \t]*[;\n]/ . empty_part_re*) "\n"
  let semicol_eol = del (/[ \t]*[;\n]/ . empty_part_re*) "\n"
  let brace_eol = del /[ \t\n]+/ "\n"

  let key_re = /[A-Za-z0-9_]+(\[[0-9A-Za-z_,]+\])?/ - ("unset" | "export")
  let matching_re = "${!" . key_re . /[\*@]\}/
  let eq = Util.del_str "="

  let eol_for_comment = del /([ \t]*\n)([ \t]*(#[ \t]*)?\n)*/ "\n"
  let comment = Util.comment_generic_seteol /[ \t]*#[ \t]*/ " # " eol_for_comment
  (* comment_eol in shell MUST begin with a space *)
  let comment_eol = Util.comment_generic_seteol /[ \t]+#[ \t]*/ " # " eol_for_comment
  let comment_or_eol = comment_eol | semicol_eol

  let xchgs   = Build.xchgs
  let semicol = del /;?/ ""

  let char  = /[^`;()'"&|\n\\# \t]#*|\\\\./
  let dquot =
       let char = /[^"\\]|\\\\./ | Rx.cl
    in "\"" . char* . "\""                    (* " Emacs, relax *)
  let squot = /'[^']*'/
  let bquot = /`[^`\n]+`/
  (* dbquot don't take spaces or semi-colons *)
  let dbquot = /``[^` \t\n;]+``/
  let dollar_assign = /\$\([^\(\)#\n]*\)/
  let dollar_arithm = /\$\(\([^\)#\n]*\)\)/

  let anyquot = (char|dquot|squot|dollar_assign|dollar_arithm)+ | bquot | dbquot
  let sto_to_semicol = store (anyquot . (Rx.cl_or_space . anyquot)*)

  (* Array values of the form '(val1 val2 val3)'. We do not handle empty *)
  (* arrays here because of typechecking headaches. Instead, they are    *)
  (* treated as a simple value                                           *)
  let array =
    let array_value = store anyquot in
    del /\([ \t]*/ "(" . counter "values" .
      [ seq "values" . array_value ] .
      [ del /[ \t\n]+/ " " . seq "values" . array_value ] *
      . del /[ \t]*\)/ ")"

  (* Treat an empty list () as a value '()'; that's not quite correct *)
  (* but fairly close.                                                *)
  let simple_value =
    let empty_array = /\([ \t]*\)/ in
      store (anyquot | empty_array)?

  let export = [ key "export" . Util.del_ws_spc ]
  let kv = Util.indent . export? . key key_re
           . eq . (simple_value | array)

  let var_action (name:string) =
    Util.indent . del name name . Util.del_ws_spc
    . label ("@" . name) . counter "var_action"
    . Build.opt_list [ seq "var_action" . store (key_re | matching_re) ] Util.del_ws_spc

  let unset = var_action "unset"
  let bare_export = var_action "export"

  let source =
    Util.indent
    . del /\.|source/ "." . label ".source"
    . Util.del_ws_spc . store /[^;=# \t\n]+/

  let shell_builtin_cmds = "ulimit" | "shift" | "exit"

  let eval =
    Util.indent . Util.del_str "eval" . Util.del_ws_spc
    . label "@eval" . store anyquot

  let alias =
    Util.indent . Util.del_str "alias" . Util.del_ws_spc
    . label "@alias" . store key_re . eq
    . [ label "value" . store anyquot ]

  let builtin =
    Util.indent . label "@builtin"
    . store shell_builtin_cmds
    . (Sep.cl_or_space
    . [ label "args" . sto_to_semicol ])?

  let keyword (kw:string) = Util.indent . Util.del_str kw
  let keyword_label (kw:string) (lbl:string) = keyword kw . label lbl

  let return =
    Util.indent . label "@return"
    . Util.del_str "return"
    . ( Util.del_ws_spc . store Rx.integer )?

  let action (operator:string) (lbl:string) (sto:lens) =
       let sp = Rx.cl_or_opt_space | /[ \t\n]+/
    in [ del (sp . operator . sp) (" " . operator . " ")
       . label ("@".lbl) . sto ]

  let action_pipe = action "|" "pipe"
  let action_and = action "&&" "and"
  let action_or = action "||" "or"

  let condition =
    let cond (start:string) (end:string) = [ label "type" . store start ]
                                         . Util.del_ws_spc . sto_to_semicol
                                         . Util.del_ws_spc . Util.del_str end
                                         . ( action_and sto_to_semicol | action_or sto_to_semicol )*
    in Util.indent . label "@condition" . (cond "[" "]" | cond "[[" "]]")

  (* Entry types *)
  let entry_eol_item (item:lens) = [ item . comment_or_eol ]
  let entry_item (item:lens) = [ item ]

  let entry_eol_nocommand =
      entry_eol_item source
        | entry_eol_item kv
        | entry_eol_item unset
        | entry_eol_item bare_export
        | entry_eol_item builtin
        | entry_eol_item return
        | entry_eol_item condition
        | entry_eol_item eval
        | entry_eol_item alias

  let entry_noeol_nocommand =
      entry_item source
        | entry_item kv
        | entry_item unset
        | entry_item bare_export
        | entry_item builtin
        | entry_item return
        | entry_item condition
        | entry_item eval
        | entry_item alias

  (* Command *)
  let rec command =
       let env = [ key key_re . eq . store anyquot . Sep.cl_or_space ]
    in let reserved_key = /exit|shift|return|ulimit|unset|export|source|\.|if|for|select|while|until|then|else|fi|done|case|eval|alias/
    in let word = /[A-Za-z0-9_.-\/]+/
    in let entry_eol = entry_eol_nocommand | entry_eol_item command
    in let entry_noeol = entry_noeol_nocommand | entry_item command
    in let entry = entry_eol | entry_noeol
    in let pipe = action_pipe (entry_eol_item command | entry_item command)
    in let and = action_and entry
    in let or = action_or entry
    in Util.indent . label "@command" . env* . store (word - reserved_key)
     . [ Sep.cl_or_space . label "@arg" . sto_to_semicol]?
     . ( pipe | and | or )?

  let entry_eol = entry_eol_nocommand
                | entry_eol_item command

  let entry_noeol = entry_noeol_nocommand
                  | entry_item command

(************************************************************************
 * Group:                 CONDITIONALS AND LOOPS
 *************************************************************************)

  let generic_cond_start (start_kw:string) (lbl:string)
                         (then_kw:string) (contents:lens) =
      keyword_label start_kw lbl . Sep.space
      . sto_to_semicol . semicol_eol
      . keyword then_kw . eol
      . contents

  let generic_cond (start_kw:string) (lbl:string)
                       (then_kw:string) (contents:lens) (end_kw:string) =
      [ generic_cond_start start_kw lbl then_kw contents
        . keyword end_kw . comment_or_eol ]

  let cond_if (entry:lens) =
    let elif = [ generic_cond_start "elif" "@elif" "then" entry+ ] in
    let else = [ keyword_label "else" "@else" . eol . entry+ ] in
    generic_cond "if" "@if" "then" (entry+ . elif* . else?) "fi"

  let loop_for (entry:lens) =
    generic_cond "for" "@for" "do" entry+ "done"

  let loop_while (entry:lens) =
    generic_cond "while" "@while" "do" entry+ "done"

  let loop_until (entry:lens) =
    generic_cond "until" "@until" "do" entry+ "done"

  let loop_select (entry:lens) =
    generic_cond "select" "@select" "do" entry+ "done"

  let case (entry:lens) (entry_noeol:lens) =
       let pattern = [ label "@pattern" . sto_to_semicol . Sep.opt_space ]
    in let case_entry = [ label "@case_entry"
                       . Util.indent . pattern
                       . (Util.del_str "|" . Sep.opt_space . pattern)*
                       . Util.del_str ")" . eol
                       . entry* . entry_noeol?
                       . Util.indent . Util.del_str ";;" . eol ] in
      [ keyword_label "case" "@case" . Sep.space
        . store (char+ | ("\"" . char+ . "\""))
        . del /[ \t\n]+/ " " . Util.del_str "in" . eol
        . (empty* . comment* . case_entry)*
        . empty* . comment*
        . keyword "esac" . comment_or_eol ]

  let subshell (entry:lens) =
    [ Util.indent . label "@subshell"
    . Util.del_str "{" . brace_eol
    . entry+
    . Util.indent . Util.del_str "}" . eol ]

  let function (entry:lens) =
    [ Util.indent . label "@function"
    . del /(function[ \t]+)?/ ""
    . store Rx.word . del /[ \t]*\(\)/ "()"
    . (comment_eol|brace_eol) . Util.del_str "{" . brace_eol
    . entry+
    . Util.indent . Util.del_str "}" . eol ]

  let rec rec_entry =
    let entry = comment | entry_eol | rec_entry in
        cond_if entry
      | loop_for entry
      | loop_select entry
      | loop_while entry
      | loop_until entry
      | case entry entry_noeol
      | function entry
      | subshell entry

  let lns_norec = del_empty* . (comment | entry_eol) *

  let lns = del_empty* . (comment | entry_eol | rec_entry) *

  let sc_incl (n:string) = (incl ("/etc/sysconfig/" . n))
  let sc_excl (n:string) = (excl ("/etc/sysconfig/" . n))

  let filter_sysconfig =
      sc_incl "*" .
      sc_excl "bootloader" .
      sc_excl "hw-uuid" .
      sc_excl "hwconf" .
      sc_excl "ip*tables" .
      sc_excl "ip*tables.save" .
      sc_excl "kernel" .
      sc_excl "*.pub" .
      sc_excl "sysstat.ioconf" .
      sc_excl "system-config-firewall" .
      sc_excl "system-config-securitylevel" .
      sc_incl "network/config" .
      sc_incl "network/dhcp" .
      sc_incl "network/dhcp6r" .
      sc_incl "network/dhcp6s" .
      sc_incl "network/ifcfg-*" .
      sc_incl "network/if-down.d/*" .
      sc_incl "network/ifroute-*" .
      sc_incl "network/if-up.d/*" .
      sc_excl "network/if-up.d/SuSEfirewall2" .
      sc_incl "network/providers/*" .
      sc_excl "network-scripts" .
      sc_incl "network-scripts/ifcfg-*" .
      sc_excl "rhn" .
      sc_incl "rhn/allowed-actions/*" .
      sc_excl "rhn/allowed-actions/script" .
      sc_incl "rhn/allowed-actions/script/*" .
      sc_incl "rhn/rhnsd" .
      sc_excl "SuSEfirewall2.d" .
      sc_incl "SuSEfirewall2.d/cobbler" .
      sc_incl "SuSEfirewall2.d/services/*" .
      sc_excl "SuSEfirewall2.d/services/TEMPLATE" .
      sc_excl "*.systemd"

  let filter_default = incl "/etc/default/*"
                     . excl "/etc/default/grub_installdevice*"
                     . excl "/etc/default/rmt"
                     . excl "/etc/default/star"
                     . excl "/etc/default/whoopsie"
  let filter_misc    = incl "/etc/arno-iptables-firewall/debconf.cfg"
                     . incl "/etc/conf.d/*"
                     . incl "/etc/cron-apt/config"
                     . incl "/etc/environment"
                     . incl "/etc/firewalld/firewalld.conf"
                     . incl "/etc/blkid.conf"
                     . incl "/etc/adduser.conf"
                     . incl "/etc/cowpoke.conf"
                     . incl "/etc/cvs-cron.conf"
                     . incl "/etc/cvs-pserver.conf"
                     . incl "/etc/devscripts.conf"
                     . incl "/etc/kamailio/kamctlrc"
                     . incl "/etc/lintianrc"
                     . incl "/etc/lsb-release"
                     . incl "/etc/os-release"
                     . incl "/etc/periodic.conf"
                     . incl "/etc/popularity-contest.conf"
                     . incl "/etc/rc.conf"
                     . incl "/etc/rc.conf.local"
                     . incl "/etc/selinux/config"
                     . incl "/etc/ucf.conf"
                     . incl "/etc/locale.conf"
                     . incl "/etc/vconsole.conf"

  let filter = filter_sysconfig
             . filter_default
             . filter_misc
             . Util.stdexcl

  let xfm = transform lns filter

(* Local Variables: *)
(* mode: caml       *)
(* End:             *)


On FreeBSD (at least) the sudoers file is in /usr/local/etc/.

(*
Module: Sudoers
  Parses /etc/sudoers

Author: Raphael Pinson <raphink@gmail.com>

About: Reference
  This lens tries to keep as close as possible to `man sudoers` where possible.

For example, recursive definitions such as

     > Cmnd_Spec_List ::= Cmnd_Spec |
     >                    Cmnd_Spec ',' Cmnd_Spec_List

are replaced by

  >   let cmnd_spec_list = cmnd_spec . ( sep_com . cmnd_spec )*

since Augeas cannot deal with recursive definitions.
The definitions from `man sudoers` are put as commentaries for reference
throughout the file. More information can be found in the manual.

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

About: Lens Usage
  Sample usage of this lens in augtool

    * Set first Defaults to apply to the "LOCALNET" network alias
      > set /files/etc/sudoers/Defaults[1]/type "@LOCALNET"
    * List all user specifications applying explicitely to the "admin" Unix group
      > match /files/etc/sudoers/spec/user "%admin"
    * Remove the full 3rd user specification
      > rm /files/etc/sudoers/spec[3]

About: Configuration files
  This lens applies to /etc/sudoers. See <filter>.
*)

module Sudoers =
  autoload xfm

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

(* Group: Generic primitives *)
(* Variable: eol *)
let eol       = del /[ \t]*\n/ "\n"

(* Variable: indent *)
let indent    = del /[ \t]*/ ""

(* Group: Separators *)

(* Variable: sep_spc *)
let sep_spc  = del /[ \t]+/ " "

(* Variable: sep_cont *)
let sep_cont = del /([ \t]+|[ \t]*\\\\\n[ \t]*)/ " "

(* Variable: sep_cont_opt *)
let sep_cont_opt = del /([ \t]*|[ \t]*\\\\\n[ \t]*)/ " "

(* Variable: sep_com *)
let sep_com  = sep_cont_opt . Util.del_str "," . sep_cont_opt

(* Variable: sep_eq *)
let sep_eq   = sep_cont_opt . Util.del_str "=" . sep_cont_opt

(* Variable: sep_col *)
let sep_col  = sep_cont_opt . Util.del_str ":" . sep_cont_opt

(* Variable: sep_dquote *)
let sep_dquote   = Util.del_str "\""

(* Group: Stores *)

(* Variable: sto_to_com_cmnd
sto_to_com_cmnd does not begin or end with a space *)
let sto_to_com_cmnd = store /([^,=:#() \t\n\\\\]([^,=:#()\n\\\\]|\\\\[=:,\\\\])*[^,=:#() \t\n\\\\])|[^,=:#() \t\n\\\\]/

(* Variable: sto_to_com

There could be a \ in the middle of a command *)
let sto_to_com      = store /([^,=:#() \t\n\\\\][^,=:#()\n]*[^,=:#() \t\n\\\\])|[^,=:#() \t\n\\\\]/

(* Variable: sto_to_com_host *)
let sto_to_com_host = store /[^,=:#() \t\n\\\\]+/

(* Variable: sto_to_com_user
Escaped spaces are allowed *)
let sto_to_com_user = store ( /([^,=:#() \t\n]([^,=:#() \t\n]|(\\\\[ \t]))*[^,=:#() \t\n])|[^,=:#() \t\n]/
                              - /(User|Runas|Host|Cmnd)_Alias|Defaults.*/ )

(* Variable: sto_to_com_col *)
let sto_to_com_col      = store /[^",=#() \t\n\\\\]+/ (* " relax emacs *)

(* Variable: sto_to_eq *)
let sto_to_eq  = store /[^,=:#() \t\n\\\\]+/

(* Variable: sto_to_spc *)
let sto_to_spc = store /[^", \t\n\\\\]+|"[^", \t\n\\\\]+"/

(* Variable: sto_to_spc_no_dquote *)
let sto_to_spc_no_dquote = store /[^", \t\n\\\\]+/ (* " relax emacs *)

(* Variable: sto_integer *)
let sto_integer = store /[0-9]+/

(* Group: Comments and empty lines *)

(* View: comment
Map comments in "#comment" nodes *)
let comment =
  let sto_to_eol = store /([^ \t\n].*[^ \t\n]|[^ \t\n])/ in
  [ label "#comment" . del /[ \t]*#[ \t]*/ "# " . sto_to_eol . eol ]

(* View: empty
Map empty lines *)
let empty   = [ del /[ \t]*#?[ \t]*\n/ "\n" ]

(************************************************************************
 * Group:                                    ALIASES
 *************************************************************************)

(************************************************************************
 * View: alias_field
 *   Generic alias field to gather all Alias definitions
 *
 *   Definition:
 *     > User_Alias ::= NAME '=' User_List
 *     > Runas_Alias ::= NAME '=' Runas_List
 *     > Host_Alias ::= NAME '=' Host_List
 *     > Cmnd_Alias ::= NAME '=' Cmnd_List
 *
 *   Parameters:
 *     kw:string - the label string
 *     sto:lens  - the store lens
 *************************************************************************)
let alias_field (kw:string) (sto:lens) = [ label kw . sto ]

(* View: alias_list
     List of <alias_fields>, separated by commas *)
let alias_list  (kw:string) (sto:lens) =
  alias_field kw sto . ( sep_com . alias_field kw sto )*

(************************************************************************
 * View: alias_name
 *   Name of an <alias_entry_single>
 *
 *   Definition:
 *     > NAME ::= [A-Z]([A-Z][0-9]_)*
 *************************************************************************)
let alias_name
    = [ label "name" . store /[A-Z][A-Z0-9_]*/ ]

(************************************************************************
 * View: alias_entry_single
 *   Single <alias_entry>, named using <alias_name> and listing <alias_list>
 *
 *   Definition:
 *     > Alias_Type NAME = item1, item2, ...
 *
 *   Parameters:
 *     field:string - the field name, passed to <alias_list>
 *     sto:lens     - the store lens, passed to <alias_list>
 *************************************************************************)
let alias_entry_single (field:string) (sto:lens)
    = [ label "alias" . alias_name . sep_eq . alias_list field sto ]

(************************************************************************
 * View: alias_entry
 *   Alias entry, a list of comma-separated <alias_entry_single> fields
 *
 *   Definition:
 *     > Alias_Type NAME = item1, item2, item3 : NAME = item4, item5
 *
 *   Parameters:
 *     kw:string    - the alias keyword string
 *     field:string - the field name, passed to <alias_entry_single>
 *     sto:lens     - the store lens, passed to <alias_entry_single>
 *************************************************************************)
let alias_entry (kw:string) (field:string) (sto:lens)
    = [ indent . key kw . sep_cont . alias_entry_single field sto
          . ( sep_col . alias_entry_single field sto )* . eol ]

(* TODO: go further in user definitions *)
(* View: user_alias
     User_Alias, see <alias_field> *)
let user_alias  = alias_entry "User_Alias" "user" sto_to_com
(* View: runas_alias
     Run_Alias, see <alias_field> *)
let runas_alias = alias_entry "Runas_Alias" "runas_user" sto_to_com
(* View: host_alias
     Host_Alias, see <alias_field> *)
let host_alias  = alias_entry "Host_Alias" "host" sto_to_com
(* View: cmnd_alias
     Cmnd_Alias, see <alias_field> *)
let cmnd_alias  = alias_entry "Cmnd_Alias" "command" sto_to_com_cmnd

(************************************************************************
 * View: alias
 *   Every kind of Alias entry,
 *     see <user_alias>, <runas_alias>, <host_alias> and <cmnd_alias>
 *
 *   Definition:
 *     > Alias ::= 'User_Alias'  User_Alias (':' User_Alias)* |
 *     >           'Runas_Alias' Runas_Alias (':' Runas_Alias)* |
 *     >           'Host_Alias'  Host_Alias (':' Host_Alias)* |
 *     >           'Cmnd_Alias'  Cmnd_Alias (':' Cmnd_Alias)*
 *************************************************************************)
let alias = user_alias | runas_alias | host_alias | cmnd_alias
(************************************************************************
 * Group:                          DEFAULTS
 *************************************************************************)

(************************************************************************
 * View: default_type
 *   Type definition for <defaults>
 *
 *   Definition:
 *     > Default_Type ::= 'Defaults' |
 *     >                  'Defaults' '@' Host_List |
 *     >                  'Defaults' ':' User_List |
 *     >                  'Defaults' '>' Runas_List
 *************************************************************************)
let default_type     =
  let value = store /[@:>][^ \t\n\\\\]+/ in
  [ label "type" . value ]

(************************************************************************
 * View: del_negate
 *   Delete an even number of '!' signs
 *************************************************************************)
let del_negate = del /(!!)*/ ""

(************************************************************************
 * View: negate_node
 *   Negation of boolean values for <defaults>. Accept one optional '!'
 *   and produce a 'negate' node if there is one.
 *************************************************************************)
let negate_node = [ del "!" "!" . label "negate" ]

let negate_or_value (key:lens) (value:lens) =
  [ del_negate . (negate_node . key | key . value) ]

(************************************************************************
 * View: parameter_flag
 *   A flag parameter for <defaults>
 *
 *   Flags are implicitly boolean and can be turned off via the '!'  operator.
 *   Some integer, string and list parameters may also be used in a boolean
 *     context to disable them.
 *************************************************************************)
let parameter_flag_kw    = "always_set_home" | "authenticate" | "env_editor"
                         | "env_reset" | "fqdn" | "ignore_dot"
                         | "ignore_local_sudoers" | "insults" | "log_host"
                         | "log_year" | "long_otp_prompt" | "mail_always"
                         | "mail_badpass" | "mail_no_host" | "mail_no_perms"
                         | "mail_no_user" | "noexec" | "path_info"
                         | "passprompt_override" | "preserve_groups"
                         | "requiretty" | "root_sudo" | "rootpw" | "runaspw"
                         | "set_home" | "set_logname" | "setenv"
                         | "shell_noargs" | "stay_setuid" | "targetpw"
                         | "tty_tickets" | "visiblepw"

let parameter_flag       = [ del_negate . negate_node?
                               . key parameter_flag_kw ]

(************************************************************************
 * View: parameter_integer
 *   An integer parameter for <defaults>
 *************************************************************************)
let parameter_integer_nobool_kw = "passwd_tries"

let parameter_integer_nobool    = [ key parameter_integer_nobool_kw . sep_eq
                                      . del /"?/ "" . sto_integer
                                      . del /"?/ "" ]

let parameter_integer_bool_kw   = "loglinelen" | "passwd_timeout"
                                | "timestamp_timeout" | "umask"

let parameter_integer_bool      =
  negate_or_value
    (key parameter_integer_bool_kw)
    (sep_eq . del /"?/ "" . sto_integer . del /"?/ "")

let parameter_integer           = parameter_integer_nobool
                                | parameter_integer_bool

(************************************************************************
 * View: parameter_string
 *   A string parameter for <defaults>
 *
 *   An odd number of '!' operators negate the value of the item;
 *      an even number just cancel each other out.
 *************************************************************************)
let parameter_string_nobool_kw = "badpass_message" | "editor" | "mailsub"
                               | "noexec_file" | "passprompt" | "runas_default"
                               | "syslog_badpri" | "syslog_goodpri"
                               | "timestampdir" | "timestampowner" | "secure_path"

let parameter_string_nobool    = [ key parameter_string_nobool_kw . sep_eq
                                     . del /"?/ "" . sto_to_com_col
                                     . del /"?/ "" ]

let parameter_string_bool_kw   = "exempt_group" | "lecture" | "lecture_file"
                               | "listpw" | "logfile" | "mailerflags"
                               | "mailerpath" | "mailto" | "exempt_group"
                               | "syslog" | "verifypw" | "logfile"
                               | "mailerflags" | "mailerpath" | "mailto"
                               | "syslog" | "verifypw"

let parameter_string_bool      =
  negate_or_value
    (key parameter_string_bool_kw)
    (sep_eq . sto_to_com_col)

let parameter_string           = parameter_string_nobool
                               | parameter_string_bool

(************************************************************************
 * View: parameter_lists
 *   A single list parameter for <defaults>
 *
 *   All lists can be used in a boolean context
 *   The argument may be a double-quoted, space-separated list or a single
 *      value without double-quotes.
 *   The list can be replaced, added to, deleted from, or disabled
 *      by using the =, +=, -=, and ! operators respectively.
 *   An odd number of '!' operators negate the value of the item;
 *      an even number just cancel each other out.
 *************************************************************************)
let parameter_lists_kw           = "env_check" | "env_delete" | "env_keep"
let parameter_lists_value        = [ label "var" . sto_to_spc_no_dquote ]
let parameter_lists_value_dquote = [ label "var"
                                     . del /"?/ "" . sto_to_spc_no_dquote
                                     . del /"?/ "" ]

let parameter_lists_values = parameter_lists_value_dquote
                           | ( sep_dquote . parameter_lists_value
                               . ( sep_cont . parameter_lists_value )+
                               . sep_dquote )

let parameter_lists_sep    = sep_cont_opt
                             . ( [ del "+" "+" . label "append" ]
                               | [ del "-" "-" . label "remove" ] )?
                             . del "=" "=" . sep_cont_opt

let parameter_lists        =
  negate_or_value
    (key parameter_lists_kw)
    (parameter_lists_sep . parameter_lists_values)

(************************************************************************
 * View: parameter
 *   A single parameter for <defaults>
 *
 *   Definition:
 *     > Parameter ::= Parameter '=' Value |
 *     >               Parameter '+=' Value |
 *     >               Parameter '-=' Value |
 *     >               '!'* Parameter
 *
 *     Parameters may be flags, integer values, strings, or lists.
 *
 *************************************************************************)
let parameter        = parameter_flag | parameter_integer
                     | parameter_string | parameter_lists

(************************************************************************
 * View: parameter_list
 *   A list of comma-separated <parameters> for <defaults>
 *
 *   Definition:
 *     > Parameter_List ::= Parameter |
 *     >                    Parameter ',' Parameter_List
 *************************************************************************)
let parameter_list   = parameter . ( sep_com . parameter )*

(************************************************************************
 * View: defaults
 *   A Defaults entry
 *
 *   Definition:
 *     > Default_Entry ::= Default_Type Parameter_List
 *************************************************************************)
let defaults = [ indent . key "Defaults" . default_type? . sep_cont
                   . parameter_list . eol ]

(************************************************************************
 * Group:                     USER SPECIFICATION
 *************************************************************************)

(************************************************************************
 * View: runas_spec
 *   A runas specification for <spec>, using <alias_list>
 *
 *   Definition:
 *     > Runas_Spec ::= '(' Runas_List ')'
 *************************************************************************)
let runas_spec = Util.del_str "(" . alias_list "runas_user" sto_to_com
    . Util.del_str ")" . sep_cont_opt

(************************************************************************
 * View: tag_spec
 *   Tag specification for <spec>
 *
 *   Definition:
 *     > Tag_Spec ::= ('NOPASSWD:' | 'PASSWD:' | 'NOEXEC:' | 'EXEC:' |
 *     >              'SETENV:' | 'NOSETENV:')
 *************************************************************************)
let tag_spec   =
  [ label "tag" . store /(NO)?(PASSWD|EXEC|SETENV)/ . sep_col ]

(************************************************************************
 * View: cmnd_spec
 *   Command specification for <spec>,
 *     with optional <runas_spec> and any amount of <tag_specs>
 *
 *   Definition:
 *     > Cmnd_Spec ::= Runas_Spec? Tag_Spec* Cmnd
 *************************************************************************)
let cmnd_spec  =
  [ label "command" .  runas_spec? . tag_spec* . sto_to_com_cmnd ]

(************************************************************************
 * View: cmnd_spec_list
 *   A list of comma-separated <cmnd_specs>
 *
 *   Definition:
 *     > Cmnd_Spec_List ::= Cmnd_Spec |
 *     >                    Cmnd_Spec ',' Cmnd_Spec_List
 *************************************************************************)
let cmnd_spec_list = cmnd_spec . ( sep_com . cmnd_spec )*

(************************************************************************
 * View: spec_list
 *   Group of hosts with <cmnd_spec_list>
 *************************************************************************)
let spec_list = [ label "host_group" . alias_list "host" sto_to_com_host
                    . sep_eq . cmnd_spec_list ]

(************************************************************************
 * View: spec
 *   A user specification, listing colon-separated <spec_lists>
 *
 *   Definition:
 *     > User_Spec ::= User_List Host_List '=' Cmnd_Spec_List \
 *     >               (':' Host_List '=' Cmnd_Spec_List)*
 *************************************************************************)
let spec = [ label "spec" . indent
               . alias_list "user" sto_to_com_user . sep_cont
               . spec_list
               . ( sep_col . spec_list )* . eol ]

(************************************************************************
 * Group:                        LENS & FILTER
 *************************************************************************)

(* View: lns
     The sudoers lens, any amount of
       * <empty> lines
       * <comments>
       * <aliases>
       * <defaults>
       * <specs>
*)
let lns = ( empty | comment | alias | defaults | spec  )*

(* Variable: filter *)
let filter = (incl "/etc/sudoers")
    . (incl "/usr/local/etc/sudoers")
    . (incl "/etc/sudoers.d/*")
    . (incl "/usr/local/etc/sudoers.d/*")
    . Util.stdexcl

let xfm = transform lns filter

(*
Module: Sudoers
  Parses /etc/sudoers

Author: Raphael Pinson <raphink@gmail.com>

About: Reference
  This lens tries to keep as close as possible to `man sudoers` where possible.

For example, recursive definitions such as

     > Cmnd_Spec_List ::= Cmnd_Spec |
     >                    Cmnd_Spec ',' Cmnd_Spec_List

are replaced by

  >   let cmnd_spec_list = cmnd_spec . ( sep_com . cmnd_spec )*

since Augeas cannot deal with recursive definitions.
The definitions from `man sudoers` are put as commentaries for reference
throughout the file. More information can be found in the manual.

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

About: Lens Usage
  Sample usage of this lens in augtool

    * Set first Defaults to apply to the "LOCALNET" network alias
      > set /files/etc/sudoers/Defaults[1]/type "@LOCALNET"
    * List all user specifications applying explicitely to the "admin" Unix group
      > match /files/etc/sudoers/spec/user "%admin"
    * Remove the full 3rd user specification
      > rm /files/etc/sudoers/spec[3]

About: Configuration files
  This lens applies to /etc/sudoers. See <filter>.
*)

module Sudoers =
  autoload xfm

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

(* Group: Generic primitives *)
(* Variable: eol *)
let eol       = del /[ \t]*\n/ "\n"

(* Variable: indent *)
let indent    = del /[ \t]*/ ""

(* Group: Separators *)

(* Variable: sep_spc *)
let sep_spc  = del /[ \t]+/ " "

(* Variable: sep_cont *)
let sep_cont = del /([ \t]+|[ \t]*\\\\\n[ \t]*)/ " "

(* Variable: sep_cont_opt *)
let sep_cont_opt = del /([ \t]*|[ \t]*\\\\\n[ \t]*)/ " "

(* Variable: sep_com *)
let sep_com  = sep_cont_opt . Util.del_str "," . sep_cont_opt

(* Variable: sep_eq *)
let sep_eq   = sep_cont_opt . Util.del_str "=" . sep_cont_opt

(* Variable: sep_col *)
let sep_col  = sep_cont_opt . Util.del_str ":" . sep_cont_opt

(* Variable: sep_dquote *)
let sep_dquote   = Util.del_str "\""

(* Group: Stores *)

(* Variable: sto_to_com_cmnd
sto_to_com_cmnd does not begin or end with a space *)
let sto_to_com_cmnd = store /([^,=:#() \t\n\\\\]([^,=:#()\n\\\\]|\\\\[=:,\\\\])*[^,=:#() \t\n\\\\])|[^,=:#() \t\n\\\\]/

(* Variable: sto_to_com

There could be a \ in the middle of a command *)
let sto_to_com      = store /([^,=:#() \t\n\\\\][^,=:#()\n]*[^,=:#() \t\n\\\\])|[^,=:#() \t\n\\\\]/

(* Variable: sto_to_com_host *)
let sto_to_com_host = store /[^,=:#() \t\n\\\\]+/

(* Variable: sto_to_com_user
Escaped spaces are allowed *)
let sto_to_com_user = store ( /([^,=:#() \t\n]([^,=:#() \t\n]|(\\\\[ \t]))*[^,=:#() \t\n])|[^,=:#() \t\n]/
                              - /(User|Runas|Host|Cmnd)_Alias|Defaults.*/ )

(* Variable: sto_to_com_col *)
let sto_to_com_col      = store /[^",=#() \t\n\\\\]+/ (* " relax emacs *)

(* Variable: sto_to_eq *)
let sto_to_eq  = store /[^,=:#() \t\n\\\\]+/

(* Variable: sto_to_spc *)
let sto_to_spc = store /[^", \t\n\\\\]+|"[^", \t\n\\\\]+"/

(* Variable: sto_to_spc_no_dquote *)
let sto_to_spc_no_dquote = store /[^", \t\n\\\\]+/ (* " relax emacs *)

(* Variable: sto_integer *)
let sto_integer = store /[0-9]+/

(* Group: Comments and empty lines *)

(* View: comment
Map comments in "#comment" nodes *)
let comment =
  let sto_to_eol = store /([^ \t\n].*[^ \t\n]|[^ \t\n])/ in
  [ label "#comment" . del /[ \t]*#[ \t]*/ "# " . sto_to_eol . eol ]

(* View: empty
Map empty lines *)
let empty   = [ del /[ \t]*#?[ \t]*\n/ "\n" ]

(************************************************************************
 * Group:                                    ALIASES
 *************************************************************************)

(************************************************************************
 * View: alias_field
 *   Generic alias field to gather all Alias definitions
 *
 *   Definition:
 *     > User_Alias ::= NAME '=' User_List
 *     > Runas_Alias ::= NAME '=' Runas_List
 *     > Host_Alias ::= NAME '=' Host_List
 *     > Cmnd_Alias ::= NAME '=' Cmnd_List
 *
 *   Parameters:
 *     kw:string - the label string
 *     sto:lens  - the store lens
 *************************************************************************)
let alias_field (kw:string) (sto:lens) = [ label kw . sto ]

(* View: alias_list
     List of <alias_fields>, separated by commas *)
let alias_list  (kw:string) (sto:lens) =
  alias_field kw sto . ( sep_com . alias_field kw sto )*

(************************************************************************
 * View: alias_name
 *   Name of an <alias_entry_single>
 *
 *   Definition:
 *     > NAME ::= [A-Z]([A-Z][0-9]_)*
 *************************************************************************)
let alias_name
    = [ label "name" . store /[A-Z][A-Z0-9_]*/ ]

(************************************************************************
 * View: alias_entry_single
 *   Single <alias_entry>, named using <alias_name> and listing <alias_list>
 *
 *   Definition:
 *     > Alias_Type NAME = item1, item2, ...
 *
 *   Parameters:
 *     field:string - the field name, passed to <alias_list>
 *     sto:lens     - the store lens, passed to <alias_list>
 *************************************************************************)
let alias_entry_single (field:string) (sto:lens)
    = [ label "alias" . alias_name . sep_eq . alias_list field sto ]

(************************************************************************
 * View: alias_entry
 *   Alias entry, a list of comma-separated <alias_entry_single> fields
 *
 *   Definition:
 *     > Alias_Type NAME = item1, item2, item3 : NAME = item4, item5
 *
 *   Parameters:
 *     kw:string    - the alias keyword string
 *     field:string - the field name, passed to <alias_entry_single>
 *     sto:lens     - the store lens, passed to <alias_entry_single>
 *************************************************************************)
let alias_entry (kw:string) (field:string) (sto:lens)
    = [ indent . key kw . sep_cont . alias_entry_single field sto
          . ( sep_col . alias_entry_single field sto )* . eol ]

(* TODO: go further in user definitions *)
(* View: user_alias
     User_Alias, see <alias_field> *)
let user_alias  = alias_entry "User_Alias" "user" sto_to_com
(* View: runas_alias
     Run_Alias, see <alias_field> *)
let runas_alias = alias_entry "Runas_Alias" "runas_user" sto_to_com
(* View: host_alias
     Host_Alias, see <alias_field> *)
let host_alias  = alias_entry "Host_Alias" "host" sto_to_com
(* View: cmnd_alias
     Cmnd_Alias, see <alias_field> *)
let cmnd_alias  = alias_entry "Cmnd_Alias" "command" sto_to_com_cmnd

(************************************************************************
 * View: alias
 *   Every kind of Alias entry,
 *     see <user_alias>, <runas_alias>, <host_alias> and <cmnd_alias>
 *
 *   Definition:
 *     > Alias ::= 'User_Alias'  User_Alias (':' User_Alias)* |
 *     >           'Runas_Alias' Runas_Alias (':' Runas_Alias)* |
 *     >           'Host_Alias'  Host_Alias (':' Host_Alias)* |
 *     >           'Cmnd_Alias'  Cmnd_Alias (':' Cmnd_Alias)*
 *************************************************************************)
let alias = user_alias | runas_alias | host_alias | cmnd_alias
(************************************************************************
 * Group:                          DEFAULTS
 *************************************************************************)

(************************************************************************
 * View: default_type
 *   Type definition for <defaults>
 *
 *   Definition:
 *     > Default_Type ::= 'Defaults' |
 *     >                  'Defaults' '@' Host_List |
 *     >                  'Defaults' ':' User_List |
 *     >                  'Defaults' '>' Runas_List
 *************************************************************************)
let default_type     =
  let value = store /[@:>][^ \t\n\\\\]+/ in
  [ label "type" . value ]

(************************************************************************
 * View: del_negate
 *   Delete an even number of '!' signs
 *************************************************************************)
let del_negate = del /(!!)*/ ""

(************************************************************************
 * View: negate_node
 *   Negation of boolean values for <defaults>. Accept one optional '!'
 *   and produce a 'negate' node if there is one.
 *************************************************************************)
let negate_node = [ del "!" "!" . label "negate" ]

let negate_or_value (key:lens) (value:lens) =
  [ del_negate . (negate_node . key | key . value) ]

(************************************************************************
 * View: parameter_flag
 *   A flag parameter for <defaults>
 *
 *   Flags are implicitly boolean and can be turned off via the '!'  operator.
 *   Some integer, string and list parameters may also be used in a boolean
 *     context to disable them.
 *************************************************************************)
let parameter_flag_kw    = "always_set_home" | "authenticate" | "env_editor"
                         | "env_reset" | "fqdn" | "ignore_dot"
                         | "ignore_local_sudoers" | "insults" | "log_host"
                         | "log_year" | "long_otp_prompt" | "mail_always"
                         | "mail_badpass" | "mail_no_host" | "mail_no_perms"
                         | "mail_no_user" | "noexec" | "path_info"
                         | "passprompt_override" | "preserve_groups"
                         | "requiretty" | "root_sudo" | "rootpw" | "runaspw"
                         | "set_home" | "set_logname" | "setenv"
                         | "shell_noargs" | "stay_setuid" | "targetpw"
                         | "tty_tickets" | "visiblepw"

let parameter_flag       = [ del_negate . negate_node?
                               . key parameter_flag_kw ]

(************************************************************************
 * View: parameter_integer
 *   An integer parameter for <defaults>
 *************************************************************************)
let parameter_integer_nobool_kw = "passwd_tries"

let parameter_integer_nobool    = [ key parameter_integer_nobool_kw . sep_eq
                                      . del /"?/ "" . sto_integer
                                      . del /"?/ "" ]

let parameter_integer_bool_kw   = "loglinelen" | "passwd_timeout"
                                | "timestamp_timeout" | "umask"

let parameter_integer_bool      =
  negate_or_value
    (key parameter_integer_bool_kw)
    (sep_eq . del /"?/ "" . sto_integer . del /"?/ "")

let parameter_integer           = parameter_integer_nobool
                                | parameter_integer_bool

(************************************************************************
 * View: parameter_string
 *   A string parameter for <defaults>
 *
 *   An odd number of '!' operators negate the value of the item;
 *      an even number just cancel each other out.
 *************************************************************************)
let parameter_string_nobool_kw = "badpass_message" | "editor" | "mailsub"
                               | "noexec_file" | "passprompt" | "runas_default"
                               | "syslog_badpri" | "syslog_goodpri"
                               | "timestampdir" | "timestampowner" | "secure_path"

let parameter_string_nobool    = [ key parameter_string_nobool_kw . sep_eq
                                     . del /"?/ "" . sto_to_com_col
                                     . del /"?/ "" ]

let parameter_string_bool_kw   = "exempt_group" | "lecture" | "lecture_file"
                               | "listpw" | "logfile" | "mailerflags"
                               | "mailerpath" | "mailto" | "exempt_group"
                               | "syslog" | "verifypw" | "logfile"
                               | "mailerflags" | "mailerpath" | "mailto"
                               | "syslog" | "verifypw"

let parameter_string_bool      =
  negate_or_value
    (key parameter_string_bool_kw)
    (sep_eq . sto_to_com_col)

let parameter_string           = parameter_string_nobool
                               | parameter_string_bool

(************************************************************************
 * View: parameter_lists
 *   A single list parameter for <defaults>
 *
 *   All lists can be used in a boolean context
 *   The argument may be a double-quoted, space-separated list or a single
 *      value without double-quotes.
 *   The list can be replaced, added to, deleted from, or disabled
 *      by using the =, +=, -=, and ! operators respectively.
 *   An odd number of '!' operators negate the value of the item;
 *      an even number just cancel each other out.
 *************************************************************************)
let parameter_lists_kw           = "env_check" | "env_delete" | "env_keep"
let parameter_lists_value        = [ label "var" . sto_to_spc_no_dquote ]
let parameter_lists_value_dquote = [ label "var"
                                     . del /"?/ "" . sto_to_spc_no_dquote
                                     . del /"?/ "" ]

let parameter_lists_values = parameter_lists_value_dquote
                           | ( sep_dquote . parameter_lists_value
                               . ( sep_cont . parameter_lists_value )+
                               . sep_dquote )

let parameter_lists_sep    = sep_cont_opt
                             . ( [ del "+" "+" . label "append" ]
                               | [ del "-" "-" . label "remove" ] )?
                             . del "=" "=" . sep_cont_opt

let parameter_lists        =
  negate_or_value
    (key parameter_lists_kw)
    (parameter_lists_sep . parameter_lists_values)

(************************************************************************
 * View: parameter
 *   A single parameter for <defaults>
 *
 *   Definition:
 *     > Parameter ::= Parameter '=' Value |
 *     >               Parameter '+=' Value |
 *     >               Parameter '-=' Value |
 *     >               '!'* Parameter
 *
 *     Parameters may be flags, integer values, strings, or lists.
 *
 *************************************************************************)
let parameter        = parameter_flag | parameter_integer
                     | parameter_string | parameter_lists

(************************************************************************
 * View: parameter_list
 *   A list of comma-separated <parameters> for <defaults>
 *
 *   Definition:
 *     > Parameter_List ::= Parameter |
 *     >                    Parameter ',' Parameter_List
 *************************************************************************)
let parameter_list   = parameter . ( sep_com . parameter )*

(************************************************************************
 * View: defaults
 *   A Defaults entry
 *
 *   Definition:
 *     > Default_Entry ::= Default_Type Parameter_List
 *************************************************************************)
let defaults = [ indent . key "Defaults" . default_type? . sep_cont
                   . parameter_list . eol ]

(************************************************************************
 * Group:                     USER SPECIFICATION
 *************************************************************************)

(************************************************************************
 * View: runas_spec
 *   A runas specification for <spec>, using <alias_list>
 *
 *   Definition:
 *     > Runas_Spec ::= '(' Runas_List ')'
 *************************************************************************)
let runas_spec = Util.del_str "(" . alias_list "runas_user" sto_to_com
    . Util.del_str ")" . sep_cont_opt

(************************************************************************
 * View: tag_spec
 *   Tag specification for <spec>
 *
 *   Definition:
 *     > Tag_Spec ::= ('NOPASSWD:' | 'PASSWD:' | 'NOEXEC:' | 'EXEC:' |
 *     >              'SETENV:' | 'NOSETENV:')
 *************************************************************************)
let tag_spec   =
  [ label "tag" . store /(NO)?(PASSWD|EXEC|SETENV)/ . sep_col ]

(************************************************************************
 * View: cmnd_spec
 *   Command specification for <spec>,
 *     with optional <runas_spec> and any amount of <tag_specs>
 *
 *   Definition:
 *     > Cmnd_Spec ::= Runas_Spec? Tag_Spec* Cmnd
 *************************************************************************)
let cmnd_spec  =
  [ label "command" .  runas_spec? . tag_spec* . sto_to_com_cmnd ]

(************************************************************************
 * View: cmnd_spec_list
 *   A list of comma-separated <cmnd_specs>
 *
 *   Definition:
 *     > Cmnd_Spec_List ::= Cmnd_Spec |
 *     >                    Cmnd_Spec ',' Cmnd_Spec_List
 *************************************************************************)
let cmnd_spec_list = cmnd_spec . ( sep_com . cmnd_spec )*

(************************************************************************
 * View: spec_list
 *   Group of hosts with <cmnd_spec_list>
 *************************************************************************)
let spec_list = [ label "host_group" . alias_list "host" sto_to_com_host
                    . sep_eq . cmnd_spec_list ]

(************************************************************************
 * View: spec
 *   A user specification, listing colon-separated <spec_lists>
 *
 *   Definition:
 *     > User_Spec ::= User_List Host_List '=' Cmnd_Spec_List \
 *     >               (':' Host_List '=' Cmnd_Spec_List)*
 *************************************************************************)
let spec = [ label "spec" . indent
               . alias_list "user" sto_to_com_user . sep_cont
               . spec_list
               . ( sep_col . spec_list )* . eol ]

(************************************************************************
 * Group:                        LENS & FILTER
 *************************************************************************)

(* View: lns
     The sudoers lens, any amount of
       * <empty> lines
       * <comments>
       * <aliases>
       * <defaults>
       * <specs>
*)
let lns = ( empty | comment | alias | defaults | spec  )*

(* Variable: filter *)
let filter = (incl "/etc/sudoers")
    . (incl "/etc/sudoers.d/*")
    . Util.stdexcl

let xfm = transform lns filter


Add comment_c_style and comment_multiline Add empty_generic and make empty a call to it Add empty_c_style to complement comment_c_style

(*
Module: Util
  Generic module providing useful primitives

Author: David Lutterkort

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

module Util =

(*
Variable: del_str
  Delete a string and default to it

  Parameters:
     s:string - the string to delete and default to
*)
  let del_str (s:string) = del s s

(*
Variable: del_ws
  Delete mandatory whitespace
*)
  let del_ws = del /[ \t]+/

(*
Variable: del_ws_spc
  Delete mandatory whitespace, default to single space
*)
  let del_ws_spc = del_ws " "

(*
Variable: del_ws_tab
  Delete mandatory whitespace, default to single tab
*)
  let del_ws_tab = del_ws "\t"

(*
Variable: del_opt_ws
  Delete optional whitespace
*)
  let del_opt_ws = del /[ \t]*/

(*
Variable: eol
  Delete end of line, including optional trailing whitespace
*)
  let eol = del /[ \t]*\n/ "\n"

(*
Variable: indent
  Delete indentation, including leading whitespace
*)
  let indent = del /[ \t]*/ ""

(* Group: Comment
     This is a general definition of comment
     It allows indentation for comments, removes the leading and trailing spaces
     of comments and stores them in nodes, except for empty comments which are
     ignored together with empty lines

View: comment_generic
  Map comments and set default comment sign
*)

  let comment_generic (r:regexp) (d:string) =
    [ label "#comment" . del r d
        . store /([^ \t\n].*[^ \t\n]|[^ \t\n])/ . eol ]

(* View: comment
  Map comments into "#comment" nodes
*)
  let comment = comment_generic /[ \t]*#[ \t]*/ "# "

(* View: comment_eol
  Map eol comments into "#comment" nodes
  Add a space before # for end of line comments
*)
  let comment_eol = comment_generic /[ \t]*#[ \t]*/ " # "

(* View: comment_or_eol
    A <comment_eol> or <eol> *)
 let comment_or_eol = comment_eol | (del /[ \t]*#?\n/ "\n")

(* View: comment_multiline
    A C-style multiline comment *)
  let comment_multiline =
     let mline_re = (/[^ \t\n].*[^ \t\n]|[^ \t\n]/ - /.*\*\/.*/) in
     let mline = [ seq "mline"
                 . store mline_re ] in
     [ label "#mcomment" . del /[ \t]*\/\*[ \t\n]*/ "/*\n"
       . counter "mline"
       . (mline . (eol . mline)*)
       . del /[ \t\n]*\*\/[ \t]*\n/ "\n*/\n" ]

(* View: comment_c_style
    A comment line, C-style *)
  let comment_c_style =
    comment_generic /[ \t]*\/\/[ \t]*/ "// "

(* View: empty_generic
  A generic definition of <empty>
  Map empty lines, including empty comments *)
  let empty_generic (r:regexp) =
    [ del r "" . del_str "\n" ]

(* View: empty
  Map empty lines, including empty comments *)
  let empty = empty_generic /[ \t]*#?[ \t]*/

(* View: empty_c_style
  Map empty lines, including C-style empty comment *)
  let empty_c_style =
    empty_generic /[ \t]*((\/\/)|(\/\*[ \t]*\*\/))?[ \t]*/

(* View: Split *)
(* Split (SEP . ELT)* into an array-like tree where each match for ELT *)
(* appears in a separate subtree. The labels for the subtrees are      *)
(* consecutive numbers, starting at 0                                  *)
  let split (elt:lens) (sep:lens) =
    let sym = gensym "split" in
    counter sym . ( [ seq sym . sep . elt ] ) *

(* Group: Exclusions

Variable: stdexcl
  Exclusion for files that are commonly not wanted/needed
*)
  let stdexcl = (excl "*~") .
    (excl "*.rpmnew") .
    (excl "*.rpmsave") .
    (excl "*.dpkg-old") .
    (excl "*.dpkg-new") .
    (excl "*.dpkg-bak") .
    (excl "*.dpkg-dist") .
    (excl "*.augsave") .
    (excl "*.augnew")

(* Local Variables: *)
(* mode: caml       *)
(* End:             *)

(*
Module: Util
  Generic module providing useful primitives

Author: David Lutterkort

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

module Util =

(*
Variable: del_str
  Delete a string and default to it

  Parameters:
     s:string - the string to delete and default to
*)
  let del_str (s:string) = del s s

(*
Variable: del_ws
  Delete mandatory whitespace
*)
  let del_ws = del /[ \t]+/

(*
Variable: del_ws_spc
  Delete mandatory whitespace, default to single space
*)
  let del_ws_spc = del_ws " "

(*
Variable: del_ws_tab
  Delete mandatory whitespace, default to single tab
*)
  let del_ws_tab = del_ws "\t"

(*
Variable: del_opt_ws
  Delete optional whitespace
*)
  let del_opt_ws = del /[ \t]*/

(*
Variable: eol
  Delete end of line, including optional trailing whitespace
*)
  let eol = del /[ \t]*\n/ "\n"

(*
Variable: indent
  Delete indentation, including leading whitespace
*)
  let indent = del /[ \t]*/ ""

(* Group: Comment and empty
     This is a general definition of comment and empty.
     It allows indentation for comments, removes the leading and trailing spaces
     of comments and stores them in nodes, except for empty comments which are
     ignored together with empty lines

View: comment_generic
  Map comments and set default comment sign
*)

  let comment_generic (r:regexp) (d:string) =
    [ label "#comment" . del r d
        . store /([^ \t\n].*[^ \t\n]|[^ \t\n])/ . eol ]

(* View: comment
  Map comments into "#comment" nodes
*)
  let comment = comment_generic /[ \t]*#[ \t]*/ "# "

(* View: comment_eol
  Map eol comments into "#comment" nodes
  Add a space before # for end of line comments
*)
  let comment_eol = comment_generic /[ \t]*#[ \t]*/ " # "

(* View: comment_or_eol
    A <comment_eol> or <eol> *)
  let comment_or_eol = comment_eol | (del /[ \t]*#?\n/ "\n")

(*
View: empty
  Map empty lines, including empty comments
*)
  let empty   = [ del /[ \t]*#?[ \t]*\n/ "\n" ]

(* View: Split *)
(* Split (SEP . ELT)* into an array-like tree where each match for ELT *)
(* appears in a separate subtree. The labels for the subtrees are      *)
(* consecutive numbers, starting at 0                                  *)
  let split (elt:lens) (sep:lens) =
    let sym = gensym "split" in
    counter sym . ( [ seq sym . sep . elt ] ) *

(* Group: Exclusions

Variable: stdexcl
  Exclusion for files that are commonly not wanted/needed
*)
  let stdexcl = (excl "*~") .
    (excl "*.rpmnew") .
    (excl "*.rpmsave") .
    (excl "*.dpkg-old") .
    (excl "*.dpkg-new") .
    (excl "*.dpkg-bak") .
    (excl "*.dpkg-dist") .
    (excl "*.augsave") .
    (excl "*.augnew")

(* Local Variables: *)
(* mode: caml       *)
(* End:             *)