diff --git a/src/cfengine_cli/commands.py b/src/cfengine_cli/commands.py index 698203a..cff5126 100644 --- a/src/cfengine_cli/commands.py +++ b/src/cfengine_cli/commands.py @@ -1,19 +1,14 @@ -import sys import os import re import json import yaml from cfengine_cli.profile import profile_cfengine, generate_callstack from cfengine_cli.dev import dispatch_dev_subcommand -from cfengine_cli.lint import lint_args, PolicySyntaxError +from cfengine_cli.lint import lint_args from cfengine_cli.shell import user_command from cfengine_cli.paths import bin from cfengine_cli.version import cfengine_cli_version_string -from cfengine_cli.format import ( - format_policy_file, - format_json_file, - format_policy_fin_fout, -) +from cfengine_cli.format import format_paths from cfengine_cli.utils import UserError from cfengine_cli.up import validate_config from cfbs.commands import build_command @@ -51,74 +46,8 @@ def deploy() -> int: return r -def _format_filename(filename: str, line_length: int, check: bool) -> int: - """Format a single file. - - Raises PolicySyntaxError for .cf files with syntax errors.""" - if filename.endswith(".json"): - return format_json_file(filename, check) - if filename.endswith(".cf"): - return format_policy_file(filename, line_length, check) - raise UserError(f"Unrecognized file format: {filename}") - - -def _format_dirname(directory: str, line_length: int, check: bool) -> int: - ret = 0 - for root, dirs, files in os.walk(directory): - # Don't recurse into hidden folders - dirs[:] = [d for d in dirs if not d.startswith(".")] - for name in sorted(files): - if name.startswith("."): - continue # Hidden files are ignored by default - if ( - name.endswith(".x.cf") - or name.endswith(".input.cf") - or name.endswith(".output.cf") - or name.endswith(".expected.cf") - ): - continue # Test files skipped during directory traversal - if name.endswith( - (".input.json", ".jqinput.json", ".x.json", ".expected.json") - ): - continue # Test files skipped during directory traversal - filepath = os.path.join(root, name) - if name.endswith(".json") or name.endswith(".cf"): - ret |= _format_filename(filepath, line_length, check) - return ret - - def format(names, line_length, check) -> int: - try: - return _format_inner(names, line_length, check) - except PolicySyntaxError as e: - print(f"Error: {e}") - return 1 - - -def _format_inner(names, line_length, check) -> int: - if not names: - return _format_dirname(".", line_length, check) - if len(names) == 1 and names[0] == "-": - # Special case, format policy file from stdin to stdout - return format_policy_fin_fout(sys.stdin, sys.stdout, line_length, check) - - ret = 0 - for name in names: - if name == "-": - raise UserError( - "The - argument has a special meaning and cannot be combined with other paths" - ) - if not os.path.exists(name): - raise UserError(f"{name} does not exist") - if os.path.isfile(name): - ret |= _format_filename(name, line_length, check) - continue - if os.path.isdir(name): - ret |= _format_dirname(name, line_length, check) - continue - if check: - return ret - return 0 + return format_paths(names, line_length, check) def _lint(files, strict) -> int: diff --git a/src/cfengine_cli/dev.py b/src/cfengine_cli/dev.py index ddbcbb9..813dfa9 100644 --- a/src/cfengine_cli/dev.py +++ b/src/cfengine_cli/dev.py @@ -9,6 +9,7 @@ print_release_dependency_tables, ) from cfengine_cli.docs import update_docs, check_docs +from cfengine_cli.format import format_paths from cfengine_cli.syntax_tree import syntax_tree @@ -49,12 +50,20 @@ def print_dependency_tables(args) -> int: def format_docs(files) -> int: _expect_repo("documentation") - return update_docs(files) + ret = update_docs(files) + # Also run the same logic as `cfengine format` so .cf / .json files + # are formatted without having to run that command manually. + ret |= format_paths(files, line_length=80, check=False) + return ret -def lint_docs() -> int: +def lint_docs(files) -> int: _expect_repo("documentation") - return check_docs() + ret = check_docs() + # Also run the same logic as `cfengine format --check` so .cf / .json + # files are checked without having to run that command manually. + ret |= format_paths(files, line_length=80, check=True) + return ret def generate_release_information( @@ -73,7 +82,7 @@ def dispatch_dev_subcommand(subcommand, args) -> int: if subcommand == "format-docs": return format_docs(args.files) if subcommand == "lint-docs": - return lint_docs() + return lint_docs(args.files) if subcommand == "syntax-tree": return syntax_tree(args.file) if subcommand == "generate-release-information": diff --git a/src/cfengine_cli/format.py b/src/cfengine_cli/format.py index 740c23e..63865c5 100644 --- a/src/cfengine_cli/format.py +++ b/src/cfengine_cli/format.py @@ -3,11 +3,14 @@ from typing import IO import json +import os +import sys import tree_sitter_cfengine as tscfengine from tree_sitter import Language, Parser, Node from cfbs.pretty import pretty_file, pretty_check_file -from cfengine_cli.lint import check_policy_syntax +from cfengine_cli.lint import check_policy_syntax, PolicySyntaxError +from cfengine_cli.utils import UserError # Node types that increase indentation by 2 when entered INDENTED_TYPES = { @@ -985,3 +988,79 @@ def format_policy_fin_fout( new_data = fmt.buffer + "\n" fout.write(new_data) return 0 + + +def _format_filename(filename: str, line_length: int, check: bool) -> int: + """Format a single file. + + Raises PolicySyntaxError for .cf files with syntax errors.""" + if filename.endswith(".json"): + return format_json_file(filename, check) + if filename.endswith(".cf"): + return format_policy_file(filename, line_length, check) + raise UserError(f"Unrecognized file format: {filename}") + + +def _format_dirname(directory: str, line_length: int, check: bool) -> int: + ret = 0 + for root, dirs, files in os.walk(directory): + # Don't recurse into hidden folders + dirs[:] = [d for d in dirs if not d.startswith(".")] + for name in sorted(files): + if name.startswith("."): + continue # Hidden files are ignored by default + if ( + name.endswith(".x.cf") + or name.endswith(".input.cf") + or name.endswith(".output.cf") + or name.endswith(".expected.cf") + ): + continue # Test files skipped during directory traversal + if name.endswith( + (".input.json", ".jqinput.json", ".x.json", ".expected.json") + ): + continue # Test files skipped during directory traversal + filepath = os.path.join(root, name) + if name.endswith(".json") or name.endswith(".cf"): + ret |= _format_filename(filepath, line_length, check) + return ret + + +def _format_paths_inner(names, line_length, check) -> int: + if not names: + return _format_dirname(".", line_length, check) + if len(names) == 1 and names[0] == "-": + # Special case, format policy file from stdin to stdout + return format_policy_fin_fout(sys.stdin, sys.stdout, line_length, check) + + ret = 0 + for name in names: + if name == "-": + raise UserError( + "The - argument has a special meaning and cannot be combined with other paths" + ) + if not os.path.exists(name): + raise UserError(f"{name} does not exist") + if os.path.isfile(name): + ret |= _format_filename(name, line_length, check) + continue + if os.path.isdir(name): + ret |= _format_dirname(name, line_length, check) + continue + if check: + return ret + return 0 + + +def format_paths(names, line_length, check) -> int: + """Format the given paths (files and/or directories), or stdin. + + With no names, formats the current directory. A single "-" formats + policy from stdin to stdout. Returns 0 on success (or no reformat + needed), 1 when reformatting is needed in check mode or a policy file + has syntax errors.""" + try: + return _format_paths_inner(names, line_length, check) + except PolicySyntaxError as e: + print(f"Error: {e}") + return 1 diff --git a/uv.lock b/uv.lock index 8ebd0b8..c104d77 100644 --- a/uv.lock +++ b/uv.lock @@ -25,11 +25,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.5.20" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] @@ -46,11 +46,11 @@ wheels = [ [[package]] name = "cfbs" -version = "5.6.1" +version = "5.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/ab/60bc20d8aae03a6ee288cdac7a9cb35778eeed974b452895e7a6d3ff5dd9/cfbs-5.6.1.tar.gz", hash = "sha256:9e50371add952f05e2f4591d6a6280d3aca5ce60ad9449910c86a20f20768781", size = 89383, upload-time = "2026-06-12T17:21:45.984Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/46/bda65e6ee28e7bb8fa25b1b6a2ee489c9a2d58caa9c72ba2141af975309e/cfbs-5.6.2.tar.gz", hash = "sha256:25bfa7872d33e0ba78c9c794a485448216bba52ebe356e3db7e7559f22ec1001", size = 89632, upload-time = "2026-06-18T17:29:17.823Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/a0/a5df7ad2e2373b6c1cc8140de229da9450348bf563ae7609b13ed422544e/cfbs-5.6.1-py3-none-any.whl", hash = "sha256:8897f17f2544ccb741f95526bf35399fe7d2de504a261d7ad8f0881b7e097bb7", size = 85254, upload-time = "2026-06-12T17:21:44.63Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/829753a0febc5c789b6b7bcb4020314bac5aaa0346a3954390a1b377e44b/cfbs-5.6.2-py3-none-any.whl", hash = "sha256:0e24989244545c754c4165773399370ab2d1965281994e13ada3709fde77bae0", size = 85533, upload-time = "2026-06-18T17:29:16.689Z" }, ] [[package]] @@ -455,7 +455,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -466,9 +466,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]]