Skip to content

Linting Code

Linting is running documented checks to statically analyze code for common mistakes and errors.

It can also be a great way to learn a new programming language as you'll be pointed to coding conventions, often in the form of the problematic code snippet, suggestions on how to refactor it, and the reasoning behind why.

Not every language has a standard linter, and some languages have multiple linters that are popular to use.

This guide is meant to get you started with linting, from "how to install" to "how to use" linters. It contains examples for both interactive CLI and automated CI/CD-focused workflows in Python, bash, PowerShell, Ansible, Packer, Terraform, with more to be added over time.

Additional Resources

The following resources will be useful if you're getting started with linting or CI/CD.

Python

Virtual Environments

In most cases you'll want to use a venv (virtual environment) or pipx, which achieves the same.

  • Use virtual environments for programming projects, or installing libraries
  • Use pipx when you're installing a python package that will run as a program would from the CLI
  • You can use either to version-pin python packages, or have multiple versions of a package installed

See ansible-lint below for an example.

pylint

Install via pip, in a venv, or possibly with pipx:

python3 -m pip install pylint

Run in cwd, recursively:

pylint .

To configure, you can auto-generate an INI file, and save it in the project root as .pylintrc:

pylint --disable=bare-except,invalid-name --class-rgx='[A-Z][a-z]+' --generate-rcfile | tee .pylintrc >/dev/null

flake8

Install via pip, in a venv, or possibly with pipx:

python3 -m pip install flake8

Run in cwd, recursively:

flake8 .

To configure, create an INI file in the project root titled .flake8:

[flake8]
extend-ignore = E203
exclude =
    .git,
    __pycache__,
    docs/source/conf.py,
    old,
    build,
    dist
max-complexity = 10

mypy

Install via pip, in a venv, or possibly with pipx:

python3 -m pip install mypy

Run with:

mypy program.py

Bash

shellcheck

sudo apt update
sudo apt install -y shellcheck

shellcheck ./some-script.sh

The most common way to tune shellcheck output is with exclusions directly within each script.

#!/bin/bash

# shellcheck disable=SC2059
some_function() {
    <SNIP>

You could also create a ~/.shellcheckrc with the same directives.

# ~/.shellcheckrc
disable=SC2059,SC2034 # Disable individual error codes
disable=SC1090-SC1100 # Disable a range of error codes

PowerShell

PSScriptAnalyzer

This section quotes the install docs directly.

Supported PowerShell Versions and Platforms

  • Windows PowerShell 5.1 or greater

  • PowerShell 7.2.11 or greater on Windows/Linux/macOS

Install using PowerShellGet 2.x:

Install-Module -Name PSScriptAnalyzer -Force

Install using PSResourceGet 1.x:

Install-PSResource -Name PSScriptAnalyzer -Reinstall

The Force or Reinstall parameters are only necessary when you have an older version of

PSScriptAnalyzer installed. These parameters also work even when you don't have a previous version

installed.

To lint PowerShell code, you can use various built-in presets via the -Settings <Preset> option:

Invoke-ScriptAnalyzer -Path /path/to/module/ -Settings PSGallery -Recurse

Ansible

ansible-lint

For guidance on writing Ansible code, reference the Ansible Lint Documentation.

ansible-lint can be used on your playbooks, roles, or collections to check for common mistakes when writing Ansible code.

There are a number of ways to do this, but you can install ansible-lint just like ansible.

With pipx:

pipx install ansible-lint

With pipx, using a specific version:

version_number="1.2.3"
package_name='ansible-lint'
pipx install --suffix=_"$version_number" "$package_name"=="$version_number"

With pip:

python3 -m pip install --user ansible-lint

The "new" way to do this, if you also intend to leverage the latest GitHub action in your CI/CD pipeline, is to use a configuration file to specify what ansible-lint should be checking. ansible-lint will look in the current directory, and then ascend directories, until getting to the git project root, looking for one of the following filenames:

  • .ansible-lint, this file lives in the project root

  • .config/ansible-lint.yml, this file exists within a .config folder

  • .config/ansible-lint.yaml, same as the previous file

NOTE: When using the .config/ path, any paths specified in the ansible-lint.yml config file must have ../ prepended so ansible-lint can find them correctly.

The easiest way to start, is with a profile, and excluding the meta/ and tests/ paths in roles. This is a less verbose version of the .ansible-lint file used in this repo.

# .ansible-lint

# Full list of configuration options:
# https://ansible.readthedocs.io/projects/lint/configuring/

# Profiles: null, min, basic, moderate, safety, shared, production
# From left to right, the requirements to pass the profile checks become more strict.
# Safety is a good starting point.
profile: safety

# Shell globs are supported for exclude_paths:
# - https://github.com/ansible/ansible-lint/pull/1425
# - https://github.com/ansible/ansible-lint/discussions/1424
exclude_paths:
  - .cache/      # implicit unless exclude_paths is defined in config
  - .git/        # always ignore
  - .github/     # always ignore
  - "*/tests/"   # ignore tests/ folder for all roles
  - "*/meta/"    # ignore meta/ folder for all roles

# These are checks that may often cause errors / failures.
# If you need to make exceptions for any check, add it here.
warn_list:
  - yaml[line-length]

# Offline mode disables installation of requirements.yml and schema refreshing
offline: true

Over time you may want to shift the profile to shared or production, and also tell ansible-lint to check the tests/ and meta/ paths for each role if you intend to publish them to ansible-galaxy.

GItHub Actions

This is an example file that runs ansible-lint on your GitHub repo when a new commit or PR is made to the "main" branch.

# .github/workflows/ansible-lint.yml

# Taken from the following example:
# https://github.com/ansible/ansible-lint?tab=readme-ov-file#using-ansible-lint-as-a-github-action

name: ansible-lint
on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]
jobs:
  build:
    name: Ansible Lint # Naming the build is important to use it as a status check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run ansible-lint
        uses: ansible/ansible-lint@main # or version tag instead of 'main'

Errors

Older versions of ansible-lint may produces errors that are difficult to diagnose. When this happens, use a very simple main.yml file, and start slowly adding tasks or vars to this file. Once you identify a task that creates an error, you can begin narrowing down which line(s) in the task or vars are producing the error.

One example of this is new versions of Ansible lint will want you to use become_method: ansible.builtin.sudo, while older versions require become_method: sudo and will generate a schema[tasks] error in this case.

Packer

Install

This downloads the HashiCorp GPG key, adds the repo information, and installs packer on Debian/Ubuntu.

curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install packer

For reference, this is the current HashiCorp GPG key data:

pub   rsa4096/0xAA16FCBCA621E701 2023-01-10 [SC] [expires: 2028-01-09]
      Key fingerprint = 798A EC65 4E5C 1542 8C8E  42EE AA16 FCBC A621 E701
uid                             HashiCorp Security (HashiCorp Package Signing) <security+packaging@hashicorp.com>
sub   rsa4096/0x706E668369C085E9 2023-01-10 [S] [expires: 2028-01-09]

Essential Commands

Initialize a packer template. This downloads any missing plugins described in the packer {} block to your local machine.

Malicious Plugins

Review plugins described in the packer {} block of any template before executing this.

cd /path/to/my-packer-project
packer init .

Format the packer templates. Rewrites HCL2 config files to canonical format. Run this after making edits to your templates or as part of CI/CD.

packer fmt .

Validate the Packer templates. Run this after making edits to your templates or as part of CI/CD.

packer validate .

GitHub Actions

This is an example file that runs packer code validation on your GitHub repo when a new commit or PR is made to the "main" branch.

# .github/workflows/packer.yml

# Taken from:
# https://github.com/hashicorp/setup-packer

name: packer

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

env:
  PRODUCT_VERSION: "latest"

jobs:
  packer:
    runs-on: ubuntu-latest
    name: Run Packer
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup `packer`
        uses: hashicorp/setup-packer@main
        id: setup
        with:
          version: ${{ env.PRODUCT_VERSION }}

      - name: Run `packer init`
        id: init
        run: |
          packer init .

      - name: Run `packer validate`
        id: validate
        run: |
          packer validate .

Terraform

Install

This downloads the HashiCorp GPG key, adds the repo information, and installs terraform on Debian/Ubuntu.

wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

For reference, this is the current HashiCorp GPG key data:

pub   rsa4096/0xAA16FCBCA621E701 2023-01-10 [SC] [expires: 2028-01-09]
      Key fingerprint = 798A EC65 4E5C 1542 8C8E  42EE AA16 FCBC A621 E701
uid                             HashiCorp Security (HashiCorp Package Signing) <security+packaging@hashicorp.com>
sub   rsa4096/0x706E668369C085E9 2023-01-10 [S] [expires: 2028-01-09]

Essential Commands

Initialize a working directory for validation without accessing any configured backend, use:

terraform init -backend=false

Format the Terraform templates. Rewrites Terraform config files to canonical format. Run this after making edits to your templates or as part of CI/CD.

terraform fmt

Validate the Terraform templates. Run this after making edits to your templates or as part of CI/CD.

terraform validate [options]