cfengine 0.16.7__tar.gz → 0.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. {cfengine-0.16.7 → cfengine-0.18.0}/.github/workflows/update-syntax-description.yml +2 -1
  2. {cfengine-0.16.7 → cfengine-0.18.0}/PKG-INFO +1 -1
  3. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine.egg-info/PKG-INFO +1 -1
  4. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine.egg-info/SOURCES.txt +2 -0
  5. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine.egg-info/scm_file_list.json +2 -0
  6. cfengine-0.18.0/src/cfengine.egg-info/scm_version.json +8 -0
  7. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/commands.py +5 -5
  8. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/docs.py +21 -9
  9. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/format.py +126 -34
  10. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/lint.py +11 -5
  11. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/main.py +10 -1
  12. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/syntax-description.json +17 -3
  13. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/004_comments.expected.cf +3 -6
  14. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/005_bundle_comments.expected.cf +1 -2
  15. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/010_stakeholder.expected.cf +2 -4
  16. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/011_promises.expected.cf +5 -14
  17. cfengine-0.18.0/tests/format/012_sameline_comments.expected.cf +25 -0
  18. cfengine-0.18.0/tests/format/012_sameline_comments.input.cf +23 -0
  19. cfengine-0.16.7/src/cfengine.egg-info/scm_version.json +0 -8
  20. {cfengine-0.16.7 → cfengine-0.18.0}/.github/dependabot.yml +0 -0
  21. {cfengine-0.16.7 → cfengine-0.18.0}/.github/workflows/lint-policy-in-other-repos.yml +0 -0
  22. {cfengine-0.16.7 → cfengine-0.18.0}/.github/workflows/make-check.yml +0 -0
  23. {cfengine-0.16.7 → cfengine-0.18.0}/.github/workflows/pypi-publish.yml +0 -0
  24. {cfengine-0.16.7 → cfengine-0.18.0}/.github/workflows/stale.yml +0 -0
  25. {cfengine-0.16.7 → cfengine-0.18.0}/.gitignore +0 -0
  26. {cfengine-0.16.7 → cfengine-0.18.0}/.python-version +0 -0
  27. {cfengine-0.16.7 → cfengine-0.18.0}/CLAUDE.md +0 -0
  28. {cfengine-0.16.7 → cfengine-0.18.0}/HACKING.md +0 -0
  29. {cfengine-0.16.7 → cfengine-0.18.0}/LICENSE +0 -0
  30. {cfengine-0.16.7 → cfengine-0.18.0}/Makefile +0 -0
  31. {cfengine-0.16.7 → cfengine-0.18.0}/README.md +0 -0
  32. {cfengine-0.16.7 → cfengine-0.18.0}/ci/01-install.sh +0 -0
  33. {cfengine-0.16.7 → cfengine-0.18.0}/ci/02-safe-tests.sh +0 -0
  34. {cfengine-0.16.7 → cfengine-0.18.0}/ci/03-unsafe-tests.sh +0 -0
  35. {cfengine-0.16.7 → cfengine-0.18.0}/pyproject.toml +0 -0
  36. {cfengine-0.16.7 → cfengine-0.18.0}/setup.cfg +0 -0
  37. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine.egg-info/dependency_links.txt +0 -0
  38. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine.egg-info/entry_points.txt +0 -0
  39. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine.egg-info/requires.txt +0 -0
  40. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine.egg-info/top_level.txt +0 -0
  41. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/__init__.py +0 -0
  42. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/__main__.py +0 -0
  43. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/changelog.py +0 -0
  44. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/deptool-README.md +0 -0
  45. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/deptool.py +0 -0
  46. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/dev.py +0 -0
  47. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/lint_csv.py +0 -0
  48. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/lint_yml.py +0 -0
  49. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/__init__.py +0 -0
  50. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/analyze.py +0 -0
  51. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/check_download_matches_git.py +0 -0
  52. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/download.py +0 -0
  53. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/generate_git_tags.py +0 -0
  54. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/generate_release_information.py +0 -0
  55. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/generate_vcf_download.py +0 -0
  56. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/masterfiles/generate_vcf_git_checkout.py +0 -0
  57. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/paths.py +0 -0
  58. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/profile.py +0 -0
  59. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/shell.py +0 -0
  60. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/syntax_tree.py +0 -0
  61. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/up.py +0 -0
  62. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/utils.py +0 -0
  63. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/validate.py +0 -0
  64. {cfengine-0.16.7 → cfengine-0.18.0}/src/cfengine_cli/version.py +0 -0
  65. {cfengine-0.16.7 → cfengine-0.18.0}/tests/README.md +0 -0
  66. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/001_hello_world.expected.cf +0 -0
  67. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/001_hello_world.input.cf +0 -0
  68. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/002_basics.expected.cf +0 -0
  69. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/002_basics.input.cf +0 -0
  70. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/003_wrapping.expected.cf +0 -0
  71. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/003_wrapping.input.cf +0 -0
  72. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/004_comments.input.cf +0 -0
  73. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/005_bundle_comments.input.cf +0 -0
  74. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/006_remove_empty_comments.expected.cf +0 -0
  75. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/006_remove_empty_comments.input.cf +0 -0
  76. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/007_class_guarded_empty_lines.expected.cf +0 -0
  77. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/007_class_guarded_empty_lines.input.cf +0 -0
  78. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/008_long_string.expected.cf +0 -0
  79. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/008_long_string.input.cf +0 -0
  80. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/009_single_line.expected.cf +0 -0
  81. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/009_single_line.input.cf +0 -0
  82. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/010_stakeholder.input.cf +0 -0
  83. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/011_macros.expected.cf +0 -0
  84. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/011_macros.input.cf +0 -0
  85. {cfengine-0.16.7 → cfengine-0.18.0}/tests/format/011_promises.input.cf +0 -0
  86. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/001_hello_world.cf +0 -0
  87. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/002_deprecations.expected.txt +0 -0
  88. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/002_deprecations.x.cf +0 -0
  89. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/003_name_lowercase.cf +0 -0
  90. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/003_name_lowercase.expected.txt +0 -0
  91. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/003_name_lowercase.x.cf +0 -0
  92. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/004_block_type.cf +0 -0
  93. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/004_block_type.expected.txt +0 -0
  94. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/004_block_type.x.cf +0 -0
  95. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/005_syntax_error.cf +0 -0
  96. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/005_syntax_error.expected.txt +0 -0
  97. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/005_syntax_error.x.cf +0 -0
  98. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/006_empty_file.expected.txt +0 -0
  99. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/006_empty_file.x.cf +0 -0
  100. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/007_namespace.cf +0 -0
  101. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/007_namespace.expected.txt +0 -0
  102. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/007_namespace.x.cf +0 -0
  103. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/008_bundle_shadows_function.cf +0 -0
  104. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/008_bundle_shadows_function.expected.txt +0 -0
  105. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/008_bundle_shadows_function.x.cf +0 -0
  106. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/009_unknown_function_inside_vars.cf +0 -0
  107. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/009_unknown_function_inside_vars.expected.txt +0 -0
  108. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/009_unknown_function_inside_vars.x.cf +0 -0
  109. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/010_mutually_exclusive_types_vars.cf +0 -0
  110. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/010_mutually_exclusive_types_vars.expected.txt +0 -0
  111. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/010_mutually_exclusive_types_vars.x.cf +0 -0
  112. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/011_invalid_attributes.expected.txt +0 -0
  113. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/011_invalid_attributes.x.cf +0 -0
  114. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/012_function_call_arg_count.cf +0 -0
  115. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/012_function_call_arg_count.expected.txt +0 -0
  116. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/012_function_call_arg_count.x.cf +0 -0
  117. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/013_num_args_body.expected.txt +0 -0
  118. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/013_num_args_body.x.cf +0 -0
  119. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/013_num_args_bundle.expected.txt +0 -0
  120. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/013_num_args_bundle.x.cf +0 -0
  121. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/013_num_args_bundle_body.cf +0 -0
  122. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/014_variadic_func_arg_count.cf +0 -0
  123. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/014_variadic_func_arg_count.expected.txt +0 -0
  124. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/014_variadic_func_arg_count.x.cf +0 -0
  125. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/015_macro_multi_def_bundle.cf +0 -0
  126. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/015_macro_multi_def_bundle.expected.txt +0 -0
  127. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/015_macro_multi_def_bundle.x.cf +0 -0
  128. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/016_half_promises.cf +0 -0
  129. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/016_half_promises.expected.txt +0 -0
  130. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/016_half_promises.x.cf +0 -0
  131. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/017_implies_body.cf +0 -0
  132. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/017_implies_body.expected.txt +0 -0
  133. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/017_implies_body.x.cf +0 -0
  134. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/018_nested_calls.cf +0 -0
  135. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/018_nested_calls.expected.txt +0 -0
  136. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/018_nested_calls.x.cf +0 -0
  137. {cfengine-0.16.7 → cfengine-0.18.0}/tests/lint/019_bundle_name_expansion.cf +0 -0
  138. {cfengine-0.16.7 → cfengine-0.18.0}/tests/run-format-tests.sh +0 -0
  139. {cfengine-0.16.7 → cfengine-0.18.0}/tests/run-lint-tests.sh +0 -0
  140. {cfengine-0.16.7 → cfengine-0.18.0}/tests/run-shell-tests.sh +0 -0
  141. {cfengine-0.16.7 → cfengine-0.18.0}/tests/run-up-validate-tests.sh +0 -0
  142. {cfengine-0.16.7 → cfengine-0.18.0}/tests/shell/001-help.sh +0 -0
  143. {cfengine-0.16.7 → cfengine-0.18.0}/tests/shell/002-version.sh +0 -0
  144. {cfengine-0.16.7 → cfengine-0.18.0}/tests/shell/003-format.sh +0 -0
  145. {cfengine-0.16.7 → cfengine-0.18.0}/tests/shell/004-format-check.sh +0 -0
  146. {cfengine-0.16.7 → cfengine-0.18.0}/tests/shell/005-lint.sh +0 -0
  147. {cfengine-0.16.7 → cfengine-0.18.0}/tests/unit/__init__.py +0 -0
  148. {cfengine-0.16.7 → cfengine-0.18.0}/tests/unit/test_changelog.py +0 -0
  149. {cfengine-0.16.7 → cfengine-0.18.0}/tests/unit/test_deps.py +0 -0
  150. {cfengine-0.16.7 → cfengine-0.18.0}/tests/unit/test_format.py +0 -0
  151. {cfengine-0.16.7 → cfengine-0.18.0}/tests/unit/test_lint_csv.py +0 -0
  152. {cfengine-0.16.7 → cfengine-0.18.0}/tests/unit/test_lint_yml.py +0 -0
  153. {cfengine-0.16.7 → cfengine-0.18.0}/tests/unit/test_paths.py +0 -0
  154. {cfengine-0.16.7 → cfengine-0.18.0}/tests/unit/test_utils.py +0 -0
  155. {cfengine-0.16.7 → cfengine-0.18.0}/tests/unit/test_version.py +0 -0
  156. {cfengine-0.16.7 → cfengine-0.18.0}/tests/up-validate/001_complex_config.yaml +0 -0
  157. {cfengine-0.16.7 → cfengine-0.18.0}/tests/up-validate/002_version.x.yaml +0 -0
  158. {cfengine-0.16.7 → cfengine-0.18.0}/tests/up-validate/003_box.x.yaml +0 -0
  159. {cfengine-0.16.7 → cfengine-0.18.0}/tests/up-validate/004_count.x.yaml +0 -0
  160. {cfengine-0.16.7 → cfengine-0.18.0}/tests/up-validate/mocks/cloud_config.json +0 -0
  161. {cfengine-0.16.7 → cfengine-0.18.0}/tests/up-validate/mocks/vagrant +0 -0
  162. {cfengine-0.16.7 → cfengine-0.18.0}/uv.lock +0 -0
@@ -55,8 +55,9 @@ jobs:
55
55
  fi
56
56
  - name: Create Pull Request
57
57
  if: env.CHANGES_DETECTED == 'true'
58
- uses: cfengine/create-pull-request@v6
58
+ uses: cfengine/create-pull-request@v7
59
59
  with:
60
+ sign-commits: true
60
61
  title: Updated syntax-description.json
61
62
  body: Automated update to syntax-description.json [the `update-syntax-description` workflow](https://github.com/cfengine/cfengine-cli/tree/main/.github/workflows/update-syntax-description.yml).
62
63
  reviewers: |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfengine
3
- Version: 0.16.7
3
+ Version: 0.18.0
4
4
  Summary: Human-oriented CLI for interacting with CFEngine policy language, projects, and other CFEngine tools
5
5
  License: GNU GENERAL PUBLIC LICENSE
6
6
  Version 3, 29 June 2007
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfengine
3
- Version: 0.16.7
3
+ Version: 0.18.0
4
4
  Summary: Human-oriented CLI for interacting with CFEngine policy language, projects, and other CFEngine tools
5
5
  License: GNU GENERAL PUBLIC LICENSE
6
6
  Version 3, 29 June 2007
@@ -83,6 +83,8 @@ tests/format/011_macros.expected.cf
83
83
  tests/format/011_macros.input.cf
84
84
  tests/format/011_promises.expected.cf
85
85
  tests/format/011_promises.input.cf
86
+ tests/format/012_sameline_comments.expected.cf
87
+ tests/format/012_sameline_comments.input.cf
86
88
  tests/lint/001_hello_world.cf
87
89
  tests/lint/002_deprecations.expected.txt
88
90
  tests/lint/002_deprecations.x.cf
@@ -130,9 +130,11 @@
130
130
  "tests/format/004_comments.input.cf",
131
131
  "tests/format/001_hello_world.expected.cf",
132
132
  "tests/format/011_promises.input.cf",
133
+ "tests/format/012_sameline_comments.expected.cf",
133
134
  "tests/format/008_long_string.expected.cf",
134
135
  "tests/format/008_long_string.input.cf",
135
136
  "tests/format/002_basics.input.cf",
137
+ "tests/format/012_sameline_comments.input.cf",
136
138
  "tests/format/011_macros.input.cf",
137
139
  "tests/unit/__init__.py",
138
140
  "tests/unit/test_version.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.18.0",
3
+ "distance": 0,
4
+ "node": "g226b1b9a975f32e3626fd1bd8cc4578e2a6f2b94",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-07"
8
+ }
@@ -52,14 +52,14 @@ def format(names, line_length, check) -> int:
52
52
  return format_paths(names, line_length, check)
53
53
 
54
54
 
55
- def _lint(files, strict) -> int:
55
+ def _lint(files, strict, syntax_path) -> int:
56
56
  if not files:
57
- return lint_args(["."], strict)
58
- return lint_args(files, strict)
57
+ return lint_args(["."], strict, syntax_path)
58
+ return lint_args(files, strict, syntax_path)
59
59
 
60
60
 
61
- def lint(files, strict) -> int:
62
- errors = _lint(files, strict)
61
+ def lint(files, strict, syntax_path) -> int:
62
+ errors = _lint(files, strict, syntax_path)
63
63
  if errors == 0:
64
64
  print("Success, no errors found.")
65
65
  elif errors == 1:
@@ -61,15 +61,16 @@ def extract_inline_code(path, languages):
61
61
  indent = count_indent(lines[first_line])
62
62
  # Index of first line to NOT include, the line after closing triple backtick:
63
63
  last_line = child.map[1]
64
+ lines_to_extract = lines[first_line:last_line]
65
+ _remove_indentation(lines_to_extract, indent)
64
66
  yield {
65
67
  "language": language,
66
68
  "flags": flags,
67
69
  "first_line": child.map[0],
68
70
  "last_line": child.map[1],
69
71
  "indent": indent,
70
- "lines": lines[
71
- first_line:last_line
72
- ], # Includes backtick fences on both sides
72
+ # TODO: Dead code, the actual lines are extracted "twice" (2 places in the codebase)
73
+ "lines": lines_to_extract, # Includes backtick fences on both sides
73
74
  }
74
75
 
75
76
 
@@ -97,12 +98,24 @@ def get_markdown_files(start, languages):
97
98
  return return_dict
98
99
 
99
100
 
101
+ def _remove_indentation(snippet_lines, indent):
102
+ for i, line in enumerate(snippet_lines):
103
+ if line == "":
104
+ continue
105
+ assert line.startswith(" " * indent)
106
+ snippet_lines[i] = line[indent:]
107
+
108
+
100
109
  def fn_extract(origin_path, snippet_path, _language, first_line, last_line):
101
110
  try:
102
111
  with open(origin_path, "r") as f:
103
- content = f.read()
104
-
105
- code_snippet = "\n".join(content.split("\n")[first_line + 1 : last_line - 1])
112
+ lines = f.readlines()
113
+ lines = [x[0:-1] for x in lines] # Remove newlines
114
+ fence = lines[first_line]
115
+ indent = count_indent(fence)
116
+ snippet_lines = lines[first_line + 1 : last_line - 1]
117
+ _remove_indentation(snippet_lines, indent)
118
+ code_snippet = "\n".join(snippet_lines)
106
119
 
107
120
  with open(snippet_path, "w") as f:
108
121
  f.write(code_snippet + "\n")
@@ -174,7 +187,7 @@ def fn_replace(origin_path, snippet_path, _language, first_line, last_line, inde
174
187
  origin_lines = f.read().split("\n")
175
188
  pretty_lines = pretty_content.split("\n")
176
189
 
177
- pretty_lines = [" " * indent + x for x in pretty_lines]
190
+ pretty_lines = ["" if x == "" else " " * indent + x for x in pretty_lines]
178
191
 
179
192
  offset = len(pretty_lines) - len(
180
193
  origin_lines[first_line + 1 : last_line - 1]
@@ -213,7 +226,6 @@ def fn_autoformat(
213
226
  except json.decoder.JSONDecodeError:
214
227
  raise UserError(f"Invalid json in '{snippet_path}'")
215
228
  case "cf":
216
- # Note: Dead code - Not used for CFEngine policy yet
217
229
  format_policy_file(snippet_path, 80, False)
218
230
  return False
219
231
 
@@ -434,7 +446,7 @@ def _format_docs_single_arg(path):
434
446
  formatted = True
435
447
  _process_markdown_code_blocks(
436
448
  path=path,
437
- languages=["json"], # TODO: Add cfengine3 here
449
+ languages=["json", "cfengine3"],
438
450
  extract=True,
439
451
  syntax_check=False,
440
452
  output_check=False,
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from enum import IntEnum
3
4
  from typing import IO
4
5
 
5
6
  import json
@@ -35,6 +36,16 @@ BLOCK_BODY_TYPES = {"bundle_block_body", "promise_block_body", "body_block_body"
35
36
 
36
37
  PROMISER_PARTS = {"promiser", "->", "stakeholder"}
37
38
 
39
+ SAMELINE_COMMENT_SEPARATOR = " "
40
+
41
+
42
+ class FormatResult(IntEnum):
43
+ """Outcome of a formatting operation."""
44
+
45
+ NO_CHANGE = 0 # File was already formatted, no reformat needed
46
+ NEEDS_FORMAT = 1 # Reformat needed (reported in check mode)
47
+ REFORMATTED = 2 # File was reformatted successfully
48
+
38
49
 
39
50
  def _has_direct_macro(node: Node) -> bool:
40
51
  """Check if any direct child of a node is a macro (non-recursive)."""
@@ -62,12 +73,8 @@ def _contains_list_with_comment(nodes: Node | list[Node]) -> bool:
62
73
  return _contains_list_with_comment(nodes.children)
63
74
 
64
75
 
65
- def format_json_file(filename: str, check: bool) -> int:
66
- """Reformat a JSON file in place using cfbs pretty-printer.
67
-
68
- Returns 0 in case of successful reformat or no reformat needed.
69
- Returns 1 when check is True and reformat is needed.
70
- """
76
+ def format_json_file(filename: str, check: bool) -> FormatResult:
77
+ """Reformat a JSON file in place using cfbs pretty-printer."""
71
78
  assert filename.endswith(".json")
72
79
 
73
80
  if check:
@@ -76,16 +83,17 @@ def format_json_file(filename: str, check: bool) -> int:
76
83
  assert type(success) is bool
77
84
  if not success:
78
85
  print(f"JSON file '{filename}' needs reformatting")
79
- return int(not success)
86
+ return FormatResult.NEEDS_FORMAT
87
+ return FormatResult.NO_CHANGE
80
88
 
81
89
  try:
82
90
  reformatted = pretty_file(filename)
83
91
  if reformatted:
84
92
  print(f"JSON file '{filename}' was reformatted")
85
- return 0 # Successfully reformatted or no reformat needed
93
+ return FormatResult.REFORMATTED
94
+ return FormatResult.NO_CHANGE
86
95
  except json.decoder.JSONDecodeError as e:
87
- print(f"JSON file '{filename}' invalid ({str(e)})")
88
- return 1
96
+ raise UserError(f"JSON file '{filename}' invalid ({str(e)})")
89
97
 
90
98
 
91
99
  def text(node: Node) -> str:
@@ -241,6 +249,9 @@ def split_generic_list(
241
249
  # outside the function call, so we do not add trailing commas into function
242
250
  # call argument lists.
243
251
  value_end_indices: list[int] = []
252
+ # Same-line (trailing) comments are attached only after trailing commas are
253
+ # placed, so the comma lands before the comment instead of after it.
254
+ sameline_comments: dict[int, str] = {}
244
255
  for element in middle:
245
256
  if elements and element.type == ",":
246
257
  elements[-1] = elements[-1] + ","
@@ -249,7 +260,10 @@ def split_generic_list(
249
260
  elements.append(text(element))
250
261
  continue
251
262
  if element.type == "comment":
252
- elements.append(" " * indent + text(element))
263
+ if _is_sameline_comment(element) and elements:
264
+ sameline_comments[len(elements) - 1] = text(element)
265
+ else:
266
+ elements.append(" " * indent + text(element))
253
267
  continue
254
268
  line = " " * indent + stringify_single_line_node(element)
255
269
  # Strict < reserves 1 char for the comma appended after this check
@@ -275,6 +289,10 @@ def split_generic_list(
275
289
  if not elements[i].lstrip().startswith("#"):
276
290
  elements[i] = _set_trailing_comma(elements[i], trailing_comma)
277
291
  break
292
+
293
+ # Attach same-line comments to the end of their value lines
294
+ for i, comment in sameline_comments.items():
295
+ elements[i] = elements[i] + SAMELINE_COMMENT_SEPARATOR + comment
278
296
  return elements
279
297
 
280
298
 
@@ -354,7 +372,10 @@ def _format_attribute_with_macros(node: Node, indent: int) -> list[str]:
354
372
  if child.type == "macro":
355
373
  lines.append(text(child))
356
374
  elif child.type == "comment":
357
- lines.append(" " * (indent + 2) + text(child))
375
+ if _is_sameline_comment(child) and lines:
376
+ lines[-1] = lines[-1] + SAMELINE_COMMENT_SEPARATOR + text(child)
377
+ else:
378
+ lines.append(" " * (indent + 2) + text(child))
358
379
  else:
359
380
  lines.append(" " * (indent + 2) + stringify_single_line_node(child))
360
381
 
@@ -505,13 +526,19 @@ def _format_stakeholder_elements(
505
526
  return maybe_split_generic_list(middle, indent, line_length)
506
527
  # Comments present — format element-by-element to preserve them
507
528
  elements: list[str] = []
529
+ # Same-line (trailing) comments are attached only after the trailing comma is
530
+ # placed, so the comma lands before the comment instead of after it.
531
+ sameline_comments: dict[int, str] = {}
508
532
  for node in middle:
509
533
  if node.type == ",":
510
534
  if elements:
511
535
  elements[-1] = elements[-1] + ","
512
536
  continue
513
537
  if node.type == "comment":
514
- elements.append(" " * indent + text(node))
538
+ if _is_sameline_comment(node) and elements:
539
+ sameline_comments[len(elements) - 1] = text(node)
540
+ else:
541
+ elements.append(" " * indent + text(node))
515
542
  else:
516
543
  line = " " * indent + stringify_single_line_node(node)
517
544
  # Strict < reserves 1 char for the comma appended after this check
@@ -528,6 +555,10 @@ def _format_stakeholder_elements(
528
555
  if not elements[i].endswith(","):
529
556
  elements[i] = elements[i] + ","
530
557
  break
558
+
559
+ # Attach inline comments to the end of their value lines
560
+ for i, comment in sameline_comments.items():
561
+ elements[i] = elements[i] + SAMELINE_COMMENT_SEPARATOR + comment
531
562
  return elements
532
563
 
533
564
 
@@ -682,9 +713,13 @@ def _format_block_header(node: Node, fmt: Formatter) -> list[Node]:
682
713
  """Format a block header line and return the body's children for further processing."""
683
714
  header_parts: list[str] = []
684
715
  header_comments: list[str] = []
716
+ # Direct comment children of the block (excludes comments inside a
717
+ # parameter list) — candidates for being kept on the header line.
718
+ direct_comments: list[Node] = []
685
719
  for x in node.children[0:-1]:
686
720
  if x.type == "comment":
687
721
  header_comments.append(text(x))
722
+ direct_comments.append(x)
688
723
  elif x.type == "parameter_list":
689
724
  parts: list[str] = []
690
725
  for p in x.children:
@@ -696,6 +731,16 @@ def _format_block_header(node: Node, fmt: Formatter) -> list[Node]:
696
731
  else:
697
732
  header_parts.append(text(x))
698
733
  line = " ".join(header_parts)
734
+ # A lone same-line comment stays on the header line. When the header has
735
+ # more than one comment, keep them together on their own lines below the
736
+ # header instead of splitting one off onto the header line.
737
+ if (
738
+ len(header_comments) == 1
739
+ and len(direct_comments) == 1
740
+ and _is_sameline_comment(direct_comments[0])
741
+ ):
742
+ line += SAMELINE_COMMENT_SEPARATOR + header_comments[0]
743
+ header_comments = []
699
744
  if not fmt.empty:
700
745
  prev_sib = node.prev_named_sibling
701
746
  # Skip over preceding empty comments since they will be removed
@@ -725,6 +770,11 @@ def _format_block_header(node: Node, fmt: Formatter) -> list[Node]:
725
770
 
726
771
  def _needs_blank_line_before(child: Node, indent: int, line_length: int) -> bool:
727
772
  """Check if a blank separator line should precede this child node."""
773
+ # Inline (trailing) comments are appended to the preceding line, so they
774
+ # must never be preceded by a blank separator line.
775
+ if child.type == "comment" and _is_sameline_comment(child):
776
+ return False
777
+
728
778
  prev = child.prev_named_sibling
729
779
  # Empty comments preceding this child will be dropped — look past them
730
780
  # so we evaluate against the real prior content.
@@ -798,6 +848,21 @@ def _needs_blank_line_before(child: Node, indent: int, line_length: int) -> bool
798
848
  # ---------------------------------------------------------------------------
799
849
 
800
850
 
851
+ def _is_sameline_comment(node: Node) -> bool:
852
+ """Check if a comment sits on the same source line as the element before it.
853
+
854
+ A trailing comment shares the row of the preceding sibling (which may be
855
+ a leaf like ',' or ';', or a whole promise/attribute). Such comments are
856
+ kept on the same output line as that element rather than moved to their
857
+ own line."""
858
+ if node.type != "comment":
859
+ return False
860
+ prev = node.prev_sibling
861
+ if prev is None:
862
+ return False
863
+ return prev.end_point[0] == node.start_point[0]
864
+
865
+
801
866
  def _is_empty_comment(node: Node) -> bool:
802
867
  """Check if a bare '#' comment should be dropped.
803
868
 
@@ -934,7 +999,11 @@ def _autoformat(
934
999
  else:
935
1000
  fmt.print_same_line(node)
936
1001
  elif node.type == "comment":
937
- if not _is_empty_comment(node):
1002
+ if _is_empty_comment(node):
1003
+ pass
1004
+ elif _is_sameline_comment(node) and not fmt.empty:
1005
+ fmt.print_same_line(SAMELINE_COMMENT_SEPARATOR + text(node))
1006
+ else:
938
1007
  fmt.print(node, _comment_indent(node, indent))
939
1008
  else:
940
1009
  fmt.print(node, indent)
@@ -945,11 +1014,9 @@ def _autoformat(
945
1014
  # ---------------------------------------------------------------------------
946
1015
 
947
1016
 
948
- def format_policy_file(filename: str, line_length: int, check: bool) -> int:
1017
+ def format_policy_file(filename: str, line_length: int, check: bool) -> FormatResult:
949
1018
  """Format a .cf policy file in place, writing only if content changed.
950
1019
 
951
- Returns 0 in case of successful reformat or no reformat needed.
952
- Returns 1 when check is True and reformat is needed.
953
1020
  Raises PolicySyntaxError when the file has syntax errors."""
954
1021
  assert filename.endswith(".cf")
955
1022
 
@@ -968,21 +1035,28 @@ def format_policy_file(filename: str, line_length: int, check: bool) -> int:
968
1035
  fmt = Formatter()
969
1036
  _autoformat(root_node, fmt, line_length)
970
1037
 
971
- new_data = fmt.buffer + "\n"
1038
+ new_data = fmt.buffer
1039
+ if not new_data.endswith("\n"):
1040
+ # TODO: Look into why formatter sometimes outputs
1041
+ # trailing newline and other times not.
1042
+ new_data += "\n"
1043
+ assert new_data.endswith("\n") and not new_data.endswith("\n\n")
1044
+
972
1045
  if new_data != original_data.decode("utf-8"):
973
1046
  if check:
974
1047
  print(f"Policy file '{filename}' needs reformatting")
975
- return 1
1048
+ return FormatResult.NEEDS_FORMAT
976
1049
 
977
1050
  with open(filename, "w") as f:
978
1051
  f.write(new_data)
979
1052
  print(f"Policy file '{filename}' was reformatted")
980
- return 0
1053
+ return FormatResult.REFORMATTED
1054
+ return FormatResult.NO_CHANGE
981
1055
 
982
1056
 
983
1057
  def format_policy_fin_fout(
984
1058
  fin: IO[str], fout: IO[str], line_length: int, check: bool
985
- ) -> int:
1059
+ ) -> FormatResult:
986
1060
  """Format CFEngine policy read from fin, writing the result to fout.
987
1061
 
988
1062
  Raises PolicySyntaxError when the input has syntax errors."""
@@ -1002,10 +1076,12 @@ def format_policy_fin_fout(
1002
1076
 
1003
1077
  new_data = fmt.buffer + "\n"
1004
1078
  fout.write(new_data)
1005
- return 0
1079
+ if new_data != original_data.decode("utf-8"):
1080
+ return FormatResult.REFORMATTED
1081
+ return FormatResult.NO_CHANGE
1006
1082
 
1007
1083
 
1008
- def _format_filename(filename: str, line_length: int, check: bool) -> int:
1084
+ def _format_filename(filename: str, line_length: int, check: bool) -> FormatResult:
1009
1085
  """Format a single file.
1010
1086
 
1011
1087
  Raises PolicySyntaxError for .cf files with syntax errors."""
@@ -1016,8 +1092,17 @@ def _format_filename(filename: str, line_length: int, check: bool) -> int:
1016
1092
  raise UserError(f"Unrecognized file format: {filename}")
1017
1093
 
1018
1094
 
1019
- def _format_dirname(directory: str, line_length: int, check: bool) -> int:
1020
- ret = 0
1095
+ def _combine_results(a: FormatResult, b: FormatResult) -> FormatResult:
1096
+ """Combine two format results. A non-NO_CHANGE result takes precedence.
1097
+
1098
+ Within a single format run, `check` is fixed, so NEEDS_FORMAT and
1099
+ REFORMATTED never occur together — one always dominates NO_CHANGE."""
1100
+ return b if b != FormatResult.NO_CHANGE else a
1101
+
1102
+
1103
+ def _format_dirname(directory: str, line_length: int, check: bool) -> FormatResult:
1104
+ """Format files in directory recursively."""
1105
+ ret = FormatResult.NO_CHANGE
1021
1106
  for root, dirs, files in os.walk(directory):
1022
1107
  # Don't recurse into hidden folders
1023
1108
  dirs[:] = [d for d in dirs if not d.startswith(".")]
@@ -1037,18 +1122,20 @@ def _format_dirname(directory: str, line_length: int, check: bool) -> int:
1037
1122
  continue # Test files skipped during directory traversal
1038
1123
  filepath = os.path.join(root, name)
1039
1124
  if name.endswith(".json") or name.endswith(".cf"):
1040
- ret |= _format_filename(filepath, line_length, check)
1125
+ ret = _combine_results(
1126
+ ret, _format_filename(filepath, line_length, check)
1127
+ )
1041
1128
  return ret
1042
1129
 
1043
1130
 
1044
- def _format_paths_inner(names, line_length, check) -> int:
1131
+ def _format_paths_inner(names, line_length, check) -> FormatResult:
1045
1132
  if not names:
1046
1133
  return _format_dirname(".", line_length, check)
1047
1134
  if len(names) == 1 and names[0] == "-":
1048
1135
  # Special case, format policy file from stdin to stdout
1049
1136
  return format_policy_fin_fout(sys.stdin, sys.stdout, line_length, check)
1050
1137
 
1051
- ret = 0
1138
+ ret = FormatResult.NO_CHANGE
1052
1139
  for name in names:
1053
1140
  if name == "-":
1054
1141
  raise UserError(
@@ -1057,14 +1144,12 @@ def _format_paths_inner(names, line_length, check) -> int:
1057
1144
  if not os.path.exists(name):
1058
1145
  raise UserError(f"{name} does not exist")
1059
1146
  if os.path.isfile(name):
1060
- ret |= _format_filename(name, line_length, check)
1147
+ ret = _combine_results(ret, _format_filename(name, line_length, check))
1061
1148
  continue
1062
1149
  if os.path.isdir(name):
1063
- ret |= _format_dirname(name, line_length, check)
1150
+ ret = _combine_results(ret, _format_dirname(name, line_length, check))
1064
1151
  continue
1065
- if check:
1066
- return ret
1067
- return 0
1152
+ return ret
1068
1153
 
1069
1154
 
1070
1155
  def format_paths(names, line_length, check) -> int:
@@ -1075,7 +1160,14 @@ def format_paths(names, line_length, check) -> int:
1075
1160
  needed), 1 when reformatting is needed in check mode or a policy file
1076
1161
  has syntax errors."""
1077
1162
  try:
1078
- return _format_paths_inner(names, line_length, check)
1163
+ r = _format_paths_inner(names, line_length, check)
1164
+ if r == FormatResult.NEEDS_FORMAT:
1165
+ assert check
1166
+ # File needs reformatting
1167
+ return 1
1168
+ assert r in (FormatResult.NO_CHANGE, FormatResult.REFORMATTED)
1169
+ # Success - no format needed or successfully reformatted
1170
+ return 0
1079
1171
  except PolicySyntaxError as e:
1080
1172
  print(f"Error: {e}")
1081
1173
  return 1
@@ -81,9 +81,9 @@ class SyntaxData:
81
81
  BUILTIN_PROMISE_TYPES = {}
82
82
  BUILTIN_FUNCTIONS = {}
83
83
 
84
- def __init__(self):
84
+ def __init__(self, syntax_path=None):
85
85
  """Load the bundled syntax-description.json and derive lookup dicts."""
86
- self._data_dict = self._load_syntax_description()
86
+ self._data_dict = self._load_syntax_description(path=syntax_path)
87
87
  self._derive_syntax_dicts(self._data_dict)
88
88
 
89
89
  assert self.BUILTIN_BODY_TYPES
@@ -1100,6 +1100,7 @@ def _lint_main(
1100
1100
  state=None,
1101
1101
  snippet: Snippet | None = None,
1102
1102
  syntax_data=None,
1103
+ syntax_path: str | None = None,
1103
1104
  ) -> int:
1104
1105
  """This is the main function used for linting, it does all the steps on all
1105
1106
  the arguments (files / folders).
@@ -1127,7 +1128,10 @@ def _lint_main(
1127
1128
  state.mode = Mode.SYNTAX
1128
1129
 
1129
1130
  if syntax_data is None:
1130
- syntax_data = SyntaxData()
1131
+ try:
1132
+ syntax_data = SyntaxData(syntax_path)
1133
+ except FileNotFoundError:
1134
+ raise UserError(f"'{syntax_path}' does not exist")
1131
1135
 
1132
1136
  filenames = _args_to_filenames(args)
1133
1137
 
@@ -1379,9 +1383,11 @@ def lint_single_file(file: str, strict: bool = True) -> int:
1379
1383
  return _lint_main([file], strict)
1380
1384
 
1381
1385
 
1382
- def lint_args(args: Iterable[str], strict: bool = True) -> int:
1386
+ def lint_args(
1387
+ args: Iterable[str], strict: bool = True, syntax_path: str | None = None
1388
+ ) -> int:
1383
1389
  """Lint a list of args (files / folders)"""
1384
- return _lint_main(list(args), strict)
1390
+ return _lint_main(list(args), strict, syntax_path=syntax_path)
1385
1391
 
1386
1392
 
1387
1393
  def lint_policy_file_snippet(
@@ -59,6 +59,11 @@ def _get_arg_parser():
59
59
  default="yes",
60
60
  help="Strict mode. Default=yes, checks for undefined promise types, bundles, bodies, functions",
61
61
  )
62
+ lnt.add_argument(
63
+ "--syntax-description",
64
+ type=str,
65
+ help="Lint based on a user given syntax description",
66
+ )
62
67
  lnt.add_argument("files", nargs="*", help="Files to lint")
63
68
  subp.add_parser(
64
69
  "report",
@@ -188,7 +193,11 @@ def run_command_with_args(args) -> int:
188
193
  if args.command == "format":
189
194
  return commands.format(args.files, args.line_length, args.check)
190
195
  if args.command == "lint":
191
- return commands.lint(args.files, (args.strict.lower() in ("y", "ye", "yes")))
196
+ return commands.lint(
197
+ args.files,
198
+ (args.strict.lower() in ("y", "ye", "yes")),
199
+ args.syntax_description,
200
+ )
192
201
  if args.command == "report":
193
202
  return commands.report()
194
203
  if args.command == "run":
@@ -753,7 +753,7 @@
753
753
  },
754
754
  "protocol_version": {
755
755
  "attribute": "protocol_version",
756
- "range": "1,classic,2,tls,3,cookie,4,filestream,latest",
756
+ "range": "1,classic,2,tls,3,cookie,4,filestream,5,leech2,latest",
757
757
  "status": "normal",
758
758
  "type": "option",
759
759
  "visibility": "body"
@@ -1020,7 +1020,7 @@
1020
1020
  },
1021
1021
  "protocol_version": {
1022
1022
  "attribute": "protocol_version",
1023
- "range": "1,classic,2,tls,3,cookie,4,filestream,latest",
1023
+ "range": "1,classic,2,tls,3,cookie,4,filestream,5,leech2,latest",
1024
1024
  "status": "normal",
1025
1025
  "type": "option",
1026
1026
  "visibility": "body"
@@ -7368,6 +7368,13 @@
7368
7368
  "type": "clist",
7369
7369
  "visibility": "promiseType"
7370
7370
  },
7371
+ "cancel": {
7372
+ "attribute": "cancel",
7373
+ "range": "true,false,yes,no,on,off",
7374
+ "status": "normal",
7375
+ "type": "option",
7376
+ "visibility": "promiseType"
7377
+ },
7371
7378
  "classes": {
7372
7379
  "attribute": "classes",
7373
7380
  "status": "normal",
@@ -7465,6 +7472,13 @@
7465
7472
  "type": "clist",
7466
7473
  "visibility": "promiseType"
7467
7474
  },
7475
+ "timer_policy": {
7476
+ "attribute": "timer_policy",
7477
+ "range": "absolute,reset",
7478
+ "status": "normal",
7479
+ "type": "option",
7480
+ "visibility": "promiseType"
7481
+ },
7468
7482
  "unless": {
7469
7483
  "attribute": "unless",
7470
7484
  "range": "",
@@ -9395,7 +9409,7 @@
9395
9409
  },
9396
9410
  "signals": {
9397
9411
  "attribute": "signals",
9398
- "range": "(hup|int|trap|kill|pipe|cont|abrt|stop|quit|term|child|usr1|usr2|bus|segv|[0-9]+s?)",
9412
+ "range": "((?i:hup|int|trap|kill|pipe|cont|abrt|stop|quit|term|child|usr1|usr2|bus|segv)|[0-9]+s?)",
9399
9413
  "status": "normal",
9400
9414
  "type": "slist",
9401
9415
  "visibility": "promiseType"
@@ -52,17 +52,14 @@ bundle agent win_services
52
52
  "autostart_services"
53
53
  slist => {
54
54
  "Alerter",
55
- "W32Time",
56
- # Windows Time
55
+ "W32Time", # Windows Time
57
56
  };
58
57
 
59
58
  some_class::
60
59
  "autostart_services"
61
60
  slist => {
62
- "MpsSvc",
63
- # Windows Firewall
64
- "W32Time",
65
- # Windows Time
61
+ "MpsSvc", # Windows Firewall
62
+ "W32Time", # Windows Time
66
63
  };
67
64
 
68
65
  some_class::
@@ -1,5 +1,4 @@
1
- bundle agent a
2
- # @brief Description of a
1
+ bundle agent a # @brief Description of a
3
2
  {
4
3
  reports:
5
4
  "Hello, world!";