Migrating from yamllint¶
ryl is designed as a drop-in replacement for yamllint's existing rule set. If you are coming from yamllint you have two paths:
- Keep your existing YAML configuration. ryl reads
.yamllint,.yamllint.yml, and.yamllint.yamlwith the same semantics as upstream. No changes needed to get started. - Migrate to TOML. TOML is the recommended format for ryl-specific
features that have no upstream equivalent — for example the
[fix]table controlling auto-fix selection.
On the command line the mapping is mechanical: replace yamllint with ryl
check, keeping every flag and path the same. ryl check accepts the same lint
flags as yamllint (-c/-d/-f/-s/--no-warnings/--list-files/-), so
yamllint -d 'extends: default' . becomes ryl check -d 'extends: default' ..
Bare ryl <paths> also lints today but is being phased out in favour of ryl
check.
Automatic migration¶
ryl ships with a built-in converter. From the root of your project:
# Preview the converted TOML (no files written)
ryl --migrate-configs
# Write .ryl.toml next to each discovered .yamllint file
ryl --migrate-configs --migrate-write
# Write and remove the original YAML configs
ryl --migrate-configs --migrate-write --migrate-delete-old
# Write and rename the original YAML configs (e.g. .yamllint.bak)
ryl --migrate-configs --migrate-write --migrate-rename-old .bak
To migrate your user-global yamllint config (the personal defaults at
<config-dir>/yamllint/config) to ryl's own location
(<config-dir>/ryl/ryl.toml), use --migrate-user-config:
# Preview the user-global conversion
ryl --migrate-user-config
# Write it (creating the ryl/ directory if needed)
ryl --migrate-user-config --migrate-write
# Migrate project configs and the user-global config in one run
ryl --migrate-configs --migrate-user-config --migrate-write
Migrating the user-global config is optional: ryl still reads
<config-dir>/yamllint/config directly, so an unmigrated yamllint user-global
config keeps working.
Useful flags:
| Flag | Purpose |
|---|---|
--migrate-configs |
Migrate project-tree YAML configs |
--migrate-user-config |
Migrate the user-global yamllint config |
--migrate-root <DIR> |
Project search root (defaults to .) |
--migrate-stdout |
Print generated TOML to stdout instead of writing |
--migrate-write |
Write files (otherwise preview only) |
--migrate-rename-old <SUFFIX> |
Rename source YAML configs after migration |
--migrate-delete-old |
Delete source YAML configs after migration |
The --migrate-write / --migrate-stdout / --migrate-rename-old /
--migrate-delete-old flags apply to whichever migration trigger
(--migrate-configs, --migrate-user-config, or both) is set; --migrate-root
applies to project migration only.
Migration never overwrites or deletes through surprises: it skips (with a
warning, leaving the source untouched) any config whose target directory already
contains a ryl-native config (.ryl.toml or ryl.toml), and it refuses to
follow a symlink for either the source or the target (mirroring --fix). It also
refuses to overwrite an existing backup when --migrate-rename-old would clobber
one.
Known limitation: if a write is interrupted (for example, the disk fills mid write), migration may leave a partial config file behind. The next run reports it via the "a ryl-native config already exists" skip warning, so delete the partial file and re-run.
Migration produces a self-contained config. A relative extends is flattened
(its rules are inlined), resolved relative to the config's own directory first,
then the current directory. The user-global config moves to a new directory
(<config-dir>/ryl/), so a top-level ignore-from-file is inlined as ignore
patterns there too, rather than left as a relative path that would no longer
resolve. A user-global config with a rule-level ignore-from-file is skipped
with a warning (inline those patterns or use an absolute path, then re-run),
since its rule config cannot be relocated safely. Project migration keeps a
relative ignore-from-file as-is, because the .ryl.toml stays in the same
directory.
After migration, run ryl check . to confirm diagnostics match what yamllint
produced.
What is preserved¶
- Rule names, defaults, and option semantics match yamllint's existing rule set.
- Diagnostic message text is kept aligned with yamllint where practical, so existing log scrapers continue to work.
- Configuration discovery walks the same locations as yamllint, with TOML
formats checked in addition: an explicit
--config-file, then a project-local.ryl.toml,ryl.toml,.config/.ryl.toml,.config/ryl.toml,pyproject.toml,.yamllint,.yamllint.yaml, or.yamllint.yml, then theYAMLLINT_CONFIG_FILEenv var, then a user-global config. At the user-global step ryl checks its own<config-dir>/ryl/.ryl.toml(orryl.toml) first, then falls back to yamllint's<config-dir>/yamllint/config(see ryl-native user-global config for how<config-dir>resolves per platform). YAMLLINT_CONFIG_FILEhonours only a yamllint YAML config; pointing it at a.tomlerrors (seeYAMLLINT_CONFIG_FILErejects TOML).- The three built-in presets —
default,relaxed, andempty— match yamllint's behaviour. YAML configs can still useextends:to reference them; TOML configs must inline thedefault/relaxedcontent (see the example below).emptyhas no usable TOML form because ryl rejects a config that enables no rules. The full preset content is in Configuration presets. - Inline
# yamllint disable/disable-line/enablecomments are honoured with the same grammar and semantics, so existing in-file suppressions keep working. The equivalent# ryl …spelling is preferred for new files; see Inline directives.
Configuration discovery precedence¶
ryl resolves the configuration governing each file it lints, trying these
sources in order and stopping at the first hit. -d/-c and
YAMLLINT_CONFIG_FILE pin a single config for the whole run; otherwise project
discovery runs per file, so a monorepo can hold many configs, each governing its
own subtree. ryl recognises the same config sources as yamllint (-d/-c,
project files, YAMLLINT_CONFIG_FILE, a user-global config) and adds ryl-native
TOML for -c, project discovery, and the user-global config (but not
YAMLLINT_CONFIG_FILE, which stays yamllint-YAML-only). It diverges from yamllint
in two ways: yamllint folds YAMLLINT_CONFIG_FILE into the user-global slot (the
env var replaces it, and a missing target falls through to extends: default),
whereas ryl keeps the env var and the user-global config as distinct fall-through
tiers; and ryl requires the resolved config to enable at least one rule or it
exits 2 (yamllint instead lints with its default preset when nothing is found,
and silently accepts a rule-less config).
See the configuration precedence
diagram in the Quick Start for the full
resolution flow, rendered from -d/-c through project and user-global tiers to
the rule-enabled gate.
How ryl differs from yamllint¶
ryl is a drop-in replacement, but it intentionally diverges from yamllint in a small number of places. Every divergence is deliberate: ryl is more correct against the YAML 1.2.2 specification, it fails loudly instead of silently, or it avoids redundant output. The complete list:
No implicit default configuration¶
ryl never enables a rule unless a configuration explicitly turns it on. yamllint,
run with no configuration, lints with its default preset; ryl instead exits 2
with no configuration found. It also exits 2 (no rules enabled) when a
resolved config turns every rule off, where yamllint would silently lint nothing.
Why ryl differs: linting with rules the user never asked for — or
silently doing nothing — is surprising. ryl makes the rule set explicit and
reports loudly when there isn't one. The presets stay available as explicit
opt-ins, so the one-line equivalent of yamllint's out-of-the-box behaviour is a
YAML config containing extends: default (or the corresponding TOML preset from
Configuration presets). The migration converter flattens an
extends: default source into the generated TOML automatically, and warns when a
migrated config ends up enabling no rules.
Anchor and alias names containing a colon¶
YAML 1.2.2 §6.9.2 defines an anchor/alias name as ns-anchor-char+, and
ns-anchor-char excludes only the flow indicators , [ ] { } — so a colon
is a legal name character. ryl's parser and the
YAML reference parser both read a : as part of the
name; yamllint (via PyYAML) stops at the first :, which the specification does
not permit. ryl follows the spec, so the anchors rule can diverge:
Input (with &x/&foo… as noted) |
ryl (spec-conformant) | yamllint (PyYAML) |
|---|---|---|
b: {*x: 2} |
*x: is an undeclared alias; x is unused |
reads {x: 2} — no diagnostic |
&foo:bar and &foo:baz |
two distinct anchors | one anchor foo, reported duplicated |
*foo:baz |
a valid alias named foo:baz |
a syntax error (PyYAML cannot scan it) |
The unused note in row 1 and the duplicated report in row 2 require the matching
non-default anchors options (forbid-unused-anchors, forbid-duplicated-anchors); the
colon-in-name parsing divergence the table illustrates is independent of them.
Why ryl differs: the YAML specification and its reference parser are the
authority, and PyYAML's narrowing at : is non-conformant (see
adrienverge/yamllint#686 and
adrienverge/yamllint#780).
The portable, unambiguous way to use an alias as a mapping key is a space before
the colon — *foo : bar — which every parser reads identically. The
ryl-only anchors: forbid-ambiguous-anchor-alias-names
option flags welded colons so you can forbid the construct outright.
De-duplicated inputs¶
When a single run names the same file more than once — listed twice, or reached
by both a directory argument and an explicit path (ryl check . file.yaml) — ryl
processes it once. yamllint handles each occurrence, so it would report that
file's diagnostics (or, under ryl check --diff, emit its patch) twice.
Why ryl differs: duplicate output is never useful, and it is actively harmful for
--diff, whose output is meant to be applied as a patch — a repeated patch
block fails to apply on the second copy. ryl normalizes each input path (lexically,
without resolving symlinks, so a symlink and its target stay distinct) and skips a
file it has already selected.
Bare carriage-return (\r) line breaks¶
YAML 1.2.2 §5.4 defines a line break as ( CR LF ) | CR | LF, so a bare \r
(a carriage return not followed by a line feed) is a complete line break on its own.
ryl honours this everywhere — every rule treats \r, \r\n, and \n
identically. yamllint's line layer counts \n only (a bare \r is ordinary
content), so on a classic-Mac \r-only file the two disagree:
On a bare-\r line |
ryl (YAML 1.2) | yamllint (\n-only) |
|---|---|---|
spaces before the \r |
trailing-spaces flags them |
not flagged |
a long line ending at \r |
line-length measures to the \r |
counts across the \r |
a file ending in \r |
new-line-at-end-of-file is satisfied |
reports "no new line" |
the file's first break is \r |
new-lines flags it (not unix/dos) and --fix rewrites it |
invisible (no mac type) |
| line/column of any diagnostic | counted CR-aware | shifted by uncounted \rs |
On supported LF and CRLF files this is identical to yamllint — the two
line-break definitions agree exactly when there is no bare \r. The divergence is
visible only on \r-only (or mixed-\r) files, which yamllint cannot lint
faithfully anyway (and whose new-lines type has no mac value). ryl follows the
specification and its reference parser, which rank above
yamllint as the authority.
Two editing-path caveats on such files. First, ryl check --diff skips a file whose
content ends in a bare \r (a unified diff is \n-terminated by format, so that
one case has no applicable patch — use ryl check --fix, which rewrites bytes
directly); a bare \r elsewhere is diffed as line content and applies via
git apply. Second, Markdown embedding requires a Markdown file with no bare \r anywhere:
the fenced-block and front-matter parser
(pulldown-cmark) does not implement
CommonMark §2.1's bare-\r line ending, so a bare \r used as a host line ending
hides the YAML blocks from it, and a bare \r inside an embedded block (which the
parser keeps verbatim) can't be mapped back to a host position by ryl's \n-based
remap. Rather than silently check nothing — or report a wrong position — ryl reports
an error (and --fix/--diff skip with a notice) telling you to convert the file to
LF or CRLF. The YAML inside an LF/CRLF Markdown host (itself free of bare \r) is
linted CR-aware like any other.
%YAML version-directive handling¶
ryl honours the %YAML directive (YAML 1.2.2 §6.8.1) per document: a
directive-less document and %YAML 1.2 resolve under the 1.2 core schema, an
explicit %YAML 1.1 resolves under 1.1, a higher major version (%YAML 2.0)
is rejected, and a higher minor version (%YAML 1.3) is processed as 1.2 with
a warning. yamllint (via PyYAML) resolves implicit types under 1.1 regardless of
the directive, so the two disagree:
| Input | ryl | yamllint |
|---|---|---|
'no' with no directive, quoted-strings: only-when-needed |
redundant (1.2 string) | kept (1.1 boolean) |
'no' under %YAML 1.1, same rule |
kept (1.1 boolean) | kept |
'y' under %YAML 1.1, same rule |
kept (y is a 1.1 boolean) |
redundant (PyYAML omits y) |
%YAML 1.3 document |
warning, processed as 1.2 | no diagnostic |
%YAML 2.0 document |
rejected (syntax) |
rejected (syntax) |
Why ryl differs: the spec is the authority. It assigns a directive-less
document to 1.2, directs a 1.2 processor to honour %YAML 1.1, and mandates
rejecting a higher major version — and the 1.1 boolean set in the spec
includes single y/n, which PyYAML's resolver omits. Resolving under the
declared version also keeps --fix sound: it never strips the quotes from a
scalar whose value would change under the document's own %YAML 1.1.
Per-line ignores¶
ryl adds a per-line-ignores config table with no
yamllint counterpart: it suppresses chosen rules on lines (and/or files) matching a
regex/glob, the config-level complement to inline # ryl disable-line. yamllint
offers only inline directives and per-file path ignores, so a recurring exception
(every #cloud-config, every # renovate: marker) must be annotated in each file
or handled per-rule. Being ryl-only, it lives in TOML config and is rejected in
yamllint-compatible YAML config.
Comment alignment to an open block level¶
yamllint's comments-indentation requires a standalone comment to match the
indentation of the content that follows it. ryl adds a ryl-only, off-by-default
comments-indentation: allow-any-open-indent
option that also accepts a comment aligned to any still-open enclosing block
level — the pattern requested in
adrienverge/yamllint#141,
which the maintainer welcomed but yamllint has not implemented:
config:
entry:
- things
# accepted under the option: marks the end of `entry:` at its open level
options:
- more stuff
With the option off (the default) ryl matches yamllint exactly. Being ryl-only, the option is configured in TOML and rejected in yamllint-compatible YAML config.
Dash on its own line before a block mapping¶
yamllint's hyphens rule only limits the spaces after a -. ryl adds a ryl-only,
off-by-default hyphens: dash-on-own-line option that requires a
block-sequence entry's - to be on its own line when the entry is a block mapping
— the spec-style "sequence of mappings" layout requested in
adrienverge/yamllint#527, which
the maintainer welcomed but yamllint has not implemented:
items:
- name: web # flagged under the option: mapping starts on the dash line
port: 80
-
name: db # accepted: dash alone, mapping body indented below
port: 90
With the option off (the default) ryl matches yamllint exactly. Being ryl-only, the option is configured in TOML and rejected in yamllint-compatible YAML config.
JUnit and GitLab report formats¶
yamllint offers standard, parsable, colored, github, and auto output formats.
ryl keeps those and adds two machine-readable report formats: --format junit (JUnit XML)
and --format gitlab (GitLab Code Quality JSON), which write to stdout (or to a file with
-o/--output-file) so a Git forge can ingest them as a report artifact. Going beyond
yamllint, --format is repeatable and each pairs with its own --output-file, so a single
run can emit console diagnostics and one or more report files; the same targets can be
set in a ryl-only TOML [output] table. See Output formats.
ryl-native user-global config¶
yamllint's only user-global config lives at $XDG_CONFIG_HOME/yamllint/config (else
~/.config/yamllint/config on every platform). ryl still reads that path for migrators,
but checks its own <config-dir>/ryl/.ryl.toml (or ryl.toml) first, following the
ruff/Biome convention: <config-dir> is $XDG_CONFIG_HOME if set, else the platform-native
config dir (~/.config/ryl on Linux, ~/Library/Application Support/ryl on macOS,
%APPDATA%\ryl on Windows). Being ryl-only, the ryl-native path is TOML; the
yamllint-compatible path stays YAML and, matching yamllint, always resolves under
$XDG_CONFIG_HOME or ~/.config (not the native dir).
YAMLLINT_CONFIG_FILE rejects TOML¶
YAMLLINT_CONFIG_FILE is yamllint's env var, so ryl honours it only as a yamllint
YAML config. Pointing it at a .toml errors (even when the file is absent),
directing you to -c/--config-file or project discovery for ryl-native TOML. A
YAML (or extension-less) target keeps full yamllint compatibility, including
yamllint's behaviour of silently ignoring a missing target and falling through
to the next config source.
YAMLLINT_CONFIG_FILE target |
ryl | yamllint |
|---|---|---|
present .toml |
error (use -c) |
parsed as YAML (not TOML) |
missing .toml |
error (use -c) |
ignored, falls through |
missing .yml/config |
ignored, falls through | ignored, falls through |
Why ryl differs: TOML is never a valid yamllint config, so there is no
compatibility to preserve for a .toml target — loading one through the
yamllint-named var was an accidental overlap with ryl's own format. ryl fails loudly
on it and keeps ryl-native TOML on its dedicated channels (-c/--config-file,
project discovery). yamllint's -c flag likewise errors on a missing file (only its
env var is lenient), and biome's BIOME_CONFIG_PATH errors on a missing target
outright, so failing loudly on the always-wrong .toml case is well-precedented.
Side-by-side example¶
[files]
yaml = [
"*.yaml",
"*.yml",
".yamllint",
]
[rules]
anchors = "enable"
braces = "enable"
brackets = "enable"
colons = "enable"
commas = "enable"
document-end = "disable"
empty-lines = "enable"
empty-values = "disable"
float-values = "disable"
hyphens = "enable"
indentation = "enable"
key-duplicates = "enable"
key-ordering = "disable"
new-line-at-end-of-file = "enable"
new-lines = "enable"
octal-values = "disable"
trailing-spaces = "enable"
truthy = "disable"
[rules.comments]
level = "warning"
[rules.comments-indentation]
level = "warning"
[rules.document-start]
level = "warning"
[rules.line-length]
max = 120
allow-non-breakable-words = true
[rules.quoted-strings]
quote-type = "double"
required = "only-when-needed"
TOML configuration is flat
TOML configuration does not support extends. Presets are expanded
inline so the entire effective rule set is visible in one file. The
--migrate-configs flag handles the expansion for you; if you write a
TOML config by hand, start from the preset content in
Configuration presets.
Optional: configure auto-fixes¶
TOML configurations can declare which rules are eligible for ryl check --fix:
This is a ryl-only feature. See the Rules reference for the list of rules that support automatic fixing.
Keeping both files¶
.yamllint and .ryl.toml can coexist. ryl prefers TOML when both are
present in the same directory, but a project that needs to remain
compatible with yamllint in CI can keep the YAML file authoritative and
treat the TOML file as supplemental for ryl-only features.