Pure bash Markdown table generator

https://josh.fail/2022/pure-bash-markdown-table-generator/

I wanted a portable markdown table generator a few months back and threw together markdown-table.

I initially just wanted to pass a bunch of arguments to the script along with a column count and have it spit out the appropriate markdown. Once I had that working, I realized it was trivial to add support for parsing files with custom delimiters — like CSV or TSV.

The final script accepts args…

markdown-table -4 \
 "Heading 1"  "Heading 2" "Heading 3" "Heading 4" \
 "Hi"         "There"     "From"      "Markdown\!" \
 "Everything" "Is"        "So"        "Nicely Aligned\!"

Or, accepts a TSV file like test.tsv with markdown-table --tsv < test.tsv

Heading 1	Heading 2	Heading 3	Heading 4
Hi	There	From	Markdown
Everything	Is	So	Nicely Aligned

Both examples produce output like:

| Heading 1  | Heading 2 | Heading 3 | Heading 4      |
| ---------- | --------- | --------- | -------------- |
| Hi         | There     | From      | Markdown       |
| Everything | Is        | So        | Nicely Aligned |

Hope it helps!


#!/usr/bin/env bash
# Usage: markdown-table -COLUMNS [CELLS]
#        markdown-table -sSEPARATOR < file
#
# NAME
#   markdown-table -- generate markdown tables
#
# SYNOPSIS
#   markdown-table -COLUMNS [CELLS]
#   markdown-table -sSEPARATOR < file
#
# DESCRIPTION
#   markdown-table helps generate markdown tables. Manually supply arguments
#   and a column count to generate a table, or pass in a delimited file to
#   convert to a table.
#
# OPTIONS
#   -COLUMNS
#       Number of columns to include in output.
#
#   -sSEPARATOR
#       String used to separate columns in input files.
#
#   --csv
#       Shortcut for `-s,` to parse CSV files. Note that this is a "dumb" CSV
#       parser -- it won't work if your cells contain commas!
#
#   --tsv
#       Shortcut for `-s$'\t'` to parse TSV files.
#
#   -h, --help
#       Prints help text and exits.
#
# EXAMPLES
#   Build a 4 column markdown table from arguments:
#     markdown-table -4 \
#       "Heading 1"  "Heading 2" "Heading 3" "Heading 4" \
#       "Hi"         "There"     "From"      "Markdown!" \
#       "Everything" "Is"        "So"        "Nicely Aligned!"
#
#   Convert a CSV file into a markdown table:
#     markdown-table -s, < some.csv
#     markdown-table --csv < some.csv
#
#   Convert a TSV file into a markdown table:
#     markdown-table -s$'\t' < test.tsv
#     markdown-table --tsv < test.tsv
# Call this script with DEBUG=1 to add some debugging output
if [[ "$DEBUG" ]]; then
  export PS4='+ [${BASH_SOURCE##*/}:${LINENO}] '
  set -x
fi
set -e
# Echoes given args to STDERR
#
# $@ - args to pass to echo
warn() {
  echo "$@" >&2
}
# Print the help text for this program
#
# $1 - flag used to ask for help ("-h" or "--help")
print_help() {
  sed -ne '/^#/!q;s/^#$/# /;/^# /s/^# //p' < "$0" |
    awk -v f="$1" '
      f == "-h" && ($1 == "Usage:" || u) {
        u=1
        if ($0 == "") {
          exit
        } else {
          print
        }
      }
      f != "-h"
      '
}
# Returns the highest number in the given arguments
#
# $@ - one or more numeric arguments
max() {
  local max=0 arg
  for arg; do
    (( ${arg:-0} > max )) && max="$arg"
  done
  printf "%s" "$max"
}
# Formats a table in markdown format
#
# $1 - field separator string
format_table() {
  local fs="$1" buffer col current_col=0 current_row=0 min=3
  local -a lengths=()
  buffer="$(cat)"
  # First pass to get column lengths
  while read -r line; do
    current_col=0
    while read -r col; do
      lengths["$current_col"]="$(max "${#col}" "${lengths[$current_col]}")"
      current_col=$((current_col + 1))
    done <<< "${line//$fs/$'\n'}"
  done <<< "$buffer"
  # Second pass writes each row
  while read -r line; do
    current_col=0
    current_row=$((current_row + 1))
    while read -r col; do
      printf "| %-$(max "${lengths[$current_col]}" "$min")s " "$col"
      current_col=$((current_col + 1))
    done <<< "${line//$fs/$'\n'}"
    printf "|\n"
    # If this is the first row, print the header dashes
    if [[ "$current_row" -eq 1 ]]; then
      for (( current_col=0; current_col < ${#lengths[@]}; current_col++ )); do
        printf "| "
        printf "%$(max "${lengths[$current_col]}" "$min")s" | tr " " -
        printf " "
      done
      printf "|\n"
    fi
  done <<< "$buffer"
}
# Main program
main() {
  local arg cols i fs="##$$FS##"
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -h | --help) print_help "$1"; return 0 ;;
      -[0-9]*) cols="${1:1}"; shift ;;
      -s*) fs="${1:2}"; shift ;;
      --csv) fs=","; shift ;;
      --tsv) fs=$'\t'; shift ;;
      --) shift; break ;;
      -*) warn "Invalid option '$1'"; return 1 ;;
      *) break ;;
    esac
  done
  if [[ -z "$fs" ]]; then
    warn "Field separator can't be blank!"
    return 1
  elif [[ $# -gt 0 ]] && ! [[ "$cols" =~ ^[0-9]+$ ]]; then
    warn "Missing or Invalid column count!"
    return 1
  fi
  { if [[ $# -gt 0 ]]; then
      while [[ $# -gt 0 ]]; do
        for (( i=0; i < cols; i++ )); do
          if (( i + 1 == cols )); then
            printf "%s" "$1"
          else
            printf "%s%s" "$1" "$fs"
          fi
          shift
        done
        printf "\n"
      done
    else
      cat
    fi
  } | format_table "$fs"
}
main "$@"
{
"by": "ekiauhce",
"descendants": 0,
"id": 40246441,
"score": 2,
"time": 1714735738,
"title": "Pure bash Markdown table generator",
"type": "story",
"url": "https://josh.fail/2022/pure-bash-markdown-table-generator/"
}
{
"author": "Joshua Priddle",
"date": "2022-02-02T02:55:30.000Z",
"description": "I wanted a portable markdown table generator a few months back and threw together markdown-table.",
"image": null,
"logo": null,
"publisher": "josh.fail",
"title": "Pure bash markdown table generator — josh.fail",
"url": "https://josh.fail/2022/pure-bash-markdown-table-generator/"
}
{
"url": "https://josh.fail/2022/pure-bash-markdown-table-generator/",
"title": "Pure bash markdown table generator — josh.fail",
"description": "I wanted a portable markdown table generator a few months back and threw together markdown-table. I initially just wanted to pass a bunch of arguments to the script along with a column count and have it spit...",
"links": [
"https://josh.fail/2022/pure-bash-markdown-table-generator/"
],
"image": "",
"content": "<div>\n<article>\n <div>\n <p>I wanted a portable markdown table generator a few months back and threw\ntogether <code>markdown-table</code>.</p>\n<p>I initially just wanted to pass a bunch of arguments to the script along with\na column count and have it spit out the appropriate markdown. Once I had that\nworking, I realized it was trivial to add support for parsing files with\ncustom delimiters — like CSV or TSV.</p>\n<p>The final script accepts args…</p>\n<div><pre><code>markdown-table -4 \\\n \"Heading 1\" \"Heading 2\" \"Heading 3\" \"Heading 4\" \\\n \"Hi\" \"There\" \"From\" \"Markdown\\!\" \\\n \"Everything\" \"Is\" \"So\" \"Nicely Aligned\\!\"\n</code></pre></div>\n<p>Or, accepts a TSV file like <code>test.tsv</code> with <code>markdown-table --tsv &lt;\ntest.tsv</code>…</p>\n<pre><code>Heading 1\tHeading 2\tHeading 3\tHeading 4\nHi\tThere\tFrom\tMarkdown\nEverything\tIs\tSo\tNicely Aligned\n</code></pre>\n<p>Both examples produce output like:</p>\n<div><pre><code>| Heading 1 | Heading 2 | Heading 3 | Heading 4 |\n| ---------- | --------- | --------- | -------------- |\n| Hi | There | From | Markdown |\n| Everything | Is | So | Nicely Aligned |\n</code></pre></div>\n<p>Hope it helps!</p>\n<hr />\n<div><pre><code><span>#!/usr/bin/env bash</span>\n<span># Usage: markdown-table -COLUMNS [CELLS]</span>\n<span># markdown-table -sSEPARATOR &lt; file</span>\n<span>#</span>\n<span># NAME</span>\n<span># markdown-table -- generate markdown tables</span>\n<span>#</span>\n<span># SYNOPSIS</span>\n<span># markdown-table -COLUMNS [CELLS]</span>\n<span># markdown-table -sSEPARATOR &lt; file</span>\n<span>#</span>\n<span># DESCRIPTION</span>\n<span># markdown-table helps generate markdown tables. Manually supply arguments</span>\n<span># and a column count to generate a table, or pass in a delimited file to</span>\n<span># convert to a table.</span>\n<span>#</span>\n<span># OPTIONS</span>\n<span># -COLUMNS</span>\n<span># Number of columns to include in output.</span>\n<span>#</span>\n<span># -sSEPARATOR</span>\n<span># String used to separate columns in input files.</span>\n<span>#</span>\n<span># --csv</span>\n<span># Shortcut for `-s,` to parse CSV files. Note that this is a \"dumb\" CSV</span>\n<span># parser -- it won't work if your cells contain commas!</span>\n<span>#</span>\n<span># --tsv</span>\n<span># Shortcut for `-s$'\\t'` to parse TSV files.</span>\n<span>#</span>\n<span># -h, --help</span>\n<span># Prints help text and exits.</span>\n<span>#</span>\n<span># EXAMPLES</span>\n<span># Build a 4 column markdown table from arguments:</span>\n<span># markdown-table -4 \\</span>\n<span># \"Heading 1\" \"Heading 2\" \"Heading 3\" \"Heading 4\" \\</span>\n<span># \"Hi\" \"There\" \"From\" \"Markdown!\" \\</span>\n<span># \"Everything\" \"Is\" \"So\" \"Nicely Aligned!\"</span>\n<span>#</span>\n<span># Convert a CSV file into a markdown table:</span>\n<span># markdown-table -s, &lt; some.csv</span>\n<span># markdown-table --csv &lt; some.csv</span>\n<span>#</span>\n<span># Convert a TSV file into a markdown table:</span>\n<span># markdown-table -s$'\\t' &lt; test.tsv</span>\n<span># markdown-table --tsv &lt; test.tsv</span>\n<span># Call this script with DEBUG=1 to add some debugging output</span>\n<span>if</span> <span>[[</span> <span>\"</span><span>$DEBUG</span><span>\"</span> <span>]]</span><span>;</span> <span>then\n </span><span>export </span><span>PS4</span><span>=</span><span>'+ [${BASH_SOURCE##*/}:${LINENO}] '</span>\n <span>set</span> <span>-x</span>\n<span>fi\n</span><span>set</span> <span>-e</span>\n<span># Echoes given args to STDERR</span>\n<span>#</span>\n<span># $@ - args to pass to echo</span>\nwarn<span>()</span> <span>{</span>\n <span>echo</span> <span>\"</span><span>$@</span><span>\"</span> <span>&gt;</span>&amp;2\n<span>}</span>\n<span># Print the help text for this program</span>\n<span>#</span>\n<span># $1 - flag used to ask for help (\"-h\" or \"--help\")</span>\nprint_help<span>()</span> <span>{</span>\n <span>sed</span> <span>-ne</span> <span>'/^#/!q;s/^#$/# /;/^# /s/^# //p'</span> &lt; <span>\"</span><span>$0</span><span>\"</span> |\n <span>awk</span> <span>-v</span> <span>f</span><span>=</span><span>\"</span><span>$1</span><span>\"</span> <span>'\n f == \"-h\" &amp;&amp; ($1 == \"Usage:\" || u) {\n u=1\n if ($0 == \"\") {\n exit\n } else {\n print\n }\n }\n f != \"-h\"\n '</span>\n<span>}</span>\n<span># Returns the highest number in the given arguments</span>\n<span>#</span>\n<span># $@ - one or more numeric arguments</span>\nmax<span>()</span> <span>{</span>\n <span>local </span><span>max</span><span>=</span>0 arg\n <span>for </span>arg<span>;</span> <span>do</span>\n <span>((</span> <span>${</span><span>arg</span><span>:-</span><span>0</span><span>}</span> <span>&gt;</span> max <span>))</span> <span>&amp;&amp;</span> <span>max</span><span>=</span><span>\"</span><span>$arg</span><span>\"</span>\n <span>done\n </span><span>printf</span> <span>\"%s\"</span> <span>\"</span><span>$max</span><span>\"</span>\n<span>}</span>\n<span># Formats a table in markdown format</span>\n<span>#</span>\n<span># $1 - field separator string</span>\nformat_table<span>()</span> <span>{</span>\n <span>local </span><span>fs</span><span>=</span><span>\"</span><span>$1</span><span>\"</span> buffer col <span>current_col</span><span>=</span>0 <span>current_row</span><span>=</span>0 <span>min</span><span>=</span>3\n <span>local</span> <span>-a</span> <span>lengths</span><span>=()</span>\n <span>buffer</span><span>=</span><span>\"</span><span>$(</span><span>cat</span><span>)</span><span>\"</span>\n <span># First pass to get column lengths</span>\n <span>while </span><span>read</span> <span>-r</span> line<span>;</span> <span>do\n </span><span>current_col</span><span>=</span>0\n <span>while </span><span>read</span> <span>-r</span> col<span>;</span> <span>do\n </span>lengths[<span>\"</span><span>$current_col</span><span>\"</span><span>]=</span><span>\"</span><span>$(</span>max <span>\"</span><span>${#</span><span>col</span><span>}</span><span>\"</span> <span>\"</span><span>${</span><span>lengths</span><span>[</span><span>$current_col</span><span>]</span><span>}</span><span>\"</span><span>)</span><span>\"</span>\n <span>current_col</span><span>=</span><span>$((</span>current_col <span>+</span> <span>1</span><span>))</span>\n <span>done</span> <span>&lt;&lt;&lt;</span> <span>\"</span><span>${</span><span>line</span><span>//</span><span>$fs</span><span>/</span><span>$'</span><span>\\n</span><span>'</span><span>}</span><span>\"</span>\n <span>done</span> <span>&lt;&lt;&lt;</span> <span>\"</span><span>$buffer</span><span>\"</span>\n <span># Second pass writes each row</span>\n <span>while </span><span>read</span> <span>-r</span> line<span>;</span> <span>do\n </span><span>current_col</span><span>=</span>0\n <span>current_row</span><span>=</span><span>$((</span>current_row <span>+</span> <span>1</span><span>))</span>\n <span>while </span><span>read</span> <span>-r</span> col<span>;</span> <span>do\n </span><span>printf</span> <span>\"| %-</span><span>$(</span>max <span>\"</span><span>${</span><span>lengths</span><span>[</span><span>$current_col</span><span>]</span><span>}</span><span>\"</span> <span>\"</span><span>$min</span><span>\"</span><span>)</span><span>s \"</span> <span>\"</span><span>$col</span><span>\"</span>\n <span>current_col</span><span>=</span><span>$((</span>current_col <span>+</span> <span>1</span><span>))</span>\n <span>done</span> <span>&lt;&lt;&lt;</span> <span>\"</span><span>${</span><span>line</span><span>//</span><span>$fs</span><span>/</span><span>$'</span><span>\\n</span><span>'</span><span>}</span><span>\"</span>\n <span>printf</span> <span>\"|</span><span>\\n</span><span>\"</span>\n <span># If this is the first row, print the header dashes</span>\n <span>if</span> <span>[[</span> <span>\"</span><span>$current_row</span><span>\"</span> <span>-eq</span> 1 <span>]]</span><span>;</span> <span>then\n for</span> <span>((</span> <span>current_col</span><span>=</span>0<span>;</span> current_col &lt; <span>${#</span><span>lengths</span><span>[@]</span><span>}</span><span>;</span> current_col++ <span>))</span><span>;</span> <span>do\n </span><span>printf</span> <span>\"| \"</span>\n <span>printf</span> <span>\"%</span><span>$(</span>max <span>\"</span><span>${</span><span>lengths</span><span>[</span><span>$current_col</span><span>]</span><span>}</span><span>\"</span> <span>\"</span><span>$min</span><span>\"</span><span>)</span><span>s\"</span> | <span>tr</span> <span>\" \"</span> -\n <span>printf</span> <span>\" \"</span>\n <span>done\n </span><span>printf</span> <span>\"|</span><span>\\n</span><span>\"</span>\n <span>fi\n done</span> <span>&lt;&lt;&lt;</span> <span>\"</span><span>$buffer</span><span>\"</span>\n<span>}</span>\n<span># Main program</span>\nmain<span>()</span> <span>{</span>\n <span>local </span>arg cols i <span>fs</span><span>=</span><span>\"##</span><span>$$</span><span>FS##\"</span>\n <span>while</span> <span>[[</span> <span>$# </span><span>-gt</span> 0 <span>]]</span><span>;</span> <span>do\n case</span> <span>\"</span><span>$1</span><span>\"</span> <span>in</span>\n <span>-h</span> <span>|</span> <span>--help</span><span>)</span> print_help <span>\"</span><span>$1</span><span>\"</span><span>;</span> <span>return </span>0 <span>;;</span>\n -[0-9]<span>*</span><span>)</span> <span>cols</span><span>=</span><span>\"</span><span>${</span><span>1</span>:1<span>}</span><span>\"</span><span>;</span> <span>shift</span> <span>;;</span>\n <span>-s</span><span>*</span><span>)</span> <span>fs</span><span>=</span><span>\"</span><span>${</span><span>1</span>:2<span>}</span><span>\"</span><span>;</span> <span>shift</span> <span>;;</span>\n <span>--csv</span><span>)</span> <span>fs</span><span>=</span><span>\",\"</span><span>;</span> <span>shift</span> <span>;;</span>\n <span>--tsv</span><span>)</span> <span>fs</span><span>=</span><span>$'</span><span>\\t</span><span>'</span><span>;</span> <span>shift</span> <span>;;</span>\n <span>--</span><span>)</span> <span>shift</span><span>;</span> <span>break</span> <span>;;</span>\n -<span>*</span><span>)</span> warn <span>\"Invalid option '</span><span>$1</span><span>'\"</span><span>;</span> <span>return </span>1 <span>;;</span>\n <span>*</span><span>)</span> <span>break</span> <span>;;</span>\n <span>esac</span>\n <span>done\n if</span> <span>[[</span> <span>-z</span> <span>\"</span><span>$fs</span><span>\"</span> <span>]]</span><span>;</span> <span>then\n </span>warn <span>\"Field separator can't be blank!\"</span>\n <span>return </span>1\n <span>elif</span> <span>[[</span> <span>$# </span><span>-gt</span> 0 <span>]]</span> <span>&amp;&amp;</span> <span>!</span> <span>[[</span> <span>\"</span><span>$cols</span><span>\"</span> <span>=</span>~ ^[0-9]+<span>$ </span><span>]]</span><span>;</span> <span>then\n </span>warn <span>\"Missing or Invalid column count!\"</span>\n <span>return </span>1\n <span>fi</span>\n <span>{</span> <span>if</span> <span>[[</span> <span>$# </span><span>-gt</span> 0 <span>]]</span><span>;</span> <span>then\n while</span> <span>[[</span> <span>$# </span><span>-gt</span> 0 <span>]]</span><span>;</span> <span>do\n for</span> <span>((</span> <span>i</span><span>=</span>0<span>;</span> i &lt; cols<span>;</span> i++ <span>))</span><span>;</span> <span>do\n if</span> <span>((</span> i + 1 <span>==</span> cols <span>))</span><span>;</span> <span>then\n </span><span>printf</span> <span>\"%s\"</span> <span>\"</span><span>$1</span><span>\"</span>\n <span>else\n </span><span>printf</span> <span>\"%s%s\"</span> <span>\"</span><span>$1</span><span>\"</span> <span>\"</span><span>$fs</span><span>\"</span>\n <span>fi\n </span><span>shift\n </span><span>done\n </span><span>printf</span> <span>\"</span><span>\\n</span><span>\"</span>\n <span>done\n else\n </span><span>cat\n </span><span>fi</span>\n <span>}</span> | format_table <span>\"</span><span>$fs</span><span>\"</span>\n<span>}</span>\nmain <span>\"</span><span>$@</span><span>\"</span>\n</code></pre></div>\n </div>\n</article>\n </div>",
"author": "Joshua Priddle",
"favicon": "",
"source": "josh.fail",
"published": "2022-02-01T21:55:30-05:00",
"ttr": 167,
"type": "article"
}