#! Ramblings
of an autodidact...
#! Ramblings

Bash script into a Makefile

Share Tweet Share

How to convert a bash script into a Makefile

Using make can literally make your life easier

So the other day on the Pybites slack channel, Erik O'Shaughnessy and I were chatting about something and I happened to mention that I had written a bash script to generate documents from Asciidoc files but wanted to create a Makefile to do the same instead. Without hesitation, he asked for my bash script and got to work! I don't think that I could have found a better tutorial on the subject if I had looked. I've looked things up before and nothing is as succinct as what he wrote up for me.

This is all his work and even though I made some changes, the credit for it all goes to him. I thought it was so great, that I had to share it with everyone because I believe that it can help others get up to speed with creating Makefiles.

What is a make and what is a Makefile

The GNU Make project's page describes it as such:

GNU Make is a tool which controls the generation of executables and other non-source files of a program from the program's source files.

It basically means that you can automate some common tasks with it. To illustrate what it can do, I am going to show you how Erik converted my bash script.

The bash script

Here is the script in all its glory... It's not much. It was just thrown together pretty quick and it was getting the job done.

gen_book.sh
#!/bin/bash
DOC='sop'
DATE=`date +%Y-%m-%d`
REV=`grep revnumber sop.asc | cut -d ' ' -f 2`
PARAMS="--attribute revnumber=$REV --attribute revdate=$DATE"

echo "Converting to HTML..."
bundle exec asciidoctor $PARAMS $DOC.asc
echo " -- HTML outpt at $DOC.html"

echo "Converting to ePub..."
bundle exec asciidoctor-epub3 $PARAMS $DOC.asc
echo " -- ePub output at $DOC.epub"

echo "Converting to Mobi (kf8)..."
bundle exec asciidoctor-epub3 $PARAMS -a ebook-format=kf8 $DOC.asc
echo " -- Mobi output at $DOC.mobi"

echo "Converting to PDF ... (This one takes a while)"
bundle exec asciidoctor-pdf $PARAMS $DOC.asc
echo " -- PDF output at $DOC.pdf"

Creating the Makefile

Variables

This is how you assign a string to a variable name in Make. The variable name doesn't have to be all caps and the equal doesn't have to be snugged up to the variable name; it's just how I write Makefiles

FILENAME= sop

Referencing variables

$(IDENTIFIER) is how you reference a variable in Make, you can use ${} too, but I prefer $(). If you forget the parentheses or the curly braces, eg $INDENTIFER, Make will interpret that as $I with NDENTFIER appended to it. Probably not what you are expecting.

SOURCE= $(FILENAME).asc

HTML= $(FILENAME).html
PDF= $(FILENAME).pdf
EPUB= $(FILENAME).epub
MOBI= $(FILENAME).mobi

Shell commands

$(shell shell command ) is how you invoke a command and save the results to a variable. It gets kinda tricky since every time you reference $(DATE) it will execute the command. The weird assignment operator := means just assign it one time.

DATE := $(shell date +%Y-%m-%d)

Again, calling shell. This time using AWK to pull out the revision number.

The $ needs to be doubled in the command string to keep Make from trying to expand $2 into something we didn't intend.

I know there was a grep|cut in the bash version, I prefer to use awk for these kinds of snip operations since it's a single process invocation. Those are easier to deal with in this context since you don't have to worry about any pipe weirdness imposed by Make.

REVISION := $(shell awk '/revnumber/ {print $$2}' $(SOURCE))

Functions / macros

Ok this is a "function" definition that we use to build the various ASCIIDOCTOR invocations. We could have just written the format specific definitions:

ADOC_HTML= bundle exec asciidoctor
ADOC_PDF= bundle exec asiidoctor-pdf
...

The advantage of this technique is you only have to change the BUNDLE_EXEC part if the way you invoke asciidoctor changes (I don't know why it would change, but the idea is to isolate stuff that's repeated so you don't have to freaking change it everywhere).

Macro or function definition

BUNDLE_EXEC= bundle exec $(1)

Using the macro

ASCIIDOCTOR_HTML= $(call BUNDLE_EXEC,asciidoctor)
ASCIIDOCTOR_PDF=  $(call BUNDLE_EXEC,asciidoctor-pdf)
ASCIIDOCTOR_EPUB= $(call BUNDLE_EXEC,asciidoctor-epub3)
ASCIIDOCTOR_MOBI= $(call BUNDLE_EXEC,asciidoctor-epub3)

Shared flags

Here we build the shared flags used by asciidoctor by all invocations. I use the += assignment to show how you can add to a variable after it's initial assignment.

ADOC_FLAGS= --attribute revnumber=$(REVISION)
ADOC_FLAGS+= --attribute revdate=$(DATE)

Phony rule

This next bit is some make magic. The .PHONY rule is how we tell make that some of our rules are not associated directly with a file.

The all rule below is by default a dependency of .PHONY

.PHONY: html pdf epub mobi

Default rule

The default rule that Make looks for when invoked as make is all. To build the all target, make builds, replace all with "a rule" name.

rulename: dep1 dep2 dep3 ... depN
command_0
@command_1
...
comand_n

dep1: subdep1 ...

Here, the dependencies for all are $(HTML), $(PDF), $(EPUB) and $(MOBI) which expand in to the names of the files that asciidoctor will create. So make all will run the HTML, PDF, EPUB and MOBI rules in that order.

all: $(HTML) $(PDF) $(EPUB) $(MOBI)

Rules

The $(HTML) rule depends on $(SOURCE), and only executes if the source file has changed or the destination file does not exist. $@ is an alias for the name of the rule to be used in the body of the recipe.

By default, make will print the command that is being executed to stdout followed by it's output. To suppress printing the command, preface the command with an @.

Lastly, the indention is a TAB and not 8 spaces. Make is an old-school tool and will complain if it doesn't get it's tabs.

$(HTML): $(SOURCE)
    @echo Converting $(SOURCE) to $@
    @$(ASCIIDOCTOR_HTML) $(ADOC_FLAGS) $(SOURCE)

$(PDF): $(SOURCE)
    @echo Converting $(SOURCE) to $@
    @$(ASCIIDOCTOR_PDF) $(ADOC_FLAGS) $(SOURCE)

$(EPUB): $(SOURCE)
    @echo Converting $(SOURCE) to $@
    @$(ASCIIDOCTOR_EPUB) $(ADOC_FLAGS) $(SOURCE)

$(MOBI): ADOC_FLAGS += -a ebook-format=kf8
$(MOBI): $(SOURCE)
    @echo Converting $(SOURCE) to $@
    @$(ASCIIDOCTOR_MOBI) $(ADOC_FLAGS) $(SOURCE)

NOTE: The MOBI asciidoctor command requires additional flags to generate the file. This is how you would make the changes to it.

At this point, you can add each of the PHONY rules. You might want to group these together along with the regular rules for keep related blocks of logic together, but I'll add them here for now.

html: $(HTML)
pdf: $(PDF)
epub: $(EPUB)
mobi: $(MOBI)

The debug rule

The debug rule is how I checked to make sure all of the variables I constructed contained the things I thought they should.

debug:
    @echo Rule -> $@
    @echo '          SOURCE: $(SOURCE)'
    @echo '        REVISION: $(REVISION)'
    @echo '            HTML: $(HTML)'
    @echo '             PDF: $(PDF)'
    @echo '            EPUB: $(EPUB)'
    @echo '            MOBI: $(MOBI)'
    @echo '       DOC_FLAGS: $(ADOC_FLAGS)'
    @echo 'ASCIIDOCTOR HTML: $(ASCIIDOCTOR_HTML)'
    @echo ' ASCIIDOCTOR PDF: $(ASCIIDOCTOR_PDF)'
    @echo 'ASCIIDOCTOR EPUB: $(ASCIIDOCTOR_EPUB)'
    @echo 'ASCIIDOCTOR MOBI: $(ASCIIDOCTOR_MOBI)'

The clean rule

Often times we want to restart from a known good "clean" state. A clean rule is a good place to remove transient files so you ensure that all your dependencies in your project are rebuilt. In this case we just remove the translated files. We could use wildcards in this rule like rm *.html but this can have unintended consequences if we have other files in HTML format that we didn't want to smoke.

Always be as explicit as possible in clean rules.

clean:
    @/bin/rm -f $(HTML) $(PDF) $(EPUB) $(MOBI)

The help rule

I've found it to be very useful for letting users know what commands are available and what they do. Mostly it's for me, I forget everything...

help:
    @echo 'Makefile for generating documents from Asciidoc source files              '
    @echo '                                                                          '
    @echo 'Usage:                                                                    '
    @echo '   make                                runs rules specified under all     '
    @echo '   make all                            generates all of the file formats  '
    @echo '   make clean                          remove the generated files         '
    @echo '   make debug                          prints all of the variables used   '
    @echo '   make epub                           (re)generates an epub file         '
    @echo '   make help                           prints this message                '
    @echo '   make html                           (re)generates an html file         '
    @echo '   make mobi                           (re)generates a mobi file          '
    @echo '   make pdf                            (re)generates a pdf file           '
    @echo '   make -n [epub, html, mobi, pdf]     prints out the commands it would   '
    @echo '                                       run without executing them         '
    @echo '                                                                          '

Conclusion

Make is a very versatile program and can be used for many more things. For instance Erik uses it to launch his kids Minecraft server! Now that I know how a Makefile is constructed, I will be able to automate other parts of my daily tasks!

NOTE: The MOBI format kept failing on me. It would leave behind a file with the extension -kf8.epub and never generate the .mobi one. My guess is that I need to install some kind of Amazon Kindle app program or something, so I removed it from the all rule to prevent it from generating by default.

This is the final file:

# Makefile

# Main project file name
FILENAME= pnc-sop

# Variables used
SOURCE= $(FILENAME).asc

HTML= $(FILENAME).html
PDF= $(FILENAME).pdf
EPUB= $(FILENAME).epub
MOBI= $(FILENAME).mobi

# Store the current date
DATE := $(shell date +%Y-%m-%d)

# Grab revision number from the source document
REVISION := $(shell awk '/revnumber/ {print $$2}' $(SOURCE))

# Macro or 'function' definition
BUNDLE_EXEC= bundle exec $(1)

# Assigning the macro
ASCIIDOCTOR_HTML= $(call BUNDLE_EXEC,asciidoctor)
ASCIIDOCTOR_PDF=  $(call BUNDLE_EXEC,asciidoctor-pdf)
ASCIIDOCTOR_EPUB= $(call BUNDLE_EXEC,asciidoctor-epub3)
ASCIIDOCTOR_MOBI= $(call BUNDLE_EXEC,asciidoctor-epub3)

# Shared flags
ADOC_FLAGS= --attribute revnumber=$(REVISION)
ADOC_FLAGS+= --attribute revdate=$(DATE)

# Enable phony rules
.PHONY: html pdf epub mobi

# Define the make commands
all: $(HTML) $(PDF) $(EPUB)

# Define each of the commands and specifying their outputs
$(HTML): $(SOURCE)
    @echo Converting $(SOURCE) to $@
    @$(ASCIIDOCTOR_HTML) $(ADOC_FLAGS) $(SOURCE)

html: $(HTML)

$(PDF): $(SOURCE)
    @echo Converting $(SOURCE) to $@
    @$(ASCIIDOCTOR_PDF) $(ADOC_FLAGS) $(SOURCE)

pdf: $(PDF)

$(EPUB): $(SOURCE)
    @echo Converting $(SOURCE) to $@
    @$(ASCIIDOCTOR_EPUB) $(ADOC_FLAGS) $(SOURCE)

epub: $(EPUB)

$(MOBI): ADOC_FLAGS += -a ebook-format=kf8
$(MOBI): $(SOURCE)
    @echo Converting $(SOURCE) to $@
    @$(ASCIIDOCTOR_MOBI) $(ADOC_FLAGS) $(SOURCE)

mobi: $(MOBI)

# Use debug rule to check that all of the variables were
# constructed properly.
debug:
    @echo Rule -> $@
    @echo '          SOURCE: $(SOURCE)'
    @echo '        REVISION: $(REVISION)'
    @echo '            HTML: $(HTML)'
    @echo '             PDF: $(PDF)'
    @echo '            EPUB: $(EPUB)'
    @echo '            MOBI: $(MOBI)'
    @echo '       DOC_FLAGS: $(ADOC_FLAGS)'
    @echo 'ASCIIDOCTOR HTML: $(ASCIIDOCTOR_HTML)'
    @echo ' ASCIIDOCTOR PDF: $(ASCIIDOCTOR_PDF)'
    @echo 'ASCIIDOCTOR MOBI: $(ASCIIDOCTOR_MOBI)'
    @echo 'ASCIIDOCTOR EPUB: $(ASCIIDOCTOR_EPUB)'

# Simple help menu showing what commands are available
# and what they do.
help:
    @echo 'Makefile for generating documents from Asciidoc source files              '
    @echo '                                                                          '
    @echo 'Usage:                                                                    '
    @echo '   make                                runs rules specified under all     '
    @echo '   make all                            generates all of the file formats  '
    @echo '   make clean                          remove the generated files         '
    @echo '   make debug                          prints all of the variables used   '
    @echo '   make epub                           (re)generates an epub file         '
    @echo '   make help                           prints this message                '
    @echo '   make html                           (re)generates an html file         '
    @echo '   make mobi                           (re)generates a mobi file          '
    @echo '   make pdf                            (re)generates a pdf file           '
    @echo '   make -n [epub, html, mobi, pdf]     prints out the commands it would   '
    @echo '                                       run without executing them         '
    @echo '                                                                          '

# Specify clean-up rules.
clean:
    @/bin/rm -f $(HTML) $(PDF) $(EPUB) $(MOBI) $(FILENAME)-kf8.epub

Thanks again Erik! Check out his commented Makefile for further insights!


Receive Updates

ATOM