Skip to content

buildpacks-community/scafall

scafall

Build results Go Report Card codecov GoDoc GitHub license Slack Gitpod ready-to-code

A project scaffolding tool inspired by cookiecutter.

Problem

We needed a tool to create new source code projects from templates. In addition, we needed the tool to be a library written in Go. Scafall takes project templates, asks the end-user some questions and produces an output folder.

Scafall differs from some other Go scaffolding/templating tools as it passes through unknown template subsitutions. For example, if your input application source or documentation contains a {{.Foo}} template and no argument is provided (either programmatically or by the end-user) then the output file will contain the string {{.Foo}}. This allows the generation of projects where the generated source contains templates.

Installation and CLI

As a Go developer you can install scafall into your GOBIN directory.

$ go install github.com/buildpacks-community/scafall@latest

The scafall CLI should now be available for use

$ scafall http://github.com/AidanDelaney/scafall-python-eg.git
✔ Please input a project name: pyexample
? Which Python version to use: [Use arrows to move, type to filter]
  ▸ python3.10
    python3.9
    python3.8
How many digits of Pi to render: 3
$ cd pyexample
$ ./print_pi.py

Programmatic Usage

The programmatic API is documented on pkg.go.dev, which contains more examples. A basic example will prompt the end-user for any values the project scaffolding requires:

package main

import (
  "fmt"

  scafall "github.com/buildpacks-community/scafall/pkg"
)

func main() {
  s := scafall.NewScafall(scafall.WithOutputfolder("python-pi"))
  err := s.Scaffold("http://github.com/AidanDelaney/scafall-python-eg.git")
  if err != nil {
    fmt.Printf("scaffolding failed: %s", err)
  }
}

Of Arguments

When using scafall programmatically you may want to provide values for template variables. In scafall these are termed arguments. An argument may define map[string]string{"PI": "3.14"} any prompting for an alternative value to PI is skipped and the 3.14 values is used in templates. This is particularly useful where the calling code calculates a value, such as a username, and does not want the end-user to be prompted to chage this value.

Project Templates

Project templates are normal source code projects with the addition of a prompts.toml file. The prompts.toml file defines questions to ask of the end-user. The answers to the questions are available as template variables. For example, suppose we have a project template to create a new Python project, we only need to ask the end-user which python interpreter to use and how many python digits to generate:

$ scafall http://github.com/AidanDelaney/scafall-python-eg.git
? Which Python version to use: [Use arrows to move, type to filter]
  ▸ python3.10
    python3.9
    python3.8
✔ How many digits of Pi to render: 3
2022/04/06 20:28:41     create  /print_pi.py

The values for the python interpreter and number of digits to render are available as {{.PythonVersion}} and {{.NumDigits}} respectively. Thus the input template

#!env -- {{.PythonVersion}}
from math import pi

print("%.{{.NumDigits}}f" % pi)

is generated as

#!env -- python3.10
from math import pi

print("%.3f" % pi)

A project template containing a prompts.toml file will produce a generated project that omits the prompts.toml file. In addition, any root-level README.md file in the project template is not propagated to the generated project. This allows the project template to contain a README.md to explain usage of the project template.

Prompts.toml Format

The prompts.toml file is a sequence of [[prompt]] which must each deine a name and prompt. A minimal example is

[[prompt]]
name = "NumDigits"
prompt = "How many digits of Pi to render"

An example with two prompts is

[[prompt]]
name = "PythonVersion"
prompt = "Which Python version to use"
required = true
choices = ["python3.10", "python3.9", "python3.8"]

[[prompt]]
name = "NumDigits"
prompt = "How many digits of Pi to render"
default = "3"

The choices and default fields are mutually exclusive. In the case that both choices and default are used, the default is silently ignored and the first of choices becomes the default.