Skip to content

Commit

Permalink
feat: Add support for Hidden and Private Commands (#64)
Browse files Browse the repository at this point in the history
You can mark a command as hidden with . prefix :

    .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 ! prefix:

    !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

misc: Code fixes(?) that hopefully don't break anything
  • Loading branch information
TekWizely committed Dec 4, 2022
1 parent 7200531 commit ef32451
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 39 deletions.
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ In run, the entire script is executed within a single sub-shell.
- [Invoking Other Commands & Runfiles](#invoking-other-commands--runfiles)
- [RUN / RUN.BEFORE / RUN.AFTER Actions](#run--runbefore--runafter-actions)
- [.RUN / .RUNFILE Attributes](#run--runfile-attributes)
- [Hidden / Private Commands](#hidden--private-commands)
- [Hidden Commands](#hidden-commands)
- [Private Commands](#private-commands)
- [Script Shells](#script-shells)
- [Per-Command Shell Config](#per-command-shell-config)
- [Global Default Shell Config](#global-default-shell-config)
Expand Down Expand Up @@ -1441,6 +1444,88 @@ $ run test
Hello, World
```

-----------------------------
### Hidden / Private Commands

#### Hidden Commands

You can mark a command as _Hidden_ using a leading `.`:

_hidden command example_
```
##
# Prints 'Hello, Newman', then 'Goodbye, now'
# RUN hello Newman
test:
echo "Goodbye, now"
## Hello command is hidden
.hello:
echo "Hello, ${1:-world}"
```

Hidden commands don't show up when listing commands:

_list commands_
```
$ run list
Commands:
...
test Prints 'Hello, Newman', then 'Goodbye, now'
```

But they can still be invoked by using their full name, with `.`:

_run hidden command_
```
$ run .hello
Hello, world
```

#### Private Commands

You can mark a command as _Private_ using a leading `!`:

_private command example_
```
##
# Prints 'Hello, Newman', then 'Goodbye, now'
# RUN hello Newman
test:
echo "Goodbye, now"
## Hello command is private
!hello:
echo "Hello, ${1:-world}"
```

Private commands don't show up when listing commands:

_list commands_
```
$ run list
Commands:
...
test Prints 'Hello, Newman', then 'Goodbye, now'
```

And they cannot be invoked from outside the Runfile:

_try to run private command_
```
$ run hello
run: command not found: hello
$ run '!hello'
run: command not found: !hello
```

-----------------
### Script Shells

Expand Down
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
20 changes: 16 additions & 4 deletions internal/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,24 @@ func LexMain(_ *LexContext, l *lexer.Lexer) LexFn {
case matchOneOrMore(l, isSpaceOrTab):
l.Clear() // Discard
// Newline
//
case matchNewline(l):
l.EmitType(TokenNewline)
// DotID
//
case matchDotID(l):
l.EmitToken(TokenDotID)
// Keyword / ID / DashID
// Keyword / ID / [.!]? DashID
//
case matchDashID(l):
case matchCommandDefID(l):
name := strings.ToUpper(l.PeekToken())
switch {
case isMainToken(name):
l.EmitType(mainTokens[name])
case strings.HasPrefix(name, "."): // Can only match at front
l.EmitToken(TokenCommandDefID)
case strings.HasPrefix(name, "!"): // Can only match at front
l.EmitToken(TokenCommandDefID)
case strings.ContainsRune(name, runeDash):
l.EmitToken(TokenDashID)
default:
Expand Down Expand Up @@ -1012,18 +1017,25 @@ func matchDotID(l *lexer.Lexer) (ok bool) {
return false
}

// matchID
// matchID matches [a-zA-Z_] [a-zA-Z0-9_]*
//
func matchID(l *lexer.Lexer) bool {
return matchOne(l, isAlphaUnder) && matchZeroOrMore(l, isAlphaNumUnder)
}

// matchDashID
// matchDashID matches [a-zA-Z_] [a-zA-Z0-9_-]*
//
func matchDashID(l *lexer.Lexer) bool {
return matchOne(l, isAlphaUnder) && matchZeroOrMore(l, isAlphaNumUnderDash)
}

// matchCommandDefID matches [.!]? DASH_ID
// Used when defining a command, leading [.!] not needed when referencing it later
//
func matchCommandDefID(l *lexer.Lexer) bool {
return matchZeroOrOne(l, isDotOrBang) && matchDashID(l)
}

// matchConfigAttrID matches [a-zA-Z] [a-zA-Z0-9_]* ( \. [a-zA-Z0-9_]+ )*
//
func matchConfigAttrID(l *lexer.Lexer) (ok bool) {
Expand Down
4 changes: 2 additions & 2 deletions internal/lexer/runereader.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package lexer

import "io"

// readerIgnoreCR wraps a RuneReader, filtering errOut '\r'.
// readerIgnoreCR wraps a RuneReader, filtering out '\r'.
// Useful for input sources that use '\r'+'\n' for end-of-line.
//
type readerIgnoreCR struct {
Expand All @@ -19,7 +19,7 @@ func newReaderIgnoreCR(r io.RuneReader) io.RuneReader {
//
func (c *readerIgnoreCR) ReadRune() (r rune, size int, err error) {
r, size, err = c.r.ReadRune()
if size == 1 && r == '\r' {
for size == 1 && r == '\r' {
r, size, err = c.r.ReadRune()
}
return
Expand Down
5 changes: 5 additions & 0 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 Down Expand Up @@ -102,6 +103,10 @@ func isHash(r rune) bool {
return r == runeHash
}

func isDotOrBang(r rune) bool {
return r == runeDot || r == runeBang
}

// isSpaceOrTab matches tab or space
//
func isSpaceOrTab(r rune) bool {
Expand Down
26 changes: 14 additions & 12 deletions internal/lexer/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,34 @@ const (
TokenID
TokenDotID
TokenDashID
TokenCommandDefID

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
46 changes: 30 additions & 16 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,18 +257,18 @@ 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
line int
)
if flags, name, shell, line, ok = tryMatchCmdHeaderWithShell(ctx, p); !ok {
return false
}
if cmdConfig == nil {
cmdConfig = &ast.CmdConfig{}
}

if name, shell, line, ok = tryMatchCmdHeaderWithShell(ctx, p); !ok {
return false
}
ctx.pushLexFn(ctx.l.Fn)
if tryPeekType(p, lexer.TokenColon) {
p.Next()
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,49 @@ 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.TokenDotID) && !strings.ContainsRune(strings.TrimPrefix(p.Peek(1).Value(), "."), '.')) ||
tryPeekType(p, lexer.TokenCommandDefID) ||
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
}
// Name + Line
//
var name string
var line int

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

name := t.Value()
line := t.Line()
flags := config.CmdFlags(0)
// Hidden / Private
//
if strings.HasPrefix(name, ".") {
name = strings.TrimPrefix(name, ".")
flags |= config.FlagHidden
} else if strings.HasPrefix(name, "!") {
name = strings.TrimPrefix(name, "!")
flags |= config.FlagPrivate
}
// Shell
//
shell := ""
Expand All @@ -810,7 +824,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
Loading

0 comments on commit ef32451

Please sign in to comment.