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.
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 {}
.
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
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.
au FileType cf3 command! Fmt call Fmt("cffmt /dev/stdin") " fmt
au BufWritePost *.cf silent call Fmt("cffmt /dev/stdin") " fmt on save
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.