Skip to content

Formatter Behavior

What cmakefmt preserves, what it intentionally changes, and how to reason about the output when you run it across a real codebase.

cmakefmt is designed to be:

  • safe: formatting must never change the meaning of the file
  • idempotent: formatting the same file twice must produce the same result
  • predictable: line wrapping and casing follow explicit config, not heuristics you have to reverse-engineer
  • respectful of structure: comments, disabled regions, and command shapes are all first-class
  • comments and comment ordering
  • bracket arguments and bracket comments
  • disabled regions (# cmakefmt: off / # cmakefmt: on)
  • command structure as defined by the built-in or user-supplied command spec
  • blank-line separation, bounded by max_empty_lines
  • parse-tree equivalence for formatted output on supported inputs
  • command name case when command_case is not unchanged
  • keyword and flag case when keyword_case is not unchanged
  • indentation and wrapping
  • blank-line runs that exceed the configured limit
  • line-comment layout when markup or comment-reflow options are enabled

cmakefmt tries the simplest layout first and only escalates when necessary:

  1. keep a call on one line when it fits
  2. use a hanging-wrap layout when that stays readable
  3. fall back to a more vertical layout when width and grouping thresholds are exceeded

Input:

target_link_libraries(foo PUBLIC bar)

Output:

target_link_libraries(foo PUBLIC bar)

Input:

target_link_libraries(foo PUBLIC very_long_dependency_name another_dependency)

Typical output:

target_link_libraries(
foo
PUBLIC
very_long_dependency_name
another_dependency)

The exact shape depends on the command spec, line width, and wrapping thresholds in your config.

cmakefmt preserves meaningful vertical separation, but clamps runaway blank-line gaps according to format.max_empty_lines.

Input:

project(example)
add_library(foo foo.cc)

Output with max_empty_lines = 1:

project(example)
add_library(foo foo.cc)

Comments are never discarded and re-inserted later. They are tracked as real syntax nodes throughout the entire formatter pipeline.

That distinction matters. It means cmakefmt can reliably preserve:

  • standalone comments above a command
  • inline argument-list comments
  • trailing same-line comments
  • bracket comments

Example:

target_sources(foo
PRIVATE
foo.cc # platform-neutral
bar.cc)

cmakefmt keeps the trailing comment attached to the relevant argument.

When markup handling is enabled, cmakefmt can recognize and treat some comments as lists, fences, or rulers rather than opaque text.

The key knobs:

  • markup.enable_markup
  • markup.first_comment_is_literal
  • markup.literal_comment_pattern

To leave comments almost entirely alone, set enable_markup: false.

Structured commands — if/elseif/else/endif, foreach/endforeach, while/endwhile, function/endfunction, macro/endmacro, block/endblock — are treated as block constructs rather than flat calls. This affects indentation and spacing around their parentheses.

With space_before_control_paren = true:

if (WIN32)
message(STATUS "Windows build")
endif ()

Without it:

if(WIN32)
message(STATUS "Windows build")
endif()

Need to protect a block from formatting? Use a disabled region:

# cmakefmt: off
set(SPECIAL_CASE keep this exactly)
# cmakefmt: on

All of the following markers work:

  • # cmakefmt: off / # cmakefmt: on
  • # cmake-format: off / # cmake-format: on
  • # fmt: off / # fmt: on
  • # ~~~

This is the escape hatch for generated blocks, unusual macro DSLs, or legacy sections you are not ready to normalize yet.

Custom commands format well only when cmakefmt understands their structure. That is what commands: in your config is for. Once you tell the registry what counts as positional arguments, standalone flags, and keyword sections, the formatter groups and wraps those commands intelligently — instead of treating every token as an undifferentiated lump.

per_command_overrides: changes formatting knobs for a single command name without touching its argument structure.

Use it when you want:

  • a wider line_width for message
  • different casing for one specific command
  • different wrapping thresholds for a single noisy macro

Do not use it to describe a command’s argument structure. That belongs in commands:.

set() is the most common CMake command and has several distinct usage patterns. cmakefmt handles each one with a specific rule: the variable name always stays on the set( line. This is enabled by the built-in wrap_after_first_arg layout hint on the set command spec.

When everything fits on one line, it stays inline:

set(FOO bar)
set(FOO a b c)
set(FOO "value" PARENT_SCOPE)
set(ENV{FOO} "value")
set(FOO)

The variable name stays attached. Remaining items are aligned to the open parenthesis:

set(HEADERS
header_a.h
header_b.h
header_c.h
header_d.h)

When everything fits, it stays on one or two lines:

set(FOO "default" CACHE STRING "A description" FORCE)
set(CMAKE_BUILD_TYPE "Release"
CACHE STRING "Build mode for performance." FORCE)

When the CACHE section is too long, it wraps with STRING and the description nested under CACHE:

set(CMAKE_BUILD_TYPE "Release"
CACHE
STRING "A very long description that doesn't fit on one line"
FORCE)

Inline comments stay attached to the variable name:

set(MY_VAR # explanation of the variable
value_one value_two value_three)

To disable wrap_after_first_arg for set():

per_command_overrides:
set:
wrap_after_first_arg: false

This reverts to the standard vertical layout where everything wraps below the opening parenthesis:

set(
MY_VAR
value_one
value_two)

See wrap_after_first_arg in the config reference for the full option documentation.

Inline comments (# text) that follow an argument stay attached to that argument when the command wraps. The comment and argument are kept on the same line as long as the combined width fits within line_width.

target_link_libraries(
mylib
PUBLIC
dep1 # first dependency
dep2 # second dependency
dep3 # third dependency)

If a trailing comment would exceed line_width, it moves to its own line at the current indentation:

target_link_libraries(
mylib
PUBLIC
some_very_long_dependency_name
# This comment is too long to fit after the argument
another_dependency)

When wrap_after_first_arg keeps the variable name on the set( line, an inline comment stays attached:

set(foobarbaz # explanation of this variable
value_one value_two value_three)

A comment within a CACHE section is reflowed (when enable_markup is on) and placed on its own line:

set(CMAKE_BUILD_TYPE "Release"
CACHE
STRING
# This comment explains why we default to Release
# and spans multiple lines after reflow.
"Build mode for performance." FORCE)

A short comment after the closing parenthesis stays inline:

set(FOO bar) # explanation of this variable

Comments on if(), foreach(), and similar stay inline:

if(CONDITION) # check this before proceeding
message(STATUS "hello")
endif()

The presence of a trailing comment does not by itself force a command into a vertical layout. The layout decision (inline, hanging, or vertical) is made independently based on line width and wrapping thresholds. The comment is then rendered within the chosen layout.

When markup.enable_markup is true (the default), long comments that exceed line_width are reflowed with continuation lines aligned to the #:

# Before:
set(FOO bar) # this trailing comment is deliberately long enough to exceed the default line width and demonstrate reflow clearly
# After (enable_markup: true):
set(FOO bar) # this trailing comment is deliberately long enough to
# exceed the default line width and demonstrate reflow
# clearly
# After (enable_markup: false):
set(FOO bar) # this trailing comment is deliberately long enough to exceed the default line width and demonstrate reflow clearly

To disable comment reflow entirely, set markup.enable_markup: false.

--lines START:END formats only selected line ranges. This is mainly for editor workflows and partial-file automation.

Important: the selected range still lives inside a full CMake file. Surrounding structure still applies. Partial formatting is best-effort, not an isolated mini-file pass.

When a formatting result surprises you, --debug is the first thing to reach for. It surfaces everything the formatter normally keeps to itself:

  • file discovery
  • selected config files and CLI overrides
  • barrier and fence transitions
  • chosen command forms
  • effective per-command layout thresholds
  • chosen layout families
  • changed-line summaries

For structural questions — “how does the parser see this file?” or “why is this token treated as a keyword?” — use the tree dump commands:

Terminal window
cmakefmt dump ast CMakeLists.txt # raw parser AST
cmakefmt dump parse CMakeLists.txt # spec-resolved tree

See Parse Tree Dump in the CLI reference for full details and example output.

cmakefmt is a practical replacement for cmake-format, not a byte-for-byte clone. That means:

  • some outputs differ while still being valid and stable
  • the config surface has been cleaned up in places (see Config Reference for the old-to-new key name mapping)
  • workflow features are intentionally broader
  • diagnostics are intentionally much more explicit

Wrapping thresholds. cmakefmt uses a principled pretty-printing algorithm to decide layouts. The exact line at which a call wraps can differ from cmake-format’s heuristics, even with identical config values. The result is still correct and idempotent — just not always output-identical.

Keyword grouping. When cmakefmt knows a command’s structure (via the built-in registry or a commands: entry), it groups keyword sections deliberately. cmake-format without a matching spec entry would often treat the same tokens as undifferentiated positional arguments and produce a flatter layout.

Comment reflow. By default, cmakefmt preserves comments without modification. If you want comments reflowed to fit within the configured line width, enable markup.enable_markup: true.

Config key names. Several config keys were renamed for clarity. Any key cmakefmt does not recognise will produce a fast-fail error, not a silent no-op. The full renaming table is in Config Reference.

When comparing outputs during migration, judge by readability, stability, semantic preservation, and ease of automation — not solely by whether every wrapped line matches historical cmake-format output exactly.

Docs track main. For historical docs, check out a release tag in the repository and build docs/ locally.