improved gopher2600.go structure

removed modalflags package. modalflags was added very early in the
project. it's neater and simpler to implement command-line modes with
the standard flag package directly

improved log message for unsupported terminal type

remove rand.Seed(time) on startup. rand.Seed() is a deprecated function
This commit is contained in:
JetSetIlly 2023-11-19 13:16:14 +00:00
parent 8addbe5251
commit 85adbca367
9 changed files with 372 additions and 1039 deletions

View file

@ -24,59 +24,27 @@ package debugger
// library, which is where this requirement originates.
type CommandLineOptions struct {
// common to debugger and play modes
Log *bool
Spec *string
FpsCap *bool
Multiload *int
Mapping *string
Left *string
Right *string
Profile *string
ELF *string
Log bool
Spec string
FpsCap bool
Multiload int
Mapping string
Left string
Right string
Profile string
ELF string
// playmode only
ComparisonROM *string
ComparisonPrefs *string
Record *bool
PlaybackCheckROM *bool
PatchFile *string
Wav *bool
NoEject *bool
Macro *string
ComparisonROM string
ComparisonPrefs string
Record bool
PlaybackCheckROM bool
PatchFile string
Wav bool
NoEject bool
Macro string
// debugger only
InitScript *string
TermType *string
}
// NewCommandLineOptions creates a minimum instance of CommandLineOptions such
// that it is safe to dereference the fields in all situations.
//
// The values of these fields are shared by type and will be the default values
// for that type. ie. a bool is false, an int is zero, etc. Care should be
// taken therefore to replace the instance with the result from the modalflag
// (or flag) package.
func NewCommandLineOptions() CommandLineOptions {
var b bool
var s string
var i int
return CommandLineOptions{
Log: &b,
Spec: &s,
FpsCap: &b,
Multiload: &i,
Mapping: &s,
Left: &s,
Right: &s,
Profile: &s,
ELF: &s,
ComparisonROM: &s,
ComparisonPrefs: &s,
Record: &b,
PatchFile: &s,
Wav: &b,
InitScript: &s,
TermType: &s,
NoEject: &b,
}
InitScript string
TermType string
}

View file

@ -341,7 +341,7 @@ func NewDebugger(opts CommandLineOptions, create CreateUserInterface) (*Debugger
// creat a new television. this will be used during the initialisation of
// the VCS and not referred to directly again
tv, err := television.NewTelevision(*opts.Spec)
tv, err := television.NewTelevision(opts.Spec)
if err != nil {
return nil, fmt.Errorf("debugger: %w", err)
}
@ -440,7 +440,7 @@ func NewDebugger(opts CommandLineOptions, create CreateUserInterface) (*Debugger
dbg.vcs.RIOT.Ports.AttachPlugMonitor(dbg)
// set fps cap
dbg.vcs.TV.SetFPSCap(*opts.FpsCap)
dbg.vcs.TV.SetFPSCap(opts.FpsCap)
// initialise terminal
err = dbg.term.Initialise()
@ -677,7 +677,7 @@ func (dbg *Debugger) StartInDebugMode(filename string) error {
if filename == "" {
cartload = cartridgeloader.Loader{}
} else {
cartload, err = cartridgeloader.NewLoader(filename, *dbg.opts.Mapping)
cartload, err = cartridgeloader.NewLoader(filename, dbg.opts.Mapping)
if err != nil {
return fmt.Errorf("debugger: %w", err)
}
@ -688,7 +688,7 @@ func (dbg *Debugger) StartInDebugMode(filename string) error {
return fmt.Errorf("debugger: %w", err)
}
err = dbg.insertPeripheralsOnStartup(*dbg.opts.Left, *dbg.opts.Right)
err = dbg.insertPeripheralsOnStartup(dbg.opts.Left, dbg.opts.Right)
if err != nil {
return fmt.Errorf("debugger: %w", err)
}
@ -699,8 +699,8 @@ func (dbg *Debugger) StartInDebugMode(filename string) error {
}
// intialisation script because we're in debugger mode
if *dbg.opts.InitScript != "" {
scr, err := script.RescribeScript(*dbg.opts.InitScript)
if dbg.opts.InitScript != "" {
scr, err := script.RescribeScript(dbg.opts.InitScript)
if err == nil {
dbg.term.Silence(true)
err = dbg.inputLoop(scr, false)
@ -751,7 +751,7 @@ func (dbg *Debugger) StartInPlayMode(filename string) error {
if filename == "" {
cartload = cartridgeloader.Loader{}
} else {
cartload, err = cartridgeloader.NewLoader(filename, *dbg.opts.Mapping)
cartload, err = cartridgeloader.NewLoader(filename, dbg.opts.Mapping)
if err != nil {
return fmt.Errorf("debugger: %w", err)
}
@ -768,22 +768,22 @@ func (dbg *Debugger) StartInPlayMode(filename string) error {
return fmt.Errorf("debugger: %w", err)
}
err = dbg.insertPeripheralsOnStartup(*dbg.opts.Left, *dbg.opts.Right)
err = dbg.insertPeripheralsOnStartup(dbg.opts.Left, dbg.opts.Right)
if err != nil {
return fmt.Errorf("debugger: %w", err)
}
// apply patch if requested. note that this will be in addition to any
// patches applied during setup.AttachCartridge
if *dbg.opts.PatchFile != "" {
_, err := patch.CartridgeMemory(dbg.vcs.Mem.Cart, *dbg.opts.PatchFile)
if dbg.opts.PatchFile != "" {
_, err := patch.CartridgeMemory(dbg.vcs.Mem.Cart, dbg.opts.PatchFile)
if err != nil {
return fmt.Errorf("debugger: %w", err)
}
}
// record wav file
if *dbg.opts.Wav {
if dbg.opts.Wav {
fn := unique.Filename("audio", cartload.ShortName())
ww, err := wavwriter.NewWavWriter(fn)
if err != nil {
@ -793,25 +793,25 @@ func (dbg *Debugger) StartInPlayMode(filename string) error {
}
// record gameplay
if *dbg.opts.Record {
if dbg.opts.Record {
dbg.startRecording(cartload.ShortName())
}
} else {
if *dbg.opts.Record {
if dbg.opts.Record {
return fmt.Errorf("debugger: cannot make a new recording using a playback file")
}
dbg.startPlayback(filename)
}
if *dbg.opts.Macro != "" {
dbg.macro, err = macro.NewMacro(*dbg.opts.Macro, dbg, dbg.vcs.Input, dbg.vcs.TV, dbg.gui)
if dbg.opts.Macro != "" {
dbg.macro, err = macro.NewMacro(dbg.opts.Macro, dbg, dbg.vcs.Input, dbg.vcs.TV, dbg.gui)
if err != nil {
return fmt.Errorf("debugger: %w", err)
}
}
err = dbg.startComparison(*dbg.opts.ComparisonROM, *dbg.opts.ComparisonPrefs)
err = dbg.startComparison(dbg.opts.ComparisonROM, dbg.opts.ComparisonPrefs)
if err != nil {
return fmt.Errorf("debugger: %w", err)
}
@ -1046,9 +1046,9 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error)
if _, ok := cart.(*supercharger.Supercharger); ok {
switch event {
case notifications.NotifySuperchargerLoadStarted:
if *dbg.opts.Multiload >= 0 {
logger.Logf("debugger", "forcing supercharger multiload (%#02x)", uint8(*dbg.opts.Multiload))
dbg.vcs.Mem.Poke(supercharger.MutliloadByteAddress, uint8(*dbg.opts.Multiload))
if dbg.opts.Multiload >= 0 {
logger.Logf("debugger", "forcing supercharger multiload (%#02x)", uint8(dbg.opts.Multiload))
dbg.vcs.Mem.Poke(supercharger.MutliloadByteAddress, uint8(dbg.opts.Multiload))
}
case notifications.NotifySuperchargerFastloadEnded:
@ -1141,7 +1141,7 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error)
}
// check for cartridge ejection. if the NoEject option is set then return error
if *dbg.opts.NoEject && dbg.vcs.Mem.Cart.IsEjected() {
if dbg.opts.NoEject && dbg.vcs.Mem.Cart.IsEjected() {
// if there is an error left over from the AttachCartridge() call
// above, return that rather than "cartridge ejected"
if err != nil {
@ -1160,7 +1160,7 @@ func (dbg *Debugger) attachCartridge(cartload cartridgeloader.Loader) (e error)
}
dbg.CoProcDisasm.AttachCartridge(dbg)
err = dbg.CoProcDev.AttachCartridge(dbg, cartload.Filename, *dbg.opts.ELF)
err = dbg.CoProcDev.AttachCartridge(dbg, cartload.Filename, dbg.opts.ELF)
if err != nil {
logger.Logf("debugger", err.Error())
if errors.Is(err, coproc_dwarf.UnsupportedDWARF) {
@ -1235,7 +1235,7 @@ func (dbg *Debugger) endRecording() {
}
func (dbg *Debugger) startPlayback(filename string) error {
plb, err := recorder.NewPlayback(filename, *dbg.opts.PlaybackCheckROM)
plb, err := recorder.NewPlayback(filename, dbg.opts.PlaybackCheckROM)
if err != nil {
return err
}
@ -1353,7 +1353,7 @@ func (dbg *Debugger) hotload() (e error) {
}
dbg.CoProcDisasm.AttachCartridge(dbg)
dbg.CoProcDev.AttachCartridge(dbg, cartload.Filename, *dbg.opts.ELF)
dbg.CoProcDev.AttachCartridge(dbg, cartload.Filename, dbg.opts.ELF)
return nil
}

View file

@ -147,7 +147,9 @@ func TestDebugger_withNonExistantInitScript(t *testing.T) {
return &mockGUI{}, trm, nil
}
dbg, err := debugger.NewDebugger(debugger.NewCommandLineOptions(), create)
var opts debugger.CommandLineOptions
dbg, err := debugger.NewDebugger(opts, create)
if err != nil {
t.Fatalf(err.Error())
}
@ -170,7 +172,9 @@ func TestDebugger(t *testing.T) {
return &mockGUI{}, trm, nil
}
dbg, err := debugger.NewDebugger(debugger.NewCommandLineOptions(), create)
var opts debugger.CommandLineOptions
dbg, err := debugger.NewDebugger(opts, create)
if err != nil {
t.Fatalf(err.Error())
}

View file

@ -43,7 +43,7 @@ func (dsm *Disassembly) Write(output io.Writer, attr ColumnAttr) error {
// WriteBank writes the disassembly of the selected bank to io.Writer.
func (dsm *Disassembly) WriteBank(output io.Writer, attr ColumnAttr, bank int) error {
if bank >= len(dsm.disasmEntries.Entries) {
return nil
return fmt.Errorf("no bank %d in cartridge", bank)
}
ct := 0

View file

@ -17,9 +17,9 @@ package main
import (
"errors"
"flag"
"fmt"
"io"
"math/rand"
"os"
"os/signal"
"runtime"
@ -37,7 +37,6 @@ import (
"github.com/jetsetilly/gopher2600/gui/sdlimgui"
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
"github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/modalflag"
"github.com/jetsetilly/gopher2600/performance"
"github.com/jetsetilly/gopher2600/recorder"
"github.com/jetsetilly/gopher2600/regression"
@ -118,7 +117,7 @@ func main() {
// launch program as a go routine. further communication is through
// the mainSync instance
go launch(sync)
go launch(sync, os.Args[1:])
// if there is no GUI then we should sleep so that the select channel loop
// doesn't go beserk
@ -211,57 +210,42 @@ func main() {
// launch is called from main() as a goroutine. uses mainSync instance to
// indicate gui creation and to quit.
func launch(sync *mainSync) {
func launch(sync *mainSync, args []string) {
logger.Log("runtime", fmt.Sprintf("number of cores being used: %d", runtime.NumCPU()))
// we generate random numbers in some places. seed the generator with the
// current time
rand.Seed(int64(time.Now().Nanosecond()))
// get mode from command line
var mode string
md := &modalflag.Modes{Output: os.Stdout}
md.NewArgs(os.Args[1:])
md.NewMode()
md.AddSubModes("RUN", "PLAY", "DEBUG", "DISASM", "PERFORMANCE", "REGRESS", "VERSION")
p, err := md.Parse()
switch p {
case modalflag.ParseHelp:
sync.state <- stateRequest{req: reqQuit}
return
case modalflag.ParseError:
fmt.Printf("* error: %v\n", err)
sync.state <- stateRequest{req: reqQuit, args: 10}
return
if len(args) > 0 {
mode = strings.ToUpper(args[0])
}
switch md.Mode() {
var err error
switch mode {
default:
mode = "RUN"
err = emulate(mode, sync, args)
case "RUN":
fallthrough
case "PLAY":
err = emulate(govern.ModePlay, md, sync)
fallthrough
case "DEBUG":
err = emulate(govern.ModeDebugger, md, sync)
err = emulate(mode, sync, args[1:])
case "DISASM":
err = disasm(md)
err = disasm(mode, args[1:])
case "PERFORMANCE":
err = perform(md, sync)
err = perform(mode, sync, args[1:])
case "REGRESS":
err = regress(md, sync)
err = regress(mode, args[1:])
case "VERSION":
err = showVersion(md)
err = showVersion(mode, args[1:])
}
if err != nil {
// swallow power off error messages. send quit signal with return value of 20 instead
if !errors.Is(err, ports.PowerOff) {
fmt.Printf("* error in %s mode: %s\n", md.String(), err)
fmt.Printf("* error in %s mode: %s\n", mode, err)
sync.state <- stateRequest{req: reqQuit, args: 20}
return
}
@ -274,74 +258,80 @@ const defaultInitScript = "debuggerInit"
// emulate is the main emulation launch function, shared by play and debug
// modes. the other modes initialise and run the emulation differently.
func emulate(emulationMode govern.Mode, md *modalflag.Modes, sync *mainSync) error {
// start new commandline mode. to this we'll add the command line arguments
// that are specific to the emulation mode (it's unfortunate that mode is
// used to describe two separate concepts but they really have nothing to
// do with one another).
md.NewMode()
// prepare the path to the initialisation script used by the debugger. we
// can name the file in the defaultInitScript const declaration but the
// construction of the path is platform sensitive so we must do it here
defInitScript, err := resources.JoinPath(defaultInitScript)
if err != nil {
return err
func emulate(mode string, sync *mainSync, args []string) error {
var emulationMode govern.Mode
switch mode {
case "PLAY":
emulationMode = govern.ModePlay
case "RUN":
emulationMode = govern.ModePlay
case "DEBUG":
emulationMode = govern.ModeDebugger
default:
panic(fmt.Errorf("unknown emulation mode: %s", mode))
}
// new CommandLineOptions instance. this type collates the individual
// options that can be set by the command line
opts := debugger.NewCommandLineOptions()
// opts collates the individual options that can be set by the command line
var opts debugger.CommandLineOptions
// arguments common to both play and debugging modes
opts.Log = md.AddBool("log", false, "echo debugging log to stdout")
opts.Spec = md.AddString("tv", "AUTO", "television specification: AUTO, NTSC, PAL, PAL60, SECAM")
opts.FpsCap = md.AddBool("fpscap", true, "cap FPS to emulated match TV")
opts.Multiload = md.AddInt("multiload", -1, "force multiload byte (supercharger only; 0 to 255)")
opts.Mapping = md.AddString("mapping", "AUTO", "force use of cartridge mapping")
opts.Left = md.AddString("left", "AUTO", "left player port: AUTO, STICK, PADDLE, KEYPAD, GAMEPAD")
opts.Right = md.AddString("right", "AUTO", "right player port: AUTO, STICK, PADDLE, KEYPAD, GAMEPAD, SAVEKEY, ATARIVOX")
opts.Profile = md.AddString("profile", "none", "run performance check with profiling: command separated CPU, MEM, TRACE or ALL")
opts.ELF = md.AddString("elf", "", "path to corresponding ELF file for binary. only valid for some coprocessor supporting ROMs.")
flgs := flag.NewFlagSet(mode, flag.ExitOnError)
flgs.BoolVar(&opts.Log, "log", false, "echo debugging log to stdout")
flgs.StringVar(&opts.Spec, "tv", "AUTO", "televsion specifcation: AUTO, NTSC, PAL, PAL60, SECAM")
flgs.BoolVar(&opts.FpsCap, "fpscap", true, "cap FPS to emulation TV")
flgs.IntVar(&opts.Multiload, "multiload", -1, "force multiload byte (supercharger only; 0 to 255")
flgs.StringVar(&opts.Mapping, "mapping", "AUTO", "force cartridge mapper selection")
flgs.StringVar(&opts.Left, "left", "AUTO", "left player port: AUTO, STICK, PADDLE, KEYPAD, GAMEPAD")
flgs.StringVar(&opts.Right, "right", "AUTO", "left player port: AUTO, STICK, PADDLE, KEYPAD, GAMEPAD")
flgs.StringVar(&opts.Profile, "profile", "none", "run performance check with profiling: CPU, MEM, TRACE, ALL (comma sep)")
flgs.StringVar(&opts.ELF, "elf", "", "path to ELF file. only valid for some coproc supporting ROMs")
// playmode specific arguments
if emulationMode == govern.ModePlay {
opts.ComparisonROM = md.AddString("comparisonROM", "", "ROM to run in parallel for comparison")
opts.ComparisonPrefs = md.AddString("comparisonPrefs", "", "preferences for comparison emulation")
opts.Record = md.AddBool("record", false, "record user input to a file")
opts.PlaybackCheckROM = md.AddBool("playbackCheckROM", true, "whether to check ROM hash on playback")
opts.PatchFile = md.AddString("patch", "", "patch to apply to main emulation (not playback files)")
opts.Wav = md.AddBool("wav", false, "record audio to wav file")
opts.NoEject = md.AddBool("noeject", false, "a cartridge must be attached at all times. emulator will quit if not")
opts.Macro = md.AddString("macro", "", "macro file to be run on trigger")
flgs.StringVar(&opts.ComparisonROM, "comparisonROM", "", "ROM to run in parallel for comparison")
flgs.StringVar(&opts.ComparisonPrefs, "comparisonPrefs", "", "preferences for comparison emulation")
flgs.BoolVar(&opts.Record, "record", false, "record user input to playback file")
flgs.BoolVar(&opts.PlaybackCheckROM, "playbackCheckROM", true, "check ROM hashes on playback")
flgs.StringVar(&opts.PatchFile, "patch", "", "path to apply to emulation (not playback files")
flgs.BoolVar(&opts.Wav, "wav", false, "record audio to wav file")
flgs.BoolVar(&opts.NoEject, "noeject", false, "emulator will not quit is noeject is true")
flgs.StringVar(&opts.Macro, "macro", "", "macro file to be run on trigger")
}
// debugger specific arguments
if emulationMode == govern.ModeDebugger {
opts.InitScript = md.AddString("initscript", defInitScript, "script to run on debugger start")
opts.TermType = md.AddString("term", "IMGUI", "terminal type to use in debug mode: IMGUI, COLOR, PLAIN")
// prepare the path to the initialisation script used by the debugger. we
// can name the file in the defaultInitScript const declaration but the
// construction of the path is platform sensitive so we must do it here
defInitScript, err := resources.JoinPath(defaultInitScript)
if err != nil {
return err
}
flgs.StringVar(&opts.InitScript, "initscript", defInitScript, "script to run on debugger start")
flgs.StringVar(&opts.TermType, "term", "IMGUI", "terminal type: IMGUI, COLOR, PLAIN")
} else {
// non debugger emulation is always of type IMGUI
tt := "IMGUI"
opts.TermType = &tt
opts.TermType = "IMGUI"
}
// parse arguments
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
// parse args and get copy of remaining arguments
err := flgs.Parse(args)
if err != nil {
return err
}
args = flgs.Args()
// check remaining arguments. if there are any outstanding arguments to
// process then the user has made a mistake
if len(md.RemainingArgs()) > 1 {
return fmt.Errorf("too many arguments for %s mode", md)
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
// turn logging on by setting the echo function. events are still logged
// and available via the debugger but will not be "echoed" to the terminal,
// unless this option is on
if *opts.Log {
if opts.Log {
logger.SetEcho(os.Stdout, true)
} else {
logger.SetEcho(nil, false)
@ -362,7 +352,7 @@ func emulate(emulationMode govern.Mode, md *modalflag.Modes, sync *mainSync) err
var scr gui.GUI
// create GUI as appropriate
if *opts.TermType == "IMGUI" {
if opts.TermType == "IMGUI" {
sync.state <- stateRequest{req: reqCreateGUI,
args: guiCreate(func() (guiControl, error) {
return sdlimgui.NewSdlImgui(e)
@ -390,10 +380,11 @@ func emulate(emulationMode govern.Mode, md *modalflag.Modes, sync *mainSync) err
// if the GUI does not supply a terminal then use a color or plain terminal
// as a fallback
if term == nil {
switch strings.ToUpper(*opts.TermType) {
switch strings.ToUpper(opts.TermType) {
default:
fmt.Printf("! unknown terminal type (%s) defaulting to plain\n", *opts.TermType)
fallthrough
logger.Logf("terminal", "unknown terminal: %s", opts.TermType)
logger.Logf("terminal", "defaulting to plain")
term = &plainterm.PlainTerminal{}
case "PLAIN":
term = &plainterm.PlainTerminal{}
case "COLOR":
@ -410,15 +401,20 @@ func emulate(emulationMode govern.Mode, md *modalflag.Modes, sync *mainSync) err
// set up a launch function. this function is called either directly or via
// a call to performance.RunProfiler()
dbgLaunch := func() error {
var romFile string
if len(args) != 0 {
romFile = args[0]
}
switch emulationMode {
case govern.ModeDebugger:
err := dbg.StartInDebugMode(md.GetArg(0))
err := dbg.StartInDebugMode(romFile)
if err != nil {
return err
}
case govern.ModePlay:
err := dbg.StartInPlayMode(md.GetArg(0))
err := dbg.StartInPlayMode(romFile)
if err != nil {
return err
}
@ -429,7 +425,7 @@ func emulate(emulationMode govern.Mode, md *modalflag.Modes, sync *mainSync) err
// check for profiling option and either run the launch function (prepared
// above) via the performance.RunProfiler() function or directly
prf, err := performance.ParseProfileString(*opts.Profile)
prf, err := performance.ParseProfileString(opts.Profile)
if err != nil {
return err
}
@ -461,29 +457,34 @@ func emulate(emulationMode govern.Mode, md *modalflag.Modes, sync *mainSync) err
return nil
}
func disasm(md *modalflag.Modes) error {
md.NewMode()
func disasm(mode string, args []string) error {
var mapping string
var bytecode bool
var bank int
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
bytecode := md.AddBool("bytecode", false, "include bytecode in disassembly")
bank := md.AddInt("bank", -1, "show disassembly for a specific bank")
flgs := flag.NewFlagSet(mode, flag.ExitOnError)
flgs.StringVar(&mapping, "mapping", "AUTO", "force cartridge mapper selection")
flgs.BoolVar(&bytecode, "bytecode", false, "including bytecode in disassembly")
flgs.IntVar(&bank, "bank", -1, "show disassembly for a specific bank")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
// parse args and get copy of remaining arguments
err := flgs.Parse(args)
if err != nil {
return err
}
args = flgs.Args()
switch len(md.RemainingArgs()) {
switch len(args) {
case 0:
return fmt.Errorf("2600 cartridge required for %s mode", md)
return fmt.Errorf("2600 cartridge required")
case 1:
attr := disassembly.ColumnAttr{
ByteCode: *bytecode,
ByteCode: bytecode,
Label: true,
Cycles: true,
}
cartload, err := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
cartload, err := cartridgeloader.NewLoader(args[0], mapping)
if err != nil {
return err
}
@ -494,68 +495,76 @@ func disasm(md *modalflag.Modes) error {
// print what disassembly output we do have
if dsm != nil {
// ignore any further errors
_ = dsm.Write(md.Output, attr)
_ = dsm.Write(os.Stdout, attr)
}
return err
}
// output entire disassembly or just a specific bank
if *bank < 0 {
err = dsm.Write(md.Output, attr)
if bank < 0 {
err = dsm.Write(os.Stdout, attr)
} else {
err = dsm.WriteBank(md.Output, attr, *bank)
err = dsm.WriteBank(os.Stdout, attr, bank)
}
if err != nil {
return err
}
default:
return fmt.Errorf("too many arguments for %s mode", md)
return fmt.Errorf("too many arguments")
}
return nil
}
func perform(md *modalflag.Modes, sync *mainSync) error {
md.NewMode()
func perform(mode string, sync *mainSync, args []string) error {
var mapping string
var spec string
var uncapped bool
var duration string
var profile string
var log bool
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping")
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL, PAL60, SECAM")
uncapped := md.AddBool("uncapped", true, "run perfomance with no FPS cap")
duration := md.AddString("duration", "5s", "run duration (note: there is a 2s overhead)")
profile := md.AddString("profile", "NONE", "run performance check with profiling: command separated CPU, MEM, TRACE or ALL")
log := md.AddBool("log", false, "echo debugging log to stdout")
flgs := flag.NewFlagSet(mode, flag.ExitOnError)
flgs.StringVar(&mapping, "mapping", "AUTO", "form cartridge mapper selection")
flgs.StringVar(&spec, "spec", "AUTO", "television specification: NTSC, PAL, PAL60, SECAM")
flgs.BoolVar(&uncapped, "uncapped", true, "run performance no FPS cap")
flgs.StringVar(&duration, "duration", "5s", "run duation (with an additional 2s overhead)")
flgs.StringVar(&profile, "profile", "none", "run performance check with profiling: CPU, MEM, TRACE, ALL (comma sep)")
flgs.BoolVar(&log, "log", false, "echo debugging log to stdout")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
// parse args and get copy of remaining arguments
err := flgs.Parse(args)
if err != nil {
return err
}
args = flgs.Args()
// set debugging log echo
if *log {
if log {
logger.SetEcho(os.Stdout, true)
} else {
logger.SetEcho(nil, false)
}
switch len(md.RemainingArgs()) {
switch len(args) {
case 0:
return fmt.Errorf("2600 cartridge required for %s mode", md)
return fmt.Errorf("2600 cartridge required")
case 1:
cartload, err := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
cartload, err := cartridgeloader.NewLoader(args[0], mapping)
if err != nil {
return err
}
defer cartload.Close()
// check for profiling options
p, err := performance.ParseProfileString(*profile)
p, err := performance.ParseProfileString(profile)
if err != nil {
return err
}
// run performance check
err = performance.Check(md.Output, p, cartload, *spec, *uncapped, *duration)
err = performance.Check(os.Stdout, p, cartload, spec, uncapped, duration)
if err != nil {
return err
}
@ -564,147 +573,145 @@ func perform(md *modalflag.Modes, sync *mainSync) error {
// changes to the performance window impacting the play mode
default:
return fmt.Errorf("too many arguments for %s mode", md)
return fmt.Errorf("too many arguments")
}
return nil
}
func regress(md *modalflag.Modes, sync *mainSync) error {
md.NewMode()
md.AddSubModes("RUN", "LIST", "DELETE", "ADD", "REDUX", "CLEANUP")
func regress(mode string, args []string) error {
var subMode string
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
if len(args) > 0 {
subMode = strings.ToUpper(args[0])
}
var err error
switch subMode {
default:
err = regressRun(fmt.Sprintf("%s %s", mode, "RUN"), args)
case "RUN":
err = regressRun(fmt.Sprintf("%s %s", mode, subMode), args[1:])
case "LIST":
err = regressList(fmt.Sprintf("%s %s", mode, subMode), args[1:])
case "DELETE":
err = regressDelete(fmt.Sprintf("%s %s", mode, subMode), args[1:])
case "ADD":
err = regressAdd(fmt.Sprintf("%s %s", mode, subMode), args[1:])
case "REDUX":
err = regressRedux(fmt.Sprintf("%s %s", mode, subMode), args[1:])
case "CLEANUP":
err = regressCleanup(fmt.Sprintf("%s %s", mode, subMode), args[1:])
}
if err != nil {
return err
}
switch md.Mode() {
case "RUN":
md.NewMode()
return nil
}
// no additional arguments
verbose := md.AddBool("verbose", false, "output more detail (eg. error messages)")
func regressRun(mode string, args []string) error {
var verbose bool
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
flgs := flag.NewFlagSet(mode, flag.ExitOnError)
flgs.BoolVar(&verbose, "v", false, "output more detail")
err = regression.RegressRun(md.Output, *verbose, md.RemainingArgs())
if err != nil {
return err
}
// parse args and get copy of remaining arguments
err := flgs.Parse(args)
if err != nil {
return err
}
args = flgs.Args()
case "LIST":
md.NewMode()
// no additional arguments
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
switch len(md.RemainingArgs()) {
case 0:
err := regression.RegressList(md.Output)
if err != nil {
return err
}
default:
return fmt.Errorf("no additional arguments required for %s mode", md)
}
case "DELETE":
md.NewMode()
answerYes := md.AddBool("yes", false, "answer yes to confirmation")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
switch len(md.RemainingArgs()) {
case 0:
return fmt.Errorf("database key required for %s mode", md)
case 1:
// use stdin for confirmation unless "yes" flag has been sent
var confirmation io.Reader
if *answerYes {
confirmation = &yesReader{}
} else {
confirmation = os.Stdin
}
err := regression.RegressDelete(md.Output, confirmation, md.GetArg(0))
if err != nil {
return err
}
default:
return fmt.Errorf("only one entry can be deleted at at time")
}
case "ADD":
return regressAdd(md)
case "REDUX":
md.NewMode()
answerYes := md.AddBool("yes", false, "always answer yes to confirmation")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
var confirmation io.Reader
if *answerYes {
confirmation = &yesReader{}
} else {
confirmation = os.Stdin
}
return regression.RegressRedux(md.Output, confirmation)
case "CLEANUP":
md.NewMode()
answerYes := md.AddBool("yes", false, "always answer yes to confirmation")
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return err
}
var confirmation io.Reader
if *answerYes {
confirmation = &yesReader{}
} else {
confirmation = os.Stdin
}
return regression.RegressCleanup(md.Output, confirmation)
err = regression.RegressRun(os.Stdout, verbose, args)
if err != nil {
return err
}
return nil
}
func regressAdd(md *modalflag.Modes) error {
md.NewMode()
func regressList(mode string, args []string) error {
flgs := flag.NewFlagSet(mode, flag.ExitOnError)
mode := md.AddString("mode", "", "type of regression entry")
notes := md.AddString("notes", "", "additional annotation for the database")
mapping := md.AddString("mapping", "AUTO", "force use of cartridge mapping [non-playback]")
spec := md.AddString("tv", "AUTO", "television specification: NTSC, PAL, PAL60, SECAM [non-playback]")
numframes := md.AddInt("frames", 10, "number of frames to run [non-playback]")
state := md.AddString("state", "", "record emulator state at every CPU step [non-playback]")
log := md.AddBool("log", false, "echo debugging log to stdout")
// parse args and get copy of remaining arguments
err := flgs.Parse(args)
if err != nil {
return err
}
args = flgs.Args()
md.AdditionalHelp(
`The regression test to be added can be the path to a cartridge file or a previously
err = regression.RegressList(os.Stdout)
if err != nil {
return err
}
return nil
}
func regressDelete(mode string, args []string) error {
var yes bool
flgs := flag.NewFlagSet(mode, flag.ExitOnError)
flgs.BoolVar(&yes, "yes", false, "answer yes to confirmation request")
// parse args and get copy of remaining arguments
err := flgs.Parse(args)
if err != nil {
return err
}
args = flgs.Args()
switch len(args) {
case 0:
return fmt.Errorf("database key required")
case 1:
// use stdin for confirmation unless "yes" flag has been sent
var confirmation io.Reader
if yes {
confirmation = &yesReader{}
} else {
confirmation = os.Stdin
}
err := regression.RegressDelete(os.Stdout, confirmation, args[0])
if err != nil {
return err
}
default:
return fmt.Errorf("only one entry can be deleted at at time")
}
return nil
}
func regressAdd(mode string, args []string) error {
var regressMode string
var notes string
var mapping string
var spec string
var numFrames int
var state string
var log bool
flgs := flag.NewFlagSet(mode, flag.ContinueOnError)
flgs.StringVar(&regressMode, "mode", "", "type of regression entry")
flgs.StringVar(&notes, "notes", "", "additional annotation for the entry")
flgs.StringVar(&mapping, "mapping", "AUTO", "form cartridge mapper selection")
flgs.StringVar(&spec, "spec", "AUTO", "television specification: NTSC, PAL, PAL60, SECAM")
flgs.IntVar(&numFrames, "frames", 10, "number of frames to run (ignored if mode is 'playback'")
flgs.StringVar(&state, "state", "", "record emulator state at every CPU step (ignored if mode is 'playback'")
flgs.BoolVar(&log, "log", false, "echo debugging log to stdout")
// parse args and get copy of remaining arguments
err := flgs.Parse(args)
if err != nil {
if err == flag.ErrHelp {
fmt.Println()
fmt.Println(`The regression test to be added can be the path to a cartridge file or a previously
recorded playback file. For playback files, the flags marked [non-playback] do not make
sense and will be ignored.
@ -716,84 +723,78 @@ with the default VIDEO mode.
The -log flag intructs the program to echo the log to the console. Do not confuse this
with the LOG mode. Note that asking for log output will suppress regression progress meters.`)
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
return nil
}
return err
}
args = flgs.Args()
// set debugging log echo
if *log {
if log {
logger.SetEcho(os.Stdout, true)
md.Output = &nopWriter{}
} else {
logger.SetEcho(nil, false)
}
switch len(md.RemainingArgs()) {
switch len(args) {
case 0:
return fmt.Errorf("2600 cartridge or playback file required for %s mode", md)
return fmt.Errorf("2600 cartridge or playback file required")
case 1:
var reg regression.Regressor
var regressor regression.Regressor
if *mode == "" {
if err := recorder.IsPlaybackFile(md.GetArg(0)); err == nil {
*mode = "PLAYBACK"
if regressMode == "" {
if err := recorder.IsPlaybackFile(args[0]); err == nil {
regressMode = "PLAYBACK"
} else if !errors.Is(err, recorder.NotAPlaybackFile) {
return err
} else {
*mode = "VIDEO"
regressMode = "VIDEO"
}
}
switch strings.ToUpper(*mode) {
switch strings.ToUpper(regressMode) {
case "VIDEO":
cartload, err := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
cartload, err := cartridgeloader.NewLoader(args[0], mapping)
if err != nil {
return err
}
defer cartload.Close()
statetype, err := regression.NewStateType(*state)
statetype, err := regression.NewStateType(state)
if err != nil {
return err
}
reg = &regression.VideoRegression{
regressor = &regression.VideoRegression{
CartLoad: cartload,
TVtype: strings.ToUpper(*spec),
NumFrames: *numframes,
TVtype: strings.ToUpper(spec),
NumFrames: numFrames,
State: statetype,
Notes: *notes,
Notes: notes,
}
case "PLAYBACK":
// check and warn if unneeded arguments have been specified
md.Visit(func(flg string) {
if flg == "frames" {
fmt.Printf("! ignored %s flag when adding playback entry\n", flg)
}
})
reg = &regression.PlaybackRegression{
Script: md.GetArg(0),
Notes: *notes,
regressor = &regression.PlaybackRegression{
Script: args[0],
Notes: notes,
}
case "LOG":
cartload, err := cartridgeloader.NewLoader(md.GetArg(0), *mapping)
cartload, err := cartridgeloader.NewLoader(args[0], mapping)
if err != nil {
return err
}
defer cartload.Close()
reg = &regression.LogRegression{
regressor = &regression.LogRegression{
CartLoad: cartload,
TVtype: strings.ToUpper(*spec),
NumFrames: *numframes,
Notes: *notes,
TVtype: strings.ToUpper(spec),
NumFrames: numFrames,
Notes: notes,
}
}
err := regression.RegressAdd(md.Output, reg)
err := regression.RegressAdd(os.Stdout, regressor)
if err != nil {
// using carriage return (without newline) at beginning of error
// message because we want to overwrite the last output from
@ -807,19 +808,61 @@ with the LOG mode. Note that asking for log output will suppress regression prog
return nil
}
func showVersion(md *modalflag.Modes) error {
md.NewMode()
revision := md.AddBool("revision", false,
"display revision information from version control system (if available)")
func regressRedux(mode string, args []string) error {
var yes bool
p, err := md.Parse()
if err != nil || p != modalflag.ParseContinue {
flgs := flag.NewFlagSet(mode, flag.ExitOnError)
flgs.BoolVar(&yes, "yes", false, "answer yes to confirmation request")
// parse args and get copy of remaining arguments
err := flgs.Parse(args)
if err != nil {
return err
}
args = flgs.Args()
var confirmation io.Reader
if yes {
confirmation = &yesReader{}
} else {
confirmation = os.Stdin
}
return regression.RegressRedux(os.Stdout, confirmation)
}
func regressCleanup(mode string, args []string) error {
var yes bool
flgs := flag.NewFlagSet(mode, flag.ExitOnError)
flgs.BoolVar(&yes, "yes", false, "answer yes to confirmation request")
// parse args and get copy of remaining arguments
err := flgs.Parse(args)
if err != nil {
return err
}
args = flgs.Args()
var confirmation io.Reader
if yes {
confirmation = &yesReader{}
} else {
confirmation = os.Stdin
}
return regression.RegressCleanup(os.Stdout, confirmation)
}
func showVersion(mode string, args []string) error {
var revision bool
flgs := flag.NewFlagSet(mode, flag.ExitOnError)
flgs.BoolVar(&revision, "v", false, "display revision information (if available")
flgs.Parse(args)
fmt.Println(version.Version)
if *revision {
if revision {
fmt.Println(version.Revision)
}

View file

@ -1,135 +0,0 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
// Package modalflag is a wrapper for the flag package in the Go standard
// library. It provides a convenient method of handling program modes (and
// sub-modes) and allows different flags for each mode.
//
// At it's simplest it can be used as a replacement for the flag package, with
// some differences. Whereas, with flag.FlagSet you call Parse() with the array of
// strings as the only argument, with modalflag you first NewArgs() with the
// array of arguments and then Parse() with no arguments. For example (note
// that no error handling of the Parse() function is shown here):
//
// md = Modes{Output: os.Stdout}
// md.NewArgs(os.Args[1:])
// _, _ = md.Parse()
//
// The reason for his difference is to allow effective parsing of modes and
// sub-modes. We'll come to program modes in a short while.
//
// In the above example, once the arguments have been parsed, non-flag arguments
// can be retrieved with the RemainingArgs() or GetArg() function. For example,
// handling exactly one argument:
//
// switch len(md.RemainingArgs()) {
// case 0:
// return fmt.Errorf("argument required")
// case 1:
// Process(md.GetArg(0))
// default:
// return fmt.Errorf("too many arguments")
// }
//
// Adding flags is similar to the flag package. Adding a boolean flag:
//
// verbose := md.AddBool("verbose", false, "print additional log messages")
//
// These flag functions return a pointer to a variable of the specified type. The
// initial value of these variables if the default value, the second argument in
// the function call above. The Parse() function will set these values
// appropriately according what the user has requested, for example:
//
// if *verbose {
// fmt.Println(additionalLogMessage)
// }
//
// The most important difference between the standard flag package and the
// modalflag package is the ability of the latter to handle "modes". In this
// context, a mode is a special command line argument that when specified, puts
// the program into a different mode of operation. The best and most relevant
// example I can think of is the go command. The go command has many different
// modes: build, doc, get, test, etc. Each of these modes are different enough
// to require a different set of flags and expected arguments.
//
// The modalflag package handles sub-modes with the AddSubModes() function.
// This function takes any number of string arguments, each one the name of a
// mode.
//
// md.AddSubModes("run", "test", "debug")
//
// For simplicity, all sub-mode comparisons are case insensitive.
//
// Subsequent calls to Parse() will then process flags in the normal way but
// unlike the regular flag.Parse() function will check to see if the first
// argument after the flags is one of these modes. If it is, then the
// RemainingArgs() function will return all the arguments after the flags AND
// the mode selector.
//
// So, for example:
//
// md.Parse()
// switch md.Mode() {
// case "RUN":
// runMode(*verbose)
// default:
// fmt.Printf("%s not yet implemented", md.Mode())
// }
//
// Now that we've decided on what mode we're in, we can again call Parse() to
// process the remaining arguments. This example shows how we can handle return
// state and errors from the Parse() function:
//
// func runMode(verbose bool) {
// md.NewMode()
// md.AddDuration("runtime", time.ParseDuration("10s"), "max run time")
// p, err := md.Parse
// switch p {
// case ParseError:
// fmt.Println(err)
// return
// case ParseHelp:
// return
// }
// doRun(md.RemainingArguments)
// }
//
// This second call to Parse() will check for any additional flags and any
// further sub-modes (none in this example).
//
// We can chain modes together as deep as we want. For example, the "test" mode
// added above could be divided into several different modes:
//
// md = Modes{Output: os.Stdout}
// md.NewArgs(os.Args[1:])
// md.AddSubModes("run", "test", "debug")
// _, _ = md.Parse()
// switch md.Mode() {
// case "TEST":
// md.NewMode()
// md.AddSubModes("A", "B", "C")
// _, _ = md.Parse()
// switch md.Mode() {
// case "A":
// testA()
// case "B":
// testB()
// case "C":
// testC()
// }
// default:
// fmt.Printf("%s not yet implemented", md.Mode())
// }
package modalflag

View file

@ -1,93 +0,0 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package modalflag
import (
"fmt"
"io"
"strings"
)
// helpWriter is used to amend the default output from the flag package.
type helpWriter struct {
// the last []byte sent to the Write() function
buffer []byte
}
// Clear contents of output buffer.
func (hw *helpWriter) Clear() {
hw.buffer = []byte{}
}
func (hw *helpWriter) Help(output io.Writer, banner string, subModes []string, additionalHelp string) {
s := string(hw.buffer)
helpLines := strings.Split(s, "\n")
// output "no help available" message if there is no flag information and no
// sub-modes
if s == "Usage:\n" && len(subModes) == 0 {
if additionalHelp != "" {
output.Write([]byte(additionalHelp))
output.Write([]byte("\n"))
return
}
output.Write([]byte("No help available"))
if banner != "" {
output.Write([]byte(fmt.Sprintf(" for %s", banner)))
}
output.Write([]byte("\n"))
return
}
if banner != "" {
// supplement default banner with additional string
output.Write([]byte(fmt.Sprintf("%s for %s mode\n", helpLines[0], banner)))
} else {
// there is no banner so just print the default flag package banner
output.Write([]byte(helpLines[0]))
output.Write([]byte("\n"))
}
// add help message produced by flag package
if len(helpLines) > 1 {
s := strings.Join(helpLines[1:], "\n")
output.Write([]byte(s))
}
// add sub-mode information
if len(subModes) > 0 {
// add an additional new line if we've already printed flag information
if len(helpLines) > 2 {
output.Write([]byte("\n"))
}
output.Write([]byte(fmt.Sprintf(" available sub-modes: %s\n", strings.Join(subModes, ", "))))
output.Write([]byte(fmt.Sprintf(" default: %s\n", subModes[0])))
}
if additionalHelp != "" {
output.Write([]byte("\n"))
output.Write([]byte(additionalHelp))
output.Write([]byte("\n"))
}
}
// Write buffers all output.
func (hw *helpWriter) Write(p []byte) (n int, err error) {
hw.buffer = append(hw.buffer, p...)
return len(p), nil
}

View file

@ -1,296 +0,0 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package modalflag
import (
"flag"
"io"
"strings"
"time"
)
const modeSeparator = "/"
// Modes provides an easy way of handling command line arguments. The Output
// field should be specified before calling Parse() or you will not see any
// help messages.
type Modes struct {
// where to print output (help messages etc). defaults to os.Stdout
Output io.Writer
// whether Parse() has been called recently
parsed bool
// the underlying flag structure. this can be used directly as described by
// the flag.FlagSet documentation. the only thing you shouldn't do is call
// Parse() directly. Use the Parse() function of the parent Modes struct
// instead.
//
// a new flagset is created on every call to NewArgs() and NewMode()
flags *flag.FlagSet
// the argument list as specified by the NewArgs() function
args []string
argsIdx int
// the most recent list of sub-modes specified with the NewMode() function
subModes []string
// path is the series of sub-modes that have been found during subsequent
// calls to Parse()
//
// we never reset this variable
path []string
// some modes will benefit from a verbose explanation. use
additionalHelp string
minRemainingArgs int
maxRemainingArgs int
}
func (md *Modes) String() string {
return md.Path()
}
// Mode returns the last mode to be encountered.
func (md *Modes) Mode() string {
if len(md.path) == 0 {
return ""
}
return md.path[len(md.path)-1]
}
// Path returns a string all the modes encountered during parsing.
func (md *Modes) Path() string {
return strings.Join(md.path, modeSeparator)
}
// NewArgs with a string of arguments (from the command line for example).
func (md *Modes) NewArgs(args []string) {
// initialise args
md.args = args
md.argsIdx = 0
// by definition, a newly initialised Modes struct begins with a new mode
md.NewMode()
}
// NewMode indicates that further arguments should be considered part of a new
// mode.
func (md *Modes) NewMode() {
md.subModes = []string{}
md.flags = flag.NewFlagSet("", flag.ContinueOnError)
md.parsed = false
}
// AdditionalHelp allows you to add extensive help text to be displayed in
// addition to the regular help on available flags.
func (md *Modes) AdditionalHelp(help string) {
md.additionalHelp = help
}
// MinMax specifies the minimum and maximum number of non-flags arguments that
// are required.
func (md *Modes) MinMax(min int, max int) {
md.minRemainingArgs = min
md.maxRemainingArgs = max
}
// Parsed returns false if Parse() has not yet been called since either a call
// to NewArgs() or NewMode(). Note that, a Modes struct is considered to be
// Parsed() even if Parse() results in an error.
func (md *Modes) Parsed() bool {
return md.parsed
}
// ParseResult is returned from the Parse() function.
type ParseResult int
// a list of valid ParseResult values.
const (
// Continue with command line processing. How this result should be
// interpreted depends on the context, which the caller of the Parse()
// function knows best. However, generally we can say that if sub-modes
// were specified in the preceding call to NewMode() then the Mode field
// of the Modes struct should be checked.
ParseContinue ParseResult = iota
// The wrong number of non-flag arguments have been specified.
ParseTooFewArgs
ParseTooManyArgs
// Help was requested and has been printed.
ParseHelp
// an error has occurred and is returned as the second return value.
ParseError
)
// Parse the top level layer of arguments. Returns a value of ParseResult.
// The idiomatic usage is as follows:
//
// p, err := md.Parse()
// switch p {
// case modalflag.ParseHelp:
// // help message has already been printed
// return
// case modalflag.ParseError:
// printError(err)
// return
// }
//
// Help messages are handled automatically by the function. The return value
// ParseHelp is to help you guide your program appropriately. The above pattern
// suggests it should be treated similarly to an error and without the need to
// display anything further to the user.
//
// Note that the Output field of the Modes struct *must* be specified in order
// for any help messages to be visible. The most common and useful value of the
// field is os.Stdout.
func (md *Modes) Parse() (ParseResult, error) {
// flag the parsed flag in all instances, even if we eventually return an
// error
md.parsed = true
// set output of flags.Parse() to an instance of helpWriter
hw := &helpWriter{}
md.flags.SetOutput(hw)
// parse arguments
err := md.flags.Parse(md.args[md.argsIdx:])
if err != nil {
if err == flag.ErrHelp {
hw.Help(md.Output, md.Path(), md.subModes, md.additionalHelp)
hw.Clear()
return ParseHelp, nil
}
// flags have been set that are not recognised. if sub-modes and a
// default mode have been defined, set selected mode to default mode
// and continue. otherwise return error
if len(md.subModes) > 0 {
md.path = append(md.path, md.subModes[0])
} else {
return ParseError, err
}
} else if len(md.subModes) > 0 {
arg := strings.ToUpper(md.flags.Arg(0))
// check to see if the single argument is in the list of modes,
// starting off assuming it isn't
mode := md.subModes[0]
for i := range md.subModes {
if md.subModes[i] == arg {
// found matching sub-mode
mode = arg
md.argsIdx++
break // for loop
}
}
// add mode (either one we've found or the default) and add it to
// the path
md.path = append(md.path, mode)
}
r := md.RemainingArgs()
if md.minRemainingArgs > 0 && len(r) < md.minRemainingArgs {
return ParseTooFewArgs, nil
}
if md.maxRemainingArgs > 0 && len(r) > md.maxRemainingArgs {
return ParseTooManyArgs, nil
}
return ParseContinue, nil
}
// RemainingArgs after a call to Parse() ie. arguments that aren't flags or a
// listed sub-mode.
func (md *Modes) RemainingArgs() []string {
return md.flags.Args()
}
// GetArg returns the numbered argument that isn't a flag or listed sub-mode.
func (md *Modes) GetArg(i int) string {
return md.flags.Arg(i)
}
// AddSubModes to list of submodes for next parse. The first sub-mode in the
// list is considered to be the default sub-mode. If you need more control over
// this, AddDefaultSubMode() can be used.
//
// Note that sub-mode comparisons are case insensitive.
func (md *Modes) AddSubModes(submodes ...string) {
md.subModes = append(md.subModes, submodes...)
for i := range md.subModes {
md.subModes[i] = strings.ToUpper(md.subModes[i])
}
}
// AddDefaultSubMode to list of sub-modes.
func (md *Modes) AddDefaultSubMode(defSubMode string) {
md.subModes = append([]string{defSubMode}, md.subModes...)
}
// AddBool flag for next call to Parse().
func (md *Modes) AddBool(name string, value bool, usage string) *bool {
return md.flags.Bool(name, value, usage)
}
// AddDuration flag for next call to Parse().
func (md *Modes) AddDuration(name string, value time.Duration, usage string) *time.Duration {
return md.flags.Duration(name, value, usage)
}
// AddFloat64 flag for next call to Parse().
func (md *Modes) AddFloat64(name string, value float64, usage string) *float64 {
return md.flags.Float64(name, value, usage)
}
// AddInt flag for next call to Parse().
func (md *Modes) AddInt(name string, value int, usage string) *int {
return md.flags.Int(name, value, usage)
}
// AddInt64 flag for next call to Parse().
func (md *Modes) AddInt64(name string, value int64, usage string) *int64 {
return md.flags.Int64(name, value, usage)
}
// AddString flag for next call to Parse().
func (md *Modes) AddString(name string, value string, usage string) *string {
return md.flags.String(name, value, usage)
}
// AddUint flag for next call to Parse().
func (md *Modes) AddUint(name string, value uint, usage string) *uint {
return md.flags.Uint(name, value, usage)
}
// AddUint64 flag for next call to Parse().
func (md *Modes) AddUint64(name string, value uint64, usage string) *uint64 {
return md.flags.Uint64(name, value, usage)
}
// Visit visits the flags in lexicographical order, calling fn for each. It
// visits only those flags that have been set.
func (md *Modes) Visit(fn func(flag string)) {
md.flags.Visit(func(f *flag.Flag) {
fn(f.Name)
})
}

View file

@ -1,158 +0,0 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package modalflag_test
import (
"os"
"testing"
"github.com/jetsetilly/gopher2600/modalflag"
"github.com/jetsetilly/gopher2600/test"
)
func TestNoModesNoFlags(t *testing.T) {
md := modalflag.Modes{Output: os.Stdout}
md.NewArgs([]string{})
p, err := md.Parse()
if p != modalflag.ParseContinue {
t.Error("expected ParseContinue")
}
if err != nil {
t.Errorf("did not expect error: %s", err)
}
if md.Mode() != "" {
t.Errorf("did not expect to see mode as result of Parse()")
}
if md.Path() != "" {
t.Errorf("did not expect to see modes in mode path")
}
}
func TestNoModes(t *testing.T) {
md := modalflag.Modes{Output: os.Stdout}
md.NewArgs([]string{"-test", "1", "2"})
testFlag := md.AddBool("test", false, "test flag")
if *testFlag != false {
t.Error("expected *testFlag to be false before Parse()")
}
p, err := md.Parse()
if p != modalflag.ParseContinue {
t.Error("expected ParseContinue")
}
if err != nil {
t.Errorf("did not expect error: %s", err)
}
if md.Mode() != "" {
t.Errorf("did not expect to see mode as result of Parse()")
}
if md.Path() != "" {
t.Errorf("did not expect to see modes in mode path")
}
if *testFlag != true {
t.Error("expected *testFlag to be true after Parse()")
}
if len(md.RemainingArgs()) != 2 {
t.Error("expected number of RemainingArgs() to be 2 after Parse()")
}
}
func TestNoHelpAvailable(t *testing.T) {
tw := &test.Writer{}
md := modalflag.Modes{Output: tw}
md.NewArgs([]string{"-help"})
p, _ := md.Parse()
if p != modalflag.ParseHelp {
t.Error("expected ParseHelp return value from Parse()")
}
if !tw.Compare("No help available\n") {
t.Error("unexpected help message (wanted 'No help available')")
}
}
func TestHelpFlags(t *testing.T) {
tw := &test.Writer{}
md := modalflag.Modes{Output: tw}
md.NewArgs([]string{"-help"})
md.AddBool("test", true, "test flag")
p, _ := md.Parse()
if p != modalflag.ParseHelp {
t.Error("expected ParseHelp return value from Parse()")
}
expectedHelp := "Usage:\n" +
" -test\n" +
" test flag (default true)\n"
if !tw.Compare(expectedHelp) {
t.Error("unexpected help message")
}
}
func TestHelpModes(t *testing.T) {
tw := &test.Writer{}
md := modalflag.Modes{Output: tw}
md.NewArgs([]string{"-help"})
md.AddSubModes("A", "B", "C")
p, _ := md.Parse()
if p != modalflag.ParseHelp {
t.Error("expected ParseHelp return value from Parse()")
}
expectedHelp := "Usage:\n" +
" available sub-modes: A, B, C\n" +
" default: A\n"
if !tw.Compare(expectedHelp) {
t.Error("unexpected help message")
}
}
func TestHelpFlagsAndModes(t *testing.T) {
tw := &test.Writer{}
md := modalflag.Modes{Output: tw}
md.NewArgs([]string{"-help"})
md.AddBool("test", true, "test flag")
md.AddSubModes("A", "B", "C")
p, _ := md.Parse()
if p != modalflag.ParseHelp {
t.Error("expected ParseHelp return value from Parse()")
}
expectedHelp := "Usage:\n" +
" -test\n" +
" test flag (default true)\n" +
"\n" +
" available sub-modes: A, B, C\n" +
" default: A\n"
if !tw.Compare(expectedHelp) {
t.Error("unexpected help message")
}
}