Skip to content

fix: Parse prompt input JSON using object or array chars #538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions cli/cliui/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package cliui

import (
"bufio"
"encoding/json"
"fmt"
"io"
"os"
"os/signal"
"strings"
Expand Down Expand Up @@ -45,12 +47,22 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
} else {
reader := bufio.NewReader(cmd.InOrStdin())
line, err = reader.ReadString('\n')
// Multiline with single quotes!
if err == nil && strings.HasPrefix(line, "'") {
rest, err := reader.ReadString('\'')

// Check if the first line beings with JSON object or array chars.
// This enables multiline JSON to be pasted into an input, and have
// it parse properly.
if err == nil && (strings.HasPrefix(line, "{") || strings.HasPrefix(line, "[")) {
pipeReader, pipeWriter := io.Pipe()
defer pipeWriter.Close()
defer pipeReader.Close()
go func() {
_, _ = pipeWriter.Write([]byte(line))
_, _ = reader.WriteTo(pipeWriter)
}()
Comment on lines +55 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused about the purpose of the pipe here. Couldn't you just create a new bytes.Buffer from line and call reader.WriteTo(newBuf)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://pkg.go.dev/bufio#Reader.Peek would work well here too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, bytes.Buffer will EOF if read, which we don't want. I had that initially too!

I was going to change the reader to peek, but it's an unknown amount of bytes until a newline, so it felt a bit off!

Going to merge, but if the above doesn't make sense just LMK!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Makes sense to me!

var rawMessage json.RawMessage
err := json.NewDecoder(pipeReader).Decode(&rawMessage)
if err == nil {
line += rest
line = strings.Trim(line, "'")
line = string(rawMessage)
}
}
}
Expand Down
48 changes: 39 additions & 9 deletions cli/cliui/prompt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cliui_test

import (
"context"
"runtime"
"testing"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -47,7 +46,7 @@ func TestPrompt(t *testing.T) {
require.Equal(t, "yes", <-doneChan)
})

t.Run("Multiline", func(t *testing.T) {
t.Run("JSON", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
doneChan := make(chan string)
Expand All @@ -59,13 +58,44 @@ func TestPrompt(t *testing.T) {
doneChan <- resp
}()
ptty.ExpectMatch("Example")
ptty.WriteLine("'this is a")
ptty.WriteLine("test'")
newline := "\n"
if runtime.GOOS == "windows" {
newline = "\r\n"
}
require.Equal(t, "this is a"+newline+"test", <-doneChan)
ptty.WriteLine("{}")
require.Equal(t, "{}", <-doneChan)
})

t.Run("BadJSON", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
doneChan := make(chan string)
go func() {
resp, err := newPrompt(ptty, cliui.PromptOptions{
Text: "Example",
})
require.NoError(t, err)
doneChan <- resp
}()
ptty.ExpectMatch("Example")
ptty.WriteLine("{a")
require.Equal(t, "{a", <-doneChan)
})

t.Run("MultilineJSON", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
doneChan := make(chan string)
go func() {
resp, err := newPrompt(ptty, cliui.PromptOptions{
Text: "Example",
})
require.NoError(t, err)
doneChan <- resp
}()
ptty.ExpectMatch("Example")
ptty.WriteLine(`{
"test": "wow"
}`)
require.Equal(t, `{
"test": "wow"
}`, <-doneChan)
})
}

Expand Down
3 changes: 2 additions & 1 deletion cli/workspacecreate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package cli_test
import (
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/pty/ptytest"
"github.com/stretchr/testify/require"
)

func TestWorkspaceCreate(t *testing.T) {
Expand Down
3 changes: 2 additions & 1 deletion database/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import (
"sync"
"time"

"github.com/coder/coder/cryptorand"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"golang.org/x/xerrors"

"github.com/coder/coder/cryptorand"
)

// Required to prevent port collision during container creation.
Expand Down