Skip to content

Commit

Permalink
(#33) Experimental command plugins
Browse files Browse the repository at this point in the history
This introduce an experimental way to extend one fisk
command with another. Fisk commands will have --fisk-introspection
that emits an ApplicationModel.

Other fisk apps can import this using the ExternalPluginJSON()
function. Help output and more will include the full command
model and on execution it will call the command to do
the actual work

Ideally plugins will be able to define global flags that clash
with the command they plug into, in that case the value of the
global flag will be passed to the app but for now that's left
for a followup PR

Signed-off-by: R.I.Pienaar <[email protected]>
  • Loading branch information
ripienaar committed Apr 25, 2023
1 parent f95ab53 commit c3640d2
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 45 deletions.
1 change: 1 addition & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func New(name, help string) *Application {
a.Flag("completion-bash", "Output possible completions for the given args.").Hidden().UnNegatableBoolVar(&a.completion)
a.Flag("completion-script-bash", "Generate completion script for bash.").Hidden().PreAction(a.generateBashCompletionScript).UnNegatableBool()
a.Flag("completion-script-zsh", "Generate completion script for ZSH.").Hidden().PreAction(a.generateZSHCompletionScript).UnNegatableBool()
a.Flag("fisk-introspect", "Introspect the application model").Hidden().Action(a.introspectAction).UnNegatableBool()

return a
}
Expand Down
167 changes: 167 additions & 0 deletions app_plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package fisk

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
)

type pluginDelegator struct {
command string
flags map[string]*string
boolFlags map[string]*bool
unNegBoolFlags map[string]*bool
args map[string]*string
}

func (a *Application) introspectModel() *ApplicationModel {
model := a.Model()
var nf []*FlagModel
for _, flag := range model.Flags {
if flag.Name == "help" || strings.HasPrefix(flag.Name, "help-") || strings.HasPrefix(flag.Name, "completion-") || strings.HasPrefix(flag.Name, "fisk-") {
continue
}

nf = append(nf, flag)
}
model.Flags = nf

var nc []*CmdModel
for _, cmd := range model.Commands {
if cmd.Name == "help" {
continue
}
nc = append(nc, cmd)
}
model.Commands = nc

return model
}

func (a *Application) introspectAction(c *ParseContext) error {
a.Writer(os.Stdout)

j, err := json.Marshal(a.introspectModel())
if err != nil {
return err
}

fmt.Println(string(j))
return nil
}

func (c *CmdClause) addArgsFromModel(model *ArgGroupModel) {
for _, arg := range model.Args {
a := c.Arg(arg.Name, arg.Help)
a.placeholder = arg.PlaceHolder
a.required = arg.Required
a.hidden = arg.Hidden
a.defaultValues = arg.Default
a.envar = arg.Envar
c.pluginDelegator.args[arg.Name] = a.String()
}
}

func (c *CmdClause) addFlagsFromModel(model *FlagGroupModel) {
for _, flag := range model.Flags {
f := c.Flag(flag.Name, flag.Help)
f.shorthand = flag.Short
f.defaultValues = flag.Default
f.envar = flag.Envar
f.placeholder = flag.PlaceHolder
f.required = flag.Required
f.hidden = flag.Hidden

switch {
case flag.Boolean && flag.Negatable:
c.pluginDelegator.boolFlags[flag.Name] = f.Bool()

case flag.Boolean:
c.pluginDelegator.unNegBoolFlags[flag.Name] = f.UnNegatableBool()

default:
c.pluginDelegator.flags[flag.Name] = f.String()
}
}
}

func (c *CmdClause) addCommandsFromModel(model *CmdGroupModel) {
for _, cmd := range model.Commands {
cm := c.Command(cmd.Name, cmd.Help)
cm.pluginDelegator = c.pluginDelegator
cm.aliases = cmd.Aliases
cm.helpLong = cmd.HelpLong
cm.hidden = cmd.Hidden
cm.isDefault = cmd.Default
cm.addArgsFromModel(cmd.ArgGroupModel)
cm.addFlagsFromModel(cmd.FlagGroupModel)
cm.addCommandsFromModel(cmd.CmdGroupModel)
cm.Action(func(pc *ParseContext) error {
var args []string
for k, v := range cm.pluginDelegator.args {
if v != nil {
args = append(args, fmt.Sprintf("%s=%s", k, *v))
}
}

for k, v := range cm.pluginDelegator.flags {
if v != nil {
args = append(args, fmt.Sprintf("--%s=%s", k, *v))
}
}

for k, v := range cm.pluginDelegator.boolFlags {
if *v {
args = append(args, fmt.Sprintf("--%s", k))
} else {
args = append(args, fmt.Sprintf("--no-%s", k))
}
}

for k, v := range cm.pluginDelegator.unNegBoolFlags {
if *v {
args = append(args, fmt.Sprintf("--%s", k))
}
}

return exec.Command(cm.pluginDelegator.command, args...).Run()
})
}
}

func (a *Application) registerPluginModel(command string, model *ApplicationModel) (*CmdClause, error) {
cmd := a.Command(model.Name, model.Help)
cmd.pluginDelegator = &pluginDelegator{
command: command,
flags: map[string]*string{},
args: map[string]*string{},
boolFlags: map[string]*bool{},
unNegBoolFlags: map[string]*bool{},
}

cmd.addArgsFromModel(model.ArgGroupModel)
cmd.addFlagsFromModel(model.FlagGroupModel)
cmd.addCommandsFromModel(model.CmdGroupModel)

return cmd, nil
}

// ExternalPluginJSON extends the application using a plugin and a model describing the application
func (a *Application) ExternalPluginJSON(command string, model json.RawMessage) (*CmdClause, error) {
var m ApplicationModel
err := json.Unmarshal(model, &m)
if err != nil {
return nil, err
}

if m.Name == "" {
return nil, fmt.Errorf("plugin declared no name")
}
if m.Help == "" {
return nil, fmt.Errorf("plugin declared no help")
}

return a.registerPluginModel(command, &m)
}
19 changes: 10 additions & 9 deletions cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,15 +225,16 @@ type CmdClauseValidator func(*CmdClause) error
// and either subcommands or positional arguments.
type CmdClause struct {
cmdMixin
app *Application
name string
aliases []string
help string
helpLong string
isDefault bool
validator CmdClauseValidator
hidden bool
completionAlts []string
app *Application
name string
aliases []string
help string
helpLong string
isDefault bool
validator CmdClauseValidator
hidden bool
completionAlts []string
pluginDelegator *pluginDelegator
}

func newCommand(app *Application, name, help string) *CmdClause {
Expand Down
82 changes: 46 additions & 36 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var (
)

type FlagGroupModel struct {
Flags []*FlagModel
Flags []*FlagModel `json:"flags"`
}

func (f *FlagGroupModel) FlagSummary() string {
Expand Down Expand Up @@ -51,15 +51,20 @@ func (f *FlagGroupModel) FlagSummary() string {
}

type FlagModel struct {
Name string
Help string
Short rune
Default []string
Envar string
PlaceHolder string
Required bool
Hidden bool
Value Value
Name string `json:"name"`
Help string `json:"help"`
Short rune `json:"short"`
Default []string `json:"default"`
Envar string `json:"envar"`
PlaceHolder string `json:"place_holder"`
Required bool `json:"required"`
Hidden bool `json:"hidden"`

// used by plugin model
Boolean bool `json:"boolean"`
Negatable bool `json:"negatable"`

Value Value `json:"-"`
}

func (f *FlagModel) String() string {
Expand Down Expand Up @@ -103,7 +108,7 @@ func (f *FlagModel) HelpWithEnvar() string {
}

type ArgGroupModel struct {
Args []*ArgModel
Args []*ArgModel `json:"args"`
}

func (a *ArgGroupModel) ArgSummary() string {
Expand Down Expand Up @@ -134,14 +139,14 @@ func (a *ArgModel) HelpWithEnvar() string {
}

type ArgModel struct {
Name string
Help string
Default []string
Envar string
PlaceHolder string
Required bool
Hidden bool
Value Value
Name string `json:"name"`
Help string `json:"help"`
Default []string `json:"default"`
Envar string `json:"envar"`
PlaceHolder string `json:"place_holder"`
Required bool `json:"required"`
Hidden bool `json:"hidden"`
Value Value `json:"-"`
}

func (a *ArgModel) String() string {
Expand All @@ -153,7 +158,7 @@ func (a *ArgModel) String() string {
}

type CmdGroupModel struct {
Commands []*CmdModel
Commands []*CmdModel `json:"commands"`
}

func (c *CmdGroupModel) FlattenedCommands() (out []*CmdModel) {
Expand All @@ -167,14 +172,14 @@ func (c *CmdGroupModel) FlattenedCommands() (out []*CmdModel) {
}

type CmdModel struct {
Name string
Aliases []string
Help string
HelpLong string
FullCommand string
Depth int
Hidden bool
Default bool
Name string `json:"name"`
Aliases []string `json:"aliases"`
Help string `json:"help"`
HelpLong string `json:"help_long"`
FullCommand string `json:"-"`
Depth int `json:"-"`
Hidden bool `json:"hidden"`
Default bool `json:"default"`

*FlagGroupModel
*ArgGroupModel
Expand All @@ -186,13 +191,13 @@ func (c *CmdModel) String() string {
}

type ApplicationModel struct {
Name string
Help string
Cheat string
Version string
Author string
Cheats map[string]string
CheatTags []string
Name string `json:"name"`
Help string `json:"help"`
Cheat string `json:"cheat"`
Version string `json:"version"`
Author string `json:"author"`
Cheats map[string]string `json:"cheats"`
CheatTags []string `json:"cheat_tags"`

*ArgGroupModel
*CmdGroupModel
Expand Down Expand Up @@ -243,7 +248,7 @@ func (f *flagGroup) Model() *FlagGroupModel {
}

func (f *FlagClause) Model() *FlagModel {
return &FlagModel{
m := &FlagModel{
Name: f.name,
Help: f.help,
Short: f.shorthand,
Expand All @@ -254,6 +259,11 @@ func (f *FlagClause) Model() *FlagModel {
Hidden: f.hidden,
Value: f.value,
}

m.Boolean = m.IsBoolFlag()
m.Negatable = m.IsNegatable()

return m
}

func (c *cmdGroup) Model() *CmdGroupModel {
Expand Down

0 comments on commit c3640d2

Please sign in to comment.