2019-08-25 18:50:38 -06:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2020-08-28 17:41:17 -06:00
|
|
|
"flag"
|
2019-08-25 18:50:38 -06:00
|
|
|
"fmt"
|
2020-08-28 17:41:17 -06:00
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"sort"
|
|
|
|
|
2020-09-08 17:49:02 -06:00
|
|
|
"github.com/dirtbags/moth/pkg/transpile"
|
|
|
|
|
2020-08-28 17:41:17 -06:00
|
|
|
"github.com/spf13/afero"
|
2019-08-25 18:50:38 -06:00
|
|
|
)
|
|
|
|
|
2020-09-11 13:03:19 -06:00
|
|
|
// T represents the state of things
|
2020-08-28 17:41:17 -06:00
|
|
|
type T struct {
|
2020-10-12 17:44:44 -06:00
|
|
|
Stdout io.Writer
|
|
|
|
Stderr io.Writer
|
|
|
|
Args []string
|
|
|
|
BaseFs afero.Fs
|
|
|
|
fs afero.Fs
|
|
|
|
|
|
|
|
// Arguments
|
2020-09-11 13:03:19 -06:00
|
|
|
filename string
|
|
|
|
answer string
|
2019-08-25 18:50:38 -06:00
|
|
|
}
|
|
|
|
|
2020-09-11 13:03:19 -06:00
|
|
|
// Command is a function invoked by the user
|
|
|
|
type Command func() error
|
|
|
|
|
|
|
|
func nothing() error {
|
|
|
|
return nil
|
2020-08-28 17:41:17 -06:00
|
|
|
}
|
2019-08-25 18:50:38 -06:00
|
|
|
|
2020-09-16 17:54:29 -06:00
|
|
|
func usage(w io.Writer) {
|
|
|
|
fmt.Fprintln(w, "Usage: transpile COMMAND [flags]")
|
|
|
|
fmt.Fprintln(w, "")
|
|
|
|
fmt.Fprintln(w, " mothball: Compile a mothball")
|
|
|
|
fmt.Fprintln(w, " inventory: Show category inventory")
|
|
|
|
fmt.Fprintln(w, " open: Open a file for a puzzle")
|
|
|
|
fmt.Fprintln(w, " answer: Check correctness of an answer")
|
|
|
|
}
|
|
|
|
|
2020-09-11 13:03:19 -06:00
|
|
|
// ParseArgs parses arguments and runs the appropriate action.
|
|
|
|
func (t *T) ParseArgs() (Command, error) {
|
|
|
|
var cmd Command
|
|
|
|
|
|
|
|
if len(t.Args) == 1 {
|
2020-09-16 17:54:29 -06:00
|
|
|
usage(t.Stderr)
|
2020-09-11 13:03:19 -06:00
|
|
|
return nothing, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
flags := flag.NewFlagSet(t.Args[1], flag.ContinueOnError)
|
2020-10-12 17:44:44 -06:00
|
|
|
flags.SetOutput(t.Stderr)
|
2020-09-11 13:03:19 -06:00
|
|
|
directory := flags.String("dir", "", "Work directory")
|
|
|
|
|
|
|
|
switch t.Args[1] {
|
|
|
|
case "mothball":
|
|
|
|
cmd = t.DumpMothball
|
2020-10-12 17:44:44 -06:00
|
|
|
flags.StringVar(&t.filename, "out", "", "Path to create mothball (empty for stdout)")
|
2020-08-28 17:41:17 -06:00
|
|
|
case "inventory":
|
2020-09-11 13:03:19 -06:00
|
|
|
cmd = t.PrintInventory
|
2020-08-28 17:41:17 -06:00
|
|
|
case "open":
|
2020-09-11 13:03:19 -06:00
|
|
|
cmd = t.DumpFile
|
2020-10-12 17:44:44 -06:00
|
|
|
flags.StringVar(&t.filename, "file", "puzzle.json", "Filename to open")
|
2020-09-11 13:03:19 -06:00
|
|
|
case "answer":
|
|
|
|
cmd = t.CheckAnswer
|
2020-10-12 17:44:44 -06:00
|
|
|
flags.StringVar(&t.answer, "answer", "", "Answer to check")
|
2020-09-16 17:54:29 -06:00
|
|
|
case "help":
|
|
|
|
usage(t.Stderr)
|
|
|
|
return nothing, nil
|
2020-08-28 17:41:17 -06:00
|
|
|
default:
|
2020-09-18 12:52:04 -06:00
|
|
|
fmt.Fprintln(t.Stderr, "ERROR:", t.Args[1], "is not a valid command")
|
2020-09-16 17:54:29 -06:00
|
|
|
usage(t.Stderr)
|
2020-09-18 12:52:04 -06:00
|
|
|
return nothing, fmt.Errorf("Invalid command")
|
2020-09-11 13:03:19 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := flags.Parse(t.Args[2:]); err != nil {
|
|
|
|
return nothing, err
|
2020-08-28 17:41:17 -06:00
|
|
|
}
|
2020-09-11 13:03:19 -06:00
|
|
|
if *directory != "" {
|
|
|
|
t.fs = afero.NewBasePathFs(t.BaseFs, *directory)
|
|
|
|
} else {
|
|
|
|
t.fs = t.BaseFs
|
|
|
|
}
|
|
|
|
|
|
|
|
return cmd, nil
|
2020-08-28 17:41:17 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// PrintInventory prints a puzzle inventory to stdout
|
|
|
|
func (t *T) PrintInventory() error {
|
2020-09-11 13:03:19 -06:00
|
|
|
inv, err := transpile.FsInventory(t.fs)
|
2020-08-28 17:41:17 -06:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-09-04 15:29:06 -06:00
|
|
|
|
2020-09-11 13:03:19 -06:00
|
|
|
cats := make([]string, 0, len(inv))
|
|
|
|
for cat := range inv {
|
|
|
|
cats = append(cats, cat)
|
|
|
|
}
|
|
|
|
sort.Strings(cats)
|
|
|
|
for _, cat := range cats {
|
|
|
|
puzzles := inv[cat]
|
|
|
|
fmt.Fprint(t.Stdout, cat)
|
|
|
|
for _, p := range puzzles {
|
|
|
|
fmt.Fprint(t.Stdout, " ", p)
|
|
|
|
}
|
|
|
|
fmt.Fprintln(t.Stdout)
|
2020-09-04 15:29:06 -06:00
|
|
|
}
|
2020-08-28 17:41:17 -06:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-11 13:03:19 -06:00
|
|
|
// DumpFile writes a file to the writer.
|
2020-09-16 17:54:29 -06:00
|
|
|
// BUG(neale): The "open" and "answer" actions don't work on categories with an "mkcategory" executable.
|
2020-09-11 13:03:19 -06:00
|
|
|
func (t *T) DumpFile() error {
|
|
|
|
puzzle := transpile.NewFsPuzzle(t.fs)
|
2020-08-28 17:41:17 -06:00
|
|
|
|
2020-09-11 13:03:19 -06:00
|
|
|
switch t.filename {
|
2020-08-28 17:41:17 -06:00
|
|
|
case "puzzle.json", "":
|
2020-09-11 13:03:19 -06:00
|
|
|
p, err := puzzle.Puzzle()
|
2020-09-01 20:12:57 -06:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-08-28 17:41:17 -06:00
|
|
|
jp, err := json.Marshal(p)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-09-11 13:03:19 -06:00
|
|
|
t.Stdout.Write(jp)
|
2020-08-28 17:41:17 -06:00
|
|
|
default:
|
2020-09-11 13:03:19 -06:00
|
|
|
f, err := puzzle.Open(t.filename)
|
2020-08-28 17:41:17 -06:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
2020-09-11 13:03:19 -06:00
|
|
|
if _, err := io.Copy(t.Stdout, f); err != nil {
|
2020-08-28 17:41:17 -06:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-12 17:44:44 -06:00
|
|
|
// DumpMothball writes a mothball to the writer, or an output file if specified.
|
2020-09-11 13:03:19 -06:00
|
|
|
func (t *T) DumpMothball() error {
|
2020-10-12 17:44:44 -06:00
|
|
|
var w io.Writer
|
|
|
|
|
2020-09-11 13:03:19 -06:00
|
|
|
c := transpile.NewFsCategory(t.fs, "")
|
2020-10-13 19:48:37 -06:00
|
|
|
|
|
|
|
removeOnError := false
|
|
|
|
switch t.filename {
|
|
|
|
case "", "-":
|
2020-10-12 17:44:44 -06:00
|
|
|
w = t.Stdout
|
2020-10-13 19:48:37 -06:00
|
|
|
default:
|
|
|
|
removeOnError = true
|
2020-10-13 16:41:50 -06:00
|
|
|
log.Println("Writing mothball to", t.filename)
|
2020-10-12 17:44:44 -06:00
|
|
|
outf, err := t.BaseFs.Create(t.filename)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer outf.Close()
|
|
|
|
w = outf
|
2020-09-04 18:28:23 -06:00
|
|
|
}
|
2020-10-12 17:44:44 -06:00
|
|
|
if err := transpile.Mothball(c, w); err != nil {
|
2020-10-13 19:48:37 -06:00
|
|
|
if removeOnError {
|
|
|
|
t.BaseFs.Remove(t.filename)
|
|
|
|
}
|
2020-09-04 18:28:23 -06:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-11 13:03:19 -06:00
|
|
|
// CheckAnswer prints whether an answer is correct.
|
|
|
|
func (t *T) CheckAnswer() error {
|
|
|
|
c := transpile.NewFsPuzzle(t.fs)
|
|
|
|
if c.Answer(t.answer) {
|
|
|
|
fmt.Fprintln(t.Stdout, "correct")
|
|
|
|
} else {
|
|
|
|
fmt.Fprintln(t.Stdout, "wrong")
|
|
|
|
}
|
|
|
|
return nil
|
2020-09-03 20:04:43 -06:00
|
|
|
}
|
|
|
|
|
2020-08-28 17:41:17 -06:00
|
|
|
func main() {
|
|
|
|
t := &T{
|
2020-09-11 13:03:19 -06:00
|
|
|
Stdout: os.Stdout,
|
|
|
|
Stderr: os.Stderr,
|
|
|
|
Args: os.Args,
|
2020-09-16 17:54:29 -06:00
|
|
|
BaseFs: afero.NewOsFs(),
|
2020-09-11 13:03:19 -06:00
|
|
|
}
|
|
|
|
cmd, err := t.ParseArgs()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
2020-08-28 17:41:17 -06:00
|
|
|
}
|
2020-09-11 13:03:19 -06:00
|
|
|
if err := cmd(); err != nil {
|
2020-08-28 17:41:17 -06:00
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2019-08-25 18:50:38 -06:00
|
|
|
}
|