DEV Community

Justin Lin
Justin Lin

Posted on

mdsh: Run Shell Scripts in Markdown Templates

I have been using hledger as my primary personal accounting software for years. I love that I can manage my ledger in plaintext and even use Git to version control and backup.

But when it comes to generating reports, it often takes me time to figure out all the commands I need. Also, having a way to archive previous data is important. I used to write a shell script with all the report commands, and add a lot of echo statements to generate a markdown report. However, this approach is hard to read and makes the template difficult to maintain.

That's why I made mdsh, a markdown template engine written in Go, which allows you to execute shell scripts within Markdown. It allows you to use Go's template syntax in markdown, and puts the execution results in the generated output.

You can install it with Go CLI:

go install github.com/lancatlin/mdsh@latest
Enter fullscreen mode Exit fullscreen mode

The First Template

Suppose we want to generate a system information report. You can define a markdown template system-info.md as follows:

# 💥 System Information Report for {{ sh "hostname" }}

* **Hostname**: {{ sh "hostname" }}
* **Username**: {{ sh "whoami" }}
* **Uptime**: {{ sh "uptime -p" }}
* **System**: {{ sh "uname -a" }}
* **CPU**: {{ sh "uname -m" }} — {{ sh "nproc" }} cores
* **IP Address**: {{ sh "hostname -I || ip a | grep inet" }}
* **Default Gateway**: {{ sh "ip route | grep default || netstat -rn | grep default" }}
Enter fullscreen mode Exit fullscreen mode

Run the template:

mdsh system-info.md
Enter fullscreen mode Exit fullscreen mode

It will render into:

# 💥 System Information Report for `fedora`

* **Hostname**: `fedora`
* **Username**: `wancat`
* **Uptime**: `up 1 hour, 13 minutes`
* **System**: `Linux fedora 6.15.6-200.fc42.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Jul 10 15:22:32 UTC 2025 x86_64 GNU/Linux`
* **CPU**: `x86_64` — `16` cores
* **IP Address**: `172.26.198.115 2405:dc00:ec83:ec80:af9c:87ed:9bae:bd0d`
* **Default Gateway**: `default via 172.26.198.50 dev wlp1s0 proto dhcp src 172.26.198.115 metric 600`
Enter fullscreen mode Exit fullscreen mode

It makes generating reports as easy as a breeze.

Different Template Functions

Currently, it supports 3 types of template functions, which change the format they generate to:

  1. sh: puts the output in inline code
  2. shell: puts the output in a
code block
Enter fullscreen mode Exit fullscreen mode
  1. raw: puts output without any decorations

Also, any functions and syntax supported in text/template are supported.

Custom Parameters from Command Line Arguments

The best part of mdsh is that it supports custom parameters, which means you can define the parameters you're going to use in the template, and pass them through command line arguments.

You can access the parameters through both template data and environment variables. So you can access these variables from the script with ease.

For example, I need to generate a monthly report for my ledger. I need to specify begin and end times for the report.

Define the template as follows:

---
params:
  b:
    required: true
  e:
    required: true
  f:
    default: examples/ledger.j
---
// The template body
Enter fullscreen mode Exit fullscreen mode

As you can see in the code, you can define custom parameters in the params: section in the frontmatter. I defined 3 parameters: b, e, and f, which stand for begin, end, and file. (This follows the convention in hledger)

Then I can use those parameters in the template body.

# Finance Report from {{.b}} to {{.e}}

## Net Income:

{{ shell "hledger -f $f income -b $b -e $e --monthly" }}
Enter fullscreen mode Exit fullscreen mode

I use {{ .b }} to access the parameter directly and put it in the heading. Then I can use $b in the command. All the parameters will be passed as environment variables to the executing shell. So you can access them very easily.

mdsh hledger_monthly.md -b 2011-01 -e 2011-02
Enter fullscreen mode Exit fullscreen mode

It will generate as follows:

# Finance Report from 2011-01 to 2011-02
## Net Income:
Enter fullscreen mode Exit fullscreen mode
```
Income Statement 2011-01

                        ||        Jan
=========================++============
Revenues                ||
-------------------------++------------
Income:Salary           ||  $2,000.00
-------------------------++------------
                        ||  $2,000.00
=========================++============
Expenses                ||
-------------------------++------------
Expenses:Auto           ||  $5,500.00
Expenses:Books          ||     $20.00
Expenses:Food:Groceries ||    $109.00
-------------------------++------------
                        ||  $5,629.00
=========================++============
Net:                    || $-3,629.00`
```
Enter fullscreen mode Exit fullscreen mode

It can even generate usage for each template based on the frontmatter.

params:
  b:
    required: true
    usage: |
      Required.
      The begin time of report.
      Examples:
        2025-07-01
        Jul
  f:
    usage: The ledger file to parse
    default: ~/.hledger.journal
Enter fullscreen mode Exit fullscreen mode

Run the help command:

$ mdsh hledger_monthly.md -h
Usage of params:
  -b string
        Required.
        The begin time of report.
        Examples:
          2025
          Jul
         (default "2011-01")
  -e string
        Required.
        The end time of report.
        Same format as -b
         (default "2011-02")
  -f string
        The ledger file to parse (default "examples/ledger.j")
Enter fullscreen mode Exit fullscreen mode

Output Filename Template

The default setting is writing the output to stdout.
If you want to save it in a file. You can do so by specifying output: in frontmatter.

output: monthly_report_{{.b}}.md
Enter fullscreen mode Exit fullscreen mode

When running mdsh hledger_monthly.md -b 2025-07 -e 2025-08, it saves the output to monthly_report_2025-07.md. It helps you remain the naming consistency with ease.

Not only parameters, you can also put shell script into it. For example:

output: sys-report-{{ raw "date --iso-8601" }}.md
Enter fullscreen mode Exit fullscreen mode

Then it will write the result to sys-report-2025-07-25.md.

Use Case Ideas

mdsh has great potential in areas that require lots of shell scripting and documentation, like sysadmin, devops, and security. It's also good for creating documentation and tutorials, reducing the hassle of pasting command outputs over and over.

You can make it fit into your workflow by defining your own templates, with usage notes inside. Thanks to the rich features of Go's template engine, it allows huge extensibility. You can apply condition checks ({{ if }}, {{ with }}) or even loops.

Some potential usages are:

  1. Sysadmin/DevOps: system snapshots, cluster health
  2. Documentation: release notes, test results
  3. Education: lab reports, tutorials
  4. Security/Compliance: audits, vulnerability scans
  5. Personal Finance: hledger, budget reports (what I'm using it for)

And many more waiting for you to discover!


If you found this project useful, please give me a star on GitHub 🌟


Side note: I developed the first version of mdsh within 2 hours at midnight while trying out Zed, and was impressed by its performance. I didn't use much AI for this project—sometimes you just need time and space to enjoy programming.

Originally posted on wancat.cc

Top comments (0)