Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for Hidden and Private Commands #64

Merged
merged 3 commits into from
Dec 4, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: Add support for Hidden and Private Commands
You can mark a command as hidden with '@':

   @hidden:
       echo "This command is hidden"

* Can be invoked by command config RUN actions
  * RUN actions do not need to use '@'
* Will not show in CLI list of available commands
* Can still be invoked via CLI using '@name'

You can mark a command as private with '!':

   !private:
       echo "This command is private"

* Can only be invoked by command config RUN actions
  * RUN actions do not need to use '!'
* Will not show in CLI list of available commands
* Cannot be invoked from CLI
  • Loading branch information
TekWizely committed Nov 28, 2022
commit 0a9532c1b93ce802a35dbe3d789b46ec2f9931ba
2 changes: 2 additions & 0 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ func (a *ScopeDParenString) Apply(s *runfile.Scope) string {
// Cmd wraps a parsed command.
//
type Cmd struct {
Flags config.CmdFlags
Name string
Config *CmdConfig
Script []string
Expand Down Expand Up @@ -408,6 +409,7 @@ func (a *Cmd) GetCmd(r *runfile.Runfile) *runfile.RunCmd {
//
func (a *Cmd) GetCmdEnv(r *runfile.Runfile, env map[string]string) *runfile.RunCmd {
cmd := &runfile.RunCmd{
Flags: a.Flags,
Name: a.Name,
Scope: runfile.NewScope(),
Script: a.Script,
Expand Down
26 changes: 26 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,35 @@ import (
"runtime"
)

// CmdFlags captures various options for a command
type CmdFlags int

const (
// FlagHidden marks a command as Hidden
//
FlagHidden CmdFlags = 1 << iota

// FlagPrivate marks a command as Private
//
FlagPrivate
)

// Hidden returns true if flag represents Hidden
//
func (c CmdFlags) Hidden() bool {
return c&FlagHidden > 0
}

// Private returns true if flag represents Private
//
func (c CmdFlags) Private() bool {
return c&FlagPrivate > 0
}

// Command is an abstraction for a command, allowing us to mix runfile commands and custom comments (help, list, etc).
//
type Command struct {
Flags CmdFlags
Name string
Title string
Help func()
Expand Down
5 changes: 3 additions & 2 deletions internal/lexer/runes.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
// NOTE: You probably want matchNewline()
// runeNewline = '\n'
// runeReturn = '\r'
runeAt = '@'
runeBang = '!'
runeHash = '#'
runeDollar = '$'
Expand All @@ -41,8 +42,8 @@ const (
// Single-Rune tokens
//
var (
singleRunes = []byte{runeColon, runeEquals, runeLParen, runeRParen, runeLBrace, runeRBrace, runeLBracket, runeRBracket}
singleTokens = []token.Type{TokenColon, TokenEquals, TokenLParen, TokenRParen, TokenLBrace, TokenRBrace, TokenLBracket, TokenRBracket}
singleRunes = []byte{runeAt, runeBang, runeColon, runeEquals, runeLParen, runeRParen, runeLBrace, runeRBrace, runeLBracket, runeRBracket}
singleTokens = []token.Type{TokenAt, TokenBang, TokenColon, TokenEquals, TokenLParen, TokenRParen, TokenLBrace, TokenRBrace, TokenLBracket, TokenRBracket}
)
var mainTokens = map[string]token.Type{
"COMMAND": TokenCommand,
Expand Down
25 changes: 13 additions & 12 deletions internal/lexer/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,32 @@ const (
TokenDotID
TokenDashID

TokenBang
TokenColon
TokenComma
TokenAt // '@'
TokenBang // '!'
TokenColon // ':'
TokenComma // ','
TokenEquals // '=' | ':='
TokenQMarkEquals // ?=

TokenDQuote
TokenDQuote // '"'
TokenDQStringStart

TokenSQuote
TokenSQuote // "'"
TokenSQStringStart

TokenRunes
TokenEscapeSequence

TokenDollar
TokenDollar // '$'
TokenVarRefStart
TokenSubCmdStart

TokenLParen
TokenRParen
TokenLBrace
TokenRBrace
TokenLBracket
TokenRBracket
TokenLParen // '('
TokenRParen // ')'
TokenLBrace // '{'
TokenRBrace // '}'
TokenLBracket // '['
TokenRBracket // ']'

TokenParenStringStart // '( '
TokenParenStringEnd // ' )'
Expand Down
38 changes: 25 additions & 13 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ func parseMain(ctx *parseContext, p *parser.Parser) parseFn {
//
func tryMatchCmd(ctx *parseContext, p *parser.Parser, cmdConfig *ast.CmdConfig) bool {
var (
flags config.CmdFlags
name string
shell string
ok bool
Expand All @@ -265,8 +266,7 @@ func tryMatchCmd(ctx *parseContext, p *parser.Parser, cmdConfig *ast.CmdConfig)
if cmdConfig == nil {
cmdConfig = &ast.CmdConfig{}
}

if name, shell, line, ok = tryMatchCmdHeaderWithShell(ctx, p); !ok {
if flags, name, shell, line, ok = tryMatchCmdHeaderWithShell(ctx, p); !ok {
return false
}
ctx.pushLexFn(ctx.l.Fn)
Expand All @@ -291,6 +291,7 @@ func tryMatchCmd(ctx *parseContext, p *parser.Parser, cmdConfig *ast.CmdConfig)
panic(parseError(p, "command '"+name+"' contains an empty script."))
}
ctx.ast.Add(&ast.Cmd{
Flags: flags,
Name: name,
Config: cmdConfig,
Script: script,
Expand Down Expand Up @@ -764,36 +765,47 @@ func expectTestString(_ *parseContext, p *parser.Parser) ast.ScopeValueNode {
panic(parseError(p, "expecting test string end token"))
}

// tryMatchCmdHeaderWithShell matches [ [ 'CMD' ] DASH_ID ( '(' ID ')' )? ( ':' | '{' ) ]
// tryMatchCmdHeaderWithShell matches [ [ 'CMD' ] [ '@' ] DASH_ID ( '(' ID ')' )? ( ':' | '{' ) ]
//
func tryMatchCmdHeaderWithShell(ctx *parseContext, p *parser.Parser) (string, string, int, bool) {
func tryMatchCmdHeaderWithShell(ctx *parseContext, p *parser.Parser) (config.CmdFlags, string, string, int, bool) {
expectCommand := tryPeekType(p, lexer.TokenCommand)
if expectCommand {
expectTokenType(p, lexer.TokenCommand, "expecting TokenCommand")
} else {
expectCommand =
tryPeekType(p, lexer.TokenDashID) ||
tryPeekType(p, lexer.TokenAt) ||
tryPeekType(p, lexer.TokenBang) ||
tryPeekType(p, lexer.TokenDashID) ||
tryPeekTypes(p, lexer.TokenID, lexer.TokenColon) ||
tryPeekTypes(p, lexer.TokenID, lexer.TokenLParen) ||
tryPeekTypes(p, lexer.TokenID, lexer.TokenLBrace)
}
if !expectCommand {
return "", "", -1, false
return 0, "", "", -1, false
}
// Hidden (@)
//
var flags config.CmdFlags = 0
if tryPeekType(p, lexer.TokenAt) {
expectTokenType(p, lexer.TokenAt, "expecting TokenAt ('@')")
flags |= config.FlagHidden
} else
// Private (!)
//
if tryPeekType(p, lexer.TokenBang) {
expectTokenType(p, lexer.TokenBang, "expecting TokenBang ('!')")
flags |= config.FlagPrivate
}
// Name + Line
//
var name string
var line int

var t token.Token
if tryPeekType(p, lexer.TokenDashID) {
t = expectTokenType(p, lexer.TokenDashID, "expecting command name")
} else {
t = expectTokenType(p, lexer.TokenID, "expecting command name")
}
name = t.Value()
line = t.Line()

name := t.Value()
line := t.Line()
// Shell
//
shell := ""
Expand All @@ -810,7 +822,7 @@ func tryMatchCmdHeaderWithShell(ctx *parseContext, p *parser.Parser) (string, st
panic(parseError(p, "expecting TokenColon (':') or TokenLBrace ('{')"))
}
p.Clear()
return name, shell, line, true
return flags, name, shell, line, true
}

// expectCmdScript
Expand Down
15 changes: 12 additions & 3 deletions internal/runfile/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,12 +405,14 @@ func ListCommands() {
_, _ = fmt.Fprintln(config.ErrOut, "Commands:")
padLen := 0
for _, cmd := range config.CommandList {
if len(cmd.Name) > padLen {
if !cmd.Flags.Private() && !cmd.Flags.Hidden() && len(cmd.Name) > padLen {
padLen = len(cmd.Name)
}
}
for _, cmd := range config.CommandList {
_, _ = fmt.Fprintf(config.ErrOut, " %s%s %s\n", cmd.Name, strings.Repeat(" ", padLen-len(cmd.Name)), cmd.Title)
if !cmd.Flags.Private() && !cmd.Flags.Hidden() {
_, _ = fmt.Fprintf(config.ErrOut, " %s%s %s\n", cmd.Name, strings.Repeat(" ", padLen-len(cmd.Name)), cmd.Title)
}
}
}

Expand All @@ -421,13 +423,20 @@ func ListCommands() {
//
func RunHelp() int {
var cmdName string
var cmdShowHidden bool
if len(os.Args) > 0 {
cmdName = os.Args[0]
os.Args = os.Args[1:]
}
if len(cmdName) > 0 {
// Show Hidden?
//
if strings.HasPrefix(cmdName, "@") {
cmdName = strings.TrimPrefix(cmdName, "@")
cmdShowHidden = true
}
cmdName = strings.ToLower(cmdName)
if c, ok := config.CommandMap[cmdName]; ok {
if c, ok := config.CommandMap[cmdName]; ok && !c.Flags.Private() && (!c.Flags.Hidden() || cmdShowHidden) {
c.Help()
return 0
}
Expand Down
1 change: 1 addition & 0 deletions internal/runfile/runfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type RunCmdConfig struct {
// RunCmd captures a command.
//
type RunCmd struct {
Flags config.CmdFlags
Name string
Config *RunCmdConfig
Scope *Scope
Expand Down
17 changes: 15 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ func main() {
runCommandsByName := make(map[string]*runfile.RunCmd)
for _, cmdProvider := range rf.Cmds {
newRunCommand := cmdProvider.GetCmd(rf)
newRunCommandFlags := newRunCommand.Flags // May override later
newRunCommandName := newRunCommand.Name // Un-normalized name used for help
name := strings.ToLower(newRunCommand.Name) // normalize
// Keep track of which Runfile a command is registered in, to avoid dupes from same file
Expand Down Expand Up @@ -294,7 +295,7 @@ func main() {
log.Printf("NOTICE: %s:%d command %s overrides command %s defined in %s:%d", newRunCommand.Runfile, newRunCommand.Line, newRunCommand.Name, oldRunCommand.Name, oldRunCommand.Runfile, oldRunCommand.Line)
}
}
// Remove old command, taking note of command name and CommandList index, which will be re-used
// Remove old command, taking note of command flags, name and CommandList index, which will be re-used
//
delete(config.CommandMap, name) // Technically not needed but feels cleaner
for i := range config.CommandList {
Expand All @@ -303,6 +304,9 @@ func main() {
break
}
}
// For flags, first registered command wins
//
newRunCommandFlags = firstRunCommand.Flags
// For help display, first registered command wins
//
newRunCommandName = firstRunCommand.Name
Expand All @@ -317,6 +321,7 @@ func main() {
runCommandsByNameForFile[name] = newRunCommand
runCommandsByName[name] = newRunCommand
cmd := &config.Command{
Flags: newRunCommandFlags,
Name: newRunCommandName,
Title: newRunCommand.Title(),
Help: func(c *runfile.RunCmd) func() { return func() { runfile.ShowCmdHelp(c) } }(newRunCommand),
Expand Down Expand Up @@ -345,6 +350,7 @@ func main() {
// Determine which command to run
//
var cmdName string
var cmdShowHidden bool
if config.MainMode {
// In main mode, we defer parsing args to the command
//
Expand All @@ -362,6 +368,12 @@ func main() {
}
if len(os.Args) > 0 {
cmdName, os.Args = os.Args[0], os.Args[1:]
// Show Hidden?
//
if strings.HasPrefix(cmdName, "@") {
cmdName = strings.TrimPrefix(cmdName, "@")
cmdShowHidden = true
}
} else {
//
// Default (no command) action
Expand All @@ -384,11 +396,12 @@ func main() {
}
}
// Run command, if present, else error
// Hidden == not present unless command invoked with `@NAME`
//
cmdName = strings.ToLower(cmdName) // normalize
var cmd *config.Command
var ok bool
if cmd, ok = config.CommandMap[cmdName]; !ok {
if cmd, ok = config.CommandMap[cmdName]; !ok || cmd.Flags.Private() || (cmd.Flags.Hidden() && !cmdShowHidden) {
log.Printf("command not found: %s", cmdName)
runfile.ListCommands()
showUsageHint()
Expand Down