-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The main command is now `run`. Adds `help` and `version` commands. Closes #33.
- Loading branch information
Showing
7 changed files
with
387 additions
and
121 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
|
||
"github.com/anaminus/but" | ||
"github.com/anaminus/rbxmk/rbxmk/cmds" | ||
) | ||
|
||
const globalUsage = `rbxmk is a tool for managing Roblox projects. | ||
Usage: | ||
rbxmk <command> [options] | ||
Commands: | ||
%s | ||
Run "rbxmk help <command>" for more information about a command. | ||
` | ||
|
||
func init() { | ||
Commands.Register(cmds.Command{ | ||
Name: "help", | ||
Summary: "Display help.", | ||
Usage: `rbxmk help [command]`, | ||
Description: ` | ||
Displays help for a command, or general help if no command is given.`, | ||
Func: HelpCommand, | ||
}) | ||
} | ||
|
||
// HelpCommand executes the help command. | ||
func HelpCommand(flags cmds.Flags) { | ||
name, ok := flags.ShiftArg() | ||
but.IfFatal(flags.Parse(), "parse flags") | ||
if ok { | ||
if Commands.Has(name) { | ||
cmd := Commands.Get(name) | ||
flags.UsageOf(cmd)() | ||
return | ||
} | ||
but.Logf("unknown command %q\n\n", name) | ||
} | ||
list := Commands.List() | ||
width := 0 | ||
for _, cmd := range list { | ||
if len(cmd.Name) > width { | ||
width = len(cmd.Name) | ||
} | ||
} | ||
var buf bytes.Buffer | ||
for _, cmd := range list { | ||
fmt.Fprintf(&buf, "\t%-*s %s\n", width, cmd.Name, cmd.Summary) | ||
} | ||
but.Logf(globalUsage, buf.String()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package main | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"strconv" | ||
|
||
"github.com/anaminus/but" | ||
lua "github.com/anaminus/gopher-lua" | ||
"github.com/anaminus/rbxmk" | ||
"github.com/anaminus/rbxmk/formats" | ||
"github.com/anaminus/rbxmk/library" | ||
"github.com/anaminus/rbxmk/rbxmk/cmds" | ||
"github.com/anaminus/rbxmk/sources" | ||
) | ||
|
||
// shortenPath transforms the given path so that it is relative to the working | ||
// directory. Returns the original path if that fails. | ||
func shortenPath(filename string) string { | ||
if wd, err := os.Getwd(); err == nil { | ||
if abs, err := filepath.Abs(filename); err == nil { | ||
if r, err := filepath.Rel(wd, abs); err == nil { | ||
filename = r | ||
} | ||
} | ||
} | ||
return filename | ||
} | ||
|
||
// ParseLuaValue parses a string into a Lua value. Numbers, bools, and nil are | ||
// parsed into their respective types, and any other value is interpreted as a | ||
// string. | ||
func ParseLuaValue(s string) lua.LValue { | ||
switch s { | ||
case "true": | ||
return lua.LTrue | ||
case "false": | ||
return lua.LFalse | ||
case "nil": | ||
return lua.LNil | ||
} | ||
if number, err := strconv.ParseFloat(s, 64); err == nil { | ||
return lua.LNumber(number) | ||
} | ||
return lua.LString(s) | ||
} | ||
|
||
func init() { | ||
Commands.Register(cmds.Command{ | ||
Name: "run", | ||
Summary: "Execute a script.", | ||
Usage: `rbxmk run [ FILE ] [ ...VALUE ]`, | ||
Description: ` | ||
Receives a file to be executed as a Lua script. If "-" is given, then the script | ||
will be read from stdin instead. | ||
Remaining arguments are Lua values to be passed to the file. Numbers, bools, and | ||
nil are parsed into their respective types in Lua, and any other value is | ||
interpreted as a string. Within the script, these arguments can be received from | ||
the ... operator.`, | ||
Func: RunCommand, | ||
}) | ||
} | ||
|
||
// RunCommand executes the run command. | ||
func RunCommand(flags cmds.Flags) { | ||
but.IfFatal(Run(flags, nil)) | ||
} | ||
|
||
// Run is the entrypoint to the command for running scripts. init runs after the | ||
// World envrionment is fully initialized and arguments have been pushed, and | ||
// before the script runs. | ||
func Run(flags cmds.Flags, init func(rbxmk.State)) error { | ||
// Parse flags. | ||
but.IfFatal(flags.Parse(), "parse flags") | ||
args := flags.Args() | ||
if len(args) == 0 { | ||
flags.Usage() | ||
return nil | ||
} | ||
file := args[0] | ||
args = args[1:] | ||
|
||
// Initialize world. | ||
world := rbxmk.NewWorld(lua.NewState(lua.Options{ | ||
SkipOpenLibs: true, | ||
IncludeGoStackTrace: false, | ||
})) | ||
for _, f := range formats.All() { | ||
world.RegisterFormat(f()) | ||
} | ||
for _, s := range sources.All() { | ||
world.RegisterSource(s()) | ||
} | ||
for _, lib := range library.All() { | ||
if err := world.Open(lib); err != nil { | ||
but.Fatal(err) | ||
} | ||
} | ||
|
||
world.State().SetGlobal("_RBXMK_VERSION", lua.LString(Version)) | ||
|
||
// Add script arguments. | ||
for _, arg := range args { | ||
world.State().Push(ParseLuaValue(arg)) | ||
} | ||
|
||
if init != nil { | ||
init(rbxmk.State{World: world, L: world.State()}) | ||
} | ||
|
||
// Run stdin as script. | ||
if file == "-" { | ||
return world.DoFileHandle(flags.Stdin, len(args)) | ||
} | ||
|
||
// Run file as script. | ||
filename := shortenPath(filepath.Clean(file)) | ||
return world.DoFile(filename, len(args)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package main | ||
|
||
import ( | ||
"github.com/anaminus/but" | ||
"github.com/anaminus/rbxmk/rbxmk/cmds" | ||
) | ||
|
||
func init() { | ||
Commands.Register(cmds.Command{ | ||
Name: "version", | ||
Summary: "Display the version.", | ||
Usage: `rbxmk version`, | ||
Description: ` | ||
Displays the current version of rbxmk.`, | ||
Func: VersionCommand, | ||
}) | ||
} | ||
|
||
// VersionCommand executes the version command. | ||
func VersionCommand(flags cmds.Flags) { | ||
but.IfFatal(flags.Parse(), "parse flags") | ||
but.Log(Version) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
package cmds | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"os" | ||
"sort" | ||
"strings" | ||
) | ||
|
||
// FileReader represents a file that can be read from, and includes file | ||
// information. | ||
type FileReader interface { | ||
Name() string | ||
Stat() (os.FileInfo, error) | ||
Read([]byte) (int, error) | ||
} | ||
|
||
// FileWriter represents a file that can be written to, and includes file | ||
// information. | ||
type FileWriter interface { | ||
Name() string | ||
Stat() (os.FileInfo, error) | ||
Write([]byte) (int, error) | ||
} | ||
|
||
// Flags bundles a FlagSet with arguments and file descriptors. | ||
type Flags struct { | ||
*flag.FlagSet | ||
Stdin FileReader | ||
Stdout FileWriter | ||
Stderr FileWriter | ||
|
||
args []string | ||
} | ||
|
||
// NewFlags returns an initialized Flags. name is the name of the program. args | ||
// are the arguments to be parsed with the embedded FlagSet. The Flags' file | ||
// descriptors are set to the standard file descriptors. | ||
func NewFlags(name string, args []string) Flags { | ||
return Flags{ | ||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError), | ||
args: args, | ||
Stdin: os.Stdin, | ||
Stdout: os.Stdout, | ||
Stderr: os.Stderr, | ||
} | ||
} | ||
|
||
// ShiftArg attempts to return the first argument of Flags. If successful, the | ||
// first argument is removed, shifting down the remaining arguments. | ||
func (f *Flags) ShiftArg() (arg string, ok bool) { | ||
if len(f.args) == 0 { | ||
return "", false | ||
} | ||
arg = f.args[0] | ||
f.args = f.args[1:] | ||
return arg, true | ||
} | ||
|
||
// formatDesc formats a command description for readability. | ||
func formatDesc(s string) string { | ||
s = strings.TrimSpace(s) | ||
//TODO: Wrap to 80 characters. | ||
return s | ||
} | ||
|
||
// UsageOf returns a Usage function constructed from cmd. | ||
func (f *Flags) UsageOf(cmd Command) func() { | ||
var usage string | ||
var desc string | ||
if cmd.Usage != "" { | ||
usage = cmd.Usage | ||
} | ||
if cmd.Description != "" { | ||
desc = formatDesc(cmd.Description) | ||
} | ||
return func() { | ||
if usage != "" { | ||
fmt.Fprintf(f.Output(), "Usage: %s\n\n", usage) | ||
} | ||
if desc != "" { | ||
fmt.Fprintf(f.Output(), "%s", desc) | ||
} | ||
f.PrintDefaults() | ||
} | ||
} | ||
|
||
// Parse parses the Flags' arguments with the FlagSet. | ||
func (f Flags) Parse() error { | ||
return f.FlagSet.Parse(f.args) | ||
} | ||
|
||
// Command describes a subcommand to be run within the program. | ||
type Command struct { | ||
// Name is the name of the command. | ||
Name string | ||
|
||
// Summary is a short description of the command. | ||
Summary string | ||
|
||
// Usage describes the structure of the command. | ||
Usage string | ||
|
||
// Description is a detailed description of the command. | ||
Description string | ||
|
||
// Func is the function that runs when the command is invoked. | ||
Func func(Flags) | ||
} | ||
|
||
// Commands maps a name to a Command. | ||
type Commands struct { | ||
// Name is the name of the program. | ||
Name string | ||
|
||
m map[string]Command | ||
} | ||
|
||
// NewCommands returns an initialized commands. | ||
func NewCommands(name string) Commands { | ||
return Commands{ | ||
Name: name, | ||
m: map[string]Command{}, | ||
} | ||
} | ||
|
||
// Register registers cmd as cmd.Name. | ||
func (c Commands) Register(cmd Command) { | ||
c.m[cmd.Name] = cmd | ||
} | ||
|
||
// Has returns whether name is a registered command. | ||
func (c Commands) Has(name string) bool { | ||
_, ok := c.m[name] | ||
return ok | ||
} | ||
|
||
// Get returns the Command mapped to the given name. | ||
func (c Commands) Get(name string) Command { | ||
return c.m[name] | ||
} | ||
|
||
// List returns a list of commands, sorted by name. | ||
func (c Commands) List() []Command { | ||
list := make([]Command, 0, len(c.m)) | ||
for _, cmd := range c.m { | ||
list = append(list, cmd) | ||
} | ||
sort.Slice(list, func(i, j int) bool { | ||
return list[i].Name < list[j].Name | ||
}) | ||
return list | ||
} | ||
|
||
// Do executes the Command mapped to the given name. Does nothing if the name is | ||
// not defined. | ||
func (c Commands) Do(name string, args []string) { | ||
cmd := c.m[name] | ||
if cmd.Func == nil { | ||
return | ||
} | ||
flags := NewFlags(c.Name, args) | ||
flags.Usage = flags.UsageOf(cmd) | ||
cmd.Func(flags) | ||
} |
Oops, something went wrong.