How to use for, find and wildcard in a Makefile?

Viewed 1459

I am trying to do the following without any success so far:

remove:
    for file in $(shell find src/ -name migrations -type d); do rm $(wildcard "$(file)/0*.py"); done
2 Answers

If I understood well, you want to remove all files named:

src/**/migrations/0*.py

where ** can be anything, including nothing at all. find can do this alone:

find src -path '*/migrations/0*.py' -delete

And as make recipes are just bare shell, you do not need $(shell... or anything like this:

remove:
    find src -path '*/migrations/0*.py' -delete

But before running this potentially dangerous rule, you should maybe use this one until you are satisfied:

remove:
    find src -path '*/migrations/0*.py' -ok rm {} \;

The only difference is that it will ask you confirmation before deleting files. We can also make all this a bit easier:

remove-safe: DELCMD := -ok rm {} \\;
remove-unsafe: DELCMD := -delete
remove-dry-run: DELCMD := -print

remove-safe remove-unsafe remove-dry-run:
    find src -path '*/migrations/0*.py' $(DELCMD)

Use one or the other goal depending of what you want:

  • remove-dry-run to print the list of files that would be deleted without deleting them,
  • remove-safe to delete the files with confirmation,
  • remove-unsafe to delete the files without confirmation.

EDIT 1: if this an exercise about loops and make, just remember that make recipes are shell scripts (one per line) that are first expanded by make and next passed to the shell. So:

  1. If you want to use shell variables, use them in one single line (but you can break lines with a trailing \). Else they will be from different shell invocations and it will not work as expected.
  2. If you use the $ sign in your recipe for shell expansion, double them to escape the first expansion by make.

In the following I assume that you do not have directory or file names with spaces or special characters. If you do, it is time to ask a question about your shell.

So, with the bash shell, for instance, we can first build an array of target directories and then loop over them to delete the files:

SHELL := bash

remove:
    dirs=( $$(find src -type d -name migrations) ); \
    for d in "$${dirs[@]}"; do \
        rm -f "$$d"/0*.py; \
    done

EDIT 2: you could also use the builtin make loop system with one (phony) clean target per directory.

DIRS := $(shell find src -type d -name migrations)

clean-targets := $(addprefix clean-,$(DIRS))

.PHONY: remove $(clean-targets)

remove: $(clean-targets)

$(clean-targets): clean-%:
    rm -f "$*"/0*.py

The tricky part is the:

$(clean-targets): clean-%:
    rm -f "$*"/0*.py

rule. It is equivalent to one rule per migrations directory that would be:

clean-DIR: clean-%:
    rm -f "$*"/0*.py

where DIR is the directory path. And these rules are static pattern rules. In the recipe the $* make automatic variable is expanded by make as the stem of the clean-% pattern. So, if DIR is src/foo/bar/migrations, the rule for it is equivalent to:

clean-src/foo/bar/migrations:
    rm -f src/foo/bar/migrations/0*.py

The main advantage of this style over the shell loop is that make can run all recipes in parallel. If you have hundreds of migrations directories and if you have 8 or 16 cores on your computer, it can really make a difference. Just try:

make -j1 remove
make -j16 remove

and you will see the difference.

The short answer is you can't do that.

It's critical when writing makefile recipes to be clear in your own understanding of how they work: first, make will expand all make variables and functions. Then, make will pass the resulting string to the shell to be run. Finally, make waits until the shell completes and examines the exit code to find out whether it succeeded or not.

Here you're trying to use a shell for loop where the body of the loop invokes a make wildcard function. This cannot work.

Consider what you are wanting make (and the shell) to do: the shell would run its for loop, then every time it executed the body of the loop it would communicate back to make the value of some shell variable and make would expand the make wildcard function using that variable, then return the results to the shell for more processing. This is simply not possible.

Best is to write the recipe using shell constructs. A good rule of thumb is that it's never a good idea to use make's shell function inside a recipe: the recipe is already being run in a shell, so you don't need the shell function at all. It's simply confusing. Similarly, you rarely need make's wildcard function in a recipe because the shell does file globbing for you automatically.

I would write this as:

remove:
        find src/ -name migrations -type d | while read -r dir; do rm "$$file"/0*.py; done

Note we have escaped $$file with two dollar signs to keep make from expanding it as a make variable.

Related