Have you heard the good news about just?
Because of my current cancer treatment I'm on medical leave from work. I spend a LOT of my day resting, or doing very non-active things ... like watching TV Shows. I'm currently binging Star Trek: Strange New Worlds and it's really good.
In order to help stave off brain rot I decided that I would spend some time learning about various technologies that I use or would like to use, and finding out more about them. In this week's installment, I'm taking a look at just, a command runner that is so good.
justfile¶
I've been using justfiles in my projects for a while now thanks to Jeff Triplett, but there's just so much to them. I read through the docs this week and found 19 things that look useful to incorporate into my just files.
1. default-list¶
When you run just in your terminal it will run the first recipe listed in your file. One way I had overcome this was by adding the following recipe:
@_default:
just --list
This would cause just to list all of the commands available (with comments if they exist)
However, there's a different way to do this. You can add
set default-list := true
at the top of the justfile and get the same behavior.
2. Variadic parameters¶
One of the recipes I rely on frequently is
# Bring up Docker containers
[group('docker')]
@up *ARGS:
docker compose -f docker-compose.dev.yml up {{ ARGS }}
The *ARGS is what's known as a variadic parameter. These can accept a (potentially) infinite number of parameters passed to them. You can either use + to indicate 1 or more parameters, or a * to indicate 0 or more parameters.
The recipe above allows the following invocations
# Bring the Docker container up in the foreground
just up
# Bring the Docker container up in detached mode
just up -d
# Build the Docker container and then bring the Docker container up in the detached mode
just up --build -d
The new thing I learned is that they can also accept defaults. This means that I could update the recipe to be
# Bring up Docker containers
[group('docker')]
@up *ARGS='-d':
docker compose -f docker-compose.dev.yml up {{ ARGS }}
This would cause
just up
and
just up -d
to behave the same.
I could also just update the recipe to be
# Bring up Docker containers
[group('docker')]
@up *ARGS:
docker compose -f docker-compose.dev.yml up -d {{ ARGS }}
I'm trying to figure out the best use case for this, but it did seem like an interesting concept to try out
3. Variadic parameters limits¶
You may want to limit the number of parameters you allow on a variadic parameter. Just allows you to control this by using [arg('ARGS', min='1', max='4')] like this
# Bring up Docker containers
[group('docker')]
[arg('ARGS', min='1', max='4')]
@up *ARGS:
docker compose -f docker-compose.dev.yml up {{ ARGS }}
In order for this to work, you do need to add the following to your justfile
set unstable
set lists
Quick Aside ... The docs give a better example using
scpwhich likely makes more sense, but I wanted to keep the same example going here
4. Parameter constraints¶
You can constrain parameters using [arg('ARGS', pattern='--help|--version')].
Per the docs
Regular expressions are provided by the Rust regex crate. See the syntax documentation for usage examples.
We again can update the just recipe from before to be
# Bring up Docker containers
[group('docker')]
[arg('ARGS', pattern='-d|--build')]
@up *ARGS:
docker compose -f docker-compose.dev.yml up {{ ARGS }}
Again, not helpful in this case necessarily, but there are applications with other recipes I'm sure
5. Usage information¶
Usage information for a recipe may be printed with the --usage subcommand. I have the following recipe that creates new posts for this site (please excuse the mix of make with just)
@post title category:
make newpost title="{{title}}" category="{{category}}"
If I run just --usage post I get this returned
Usage: just post title category
Arguments:
title
category
This can be helpful when you aren't sure what the arguments are for a just recipe (if they exist at all!)
6. Run recipes at the end¶
You can have recipes run at the end of a recipe, not just the beginning. Typically when running a just recipe any dependencies will run before the invoked recipe.
For example, assume you have these recipes:
test: build
run tests
build:
build project
If you run just test it will first run the build recipe and then the test recipe.
Let's assume you also have a deployment recipe that you want to run AFTER the build recipe runs successfully. You could have the following justfile:
test: build && deploy
run tests
build:
build project
deploy:
deploy project
Running the command just test would do the following
- Run the
buildrecipe - Run the
testrecipe - Run the
deployrecipe
See the docs for more examples
7. Parallel Tasks¶
You can run parallel tasks with the [parallel] invocation above a recipe. This could be helpful if you have a project with a front end and a back end and you want to run the tests at the same time:
[parallel]
test: fe be
fe:
test front-end-project
be:
test back-end-project
See the docs for more details
8. Recipe documentation¶
Who doesn't love good documentation, right? just allows for a few options for docs
- The
#symbol - The
[doc('Your Comment Here')]attribute
Why choose one over the other? The # only allows for single line documentation, while the [doc()] attribute allows for multi-line documentation.
One thing to note on multi-line documentation is that it can look a bit off
This just recipe
[group('docker')]
[doc('
Bring up Docker containers
A second line
and a third
')]
@up *ARGS:
docker compose -f docker-compose.dev.yml up {{ ARGS }}
will generate this documentation
# Bring up Docker containers
# A second line
# and a third
up *ARGS
See the docs for more details
9. Groups¶
In the previous example there was [group('docker')]. This attribute allows you to group recipes together. For a really good example, see the Django Packages justfile
Running just will return
Available recipes:
[database]
pg_dump file='db.dump' # Dump database to file
pg_restore file='db.dump' # Restore database from dump file
[django]
collectstatic *ARGS="--no-input" # Collect static files
createsuperuser USERNAME EMAIL # Create a Django superuser
cron # Run scheduled data import tasks
run ARGS # Run a Django management command
shell *ARGS # Run Django shell
[docker]
build *ARGS # Builds the Docker Images with optional arguments
cibuild # Build Docker images for CI
console # Drop into a bash shell in the Django container
down *ARGS # Bring down Docker containers
logs *ARGS # View output from running containers
remove # Remove all application services and volumes
restart *ARGS # Restart all services
start *ARGS="--detach" # Start all services
status # Show status of running containers
stop # Stop all services
tail # Tail service logs
up *ARGS # Start Docker containers
[docs]
docs-build *ARGS # Build the documentation to docs/site
docs-down *ARGS # Stop documentation services
docs-up *ARGS="--detach" # Start documentation services
[linting]
lint *ARGS # Run all prek linting and code quality checks
perflint # Check for performance anti-patterns
[production]
purge_cache # Purge Cloudflare cache
[testing]
coverage *ARGS # Run tests with pytest and generate coverage reports
pytest *ARGS # Run tests with pytest
test *ARGS # Run tests using Django test runner
[utils]
bootstrap *ARGS # Perform initial setup: create venv, .env.local, and build Docker images
bump *ARGS # Update the version; Used before release to production
fmt # Format the justfile
lock *ARGS # Compile Python dependencies with hashes [alias: pip-compile]
setup # Alias for bootstrap - perform initial setup
upgrade # Upgrade existing Python dependencies to their latest versions
You can list the groups by running just --groups. However, in order to list the recipes in a specific group you need to run just --list --group <group-name>, for example, just --list --group docker.
It would be nice if running just --group docker would work, but as of just 1.59.0 it generates an error
error: the following required arguments were not provided:
--list [<MODULE>...]
Usage: just --list [<MODULE>...] --group <GROUP> [ARGUMENTS]...
For more information, try '--help'.
10. Private Recipes¶
There is a concept of private recipes by using the [private] attribute. The main use is helper recipes that exist only to be dependencies or building blocks.
For example, in the Django Packages justfile the recipe
# Format the justfile
[group('utils')]
@fmt:
just --fmt --unstable
could be changed to
# Format the justfile
[private]
[group('utils')]
@fmt:
just --fmt --unstable
This would cause the fmt recipe to not be listed when showing all of the recipes.
11. Shell expanded strings¶
Per the docs
Strings prefixed with
xare shell expanded (as of version 1.27.0)
One interesting thing that can be done here is that you can keep common recipes in one place and pull them into every project’s justfile like this:
import x'~/.config/just/common.just'
More on common justfiles a little later
12. Set your shell¶
You can set the shell that just will use. This can be especially helpful on Windows where you are running Powershell instead of WSL.
On Windows you can set it like this:
[windows]
set shell := ["powershell.exe", "-NoLogo", "-Command"]
This can make it much easier to have a single justfile that can be used on Windows, macOS, or Linux.
See more at the docs
13. Working Directory¶
You can set the working directory for an entire justfile or a single recipe. I'm still trying to figure out the best use case for this, but one thing that might be helpful is that if you have a sub-directory called scripts that has all of your scripts and you don't want to have every just recipe that invokes a script to look like this:
simple:
./scripts/simple.sh
complicated:
./scripts/complicated.sh
You could add set working-directory := 'scripts' like this
set working-directory := 'scripts'
simple:
./simple.sh
complicated:
./complicated.sh
Again, I'm still trying to figure out a good use case for this, but it was interesting enough to catch my eye
See docs for more details
14. Importing other justfiles¶
If you need/want to separate your various just recipes into separate files you can do that. Assume that you have a justfile and several sub-justfiles that look something like this:
.
├── be.just
├── fe.just
├── docker.just
└── justfile
The be.just might look like this:
[doc('Used to run the back end tests')]
[group('backend')]
@be-test:
echo "run back end tests"
[doc('Used to run the back end build')]
[group('backend')]
@be-build: be-test
echo "run back end build"
the fe.just might look like this
[doc('Used to run the front end tests')]
[group('frontend')]
@fe-test:
echo "run front end tests"
[doc('Used to run the front end build')]
[group('frontend')]
@fe-build: fe-test
echo "run front end build"
the docker.just might look like this:
[doc('Bring Docker Containers up')]
[group('docker')]
up *ARGS:
docker compose up {{ARGS}}
[doc('Bring Docker Containers down')]
[group('docker')]
down:
docker compose down
We can then have a justfile that looks like this:
import 'be.just'
import 'fe.just'
import 'docker.just'
set default-list := true
We can set whatever other settings in the justfile we want. This allows us to have a separation of concerns for our just recipes.
See the docs for more details. You can also pull shared recipes in from a common file — see tip 11.
15. set fallback¶
When you add set fallback you can traverse the directory up to a justfile with the recipe you're trying to find. For example, if you have a justfile in your home directory that looks like this:
@default:
just --list
[no-cd]
pwd:
pwd
[no-cd]
gist file:
gh api gists -F 'files[{{ file }}][content]=@{{ file }}'
@usage time="daily":
npx ccusage@latest {{ time }} --breakdown
[no-cd]
@gh profile:
uvx gh-profiler {{ profile }}
@glm:
uvx glm-launch launch claude
but you're in the directory ~/code/ryancheley.com that has a justfile that looks like this:
set default-list := true
@html:
make html
@clean:
make clean
@regenerate:
make regenerate
@serve:
pelican --listen --autoreload
@publish:
make publish
@vercel: html
rsync -av pelican.db metadata.json search.ryancheley@do-web-p-003:~
@toot:
make toot
@post title category:
make newpost title="{{title}}" category="{{category}}"
@micro title:
make newpost title="{{title}}" category="microblog"
@sync:
make sync
@lock:
make lock
@check:
pre-commit run --all-files
@codespell:
codespell content -i 3 --ignore-words=ignore-words.txt
# Generate resume and cover letter PDFs from the Markdown in resume-resources/
@resume:
#!/usr/bin/env bash
set -euo pipefail
cd resume-resources
header=$(mktemp)
printf '\\AtBeginDocument{\\raggedright}\n' > "$header"
pandoc resume.md -o "Ryan Cheley Resume.pdf" --pdf-engine=xelatex -V geometry:margin=1in -V linkcolor:blue -H "$header"
pandoc cover-letter.md -o "Ryan Cheley Cover Letter.pdf" --pdf-engine=xelatex -V geometry:margin=1in -V linkcolor:blue -H "$header"
rm -f "$header"
# Bring up Docker containers
[group('docker')]
@up *ARGS:
docker compose -f docker-compose.dev.yml up {{ ARGS }}
# Bring down Docker containers
[group('docker')]
@down *ARGS:
docker compose down {{ ARGS }}
# Builds the Docker Images with optional arguments
[group('docker')]
@build *ARGS:
docker compose {{ ARGS }} build
If you wanted to run the usage recipe you'd get an error like this:
error: justfile does not contain recipe `usage`
However, if you add set fallback to the top of the justfile in ~/code/ryancheley.com then the recipe will run without issue. For example:
set default-list := true
set fallback
@html:
make html
...
See docs for more details
16. Remote justfile¶
Let's say you really like the Django Packages justfile and you wanted to include it in your justfile. You could add the following to the top of your just file
import? 'djangopackages.just'
set allow-duplicate-recipes
The first line optionally imports a file called djangopackages.just. The second line isn't strictly necessary, but in the event you have a recipe with the same name as one used in djangopackages.just this allows you to not get an error. One caveat is that the last version of the recipe is what will be used. In this example, since the import? 'djangopackages.just' appears at the top, the lock recipe will be the one from the justfile and not from djangopackages.just
You can then add this recipe to your justfile:
fetch:
curl https://raw.githubusercontent.com/djangopackages/djangopackages/refs/heads/main/justfile > djangopackages.just
In order to have the djangopackages.just file you'll need to run the just fetch recipe.
17. The --choose flag¶
The --choose flag will list out recipes (with some exceptions) so you can select them; good for learning about the recipes available. In order to use this you'll need to install fzf. This can be installed with brew install fzf on macOS.
One caveat here is that it won't list out recipes that have arguments passed to them. Additionally private recipes (item 10) and aliases are skipped.
See the docs here.
18. Confirm attribute¶
Let's say you have a super destructive just recipe like this:
delete-all:
rm -rf *
When you run just delete-all it will run the command without prompting. You can prevent that behavior by adding the [confirm] attribute to the recipe
[confirm]
delete-all:
rm -rf *
So when you run just delete-all instead of nuking all of the files, you'll get prompted to confirm that decision
Run recipe `delete-all`?
N if you invoked the recipe by mistake.
See the docs for more details.
19. Format your justfile¶
A simple way to check that your justfile is formatted correctly is to run just --fmt --check
You can then run just --fmt to format it correctly.
See the docs for more details.
Using justpath to go on a pyrrhic adventure to clean up my PATH
A while ago I heard about a project called justpath from Jeff Tripplet on Mastodon. It seemed like a neat project to try and clean up my path and I figured, what the heck, let me give it a try.
I installed it and when I ran it for the first time, the output looked like this
1 /Users/ryan/.cargo/bin
2 /usr/local/opt/ruby/bin (resolves to /usr/local/Cellar/ruby/3.2.2_1/bin)
3 /Users/ryan/google-cloud-sdk/bin
4 /Users/ryan/.pyenv/shims
5 /opt/homebrew/Cellar/pyenv-virtualenv/1.2.3/shims
6 /opt/homebrew/bin
7 /opt/homebrew/sbin
8 /Library/Frameworks/Python.framework/Versions/3.12/bin
9 /usr/local/bin (duplicates: 3)
10 /usr/local/sbin
11 /Library/Frameworks/Python.framework/Versions/3.11/bin
12 /Library/Frameworks/Python.framework/Versions/3.10/bin
13 /usr/local/bin (duplicates: 3)
14 /System/Cryptexes/App/usr/bin (resolves to /System/Volumes/Preboot/Cryptexes/App/usr/bin)
15 /usr/bin
16 /bin
17 /usr/sbin
18 /sbin
19 /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin (resolves to /private/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin, directory does not exist)
20 /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin (resolves to /private/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin, directory does not exist)
21 /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin (resolves to /private/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin, directory does not exist)
22 /opt/X11/bin
23 /Library/Apple/usr/bin
24 /Library/TeX/texbin (resolves to /usr/local/texlive/2024basic/bin/universal-darwin)
25 /usr/local/share/dotnet
26 ~/.dotnet/tools (directory does not exist)
27 /Library/Frameworks/Mono.framework/Versions/Current/Commands (resolves to /Library/Frameworks/Mono.framework/Versions/6.12.0/Commands)
28 /Applications/Postgres.app/Contents/Versions/latest/bin (resolves to /Applications/Postgres.app/Contents/Versions/14/bin)
29 /Applications/Xamarin Workbooks.app/Contents/SharedSupport/path-bin (directory does not exist)
30 /Users/ryan/.local/bin (duplicates: 2)
31 /usr/local/bin (duplicates: 3)
32 /Users/ryan/.local/bin (duplicates: 2)
That's a lot to look at, but helpfully there are a few flags to get only the 'bad' items
justpath --invalid
justpath --duplicates
Running justpath --invalid got this
18 /var/run/com.apple.security.cryptexd/codex.system/bootstrap/ usr/local/bin (resolves to /private/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin, directory does not exist)
19 /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin (resolves to /private/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin, directory does not exist)
20 /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin (resolves to /private/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin, directory does not exist)
26 ~/.dotnet/tools (directory does not exist)
29 /Applications/Xamarin Workbooks.app/Contents/SharedSupport/path-bin (directory does not exist)
and running justpath --duplicates got this
8 /usr/local/bin (duplicates: 3)
12 /usr/local/bin (duplicates: 3)
30 /Users/ryan/.local/bin (duplicates: 2)
31 /usr/local/bin (duplicates: 3)
32 /Users/ryan/.local/bin (duplicates: 2)
Great! Now I know what is invalid and what is duplicated. Surely justpath will have a command to clean this up, right?

Turns out not so much, and for good reasons:
justpathdoes not alter the path itself, it provides the corrected version that a user can apply further. A child process the one that Python is running in cannot alter the parent environment PATH. As for reading the PATH -justpathrelies on what is available fromos.environ['PATH'].
So I posted on Mastodon to see how others may have approached this and Jeff replied back
I ran the tool and copied the output before starting from an empty PATH.
Then I ran justpath and added them back one at a time.
mise helped me cut at least half a dozen weird/duplicate path statements alone. I had quite a bit of tool overlap.
I poked around with trying to find where the invalid values were coming from and found this answer
Cryptexes are used to update parts of macOS quickly, without requiring a full rebuild of the SSV (see this answer for details). I don't know whether removing these paths will break the installation of such cryptexes. But if you want to take the risk, you can remove
/etc/paths.d/10-cryptex(or move it to a safe place in case you need it later on). PS: Invalid entries in PATH don't really hurt, they primarily slow down (a very little bit) the lookup of new commands run from the shell the first time.
This supported my assumption that the invalid PATH variables were likely due to macOS upgrades (I guessed that based on the existence of invalid PATH variables on other Macs in my house that didn't have a programmer / developer using them)
So, with that I found 2 files that were in /etc/paths.d and moved them to my desktop. Once that was done I restarted my terminal and got this output
1 /Users/ryan/.cargo/bin
2 /usr/local/opt/ruby/bin (resolves to /usr/local/Cellar/ruby/3.2.2_1/bin)
3 /Users/ryan/google-cloud-sdk/bin
4 /Users/ryan/.pyenv/shims
5 /opt/homebrew/Cellar/pyenv-virtualenv/1.2.3/shims
6 /opt/homebrew/bin
7 /opt/homebrew/sbin
8 /Library/Frameworks/Python.framework/Versions/3.12/bin
9 /usr/local/bin (duplicates: 3)
10 /usr/local/sbin
11 /Library/Frameworks/Python.framework/Versions/3.11/bin
12 /Library/Frameworks/Python.framework/Versions/3.10/bin
13 /usr/local/bin (duplicates: 3)
14 /System/Cryptexes/App/usr/bin (resolves to /System/Volumes/Preboot/Cryptexes/App/usr/bin)
15 /usr/bin
16 /bin
17 /usr/sbin
18 /sbin
19 /opt/X11/bin
20 /Library/Apple/usr/bin
21 /Library/TeX/texbin (resolves to /usr/local/texlive/2024basic/bin/universal-darwin)
22 /usr/local/share/dotnet
23 /Library/Frameworks/Mono.framework/Versions/Current/Commands (resolves to /Library/Frameworks/Mono.framework/Versions/6.12.0/Commands)
24 /Applications/Postgres.app/Contents/Versions/latest/bin (resolves to /Applications/Postgres.app/Contents/Versions/14/bin)
25 /Users/ryan/.local/bin (duplicates: 2)
26 /usr/local/bin (duplicates: 3)
27 /Users/ryan/.local/bin (duplicates: 2)
This just left the duplicates that were being added. There are a few spots where applications (or people!) will add items to PATH and there are MANY opinions on which one is The Right Way TM. I use zshell and looked in each of these files (~/.profile, ~/.zshrc, ~/.zprofile) and found that only my .zshrc file contained the addition to PATH for the duplicates I was seeing
/usr/local/bin/Users/ryan/.local/bin
With that, I simply commented them out of my .zshrc, restarted my terminal and now I am down to only two duplicates duplicate
8 /usr/local/bin (duplicates: 2)
12 /usr/local/bin (duplicates: 2)
I looked everywhere trying to find where this duplicate was coming from. I tried a few variations of find
find . -type f -name '.*' 2>/dev/null -exec grep -H '/usr/local/bin' {} \;
But the results were only the commented out lines from .zshrc.
I did find this issue on the Homebrew repo and thought I might be onto something, but it was for a different path so that doesn't seem to be the culprit.
I eventually gave up on trying to find the one true source of the duplicate entry (though I suspect that there is something adding it in the same way that Homebrew is can add a duplicate PATH variable) because I found this command
typeset -U path
I added it to my .zshrc, restarted my terminal and voila, a clean PATH
1 /Users/ryan/.cargo/bin
2 /Users/ryan/google-cloud-sdk/bin
3 /Users/ryan/.pyenv/shims
4 /opt/homebrew/Cellar/pyenv-virtualenv/1.2.3/shims
5 /opt/homebrew/bin
6 /opt/homebrew/sbin
7 /Library/Frameworks/Python.framework/Versions/3.12/bin
8 /usr/local/bin
9 /usr/local/sbin
10 /Library/Frameworks/Python.framework/Versions/3.11/bin
11 /Library/Frameworks/Python.framework/Versions/3.10/bin
12 /System/Cryptexes/App/usr/bin (resolves to /System/Volumes/Preboot/Cryptexes/App/usr/bin)
13 /usr/bin
14 /bin
15 /usr/sbin
16 /sbin
17 /opt/X11/bin
18 /Library/Apple/usr/bin
19 /Library/TeX/texbin (resolves to /usr/local/texlive/2024basic/bin/universal-darwin)
20 /usr/local/share/dotnet
21 /Library/Frameworks/Mono.framework/Versions/Current/Commands (resolves to /Library/Frameworks/Mono.framework/Versions/6.12.0/Commands)
22 /Applications/Postgres.app/Contents/Versions/latest/bin (resolves to /Applications/Postgres.app/Contents/Versions/16/
I still want to try and figure out where that extra /usr/local/bin is coming from, but for now, I have been able to clean up my PATH.
So, the question is, does this really matter? In the answer I found the last statement really caught my attention
PS: Invalid entries in PATH don't really hurt, they primarily slow down (a very little bit) the lookup of new commands run from the shell the first time.
So the answer is, probably not, BUT just knowing that I had duplicates and invalid entries in my PATH was like a splinter in my brain 1 that needed to be excised.
What are your experiences with your PATH? Have you gone to these lengths to clean it up, figured, "Meh, it's not hurting anything, why bother?"
- Yes, that is a Matrix reference ↩︎
djhtml and justfile
I had read about a project called djhtml and wanted to use it on one of my projects. The documentation is really good for adding it to precommit-ci, but I wasn't sure what I needed to do to just run it on the command line.
It took a bit of googling, but I was finally able to get the right incantation of commands to be able to get it to run on my templates:
djhtml -i $(find templates -name '*.html' -print)
But of course because I have the memory of a goldfish and this is more than 3 commands to try to remember to string together, instead of telling myself I would remember it, I simply added it to a just file and now have this recipe:
# applies djhtml linting to templates
djhtml:
djhtml -i $(find templates -name '*.html' -print)
This means that I can now run just djhtml and I can apply djhtml's linting to my templates.
Pretty darn cool if you ask me. But then I got to thinking, I can make this a bit more general for 'linting' type activities. I include all of these in my precommit-ci, but I figured, what the heck, might as well have a just recipe for all of them!
So I refactored the recipe to be this:
# applies linting to project (black, djhtml, flake8)
lint:
djhtml -i $(find templates -name '*.html' -print)
black .
flake8 .
And now I can run all of these linting style libraries with a single command just lint