Use cases

Moving bash check scripts to alint

Large repositories collect check scripts: a hack/verify-*.sh directory, Python checkers under scripts/, lint targets in a Makefile. alint takes over the ones that check files, names and config values, in one config and one run. Scripts that parse code or call the network stay scripts, and alint can run those too.

From a one-liner to a rule

The shell lines that turn up in check scripts, and the rule kind that does the same job. Each kind links to its page in the rule catalog, with its options and an example.

In a scriptalint rule kindNotes
grep -rn 'TODO\|FIXME' src/file_content_forbiddenAdd git_blame_age to flag only the ones older than, say, 180 days.
find . -name '*.swp' -o -name '*~'file_absentThe hygiene/no-tracked-artifacts ruleset forbids editor backups and removes them with alint fix.
find . -name .DS_Storefile_absent with a file_remove fixAlso in hygiene/no-tracked-artifacts.
for d in packages/*/; do test -f "$d/README.md"; donefor_each_dir with a nested file_existsThe monorepo ruleset has this rule.
grep -q 'license =' crates/*/Cargo.tomltoml_path_matches over crates/*/Cargo.tomlThe second example below shows what the grep misses.
[ -x "$f" ] for every *.shexecutable_bit with require: trueOr pair executable_has_shebang with shebang_has_executable.
grep -rnE '[[:space:]]+$' src/no_trailing_whitespaceGive the rule a file_trim_trailing_whitespace fix and alint fix trims it. oss-baseline's rule has one.
A check for CRLF line endingsline_endings with target: lfGive the rule a file_normalize_line_endings fix and alint fix converts the file.
git diff --checkno_merge_conflict_markers and no_trailing_whitespaceoss-baseline has both for text and config files.
[[ $f =~ ^test-.*\.js$ ]] in test/parallelfilename_regexThe naming convention nodejs/node uses for its test files.
jq -e '.license == "MIT"' package.jsonjson_path_equalsUse json_path_matches for a regex.
python -c 'import json, sys; json.load(open(sys.argv[1]))'json_schema_passesA file that doesn't parse is a finding. alint accepts comments and trailing commas, which Python's json module rejects.
yq '.permissions' .github/workflows/*.ymlgha-workflow-contents-readIn the ci/github-actions ruleset, or write a yaml_path_equals rule.
grep -E 'uses: .*@[0-9a-f]{40}'gha-pin-actions-to-shaAlso in ci/github-actions.
head -1 src/*.rs | grep SPDXfile_headerA file_prepend fix adds the missing header.
find . -size +5Mfile_max_sizePut vendored and test data directories in exclude.
find migrations -name '*.sql' -printf '%f\n' | sort | uniq -dunique_byThe default key is the file name; {stem} ignores the extension.

Two scripts side by side

Every package has a README

hack/verify-readmes.sh
#!/usr/bin/env bash
set -euo pipefail
fail=0
for d in packages/*/; do
  if [ ! -f "${d}README.md" ]; then
    echo "missing: ${d}README.md" >&2
    fail=1
  fi
done
exit $fail
bash hack/verify-readmes.sh
missing: packages/emails/README.mdmissing: packages/web/README.md

Both find the two packages. The rule's report names the rule and comes in any of alint's8 output formats, and --changed doesn't skip it: rules across files always see the whole tree.

.alint.yml
- id: package-readme
  kind: for_each_dir
  select: "packages/*"
  require:
    - kind: file_exists
      paths: "{path}/README.md"
      message: "{path} has no README.md"
  level: error
alint check
packages/emails  ✗  error    package-readme              packages/emails has no README.md packages/web  ✗  error    package-readme              packages/web has no README.md
packages/emails  ✗  error    package-readme              packages/emails has no              README.md packages/web  ✗  error    package-readme              packages/web has no              README.md

The monorepo ruleset ships this rule for packages/*, crates/*, apps/* and services/*.

Every crate declares a license

hack/verify-licenses.sh
#!/usr/bin/env bash
for f in crates/*/Cargo.toml; do
  grep -q 'license =' "$f" || { echo "no license: $f"; exit 1; }
done
bash hack/verify-licenses.sh
no license: crates/core/Cargo.toml

The script stops at the first crate without a license, and a commented-out # license = "MIT" line in crates/cli/Cargo.toml satisfies its grep, so it reports 1 of the 3 crates that declare no license. The rule reads package.license from each Cargo.toml and checks that the value looks like an SPDX expression.

.alint.yml
- id: every-crate-declares-license
  kind: toml_path_matches
  paths: "crates/*/Cargo.toml"
  path: "$.package.license"
  matches: '^[A-Za-z0-9.+-]+( (OR|AND) [A-Za-z0-9.+-]+)*$'
  message: "Set package.license to an SPDX expression"
  level: error
alint check
crates/cli/Cargo.toml  ✗  error    every-crate-declares-license              Set package.license to an SPDX expression crates/core/Cargo.toml  ✗  error    every-crate-declares-license              Set package.license to an SPDX expression crates/web/Cargo.toml  ✗  error    every-crate-declares-license              Set package.license to an SPDX expression
crates/cli/Cargo.toml  ✗  error    every-crate-declares-license              Set package.license to an              SPDX expression crates/core/Cargo.toml  ✗  error    every-crate-declares-license              Set package.license to an              SPDX expression crates/web/Cargo.toml  ✗  error    every-crate-declares-license              Set package.license to an              SPDX expression

The glob only matches directories that have a Cargo.toml, so a fixtures folder under crates/ is left alone.

Checks that need more than grep

The script checks thatRule kind
A generated file matches what its generator producesgenerated_file_fresh
Only some packages may import a pathimport_gate
Vendored files match a checksum filepair_hash
Paths listed in a manifest exist, such as workspace membersregistry_paths_resolve
A value agrees across files, such as a version in two manifestscross_file
No import cycles or layering violations between filesfile_graph
A change to one file comes with a change to anotherpair_changed_together
Every commit in a pull request has the right messagegit_commit_message with since:

Which scripts move

The script checksIn alint
Which files exist, what they're called, what they containRule kinds for existence, naming and content
Values in package.json, Cargo.toml or workflow filesStructured query rules, with JSONPath
Relations between files, such as a README in every packageCross-file rules
Commit messages, sign-offs, which files a pull request changesGit hygiene rules
Anything that needs a compiler or parser: types, API compatibility, module graphsKeep the script, and run it from a command rule if you like
Builds, tool versions, network probesKeep the script

What stays. Checks that need type information, such as Kubernetes' verify-typecheck.sh; checks that build a binary and inspect it; link checkers, license lookups and registry calls; module graphs from go list or cargo metadata. file_graph covers edges between files, read from their contents.

In Kubernetes, 17 of the 51 checks the verify scripts make stay in them.

Running the scripts you keep

A command rule runs a program for each matching file and reports a non-zero exit as a finding, so CI still calls one tool:

.alint.yml
- id: shellcheck
  kind: command
  paths: "**/*.sh"
  command: ["shellcheck", "-x", "{path}"]
  level: error

- id: templated-fields
  kind: command
  paths: "src/**/*.py"
  command: ["scripts/check-templated-fields.py", "{path}"]
  timeout: 60
  level: warning
.alint.yml
- id: shellcheck
  kind: command
  paths: "**/*.sh"
  command: ["shellcheck", "-x", "{path}"]
  level: error

- id: templated-fields
  kind: command
  paths: "src/**/*.py"
  command: ["scripts/check-templated-fields.py", "{path}"]
  timeout: 60
  level: warning
  1. Lines 1 to 5

    ShellCheck, once per shell script. Under --changed, only for the scripts in the diff.

  2. Lines 7 to 12

    Your own checker, with a longer timeout than the default of 30 seconds and a warning instead of an error.

  • Command rules work only in your own top-level config; a ruleset pulled in with extends: can't add one.
  • The program gets the file path in its arguments as {path} and in ALINT_PATH. Other variables carry the repository root, the rule id and level, and your vars and facts; the command rule page lists them.

Three repositories

alint configs were written for these projects, with every check they already run sorted. The projects don't use alint; each reference page has the config and what it found.

RepositoryChecks it ran in May 2026In the alint config
kubernetes/kubernetes50 hack/verify-*.sh scripts; make verify ran most of them on every pull requestThe config covers about two thirds of what they check, 34 of the 51 checks counted: most as alint rules, and the code, docs and vendor generators by running Kubernetes' own scripts from alint, which fails when one would change a file. The rest needs Go's type checker, a build or the network.
python/cpython35 pre-commit hooks, Make targets that lint or regenerate code, checking scripts under Tools/build and 25 workflowsThe config covers 25 of the 31 checks counted. The C API checks need the C code or the built interpreter, so they stay Make targets.
apache/airflowA 1,085-line Python script validates the 101 provider packagesThe config checks the provider layout with four rules, without importing any package.

A plan for the move

  1. Sort each script into the rows of the "Which scripts move" table.
  2. Start .alint.yml with the bundled rulesets for your stack. alint list prints every rule they bring in, so you can cross off the scripts they already cover.
  3. Add a rule for each remaining script that moves. Use readable ids; they appear in every report.
  4. Run alint check on your main branch. Expect a few findings the scripts missed and a few rules with paths set too wide. Record what you won't fix yet with alint baseline.
  5. Wrap the scripts you keep in command rules.
  6. Replace the CI steps with one alint check and pick the output your CI reads: SARIF, GitHub annotations, JUnit or GitLab code quality are among the 8 output formats.

alint suggest looks for problems already in the tree and proposes rules for them, which helps with step 3.

Try it

alint init writes a starter config for the languages and tools it finds. For a monorepo with packages, a first version can be three rulesets:

.alint.yml
version: 1
extends:
  - alint://bundled/oss-baseline@v1
  - alint://bundled/monorepo@v1
  - alint://bundled/hygiene/no-tracked-artifacts@v1

Install alint, run alint check, then add the rules your scripts need. The examples have a starter per stack.

Next: the Kubernetes reference config, with what its verify scripts check and which checks alint covers; the rule catalog and the cookbook; and how alint compares.