TekWizely / bash-tpl

A smart, lightweight shell script templating engine, written in Bash
MIT License
65 stars 4 forks source link

Allow variable with `.tpl` file path as argument to `.INCLUDE` directive #16

Closed benjamindblock closed 3 months ago

benjamindblock commented 1 year ago

First, thanks for the great work on bash-tpl -- I've been using it to great effect when making some new static sites.

I ran across an issue recently -- it appears that .INCLUDE cannot be used with a variable to render another .tpl file dynamically (eg., .INCLUDE $1).

Bash version: GNU bash, version 3.2.57(1)-release (arm64-apple-darwin22)

Repro steps:

  1. Create a top-level "layout" file.

    printf '<div class="container">\n  .INCLUDE $1\n</div>\n' > layout.tpl

    Which produces:

    <div class="container">
    .INCLUDE $1
    </div>
  2. Create a "partial" view file with some content

    echo '<span>Subcontent should be displayed</span>' > partial.tpl

    Which produces:

    <span>Subcontent should be displayed</span>
  3. Call bash-tpl

    bash-tpl layout.tpl partial.tpl

This produces the following:

printf "%s\n" \<div\ class=\"container\"\>
  printf "%s\n" \ \ \</div\>

I expected this output:

printf "%s\n" \<div\ class=\"container\"\>
  printf "%s\n" \ \ Subcontent\ should\ be\ displayed
printf "%s\n" \</div\>

I would expect that the argument $1 could be used in the .INCLUDE directive to dynamically pass in the .tpl file that should be rendered. I also could be missing something straightforward here, in which case any insight would be appreciated.

If this is not currently a feature, having this ability in bash-tpl would be great.

TekWizely commented 1 year ago

Greetings @benjamindblock and thank you for using my project and for taking the time to open this issue !

I've done some tests and I am pleasantly surprised !! it looks like the technique I use to parse directive arguments does honor variable references.

The issue you're seeing in your example is that the $[0-9] variables no-longer reference the original command line args but are instead referencing the arguments passed to the process_directive function that handles the directive.

If you wanna see something cool, try this:

layout.tpl

<div class="container">
  .INCLUDE ${INCLUDE_TPL}
</div>

partial.tpl

<span>Subcontent should be displayed</span>

process template

$ INCLUDE_TPL=partial.tpl bash-tpl layout.tpl

output

printf "%s\n" \<div\ class=\"container\"\>
  printf "%s\n" \ \ \<span\>Subcontent\ should\ be\ displayed\</span\>
printf "%s\n" \</div\>

To officially support abusing this technique within bash_tpl, I think we need to expose the command line arguments in a variable that can be referenced from the directive.

maybe something like $ARGS[x]

Honestly though that feels a bit messy in the case of multiple arguments being available for use.

Maybe adding a command-line flag to create named variables could more useful. Something like:

$ bash-tpl layout.tpl --var INCLUDE_TPL=partial.tpl

Or maybe a .ENV directive that could read a .env file ..

I'll grind on this a bit more, but in the meantime lemme know your thoughts and thanks again for participating in my project!

-TW

benjamindblock commented 1 year ago

@TekWizely Aha -- thanks for the details on the process_directive function -- I tried out the .INCLUDE call with the explicit named variable and everything worked as expected. Brilliant.

I really like the idea of a .ENV directive that could load variables from an .env type of file. I'm actually doing a similar hack with eval(...) right now to accomplish just that type of behavior. Brief explanation following...

In my static site generator, each page has a .meta file that can include a tpl= key if multiple pages share a template (like blog posts).

# blog20230415.meta
title="Website | April 15th"
content="Long text..."
tpl="tpls/blog_template.tpl.html

I then use eval(...) to load those variables and render the primary site layout and the subcontent for a particular page with those vars present.

content() {
  bash <(bin/bash-tpl tpls/layout.tpl.html)
}

renderPage() {
  echo "$(eval $(cat "$1") content)"
}

# Example: renderPage "renderPage "blog20230415.meta" > "blog20230415.html"

Example view files:

<!-- tpls/layout.tpl.html -->
<header><% ${title} %></header>
<body>
  .INCLUDE ${tpl}
</body>
<!-- tpls/blog_template.tpl.html -->
<div><% ${content} %></div>

If an .ENV directive was included, we could simplify this nicely.

renderPage() {
  echo "$(bash <(bin/bash-tpl tpls/layout.tpl.html blog20230415.meta))"
}
<!-- tpls/layout.tpl.html -->
%
  .ENV ${1}
%
<header><% ${title} %></header>
<body>
  .INCLUDE ${tpl}
</body>
benjamindblock commented 1 year ago

I've come up with a workaround for rendering subcontent that avoids .INCLUDE and instead just calls source on the subcontent and renders it with the standard <% %> tags. It's been working pretty nicely, though I've still been using .INCLUDE though for partials that require no args (as it has a nicer interface for this sort of thing).

Here's a small example:

renderContent() {
  META_FILE="$1"
  bash <(bin/bash-tpl tpls/site/layout.tpl.html) "${META_FILE}"
}

blogs/1/index.meta

title="Blog Post #1"
tpl="blog.tpl.html"
content="Some content"

layout.html.tpl

%
  META_FILE="$1"
  source "${META_FILE}"

  # Fail if "$tpl" is missing.
  SUBCONTENT="$(source <(bin/bash-tpl ${tpl:?}))"
%
<head>
  <title><% $title %></title>
</head>

<div id="subcontent">
  <% ${SUBCONTENT} %>
</div>

blog.tpl.html

<div id="blog">
  <% "${content}" %>
</div>

This would all get kicked off in a script running: renderContent "blogs/1/index.meta"

TekWizely commented 1 year ago

Hey @benjamindblock it occurs to me that you may be invoking bash-tpl more often than needed.

Bash-tpl is actually a Template Generator ie:

The output of bash-tpl is a shell-script that you can re-use without bash-tpl, with bash-tpl only being needed when the original template changes (ie. the resulting shell script changes).

This fact is a bit obscured because the resulting shell script cannot be executed directly without a little setup + processing:

  1. Scripts intended to be executed (not sourced), need a #! header
  2. Scripts intended to be executed (not sourced), need a +x modifier

Here's an example in action:

compile-once.tpl

% #!/usr/bin/env sh
Hello, world

usage example

$ bash-tpl compile-once.tpl -o use-many-times.sh

$ chmod +x use-many-times.sh

$ ./use-many-times.sh

Hello, world

Here's an example for runtime (sourced) includes:

compile-once-outer.tpl

% #!/usr/bin/env sh
% source ${INCLUDE?}

compile-once-sourced.tpl

Hello, world

usage example

$ bash-tpl compile-once-outer.tpl -o use-many-times-outer.sh

$ chmod +x use-many-times-outer.sh

$ bash-tpl compile-once-sourced.tpl -o use-many-times-sourced.sh

$ INCLUDE="./use-many-times-sourced.sh" ./use-many-times-outer.sh

Hello, world

Makefile

With these ideas in mind, I whipped up a small makefile that might be useful for compiling a directory containing templates into a directory containing re-usable scripts:

Makefile

.PHONY: all clean

.DEFAULT_GOAL := all

BASH_TPL := bash-tpl

TPL_DIR := ./tpl
SH_DIR  := ./sh

TPL_FILES := $(wildcard $(TPL_DIR)/*.tpl $(TPL_DIR)/**/*.tpl)
SH_FILES  := $(patsubst $(TPL_DIR)/%.tpl,$(SH_DIR)/%.sh,$(TPL_FILES))

all: $(SH_FILES) ## Compile templates into shell scipts

clean: ## Remove any shell scripts that have matching templates
    rm -f $(SH_FILES)

$(SH_DIR)/%.sh: $(TPL_DIR)/%.tpl
    @mkdir -p $(dir $@)
    $(BASH_TPL) $< -o $@
    @chmod +x $@

This makefile is very much a quick hack, but may serve as a useful starter for your project.

I'm interested in seeing what you can do with these ideas in your project.

Please give it some thought and lemme know what you think!

-TW

TekWizely commented 1 year ago

hey @benjamindblock just a quick ping to see if you had a chance to read my post above?

benjamindblock commented 1 year ago

Hey @TekWizely -- yes, thanks for all the details! I was, like you suspected, calling bash-tpl many more times than necessary.

With your updates I was able to get the build times for my static site from 11s down to 3s (the site has ~40 HTML pages to build). There may still be a few more optimizations I can make to speed that up a little bit more.

I'll be AFK for a few days but will post some more details about my improvements after that!

TekWizely commented 1 year ago

Hey @benjamindblock just checking back to see how things are going? BTW: I think I may have stumbled onto your site?

benjamindblock commented 1 year ago

Hey @TekWizely, apologies for the late response -- busy summer. Did some work on the site this week and made the repo public (for now), in case you're interested in taking a look at the patterns I setup with Bash-TPL (thanks to your input): https://github.com/benjamindblock/eveningjazz.net

Ultimately I was able to get the full site build down to ~3s for 33 .html pages. Not too bad, I think!

TekWizely commented 3 months ago

@benjamindblock Thanks for the final update post - I'm going to close this now as I think we got a working solution to the initial concern. Thanks!