diff --git a/.travis.yml b/.travis.yml index 7a6401d..4e40dfa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,10 +4,10 @@ matrix: include: - go: 1.x env: LATEST=true - - go: 1.7.x - go: 1.8.x - go: 1.9.x - go: 1.10.x + - go: 1.11.x - go: tip allow_failures: - go: tip diff --git a/cmd/env.go b/cmd/env.go new file mode 100644 index 0000000..9eab6ab --- /dev/null +++ b/cmd/env.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "fmt" + "os" + "runtime" + + "github.com/spf13/cobra" +) + +var env = &cobra.Command{ + Use: "env", + Short: "Displays the current environment setup used by Hussar", + Run: func(cmd *cobra.Command, args []string) { + printEnvironment() + }, +} + +func printEnvironment() { + fmt.Printf("arch: %s\n", runtime.GOARCH) + fmt.Printf("os: %s\n", runtime.GOOS) + fmt.Printf("bin: %s\n", os.Args[0]) + fmt.Printf("gc: %s\n", runtime.Version()) + fmt.Printf("vers: %s\n", version) + fmt.Printf("build: %s\n", build) +} diff --git a/cmd/interactive.go b/cmd/interactive.go new file mode 100644 index 0000000..d219d0c --- /dev/null +++ b/cmd/interactive.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/hussar-lang/hussar/repl" + + "github.com/spf13/cobra" +) + +var interactive = &cobra.Command{ + Use: "interactive", + Short: "Start the interactive shell", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Starting Hussar interactive interpreter.") + repl.Start(os.Stdin, os.Stdout) + }, +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..9636eeb --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + version string + build string +) + +var rootCmd = &cobra.Command{ + Use: "hussar", +} + +// Setup populates the version and build fields +func Setup(versionStr string, buildStr string) { + version = versionStr + build = buildStr + rootCmd.Short = fmt.Sprintf("The Hussar programming language - %s (build %s)", version, build) + rootCmd.SetVersionTemplate(fmt.Sprintf("%s (build %s)", version, build)) +} + +// Execute executes the commands +func Execute() { + rootCmd.AddCommand(run, interactive, env) + if err := rootCmd.Execute(); err != nil { + log.WithError(err).Fatal() + } +} + +func init() { + cobra.OnInitialize(initialize) + + // Global flags + rootCmd.PersistentFlags().String("log.level", "warn", "one of debug, info, warn, error or fatal") + rootCmd.PersistentFlags().String("log.format", "text", "one of text or json") + + // Flag binding + viper.BindPFlags(rootCmd.PersistentFlags()) +} + +func initialize() { + // Environment variables + viper.SetEnvPrefix("hussar") + viper.AutomaticEnv() + + // Configuration file + viper.SetConfigName("hs-config") + viper.AddConfigPath(".") + viper.AddConfigPath("$HOME/.hussar/") + if err := viper.ReadInConfig(); err != nil { + log.Info("No valid configuration file found") + } + lvl := viper.GetString("log.level") + l, err := log.ParseLevel(lvl) + if err != nil { + log.WithField("level", lvl).Warn("Invalid log level, fallback to 'warn'") + } else { + log.SetLevel(l) + } + switch viper.GetString("log.format") { + case "json": + log.SetFormatter(&log.JSONFormatter{}) + default: + case "text": + log.SetFormatter(&log.TextFormatter{}) + } +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..3989ffe --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,109 @@ +package cmd + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/ttacon/chalk" + + "github.com/hussar-lang/hussar/evaluator" + "github.com/hussar-lang/hussar/lexer" + "github.com/hussar-lang/hussar/object" + "github.com/hussar-lang/hussar/parser" +) + +var run = &cobra.Command{ + Use: "run", + Short: "Run the given script", + Run: func(cmd *cobra.Command, args []string) { + runFromFile() + }, +} + +func init() { + run.Flags().String("source.file", "", "the source file to run") + viper.BindPFlags(run.Flags()) +} + +func runFromFile() { + file, err := getSourceFile(viper.GetString("source.file")) + if err != nil { + log.Fatal(err) + } + + l := lexer.New(string(file)) + p := parser.New(l) + program := p.ParseProgram() + + if len(p.Errors()) != 0 { + printParserErrors(os.Stderr, p.Errors()) + os.Exit(1) + } + + env := object.NewEnvironment() + eval := evaluator.Eval(program, env) + if eval.Inspect() != "NULL" { + fmt.Println(eval.Inspect()) + } +} + +func getSourceFile(sourceFile string) ([]byte, error) { + if sourceFile == "" { + cwd, err := os.Getwd() + if err != nil { + log.WithError(err).Fatal() + } + + var files []string + filepath.Walk(cwd, func(path string, f os.FileInfo, _ error) error { + if !f.IsDir() { + if filepath.Ext(path) == "hss" { + files = append(files, f.Name()) + } + } + return nil + }) + + if len(files) == 0 { + log.WithError(errors.New("no source files found in current directory")).Fatal() + } else if len(files) > 1 { + for _, f := range files { + if strings.ToLower(f) == "main.hss" { + sourceFile = f + break + } + } + + // Hack, I know + if sourceFile == "" { + log.WithError(errors.New("no main source file found in current directory - specify one with the source.file flag")).Fatal() + } + } else { + sourceFile = files[0] + } + } + + source, err := ioutil.ReadFile(sourceFile) + if err != nil { + log.Fatal(err) + } + + return source, nil +} + +func printParserErrors(out io.Writer, errors []string) { + errColor := chalk.Red.NewStyle().WithTextStyle(chalk.Bold).Style + + io.WriteString(out, errColor("PARSER ERROR!\n")) + for _, msg := range errors { + io.WriteString(out, errColor(" [!] ")+msg+"\n") + } +} diff --git a/main.go b/main.go index 19027e2..81785d6 100644 --- a/main.go +++ b/main.go @@ -1,96 +1,15 @@ package main import ( - "fmt" - "io" - "io/ioutil" - "os" - - "github.com/hussar-lang/hussar/evaluator" - "github.com/hussar-lang/hussar/lexer" - "github.com/hussar-lang/hussar/object" - "github.com/hussar-lang/hussar/parser" - "github.com/hussar-lang/hussar/repl" - - log "github.com/sirupsen/logrus" - "github.com/ttacon/chalk" - "gopkg.in/alecthomas/kingpin.v2" + "github.com/hussar-lang/hussar/cmd" ) var ( GitCommit string VersionString string - - app = kingpin.New("hussar", "The Hussar interpreter") - verbose = app.Flag("verbose", "Enable verbose logging.").Short('v').Bool() - - // TODO: run interactive mode if no subcommand was given (see #1) - interactive = app.Command("interactive", "Interactive REPL") - - run = app.Command("run", "Run Hussar code") - runFile = run.Flag("file", "Code to run").Required().Short('f').ExistingFile() ) -func init() { - log.SetFormatter(&log.TextFormatter{FullTimestamp: true}) - log.SetOutput(os.Stdout) -} - func main() { - app.Version(fmt.Sprintf("%s (%s)", VersionString, GitCommit)) - args, err := app.Parse(os.Args[1:]) - - if *verbose { - log.SetLevel(log.DebugLevel) - } else { - log.SetLevel(log.WarnLevel) - } - - switch kingpin.MustParse(args, err) { - case run.FullCommand(): - log.WithFields(log.Fields{ - "File": *runFile, - "Verbose": *verbose, - }).Debug("Received run command") - - runFromFile() - case interactive.FullCommand(): - startRepl() - } -} - -func runFromFile() { - file, err := ioutil.ReadFile(*runFile) - if err != nil { - log.Fatal(err) - } - - l := lexer.New(string(file)) - p := parser.New(l) - program := p.ParseProgram() - - if len(p.Errors()) != 0 { - printParserErrors(os.Stdout, p.Errors()) - os.Exit(21) - } - - env := object.NewEnvironment() - eval := evaluator.Eval(program, env) - if eval.Inspect() != "NULL" { - fmt.Println(eval.Inspect()) - } -} - -func startRepl() { - fmt.Printf("Starting Hussar interactive interpreter v%s\n", VersionString) - repl.Start(os.Stdin, os.Stdout) -} - -func printParserErrors(out io.Writer, errors []string) { - errColor := chalk.Red.NewStyle().WithTextStyle(chalk.Bold).Style - - io.WriteString(out, errColor("PARSER ERROR!\n")) - for _, msg := range errors { - io.WriteString(out, errColor(" [!] ")+msg+"\n") - } + cmd.Setup(VersionString, GitCommit) + cmd.Execute() }