Skip to content

Commit

Permalink
restructure otti package, improve integrated help
Browse files Browse the repository at this point in the history
  • Loading branch information
xrstf committed Nov 19, 2023
1 parent 2a51a2d commit 097a40e
Show file tree
Hide file tree
Showing 14 changed files with 303 additions and 157 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ go install go.xrstf.de/otto/cmd/otti
Alternatively, you can download the [latest release](https://github.com/xrstf/otto/releases/latest)
from GitHub.

## Usage

Otti has extensive help built right into it, try running `otti help` to get started.

## Embedding

Otto is well suited to be embedded into Go applications. A clean and simple API makes it a breeze:
Expand Down
52 changes: 23 additions & 29 deletions cmd/otti/console.go → cmd/otti/cmd/console/command.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 Christoph Mewes
// SPDX-License-Identifier: MIT

package main
package console

import (
_ "embed"
Expand All @@ -13,61 +13,55 @@ import (

"github.com/chzyer/readline"
"go.xrstf.de/otto"
cmdtypes "go.xrstf.de/otto/cmd/otti/types"
"go.xrstf.de/otto/cmd/otti/util"
"go.xrstf.de/otto/docs"
"go.xrstf.de/otto/pkg/eval/types"
)

//go:embed help.txt
//go:embed help.md
var helpText string

func printPrompt() {
fmt.Print("⮞ ")
}

func displayHelp(ctx types.Context, helpTopics []docs.Topic, opts *options) error {
var builder strings.Builder
builder.WriteString(strings.TrimSpace(helpText))
builder.WriteString("\n\n")
func helpCommand(ctx types.Context, helpTopics []docs.Topic, opts *cmdtypes.Options) error {
fmt.Println(util.RenderMarkdown(strings.TrimSpace(helpText), 2))
fmt.Println(util.RenderHelpTopics(helpTopics, 2))

width := 0
for _, topic := range helpTopics {
if l := len(topic.CliNames[0]); l > width {
width = l
}
}
return nil
}

format := fmt.Sprintf("* %%-%ds – %%s\n", width)
for _, topic := range helpTopics {
builder.WriteString(fmt.Sprintf(format, topic.CliNames[0], topic.Description))
func helpTopicCommand(helpTopics []docs.Topic, topic string) error {
rendered, err := util.RenderHelpTopic(helpTopics, topic, 2)
if err != nil {
return err
}

printMarkdown(builder.String())
fmt.Println(rendered)

return nil
}

func cleanInput(text string) string {
return strings.TrimSpace(text)
}

type replCommandFunc func(ctx types.Context, helpTopics []docs.Topic, opts *options) error
type replCommandFunc func(ctx types.Context, helpTopics []docs.Topic, opts *cmdtypes.Options) error

var replCommands = map[string]replCommandFunc{
"help": displayHelp,
"help": helpCommand,
}

func runConsole(opts *options, args []string) error {
func Run(opts *cmdtypes.Options, args []string) error {
rl, err := readline.New("⮞ ")
if err != nil {
return fmt.Errorf("failed to setup readline prompt: %w", err)
}

files, err := loadFiles(opts, args)
files, err := util.LoadFiles(opts, args)
if err != nil {
return fmt.Errorf("failed to read inputs: %w", err)
}

ctx, err := setupOttoContext(files)
ctx, err := util.SetupOttoContext(files)
if err != nil {
return fmt.Errorf("failed to setup context: %w", err)
}
Expand All @@ -83,12 +77,12 @@ func runConsole(opts *options, args []string) error {
if err != nil { // io.EOF
break
}
line = cleanInput(line)
line = strings.TrimSpace(line)
if line == "" {
continue
}

newCtx, stop, err := processReplInput(ctx, helpTopics, opts, line)
newCtx, stop, err := processInput(ctx, helpTopics, opts, line)
if err != nil {
parseErr := &otto.ParseError{}
if errors.As(err, parseErr) {
Expand All @@ -110,14 +104,14 @@ func runConsole(opts *options, args []string) error {
return nil
}

func processReplInput(ctx types.Context, helpTopics []docs.Topic, opts *options, input string) (newCtx types.Context, stop bool, err error) {
func processInput(ctx types.Context, helpTopics []docs.Topic, opts *cmdtypes.Options, input string) (newCtx types.Context, stop bool, err error) {
if command, exists := replCommands[input]; exists {
return ctx, false, command(ctx, helpTopics, opts)
}

if prefix := "help "; strings.HasPrefix(input, prefix) {
topicName := strings.TrimPrefix(input, prefix)
return ctx, false, renderHelpTopic(helpTopics, topicName)
return ctx, false, helpTopicCommand(helpTopics, topicName)
}

if strings.EqualFold("exit", input) {
Expand Down
File renamed without changes.
47 changes: 47 additions & 0 deletions cmd/otti/cmd/help/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2023 Christoph Mewes
// SPDX-License-Identifier: MIT

package help

import (
_ "embed"
"fmt"
"strings"

"github.com/spf13/pflag"
"go.xrstf.de/otto/cmd/otti/types"
"go.xrstf.de/otto/cmd/otti/util"
"go.xrstf.de/otto/docs"
)

//go:embed help.md
var helpText string

func Run(opts *types.Options, args []string) error {
helpTopics := docs.Topics()

// do not show function docs for "--help help if"
if !opts.ShowHelp && len(args) == 2 && args[0] == "help" {
rendered, err := util.RenderHelpTopic(helpTopics, args[1], 0)
if err == nil {
fmt.Println(rendered)
return nil
}

fmt.Printf("Error: %v\n", err)
fmt.Println()
fmt.Println("The following topics are available:")
fmt.Println()
fmt.Println(util.RenderHelpTopics(helpTopics, 0))

return nil
}

fmt.Println(util.RenderMarkdown(strings.TrimSpace(helpText), 0))
fmt.Println(util.RenderHelpTopics(helpTopics, 0))
fmt.Println()

pflag.Usage()

return nil
}
35 changes: 35 additions & 0 deletions cmd/otti/cmd/help/help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# The Otto interpreter :)

Otti is a command line interpreter for the Otto programming language. Otti can
read multiple JSON/YAML files and then apply JSON paths or scripts to them. For
quicker development, an interactive REPL is also available.

## Modes

Otti can run in one of two modes:

* **Interactive Mode** is enabled by passing `--interactive` (or `-i`). This will
start a REPL session where Otto scripts are read from stdin and evaluated
against the loaded files.
* **Script Mode** is used the an Otto script is passed either as the first
argument or read from a file defined by `--script`. In this mode Otti will
run all statements from the script and print the resulting value, then it exits.

Examples:

* `otti '.foo' myfile.json`
* `otti '(set .foo "bar") (set .users 42) .' myfile.json`
* `otti --script convert.otto myfile.json`

## File Handling

The first loaded file is known as the "document". Its content is available via
path expressions like `.foo[0]`. All loaded files are also available via the
`$files` variable (i.e. `.` is the same as `$files[0]` for reading, but when
writing data, there is a difference between both notations; refer to the docs
for `set` for more information).

## Help

Help is available by using `help` as the first argument to Otto. This can be
followed by a topic, like `help if`.
24 changes: 13 additions & 11 deletions cmd/otti/script.go → cmd/otti/cmd/script/command.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 Christoph Mewes
// SPDX-License-Identifier: MIT

package main
package script

import (
"encoding/json"
Expand All @@ -11,24 +11,26 @@ import (
"strings"

"go.xrstf.de/otto"
"go.xrstf.de/otto/cmd/otti/types"
"go.xrstf.de/otto/cmd/otti/util"
"go.xrstf.de/otto/pkg/debug"

"gopkg.in/yaml.v3"
)

func runScript(opts *options, args []string) error {
func Run(opts *types.Options, args []string) error {
// determine input script to evaluate
script := ""
scriptName := ""

if opts.scriptFile != "" {
content, err := os.ReadFile(opts.scriptFile)
if opts.ScriptFile != "" {
content, err := os.ReadFile(opts.ScriptFile)
if err != nil {
return fmt.Errorf("failed to read script from %s: %w", opts.scriptFile, err)
return fmt.Errorf("failed to read script from %s: %w", opts.ScriptFile, err)
}

script = strings.TrimSpace(string(content))
scriptName = opts.scriptFile
scriptName = opts.ScriptFile
} else {
if len(args) == 0 {
return errors.New("no script provided either via argument or --script")
Expand All @@ -47,7 +49,7 @@ func runScript(opts *options, args []string) error {
}

// show AST and quit if desired
if opts.printAst {
if opts.PrintAst {
if err := debug.Dump(program, os.Stdout); err != nil {
return fmt.Errorf("failed to dump AST: %w", err)
}
Expand All @@ -56,13 +58,13 @@ func runScript(opts *options, args []string) error {
}

// load all remaining args as input files
files, err := loadFiles(opts, args)
files, err := util.LoadFiles(opts, args)
if err != nil {
return fmt.Errorf("failed to read inputs: %w", err)
}

// setup the evaluation context
ctx, err := setupOttoContext(files)
ctx, err := util.SetupOttoContext(files)
if err != nil {
return fmt.Errorf("failed to setup context: %w", err)
}
Expand All @@ -78,12 +80,12 @@ func runScript(opts *options, args []string) error {
Encode(v any) error
}

if opts.formatYaml {
if opts.FormatYaml {
encoder = yaml.NewEncoder(os.Stdout)
encoder.(*yaml.Encoder).SetIndent(2)
} else {
encoder = json.NewEncoder(os.Stdout)
if opts.prettyPrint {
if opts.PrettyPrint {
encoder.(*json.Encoder).SetIndent("", " ")
}
}
Expand Down
21 changes: 21 additions & 0 deletions cmd/otti/cmd/script/help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Welcome to the Otto interpreter :)

You can enter one of

* A path expression, like `.foo` or `.foo[0].bar` to access the global document.
* An expression like (+ .foo 42) to compute data by functions; see the topics
below or the Otto website for a complete list of available functions.
* A scalar JSON value, like `3` or `[1 2 3]`, which will simply return that
exact value with no further side effects. Not super useful usually.

## Commands

Additionally, the following commands can be used:

* help – Show this help text.
* help TOPIC – Show help for a specific topic.
* exit – Exit Otti immediately.

## Help Topics

The following topics are available and can be accessed using `help TOPIC`:
36 changes: 0 additions & 36 deletions cmd/otti/help.go

This file was deleted.

Loading

0 comments on commit 097a40e

Please sign in to comment.