# Multiple, optional linters for a single filetype

**URL:** <https://discuss.kakoune.com/t/multiple-optional-linters-for-a-single-filetype/2541>\
**Category:** Recipes & Guides\
**Created:** [September 11, 2024, 3:31pm UTC](https://discuss.kakoune.com/t/multiple-optional-linters-for-a-single-filetype/2541 "2024-09-11T15:31:05Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![Screwtapello](https://discuss.kakoune.com/user_avatar/discuss.kakoune.com/screwtapello/32/12_2.png) [@Screwtapello](https://discuss.kakoune.com/u/Screwtapello)\
**Post date:** [September 11, 2024, 3:31pm UTC](https://discuss.kakoune.com/t/multiple-optional-linters-for-a-single-filetype/2541/1 "2024-09-11T15:31:05Z")

</div>

Python has a number of linters and static analysis tools available, some of them are meta-tools that run multiple different kinds of checks over your code at once. Unfortunately the check that the meta-tools always seem to leave out is static type checking, using a tool like [mypy](https://mypy.readthedocs.io/). And so I want to run two linters on my Python code, `mypy` for type-checking and `flake8` for everything else.

Kakoune has only one `lintcmd` option, which contains a shell-command that will be given the path to a file to check. That means we can run multiple linters by using a shell function to run both of them:

```auto
set-option window lintcmd %{ runlints() {
        mypy --show-column-numbers "$1"
        flake8 --max-complexity 10 "$1"
}; runlints }

```

The `lintcmd` will be given to the shell with a filename added on; this causes the `runlints` function to be defined, and then immediately called with the filename. Inside the function, each linter is called with `$1`, the filename that was passed to the `runlints` function.

But that’s not enough. I use my Kakoune config on different systems, and they don’t always have the same tools available. I’d like to gracefully ignore tools that don’t exist, perhaps with a warning if there’s no linters available at all.

This is what I came up with:

```auto
hook global WinSetOption filetype=python %{
    evaluate-commands %sh{
        # Clear existing arguments
        # so we can use it as an array.
        set --

        # Add these linters to the array, if we have them.
        for linter in \
                "mypy --strict --show-column-numbers" \
                "flake8 --max-complexity 10"
        do
            # The first whitespace-delimited word of the linter
            tool_name=${linter%% *}
            # If we have that tool...
            if command -V "$tool_name" >/dev/null 2>/dev/null; then
                # ...add it to the list
                set -- "$@" "$linter"
            fi
        done

        # If we found any linters...
        if [$# -gt 0]; then
            # ...add them all to the lintcmd option,
            # using a shell function to run each linter sequentially
            # on the given file.
            echo "set-option window lintcmd %{ l() {"
            printf '%s "$1"\n' "$@"
            echo "}; l }"

            # ...and automatically lint the buffer after writing it.
            echo hook window -group python-auto-lint BufWritePost '.*' lint-buffer
            echo hook window WinSetOption filetype=.* %{ rmhooks window python-auto-lint; unset-option window lintcmd }
        else
            # Otherwise, put a warning in the *debug* buffer
            echo "Could not find any supported linters" 1>&2
        fi
    }
}

```
