{"id":16857627,"url":"https://github.com/miekg/cf","last_synced_at":"2025-04-11T07:45:44.243Z","repository":{"id":87504832,"uuid":"606512986","full_name":"miekg/cf","owner":"miekg","description":"CFEngine formatter","archived":false,"fork":false,"pushed_at":"2024-09-06T13:50:57.000Z","size":2273,"stargazers_count":6,"open_issues_count":1,"forks_count":1,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-03-25T05:19:45.251Z","etag":null,"topics":["cfengine","prettier"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/miekg.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-25T18:04:41.000Z","updated_at":"2024-09-22T13:09:18.000Z","dependencies_parsed_at":"2024-10-13T14:23:40.169Z","dependency_job_id":null,"html_url":"https://github.com/miekg/cf","commit_stats":null,"previous_names":[],"tags_count":37,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miekg%2Fcf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miekg%2Fcf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miekg%2Fcf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miekg%2Fcf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/miekg","download_url":"https://codeload.github.com/miekg/cf/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248359378,"owners_count":21090519,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cfengine","prettier"],"created_at":"2024-10-13T14:08:48.838Z","updated_at":"2025-04-11T07:45:44.182Z","avatar_url":"https://github.com/miekg.png","language":"Go","readme":"# CFEngine pretty printer\n\nCf is a formatter for CFEngine files, think of it as 'gofmt' (from golang) for .cf files. See\ncmd/cffmt for the CLI.\n\nCf should handle all CFEngine files, allthough the syntax is _so_ liberal, especially where you can\nplace comments (the official yacc parser/lexer throws away comments) that there is always a chance a\nfile isn't parseable. The new data type which can parse inline json can also cause trouble.\n\nIf a file has a top-level comment of the form: `# cffmt:no` the file will not be parsed and the\noriginal input will be outputted instead.\n\nIf you have an `slist` in a contraint you can put `# cffmt:list` above it if you want each item\nto be printed on a new line.\nIf a list has less then 10 items *and* at least one of these items is a comment, it will be printed\nas if `cffmt:list` has been given.\n\nIf you have a \"normal\" looking CFEngine file that isn't parsed correctly, please open an issue with\nthe _most_ _minimal_ CFEngine syntax that fails to parse.\n\nComments that are placed in \"obvious\"(*) places are handled well, but there are corner cases where they\nlead to a parse error. Directly after a `bundle` or `body` for instance. Some of these are fixable\n(and you should file a bug), others are in the hard-to-fix area and will not be supported. Comments\nare coalesced into a single block, even if they were separated by a newline. I.e.\n\n~~~\n# a comment\n\n# another comment\n~~~\n\nBecomes:\n\n~~~\n# a comment\n# another comment\n~~~\n\nIf you want to keep the separation you need to add '#' on the empty lines.\n\n- (*) \"obvious\": not in a list, not in a function argument.\n\n## Layout\n\nCf uses an indent of 2 spaces to indent elements of the tree when pretty printing. Further more:\n\n- the promise guard (i.e. `files:` has 2 newlines above it, if it's not the first in the file\n- the class guard (i.e. `any::`) (if given) has a empty line above it, but is attached to the\n  promiser.\n- the promiser is always attached to the constraint expressions\n\nEmpty promise guards are removed, i.e. `commands:` without any commands defined will be removed\nfrom the output:\n\n~~~ cfengine\nany::\n  \"Clients\" or =\u003e { machine3, machine32 };\n\ncommands:\n\nfiles:\n~~~\n\nBecomes:\n\n~~~ cfengine\nany::\n  \"Clients\" or =\u003e { machine3, machine32 };\n~~~\n\nCf aligns fat-arrows in constraint expressions, this is also true for selections in bodies.\n\n~~~ cfengine\n\"/etc/apparmor.d\"\n             delete =\u003e tidy,\n \tdepth_search =\u003e recurse(\"0\"),\n             file_select =\u003e by_name(\"session\");\n~~~\n\nBecomes:\n\n~~~ cfengine\n\"/etc/apparmor.d\"\n  delete       =\u003e tidy,\n  depth_search =\u003e recurse(\"0\"),\n  file_select  =\u003e by_name(\"lightdm-guest-session\");\n~~~\n\nIf there is only a single constraint it will be printed on the same line:\n\n~~~ cfengine\n   \"getcapExists\"\n        expression =\u003e fileexists(\"/sbin/getcap\");\n~~~\n\nBecomes:\n\n~~~ cfengine\n\"getcapExists\" expression =\u003e fileexists(\"/sbin/getcap\");\n~~~\n\nIf there are multiple promises and they all have single constraints, the promises themselves are\naligned and the newline between them is deleted:\n\n~~~ cfengine\n\"getcapExists\"\n     expression =\u003e fileexists(\"/sbin/getcap\");\n\n\"setcapExists\"  expression =\u003e fileexists(\"/sbin/setcap\");\n~~~\n\nTo:\n\n~~~ cfengine\n\"getcapExists\" expression =\u003e fileexists(\"/sbin/getcap\");\n\"setcapExists\" expression =\u003e fileexists(\"/sbin/setcap\");\n~~~\n\nIf a single constraint has a 'contain =\u003e' or 'comment =\u003e' they will _not_ be printed on the same\nline. This is to show important things on the left hand side, (see align.go for details), i.e:\n\n~~~ cfengine\nprintvm::\n \"printer[xxx]\"\tstring =\u003e \"ps.ppd\";\n  \"printer[xxx]\"\t\tstring =\u003e \"ps.ppd\";\n  \"printer[xxx]\" slist =\u003e {\"ps.ppd\"};\n~~~\n\nTo:\n\n~~~ cfengine\nprintvm::\n  \"printer[xxx]\" string =\u003e \"ps.ppd\";\n  \"printer[xxx]\" string =\u003e \"ps.ppd\";\n  \"printer[xxx]\" slist  =\u003e {\"ps.ppd\"};\n~~~\n\nBut if one of the constraints was `contain` or `comment`:\n\n~~~ cfengine\nprintvm::\n  \"printer[xxx]\" string =\u003e \"ps.ppd\";\n  \"printer[xxx]\" comment =\u003e \"ps.ppd\";\n  \"printer[xxx]\" string =\u003e \"ps.ppd\";\n~~~\n\nWill instead become:\n~~~ cfengine\nprintvm::\n  \"printer[xxx]\" string =\u003e \"ps.ppd\";\n\n  \"printer[xxx]\"\n    comment =\u003e \"ps.ppd\";\n\n  \"printer[xxx]\" string =\u003e \"ps.ppd\";\n~~~\n\nTrailing commas of lists are removed. List are wrapped at the 120th column: (assuming ggg, is on the\n120th column):\n\n~~~ cfengine\n\"Clients\"         or =\u003e { aaa, bbb, ccc, dddd, eee, fff,\n  ggg, hhhh };\n~~~\n\nTo:\n\n~~~ cfengine\n\"Clients\"         or =\u003e { aaa, bbb, ccc, dddd, eee, fff, ggg,\n                          hhhh };\n~~~\n\nIt also makes sure there isn't a dangling `};` on a line. Empty lists are compressed to `{}`.\n\n## Installation\n\nInstall the `cffmt` binary with: `go install github.com/miekg/cf/cmd/cffmt@main`. Then use it by\ngiving it a filename or piping to standard input. The pretty printed document is printed to standard\noutput.\n\n    cffmt ../../testdata/promtest.cf\n\n## Abstract Syntax Tree\n\nIf you only want see the AST use -a, and throw away standard output:\n\n~~~\ncmd/cffmt/cffmt -a -p=false testdata/arg-list.cf \u003e/dev/null\n2023/03/11 22:29:51 Parse Tree:\nSpecification\n└─ Bundle\n   ├─ {Keyword bundle}\n   ├─ {Keyword agent}\n   ├─ {NameFunction bla}\n   └─ BundleBody\n      ├─ PromiseGuard\n      │  └─ {KeywordDeclaration vars}\n      └─ ClassPromises\n         └─ Promise\n            ├─ {TokenType(-994) \"installed_canonified\"}\n            ├─ Constraint\n            │  ├─ {KeywordType slist}\n            │  ├─ FatArrow\n            │  │  └─ {TokenType(-996) =\u003e}\n            │  └─ Rval\n            │     └─ Qstring\n            │        └─ {TokenType(-994) \"aaa\"}\n            └─ {Punctuation ;}\n~~~\n\nFrom this input file:\n\n~~~ cfengine\nbundle agent bla\n{\n vars:\n    \"installed_canonified\"\n        slist =\u003e \"aaa\";\n}\n~~~\n\nThe plain strings, i.e. `Bundle` are non-terminals, while the `{TokenType(-994) ...}` and\n`{KeywordTYpe ...}` are terminals. That first terminal has a \"local\" type that we define, in this\ncase it is a `token.Qstring`, otherwise it's a original `chroma.Token`. In both cases the type is a\n`chroma.Token`. See `internal/parse/print.go` on how that tree is walked.\n\n## Autofmt in (neo)vim\n\n~~~\nau FileType cf3 command! Fmt call Fmt(\"cffmt /dev/stdin\") \" fmt\nau BufWritePost *.cf silent call Fmt(\"cffmt /dev/stdin\") \" fmt on save\n~~~\n\n## Developing\n\nLexing is via Chroma (not 100% perfect, but we work around this in `lex.go`). We have a\nrecursive descent parser to create the AST, this is using *rd.Builder. Once we have the AST the\nprinting is relatively simple (`internal/parse/print.go`).\n\nhttps://github.com/cfengine/core/blob/master/libpromises/cf3parse.y contains the grammar we're\nreimplementing here. Note that one doesn't deal with comments, and is not used to build an AST.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmiekg%2Fcf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmiekg%2Fcf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmiekg%2Fcf/lists"}