Skip to content

Instantly share code, notes, and snippets.

@pjeby
Last active January 2, 2024 19:02
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save pjeby/c137ace4d91e61e8f1f80e92d84e8b70 to your computer and use it in GitHub Desktop.
Save pjeby/c137ace4d91e61e8f1f80e92d84e8b70 to your computer and use it in GitHub Desktop.
"You got your markdown in my shell script!" "No, you got your shell script in my markdown!"

Mixed Markdown and Shell Scripting

: By the power of this magic string: ex: set ft=markdown ;:<<'```shell' #, this file is now both a markdown document and an executable shell script. chmod +x it and try running it!

The above line does just what it says. More specifically, when placed within in the first 5 lines and preceded only by blank lines or #-prefixed markdown headers:

  1. The first part of the magic string makes github and various editors (e.g. atom with the vim-modeline packge) treat the file as having markdown syntax (even if the file doesn't have an extension)
  2. The second part (if run in a shell), makes the shell skip execution until it encounters the next ```shell block.

(The line also has to start with a : so that it's valid shell code.)

Execution then picks up at the next triple-backquoted code block tagged as shell. If you want multiple shell blocks, just terminate them as follows:

echo Hello world!

:<<'```shell'  # this will skip to the next shell block

You can also embed other languages, eg. python:

# skip to python block, run python on it, skip to next shell
:<<'```python' ; python <<'```' ; :<<'```shell'

Well, it's a bit awkward. But here's the python:

print("hello again!")

And your last shell block can either end with :<<'```' if it's the last text in the file, or else explicitly exit or exec something to avoid reading any trailing markdown.

exit $?

And now we're done! It's a little messy, but it works in a pinch. If you'd rather have a saner way of doing this, check out mdsh -- the magic string in that case looks more like:

: By the power of this magic string, ex: set ft=markdown ; exec mdsh "$0" "$@" #, I won't need to add magic strings in every code block, or have a tacky exit block! (And I can declare interpreters for non-shell languages instead of using ugly triple heredoc lines.)

mdsh also has a ton of other features for writing literate shelldown. Check it out!

Nitpicker's Notes

Some people may wish to point out that a file without a shebang line is not technically a valid executable and is therefore not portable. Not only are they missing the point of how frickin' awesome this is, but they are also technically incorrect. (The best kind of incorrect!)

You see, POSIX 2008.1-compliant shells are required to treat shebangless text files as shell scripts -- and you wouldn't want to use a non-POSIX-compliant shell, would you? (Even busybox is compliant enough!) For that matter, since POSIX.2008-1, the "treat other files as a shell script" behavior applies to execvp() and execlp() as well.

So the only time a non-shebang file is in any danger of failing execution is when it's being executed in a non-POSIX environment or by a program that doesn't use those functions to do the exec. (And in such cases, you can simply prefix your command string with env or sh or whatever!) So don't let this detail get in the way of how awesome it can be to mix markdown and shell scripting.

(You can also just use a shebang line, if it bothers you that much or yours or your users' environment sucks that hard and you don't mind your markdown file having a huge heading saying !/usr/bin/env or some such. The point is, think about your use cases instead of just blindly opting for one path or another.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment