Skip to content

miekg/cf

Repository files navigation

CFEngine pretty printer

Cf is a formatter for CFEngine files, think of it as 'gofmt' (from golang) for .cf files. See cmd/cffmt for the CLI.

Cf should handle all CFEngine files, allthough the syntax is so liberal, especially where you can place comments (the official yacc parser/lexer throws away comments) that there is always a chance a file isn't parseable. The new data type which can parse inline json can also cause trouble.

If a file has a top-level comment of the form: # cffmt:no the file will not be parsed and the original input will be outputted instead.

If you have an slist in a contraint you can put # cffmt:list above it if you want each item to be printed on a new line. If a list has less then 10 items and at least one of these items is a comment, it will be printed as if cffmt:list has been given.

If you have a "normal" looking CFEngine file that isn't parsed correctly, please open an issue with the most minimal CFEngine syntax that fails to parse.

Comments that are placed in "obvious"(*) places are handled well, but there are corner cases where they lead to a parse error. Directly after a bundle or body for instance. Some of these are fixable (and you should file a bug), others are in the hard-to-fix area and will not be supported. Comments are coalesced into a single block, even if they were separated by a newline. I.e.

# a comment

# another comment

Becomes:

# a comment
# another comment

If you want to keep the separation you need to add '#' on the empty lines.

  • (*) "obvious": not in a list, not in a function argument.

Layout

Cf uses an indent of 2 spaces to indent elements of the tree when pretty printing. Further more:

  • the promise guard (i.e. files: has 2 newlines above it, if it's not the first in the file
  • the class guard (i.e. any::) (if given) has a empty line above it, but is attached to the promiser.
  • the promiser is always attached to the constraint expressions

Empty promise guards are removed, i.e. commands: without any commands defined will be removed from the output:

any::
  "Clients" or => { machine3, machine32 }

commands:

files:

Becomes:

any::
  "Clients" or => { machine3, machine32 }

Cf aligns fat-arrows in constraint expressions, this is also true for selections in bodies.

"/etc/apparmor.d"
             delete => tidy,
 	depth_search => recurse("0"),
             file_select => by_name("session");

Becomes:

"/etc/apparmor.d"
  delete       => tidy,
  depth_search => recurse("0"),
  file_select  => by_name("lightdm-guest-session");

If there is only a single constraint it will be printed on the same line:

   "getcapExists"
        expression => fileexists("/sbin/getcap");

Becomes:

"getcapExists" expression => fileexists("/sbin/getcap");

If there are multiple promises and they all have single constraints, the promises themselves are aligned and the newline between them is deleted:

"getcapExists"
     expression => fileexists("/sbin/getcap");

"setcapExists"  expression => fileexists("/sbin/setcap");

To:

"getcapExists" expression => fileexists("/sbin/getcap");
"setcapExists" expression => fileexists("/sbin/setcap");

If a single constraint has a 'contain =>' or 'comment =>' they will not be printed on the same line. This is to show important things on the left hand side, (see align.go for details), i.e:

printvm::
 "printer[xxx]"	string => "ps.ppd";
  "printer[xxx]"		string => "ps.ppd";
  "printer[xxx]" slist => {"ps.ppd"};

To:

printvm::
  "printer[xxx]" string => "ps.ppd";
  "printer[xxx]" string => "ps.ppd";
  "printer[xxx]" slist  => {"ps.ppd"};

But if one of the constraints was contain or comment:

printvm::
  "printer[xxx]" string => "ps.ppd";
  "printer[xxx]" comment => "ps.ppd";
  "printer[xxx]" string => "ps.ppd";

Will instead become:

printvm::
  "printer[xxx]" string => "ps.ppd";

  "printer[xxx]"
    comment => "ps.ppd";

  "printer[xxx]" string => "ps.ppd";

Trailing commas of lists are removed. List are wrapped at the 120th column: (assuming ggg, is on the 120th column):

"Clients"         or => { aaa, bbb, ccc, dddd, eee, fff,
  ggg, hhhh };

To:

"Clients"         or => { aaa, bbb, ccc, dddd, eee, fff, ggg,
                          hhhh };

It also makes sure there isn't a dangling }; on a line. Empty lists are compressed to {}.

Installation

Install the cffmt binary with: go install github.com/miekg/cf/cmd/cffmt@main. Then use it by giving it a filename or piping to standard input. The pretty printed document is printed to standard output.

cffmt ../../testdata/promtest.cf

Abstract Syntax Tree

If you only want see the AST use -a, and throw away standard output:

cmd/cffmt/cffmt -a -p=false testdata/arg-list.cf >/dev/null
2023/03/11 22:29:51 Parse Tree:
Specification
└─ Bundle
   ├─ {Keyword bundle}
   ├─ {Keyword agent}
   ├─ {NameFunction bla}
   └─ BundleBody
      ├─ PromiseGuard
      │  └─ {KeywordDeclaration vars}
      └─ ClassPromises
         └─ Promise
            ├─ {TokenType(-994) "installed_canonified"}
            ├─ Constraint
            │  ├─ {KeywordType slist}
            │  ├─ FatArrow
            │  │  └─ {TokenType(-996) =>}
            │  └─ Rval
            │     └─ Qstring
            │        └─ {TokenType(-994) "aaa"}
            └─ {Punctuation ;}

From this input file:

bundle agent bla
{
 vars:
    "installed_canonified"
        slist => "aaa";
}

The plain strings, i.e. Bundle are non-terminals, while the {TokenType(-994) ...} and {KeywordTYpe ...} are terminals. That first terminal has a "local" type that we define, in this case it is a token.Qstring, otherwise it's a original chroma.Token. In both cases the type is a chroma.Token. See internal/parse/print.go on how that tree is walked.

Autofmt in (neo)vim

au FileType cf3 command! Fmt call Fmt("cffmt /dev/stdin") " fmt
au BufWritePost *.cf silent call Fmt("cffmt /dev/stdin") " fmt on save

Developing

Lexing is via Chroma (not 100% perfect, but we work around this in lex.go). We have a recursive descent parser to create the AST, this is using *rd.Builder. Once we have the AST the printing is relatively simple (internal/parse/print.go).

https://github.com/cfengine/core/blob/master/libpromises/cf3parse.y contains the grammar we're reimplementing here. Note that one doesn't deal with comments, and is not used to build an AST.