Pluck for Markdown (PluckMD) is a CLI tool built on the Blocky Pluck tool, which allows you to "pluck" Golang type and function definitions from source code files. PluckMD uses this functionality to programmatically replace code blocks in Markdown files to help ensure your code documentation stays up-to-date. Along with Go, PluckMD also supports plucking YAML code as well.
PluckMD recursively searches a given directory for Markdown files. For each file, it scans the contents looking for Markdown comments that contain a PluckMD "directive", where a directive looks something like:
pluck("lang", "kind", "name", "source", start, end)
Let's look at a concrete example. There is a file in our repository called
goplucker.go that contains a GoPlucker struct with a Pluck method. To
extract the Pluck function and include it here in our README, we
define a Markdown comment containing the following directive:
pluck("go", "function", "GoPlucker.Pluck", "https://github.com/tahardi/pluckmd/blob/main/internal/pluck/goplucker.go", -1, -1)
This directive tells PluckMD to pluck a Go function called GoPlucker.Pluck
from a file located at the given URL and to hide the function body (the pair
-1,-1 is used to indicate that we don't want to display the body).
If you view the "raw" version of our README.md, you will see a comment immediately
following this text that contains our directive. Initially, the code block
below was empty, but after running pluckmd --dir . it was populated using the
information contained in the directive.
func (g *GoPlucker) Pluck(
ctx context.Context,
code string,
name string,
kind Kind,
) (string, error) {
// ...
}Check for yourself. Delete the contents of the code block but leave
the opening ticks, language identifier, and closing ticks. Then run
pluckmd --dir . and the code block will once again be populated with the
GoPlucker.Pluck function.
The start and end fields can be used to display only a portion of
the struct or function body. This is useful when you want to walk a user through
the logical sections of a function or struct. For example, let's look at the
first part of the GoPlucker.Pluck function:
func (g *GoPlucker) Pluck(
ctx context.Context,
code string,
name string,
kind Kind,
) (string, error) {
switch kind {
case File:
return code, nil
case Func, Type:
break
case Node:
return "", fmt.Errorf("%w: node kind not supported", ErrGoPlucker)
default:
return "", fmt.Errorf("%w: unrecognized kind: %v", ErrGoPlucker, kind)
}
// ...
}Here we might describe what this first section of the function is doing, before moving on to the next bit...
func (g *GoPlucker) Pluck(
ctx context.Context,
code string,
name string,
kind Kind,
) (string, error) {
// ...
var out bytes.Buffer
var stderr bytes.Buffer
pick := fmt.Sprintf("%s=%s:%s", PickArg, kind, name)
cmd := exec.CommandContext(ctx, GoPluckCmd, pick)
cmd.Stdin = strings.NewReader(code)
cmd.Stdout = &out
cmd.Stderr = &stderr
// ...
}...and the next one...
func (g *GoPlucker) Pluck(
ctx context.Context,
code string,
name string,
kind Kind,
) (string, error) {
// ...
err := cmd.Run()
if err != nil {
return "", fmt.Errorf(
"%w: running %s: %s",
ErrGoPlucker,
GoPluckCmd,
stderr.String(),
)
}
return out.String(), nil
}...until we reach the end. Finally, we might use the pair (0, 0) to tell
PluckMD to display the entire function body:
func (g *GoPlucker) Pluck(
ctx context.Context,
code string,
name string,
kind Kind,
) (string, error) {
switch kind {
case File:
return code, nil
case Func, Type:
break
case Node:
return "", fmt.Errorf("%w: node kind not supported", ErrGoPlucker)
default:
return "", fmt.Errorf("%w: unrecognized kind: %v", ErrGoPlucker, kind)
}
var out bytes.Buffer
var stderr bytes.Buffer
pick := fmt.Sprintf("%s=%s:%s", PickArg, kind, name)
cmd := exec.CommandContext(ctx, GoPluckCmd, pick)
cmd.Stdin = strings.NewReader(code)
cmd.Stdout = &out
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return "", fmt.Errorf(
"%w: running %s: %s",
ErrGoPlucker,
GoPluckCmd,
stderr.String(),
)
}
return out.String(), nil
}- Install the Blocky Pluck CLI tool.
go install github.com/blocky/pluck/cmd/pluck@v0.1.1- Install the Bearclave PluckMD CLI tool.
go install github.com/tahardi/pluckmd/cmd/pluckmd@v0.1.1- That's it! Add some pluck directives to your Markdown files and try it out! Simply define an empty code block with a pluck directive on the line that immediately precedes it and then run PluckMD.
Here we detail the various options and operating modes supported by the PluckMD CLI tool.
Below is an example illustrating how to use the pluckmd tool:
pluckmd --dir . \
--ignore-dir testdata/ \
--ignore-dir .github/ \
--timeout 120Note you must pass a --dir argument so pluckmd knows which directory to
process, whereas the other arguments are optional. You can use pluckmd --help
for a full list and description of supported arguments.
Directives are used to tell pluckmd what code to fetch, where to fetch it from,
and how to render it. The format for a directive is:
pluck("lang", "kind", "name", "source", start, end)
Currently, PluckMD supports plucking code for the following languages:
goyaml
This field indicates what kind of code block is being plucked.
fileused to read an entire file. Can be used with bothgoandyaml.functionused to read a function. Only used withgo.nodeused to read a node component. Only used withyaml.typeused to read a type definition. Only used withgo.
The name of the code or file to be plucked.
GoPluckerthe name of a standalone function or typeGoPlucker.Pluckfunctions defined on structs are named<struct>.<func>enclave-sev.yamlwhen readings files the entire filename must be specifiedenclave.args.domainfor nested YAML nodes you must specify the node path
Currently, PluckMD supports fetching source code from:
- GitHub
- Local Files
The local fetcher reads local files given an absolute or relative path. Note
that relative paths are assumed to be relative to the directory in which
pluckmd is run.
For example, this repository has a makefile target for running pluckmd to
(re-)generate code blocks in our README.md. Since pluckmd is run from the
top-level of this repository, we must use a path relative to the top-level
directory of our repository:
pluck("go", "function", "GoPlucker.Pluck", "internal/pluck/goplucker.go", -1, -1)
func (g *GoPlucker) Pluck(
ctx context.Context,
code string,
name string,
kind Kind,
) (string, error) {
// ...
}The local fetcher is useful when you want to include documentation not yet tracked in a remote repository, such as when you are working on a feature branch that introduces a new function.
The pair [start, end) is used to display a portion of the plucked type or
function body. Currently, this feature is only supported for Golang code and
not YAML. Depending on whether the displayed code is at the beginning, middle, or
end of the body, PluckMD will add lines containing // ... to indicate to the
reader that part of the body is hidden.
There are two special pairs to be aware of:
-1, -1indicates that the entire code block should be excluded from the output0, 0indicates that the entire code block should be included in the output
The YAML plucker can be used to extract specific YAML components from a file.
Note that the YAML plucker does not currently support returning partial YAML
components. Thus, the start and end parameters of the pluck directive are
ignored. Let's use our enclave-sev.yaml file to demonstrate how to pluck YAML.
Use the following directive to print the entire contents of the file:
pluck("yaml", "file", "enclave-sev.yaml", "internal/pluck/testdata/enclave-sev.yaml", -1, -1)
platform: "sev"
enclave:
addr: "http://127.0.0.1:8083"
addr_tls: "https://127.0.0.1:8444"
args:
domain: "bearclave.tee"
proxy:
addr_tls: "http://127.0.0.1:8084"
rev_addr: "http://0.0.0.0:8080"
rev_addr_tls: "https://0.0.0.0:8443"Note that the type and func kinds only apply to programming languages such
as Go. To print a component of a configuration language such as YAML, use the
node kind and the path to the component within the file:
pluck("yaml", "node", "enclave", "internal/pluck/testdata/enclave-sev.yaml", -1, -1)
enclave:
addr: "http://127.0.0.1:8083"
addr_tls: "https://127.0.0.1:8444"
args:
domain: "bearclave.tee"You must provide the full path to the component within the file:
pluck("yaml", "node", "enclave.args", "internal/pluck/testdata/enclave-sev.yaml", -1, -1)
args:
domain: "bearclave.tee"- pluck directives are contained within a single-line Markdown comment (i.e.,
<!-- directive -->) - pluck directives are on the line directly preceding the code block
- if code blocks are indented, the pluck directive has the same indentation
- the code block is marked as Golang or YAML code
- the YAML snipper does not currently support returning partial YAML components
- the YAML plucker may not support all YAML features