Skip to content

Commit

Permalink
- Added support for displaying subcommand descriptions and help texts.
Browse files Browse the repository at this point in the history
- Updated readme.
  • Loading branch information
Kristoffer Ahl committed Jan 28, 2020
1 parent 9f2281b commit 8bb3c37
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 16 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# go-centry

A library for bash command-line programs, written [go](https://golang.org).
A tool for building declarative CLI's over bash scripts, written in [go](https://golang.org).

## Installation

Expand Down
18 changes: 15 additions & 3 deletions cmd/centry/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,30 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) {
} else {
for _, fn := range funcs {
fn := fn
cmd := cmd
namespace := script.CreateFunctionNamespace(cmd.Name)

if fn != cmd.Name && strings.HasPrefix(fn, namespace) == false {
if fn.Name != cmd.Name && strings.HasPrefix(fn.Name, namespace) == false {
continue
}

cmdKey := strings.Replace(fn, script.FunctionNameSplitChar(), " ", -1)
if fn.Description != "" {
cmd.Description = fn.Description
}

if fn.Help != "" {
cmd.Help = fn.Help
}

cmdKey := strings.Replace(fn.Name, script.FunctionNameSplitChar(), " ", -1)
c.Commands[cmdKey] = func() (cli.Command, error) {
return &ScriptCommand{
Context: context,
Log: logger.WithFields(logrus.Fields{}),
GlobalOptions: options,
Command: cmd,
Script: script,
Function: fn,
Function: fn.Name,
}, nil
}

Expand Down Expand Up @@ -149,5 +158,8 @@ func createScript(cmd config.Command, context *Context) shell.Script {
return &shell.BashScript{
BasePath: context.manifest.BasePath,
Path: cmd.Path,
Log: context.log.GetLogger().WithFields(logrus.Fields{
"script": cmd.Path,
}),
}
}
4 changes: 2 additions & 2 deletions cmd/centry/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,15 @@ func (sc *ServeCommand) executeHandler() func(w http.ResponseWriter, r *http.Req
context := NewContext(API, io)

context.commandEnabledFunc = func(cmd config.Command) bool {
if cmd.Annotations == nil || cmd.Annotations[config.APIServeAnnotation] != config.TrueString {
if cmd.Annotations == nil || cmd.Annotations[config.CommandAnnotationAPIServe] != config.TrueString {
return false
}

return true
}

context.optionEnabledFunc = func(opt config.Option) bool {
if opt.Annotations == nil || opt.Annotations[config.APIServeAnnotation] != config.TrueString {
if opt.Annotations == nil || opt.Annotations[config.CommandAnnotationAPIServe] != config.TrueString {
return false
}

Expand Down
8 changes: 8 additions & 0 deletions examples/centry/commands/get.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
#!/usr/bin/env bash

# centry.cmd.description/get:env=Prints environment variables
# centry.cmd.help/get:env=Prints environment variables. Usage: ./stack get env [<...options>]
get:env() {
env | ${SORTED}
}

# centry.cmd.description/get:files=Prints files from the current working directory
# centry.cmd.help/get:files=Prints files from the current working directory. Usage: ./stack get files [<...options>]
get:files() {
ls -ahl | ${SORTED}
}
11 changes: 7 additions & 4 deletions examples/centry/commands/rotate.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
#!/usr/bin/env bash

rotate:secrets () {
# centry.cmd.description/rotate:secrets=Rotate secrets
rotate:secrets() {
echo "rotate:secrets ($*)"
}

rotate:kubernetes:nodes () {
echo "rotate:kubernetes:nodes ($*)"
# centry.cmd.description/rotate:kubernetes:workers=Rotate kubernetes worker nodes
rotate:kubernetes:workers() {
echo "rotate:kubernetes:workers ($*)"
}

rotate:kubernetes:masters () {
# centry.cmd.description/rotate:kubernetes:masters=Rotate kubernetes master nodes
rotate:kubernetes:masters() {
echo "rotate:kubernetes;masters ($*)"
}
56 changes: 54 additions & 2 deletions internal/pkg/config/annotations.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,59 @@
package config

import (
"fmt"
"strings"
)

// TrueString defines a true value
const TrueString string = "true"

// APIServeAnnotation defines an annotation
const APIServeAnnotation string = "centry.api/serve"
// CommandAnnotationAPIServe defines an annotation
const CommandAnnotationAPIServe string = "centry.api/serve"

// CommandAnnotationDescriptionNamespace denines an annotation namespace
const CommandAnnotationDescriptionNamespace string = "centry.cmd.description"

// CommandAnnotationHelpNamespace denines an annotation namespace
const CommandAnnotationHelpNamespace string = "centry.cmd.help"

// CommandAnnotationNamespaces holds a list of command annotation namespaces
var CommandAnnotationNamespaces = []string{
CommandAnnotationDescriptionNamespace,
CommandAnnotationHelpNamespace,
}

// Annotation defines an annotation
type Annotation struct {
Namespace string
Key string
Value string
}

// ParseAnnotation parses text into an annotation
func ParseAnnotation(text string) (*Annotation, error) {
text = strings.TrimSpace(text)

if !strings.HasPrefix(text, "centry") || !strings.Contains(text, "=") || !strings.Contains(text, "/") {
return nil, nil
}

kvp := strings.Split(text, "=")
if len(kvp) != 2 {
return nil, fmt.Errorf("Failed to parse annotation! The text \"%s\" is not a valid annotation", text)
}

nk := kvp[0]
v := kvp[1]

nkkvp := strings.Split(nk, "/")
if len(nkkvp) != 2 {
return nil, fmt.Errorf("Failed to parse annotation! The text \"%s\" is not a valid annotation", text)
}

return &Annotation{
Namespace: nkkvp[0],
Key: nkkvp[1],
Value: v,
}, nil
}
2 changes: 1 addition & 1 deletion internal/pkg/config/schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 67 additions & 2 deletions internal/pkg/shell/bash.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package shell

import (
"bufio"
"fmt"
"os"
"os/exec"
"path"
"strings"

"github.com/kristofferahl/go-centry/internal/pkg/config"
"github.com/kristofferahl/go-centry/internal/pkg/io"
"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -58,8 +61,8 @@ func (s *BashScript) FullPath() string {
return path.Join(s.BasePath, s.Path)
}

// Functions returns the command functions matching the command name
func (s *BashScript) Functions() ([]string, error) {
// FunctionNames returns functions in declared in the script
func (s *BashScript) FunctionNames() ([]string, error) {
callArgs := []string{"-c", fmt.Sprintf("source %s; declare -F", s.FullPath())}

io, buf := io.BufferedCombined()
Expand All @@ -82,6 +85,68 @@ func (s *BashScript) Functions() ([]string, error) {
return functions, nil
}

// FunctionAnnotations returns function annotations in declared in the script
func (s *BashScript) FunctionAnnotations() ([]*config.Annotation, error) {
annotations := make([]*config.Annotation, 0)

file, err := os.Open(s.FullPath())
if err != nil {
return nil, err
}
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
t := scanner.Text()
if strings.HasPrefix(t, "#") {
a, err := config.ParseAnnotation(strings.TrimLeft(t, "#"))
if err != nil {
s.Log.Debugf("%s", err.Error())
} else if a != nil {
annotations = append(annotations, a)
}
}
}

if err := scanner.Err(); err != nil {
return nil, err
}

return annotations, nil
}

// Functions returns the command functions
func (s *BashScript) Functions() ([]*Function, error) {
funcs := make([]*Function, 0)

fnames, err := s.FunctionNames()
if err != nil {
return nil, err
}

annotations, err := s.FunctionAnnotations()
if err != nil {
return nil, err
}

for _, fname := range fnames {
f := &Function{Name: fname}
for _, a := range annotations {
if a.Key == fname {
switch a.Namespace {
case config.CommandAnnotationDescriptionNamespace:
f.Description = a.Value
case config.CommandAnnotationHelpNamespace:
f.Help = a.Value
}
}
}
funcs = append(funcs, f)
}

return funcs, nil
}

// CreateFunctionNamespace returns a namespaced function name
func (s *BashScript) CreateFunctionNamespace(name string) string {
return fmt.Sprintf("%s%s", name, s.FunctionNameSplitChar())
Expand Down
9 changes: 8 additions & 1 deletion internal/pkg/shell/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ type Executable interface {
Run(io io.InputOutput, args []string) error
}

// Function defines a function
type Function struct {
Name string
Description string
Help string
}

// Script defines the interface of a script file
type Script interface {
Language() string
Executable() Executable
FullPath() string
RelativePath() string
Functions() (funcs []string, err error)
Functions() (funcs []*Function, err error)
CreateFunctionNamespace(name string) string
FunctionNameSplitChar() string
}

0 comments on commit 8bb3c37

Please sign in to comment.