TL;DR The following script will export a given org file to HTML, using emacsclient if an Emacs daemon is running and fall back to using Emacs if no Emacs daemon is running. For more options including a one-liner, as alias or usage in GNU make keep on reading.
#!/bin/sh if emacsclient -a false -e 't'; then emacsclient \ -e '(progn (find-file "$1") (org-html-export-to-html))' else emacs --batch \ -l ~/.emacs.d/init.el \ -f org-html-export-to-html \ --kill "$1" fi
At 200ok we use Org mode a lot. And by "a lot", I mean basically for everything from writing (documentation, concepts, quotes, and blog posts like this one), to organizing work (budgeting, capacity planning, time tracking, ticketing, meeting minutes), even our ledgers are generated in org. Some of our processes are highly automated, and we built quite some tooling to facilitate automation. Most of it is not fit for open-sourcing, but you might have come across organice (a FLOSS implementation of Org mode without the dependency of Emacs, built for mobile and desktop browsers) or ukko (a versatile static site generator), which uses the pattern described in this article to render HTML pages from org sources.
Naturally, when sharing documents, like meeting minutes, quotes, and
invoices with our business partners, using org internally boils down
to exporting to HTML or PDF. From within Emacs' org-mode, this is done
with C-c C-e h h
or C-c C-e l p
, respectively. When we want to
automate this, we have to be able to trigger the export from the
command line. Using HTML as an example, this can be done with the
following command:
emacs --batch \ --load ~/.emacs.d/init.el \ --funcall org-html-export-to-html \ --kill "${ORG_FILE}"
Let's break the command down. --batch
will run Emacs in batch mode;
this means we have to provide the file to load with --load
, as well
as the function to call with --funcall
. We will load the user's
standard Emacs configuration to ensure we have the same setup as if we
exported interactively. In theory, the file loaded could be limited to
the required packages and configuration to save loading time, but this
optimization pales in comparison to the time saver I will soon lay
out. --kill
will ensure that Emacs exits after the call to our
function has returned.
This command takes some time, as it not only will have to start Emacs
but also load the users config, which might load a lot of packages.
The actual export is rather quick, even for larger org files. The
noticeable wait stems from starting Emacs and loading packages.
Therefore Emacs users who close and open Emacs regularly usually have
Emacs daemon in place. An instance of Emacs that runs as a daemon,
also known as Emacs server. Starting an Emacs daemon is as easy as
running emacs --daemon
. With Emacs running as daemon emacsclient
is used to connect. Emacsclient doesn't provide the same "batch"
functionality. But we can still use it to export org files through
Emacs daemon. We'll simply need to pass it some elisp code to do the
same.
emacsclient \ --eval "(progn (find-file \"${ORG_FILE}\") (org-html-export-to-html))"
With --eval
we can pass elisp code to the running instance of Emacs
daemon. progn
is an elisp special form that allows us to put
multiple function calls in a sequence. find-file
will find and, more
importantly, load the given file into a buffer. Finally
org-html-export-to-html
will export the file to HTML. This is much
faster as it happens in the daemon, an already running instance of
Emacs.
Now you'll want to ensure that your automation always works (because what good are automations if they fail). If you run the command on your machine, it might be safe to assume you have an Emacs daemon running. But there are plenty of good reasons or environments which don't have an Emacs daemon running. Think of continuous integration systems or other developer's systems. This means we should check if an Emacs daemon is available and resort to just using Emacs otherwise. Fortunately, checking is easy:
emacsclient --alternate-editor false --eval 't'
This will run emacsclient with the option --alternate-editor
which
allows us to define an executable that should be run unless an Emacs
daemon is found to connect to. We specify false
as alternate editor
because in order to check if Emacs daemon is running, we don't want
another editor to open, instead we want the command to fail, i.e.
return a non-zero exit code, which false
does. --eval 't'
will
just evaluate to true, which essential is a noop, which will make
emacsclient return with a zero exit code, meaning success. This can
conveniently be used as a condition to if
. Hence the final script
will look like this:
#!/bin/sh if emacsclient --alternate-editor false --eval 't'; then emacsclient \ --eval "(progn (find-file \"$1\") (org-html-export-to-html))" else emacs --batch \ --load ~/.emacs.d/init.el \ --funcall org-html-export-to-html \ --kill "$1" fi
So far so good. This solution follows the principle of ceteris paribus. This means the file will be exported and everything else is unchanged, including the fact that Emacs daemon was either running or not. If you are willing to sacrifice the principle of ceteris paribus the solution can even be reduced to:
#!/bin/sh emacsclient --alternate-editor "" \ --eval "(progn (find-file \"$1\") (org-html-export-to-html))"
From the manpage of emacs client:
-a, –alternate-editor=EDITOR
if the Emacs server is not running, run the specified editor instead. This can also be specified via the 'ALTERNATE\EDITOR' environment variable. If the value of EDITOR is the empty string, then Emacs is started in daemon mode and emacsclient will try to connect to it.
This will start Emacs daemon if it is not running, which not only makes the check for a running Emacs daemon unnescessary it will also automatically speed up consecutive exports.
Additionally, this is so short, instead of a shell script this could be an alias when wrapped in an immediate function.
alias org2html='f() { emacsclient -a "" -e "(progn (find-file \"$1\") (org-html-export-to-html))" };f'
As a bonus for the reader who made it to this point. Here is the snippet of how we use the pattern above in our Makefiles. "Wait, what?" you say. Why Makefiles? Much like org, we are using GNU make a lot. And by "a lot", I mean basically for everything – you see where this is going. I cannot stress enough how useful GNU make is! It's exactly like the late Joe Armstrong said:
Make is the only build tool you'll ever need to learn. […] I cannot understand why people use specialized build tools for their different languages. […] Once you've learned how make works, you use it for everything, and then you don't need to learn a new build tool when you change your programming language. It's very easy to learn as well.
(Word of warning when copying this code: GNU make is very picky about tabs and space, and those are easy to mess up when copying and pasting code from a website.)
F?=default.org .PHONY: export export: ## Exports a given org file F to HTML if emacsclient -a false -e 't'; then \ emacsclient -e '(progn (find-file "$(F)") (org-html-export-to-html))'; \ else \ emacs --batch -l ~/.emacs.d/init.el $(F) -f org-html-export-to-html --kill; \ fi .PHONY: watch watch: ## watches subdirectories and exports and syncs on change filewatcher -l *.org 'make export F=$$FILENAME; make sync'
Having this in your Makefile will export default.org
to a HTML file
with make export
. This can be applied to any other org file by
providing its path as F to make.
make export F=path/to/your.org
Additionally, the make target watch
uses Filewatcher CLI to observe
changes to org files in any subdirectories and run make export
and
make sync
for a fully automated deployment process on save.
If you liked this post, please consider supporting our Free and Open Source software work – you can sponsor us on Github and Patreon or star our FLOSS repositories.