diff --git a/docs/content/docs/guide/cli.md b/docs/content/docs/guide/cli.md
index 1c7609ef6..efba9b122 100644
--- a/docs/content/docs/guide/cli.md
+++ b/docs/content/docs/guide/cli.md
@@ -409,6 +409,111 @@ You can specify a different directory than the current one by using the -d/--dir
{{< /details >}}
+{{< details "tkn pac cel" >}}
+
+### CEL Expression Evaluator
+
+`tkn pac cel` — Evaluate CEL (Common Expression Language) expressions interactively with webhook payloads.
+
+This command allows you to test and debug CEL expressions as they would be evaluated by Pipelines-as-Code, using real webhook payloads and headers. It supports interactive and non-interactive modes, provider auto-detection, and persistent history.
+
+To be able to have the CEL evaluator working, you need to have the payload and the headers available in a file. The best way to do this is to go to the webhook configuration on your git provider and copy the payload and headers to different files.
+
+The payload is the JSON content of the webhook request, The headers file supports multiple formats:
+
+1. **Plain HTTP headers format** (as shown above)
+2. **JSON format**:
+
+ ```json
+ {
+ "X-GitHub-Event": "pull_request",
+ "Content-Type": "application/json",
+ "User-Agent": "GitHub-Hookshot/2d5e4d4"
+ }
+ ```
+
+3. **Gosmee-generated shell scripts**: The command automatically detects and parses shell scripts generated by [gosmee](https://github.com/chmouel/gosmee) which are generated when using the `--save` feature, extracting headers from curl commands with `-H` flags:
+
+ ```bash
+ #!/usr/bin/env bash
+ curl -X POST "http://localhost:8080/" \
+ -H "X-GitHub-Event: pull_request" \
+ -H "Content-Type: application/json" \
+ -H "User-Agent: GitHub-Hookshot/2d5e4d4" \
+ -d @payload.json
+ ```
+
+#### Usage
+
+```shell
+tkn pac cel -b
-H
+```
+
+* `-b, --body`: Path to JSON body file (webhook payload)
+* `-H, --headers`: Path to headers file (plain text, JSON, or gosmee script)
+* `-p, --provider`: Provider (auto, github, gitlab, bitbucket-cloud, bitbucket-datacenter, gitea)
+
+#### Interactive Mode
+
+If run in a terminal, you'll get a prompt:
+
+```console
+CEL expression>
+```
+
+* Use ↑/↓ arrows to navigate history.
+* History is saved and loaded automatically.
+* Press Enter on an empty line to exit.
+
+#### Non-Interactive Mode
+
+Pipe expressions via stdin:
+
+```shell
+echo 'event == "pull_request"' | tkn pac cel -b body.json -H headers.txt
+```
+
+#### Available Variables
+
+* **Direct variables** (top-level, as per PAC documentation):
+ * `event` — event type (push, pull_request)
+ * `target_branch` — target branch name
+ * `source_branch` — source branch name
+ * `target_url` — target repository URL
+ * `source_url` — source repository URL
+ * `event_title` — PR title or commit message
+
+* **Webhook payload** (`body.*`): All fields from the webhook JSON.
+* **HTTP headers** (`headers.*`): All HTTP headers.
+* **Files** (`files.*`): Always empty in CLI mode.
+ **Note:** `fileChanged`, `fileDeleted`, `fileModified` and similar functions are **not implemented yet** in the CLI.
+* **PAC Parameters** (`pac.*`): All variables for backward compatibility.
+
+#### Example Expressions
+
+```text
+event == "pull_request" && target_branch == "main"
+event == "pull_request" && source_branch.matches(".*feat/.*")
+body.action == "synchronize"
+!body.pull_request.draft
+headers['x-github-event'] == "pull_request"
+event == "pull_request" && target_branch != "experimental"
+```
+
+#### Limitations
+
+* `files.*` variables are always empty in CLI mode.
+* Functions like `fileChanged`, `fileDeleted`, `fileModified` are **not implemented yet** in the CLI.
+
+#### Cross-Platform History
+
+* History is saved in a cache directory:
+ * Linux/macOS: `~/.cache/tkn-pac/cel-history`
+ * Windows: `%USERPROFILE%\.cache\tkn-pac\cel-history`
+* The directory is created automatically if it does not exist.
+
+{{< /details >}}
+
## Screenshot

diff --git a/go.mod b/go.mod
index c3b182963..0f1460966 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
code.gitea.io/sdk/gitea v0.21.0
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/bradleyfalzon/ghinstallation/v2 v2.15.0
+ github.com/chzyer/readline v1.5.1
github.com/cloudevents/sdk-go/v2 v2.16.0
github.com/fvbommel/sortorder v1.1.0
github.com/gobwas/glob v0.2.3
@@ -129,7 +130,7 @@ require (
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.33.0 // indirect
- golang.org/x/term v0.31.0 // indirect
+ golang.org/x/term v0.31.0
golang.org/x/time v0.11.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/api v0.231.0 // indirect
diff --git a/go.sum b/go.sum
index 7628e2917..412cac530 100644
--- a/go.sum
+++ b/go.sum
@@ -88,8 +88,14 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
+github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
+github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
+github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.15.2 h1:AbtPqiUDzKup5JpTZzO297/QXgL/TAdpdXQCNwLzlaM=
github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.15.2/go.mod h1:ZbYLE+yaEQ2j4vbRc9qzvGmg30A9LhwFt/1bSebNnbU=
@@ -675,6 +681,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/pkg/cel/cel.go b/pkg/cel/cel.go
index e53a8034f..a4f316884 100644
--- a/pkg/cel/cel.go
+++ b/pkg/cel/cel.go
@@ -55,12 +55,26 @@ func Value(query string, body any, headers, pacParams map[string]string, changed
decls.NewVariable("headers", mapStrDyn),
decls.NewVariable("pac", mapStrDyn),
decls.NewVariable("files", mapStrDyn),
+ // Direct variables as per documentation
+ decls.NewVariable("event", types.StringType),
+ decls.NewVariable("target_branch", types.StringType),
+ decls.NewVariable("source_branch", types.StringType),
+ decls.NewVariable("target_url", types.StringType),
+ decls.NewVariable("source_url", types.StringType),
+ decls.NewVariable("event_title", types.StringType),
))
val, err := evaluate(query, celDec, map[string]any{
"body": jsonMap,
"pac": pacParams,
"headers": headers,
"files": changedFiles,
+ // Direct variables
+ "event": pacParams["event"],
+ "target_branch": pacParams["target_branch"],
+ "source_branch": pacParams["source_branch"],
+ "target_url": pacParams["target_url"],
+ "source_url": pacParams["source_url"],
+ "event_title": pacParams["event_title"],
})
if err != nil {
return nil, err
diff --git a/pkg/cmd/tknpac/cel/cel.go b/pkg/cmd/tknpac/cel/cel.go
new file mode 100644
index 000000000..f7b316df1
--- /dev/null
+++ b/pkg/cmd/tknpac/cel/cel.go
@@ -0,0 +1,1099 @@
+package cel
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "maps"
+ "net/http"
+ "os"
+ "path"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ _ "embed"
+
+ giteaStructs "code.gitea.io/gitea/modules/structs"
+ "github.com/AlecAivazis/survey/v2"
+ "github.com/chzyer/readline"
+ "github.com/google/go-github/v71/github"
+ pkgcel "github.com/openshift-pipelines/pipelines-as-code/pkg/cel"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/formatting"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/provider/bitbucketcloud/types"
+ providergh "github.com/openshift-pipelines/pipelines-as-code/pkg/provider/github"
+ "github.com/spf13/cobra"
+ gitlab "gitlab.com/gitlab-org/api/client-go"
+ "golang.org/x/term"
+)
+
+const (
+ bodyFileFlag = "body"
+ headersFileFlag = "headers"
+ providerFlag = "provider"
+ githubTokenFlag = "github-token"
+)
+
+//go:embed templates/help.tmpl
+var helpString string
+
+// getHistoryFilePath returns the cross-platform path for the CEL history file.
+func getHistoryFilePath() (string, error) {
+ // Get user's home directory (works on Windows, macOS, Linux)
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+
+ // Create the cache directory path
+ cacheDir := filepath.Join(homeDir, ".cache", "tkn-pac")
+
+ // Ensure the directory exists
+ if err := os.MkdirAll(cacheDir, 0o755); err != nil {
+ return "", err
+ }
+
+ // Return the full path to the history file
+ return filepath.Join(cacheDir, "cel-history"), nil
+}
+
+func parseHTTPHeaders(s string) (map[string]string, error) {
+ headers := make(map[string]string)
+ scanner := bufio.NewScanner(strings.NewReader(s))
+ for scanner.Scan() {
+ line := scanner.Text()
+ if strings.TrimSpace(line) == "" {
+ continue
+ }
+ parts := strings.SplitN(line, ":", 2)
+ if len(parts) != 2 {
+ continue // or return error if strict
+ }
+ key := strings.TrimSpace(parts[0])
+ value := strings.TrimSpace(parts[1])
+ headers[key] = value
+ }
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+ return headers, nil
+}
+
+// getHeaderCaseInsensitive performs case-insensitive header lookup.
+func getHeaderCaseInsensitive(headers map[string]string, key string) string {
+ // First try exact match
+ if value, ok := headers[key]; ok {
+ return value
+ }
+ // Then try case-insensitive match
+ lowerKey := strings.ToLower(key)
+ for k, v := range headers {
+ if strings.ToLower(k) == lowerKey {
+ return v
+ }
+ }
+ return ""
+}
+
+// parseCurlHeaders extracts headers from a curl command string.
+// This function parses curl commands like those generated by gosmee,
+// extracting -H "Header: Value" flags.
+func parseCurlHeaders(curlCommand string) (map[string]string, error) {
+ headers := make(map[string]string)
+
+ // Split the command into tokens, handling quoted strings
+ tokens, err := splitCurlCommand(curlCommand)
+ if err != nil {
+ return nil, err
+ }
+
+ // Look for -H flags followed by header values
+ for i := 0; i < len(tokens); i++ {
+ if tokens[i] == "-H" && i+1 < len(tokens) {
+ headerValue := tokens[i+1]
+ // Parse "Header: Value" format
+ if parts := strings.SplitN(headerValue, ":", 2); len(parts) == 2 {
+ key := strings.TrimSpace(parts[0])
+ value := strings.TrimSpace(parts[1])
+ headers[key] = value
+ }
+ i++ // Skip the header value token
+ }
+ }
+
+ return headers, nil
+}
+
+// splitCurlCommand splits a curl command string into tokens, properly handling quoted strings.
+func splitCurlCommand(command string) ([]string, error) {
+ var tokens []string
+ var current strings.Builder
+ inQuotes := false
+ quoteChar := byte(0)
+
+ for i := 0; i < len(command); i++ {
+ char := command[i]
+
+ switch {
+ case !inQuotes && (char == '"' || char == '\''):
+ inQuotes = true
+ quoteChar = char
+ case inQuotes && char == quoteChar:
+ inQuotes = false
+ quoteChar = 0
+ case !inQuotes && (char == ' ' || char == '\t' || char == '\n'):
+ if current.Len() > 0 {
+ tokens = append(tokens, current.String())
+ current.Reset()
+ }
+ default:
+ current.WriteByte(char)
+ }
+ }
+
+ if inQuotes {
+ return nil, fmt.Errorf("unterminated quote in curl command")
+ }
+
+ if current.Len() > 0 {
+ tokens = append(tokens, current.String())
+ }
+
+ // Ensure we always return a non-nil slice
+ if tokens == nil {
+ tokens = []string{}
+ }
+
+ return tokens, nil
+}
+
+// isGosmeeScript detects if the content appears to be a gosmee-generated shell script.
+// It looks for patterns like "curl" commands with typical gosmee characteristics.
+func isGosmeeScript(content string) bool {
+ lines := strings.Split(content, "\n")
+ for _, line := range lines {
+ trimmed := strings.TrimSpace(line)
+ // Look for curl commands that contain -H flags (typical of webhook scripts)
+ if strings.HasPrefix(trimmed, "curl") && strings.Contains(trimmed, "-H") {
+ return true
+ }
+ }
+ return false
+}
+
+// parseGosmeeScript extracts headers from a gosmee-generated shell script.
+// It finds curl commands and extracts headers from their -H flags.
+func parseGosmeeScript(content string) (map[string]string, error) {
+ headers := make(map[string]string)
+ lines := strings.Split(content, "\n")
+
+ for _, line := range lines {
+ trimmed := strings.TrimSpace(line)
+ if strings.HasPrefix(trimmed, "curl") && strings.Contains(trimmed, "-H") {
+ // Parse headers from this curl command
+ curlHeaders, err := parseCurlHeaders(trimmed)
+ if err != nil {
+ continue // Skip malformed curl commands
+ }
+ // Merge headers (later commands override earlier ones)
+ maps.Copy(headers, curlHeaders)
+ }
+ }
+
+ if len(headers) == 0 {
+ return nil, fmt.Errorf("no headers found in gosmee script")
+ }
+
+ return headers, nil
+}
+
+func eventFromGitHub(body []byte, headers map[string]string) (*info.Event, error) {
+ event := info.NewEvent()
+ event.EventType = getHeaderCaseInsensitive(headers, "X-GitHub-Event")
+ event.Request.Payload = body
+ event.Request.Header = http.Header{}
+ for k, v := range headers {
+ event.Request.Header.Set(k, v)
+ }
+
+ ghEvent, err := github.ParseWebHook(event.EventType, body)
+ if err != nil {
+ return nil, err
+ }
+
+ // Store the parsed GitHub event for CEL body access
+ event.Event = ghEvent
+
+ switch e := ghEvent.(type) {
+ case *github.PushEvent:
+ event.TriggerTarget = triggertype.Push
+ event.Organization = e.GetRepo().GetOwner().GetLogin()
+ event.Repository = e.GetRepo().GetName()
+ event.DefaultBranch = e.GetRepo().GetDefaultBranch()
+ event.URL = e.GetRepo().GetHTMLURL()
+ sha := e.GetHeadCommit().GetID()
+ if sha == "" {
+ sha = e.GetAfter()
+ }
+ event.SHA = sha
+ event.SHAURL = e.GetHeadCommit().GetURL()
+ event.SHATitle = e.GetHeadCommit().GetMessage()
+ event.Sender = e.GetSender().GetLogin()
+ event.BaseBranch = e.GetRef()
+ event.HeadBranch = event.BaseBranch
+ event.BaseURL = event.URL
+ event.HeadURL = event.URL
+ case *github.PullRequestEvent:
+ event.TriggerTarget = triggertype.PullRequest
+ event.Organization = e.GetRepo().GetOwner().GetLogin()
+ event.Repository = e.GetRepo().GetName()
+ event.DefaultBranch = e.GetRepo().GetDefaultBranch()
+ event.URL = e.GetRepo().GetHTMLURL()
+ event.SHA = e.GetPullRequest().Head.GetSHA()
+ event.BaseBranch = e.GetPullRequest().Base.GetRef()
+ event.HeadBranch = e.GetPullRequest().Head.GetRef()
+ event.BaseURL = e.GetPullRequest().Base.GetRepo().GetHTMLURL()
+ event.HeadURL = e.GetPullRequest().Head.GetRepo().GetHTMLURL()
+ event.Sender = e.GetPullRequest().GetUser().GetLogin()
+ event.PullRequestNumber = e.GetPullRequest().GetNumber()
+ event.PullRequestTitle = e.GetPullRequest().GetTitle()
+ for _, l := range e.GetPullRequest().Labels {
+ event.PullRequestLabel = append(event.PullRequestLabel, l.GetName())
+ }
+ case *github.IssueCommentEvent:
+ event.TriggerTarget = triggertype.PullRequest
+ if e.GetRepo() != nil {
+ event.Organization = e.GetRepo().GetOwner().GetLogin()
+ event.Repository = e.GetRepo().GetName()
+ event.DefaultBranch = e.GetRepo().GetDefaultBranch()
+ event.URL = e.GetRepo().GetHTMLURL()
+ }
+ event.Sender = e.GetSender().GetLogin()
+ event.TriggerComment = e.GetComment().GetBody()
+ if pr := e.GetIssue().GetPullRequestLinks(); pr != nil {
+ num, err := strconv.Atoi(path.Base(pr.GetHTMLURL()))
+ if err == nil {
+ event.PullRequestNumber = num
+ }
+ }
+ case *github.CommitCommentEvent:
+ event.TriggerTarget = triggertype.Push
+ event.Organization = e.GetRepo().GetOwner().GetLogin()
+ event.Repository = e.GetRepo().GetName()
+ event.DefaultBranch = e.GetRepo().GetDefaultBranch()
+ event.URL = e.GetRepo().GetHTMLURL()
+ event.Sender = e.GetSender().GetLogin()
+ event.SHA = e.GetComment().GetCommitID()
+ event.SHAURL = e.GetComment().GetHTMLURL()
+ event.HeadBranch = event.DefaultBranch
+ event.BaseBranch = event.DefaultBranch
+ event.HeadURL = event.URL
+ event.BaseURL = event.URL
+ event.TriggerComment = e.GetComment().GetBody()
+ default:
+ return nil, fmt.Errorf("unsupported github event %T", e)
+ }
+ return event, nil
+}
+
+// eventFromGitHubWithProvider uses the actual GitHub provider to parse events with proper enrichment.
+// This ensures consistency with the controller's event processing, but adapted for CLI usage.
+func eventFromGitHubWithProvider(body []byte, headers map[string]string, githubToken string) (*info.Event, error) {
+ // If no token provided, fall back to basic parsing
+ if githubToken == "" {
+ return eventFromGitHub(body, headers)
+ }
+
+ // Start with basic parsing to get initial event structure
+ event, err := eventFromGitHub(body, headers)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create a GitHub client for API calls
+ ctx := context.Background()
+ client, _, _ := providergh.MakeClient(ctx, "", githubToken)
+
+ // Parse the GitHub webhook to get the typed event
+ ghEvent, err := github.ParseWebHook(event.EventType, body)
+ if err != nil {
+ return event, nil // nolint: nilerr // fallback to basic parsing
+ }
+
+ // For specific event types that benefit from API enrichment, enhance them
+ switch e := ghEvent.(type) {
+ case *github.IssueCommentEvent:
+ // For issue comments, we need to fetch PR details to get proper branch info
+ if !e.GetIssue().IsPullRequest() {
+ return event, nil // not a PR comment
+ }
+
+ prURL := e.GetIssue().GetPullRequestLinks().GetHTMLURL()
+ prNumber, err := strconv.Atoi(path.Base(prURL))
+ if err != nil {
+ return event, nil // nolint: nilerr // fallback to basic parsing if URL parsing fails
+ }
+
+ // Fetch PR details
+ pr, _, err := client.PullRequests.Get(ctx, event.Organization, event.Repository, prNumber)
+ if err != nil {
+ return event, nil // nolint: nilerr // fallback to basic parsing if API call fails
+ }
+
+ // Populate the missing fields that would be enriched by the provider
+ event.SHA = pr.GetHead().GetSHA()
+ event.SHAURL = fmt.Sprintf("%s/commit/%s", pr.GetHTMLURL(), pr.GetHead().GetSHA())
+ event.PullRequestTitle = pr.GetTitle()
+ event.HeadBranch = pr.GetHead().GetRef()
+ event.BaseBranch = pr.GetBase().GetRef()
+ event.HeadURL = pr.GetHead().GetRepo().GetHTMLURL()
+ event.BaseURL = pr.GetBase().GetRepo().GetHTMLURL()
+ event.DefaultBranch = pr.GetBase().GetRepo().GetDefaultBranch()
+ event.URL = pr.GetBase().GetRepo().GetHTMLURL()
+
+ // Add PR labels
+ event.PullRequestLabel = nil // clear any existing labels
+ for _, label := range pr.Labels {
+ event.PullRequestLabel = append(event.PullRequestLabel, label.GetName())
+ }
+
+ return event, nil
+ default:
+ // For other events, basic parsing is sufficient
+ return event, nil
+ }
+}
+
+func pacParamsFromEvent(event *info.Event) map[string]string {
+ repoURL := event.URL
+ if event.CloneURL != "" {
+ repoURL = event.CloneURL
+ }
+ gitTag := ""
+ if after, ok := strings.CutPrefix(event.BaseBranch, "refs/tags/"); ok {
+ gitTag = after
+ }
+ triggerComment := strings.ReplaceAll(strings.ReplaceAll(event.TriggerComment, "\r\n", "\\n"), "\n", "\\n")
+ pullRequestLabels := strings.Join(event.PullRequestLabel, "\n")
+
+ // Get event title based on trigger type
+ eventTitle := event.PullRequestTitle
+ if event.TriggerTarget == triggertype.Push {
+ eventTitle = event.SHATitle
+ }
+
+ return map[string]string{
+ "revision": event.SHA,
+ "repo_url": repoURL,
+ "repo_owner": strings.ToLower(event.Organization),
+ "repo_name": strings.ToLower(event.Repository),
+ "target_branch": formatting.SanitizeBranch(event.BaseBranch),
+ "source_branch": formatting.SanitizeBranch(event.HeadBranch),
+ "git_tag": gitTag,
+ "source_url": event.HeadURL,
+ "target_url": event.BaseURL,
+ "sender": strings.ToLower(event.Sender),
+ "target_namespace": "",
+ "event_type": event.EventType,
+ "event": event.TriggerTarget.String(),
+ "event_title": eventTitle,
+ "trigger_comment": triggerComment,
+ "pull_request_labels": pullRequestLabels,
+ }
+}
+
+// detectProvider automatically detects the provider from headers and payload.
+func detectProvider(headers map[string]string, body []byte) (string, error) {
+ // Check for GitHub provider (most common)
+ if getHeaderCaseInsensitive(headers, "X-GitHub-Event") != "" {
+ // Check if it's actually Gitea (which also sets X-GitHub-Event)
+ if getHeaderCaseInsensitive(headers, "X-Gitea-Event-Type") != "" {
+ return "gitea", nil
+ }
+ return "github", nil
+ }
+
+ // Check for GitLab provider
+ if getHeaderCaseInsensitive(headers, "X-Gitlab-Event") != "" {
+ return "gitlab", nil
+ }
+
+ // Check for Bitbucket Cloud (uses User-Agent header)
+ if userAgent := getHeaderCaseInsensitive(headers, "User-Agent"); userAgent != "" {
+ if strings.Contains(strings.ToLower(userAgent), "bitbucket") {
+ // Try to distinguish between Cloud and Data Center by payload structure
+ var payload map[string]any
+ if json.Unmarshal(body, &payload) == nil {
+ if actor, ok := payload["actor"].(map[string]any); ok {
+ if _, hasAccountID := actor["account_id"]; hasAccountID {
+ return "bitbucket-cloud", nil
+ }
+ // Heuristic: if it has an `id` but not an `account_id`, assume it's Data Center
+ if _, hasID := actor["id"]; hasID {
+ return "bitbucket-datacenter", nil
+ }
+ }
+ }
+ // Default to cloud if we can't determine
+ return "bitbucket-cloud", nil
+ }
+ }
+
+ // Check for Gitea provider (backup check in case header is missing)
+ if getHeaderCaseInsensitive(headers, "X-Gitea-Event-Type") != "" {
+ return "gitea", nil
+ }
+
+ // Try to detect from payload structure as last resort
+ var payload map[string]any
+ if json.Unmarshal(body, &payload) == nil {
+ // GitHub-like structure
+ if repository, ok := payload["repository"].(map[string]any); ok {
+ if htmlURL, ok := repository["html_url"].(string); ok {
+ if strings.Contains(htmlURL, "github.com") {
+ return "github", nil
+ }
+ if strings.Contains(htmlURL, "gitlab.com") || strings.Contains(htmlURL, "gitlab") {
+ return "gitlab", nil
+ }
+ }
+ }
+
+ // GitLab-specific structure
+ if project, ok := payload["project"].(map[string]any); ok {
+ if webURL, ok := project["web_url"].(string); ok {
+ if strings.Contains(webURL, "gitlab") {
+ return "gitlab", nil
+ }
+ }
+ }
+
+ // Bitbucket-specific structure
+ if repository, ok := payload["repository"].(map[string]any); ok {
+ if links, ok := repository["links"].(map[string]any); ok {
+ if html, ok := links["html"].(map[string]any); ok {
+ if href, ok := html["href"].(string); ok {
+ if strings.Contains(href, "bitbucket") {
+ return "bitbucket-cloud", nil
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return "", fmt.Errorf("unable to detect provider from headers or payload")
+}
+
+func eventFromGitLab(body []byte, headers map[string]string) (*info.Event, error) {
+ event := info.NewEvent()
+ event.EventType = getHeaderCaseInsensitive(headers, "X-Gitlab-Event")
+ event.Request.Payload = body
+ event.Request.Header = http.Header{}
+ for k, v := range headers {
+ event.Request.Header.Set(k, v)
+ }
+
+ // Parse GitLab webhook payload
+ eventInt, err := gitlab.ParseWebhook(gitlab.EventType(event.EventType), body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse GitLab webhook: %w", err)
+ }
+
+ event.Event = eventInt
+
+ // Extract common event information from GitLab payload
+ switch gitEvent := eventInt.(type) {
+ case *gitlab.MergeEvent:
+ event.Organization = extractOrgFromPath(gitEvent.Project.PathWithNamespace)
+ event.Repository = extractRepoFromPath(gitEvent.Project.PathWithNamespace)
+ event.Sender = gitEvent.User.Username
+ event.URL = gitEvent.Project.WebURL
+ event.SHA = gitEvent.ObjectAttributes.LastCommit.ID
+ event.HeadBranch = gitEvent.ObjectAttributes.SourceBranch
+ event.BaseBranch = gitEvent.ObjectAttributes.TargetBranch
+ event.PullRequestNumber = gitEvent.ObjectAttributes.IID
+ event.PullRequestTitle = gitEvent.ObjectAttributes.Title
+ event.TriggerTarget = triggertype.PullRequest
+ if gitEvent.ObjectAttributes.Action == "close" {
+ event.TriggerTarget = triggertype.PullRequestClosed
+ }
+ case *gitlab.PushEvent:
+ if len(gitEvent.Commits) == 0 {
+ return nil, fmt.Errorf("no commits attached to this push event")
+ }
+ lastCommitIdx := len(gitEvent.Commits) - 1
+ event.Organization = extractOrgFromPath(gitEvent.Project.PathWithNamespace)
+ event.Repository = extractRepoFromPath(gitEvent.Project.PathWithNamespace)
+ event.Sender = gitEvent.UserUsername
+ event.URL = gitEvent.Project.WebURL
+ event.SHA = gitEvent.Commits[lastCommitIdx].ID
+ event.SHATitle = gitEvent.Commits[lastCommitIdx].Title
+ event.HeadBranch = gitEvent.Ref
+ event.BaseBranch = gitEvent.Ref
+ event.TriggerTarget = triggertype.Push
+ case *gitlab.TagEvent:
+ if len(gitEvent.Commits) == 0 {
+ return nil, fmt.Errorf("no commits attached to this tag event")
+ }
+ lastCommitIdx := len(gitEvent.Commits) - 1
+ event.Organization = extractOrgFromPath(gitEvent.Project.PathWithNamespace)
+ event.Repository = extractRepoFromPath(gitEvent.Project.PathWithNamespace)
+ event.Sender = gitEvent.UserUsername
+ event.URL = gitEvent.Project.WebURL
+ event.SHA = gitEvent.Commits[lastCommitIdx].ID
+ event.SHATitle = gitEvent.Commits[lastCommitIdx].Title
+ event.HeadBranch = gitEvent.Ref
+ event.BaseBranch = gitEvent.Ref
+ event.TriggerTarget = triggertype.Push
+ default:
+ return nil, fmt.Errorf("unsupported GitLab event type: %T", gitEvent)
+ }
+
+ return event, nil
+}
+
+func eventFromBitbucketCloud(body []byte, headers map[string]string) (*info.Event, error) {
+ event := info.NewEvent()
+ event.Request.Payload = body
+ event.Request.Header = http.Header{}
+ for k, v := range headers {
+ event.Request.Header.Set(k, v)
+ }
+
+ // Parse Bitbucket Cloud webhook event type from headers (X-Event-Key), case-insensitive
+ var eventType string
+ for k, v := range headers {
+ if strings.EqualFold(k, "X-Event-Key") {
+ eventType = v
+ break
+ }
+ }
+ if eventType == "" {
+ return nil, fmt.Errorf("missing X-Event-Key header for Bitbucket Cloud webhook")
+ }
+
+ event.EventType = eventType
+
+ switch {
+ case strings.HasPrefix(eventType, "pullrequest:"):
+ var prEvent types.PullRequestEvent
+ if err := json.Unmarshal(body, &prEvent); err != nil {
+ return nil, fmt.Errorf("failed to parse Bitbucket Cloud pull request event: %w", err)
+ }
+ event.Event = &prEvent
+ event.Organization = prEvent.Repository.Workspace.Slug
+ repoParts := strings.Split(prEvent.Repository.FullName, "/")
+ if len(repoParts) > 1 {
+ event.Repository = repoParts[1]
+ } else {
+ event.Repository = prEvent.Repository.FullName
+ }
+ event.Sender = prEvent.PullRequest.Author.Nickname
+ event.URL = prEvent.Repository.Links.HTML.HRef
+ event.SHA = prEvent.PullRequest.Source.Commit.Hash
+ event.HeadBranch = prEvent.PullRequest.Source.Branch.Name
+ event.BaseBranch = prEvent.PullRequest.Destination.Branch.Name
+ event.PullRequestNumber = prEvent.PullRequest.ID
+ event.PullRequestTitle = prEvent.PullRequest.Title
+ event.TriggerTarget = triggertype.PullRequest
+ if eventType == "pullrequest:rejected" || eventType == "pullrequest:fulfilled" {
+ event.TriggerTarget = triggertype.PullRequestClosed
+ }
+ case eventType == "repo:push":
+ var pushEvent types.PushRequestEvent
+ if err := json.Unmarshal(body, &pushEvent); err != nil {
+ return nil, fmt.Errorf("failed to parse Bitbucket Cloud push event: %w", err)
+ }
+ event.Event = &pushEvent
+ event.Organization = pushEvent.Repository.Workspace.Slug
+ repoParts := strings.Split(pushEvent.Repository.FullName, "/")
+ if len(repoParts) > 1 {
+ event.Repository = repoParts[1]
+ } else {
+ event.Repository = pushEvent.Repository.FullName
+ }
+ event.Sender = pushEvent.Actor.Nickname
+ event.URL = pushEvent.Repository.Links.HTML.HRef
+ if len(pushEvent.Push.Changes) > 0 {
+ event.SHA = pushEvent.Push.Changes[0].New.Target.Hash
+ event.HeadBranch = pushEvent.Push.Changes[0].New.Name
+ event.BaseBranch = pushEvent.Push.Changes[0].New.Name
+ }
+ event.TriggerTarget = triggertype.Push
+ default:
+ return nil, fmt.Errorf("unsupported Bitbucket Cloud event type: %s", eventType)
+ }
+
+ return event, nil
+}
+
+func eventFromBitbucketDataCenter(body []byte, headers map[string]string) (*info.Event, error) {
+ event := info.NewEvent()
+ event.Request.Payload = body
+ event.Request.Header = http.Header{}
+ for k, v := range headers {
+ event.Request.Header.Set(k, v)
+ }
+
+ // Parse Bitbucket Data Center webhook event type from headers (X-Event-Key), case-insensitive
+ var eventType string
+ for k, v := range headers {
+ if strings.EqualFold(k, "X-Event-Key") {
+ eventType = v
+ break
+ }
+ }
+ if eventType == "" {
+ return nil, fmt.Errorf("missing X-Event-Key header for Bitbucket Data Center webhook")
+ }
+
+ event.EventType = eventType
+
+ switch {
+ case strings.HasPrefix(eventType, "pr:"):
+ // Parse as a generic pull request event structure
+ var prData map[string]any
+ if err := json.Unmarshal(body, &prData); err != nil {
+ return nil, fmt.Errorf("failed to parse Bitbucket Data Center pull request event: %w", err)
+ }
+ event.Event = prData
+
+ // Extract basic information from the payload structure
+ if pullRequest, ok := prData["pullRequest"].(map[string]any); ok {
+ if toRef, ok := pullRequest["toRef"].(map[string]any); ok {
+ if repository, ok := toRef["repository"].(map[string]any); ok {
+ if project, ok := repository["project"].(map[string]any); ok {
+ if key, ok := project["key"].(string); ok {
+ event.Organization = key
+ }
+ }
+ if name, ok := repository["name"].(string); ok {
+ event.Repository = name
+ }
+ }
+ if displayID, ok := toRef["displayId"].(string); ok {
+ event.BaseBranch = displayID
+ }
+ }
+ if fromRef, ok := pullRequest["fromRef"].(map[string]any); ok {
+ if displayID, ok := fromRef["displayId"].(string); ok {
+ event.HeadBranch = displayID
+ }
+ if latestCommit, ok := fromRef["latestCommit"].(string); ok {
+ event.SHA = latestCommit
+ }
+ }
+ if id, ok := pullRequest["id"].(float64); ok {
+ event.PullRequestNumber = int(id)
+ }
+ if title, ok := pullRequest["title"].(string); ok {
+ event.PullRequestTitle = title
+ }
+ }
+ if actor, ok := prData["actor"].(map[string]any); ok {
+ if name, ok := actor["name"].(string); ok {
+ event.Sender = name
+ }
+ }
+ event.TriggerTarget = triggertype.PullRequest
+ case eventType == "repo:refs_changed":
+ // Parse as a generic push event structure
+ var pushData map[string]any
+ if err := json.Unmarshal(body, &pushData); err != nil {
+ return nil, fmt.Errorf("failed to parse Bitbucket Data Center push event: %w", err)
+ }
+ event.Event = pushData
+
+ // Extract basic information
+ if repository, ok := pushData["repository"].(map[string]any); ok {
+ if project, ok := repository["project"].(map[string]any); ok {
+ if key, ok := project["key"].(string); ok {
+ event.Organization = key
+ }
+ }
+ if name, ok := repository["name"].(string); ok {
+ event.Repository = name
+ }
+ }
+ if actor, ok := pushData["actor"].(map[string]any); ok {
+ if name, ok := actor["name"].(string); ok {
+ event.Sender = name
+ }
+ }
+ if changes, ok := pushData["changes"].([]any); ok && len(changes) > 0 {
+ if change, ok := changes[0].(map[string]any); ok {
+ if toHash, ok := change["toHash"].(string); ok {
+ event.SHA = toHash
+ }
+ if refID, ok := change["refId"].(string); ok {
+ event.HeadBranch = refID
+ event.BaseBranch = refID
+ }
+ }
+ }
+ event.TriggerTarget = triggertype.Push
+ default:
+ return nil, fmt.Errorf("unsupported Bitbucket Data Center event type: %s", eventType)
+ }
+
+ return event, nil
+}
+
+func eventFromGitea(body []byte, headers map[string]string) (*info.Event, error) {
+ event := info.NewEvent()
+ event.EventType = getHeaderCaseInsensitive(headers, "X-Gitea-Event-Type")
+ if event.EventType == "" {
+ return nil, fmt.Errorf("missing X-Gitea-Event-Type header for Gitea webhook")
+ }
+ event.Request.Payload = body
+ event.Request.Header = http.Header{}
+ for k, v := range headers {
+ event.Request.Header.Set(k, v)
+ }
+
+ // Parse Gitea webhook payload manually since parseWebhook is not exported
+ var eventInt any
+ switch event.EventType {
+ case "push":
+ eventInt = &giteaStructs.PushPayload{}
+ case "pull_request":
+ eventInt = &giteaStructs.PullRequestPayload{}
+ case "issue_comment", "pull_request_comment":
+ eventInt = &giteaStructs.IssueCommentPayload{}
+ default:
+ return nil, fmt.Errorf("unsupported Gitea event type: %s", event.EventType)
+ }
+
+ // Parse the payload into the eventInt interface
+ if err := json.Unmarshal(body, &eventInt); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal Gitea payload: %w", err)
+ }
+
+ event.Event = eventInt
+
+ // Extract common event information from Gitea payload
+ switch gitEvent := eventInt.(type) {
+ case *giteaStructs.PullRequestPayload:
+ event.Organization = gitEvent.Repository.Owner.UserName
+ event.Repository = gitEvent.Repository.Name
+ event.Sender = gitEvent.Sender.UserName
+ event.URL = gitEvent.Repository.HTMLURL
+ event.SHA = gitEvent.PullRequest.Head.Sha
+ event.HeadBranch = gitEvent.PullRequest.Head.Ref
+ event.BaseBranch = gitEvent.PullRequest.Base.Ref
+ event.PullRequestNumber = int(gitEvent.Index)
+ event.PullRequestTitle = gitEvent.PullRequest.Title
+ event.TriggerTarget = triggertype.PullRequest
+ if gitEvent.Action == giteaStructs.HookIssueClosed {
+ event.TriggerTarget = triggertype.PullRequestClosed
+ }
+ case *giteaStructs.PushPayload:
+ event.Organization = gitEvent.Repo.Owner.UserName
+ event.Repository = gitEvent.Repo.Name
+ event.Sender = gitEvent.Sender.UserName
+ event.URL = gitEvent.Repo.HTMLURL
+ event.SHA = gitEvent.HeadCommit.ID
+ if event.SHA == "" {
+ event.SHA = gitEvent.Before
+ }
+ event.SHATitle = gitEvent.HeadCommit.Message
+ event.HeadBranch = gitEvent.Ref
+ event.BaseBranch = gitEvent.Ref
+ event.TriggerTarget = triggertype.Push
+ case *giteaStructs.IssueCommentPayload:
+ if gitEvent.Issue.PullRequest == nil {
+ return nil, fmt.Errorf("issue comment is not from a pull request")
+ }
+ event.Organization = gitEvent.Repository.Owner.UserName
+ event.Repository = gitEvent.Repository.Name
+ event.Sender = gitEvent.Sender.UserName
+ event.URL = gitEvent.Repository.HTMLURL
+ event.TriggerTarget = triggertype.PullRequest
+ // For comments, we'll need to make additional API calls to get PR details
+ // For now, we'll set basic info
+ event.PullRequestNumber = extractPullRequestNumber(gitEvent.Issue.URL)
+ default:
+ return nil, fmt.Errorf("unsupported Gitea event type: %T", gitEvent)
+ }
+
+ return event, nil
+}
+
+// Helper functions.
+func extractOrgFromPath(pathWithNamespace string) string {
+ parts := strings.Split(pathWithNamespace, "/")
+ if len(parts) >= 2 {
+ return strings.Join(parts[:len(parts)-1], "/")
+ }
+ return ""
+}
+
+func extractRepoFromPath(pathWithNamespace string) string {
+ parts := strings.Split(pathWithNamespace, "/")
+ if len(parts) > 0 {
+ return parts[len(parts)-1]
+ }
+ return ""
+}
+
+func extractPullRequestNumber(issueURL string) int {
+ // Extract pull request number from issue URL
+ // This is a simplified implementation
+ parts := strings.Split(issueURL, "/")
+ for i, part := range parts {
+ if part == "issues" && i+1 < len(parts) {
+ if num, err := strconv.Atoi(parts[i+1]); err == nil {
+ return num
+ }
+ }
+ }
+ return 0
+}
+
+func Command(ioStreams *cli.IOStreams) *cobra.Command {
+ var bodyFile, headersFile, provider, githubToken string
+
+ cmd := &cobra.Command{
+ Use: "cel",
+ Short: "Evaluate CEL expressions interactively with webhook payloads",
+ Long: `Evaluate CEL expressions interactively with webhook payloads.
+
+The command automatically detects the git provider from the webhook headers and payload structure.
+Supported providers: GitHub, GitLab, Bitbucket Cloud, Bitbucket Data Center, and Gitea.
+
+You can provide webhook payload and headers from files to test CEL expressions
+that would be used in PipelineRun configurations.`,
+ RunE: func(_ *cobra.Command, _ []string) error {
+ body := map[string]any{}
+ headers := map[string]string{}
+ var bodyBytes []byte
+
+ if bodyFile != "" {
+ b, err := os.ReadFile(bodyFile)
+ if err != nil {
+ return err
+ }
+ bodyBytes = b
+ if err := json.Unmarshal(b, &body); err != nil {
+ return err
+ }
+ }
+
+ if headersFile != "" {
+ b, err := os.ReadFile(headersFile)
+ if err != nil {
+ return err
+ }
+ bs := bytes.TrimSpace(b)
+ switch {
+ case len(bs) > 0 && (bs[0] == '{' || bs[0] == '['):
+ // JSON format headers
+ if err := json.Unmarshal(bs, &headers); err != nil {
+ return err
+ }
+ case isGosmeeScript(string(bs)):
+ // Gosmee-generated shell script with curl commands
+ h, err := parseGosmeeScript(string(bs))
+ if err != nil {
+ return err
+ }
+ headers = h
+ default:
+ // Plain HTTP headers format
+ h, err := parseHTTPHeaders(string(bs))
+ if err != nil {
+ return err
+ }
+ headers = h
+ }
+ }
+ // nolint:ineffassign,staticcheck
+ pacParams := map[string]string{}
+ // Auto-detect provider if not specified explicitly
+ if provider == "auto" {
+ detectedProvider, err := detectProvider(headers, bodyBytes)
+ if err != nil {
+ return fmt.Errorf("auto-detection failed: %w", err)
+ }
+ provider = detectedProvider
+ }
+
+ switch provider {
+ case "github":
+ var event *info.Event
+ var err error
+ if githubToken != "" {
+ event, err = eventFromGitHubWithProvider(bodyBytes, headers, githubToken)
+ } else {
+ event, err = eventFromGitHub(bodyBytes, headers)
+ }
+ if err != nil {
+ return err
+ }
+ pacParams = pacParamsFromEvent(event)
+ case "gitlab":
+ event, err := eventFromGitLab(bodyBytes, headers)
+ if err != nil {
+ return err
+ }
+ pacParams = pacParamsFromEvent(event)
+ case "bitbucket-cloud":
+ event, err := eventFromBitbucketCloud(bodyBytes, headers)
+ if err != nil {
+ return err
+ }
+ pacParams = pacParamsFromEvent(event)
+ case "bitbucket-datacenter":
+ event, err := eventFromBitbucketDataCenter(bodyBytes, headers)
+ if err != nil {
+ return err
+ }
+ pacParams = pacParamsFromEvent(event)
+ case "gitea":
+ event, err := eventFromGitea(bodyBytes, headers)
+ if err != nil {
+ return err
+ }
+ pacParams = pacParamsFromEvent(event)
+ default:
+ return fmt.Errorf("unsupported provider %s", provider)
+ }
+
+ fmt.Fprintln(ioStreams.Out, strings.TrimSpace(
+ fmt.Sprintf(helpString, provider))+"\n")
+ // Check if stdin is a terminal (interactive mode) or pipe/file (non-interactive mode)
+ if term.IsTerminal(int(os.Stdin.Fd())) {
+ // Get cross-platform history file path
+ historyFile, err := getHistoryFilePath()
+ if err != nil {
+ // If we can't get history file path, continue without history
+ historyFile = ""
+ }
+
+ // Interactive mode: use readline with history
+ rl, err := readline.NewEx(&readline.Config{
+ Prompt: "CEL expression> ",
+ HistoryFile: historyFile,
+ AutoComplete: nil,
+ InterruptPrompt: "^C",
+ EOFPrompt: "exit",
+ HistorySearchFold: true,
+ })
+ if err != nil {
+ // Fallback to survey if readline fails
+ for {
+ var expr string
+ if err := survey.AskOne(&survey.Input{Message: "CEL expression"}, &expr); err != nil {
+ return err
+ }
+ if expr == "" {
+ break
+ }
+
+ // Create files data structure (always empty in CLI mode)
+ filesData := map[string]any{
+ "all": []string{},
+ "added": []string{},
+ "deleted": []string{},
+ "modified": []string{},
+ "renamed": []string{},
+ }
+
+ val, err := pkgcel.Value(expr, body, headers, pacParams, filesData)
+ if err != nil {
+ fmt.Fprintln(ioStreams.Out, err)
+ } else {
+ fmt.Fprintf(ioStreams.Out, "%v\n", val)
+ }
+ }
+ return nil
+ }
+ defer rl.Close()
+
+ fmt.Fprintln(ioStreams.Out, "Type CEL expressions (use ↑/↓ for history, empty line to exit):")
+ for {
+ expr, err := rl.Readline()
+ if err != nil {
+ if errors.Is(err, readline.ErrInterrupt) || errors.Is(err, io.EOF) {
+ break
+ }
+ return err
+ }
+
+ expr = strings.TrimSpace(expr)
+ if expr == "" {
+ break
+ }
+
+ // Create files data structure (always empty in CLI mode)
+ filesData := map[string]any{
+ "all": []string{},
+ "added": []string{},
+ "deleted": []string{},
+ "modified": []string{},
+ "renamed": []string{},
+ }
+
+ val, err := pkgcel.Value(expr, body, headers, pacParams, filesData)
+ if err != nil {
+ fmt.Fprintln(ioStreams.Out, err)
+ } else {
+ fmt.Fprintf(ioStreams.Out, "%v\n", val)
+ }
+ }
+ } else {
+ // Non-interactive mode: read from stdin
+ scanner := bufio.NewScanner(os.Stdin)
+ hasInput := false
+ for scanner.Scan() {
+ hasInput = true
+ expr := strings.TrimSpace(scanner.Text())
+ if expr == "" {
+ continue
+ }
+
+ // Create files data structure (always empty in CLI mode)
+ filesData := map[string]any{
+ "all": []string{},
+ "added": []string{},
+ "deleted": []string{},
+ "modified": []string{},
+ "renamed": []string{},
+ }
+
+ val, err := pkgcel.Value(expr, body, headers, pacParams, filesData)
+ if err != nil {
+ fmt.Fprintln(ioStreams.Out, err)
+ } else {
+ fmt.Fprintf(ioStreams.Out, "%v\n", val)
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return fmt.Errorf("error reading from stdin: %w", err)
+ }
+ // If no input was provided via stdin in non-interactive mode, just exit gracefully
+ // This allows the command to work in test scenarios
+ if !hasInput {
+ // Exit gracefully without error - this is expected when testing
+ return nil
+ }
+ }
+ return nil
+ },
+ Annotations: map[string]string{"commandType": "main"},
+ }
+
+ cmd.Flags().StringVarP(&bodyFile, bodyFileFlag, "b", "", "path to JSON body file")
+ cmd.Flags().StringVarP(&headersFile, headersFileFlag, "H", "", "path to headers file (JSON, HTTP format, or gosmee-generated shell script)")
+ cmd.Flags().StringVarP(&provider, providerFlag, "p", "auto", "payload provider (auto, github, gitlab, bitbucket-cloud, bitbucket-datacenter, gitea)")
+ cmd.Flags().StringVarP(&githubToken, githubTokenFlag, "t", "", "GitHub personal access token for API enrichment (enables full event processing)")
+ return cmd
+}
diff --git a/pkg/cmd/tknpac/cel/cel_test.go b/pkg/cmd/tknpac/cel/cel_test.go
new file mode 100644
index 000000000..0af301f47
--- /dev/null
+++ b/pkg/cmd/tknpac/cel/cel_test.go
@@ -0,0 +1,1829 @@
+package cel
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ pkgcel "github.com/openshift-pipelines/pipelines-as-code/pkg/cel"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype"
+ "gotest.tools/v3/assert"
+ "gotest.tools/v3/fs"
+)
+
+func newIOStream() (*cli.IOStreams, *bytes.Buffer, *bytes.Buffer) {
+ in := &bytes.Buffer{}
+ out := &bytes.Buffer{}
+ errOut := &bytes.Buffer{}
+ return &cli.IOStreams{
+ In: io.NopCloser(in),
+ Out: out,
+ ErrOut: errOut,
+ }, out, errOut
+}
+
+func TestParseHTTPHeaders(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want map[string]string
+ wantErr bool
+ }{
+ {
+ name: "valid headers",
+ input: `Accept: */*
+Content-Type: application/json
+User-Agent: GitHub-Hookshot/2d5e4d4
+X-GitHub-Event: pull_request`,
+ want: map[string]string{
+ "Accept": "*/*",
+ "Content-Type": "application/json",
+ "User-Agent": "GitHub-Hookshot/2d5e4d4",
+ "X-GitHub-Event": "pull_request",
+ },
+ wantErr: false,
+ },
+ {
+ name: "headers with extra spaces",
+ input: ` Accept : */*
+ Content-Type:application/json `,
+ want: map[string]string{
+ "Accept": "*/*",
+ "Content-Type": "application/json",
+ },
+ wantErr: false,
+ },
+ {
+ name: "empty input",
+ input: "",
+ want: map[string]string{},
+ wantErr: false,
+ },
+ {
+ name: "headers with empty lines",
+ input: `Accept: */*
+
+Content-Type: application/json
+
+`,
+ want: map[string]string{
+ "Accept": "*/*",
+ "Content-Type": "application/json",
+ },
+ wantErr: false,
+ },
+ {
+ name: "malformed header line ignored",
+ input: `Accept: */*
+malformed-line-without-colon
+Content-Type: application/json`,
+ want: map[string]string{
+ "Accept": "*/*",
+ "Content-Type": "application/json",
+ },
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := parseHTTPHeaders(tt.input)
+ if tt.wantErr {
+ assert.Assert(t, err != nil)
+ return
+ }
+ assert.NilError(t, err)
+ assert.DeepEqual(t, got, tt.want)
+ })
+ }
+}
+
+func TestSplitCurlCommand(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want []string
+ wantErr bool
+ }{
+ {
+ name: "simple curl command",
+ input: `curl -X POST "http://localhost:8080"`,
+ want: []string{"curl", "-X", "POST", "http://localhost:8080"},
+ },
+ {
+ name: "curl with headers",
+ input: `curl -H "Content-Type: application/json" -H "X-GitHub-Event: pull_request"`,
+ want: []string{"curl", "-H", "Content-Type: application/json", "-H", "X-GitHub-Event: pull_request"},
+ },
+ {
+ name: "curl with single quotes",
+ input: `curl -H 'Content-Type: application/json' -H 'X-GitHub-Event: pull_request'`,
+ want: []string{"curl", "-H", "Content-Type: application/json", "-H", "X-GitHub-Event: pull_request"},
+ },
+ {
+ name: "complex curl command",
+ input: `curl -sSi -H "Content-Type: application/json" -X POST -d @payload.json "http://localhost:8080"`,
+ want: []string{"curl", "-sSi", "-H", "Content-Type: application/json", "-X", "POST", "-d", "@payload.json", "http://localhost:8080"},
+ },
+ {
+ name: "unterminated quote",
+ input: `curl -H "Content-Type: application/json`,
+ wantErr: true,
+ },
+ {
+ name: "empty string",
+ input: "",
+ want: []string{},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := splitCurlCommand(tt.input)
+ if tt.wantErr {
+ assert.Assert(t, err != nil)
+ return
+ }
+ assert.NilError(t, err)
+ assert.DeepEqual(t, got, tt.want)
+ })
+ }
+}
+
+func TestParseCurlHeaders(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want map[string]string
+ wantErr bool
+ }{
+ {
+ name: "simple curl with headers",
+ input: `curl -H "Content-Type: application/json" -H "X-GitHub-Event: pull_request"`,
+ want: map[string]string{
+ "Content-Type": "application/json",
+ "X-GitHub-Event": "pull_request",
+ },
+ },
+ {
+ name: "curl with mixed arguments",
+ input: `curl -X POST -H "Content-Type: application/json" -d @payload.json -H "X-GitHub-Event: pull_request" "http://localhost:8080"`,
+ want: map[string]string{
+ "Content-Type": "application/json",
+ "X-GitHub-Event": "pull_request",
+ },
+ },
+ {
+ name: "curl with no headers",
+ input: `curl -X POST "http://localhost:8080"`,
+ want: map[string]string{},
+ },
+ {
+ name: "curl with malformed header",
+ input: `curl -H "MalformedHeader" -H "Good-Header: value"`,
+ want: map[string]string{
+ "Good-Header": "value",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := parseCurlHeaders(tt.input)
+ if tt.wantErr {
+ assert.Assert(t, err != nil)
+ return
+ }
+ assert.NilError(t, err)
+ assert.DeepEqual(t, got, tt.want)
+ })
+ }
+}
+
+func TestIsGosmeeScript(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expect bool
+ }{
+ {
+ name: "typical gosmee script",
+ input: `#!/usr/bin/env bash
+set -euxfo pipefail
+curl -sSi -H "Content-Type: application/json" -H "X-GitHub-Event: pull_request" -X POST -d @payload.json http://localhost:8080`,
+ expect: true,
+ },
+ {
+ name: "simple curl command",
+ input: `curl -H "Content-Type: application/json" http://localhost:8080`,
+ expect: true,
+ },
+ {
+ name: "curl without headers",
+ input: `curl http://localhost:8080`,
+ expect: false,
+ },
+ {
+ name: "plain text headers",
+ input: `Content-Type: application/json
+X-GitHub-Event: pull_request`,
+ expect: false,
+ },
+ {
+ name: "json headers",
+ input: `{
+ "Content-Type": "application/json",
+ "X-GitHub-Event": "pull_request"
+}`,
+ expect: false,
+ },
+ {
+ name: "empty input",
+ input: "",
+ expect: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := isGosmeeScript(tt.input)
+ assert.Equal(t, got, tt.expect)
+ })
+ }
+}
+
+func TestParseGosmeeScript(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want map[string]string
+ wantErr bool
+ }{
+ {
+ name: "typical gosmee script",
+ input: `#!/usr/bin/env bash
+# Copyright 2023 Chmouel Boudjnah
+set -euxfo pipefail
+
+curl -sSi -H "Content-Type: application/json" -H "X-GitHub-Event: pull_request" -H "User-Agent: GitHub-Hookshot/2d5e4d4" -X POST -d @payload.json http://localhost:8080`,
+ want: map[string]string{
+ "Content-Type": "application/json",
+ "X-GitHub-Event": "pull_request",
+ "User-Agent": "GitHub-Hookshot/2d5e4d4",
+ },
+ },
+ {
+ name: "real-world example from gosmee",
+ input: `#!/usr/bin/env bash
+# Replay script with headers and JSON payload to the target controller.
+set -euxfo pipefail
+cd $(dirname $(readlink -f $0))
+
+curl -sSi -H "Content-Type: application/json" -H 'X-Forwarded-Proto: https' -H 'Accept-Encoding: gzip' -H 'Content-Length: 17' -H 'X-Forwarded-Host: hook.pipelinesascode.com' -H 'Accept: */*' -X POST -d @./payload.json ${targetURL}`,
+ want: map[string]string{
+ "Content-Type": "application/json",
+ "X-Forwarded-Proto": "https",
+ "Accept-Encoding": "gzip",
+ "Content-Length": "17",
+ "X-Forwarded-Host": "hook.pipelinesascode.com",
+ "Accept": "*/*",
+ },
+ },
+ {
+ name: "script without curl commands",
+ input: `#!/bin/bash
+echo "No curl commands here"
+exit 0`,
+ wantErr: true,
+ },
+ {
+ name: "curl without headers",
+ input: `#!/bin/bash
+curl http://localhost:8080`,
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := parseGosmeeScript(tt.input)
+ if tt.wantErr {
+ assert.Assert(t, err != nil)
+ return
+ }
+ assert.NilError(t, err)
+ assert.DeepEqual(t, got, tt.want)
+ })
+ }
+}
+
+func TestEventFromGitHub(t *testing.T) {
+ pullRequestPayload := `{
+ "action": "synchronize",
+ "number": 1234,
+ "pull_request": {
+ "title": "fix something somewhere out there",
+ "number": 1234,
+ "user": {
+ "login": "pachito"
+ },
+ "head": {
+ "ref": "parsepayload",
+ "sha": "178fd7ac8826595cffaa23e574eb0c02c3e76dcf",
+ "repo": {
+ "html_url": "https://github.com/pachito/pipelines-as-code"
+ }
+ },
+ "base": {
+ "ref": "main",
+ "repo": {
+ "html_url": "https://github.com/openshift-pipelines/pipelines-as-code"
+ }
+ },
+ "labels": []
+ },
+ "repository": {
+ "name": "pipelines-as-code",
+ "full_name": "openshift-pipelines/pipelines-as-code",
+ "owner": {
+ "login": "openshift-pipelines"
+ },
+ "html_url": "https://github.com/openshift-pipelines/pipelines-as-code",
+ "default_branch": "main"
+ },
+ "sender": {
+ "login": "pachito"
+ }
+}`
+
+ pushPayload := `{
+ "ref": "refs/heads/main",
+ "after": "123abc456def",
+ "head_commit": {
+ "id": "123abc456def",
+ "message": "Update README",
+ "url": "https://github.com/owner/repo/commit/123abc456def"
+ },
+ "repository": {
+ "name": "test-repo",
+ "owner": {
+ "login": "test-owner"
+ },
+ "html_url": "https://github.com/test-owner/test-repo",
+ "default_branch": "main"
+ },
+ "sender": {
+ "login": "test-user"
+ }
+}`
+
+ tests := []struct {
+ name string
+ body []byte
+ headers map[string]string
+ wantErr bool
+ checks func(t *testing.T, event *info.Event)
+ }{
+ {
+ name: "pull request event",
+ body: []byte(pullRequestPayload),
+ headers: map[string]string{
+ "X-GitHub-Event": "pull_request",
+ },
+ wantErr: false,
+ checks: func(t *testing.T, event *info.Event) {
+ assert.Equal(t, event.EventType, "pull_request")
+ assert.Equal(t, event.Organization, "openshift-pipelines")
+ assert.Equal(t, event.Repository, "pipelines-as-code")
+ assert.Equal(t, event.BaseBranch, "main")
+ assert.Equal(t, event.HeadBranch, "parsepayload")
+ assert.Equal(t, event.Sender, "pachito")
+ assert.Equal(t, event.PullRequestTitle, "fix something somewhere out there")
+ assert.Equal(t, event.PullRequestNumber, 1234)
+ assert.Equal(t, event.SHA, "178fd7ac8826595cffaa23e574eb0c02c3e76dcf")
+ },
+ },
+ {
+ name: "push event",
+ body: []byte(pushPayload),
+ headers: map[string]string{
+ "X-GitHub-Event": "push",
+ },
+ wantErr: false,
+ checks: func(t *testing.T, event *info.Event) {
+ assert.Equal(t, event.EventType, "push")
+ assert.Equal(t, event.Organization, "test-owner")
+ assert.Equal(t, event.Repository, "test-repo")
+ assert.Equal(t, event.BaseBranch, "refs/heads/main")
+ assert.Equal(t, event.HeadBranch, "refs/heads/main")
+ assert.Equal(t, event.Sender, "test-user")
+ assert.Equal(t, event.SHATitle, "Update README")
+ assert.Equal(t, event.SHA, "123abc456def")
+ },
+ },
+ {
+ name: "invalid json",
+ body: []byte(`{"invalid": json}`),
+ headers: map[string]string{
+ "X-GitHub-Event": "pull_request",
+ },
+ wantErr: true,
+ },
+ {
+ name: "unsupported event type",
+ body: []byte(`{"action": "test"}`),
+ headers: map[string]string{
+ "X-GitHub-Event": "unsupported_event",
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ event, err := eventFromGitHub(tt.body, tt.headers)
+ if tt.wantErr {
+ assert.Assert(t, err != nil)
+ return
+ }
+ assert.NilError(t, err)
+ assert.Assert(t, event != nil)
+ if tt.checks != nil {
+ tt.checks(t, event)
+ }
+ })
+ }
+}
+
+func TestEventFromGitHubMoreEventTypes(t *testing.T) {
+ issueCommentPayload := `{
+ "action": "created",
+ "issue": {
+ "number": 123,
+ "pull_request": {
+ "html_url": "https://github.com/owner/repo/pull/123"
+ }
+ },
+ "comment": {
+ "body": "/ok-to-test"
+ },
+ "repository": {
+ "name": "test-repo",
+ "owner": {
+ "login": "test-owner"
+ },
+ "html_url": "https://github.com/test-owner/test-repo",
+ "default_branch": "main"
+ },
+ "sender": {
+ "login": "test-user"
+ }
+}`
+
+ commitCommentPayload := `{
+ "action": "created",
+ "comment": {
+ "commit_id": "abc123def456",
+ "body": "/retest",
+ "html_url": "https://github.com/owner/repo/commit/abc123def456#comment"
+ },
+ "repository": {
+ "name": "test-repo",
+ "owner": {
+ "login": "test-owner"
+ },
+ "html_url": "https://github.com/test-owner/test-repo",
+ "default_branch": "main"
+ },
+ "sender": {
+ "login": "test-user"
+ }
+}`
+
+ tests := []struct {
+ name string
+ body []byte
+ headers map[string]string
+ wantErr bool
+ checks func(t *testing.T, event *info.Event)
+ }{
+ {
+ name: "issue comment event",
+ body: []byte(issueCommentPayload),
+ headers: map[string]string{
+ "X-GitHub-Event": "issue_comment",
+ },
+ wantErr: false,
+ checks: func(t *testing.T, event *info.Event) {
+ assert.Equal(t, event.EventType, "issue_comment")
+ assert.Equal(t, event.Organization, "test-owner")
+ assert.Equal(t, event.Repository, "test-repo")
+ assert.Equal(t, event.Sender, "test-user")
+ assert.Equal(t, event.TriggerComment, "/ok-to-test")
+ assert.Equal(t, event.PullRequestNumber, 123)
+ assert.Equal(t, event.TriggerTarget, triggertype.PullRequest)
+ },
+ },
+ {
+ name: "commit comment event",
+ body: []byte(commitCommentPayload),
+ headers: map[string]string{
+ "X-GitHub-Event": "commit_comment",
+ },
+ wantErr: false,
+ checks: func(t *testing.T, event *info.Event) {
+ assert.Equal(t, event.EventType, "commit_comment")
+ assert.Equal(t, event.Organization, "test-owner")
+ assert.Equal(t, event.Repository, "test-repo")
+ assert.Equal(t, event.Sender, "test-user")
+ assert.Equal(t, event.TriggerComment, "/retest")
+ assert.Equal(t, event.SHA, "abc123def456")
+ assert.Equal(t, event.TriggerTarget, triggertype.Push)
+ },
+ },
+ {
+ name: "missing X-GitHub-Event header",
+ body: []byte(`{"action": "test"}`),
+ headers: map[string]string{
+ "Content-Type": "application/json",
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ event, err := eventFromGitHub(tt.body, tt.headers)
+ if tt.wantErr {
+ assert.Assert(t, err != nil)
+ return
+ }
+ assert.NilError(t, err)
+ assert.Assert(t, event != nil)
+ if tt.checks != nil {
+ tt.checks(t, event)
+ }
+ })
+ }
+}
+
+// TestEventFromGitHubWithProvider tests the enhanced GitHub event processing
+// that uses API calls to enrich events when a token is provided.
+func TestEventFromGitHubWithProvider(t *testing.T) {
+ // This is a mock test since we don't want to make real API calls in unit tests.
+ // We test the fallback behavior when no token is provided and ensure
+ // the enhanced function doesn't break existing functionality.
+
+ issueCommentPayload := `{
+ "action": "created",
+ "issue": {
+ "number": 123,
+ "pull_request": {
+ "html_url": "https://github.com/test-owner/test-repo/pull/123"
+ }
+ },
+ "comment": {
+ "body": "/ok-to-test"
+ },
+ "repository": {
+ "name": "test-repo",
+ "owner": {
+ "login": "test-owner"
+ },
+ "html_url": "https://github.com/test-owner/test-repo",
+ "default_branch": "main"
+ },
+ "sender": {
+ "login": "test-user"
+ }
+}`
+
+ headers := map[string]string{
+ "X-GitHub-Event": "issue_comment",
+ }
+
+ t.Run("fallback to basic parsing when no token provided", func(t *testing.T) {
+ // Test with empty token (should fallback to basic parsing)
+ event, err := eventFromGitHubWithProvider([]byte(issueCommentPayload), headers, "")
+ assert.NilError(t, err)
+ assert.Assert(t, event != nil)
+ assert.Equal(t, event.EventType, "issue_comment")
+ assert.Equal(t, event.Organization, "test-owner")
+ assert.Equal(t, event.Repository, "test-repo")
+ assert.Equal(t, event.PullRequestNumber, 123)
+
+ // With basic parsing, these fields should not be populated for issue_comment
+ assert.Equal(t, event.SHA, "")
+ assert.Equal(t, event.HeadBranch, "")
+ assert.Equal(t, event.BaseBranch, "")
+ })
+
+ t.Run("handles invalid token gracefully", func(t *testing.T) {
+ // Test with invalid token (should fallback to basic parsing)
+ event, err := eventFromGitHubWithProvider([]byte(issueCommentPayload), headers, "invalid-token")
+ assert.NilError(t, err) // Should not fail, just fallback
+ assert.Assert(t, event != nil)
+ assert.Equal(t, event.EventType, "issue_comment")
+ assert.Equal(t, event.Organization, "test-owner")
+ assert.Equal(t, event.Repository, "test-repo")
+ })
+
+ t.Run("handles pull request events without token", func(t *testing.T) {
+ // For PR events, both basic and enhanced parsing should work the same
+ prPayload := `{
+ "action": "opened",
+ "pull_request": {
+ "number": 123,
+ "title": "Test PR",
+ "head": {
+ "sha": "abc123",
+ "ref": "feature-branch"
+ },
+ "base": {
+ "ref": "main"
+ },
+ "user": {
+ "login": "test-user"
+ }
+ },
+ "repository": {
+ "name": "test-repo",
+ "owner": {
+ "login": "test-owner"
+ },
+ "html_url": "https://github.com/test-owner/test-repo",
+ "default_branch": "main"
+ }
+}`
+ prHeaders := map[string]string{
+ "X-GitHub-Event": "pull_request",
+ }
+
+ event, err := eventFromGitHubWithProvider([]byte(prPayload), prHeaders, "")
+ assert.NilError(t, err)
+ assert.Assert(t, event != nil)
+ assert.Equal(t, event.EventType, "pull_request")
+ assert.Equal(t, event.SHA, "abc123")
+ assert.Equal(t, event.HeadBranch, "feature-branch")
+ assert.Equal(t, event.BaseBranch, "main")
+ assert.Equal(t, event.PullRequestTitle, "Test PR")
+ })
+}
+
+func TestPacParamsFromEvent(t *testing.T) {
+ tests := []struct {
+ name string
+ event *info.Event
+ want map[string]string
+ }{
+ {
+ name: "pull request event",
+ event: &info.Event{
+ EventType: "pull_request",
+ Organization: "OpenShift-Pipelines",
+ Repository: "Pipelines-As-Code",
+ BaseBranch: "main",
+ HeadBranch: "feature-branch",
+ Sender: "TestUser",
+ SHA: "abc123",
+ URL: "https://github.com/openshift-pipelines/pipelines-as-code",
+ BaseURL: "https://github.com/openshift-pipelines/pipelines-as-code",
+ HeadURL: "https://github.com/user/pipelines-as-code",
+ PullRequestTitle: "Add new feature",
+ TriggerTarget: triggertype.PullRequest,
+ TriggerComment: "test comment\nwith newlines",
+ PullRequestLabel: []string{"bug", "enhancement"},
+ },
+ want: map[string]string{
+ "revision": "abc123",
+ "repo_url": "https://github.com/openshift-pipelines/pipelines-as-code",
+ "repo_owner": "openshift-pipelines",
+ "repo_name": "pipelines-as-code",
+ "target_branch": "main",
+ "source_branch": "feature-branch",
+ "git_tag": "",
+ "source_url": "https://github.com/user/pipelines-as-code",
+ "target_url": "https://github.com/openshift-pipelines/pipelines-as-code",
+ "sender": "testuser",
+ "target_namespace": "",
+ "event_type": "pull_request",
+ "event": "pull_request",
+ "event_title": "Add new feature",
+ "trigger_comment": "test comment\\nwith newlines",
+ "pull_request_labels": "bug\nenhancement",
+ },
+ },
+ {
+ name: "push event with tag",
+ event: &info.Event{
+ EventType: "push",
+ Organization: "test-org",
+ Repository: "test-repo",
+ BaseBranch: "refs/tags/v1.0.0",
+ HeadBranch: "refs/tags/v1.0.0",
+ Sender: "test-user",
+ SHA: "def456",
+ URL: "https://github.com/test-org/test-repo",
+ BaseURL: "https://github.com/test-org/test-repo",
+ HeadURL: "https://github.com/test-org/test-repo",
+ SHATitle: "Release v1.0.0",
+ TriggerTarget: triggertype.Push,
+ },
+ want: map[string]string{
+ "revision": "def456",
+ "repo_url": "https://github.com/test-org/test-repo",
+ "repo_owner": "test-org",
+ "repo_name": "test-repo",
+ "target_branch": "refs/tags/v1.0.0",
+ "source_branch": "refs/tags/v1.0.0",
+ "git_tag": "v1.0.0",
+ "source_url": "https://github.com/test-org/test-repo",
+ "target_url": "https://github.com/test-org/test-repo",
+ "sender": "test-user",
+ "target_namespace": "",
+ "event_type": "push",
+ "event": "push",
+ "event_title": "Release v1.0.0",
+ "trigger_comment": "",
+ "pull_request_labels": "",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := pacParamsFromEvent(tt.event)
+ assert.DeepEqual(t, got, tt.want)
+ })
+ }
+}
+
+func TestPacParamsFromEventEdgeCases(t *testing.T) {
+ tests := []struct {
+ name string
+ event *info.Event
+ want map[string]string
+ }{
+ {
+ name: "event with clone URL preference",
+ event: &info.Event{
+ EventType: "push",
+ Organization: "test-org",
+ Repository: "test-repo",
+ URL: "https://github.com/test-org/test-repo",
+ CloneURL: "git@github.com:test-org/test-repo.git",
+ TriggerTarget: triggertype.Push,
+ },
+ want: map[string]string{
+ "revision": "",
+ "repo_url": "git@github.com:test-org/test-repo.git", // CloneURL takes precedence
+ "repo_owner": "test-org",
+ "repo_name": "test-repo",
+ "target_branch": "",
+ "source_branch": "",
+ "git_tag": "",
+ "source_url": "",
+ "target_url": "",
+ "sender": "",
+ "target_namespace": "",
+ "event_type": "push",
+ "event": "push",
+ "event_title": "",
+ "trigger_comment": "",
+ "pull_request_labels": "",
+ },
+ },
+ {
+ name: "event with carriage return in comment",
+ event: &info.Event{
+ TriggerTarget: triggertype.PullRequest,
+ TriggerComment: "line1\r\nline2\nline3",
+ },
+ want: map[string]string{
+ "revision": "",
+ "repo_url": "",
+ "repo_owner": "",
+ "repo_name": "",
+ "target_branch": "",
+ "source_branch": "",
+ "git_tag": "",
+ "source_url": "",
+ "target_url": "",
+ "sender": "",
+ "target_namespace": "",
+ "event_type": "",
+ "event": "pull_request",
+ "event_title": "",
+ "trigger_comment": "line1\\nline2\\nline3", // newlines escaped
+ "pull_request_labels": "",
+ },
+ },
+ {
+ name: "empty event",
+ event: &info.Event{
+ TriggerTarget: triggertype.Push,
+ },
+ want: map[string]string{
+ "revision": "",
+ "repo_url": "",
+ "repo_owner": "",
+ "repo_name": "",
+ "target_branch": "",
+ "source_branch": "",
+ "git_tag": "",
+ "source_url": "",
+ "target_url": "",
+ "sender": "",
+ "target_namespace": "",
+ "event_type": "",
+ "event": "push",
+ "event_title": "",
+ "trigger_comment": "",
+ "pull_request_labels": "",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := pacParamsFromEvent(tt.event)
+ assert.DeepEqual(t, got, tt.want)
+ })
+ }
+}
+
+func TestCommandExecution(t *testing.T) {
+ pullRequestPayload := `{
+ "action": "synchronize",
+ "number": 1234,
+ "pull_request": {
+ "title": "fix something somewhere out there",
+ "number": 1234,
+ "user": {
+ "login": "pachito"
+ },
+ "head": {
+ "ref": "parsepayload",
+ "sha": "178fd7ac8826595cffaa23e574eb0c02c3e76dcf",
+ "repo": {
+ "html_url": "https://github.com/pachito/pipelines-as-code"
+ }
+ },
+ "base": {
+ "ref": "main",
+ "repo": {
+ "html_url": "https://github.com/openshift-pipelines/pipelines-as-code"
+ }
+ },
+ "labels": [],
+ "draft": false
+ },
+ "repository": {
+ "name": "pipelines-as-code",
+ "full_name": "openshift-pipelines/pipelines-as-code",
+ "owner": {
+ "login": "openshift-pipelines"
+ },
+ "html_url": "https://github.com/openshift-pipelines/pipelines-as-code",
+ "default_branch": "main"
+ },
+ "sender": {
+ "login": "pachito"
+ }
+}`
+
+ headers := `Accept: */*
+Content-Type: application/json
+User-Agent: GitHub-Hookshot/2d5e4d4
+X-GitHub-Event: pull_request`
+
+ tests := []struct {
+ name string
+ bodyContent string
+ headersContent string
+ provider string
+ wantErr bool
+ wantErrContains string
+ wantOutContains []string
+ }{
+ {
+ name: "valid pull request payload",
+ bodyContent: pullRequestPayload,
+ headersContent: headers,
+ provider: "github",
+ wantErr: false, // Interactive mode should exit gracefully on EOF
+ wantOutContains: []string{
+ "CEL Expression Evaluator for Pipelines as Code",
+ "PAC Parameters (pac.* - for backward compatibility):",
+ "pac.event",
+ "pac.target_branch",
+ "body.action",
+ "headers['x-github-event']",
+ },
+ },
+ {
+ name: "no files provided",
+ provider: "github",
+ wantErr: true,
+ wantErrContains: "unknown X-Github-Event",
+ },
+ {
+ name: "no body file",
+ headersContent: headers,
+ provider: "github",
+ wantErr: true,
+ wantErrContains: "unexpected end of JSON input",
+ },
+ {
+ name: "unsupported provider",
+ bodyContent: pullRequestPayload,
+ headersContent: headers,
+ provider: "invalid-provider",
+ wantErr: true,
+ wantErrContains: "unsupported provider invalid-provider",
+ },
+ {
+ name: "invalid json body",
+ bodyContent: `{"invalid": json}`,
+ headersContent: headers,
+ provider: "github",
+ wantErr: true,
+ wantErrContains: "invalid character",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := fs.NewDir(t, "cel-test")
+ defer tempDir.Remove()
+
+ var bodyFile, headersFile string
+
+ if tt.bodyContent != "" {
+ bodyFile = tempDir.Join("payload.json")
+ err := os.WriteFile(bodyFile, []byte(tt.bodyContent), 0o600)
+ assert.NilError(t, err)
+ }
+
+ if tt.headersContent != "" {
+ headersFile = tempDir.Join("headers.txt")
+ err := os.WriteFile(headersFile, []byte(tt.headersContent), 0o600)
+ assert.NilError(t, err)
+ }
+
+ ioStreams, out, errOut := newIOStream()
+
+ // Write empty input to stdin to exit the interactive loop immediately
+ ioStreams.In = io.NopCloser(strings.NewReader("\n"))
+
+ cmd := Command(ioStreams)
+ cmd.SetArgs([]string{
+ "--provider", tt.provider,
+ })
+
+ if bodyFile != "" {
+ if err := cmd.Flags().Set("body", bodyFile); err != nil {
+ t.Fatalf("failed to set body flag: %v", err)
+ }
+ }
+ if headersFile != "" {
+ if err := cmd.Flags().Set("headers", headersFile); err != nil {
+ t.Fatalf("failed to set headers flag: %v", err)
+ }
+ }
+
+ err := cmd.Execute()
+
+ if tt.wantErr {
+ assert.Assert(t, err != nil)
+ if tt.wantErrContains != "" {
+ assert.Assert(t, strings.Contains(err.Error(), tt.wantErrContains),
+ "error %q should contain %q", err.Error(), tt.wantErrContains)
+ }
+ // For EOF error (successful processing but empty input), check output still
+ if tt.wantErrContains == "EOF" && len(tt.wantOutContains) > 0 {
+ outStr := out.String()
+ errStr := errOut.String()
+ for _, want := range tt.wantOutContains {
+ assert.Assert(t, strings.Contains(outStr, want) || strings.Contains(errStr, want),
+ "output should contain %q, got out: %q, err: %q", want, outStr, errStr)
+ }
+ }
+ return
+ }
+
+ assert.NilError(t, err)
+
+ outStr := out.String()
+ errStr := errOut.String()
+
+ for _, want := range tt.wantOutContains {
+ assert.Assert(t, strings.Contains(outStr, want) || strings.Contains(errStr, want),
+ "output should contain %q, got out: %q, err: %q", want, outStr, errStr)
+ }
+ })
+ }
+}
+
+func TestCommandFileHandling(t *testing.T) {
+ tests := []struct {
+ name string
+ headersContent string
+ isJSON bool
+ wantErr bool
+ wantErrContains string
+ }{
+ {
+ name: "plain text headers",
+ headersContent: `Accept: */*
+Content-Type: application/json
+X-GitHub-Event: pull_request`,
+ isJSON: false,
+ wantErr: true,
+ wantErrContains: "unexpected end of JSON input",
+ },
+ {
+ name: "json headers",
+ headersContent: `{
+ "Accept": "*/*",
+ "Content-Type": "application/json",
+ "X-GitHub-Event": "pull_request"
+}`,
+ isJSON: true,
+ wantErr: true,
+ wantErrContains: "unexpected end of JSON input",
+ },
+ {
+ name: "invalid json headers",
+ headersContent: `{"invalid": json}`,
+ isJSON: true,
+ wantErr: true,
+ wantErrContains: "invalid character",
+ },
+ {
+ name: "empty headers file",
+ headersContent: "",
+ wantErr: true,
+ wantErrContains: "unknown X-Github-Event",
+ },
+ {
+ name: "gosmee script",
+ headersContent: `#!/usr/bin/env bash
+set -euxfo pipefail
+curl -sSi -H "Content-Type: application/json" -H "X-GitHub-Event: pull_request" -H "User-Agent: GitHub-Hookshot/2d5e4d4" -X POST -d @payload.json http://localhost:8080`,
+ wantErr: true,
+ wantErrContains: "unexpected end of JSON input", // Still expects body file
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := fs.NewDir(t, "cel-headers-test")
+ defer tempDir.Remove()
+
+ headersFile := tempDir.Join("headers.txt")
+ err := os.WriteFile(headersFile, []byte(tt.headersContent), 0o600)
+ assert.NilError(t, err)
+
+ ioStreams, _, _ := newIOStream()
+ // Write empty input to stdin to exit immediately
+ ioStreams.In = io.NopCloser(strings.NewReader("\n"))
+
+ cmd := Command(ioStreams)
+ cmd.SetArgs([]string{
+ "--provider", "github",
+ "--headers", headersFile,
+ })
+
+ err = cmd.Execute()
+
+ if tt.wantErr {
+ assert.Assert(t, err != nil)
+ if tt.wantErrContains != "" {
+ assert.Assert(t, strings.Contains(err.Error(), tt.wantErrContains),
+ "error %q should contain %q", err.Error(), tt.wantErrContains)
+ }
+ } else {
+ assert.NilError(t, err)
+ }
+ })
+ }
+}
+
+func TestCommandFlags(t *testing.T) {
+ ioStreams, _, _ := newIOStream()
+ cmd := Command(ioStreams)
+
+ // Test that flags are properly defined
+ bodyFlag := cmd.Flags().Lookup("body")
+ assert.Assert(t, bodyFlag != nil)
+ assert.Equal(t, bodyFlag.Shorthand, "b")
+ assert.Equal(t, bodyFlag.Usage, "path to JSON body file")
+
+ headersFlag := cmd.Flags().Lookup("headers")
+ assert.Assert(t, headersFlag != nil)
+ assert.Equal(t, headersFlag.Shorthand, "H")
+ assert.Equal(t, headersFlag.Usage, "path to headers file (JSON, HTTP format, or gosmee-generated shell script)")
+
+ providerFlag := cmd.Flags().Lookup("provider")
+ assert.Assert(t, providerFlag != nil)
+ assert.Equal(t, providerFlag.Shorthand, "p")
+ assert.Equal(t, providerFlag.Usage, "payload provider (auto, github, gitlab, bitbucket-cloud, bitbucket-datacenter, gitea)")
+ assert.Equal(t, providerFlag.DefValue, "auto")
+}
+
+func TestInvalidFiles(t *testing.T) {
+ tests := []struct {
+ name string
+ createFile bool
+ fileContent string
+ fileFlag string
+ wantErrContains string
+ }{
+ {
+ name: "non-existent body file",
+ createFile: false,
+ fileFlag: "body",
+ wantErrContains: "no such file or directory",
+ },
+ {
+ name: "non-existent headers file",
+ createFile: false,
+ fileFlag: "headers",
+ wantErrContains: "no such file or directory",
+ },
+ {
+ name: "invalid json in body file",
+ createFile: true,
+ fileContent: `{"invalid": json}`,
+ fileFlag: "body",
+ wantErrContains: "invalid character",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := fs.NewDir(t, "cel-invalid-test")
+ defer tempDir.Remove()
+
+ var filePath string
+ if tt.createFile {
+ filePath = tempDir.Join("test-file")
+ err := os.WriteFile(filePath, []byte(tt.fileContent), 0o600)
+ assert.NilError(t, err)
+ } else {
+ filePath = filepath.Join(tempDir.Path(), "non-existent-file")
+ }
+
+ ioStreams, _, _ := newIOStream()
+ ioStreams.In = io.NopCloser(strings.NewReader("\n"))
+
+ cmd := Command(ioStreams)
+ args := []string{"--provider", "github"}
+
+ if tt.fileFlag == "body" {
+ args = append(args, "--body", filePath)
+ } else {
+ args = append(args, "--headers", filePath)
+ }
+
+ cmd.SetArgs(args)
+
+ err := cmd.Execute()
+ assert.Assert(t, err != nil)
+ assert.Assert(t, strings.Contains(err.Error(), tt.wantErrContains),
+ "error %q should contain %q", err.Error(), tt.wantErrContains)
+ })
+ }
+}
+
+func TestCommandWithGosmeeScript(t *testing.T) {
+ // Real-world pull request payload
+ pullRequestPayload := `{
+ "action": "opened",
+ "number": 1,
+ "pull_request": {
+ "id": 1,
+ "number": 1,
+ "title": "Test PR",
+ "user": {
+ "login": "testuser",
+ "id": 1
+ },
+ "body": "This is a test PR",
+ "head": {
+ "label": "testuser:feature",
+ "ref": "feature",
+ "sha": "abc123",
+ "repo": {
+ "id": 1,
+ "name": "test-repo",
+ "full_name": "testuser/test-repo",
+ "html_url": "https://github.com/testuser/test-repo"
+ }
+ },
+ "base": {
+ "label": "testorg:main",
+ "ref": "main",
+ "sha": "def456",
+ "repo": {
+ "id": 2,
+ "name": "test-repo",
+ "full_name": "testorg/test-repo",
+ "html_url": "https://github.com/testorg/test-repo"
+ }
+ },
+ "draft": false
+ },
+ "repository": {
+ "id": 2,
+ "name": "test-repo",
+ "full_name": "testorg/test-repo",
+ "owner": {
+ "login": "testorg",
+ "id": 2
+ },
+ "html_url": "https://github.com/testorg/test-repo",
+ "default_branch": "main"
+ },
+ "sender": {
+ "login": "testuser",
+ "id": 1
+ }
+}`
+
+ // Gosmee script content
+ gosmeeScript := `#!/usr/bin/env bash
+# Copyright 2023 Chmouel Boudjnah
+# Replay script with headers and JSON payload to the target controller.
+#
+set -euxfo pipefail
+cd $(dirname $(readlink -f $0))
+
+targetURL="http://localhost:8082"
+if [[ ${1:-""} == -l ]]; then
+ targetURL="http://localhost:8082"
+elif [[ -n ${1:-""} ]]; then
+ targetURL=${1}
+elif [[ -n ${GOSMEE_DEBUG_SERVICE:-""} ]]; then
+ targetURL=${GOSMEE_DEBUG_SERVICE}
+fi
+
+curl -sSi -H "Content-Type: application/json" -H 'X-Forwarded-Proto: https' -H 'Accept-Encoding: gzip' -H 'Content-Length: 17' -H 'User-Agent: curl/8.7.1' -H 'X-Forwarded-Host: hook.pipelinesascode.com' -H 'X-Forwarded-For: 82.66.174.128' -H 'Accept: */*' -H 'X-GitHub-Event: pull_request' -H 'Via: 2.0 Caddy' -X POST -d @./payload.json ${targetURL}`
+
+ tempDir := fs.NewDir(t, "cel-gosmee-test")
+ defer tempDir.Remove()
+
+ // Create body file
+ bodyFile := tempDir.Join("payload.json")
+ err := os.WriteFile(bodyFile, []byte(pullRequestPayload), 0o600)
+ assert.NilError(t, err)
+
+ // Create gosmee script file
+ headersFile := tempDir.Join("replay.sh")
+ err = os.WriteFile(headersFile, []byte(gosmeeScript), 0o700) // Executable
+ assert.NilError(t, err)
+
+ ioStreams, out, _ := newIOStream()
+ // Write empty input to stdin to exit the interactive loop immediately
+ ioStreams.In = io.NopCloser(strings.NewReader("\n"))
+
+ cmd := Command(ioStreams)
+ cmd.SetArgs([]string{
+ "--provider", "github",
+ "--body", bodyFile,
+ "--headers", headersFile,
+ })
+
+ err = cmd.Execute()
+ // Interactive mode should exit gracefully on EOF
+ assert.NilError(t, err)
+
+ outStr := out.String()
+ // Verify the help text was printed and gosmee headers were parsed
+ assert.Assert(t, strings.Contains(outStr, "CEL Expression Evaluator for Pipelines as Code"))
+ assert.Assert(t, strings.Contains(outStr, "pac.event"))
+ assert.Assert(t, strings.Contains(outStr, "body.action"))
+ assert.Assert(t, strings.Contains(outStr, "headers['x-github-event']"))
+
+ // Test that the provider was auto-detected as GitHub
+ assert.Assert(t, strings.Contains(outStr, "Detected provider: github"))
+}
+
+func TestCommandWithRealWorldPayloads(t *testing.T) {
+ // Real-world payload from a GitHub PR opened event
+ realPRPayload := `{
+ "action": "opened",
+ "number": 1,
+ "pull_request": {
+ "id": 1,
+ "number": 1,
+ "title": "Test PR",
+ "user": {
+ "login": "testuser",
+ "id": 1
+ },
+ "body": "This is a test PR",
+ "created_at": "2023-01-01T00:00:00Z",
+ "head": {
+ "label": "testuser:feature",
+ "ref": "feature",
+ "sha": "abc123",
+ "repo": {
+ "id": 1,
+ "name": "test-repo",
+ "full_name": "testuser/test-repo",
+ "html_url": "https://github.com/testuser/test-repo"
+ }
+ },
+ "base": {
+ "label": "testorg:main",
+ "ref": "main",
+ "sha": "def456",
+ "repo": {
+ "id": 2,
+ "name": "test-repo",
+ "full_name": "testorg/test-repo",
+ "html_url": "https://github.com/testorg/test-repo"
+ }
+ },
+ "draft": false,
+ "labels": [
+ {
+ "id": 1,
+ "name": "enhancement",
+ "color": "a2eeef"
+ }
+ ]
+ },
+ "repository": {
+ "id": 2,
+ "name": "test-repo",
+ "full_name": "testorg/test-repo",
+ "owner": {
+ "login": "testorg",
+ "id": 2
+ },
+ "html_url": "https://github.com/testorg/test-repo",
+ "default_branch": "main"
+ },
+ "sender": {
+ "login": "testuser",
+ "id": 1
+ }
+}`
+
+ realHeaders := `Accept: */*
+Accept-Encoding: gzip, deflate, br
+Content-Type: application/json
+User-Agent: GitHub-Hookshot/044aadd
+X-GitHub-Delivery: 12345678-1234-1234-1234-123456789012
+X-GitHub-Event: pull_request
+X-GitHub-Hook-ID: 123456789
+X-GitHub-Hook-Installation-Target-ID: 987654321
+X-GitHub-Hook-Installation-Target-Type: repository`
+
+ tempDir := fs.NewDir(t, "cel-realworld-test")
+ defer tempDir.Remove()
+
+ bodyFile := tempDir.Join("payload.json")
+ err := os.WriteFile(bodyFile, []byte(realPRPayload), 0o600)
+ assert.NilError(t, err)
+
+ headersFile := tempDir.Join("headers.txt")
+ err = os.WriteFile(headersFile, []byte(realHeaders), 0o600)
+ assert.NilError(t, err)
+
+ ioStreams, out, _ := newIOStream()
+ // Write empty input to stdin to exit the interactive loop immediately
+ ioStreams.In = io.NopCloser(strings.NewReader("\n"))
+
+ cmd := Command(ioStreams)
+ cmd.SetArgs([]string{
+ "--provider", "github",
+ "--body", bodyFile,
+ "--headers", headersFile,
+ })
+
+ err = cmd.Execute()
+ // Interactive mode should exit gracefully on EOF
+ assert.NilError(t, err)
+
+ outStr := out.String()
+ // Verify the help text was printed
+ assert.Assert(t, strings.Contains(outStr, "CEL Expression Evaluator for Pipelines as Code"))
+ assert.Assert(t, strings.Contains(outStr, "pac.event"))
+ assert.Assert(t, strings.Contains(outStr, "body.action"))
+ assert.Assert(t, strings.Contains(outStr, "headers['x-github-event']"))
+}
+
+func TestCELExpressionEvaluation(t *testing.T) {
+ // This test simulates the CEL evaluation by using the same components
+ // the command uses, but without the interactive prompt
+ pullRequestPayload := `{
+ "action": "synchronize",
+ "number": 1234,
+ "pull_request": {
+ "title": "fix something somewhere out there",
+ "number": 1234,
+ "user": {
+ "login": "pachito"
+ },
+ "head": {
+ "ref": "parsepayload",
+ "sha": "178fd7ac8826595cffaa23e574eb0c02c3e76dcf",
+ "repo": {
+ "html_url": "https://github.com/pachito/pipelines-as-code"
+ }
+ },
+ "base": {
+ "ref": "main",
+ "repo": {
+ "html_url": "https://github.com/openshift-pipelines/pipelines-as-code"
+ }
+ },
+ "labels": [],
+ "draft": false
+ },
+ "repository": {
+ "name": "pipelines-as-code",
+ "full_name": "openshift-pipelines/pipelines-as-code",
+ "owner": {
+ "login": "openshift-pipelines"
+ },
+ "html_url": "https://github.com/openshift-pipelines/pipelines-as-code",
+ "default_branch": "main"
+ },
+ "sender": {
+ "login": "pachito"
+ }
+}`
+
+ headers := map[string]string{
+ "X-GitHub-Event": "pull_request",
+ "Content-Type": "application/json",
+ }
+
+ // Parse the event
+ event, err := eventFromGitHub([]byte(pullRequestPayload), headers)
+ assert.NilError(t, err)
+
+ // Generate PAC parameters
+ pacParams := pacParamsFromEvent(event)
+
+ // Parse the body for CEL access
+ var body map[string]any
+ err = json.Unmarshal([]byte(pullRequestPayload), &body)
+ assert.NilError(t, err)
+
+ // Create files data structure (empty for CLI)
+ filesData := map[string]any{
+ "all": []string{},
+ "added": []string{},
+ "deleted": []string{},
+ "modified": []string{},
+ "renamed": []string{},
+ }
+
+ // Test various CEL expressions that would be commonly used
+ tests := []struct {
+ name string
+ expression string
+ wantResult string // Using string to represent the expected result
+ wantErr bool
+ }{
+ {
+ name: "pac event type",
+ expression: `pac.event == "pull_request"`,
+ wantResult: "true",
+ },
+ {
+ name: "pac target branch",
+ expression: `pac.target_branch == "main"`,
+ wantResult: "true",
+ },
+ {
+ name: "pac sender",
+ expression: `pac.sender == "pachito"`,
+ wantResult: "true",
+ },
+ {
+ name: "body action",
+ expression: `body.action == "synchronize"`,
+ wantResult: "true",
+ },
+ {
+ name: "body PR number",
+ expression: `body.number == 1234`,
+ wantResult: "true",
+ },
+ {
+ name: "body PR not draft",
+ expression: `!body.pull_request.draft`,
+ wantResult: "true",
+ },
+ {
+ name: "headers check",
+ expression: `headers['X-GitHub-Event'] == "pull_request"`,
+ wantResult: "true",
+ },
+ {
+ name: "complex expression",
+ expression: `pac.event == "pull_request" && pac.target_branch == "main" && body.action == "synchronize"`,
+ wantResult: "true",
+ },
+ {
+ name: "false condition",
+ expression: `pac.target_branch == "develop"`,
+ wantResult: "false",
+ },
+ {
+ name: "invalid expression",
+ expression: `invalid.syntax...`,
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := pkgcel.Value(tt.expression, body, headers, pacParams, filesData)
+ if tt.wantErr {
+ assert.Assert(t, err != nil)
+ return
+ }
+ assert.NilError(t, err)
+ resultStr := fmt.Sprintf("%v", result)
+ assert.Equal(t, resultStr, tt.wantResult)
+ })
+ }
+}
+
+func TestDetectProvider(t *testing.T) {
+ tests := []struct {
+ name string
+ headers map[string]string
+ body []byte
+ expectedProvider string
+ expectedError bool
+ }{
+ {
+ name: "GitHub provider",
+ headers: map[string]string{
+ "X-GitHub-Event": "pull_request",
+ },
+ body: []byte(`{"repository": {"html_url": "https://github.com/owner/repo"}}`),
+ expectedProvider: "github",
+ },
+ {
+ name: "Gitea provider",
+ headers: map[string]string{
+ "X-GitHub-Event": "pull_request",
+ "X-Gitea-Event-Type": "pull_request",
+ },
+ body: []byte(`{"repository": {"html_url": "https://gitea.com/owner/repo"}}`),
+ expectedProvider: "gitea",
+ },
+ {
+ name: "GitLab provider",
+ headers: map[string]string{
+ "X-Gitlab-Event": "Merge Request Hook",
+ },
+ body: []byte(`{"project": {"web_url": "https://gitlab.com/owner/repo"}}`),
+ expectedProvider: "gitlab",
+ },
+ {
+ name: "Bitbucket Cloud provider",
+ headers: map[string]string{
+ "User-Agent": "Bitbucket-Webhooks/2.0",
+ },
+ body: []byte(`{"actor": {"account_id": "123"}}`),
+ expectedProvider: "bitbucket-cloud",
+ },
+ {
+ name: "Bitbucket Data Center provider",
+ headers: map[string]string{
+ "User-Agent": "Bitbucket-Webhooks/2.0",
+ },
+ body: []byte(`{"actor": {"id": 123}}`),
+ expectedProvider: "bitbucket-datacenter",
+ },
+ {
+ name: "Unknown provider",
+ headers: map[string]string{
+ "X-Unknown-Header": "value",
+ },
+ body: []byte(`{}`),
+ expectedError: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ provider, err := detectProvider(tt.headers, tt.body)
+ if tt.expectedError {
+ assert.Assert(t, err != nil)
+ } else {
+ assert.NilError(t, err)
+ assert.Equal(t, provider, tt.expectedProvider)
+ }
+ })
+ }
+}
+
+func TestEventFromGitLab(t *testing.T) {
+ tests := []struct {
+ name string
+ headers map[string]string
+ body []byte
+ expectedError bool
+ expectedOrg string
+ expectedRepo string
+ expectedSender string
+ expectedTrigger string
+ }{
+ {
+ name: "GitLab merge request event",
+ headers: map[string]string{
+ "X-Gitlab-Event": "Merge Request Hook",
+ },
+ body: []byte(`{
+ "object_kind": "merge_request",
+ "user": {"username": "testuser"},
+ "project": {
+ "path_with_namespace": "testorg/testrepo",
+ "web_url": "https://gitlab.com/testorg/testrepo"
+ },
+ "object_attributes": {
+ "iid": 1,
+ "target_branch": "main",
+ "source_branch": "feature",
+ "title": "Test MR"
+ }
+ }`),
+ expectedOrg: "testorg",
+ expectedRepo: "testrepo",
+ expectedSender: "testuser",
+ expectedTrigger: triggertype.PullRequest.String(),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ event, err := eventFromGitLab(tt.body, tt.headers)
+ if tt.expectedError {
+ assert.Assert(t, err != nil)
+ return
+ }
+ assert.NilError(t, err)
+ assert.Equal(t, event.Organization, tt.expectedOrg)
+ assert.Equal(t, event.Repository, tt.expectedRepo)
+ assert.Equal(t, event.Sender, tt.expectedSender)
+ assert.Equal(t, event.TriggerTarget.String(), tt.expectedTrigger)
+ })
+ }
+}
+
+func TestDirectCELVariables(t *testing.T) {
+ // Test that the direct CEL variables (as per PAC documentation) work correctly
+ pullRequestPayload := `{
+ "action": "opened",
+ "number": 123,
+ "pull_request": {
+ "title": "Add feature",
+ "user": {
+ "login": "testuser"
+ },
+ "head": {
+ "ref": "feature-branch",
+ "sha": "abc123",
+ "repo": {
+ "html_url": "https://github.com/testuser/test-repo"
+ }
+ },
+ "base": {
+ "ref": "main",
+ "repo": {
+ "html_url": "https://github.com/testorg/test-repo"
+ }
+ },
+ "draft": false
+ },
+ "repository": {
+ "name": "test-repo",
+ "full_name": "testorg/test-repo",
+ "owner": {
+ "login": "testorg"
+ },
+ "html_url": "https://github.com/testorg/test-repo",
+ "default_branch": "main"
+ },
+ "sender": {
+ "login": "testuser"
+ }
+}`
+
+ headers := map[string]string{
+ "X-GitHub-Event": "pull_request",
+ "Content-Type": "application/json",
+ }
+
+ // Parse the event
+ event, err := eventFromGitHub([]byte(pullRequestPayload), headers)
+ assert.NilError(t, err)
+
+ // Generate PAC parameters
+ pacParams := pacParamsFromEvent(event)
+
+ // Parse the body for CEL access
+ var body map[string]any
+ err = json.Unmarshal([]byte(pullRequestPayload), &body)
+ assert.NilError(t, err)
+
+ // Create files data structure (empty for CLI)
+ filesData := map[string]any{
+ "all": []string{},
+ "added": []string{},
+ "deleted": []string{},
+ "modified": []string{},
+ "renamed": []string{},
+ }
+
+ // Test direct CEL variables as per PAC documentation
+ tests := []struct {
+ name string
+ expression string
+ wantResult string
+ wantErr bool
+ }{
+ {
+ name: "direct event variable",
+ expression: `event == "pull_request"`,
+ wantResult: "true",
+ },
+ {
+ name: "direct target_branch variable",
+ expression: `target_branch == "main"`,
+ wantResult: "true",
+ },
+ {
+ name: "direct source_branch variable",
+ expression: `source_branch == "feature-branch"`,
+ wantResult: "true",
+ },
+ {
+ name: "direct event_title variable",
+ expression: `event_title == "Add feature"`,
+ wantResult: "true",
+ },
+ {
+ name: "direct target_url variable",
+ expression: `target_url.contains("testorg/test-repo")`,
+ wantResult: "true",
+ },
+ {
+ name: "direct source_url variable",
+ expression: `source_url.contains("testuser/test-repo")`,
+ wantResult: "true",
+ },
+ {
+ name: "combined expression like PAC docs",
+ expression: `event == "pull_request" && target_branch == "main"`,
+ wantResult: "true",
+ },
+ {
+ name: "regex matching like PAC docs",
+ expression: `source_branch.matches(".*feature.*")`,
+ wantResult: "true",
+ },
+ {
+ name: "negative condition like PAC docs",
+ expression: `event == "pull_request" && target_branch != "experimental"`,
+ wantResult: "true",
+ },
+ {
+ name: "backward compatibility - pac variables still work",
+ expression: `pac.event == "pull_request"`,
+ wantResult: "true",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := pkgcel.Value(tt.expression, body, headers, pacParams, filesData)
+ if tt.wantErr {
+ assert.Assert(t, err != nil)
+ return
+ }
+ assert.NilError(t, err)
+ resultStr := fmt.Sprintf("%v", result)
+ assert.Equal(t, resultStr, tt.wantResult)
+ })
+ }
+}
diff --git a/pkg/cmd/tknpac/cel/templates/help.tmpl b/pkg/cmd/tknpac/cel/templates/help.tmpl
new file mode 100644
index 000000000..0717ce382
--- /dev/null
+++ b/pkg/cmd/tknpac/cel/templates/help.tmpl
@@ -0,0 +1,38 @@
+🔍 CEL Expression Evaluator for Pipelines as Code
+🚰 Detected provider: %s
+
+📋 Available variables:
+ 🎯 Direct variables (as per PAC documentation):
+ • event - event type (push, pull_request)
+ • target_branch - target branch name
+ • source_branch - source branch name
+ • target_url - target repository URL
+ • source_url - source repository URL
+ • event_title - PR title or commit message
+ 📦 Webhook payload (body.*):
+ • body.action - PR action (opened, synchronize, etc.)
+ • body.number - PR number
+ • body.pull_request.user.login - PR author
+ • body.pull_request.draft - true if PR is draft
+ 📡 HTTP headers (headers.*):
+ • headers['x-github-event'] - GitHub event type
+ • headers['x-github-delivery'] - GitHub delivery ID
+ • headers['content-type'] - Request content type
+ • headers['user-agent'] - User agent (GitHub-Hookshot/...)
+ 📁 Files (files.*):
+ ⚠️ Note: File-related variables (e.g., files.all) and functions (e.g., fileChanged) are not supported in the CLI and will be empty or false.
+ 🔧 PAC Parameters (pac.* - for backward compatibility):
+ • pac.event, pac.target_branch, pac.source_branch, etc.
+
+� Headers file formats supported:
+ • JSON format: {"X-GitHub-Event": "pull_request", "Content-Type": "application/json"}
+ • HTTP format: X-GitHub-Event: pull_request
+ • Gosmee script: Shell scripts with curl commands and -H flags (automatically detected)
+
+�💡 Example expressions:
+ ✓ event == "pull_request" && target_branch == "main"
+ ✓ event == "pull_request" && source_branch.matches(".*feat/.*")
+ ✓ body.action == "synchronize"
+ ✓ !body.pull_request.draft
+ ✓ headers['x-github-event'] == "pull_request"
+ ✓ event == "pull_request" && target_branch != "experimental"
diff --git a/pkg/cmd/tknpac/root.go b/pkg/cmd/tknpac/root.go
index 9f4defc92..206b185a6 100644
--- a/pkg/cmd/tknpac/root.go
+++ b/pkg/cmd/tknpac/root.go
@@ -3,6 +3,7 @@ package tknpac
import (
"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
"github.com/openshift-pipelines/pipelines-as-code/pkg/cmd/tknpac/bootstrap"
+ "github.com/openshift-pipelines/pipelines-as-code/pkg/cmd/tknpac/cel"
"github.com/openshift-pipelines/pipelines-as-code/pkg/cmd/tknpac/completion"
"github.com/openshift-pipelines/pipelines-as-code/pkg/cmd/tknpac/create"
"github.com/openshift-pipelines/pipelines-as-code/pkg/cmd/tknpac/deleterepo"
@@ -43,6 +44,7 @@ func Root(clients *params.Run) *cobra.Command {
cmd.AddCommand(completion.Command())
cmd.AddCommand(bootstrap.Command(clients, ioStreams))
cmd.AddCommand(generate.Command(clients, ioStreams))
+ cmd.AddCommand(cel.Command(ioStreams))
cmd.AddCommand(webhook.Root(clients, ioStreams))
return cmd
}
diff --git a/vendor/github.com/chzyer/readline/.gitignore b/vendor/github.com/chzyer/readline/.gitignore
new file mode 100644
index 000000000..a3062beae
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/.gitignore
@@ -0,0 +1 @@
+.vscode/*
diff --git a/vendor/github.com/chzyer/readline/.travis.yml b/vendor/github.com/chzyer/readline/.travis.yml
new file mode 100644
index 000000000..9c3595543
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/.travis.yml
@@ -0,0 +1,8 @@
+language: go
+go:
+ - 1.x
+script:
+ - GOOS=windows go install github.com/chzyer/readline/example/...
+ - GOOS=linux go install github.com/chzyer/readline/example/...
+ - GOOS=darwin go install github.com/chzyer/readline/example/...
+ - go test -race -v
diff --git a/vendor/github.com/chzyer/readline/CHANGELOG.md b/vendor/github.com/chzyer/readline/CHANGELOG.md
new file mode 100644
index 000000000..14ff5be13
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/CHANGELOG.md
@@ -0,0 +1,58 @@
+# ChangeLog
+
+### 1.4 - 2016-07-25
+
+* [#60][60] Support dynamic autocompletion
+* Fix ANSI parser on Windows
+* Fix wrong column width in complete mode on Windows
+* Remove dependent package "golang.org/x/crypto/ssh/terminal"
+
+### 1.3 - 2016-05-09
+
+* [#38][38] add SetChildren for prefix completer interface
+* [#42][42] improve multiple lines compatibility
+* [#43][43] remove sub-package(runes) for gopkg compatibility
+* [#46][46] Auto complete with space prefixed line
+* [#48][48] support suspend process (ctrl+Z)
+* [#49][49] fix bug that check equals with previous command
+* [#53][53] Fix bug which causes integer divide by zero panicking when input buffer is empty
+
+### 1.2 - 2016-03-05
+
+* Add a demo for checking password strength [example/readline-pass-strength](https://github.com/chzyer/readline/blob/master/example/readline-pass-strength/readline-pass-strength.go), , written by [@sahib](https://github.com/sahib)
+* [#23][23], support stdin remapping
+* [#27][27], add a `UniqueEditLine` to `Config`, which will erase the editing line after user submited it, usually use in IM.
+* Add a demo for multiline [example/readline-multiline](https://github.com/chzyer/readline/blob/master/example/readline-multiline/readline-multiline.go) which can submit one SQL by multiple lines.
+* Supports performs even stdin/stdout is not a tty.
+* Add a new simple apis for single instance, check by [here](https://github.com/chzyer/readline/blob/master/std.go). It need to save history manually if using this api.
+* [#28][28], fixes the history is not working as expected.
+* [#33][33], vim mode now support `c`, `d`, `x (delete character)`, `r (replace character)`
+
+### 1.1 - 2015-11-20
+
+* [#12][12] Add support for key ``/``/``
+* Only enter raw mode as needed (calling `Readline()`), program will receive signal(e.g. Ctrl+C) if not interact with `readline`.
+* Bugs fixed for `PrefixCompleter`
+* Press `Ctrl+D` in empty line will cause `io.EOF` in error, Press `Ctrl+C` in anytime will cause `ErrInterrupt` instead of `io.EOF`, this will privodes a shell-like user experience.
+* Customable Interrupt/EOF prompt in `Config`
+* [#17][17] Change atomic package to use 32bit function to let it runnable on arm 32bit devices
+* Provides a new password user experience(`readline.ReadPasswordEx()`).
+
+### 1.0 - 2015-10-14
+
+* Initial public release.
+
+[12]: https://github.com/chzyer/readline/pull/12
+[17]: https://github.com/chzyer/readline/pull/17
+[23]: https://github.com/chzyer/readline/pull/23
+[27]: https://github.com/chzyer/readline/pull/27
+[28]: https://github.com/chzyer/readline/pull/28
+[33]: https://github.com/chzyer/readline/pull/33
+[38]: https://github.com/chzyer/readline/pull/38
+[42]: https://github.com/chzyer/readline/pull/42
+[43]: https://github.com/chzyer/readline/pull/43
+[46]: https://github.com/chzyer/readline/pull/46
+[48]: https://github.com/chzyer/readline/pull/48
+[49]: https://github.com/chzyer/readline/pull/49
+[53]: https://github.com/chzyer/readline/pull/53
+[60]: https://github.com/chzyer/readline/pull/60
diff --git a/vendor/github.com/chzyer/readline/LICENSE b/vendor/github.com/chzyer/readline/LICENSE
new file mode 100644
index 000000000..c9afab3dc
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Chzyer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/vendor/github.com/chzyer/readline/README.md b/vendor/github.com/chzyer/readline/README.md
new file mode 100644
index 000000000..4b0a5ff58
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/README.md
@@ -0,0 +1,114 @@
+[](https://travis-ci.org/chzyer/readline)
+[](LICENSE.md)
+[](https://github.com/chzyer/readline/releases)
+[](https://godoc.org/github.com/chzyer/readline)
+[](#backers)
+[](#sponsors)
+
+
+
+
+
+
+
+A powerful readline library in `Linux` `macOS` `Windows` `Solaris` `AIX`
+
+## Guide
+
+* [Demo](example/readline-demo/readline-demo.go)
+* [Shortcut](doc/shortcut.md)
+
+## Repos using readline
+
+[](https://github.com/cockroachdb/cockroach)
+[](https://github.com/robertkrimen/otto)
+[](https://github.com/remind101/empire)
+[](https://github.com/mehrdadrad/mylg)
+[](https://github.com/knq/usql)
+[](https://github.com/youtube/doorman)
+[](https://github.com/bom-d-van/harp)
+[](https://github.com/abiosoft/ishell)
+[](https://github.com/Netflix/hal-9001)
+[](https://github.com/docker/go-p9p)
+
+
+## Feedback
+
+If you have any questions, please submit a github issue and any pull requests is welcomed :)
+
+* [https://twitter.com/chzyer](https://twitter.com/chzyer)
+* [http://weibo.com/2145262190](http://weibo.com/2145262190)
+
+
+## Backers
+
+Love Readline? Help me keep it alive by donating funds to cover project expenses!
+[[Become a backer](https://opencollective.com/readline#backer)]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Sponsors
+
+Become a sponsor and get your logo here on our Github page. [[Become a sponsor](https://opencollective.com/readline#sponsor)]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vendor/github.com/chzyer/readline/ansi_windows.go b/vendor/github.com/chzyer/readline/ansi_windows.go
new file mode 100644
index 000000000..63b908c18
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/ansi_windows.go
@@ -0,0 +1,249 @@
+// +build windows
+
+package readline
+
+import (
+ "bufio"
+ "io"
+ "strconv"
+ "strings"
+ "sync"
+ "unicode/utf8"
+ "unsafe"
+)
+
+const (
+ _ = uint16(0)
+ COLOR_FBLUE = 0x0001
+ COLOR_FGREEN = 0x0002
+ COLOR_FRED = 0x0004
+ COLOR_FINTENSITY = 0x0008
+
+ COLOR_BBLUE = 0x0010
+ COLOR_BGREEN = 0x0020
+ COLOR_BRED = 0x0040
+ COLOR_BINTENSITY = 0x0080
+
+ COMMON_LVB_UNDERSCORE = 0x8000
+ COMMON_LVB_BOLD = 0x0007
+)
+
+var ColorTableFg = []word{
+ 0, // 30: Black
+ COLOR_FRED, // 31: Red
+ COLOR_FGREEN, // 32: Green
+ COLOR_FRED | COLOR_FGREEN, // 33: Yellow
+ COLOR_FBLUE, // 34: Blue
+ COLOR_FRED | COLOR_FBLUE, // 35: Magenta
+ COLOR_FGREEN | COLOR_FBLUE, // 36: Cyan
+ COLOR_FRED | COLOR_FBLUE | COLOR_FGREEN, // 37: White
+}
+
+var ColorTableBg = []word{
+ 0, // 40: Black
+ COLOR_BRED, // 41: Red
+ COLOR_BGREEN, // 42: Green
+ COLOR_BRED | COLOR_BGREEN, // 43: Yellow
+ COLOR_BBLUE, // 44: Blue
+ COLOR_BRED | COLOR_BBLUE, // 45: Magenta
+ COLOR_BGREEN | COLOR_BBLUE, // 46: Cyan
+ COLOR_BRED | COLOR_BBLUE | COLOR_BGREEN, // 47: White
+}
+
+type ANSIWriter struct {
+ target io.Writer
+ wg sync.WaitGroup
+ ctx *ANSIWriterCtx
+ sync.Mutex
+}
+
+func NewANSIWriter(w io.Writer) *ANSIWriter {
+ a := &ANSIWriter{
+ target: w,
+ ctx: NewANSIWriterCtx(w),
+ }
+ return a
+}
+
+func (a *ANSIWriter) Close() error {
+ a.wg.Wait()
+ return nil
+}
+
+type ANSIWriterCtx struct {
+ isEsc bool
+ isEscSeq bool
+ arg []string
+ target *bufio.Writer
+ wantFlush bool
+}
+
+func NewANSIWriterCtx(target io.Writer) *ANSIWriterCtx {
+ return &ANSIWriterCtx{
+ target: bufio.NewWriter(target),
+ }
+}
+
+func (a *ANSIWriterCtx) Flush() {
+ a.target.Flush()
+}
+
+func (a *ANSIWriterCtx) process(r rune) bool {
+ if a.wantFlush {
+ if r == 0 || r == CharEsc {
+ a.wantFlush = false
+ a.target.Flush()
+ }
+ }
+ if a.isEscSeq {
+ a.isEscSeq = a.ioloopEscSeq(a.target, r, &a.arg)
+ return true
+ }
+
+ switch r {
+ case CharEsc:
+ a.isEsc = true
+ case '[':
+ if a.isEsc {
+ a.arg = nil
+ a.isEscSeq = true
+ a.isEsc = false
+ break
+ }
+ fallthrough
+ default:
+ a.target.WriteRune(r)
+ a.wantFlush = true
+ }
+ return true
+}
+
+func (a *ANSIWriterCtx) ioloopEscSeq(w *bufio.Writer, r rune, argptr *[]string) bool {
+ arg := *argptr
+ var err error
+
+ if r >= 'A' && r <= 'D' {
+ count := short(GetInt(arg, 1))
+ info, err := GetConsoleScreenBufferInfo()
+ if err != nil {
+ return false
+ }
+ switch r {
+ case 'A': // up
+ info.dwCursorPosition.y -= count
+ case 'B': // down
+ info.dwCursorPosition.y += count
+ case 'C': // right
+ info.dwCursorPosition.x += count
+ case 'D': // left
+ info.dwCursorPosition.x -= count
+ }
+ SetConsoleCursorPosition(&info.dwCursorPosition)
+ return false
+ }
+
+ switch r {
+ case 'J':
+ killLines()
+ case 'K':
+ eraseLine()
+ case 'm':
+ color := word(0)
+ for _, item := range arg {
+ var c int
+ c, err = strconv.Atoi(item)
+ if err != nil {
+ w.WriteString("[" + strings.Join(arg, ";") + "m")
+ break
+ }
+ if c >= 30 && c < 40 {
+ color ^= COLOR_FINTENSITY
+ color |= ColorTableFg[c-30]
+ } else if c >= 40 && c < 50 {
+ color ^= COLOR_BINTENSITY
+ color |= ColorTableBg[c-40]
+ } else if c == 4 {
+ color |= COMMON_LVB_UNDERSCORE | ColorTableFg[7]
+ } else if c == 1 {
+ color |= COMMON_LVB_BOLD | COLOR_FINTENSITY
+ } else { // unknown code treat as reset
+ color = ColorTableFg[7]
+ }
+ }
+ if err != nil {
+ break
+ }
+ kernel.SetConsoleTextAttribute(stdout, uintptr(color))
+ case '\007': // set title
+ case ';':
+ if len(arg) == 0 || arg[len(arg)-1] != "" {
+ arg = append(arg, "")
+ *argptr = arg
+ }
+ return true
+ default:
+ if len(arg) == 0 {
+ arg = append(arg, "")
+ }
+ arg[len(arg)-1] += string(r)
+ *argptr = arg
+ return true
+ }
+ *argptr = nil
+ return false
+}
+
+func (a *ANSIWriter) Write(b []byte) (int, error) {
+ a.Lock()
+ defer a.Unlock()
+
+ off := 0
+ for len(b) > off {
+ r, size := utf8.DecodeRune(b[off:])
+ if size == 0 {
+ return off, io.ErrShortWrite
+ }
+ off += size
+ a.ctx.process(r)
+ }
+ a.ctx.Flush()
+ return off, nil
+}
+
+func killLines() error {
+ sbi, err := GetConsoleScreenBufferInfo()
+ if err != nil {
+ return err
+ }
+
+ size := (sbi.dwCursorPosition.y - sbi.dwSize.y) * sbi.dwSize.x
+ size += sbi.dwCursorPosition.x
+
+ var written int
+ kernel.FillConsoleOutputAttribute(stdout, uintptr(ColorTableFg[7]),
+ uintptr(size),
+ sbi.dwCursorPosition.ptr(),
+ uintptr(unsafe.Pointer(&written)),
+ )
+ return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '),
+ uintptr(size),
+ sbi.dwCursorPosition.ptr(),
+ uintptr(unsafe.Pointer(&written)),
+ )
+}
+
+func eraseLine() error {
+ sbi, err := GetConsoleScreenBufferInfo()
+ if err != nil {
+ return err
+ }
+
+ size := sbi.dwSize.x
+ sbi.dwCursorPosition.x = 0
+ var written int
+ return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '),
+ uintptr(size),
+ sbi.dwCursorPosition.ptr(),
+ uintptr(unsafe.Pointer(&written)),
+ )
+}
diff --git a/vendor/github.com/chzyer/readline/complete.go b/vendor/github.com/chzyer/readline/complete.go
new file mode 100644
index 000000000..c08c99414
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/complete.go
@@ -0,0 +1,285 @@
+package readline
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+)
+
+type AutoCompleter interface {
+ // Readline will pass the whole line and current offset to it
+ // Completer need to pass all the candidates, and how long they shared the same characters in line
+ // Example:
+ // [go, git, git-shell, grep]
+ // Do("g", 1) => ["o", "it", "it-shell", "rep"], 1
+ // Do("gi", 2) => ["t", "t-shell"], 2
+ // Do("git", 3) => ["", "-shell"], 3
+ Do(line []rune, pos int) (newLine [][]rune, length int)
+}
+
+type TabCompleter struct{}
+
+func (t *TabCompleter) Do([]rune, int) ([][]rune, int) {
+ return [][]rune{[]rune("\t")}, 0
+}
+
+type opCompleter struct {
+ w io.Writer
+ op *Operation
+ width int
+
+ inCompleteMode bool
+ inSelectMode bool
+ candidate [][]rune
+ candidateSource []rune
+ candidateOff int
+ candidateChoise int
+ candidateColNum int
+}
+
+func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter {
+ return &opCompleter{
+ w: w,
+ op: op,
+ width: width,
+ }
+}
+
+func (o *opCompleter) doSelect() {
+ if len(o.candidate) == 1 {
+ o.op.buf.WriteRunes(o.candidate[0])
+ o.ExitCompleteMode(false)
+ return
+ }
+ o.nextCandidate(1)
+ o.CompleteRefresh()
+}
+
+func (o *opCompleter) nextCandidate(i int) {
+ o.candidateChoise += i
+ o.candidateChoise = o.candidateChoise % len(o.candidate)
+ if o.candidateChoise < 0 {
+ o.candidateChoise = len(o.candidate) + o.candidateChoise
+ }
+}
+
+func (o *opCompleter) OnComplete() bool {
+ if o.width == 0 {
+ return false
+ }
+ if o.IsInCompleteSelectMode() {
+ o.doSelect()
+ return true
+ }
+
+ buf := o.op.buf
+ rs := buf.Runes()
+
+ if o.IsInCompleteMode() && o.candidateSource != nil && runes.Equal(rs, o.candidateSource) {
+ o.EnterCompleteSelectMode()
+ o.doSelect()
+ return true
+ }
+
+ o.ExitCompleteSelectMode()
+ o.candidateSource = rs
+ newLines, offset := o.op.cfg.AutoComplete.Do(rs, buf.idx)
+ if len(newLines) == 0 {
+ o.ExitCompleteMode(false)
+ return true
+ }
+
+ // only Aggregate candidates in non-complete mode
+ if !o.IsInCompleteMode() {
+ if len(newLines) == 1 {
+ buf.WriteRunes(newLines[0])
+ o.ExitCompleteMode(false)
+ return true
+ }
+
+ same, size := runes.Aggregate(newLines)
+ if size > 0 {
+ buf.WriteRunes(same)
+ o.ExitCompleteMode(false)
+ return true
+ }
+ }
+
+ o.EnterCompleteMode(offset, newLines)
+ return true
+}
+
+func (o *opCompleter) IsInCompleteSelectMode() bool {
+ return o.inSelectMode
+}
+
+func (o *opCompleter) IsInCompleteMode() bool {
+ return o.inCompleteMode
+}
+
+func (o *opCompleter) HandleCompleteSelect(r rune) bool {
+ next := true
+ switch r {
+ case CharEnter, CharCtrlJ:
+ next = false
+ o.op.buf.WriteRunes(o.op.candidate[o.op.candidateChoise])
+ o.ExitCompleteMode(false)
+ case CharLineStart:
+ num := o.candidateChoise % o.candidateColNum
+ o.nextCandidate(-num)
+ case CharLineEnd:
+ num := o.candidateColNum - o.candidateChoise%o.candidateColNum - 1
+ o.candidateChoise += num
+ if o.candidateChoise >= len(o.candidate) {
+ o.candidateChoise = len(o.candidate) - 1
+ }
+ case CharBackspace:
+ o.ExitCompleteSelectMode()
+ next = false
+ case CharTab, CharForward:
+ o.doSelect()
+ case CharBell, CharInterrupt:
+ o.ExitCompleteMode(true)
+ next = false
+ case CharNext:
+ tmpChoise := o.candidateChoise + o.candidateColNum
+ if tmpChoise >= o.getMatrixSize() {
+ tmpChoise -= o.getMatrixSize()
+ } else if tmpChoise >= len(o.candidate) {
+ tmpChoise += o.candidateColNum
+ tmpChoise -= o.getMatrixSize()
+ }
+ o.candidateChoise = tmpChoise
+ case CharBackward:
+ o.nextCandidate(-1)
+ case CharPrev:
+ tmpChoise := o.candidateChoise - o.candidateColNum
+ if tmpChoise < 0 {
+ tmpChoise += o.getMatrixSize()
+ if tmpChoise >= len(o.candidate) {
+ tmpChoise -= o.candidateColNum
+ }
+ }
+ o.candidateChoise = tmpChoise
+ default:
+ next = false
+ o.ExitCompleteSelectMode()
+ }
+ if next {
+ o.CompleteRefresh()
+ return true
+ }
+ return false
+}
+
+func (o *opCompleter) getMatrixSize() int {
+ line := len(o.candidate) / o.candidateColNum
+ if len(o.candidate)%o.candidateColNum != 0 {
+ line++
+ }
+ return line * o.candidateColNum
+}
+
+func (o *opCompleter) OnWidthChange(newWidth int) {
+ o.width = newWidth
+}
+
+func (o *opCompleter) CompleteRefresh() {
+ if !o.inCompleteMode {
+ return
+ }
+ lineCnt := o.op.buf.CursorLineCount()
+ colWidth := 0
+ for _, c := range o.candidate {
+ w := runes.WidthAll(c)
+ if w > colWidth {
+ colWidth = w
+ }
+ }
+ colWidth += o.candidateOff + 1
+ same := o.op.buf.RuneSlice(-o.candidateOff)
+
+ // -1 to avoid reach the end of line
+ width := o.width - 1
+ colNum := width / colWidth
+ if colNum != 0 {
+ colWidth += (width - (colWidth * colNum)) / colNum
+ }
+
+ o.candidateColNum = colNum
+ buf := bufio.NewWriter(o.w)
+ buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
+
+ colIdx := 0
+ lines := 1
+ buf.WriteString("\033[J")
+ for idx, c := range o.candidate {
+ inSelect := idx == o.candidateChoise && o.IsInCompleteSelectMode()
+ if inSelect {
+ buf.WriteString("\033[30;47m")
+ }
+ buf.WriteString(string(same))
+ buf.WriteString(string(c))
+ buf.Write(bytes.Repeat([]byte(" "), colWidth-runes.WidthAll(c)-runes.WidthAll(same)))
+
+ if inSelect {
+ buf.WriteString("\033[0m")
+ }
+
+ colIdx++
+ if colIdx == colNum {
+ buf.WriteString("\n")
+ lines++
+ colIdx = 0
+ }
+ }
+
+ // move back
+ fmt.Fprintf(buf, "\033[%dA\r", lineCnt-1+lines)
+ fmt.Fprintf(buf, "\033[%dC", o.op.buf.idx+o.op.buf.PromptLen())
+ buf.Flush()
+}
+
+func (o *opCompleter) aggCandidate(candidate [][]rune) int {
+ offset := 0
+ for i := 0; i < len(candidate[0]); i++ {
+ for j := 0; j < len(candidate)-1; j++ {
+ if i > len(candidate[j]) {
+ goto aggregate
+ }
+ if candidate[j][i] != candidate[j+1][i] {
+ goto aggregate
+ }
+ }
+ offset = i
+ }
+aggregate:
+ return offset
+}
+
+func (o *opCompleter) EnterCompleteSelectMode() {
+ o.inSelectMode = true
+ o.candidateChoise = -1
+ o.CompleteRefresh()
+}
+
+func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) {
+ o.inCompleteMode = true
+ o.candidate = candidate
+ o.candidateOff = offset
+ o.CompleteRefresh()
+}
+
+func (o *opCompleter) ExitCompleteSelectMode() {
+ o.inSelectMode = false
+ o.candidate = nil
+ o.candidateChoise = -1
+ o.candidateOff = -1
+ o.candidateSource = nil
+}
+
+func (o *opCompleter) ExitCompleteMode(revent bool) {
+ o.inCompleteMode = false
+ o.ExitCompleteSelectMode()
+}
diff --git a/vendor/github.com/chzyer/readline/complete_helper.go b/vendor/github.com/chzyer/readline/complete_helper.go
new file mode 100644
index 000000000..58d724872
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/complete_helper.go
@@ -0,0 +1,165 @@
+package readline
+
+import (
+ "bytes"
+ "strings"
+)
+
+// Caller type for dynamic completion
+type DynamicCompleteFunc func(string) []string
+
+type PrefixCompleterInterface interface {
+ Print(prefix string, level int, buf *bytes.Buffer)
+ Do(line []rune, pos int) (newLine [][]rune, length int)
+ GetName() []rune
+ GetChildren() []PrefixCompleterInterface
+ SetChildren(children []PrefixCompleterInterface)
+}
+
+type DynamicPrefixCompleterInterface interface {
+ PrefixCompleterInterface
+ IsDynamic() bool
+ GetDynamicNames(line []rune) [][]rune
+}
+
+type PrefixCompleter struct {
+ Name []rune
+ Dynamic bool
+ Callback DynamicCompleteFunc
+ Children []PrefixCompleterInterface
+}
+
+func (p *PrefixCompleter) Tree(prefix string) string {
+ buf := bytes.NewBuffer(nil)
+ p.Print(prefix, 0, buf)
+ return buf.String()
+}
+
+func Print(p PrefixCompleterInterface, prefix string, level int, buf *bytes.Buffer) {
+ if strings.TrimSpace(string(p.GetName())) != "" {
+ buf.WriteString(prefix)
+ if level > 0 {
+ buf.WriteString("├")
+ buf.WriteString(strings.Repeat("─", (level*4)-2))
+ buf.WriteString(" ")
+ }
+ buf.WriteString(string(p.GetName()) + "\n")
+ level++
+ }
+ for _, ch := range p.GetChildren() {
+ ch.Print(prefix, level, buf)
+ }
+}
+
+func (p *PrefixCompleter) Print(prefix string, level int, buf *bytes.Buffer) {
+ Print(p, prefix, level, buf)
+}
+
+func (p *PrefixCompleter) IsDynamic() bool {
+ return p.Dynamic
+}
+
+func (p *PrefixCompleter) GetName() []rune {
+ return p.Name
+}
+
+func (p *PrefixCompleter) GetDynamicNames(line []rune) [][]rune {
+ var names = [][]rune{}
+ for _, name := range p.Callback(string(line)) {
+ names = append(names, []rune(name+" "))
+ }
+ return names
+}
+
+func (p *PrefixCompleter) GetChildren() []PrefixCompleterInterface {
+ return p.Children
+}
+
+func (p *PrefixCompleter) SetChildren(children []PrefixCompleterInterface) {
+ p.Children = children
+}
+
+func NewPrefixCompleter(pc ...PrefixCompleterInterface) *PrefixCompleter {
+ return PcItem("", pc...)
+}
+
+func PcItem(name string, pc ...PrefixCompleterInterface) *PrefixCompleter {
+ name += " "
+ return &PrefixCompleter{
+ Name: []rune(name),
+ Dynamic: false,
+ Children: pc,
+ }
+}
+
+func PcItemDynamic(callback DynamicCompleteFunc, pc ...PrefixCompleterInterface) *PrefixCompleter {
+ return &PrefixCompleter{
+ Callback: callback,
+ Dynamic: true,
+ Children: pc,
+ }
+}
+
+func (p *PrefixCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) {
+ return doInternal(p, line, pos, line)
+}
+
+func Do(p PrefixCompleterInterface, line []rune, pos int) (newLine [][]rune, offset int) {
+ return doInternal(p, line, pos, line)
+}
+
+func doInternal(p PrefixCompleterInterface, line []rune, pos int, origLine []rune) (newLine [][]rune, offset int) {
+ line = runes.TrimSpaceLeft(line[:pos])
+ goNext := false
+ var lineCompleter PrefixCompleterInterface
+ for _, child := range p.GetChildren() {
+ childNames := make([][]rune, 1)
+
+ childDynamic, ok := child.(DynamicPrefixCompleterInterface)
+ if ok && childDynamic.IsDynamic() {
+ childNames = childDynamic.GetDynamicNames(origLine)
+ } else {
+ childNames[0] = child.GetName()
+ }
+
+ for _, childName := range childNames {
+ if len(line) >= len(childName) {
+ if runes.HasPrefix(line, childName) {
+ if len(line) == len(childName) {
+ newLine = append(newLine, []rune{' '})
+ } else {
+ newLine = append(newLine, childName)
+ }
+ offset = len(childName)
+ lineCompleter = child
+ goNext = true
+ }
+ } else {
+ if runes.HasPrefix(childName, line) {
+ newLine = append(newLine, childName[len(line):])
+ offset = len(line)
+ lineCompleter = child
+ }
+ }
+ }
+ }
+
+ if len(newLine) != 1 {
+ return
+ }
+
+ tmpLine := make([]rune, 0, len(line))
+ for i := offset; i < len(line); i++ {
+ if line[i] == ' ' {
+ continue
+ }
+
+ tmpLine = append(tmpLine, line[i:]...)
+ return doInternal(lineCompleter, tmpLine, len(tmpLine), origLine)
+ }
+
+ if goNext {
+ return doInternal(lineCompleter, nil, 0, origLine)
+ }
+ return
+}
diff --git a/vendor/github.com/chzyer/readline/complete_segment.go b/vendor/github.com/chzyer/readline/complete_segment.go
new file mode 100644
index 000000000..5ceadd80f
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/complete_segment.go
@@ -0,0 +1,82 @@
+package readline
+
+type SegmentCompleter interface {
+ // a
+ // |- a1
+ // |--- a11
+ // |- a2
+ // b
+ // input:
+ // DoTree([], 0) [a, b]
+ // DoTree([a], 1) [a]
+ // DoTree([a, ], 0) [a1, a2]
+ // DoTree([a, a], 1) [a1, a2]
+ // DoTree([a, a1], 2) [a1]
+ // DoTree([a, a1, ], 0) [a11]
+ // DoTree([a, a1, a], 1) [a11]
+ DoSegment([][]rune, int) [][]rune
+}
+
+type dumpSegmentCompleter struct {
+ f func([][]rune, int) [][]rune
+}
+
+func (d *dumpSegmentCompleter) DoSegment(segment [][]rune, n int) [][]rune {
+ return d.f(segment, n)
+}
+
+func SegmentFunc(f func([][]rune, int) [][]rune) AutoCompleter {
+ return &SegmentComplete{&dumpSegmentCompleter{f}}
+}
+
+func SegmentAutoComplete(completer SegmentCompleter) *SegmentComplete {
+ return &SegmentComplete{
+ SegmentCompleter: completer,
+ }
+}
+
+type SegmentComplete struct {
+ SegmentCompleter
+}
+
+func RetSegment(segments [][]rune, cands [][]rune, idx int) ([][]rune, int) {
+ ret := make([][]rune, 0, len(cands))
+ lastSegment := segments[len(segments)-1]
+ for _, cand := range cands {
+ if !runes.HasPrefix(cand, lastSegment) {
+ continue
+ }
+ ret = append(ret, cand[len(lastSegment):])
+ }
+ return ret, idx
+}
+
+func SplitSegment(line []rune, pos int) ([][]rune, int) {
+ segs := [][]rune{}
+ lastIdx := -1
+ line = line[:pos]
+ pos = 0
+ for idx, l := range line {
+ if l == ' ' {
+ pos = 0
+ segs = append(segs, line[lastIdx+1:idx])
+ lastIdx = idx
+ } else {
+ pos++
+ }
+ }
+ segs = append(segs, line[lastIdx+1:])
+ return segs, pos
+}
+
+func (c *SegmentComplete) Do(line []rune, pos int) (newLine [][]rune, offset int) {
+
+ segment, idx := SplitSegment(line, pos)
+
+ cands := c.DoSegment(segment, idx)
+ newLine, offset = RetSegment(segment, cands, idx)
+ for idx := range newLine {
+ newLine[idx] = append(newLine[idx], ' ')
+ }
+ return newLine, offset
+}
diff --git a/vendor/github.com/chzyer/readline/history.go b/vendor/github.com/chzyer/readline/history.go
new file mode 100644
index 000000000..6b17c464b
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/history.go
@@ -0,0 +1,330 @@
+package readline
+
+import (
+ "bufio"
+ "container/list"
+ "fmt"
+ "os"
+ "strings"
+ "sync"
+)
+
+type hisItem struct {
+ Source []rune
+ Version int64
+ Tmp []rune
+}
+
+func (h *hisItem) Clean() {
+ h.Source = nil
+ h.Tmp = nil
+}
+
+type opHistory struct {
+ cfg *Config
+ history *list.List
+ historyVer int64
+ current *list.Element
+ fd *os.File
+ fdLock sync.Mutex
+ enable bool
+}
+
+func newOpHistory(cfg *Config) (o *opHistory) {
+ o = &opHistory{
+ cfg: cfg,
+ history: list.New(),
+ enable: true,
+ }
+ return o
+}
+
+func (o *opHistory) Reset() {
+ o.history = list.New()
+ o.current = nil
+}
+
+func (o *opHistory) IsHistoryClosed() bool {
+ o.fdLock.Lock()
+ defer o.fdLock.Unlock()
+ return o.fd.Fd() == ^(uintptr(0))
+}
+
+func (o *opHistory) Init() {
+ if o.IsHistoryClosed() {
+ o.initHistory()
+ }
+}
+
+func (o *opHistory) initHistory() {
+ if o.cfg.HistoryFile != "" {
+ o.historyUpdatePath(o.cfg.HistoryFile)
+ }
+}
+
+// only called by newOpHistory
+func (o *opHistory) historyUpdatePath(path string) {
+ o.fdLock.Lock()
+ defer o.fdLock.Unlock()
+ f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
+ if err != nil {
+ return
+ }
+ o.fd = f
+ r := bufio.NewReader(o.fd)
+ total := 0
+ for ; ; total++ {
+ line, err := r.ReadString('\n')
+ if err != nil {
+ break
+ }
+ // ignore the empty line
+ line = strings.TrimSpace(line)
+ if len(line) == 0 {
+ continue
+ }
+ o.Push([]rune(line))
+ o.Compact()
+ }
+ if total > o.cfg.HistoryLimit {
+ o.rewriteLocked()
+ }
+ o.historyVer++
+ o.Push(nil)
+ return
+}
+
+func (o *opHistory) Compact() {
+ for o.history.Len() > o.cfg.HistoryLimit && o.history.Len() > 0 {
+ o.history.Remove(o.history.Front())
+ }
+}
+
+func (o *opHistory) Rewrite() {
+ o.fdLock.Lock()
+ defer o.fdLock.Unlock()
+ o.rewriteLocked()
+}
+
+func (o *opHistory) rewriteLocked() {
+ if o.cfg.HistoryFile == "" {
+ return
+ }
+
+ tmpFile := o.cfg.HistoryFile + ".tmp"
+ fd, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0666)
+ if err != nil {
+ return
+ }
+
+ buf := bufio.NewWriter(fd)
+ for elem := o.history.Front(); elem != nil; elem = elem.Next() {
+ buf.WriteString(string(elem.Value.(*hisItem).Source) + "\n")
+ }
+ buf.Flush()
+
+ // replace history file
+ if err = os.Rename(tmpFile, o.cfg.HistoryFile); err != nil {
+ fd.Close()
+ return
+ }
+
+ if o.fd != nil {
+ o.fd.Close()
+ }
+ // fd is write only, just satisfy what we need.
+ o.fd = fd
+}
+
+func (o *opHistory) Close() {
+ o.fdLock.Lock()
+ defer o.fdLock.Unlock()
+ if o.fd != nil {
+ o.fd.Close()
+ }
+}
+
+func (o *opHistory) FindBck(isNewSearch bool, rs []rune, start int) (int, *list.Element) {
+ for elem := o.current; elem != nil; elem = elem.Prev() {
+ item := o.showItem(elem.Value)
+ if isNewSearch {
+ start += len(rs)
+ }
+ if elem == o.current {
+ if len(item) >= start {
+ item = item[:start]
+ }
+ }
+ idx := runes.IndexAllBckEx(item, rs, o.cfg.HistorySearchFold)
+ if idx < 0 {
+ continue
+ }
+ return idx, elem
+ }
+ return -1, nil
+}
+
+func (o *opHistory) FindFwd(isNewSearch bool, rs []rune, start int) (int, *list.Element) {
+ for elem := o.current; elem != nil; elem = elem.Next() {
+ item := o.showItem(elem.Value)
+ if isNewSearch {
+ start -= len(rs)
+ if start < 0 {
+ start = 0
+ }
+ }
+ if elem == o.current {
+ if len(item)-1 >= start {
+ item = item[start:]
+ } else {
+ continue
+ }
+ }
+ idx := runes.IndexAllEx(item, rs, o.cfg.HistorySearchFold)
+ if idx < 0 {
+ continue
+ }
+ if elem == o.current {
+ idx += start
+ }
+ return idx, elem
+ }
+ return -1, nil
+}
+
+func (o *opHistory) showItem(obj interface{}) []rune {
+ item := obj.(*hisItem)
+ if item.Version == o.historyVer {
+ return item.Tmp
+ }
+ return item.Source
+}
+
+func (o *opHistory) Prev() []rune {
+ if o.current == nil {
+ return nil
+ }
+ current := o.current.Prev()
+ if current == nil {
+ return nil
+ }
+ o.current = current
+ return runes.Copy(o.showItem(current.Value))
+}
+
+func (o *opHistory) Next() ([]rune, bool) {
+ if o.current == nil {
+ return nil, false
+ }
+ current := o.current.Next()
+ if current == nil {
+ return nil, false
+ }
+
+ o.current = current
+ return runes.Copy(o.showItem(current.Value)), true
+}
+
+// Disable the current history
+func (o *opHistory) Disable() {
+ o.enable = false
+}
+
+// Enable the current history
+func (o *opHistory) Enable() {
+ o.enable = true
+}
+
+func (o *opHistory) debug() {
+ Debug("-------")
+ for item := o.history.Front(); item != nil; item = item.Next() {
+ Debug(fmt.Sprintf("%+v", item.Value))
+ }
+}
+
+// save history
+func (o *opHistory) New(current []rune) (err error) {
+
+ // history deactivated
+ if !o.enable {
+ return nil
+ }
+
+ current = runes.Copy(current)
+
+ // if just use last command without modify
+ // just clean lastest history
+ if back := o.history.Back(); back != nil {
+ prev := back.Prev()
+ if prev != nil {
+ if runes.Equal(current, prev.Value.(*hisItem).Source) {
+ o.current = o.history.Back()
+ o.current.Value.(*hisItem).Clean()
+ o.historyVer++
+ return nil
+ }
+ }
+ }
+
+ if len(current) == 0 {
+ o.current = o.history.Back()
+ if o.current != nil {
+ o.current.Value.(*hisItem).Clean()
+ o.historyVer++
+ return nil
+ }
+ }
+
+ if o.current != o.history.Back() {
+ // move history item to current command
+ currentItem := o.current.Value.(*hisItem)
+ // set current to last item
+ o.current = o.history.Back()
+
+ current = runes.Copy(currentItem.Tmp)
+ }
+
+ // err only can be a IO error, just report
+ err = o.Update(current, true)
+
+ // push a new one to commit current command
+ o.historyVer++
+ o.Push(nil)
+ return
+}
+
+func (o *opHistory) Revert() {
+ o.historyVer++
+ o.current = o.history.Back()
+}
+
+func (o *opHistory) Update(s []rune, commit bool) (err error) {
+ o.fdLock.Lock()
+ defer o.fdLock.Unlock()
+ s = runes.Copy(s)
+ if o.current == nil {
+ o.Push(s)
+ o.Compact()
+ return
+ }
+ r := o.current.Value.(*hisItem)
+ r.Version = o.historyVer
+ if commit {
+ r.Source = s
+ if o.fd != nil {
+ // just report the error
+ _, err = o.fd.Write([]byte(string(r.Source) + "\n"))
+ }
+ } else {
+ r.Tmp = append(r.Tmp[:0], s...)
+ }
+ o.current.Value = r
+ o.Compact()
+ return
+}
+
+func (o *opHistory) Push(s []rune) {
+ s = runes.Copy(s)
+ elem := o.history.PushBack(&hisItem{Source: s})
+ o.current = elem
+}
diff --git a/vendor/github.com/chzyer/readline/operation.go b/vendor/github.com/chzyer/readline/operation.go
new file mode 100644
index 000000000..b60939a91
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/operation.go
@@ -0,0 +1,537 @@
+package readline
+
+import (
+ "errors"
+ "io"
+ "sync"
+)
+
+var (
+ ErrInterrupt = errors.New("Interrupt")
+)
+
+type InterruptError struct {
+ Line []rune
+}
+
+func (*InterruptError) Error() string {
+ return "Interrupted"
+}
+
+type Operation struct {
+ m sync.Mutex
+ cfg *Config
+ t *Terminal
+ buf *RuneBuffer
+ outchan chan []rune
+ errchan chan error
+ w io.Writer
+
+ history *opHistory
+ *opSearch
+ *opCompleter
+ *opPassword
+ *opVim
+}
+
+func (o *Operation) SetBuffer(what string) {
+ o.buf.Set([]rune(what))
+}
+
+type wrapWriter struct {
+ r *Operation
+ t *Terminal
+ target io.Writer
+}
+
+func (w *wrapWriter) Write(b []byte) (int, error) {
+ if !w.t.IsReading() {
+ return w.target.Write(b)
+ }
+
+ var (
+ n int
+ err error
+ )
+ w.r.buf.Refresh(func() {
+ n, err = w.target.Write(b)
+ })
+
+ if w.r.IsSearchMode() {
+ w.r.SearchRefresh(-1)
+ }
+ if w.r.IsInCompleteMode() {
+ w.r.CompleteRefresh()
+ }
+ return n, err
+}
+
+func NewOperation(t *Terminal, cfg *Config) *Operation {
+ width := cfg.FuncGetWidth()
+ op := &Operation{
+ t: t,
+ buf: NewRuneBuffer(t, cfg.Prompt, cfg, width),
+ outchan: make(chan []rune),
+ errchan: make(chan error, 1),
+ }
+ op.w = op.buf.w
+ op.SetConfig(cfg)
+ op.opVim = newVimMode(op)
+ op.opCompleter = newOpCompleter(op.buf.w, op, width)
+ op.opPassword = newOpPassword(op)
+ op.cfg.FuncOnWidthChanged(func() {
+ newWidth := cfg.FuncGetWidth()
+ op.opCompleter.OnWidthChange(newWidth)
+ op.opSearch.OnWidthChange(newWidth)
+ op.buf.OnWidthChange(newWidth)
+ })
+ go op.ioloop()
+ return op
+}
+
+func (o *Operation) SetPrompt(s string) {
+ o.buf.SetPrompt(s)
+}
+
+func (o *Operation) SetMaskRune(r rune) {
+ o.buf.SetMask(r)
+}
+
+func (o *Operation) GetConfig() *Config {
+ o.m.Lock()
+ cfg := *o.cfg
+ o.m.Unlock()
+ return &cfg
+}
+
+func (o *Operation) ioloop() {
+ for {
+ keepInSearchMode := false
+ keepInCompleteMode := false
+ r := o.t.ReadRune()
+
+ if o.GetConfig().FuncFilterInputRune != nil {
+ var process bool
+ r, process = o.GetConfig().FuncFilterInputRune(r)
+ if !process {
+ o.t.KickRead()
+ o.buf.Refresh(nil) // to refresh the line
+ continue // ignore this rune
+ }
+ }
+
+ if r == 0 { // io.EOF
+ if o.buf.Len() == 0 {
+ o.buf.Clean()
+ select {
+ case o.errchan <- io.EOF:
+ }
+ break
+ } else {
+ // if stdin got io.EOF and there is something left in buffer,
+ // let's flush them by sending CharEnter.
+ // And we will got io.EOF int next loop.
+ r = CharEnter
+ }
+ }
+ isUpdateHistory := true
+
+ if o.IsInCompleteSelectMode() {
+ keepInCompleteMode = o.HandleCompleteSelect(r)
+ if keepInCompleteMode {
+ continue
+ }
+
+ o.buf.Refresh(nil)
+ switch r {
+ case CharEnter, CharCtrlJ:
+ o.history.Update(o.buf.Runes(), false)
+ fallthrough
+ case CharInterrupt:
+ o.t.KickRead()
+ fallthrough
+ case CharBell:
+ continue
+ }
+ }
+
+ if o.IsEnableVimMode() {
+ r = o.HandleVim(r, o.t.ReadRune)
+ if r == 0 {
+ continue
+ }
+ }
+
+ switch r {
+ case CharBell:
+ if o.IsSearchMode() {
+ o.ExitSearchMode(true)
+ o.buf.Refresh(nil)
+ }
+ if o.IsInCompleteMode() {
+ o.ExitCompleteMode(true)
+ o.buf.Refresh(nil)
+ }
+ case CharTab:
+ if o.GetConfig().AutoComplete == nil {
+ o.t.Bell()
+ break
+ }
+ if o.OnComplete() {
+ keepInCompleteMode = true
+ } else {
+ o.t.Bell()
+ break
+ }
+
+ case CharBckSearch:
+ if !o.SearchMode(S_DIR_BCK) {
+ o.t.Bell()
+ break
+ }
+ keepInSearchMode = true
+ case CharCtrlU:
+ o.buf.KillFront()
+ case CharFwdSearch:
+ if !o.SearchMode(S_DIR_FWD) {
+ o.t.Bell()
+ break
+ }
+ keepInSearchMode = true
+ case CharKill:
+ o.buf.Kill()
+ keepInCompleteMode = true
+ case MetaForward:
+ o.buf.MoveToNextWord()
+ case CharTranspose:
+ o.buf.Transpose()
+ case MetaBackward:
+ o.buf.MoveToPrevWord()
+ case MetaDelete:
+ o.buf.DeleteWord()
+ case CharLineStart:
+ o.buf.MoveToLineStart()
+ case CharLineEnd:
+ o.buf.MoveToLineEnd()
+ case CharBackspace, CharCtrlH:
+ if o.IsSearchMode() {
+ o.SearchBackspace()
+ keepInSearchMode = true
+ break
+ }
+
+ if o.buf.Len() == 0 {
+ o.t.Bell()
+ break
+ }
+ o.buf.Backspace()
+ if o.IsInCompleteMode() {
+ o.OnComplete()
+ }
+ case CharCtrlZ:
+ o.buf.Clean()
+ o.t.SleepToResume()
+ o.Refresh()
+ case CharCtrlL:
+ ClearScreen(o.w)
+ o.Refresh()
+ case MetaBackspace, CharCtrlW:
+ o.buf.BackEscapeWord()
+ case CharCtrlY:
+ o.buf.Yank()
+ case CharEnter, CharCtrlJ:
+ if o.IsSearchMode() {
+ o.ExitSearchMode(false)
+ }
+ o.buf.MoveToLineEnd()
+ var data []rune
+ if !o.GetConfig().UniqueEditLine {
+ o.buf.WriteRune('\n')
+ data = o.buf.Reset()
+ data = data[:len(data)-1] // trim \n
+ } else {
+ o.buf.Clean()
+ data = o.buf.Reset()
+ }
+ o.outchan <- data
+ if !o.GetConfig().DisableAutoSaveHistory {
+ // ignore IO error
+ _ = o.history.New(data)
+ } else {
+ isUpdateHistory = false
+ }
+ case CharBackward:
+ o.buf.MoveBackward()
+ case CharForward:
+ o.buf.MoveForward()
+ case CharPrev:
+ buf := o.history.Prev()
+ if buf != nil {
+ o.buf.Set(buf)
+ } else {
+ o.t.Bell()
+ }
+ case CharNext:
+ buf, ok := o.history.Next()
+ if ok {
+ o.buf.Set(buf)
+ } else {
+ o.t.Bell()
+ }
+ case CharDelete:
+ if o.buf.Len() > 0 || !o.IsNormalMode() {
+ o.t.KickRead()
+ if !o.buf.Delete() {
+ o.t.Bell()
+ }
+ break
+ }
+
+ // treat as EOF
+ if !o.GetConfig().UniqueEditLine {
+ o.buf.WriteString(o.GetConfig().EOFPrompt + "\n")
+ }
+ o.buf.Reset()
+ isUpdateHistory = false
+ o.history.Revert()
+ o.errchan <- io.EOF
+ if o.GetConfig().UniqueEditLine {
+ o.buf.Clean()
+ }
+ case CharInterrupt:
+ if o.IsSearchMode() {
+ o.t.KickRead()
+ o.ExitSearchMode(true)
+ break
+ }
+ if o.IsInCompleteMode() {
+ o.t.KickRead()
+ o.ExitCompleteMode(true)
+ o.buf.Refresh(nil)
+ break
+ }
+ o.buf.MoveToLineEnd()
+ o.buf.Refresh(nil)
+ hint := o.GetConfig().InterruptPrompt + "\n"
+ if !o.GetConfig().UniqueEditLine {
+ o.buf.WriteString(hint)
+ }
+ remain := o.buf.Reset()
+ if !o.GetConfig().UniqueEditLine {
+ remain = remain[:len(remain)-len([]rune(hint))]
+ }
+ isUpdateHistory = false
+ o.history.Revert()
+ o.errchan <- &InterruptError{remain}
+ default:
+ if o.IsSearchMode() {
+ o.SearchChar(r)
+ keepInSearchMode = true
+ break
+ }
+ o.buf.WriteRune(r)
+ if o.IsInCompleteMode() {
+ o.OnComplete()
+ keepInCompleteMode = true
+ }
+ }
+
+ listener := o.GetConfig().Listener
+ if listener != nil {
+ newLine, newPos, ok := listener.OnChange(o.buf.Runes(), o.buf.Pos(), r)
+ if ok {
+ o.buf.SetWithIdx(newPos, newLine)
+ }
+ }
+
+ o.m.Lock()
+ if !keepInSearchMode && o.IsSearchMode() {
+ o.ExitSearchMode(false)
+ o.buf.Refresh(nil)
+ } else if o.IsInCompleteMode() {
+ if !keepInCompleteMode {
+ o.ExitCompleteMode(false)
+ o.Refresh()
+ } else {
+ o.buf.Refresh(nil)
+ o.CompleteRefresh()
+ }
+ }
+ if isUpdateHistory && !o.IsSearchMode() {
+ // it will cause null history
+ o.history.Update(o.buf.Runes(), false)
+ }
+ o.m.Unlock()
+ }
+}
+
+func (o *Operation) Stderr() io.Writer {
+ return &wrapWriter{target: o.GetConfig().Stderr, r: o, t: o.t}
+}
+
+func (o *Operation) Stdout() io.Writer {
+ return &wrapWriter{target: o.GetConfig().Stdout, r: o, t: o.t}
+}
+
+func (o *Operation) String() (string, error) {
+ r, err := o.Runes()
+ return string(r), err
+}
+
+func (o *Operation) Runes() ([]rune, error) {
+ o.t.EnterRawMode()
+ defer o.t.ExitRawMode()
+
+ listener := o.GetConfig().Listener
+ if listener != nil {
+ listener.OnChange(nil, 0, 0)
+ }
+
+ o.buf.Refresh(nil) // print prompt
+ o.t.KickRead()
+ select {
+ case r := <-o.outchan:
+ return r, nil
+ case err := <-o.errchan:
+ if e, ok := err.(*InterruptError); ok {
+ return e.Line, ErrInterrupt
+ }
+ return nil, err
+ }
+}
+
+func (o *Operation) PasswordEx(prompt string, l Listener) ([]byte, error) {
+ cfg := o.GenPasswordConfig()
+ cfg.Prompt = prompt
+ cfg.Listener = l
+ return o.PasswordWithConfig(cfg)
+}
+
+func (o *Operation) GenPasswordConfig() *Config {
+ return o.opPassword.PasswordConfig()
+}
+
+func (o *Operation) PasswordWithConfig(cfg *Config) ([]byte, error) {
+ if err := o.opPassword.EnterPasswordMode(cfg); err != nil {
+ return nil, err
+ }
+ defer o.opPassword.ExitPasswordMode()
+ return o.Slice()
+}
+
+func (o *Operation) Password(prompt string) ([]byte, error) {
+ return o.PasswordEx(prompt, nil)
+}
+
+func (o *Operation) SetTitle(t string) {
+ o.w.Write([]byte("\033[2;" + t + "\007"))
+}
+
+func (o *Operation) Slice() ([]byte, error) {
+ r, err := o.Runes()
+ if err != nil {
+ return nil, err
+ }
+ return []byte(string(r)), nil
+}
+
+func (o *Operation) Close() {
+ select {
+ case o.errchan <- io.EOF:
+ default:
+ }
+ o.history.Close()
+}
+
+func (o *Operation) SetHistoryPath(path string) {
+ if o.history != nil {
+ o.history.Close()
+ }
+ o.cfg.HistoryFile = path
+ o.history = newOpHistory(o.cfg)
+}
+
+func (o *Operation) IsNormalMode() bool {
+ return !o.IsInCompleteMode() && !o.IsSearchMode()
+}
+
+func (op *Operation) SetConfig(cfg *Config) (*Config, error) {
+ op.m.Lock()
+ defer op.m.Unlock()
+ if op.cfg == cfg {
+ return op.cfg, nil
+ }
+ if err := cfg.Init(); err != nil {
+ return op.cfg, err
+ }
+ old := op.cfg
+ op.cfg = cfg
+ op.SetPrompt(cfg.Prompt)
+ op.SetMaskRune(cfg.MaskRune)
+ op.buf.SetConfig(cfg)
+ width := op.cfg.FuncGetWidth()
+
+ if cfg.opHistory == nil {
+ op.SetHistoryPath(cfg.HistoryFile)
+ cfg.opHistory = op.history
+ cfg.opSearch = newOpSearch(op.buf.w, op.buf, op.history, cfg, width)
+ }
+ op.history = cfg.opHistory
+
+ // SetHistoryPath will close opHistory which already exists
+ // so if we use it next time, we need to reopen it by `InitHistory()`
+ op.history.Init()
+
+ if op.cfg.AutoComplete != nil {
+ op.opCompleter = newOpCompleter(op.buf.w, op, width)
+ }
+
+ op.opSearch = cfg.opSearch
+ return old, nil
+}
+
+func (o *Operation) ResetHistory() {
+ o.history.Reset()
+}
+
+// if err is not nil, it just mean it fail to write to file
+// other things goes fine.
+func (o *Operation) SaveHistory(content string) error {
+ return o.history.New([]rune(content))
+}
+
+func (o *Operation) Refresh() {
+ if o.t.IsReading() {
+ o.buf.Refresh(nil)
+ }
+}
+
+func (o *Operation) Clean() {
+ o.buf.Clean()
+}
+
+func FuncListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) Listener {
+ return &DumpListener{f: f}
+}
+
+type DumpListener struct {
+ f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)
+}
+
+func (d *DumpListener) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
+ return d.f(line, pos, key)
+}
+
+type Listener interface {
+ OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)
+}
+
+type Painter interface {
+ Paint(line []rune, pos int) []rune
+}
+
+type defaultPainter struct{}
+
+func (p *defaultPainter) Paint(line []rune, _ int) []rune {
+ return line
+}
diff --git a/vendor/github.com/chzyer/readline/password.go b/vendor/github.com/chzyer/readline/password.go
new file mode 100644
index 000000000..414288c2a
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/password.go
@@ -0,0 +1,33 @@
+package readline
+
+type opPassword struct {
+ o *Operation
+ backupCfg *Config
+}
+
+func newOpPassword(o *Operation) *opPassword {
+ return &opPassword{o: o}
+}
+
+func (o *opPassword) ExitPasswordMode() {
+ o.o.SetConfig(o.backupCfg)
+ o.backupCfg = nil
+}
+
+func (o *opPassword) EnterPasswordMode(cfg *Config) (err error) {
+ o.backupCfg, err = o.o.SetConfig(cfg)
+ return
+}
+
+func (o *opPassword) PasswordConfig() *Config {
+ return &Config{
+ EnableMask: true,
+ InterruptPrompt: "\n",
+ EOFPrompt: "\n",
+ HistoryLimit: -1,
+ Painter: &defaultPainter{},
+
+ Stdout: o.o.cfg.Stdout,
+ Stderr: o.o.cfg.Stderr,
+ }
+}
diff --git a/vendor/github.com/chzyer/readline/rawreader_windows.go b/vendor/github.com/chzyer/readline/rawreader_windows.go
new file mode 100644
index 000000000..073ef150a
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/rawreader_windows.go
@@ -0,0 +1,125 @@
+// +build windows
+
+package readline
+
+import "unsafe"
+
+const (
+ VK_CANCEL = 0x03
+ VK_BACK = 0x08
+ VK_TAB = 0x09
+ VK_RETURN = 0x0D
+ VK_SHIFT = 0x10
+ VK_CONTROL = 0x11
+ VK_MENU = 0x12
+ VK_ESCAPE = 0x1B
+ VK_LEFT = 0x25
+ VK_UP = 0x26
+ VK_RIGHT = 0x27
+ VK_DOWN = 0x28
+ VK_DELETE = 0x2E
+ VK_LSHIFT = 0xA0
+ VK_RSHIFT = 0xA1
+ VK_LCONTROL = 0xA2
+ VK_RCONTROL = 0xA3
+)
+
+// RawReader translate input record to ANSI escape sequence.
+// To provides same behavior as unix terminal.
+type RawReader struct {
+ ctrlKey bool
+ altKey bool
+}
+
+func NewRawReader() *RawReader {
+ r := new(RawReader)
+ return r
+}
+
+// only process one action in one read
+func (r *RawReader) Read(buf []byte) (int, error) {
+ ir := new(_INPUT_RECORD)
+ var read int
+ var err error
+next:
+ err = kernel.ReadConsoleInputW(stdin,
+ uintptr(unsafe.Pointer(ir)),
+ 1,
+ uintptr(unsafe.Pointer(&read)),
+ )
+ if err != nil {
+ return 0, err
+ }
+ if ir.EventType != EVENT_KEY {
+ goto next
+ }
+ ker := (*_KEY_EVENT_RECORD)(unsafe.Pointer(&ir.Event[0]))
+ if ker.bKeyDown == 0 { // keyup
+ if r.ctrlKey || r.altKey {
+ switch ker.wVirtualKeyCode {
+ case VK_RCONTROL, VK_LCONTROL:
+ r.ctrlKey = false
+ case VK_MENU: //alt
+ r.altKey = false
+ }
+ }
+ goto next
+ }
+
+ if ker.unicodeChar == 0 {
+ var target rune
+ switch ker.wVirtualKeyCode {
+ case VK_RCONTROL, VK_LCONTROL:
+ r.ctrlKey = true
+ case VK_MENU: //alt
+ r.altKey = true
+ case VK_LEFT:
+ target = CharBackward
+ case VK_RIGHT:
+ target = CharForward
+ case VK_UP:
+ target = CharPrev
+ case VK_DOWN:
+ target = CharNext
+ }
+ if target != 0 {
+ return r.write(buf, target)
+ }
+ goto next
+ }
+ char := rune(ker.unicodeChar)
+ if r.ctrlKey {
+ switch char {
+ case 'A':
+ char = CharLineStart
+ case 'E':
+ char = CharLineEnd
+ case 'R':
+ char = CharBckSearch
+ case 'S':
+ char = CharFwdSearch
+ }
+ } else if r.altKey {
+ switch char {
+ case VK_BACK:
+ char = CharBackspace
+ }
+ return r.writeEsc(buf, char)
+ }
+ return r.write(buf, char)
+}
+
+func (r *RawReader) writeEsc(b []byte, char rune) (int, error) {
+ b[0] = '\033'
+ n := copy(b[1:], []byte(string(char)))
+ return n + 1, nil
+}
+
+func (r *RawReader) write(b []byte, char rune) (int, error) {
+ n := copy(b, []byte(string(char)))
+ return n, nil
+}
+
+func (r *RawReader) Close() error {
+ return nil
+}
diff --git a/vendor/github.com/chzyer/readline/readline.go b/vendor/github.com/chzyer/readline/readline.go
new file mode 100644
index 000000000..63b917101
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/readline.go
@@ -0,0 +1,338 @@
+// Readline is a pure go implementation for GNU-Readline kind library.
+//
+// example:
+// rl, err := readline.New("> ")
+// if err != nil {
+// panic(err)
+// }
+// defer rl.Close()
+//
+// for {
+// line, err := rl.Readline()
+// if err != nil { // io.EOF
+// break
+// }
+// println(line)
+// }
+//
+package readline
+
+import (
+ "io"
+)
+
+type Instance struct {
+ Config *Config
+ Terminal *Terminal
+ Operation *Operation
+}
+
+type Config struct {
+ // prompt supports ANSI escape sequence, so we can color some characters even in windows
+ Prompt string
+
+ // readline will persist historys to file where HistoryFile specified
+ HistoryFile string
+ // specify the max length of historys, it's 500 by default, set it to -1 to disable history
+ HistoryLimit int
+ DisableAutoSaveHistory bool
+ // enable case-insensitive history searching
+ HistorySearchFold bool
+
+ // AutoCompleter will called once user press TAB
+ AutoComplete AutoCompleter
+
+ // Any key press will pass to Listener
+ // NOTE: Listener will be triggered by (nil, 0, 0) immediately
+ Listener Listener
+
+ Painter Painter
+
+ // If VimMode is true, readline will in vim.insert mode by default
+ VimMode bool
+
+ InterruptPrompt string
+ EOFPrompt string
+
+ FuncGetWidth func() int
+
+ Stdin io.ReadCloser
+ StdinWriter io.Writer
+ Stdout io.Writer
+ Stderr io.Writer
+
+ EnableMask bool
+ MaskRune rune
+
+ // erase the editing line after user submited it
+ // it use in IM usually.
+ UniqueEditLine bool
+
+ // filter input runes (may be used to disable CtrlZ or for translating some keys to different actions)
+ // -> output = new (translated) rune and true/false if continue with processing this one
+ FuncFilterInputRune func(rune) (rune, bool)
+
+ // force use interactive even stdout is not a tty
+ FuncIsTerminal func() bool
+ FuncMakeRaw func() error
+ FuncExitRaw func() error
+ FuncOnWidthChanged func(func())
+ ForceUseInteractive bool
+
+ // private fields
+ inited bool
+ opHistory *opHistory
+ opSearch *opSearch
+}
+
+func (c *Config) useInteractive() bool {
+ if c.ForceUseInteractive {
+ return true
+ }
+ return c.FuncIsTerminal()
+}
+
+func (c *Config) Init() error {
+ if c.inited {
+ return nil
+ }
+ c.inited = true
+ if c.Stdin == nil {
+ c.Stdin = NewCancelableStdin(Stdin)
+ }
+
+ c.Stdin, c.StdinWriter = NewFillableStdin(c.Stdin)
+
+ if c.Stdout == nil {
+ c.Stdout = Stdout
+ }
+ if c.Stderr == nil {
+ c.Stderr = Stderr
+ }
+ if c.HistoryLimit == 0 {
+ c.HistoryLimit = 500
+ }
+
+ if c.InterruptPrompt == "" {
+ c.InterruptPrompt = "^C"
+ } else if c.InterruptPrompt == "\n" {
+ c.InterruptPrompt = ""
+ }
+ if c.EOFPrompt == "" {
+ c.EOFPrompt = "^D"
+ } else if c.EOFPrompt == "\n" {
+ c.EOFPrompt = ""
+ }
+
+ if c.AutoComplete == nil {
+ c.AutoComplete = &TabCompleter{}
+ }
+ if c.FuncGetWidth == nil {
+ c.FuncGetWidth = GetScreenWidth
+ }
+ if c.FuncIsTerminal == nil {
+ c.FuncIsTerminal = DefaultIsTerminal
+ }
+ rm := new(RawMode)
+ if c.FuncMakeRaw == nil {
+ c.FuncMakeRaw = rm.Enter
+ }
+ if c.FuncExitRaw == nil {
+ c.FuncExitRaw = rm.Exit
+ }
+ if c.FuncOnWidthChanged == nil {
+ c.FuncOnWidthChanged = DefaultOnWidthChanged
+ }
+
+ return nil
+}
+
+func (c Config) Clone() *Config {
+ c.opHistory = nil
+ c.opSearch = nil
+ return &c
+}
+
+func (c *Config) SetListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) {
+ c.Listener = FuncListener(f)
+}
+
+func (c *Config) SetPainter(p Painter) {
+ c.Painter = p
+}
+
+func NewEx(cfg *Config) (*Instance, error) {
+ t, err := NewTerminal(cfg)
+ if err != nil {
+ return nil, err
+ }
+ rl := t.Readline()
+ if cfg.Painter == nil {
+ cfg.Painter = &defaultPainter{}
+ }
+ return &Instance{
+ Config: cfg,
+ Terminal: t,
+ Operation: rl,
+ }, nil
+}
+
+func New(prompt string) (*Instance, error) {
+ return NewEx(&Config{Prompt: prompt})
+}
+
+func (i *Instance) ResetHistory() {
+ i.Operation.ResetHistory()
+}
+
+func (i *Instance) SetPrompt(s string) {
+ i.Operation.SetPrompt(s)
+}
+
+func (i *Instance) SetMaskRune(r rune) {
+ i.Operation.SetMaskRune(r)
+}
+
+// change history persistence in runtime
+func (i *Instance) SetHistoryPath(p string) {
+ i.Operation.SetHistoryPath(p)
+}
+
+// readline will refresh automatic when write through Stdout()
+func (i *Instance) Stdout() io.Writer {
+ return i.Operation.Stdout()
+}
+
+// readline will refresh automatic when write through Stdout()
+func (i *Instance) Stderr() io.Writer {
+ return i.Operation.Stderr()
+}
+
+// switch VimMode in runtime
+func (i *Instance) SetVimMode(on bool) {
+ i.Operation.SetVimMode(on)
+}
+
+func (i *Instance) IsVimMode() bool {
+ return i.Operation.IsEnableVimMode()
+}
+
+func (i *Instance) GenPasswordConfig() *Config {
+ return i.Operation.GenPasswordConfig()
+}
+
+// we can generate a config by `i.GenPasswordConfig()`
+func (i *Instance) ReadPasswordWithConfig(cfg *Config) ([]byte, error) {
+ return i.Operation.PasswordWithConfig(cfg)
+}
+
+func (i *Instance) ReadPasswordEx(prompt string, l Listener) ([]byte, error) {
+ return i.Operation.PasswordEx(prompt, l)
+}
+
+func (i *Instance) ReadPassword(prompt string) ([]byte, error) {
+ return i.Operation.Password(prompt)
+}
+
+type Result struct {
+ Line string
+ Error error
+}
+
+func (l *Result) CanContinue() bool {
+ return len(l.Line) != 0 && l.Error == ErrInterrupt
+}
+
+func (l *Result) CanBreak() bool {
+ return !l.CanContinue() && l.Error != nil
+}
+
+func (i *Instance) Line() *Result {
+ ret, err := i.Readline()
+ return &Result{ret, err}
+}
+
+// err is one of (nil, io.EOF, readline.ErrInterrupt)
+func (i *Instance) Readline() (string, error) {
+ return i.Operation.String()
+}
+
+func (i *Instance) ReadlineWithDefault(what string) (string, error) {
+ i.Operation.SetBuffer(what)
+ return i.Operation.String()
+}
+
+func (i *Instance) SaveHistory(content string) error {
+ return i.Operation.SaveHistory(content)
+}
+
+// same as readline
+func (i *Instance) ReadSlice() ([]byte, error) {
+ return i.Operation.Slice()
+}
+
+// we must make sure that call Close() before process exit.
+// if there has a pending reading operation, that reading will be interrupted.
+// so you can capture the signal and call Instance.Close(), it's thread-safe.
+func (i *Instance) Close() error {
+ i.Config.Stdin.Close()
+ i.Operation.Close()
+ if err := i.Terminal.Close(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// call CaptureExitSignal when you want readline exit gracefully.
+func (i *Instance) CaptureExitSignal() {
+ CaptureExitSignal(func() {
+ i.Close()
+ })
+}
+
+func (i *Instance) Clean() {
+ i.Operation.Clean()
+}
+
+func (i *Instance) Write(b []byte) (int, error) {
+ return i.Stdout().Write(b)
+}
+
+// WriteStdin prefill the next Stdin fetch
+// Next time you call ReadLine() this value will be writen before the user input
+// ie :
+// i := readline.New()
+// i.WriteStdin([]byte("test"))
+// _, _= i.Readline()
+//
+// gives
+//
+// > test[cursor]
+func (i *Instance) WriteStdin(val []byte) (int, error) {
+ return i.Terminal.WriteStdin(val)
+}
+
+func (i *Instance) SetConfig(cfg *Config) *Config {
+ if i.Config == cfg {
+ return cfg
+ }
+ old := i.Config
+ i.Config = cfg
+ i.Operation.SetConfig(cfg)
+ i.Terminal.SetConfig(cfg)
+ return old
+}
+
+func (i *Instance) Refresh() {
+ i.Operation.Refresh()
+}
+
+// HistoryDisable the save of the commands into the history
+func (i *Instance) HistoryDisable() {
+ i.Operation.history.Disable()
+}
+
+// HistoryEnable the save of the commands into the history (default on)
+func (i *Instance) HistoryEnable() {
+ i.Operation.history.Enable()
+}
diff --git a/vendor/github.com/chzyer/readline/remote.go b/vendor/github.com/chzyer/readline/remote.go
new file mode 100644
index 000000000..74dbf5690
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/remote.go
@@ -0,0 +1,475 @@
+package readline
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "sync"
+ "sync/atomic"
+)
+
+type MsgType int16
+
+const (
+ T_DATA = MsgType(iota)
+ T_WIDTH
+ T_WIDTH_REPORT
+ T_ISTTY_REPORT
+ T_RAW
+ T_ERAW // exit raw
+ T_EOF
+)
+
+type RemoteSvr struct {
+ eof int32
+ closed int32
+ width int32
+ reciveChan chan struct{}
+ writeChan chan *writeCtx
+ conn net.Conn
+ isTerminal bool
+ funcWidthChan func()
+ stopChan chan struct{}
+
+ dataBufM sync.Mutex
+ dataBuf bytes.Buffer
+}
+
+type writeReply struct {
+ n int
+ err error
+}
+
+type writeCtx struct {
+ msg *Message
+ reply chan *writeReply
+}
+
+func newWriteCtx(msg *Message) *writeCtx {
+ return &writeCtx{
+ msg: msg,
+ reply: make(chan *writeReply),
+ }
+}
+
+func NewRemoteSvr(conn net.Conn) (*RemoteSvr, error) {
+ rs := &RemoteSvr{
+ width: -1,
+ conn: conn,
+ writeChan: make(chan *writeCtx),
+ reciveChan: make(chan struct{}),
+ stopChan: make(chan struct{}),
+ }
+ buf := bufio.NewReader(rs.conn)
+
+ if err := rs.init(buf); err != nil {
+ return nil, err
+ }
+
+ go rs.readLoop(buf)
+ go rs.writeLoop()
+ return rs, nil
+}
+
+func (r *RemoteSvr) init(buf *bufio.Reader) error {
+ m, err := ReadMessage(buf)
+ if err != nil {
+ return err
+ }
+ // receive isTerminal
+ if m.Type != T_ISTTY_REPORT {
+ return fmt.Errorf("unexpected init message")
+ }
+ r.GotIsTerminal(m.Data)
+
+ // receive width
+ m, err = ReadMessage(buf)
+ if err != nil {
+ return err
+ }
+ if m.Type != T_WIDTH_REPORT {
+ return fmt.Errorf("unexpected init message")
+ }
+ r.GotReportWidth(m.Data)
+
+ return nil
+}
+
+func (r *RemoteSvr) HandleConfig(cfg *Config) {
+ cfg.Stderr = r
+ cfg.Stdout = r
+ cfg.Stdin = r
+ cfg.FuncExitRaw = r.ExitRawMode
+ cfg.FuncIsTerminal = r.IsTerminal
+ cfg.FuncMakeRaw = r.EnterRawMode
+ cfg.FuncExitRaw = r.ExitRawMode
+ cfg.FuncGetWidth = r.GetWidth
+ cfg.FuncOnWidthChanged = func(f func()) {
+ r.funcWidthChan = f
+ }
+}
+
+func (r *RemoteSvr) IsTerminal() bool {
+ return r.isTerminal
+}
+
+func (r *RemoteSvr) checkEOF() error {
+ if atomic.LoadInt32(&r.eof) == 1 {
+ return io.EOF
+ }
+ return nil
+}
+
+func (r *RemoteSvr) Read(b []byte) (int, error) {
+ r.dataBufM.Lock()
+ n, err := r.dataBuf.Read(b)
+ r.dataBufM.Unlock()
+ if n == 0 {
+ if err := r.checkEOF(); err != nil {
+ return 0, err
+ }
+ }
+
+ if n == 0 && err == io.EOF {
+ <-r.reciveChan
+ r.dataBufM.Lock()
+ n, err = r.dataBuf.Read(b)
+ r.dataBufM.Unlock()
+ }
+ if n == 0 {
+ if err := r.checkEOF(); err != nil {
+ return 0, err
+ }
+ }
+
+ return n, err
+}
+
+func (r *RemoteSvr) writeMsg(m *Message) error {
+ ctx := newWriteCtx(m)
+ r.writeChan <- ctx
+ reply := <-ctx.reply
+ return reply.err
+}
+
+func (r *RemoteSvr) Write(b []byte) (int, error) {
+ ctx := newWriteCtx(NewMessage(T_DATA, b))
+ r.writeChan <- ctx
+ reply := <-ctx.reply
+ return reply.n, reply.err
+}
+
+func (r *RemoteSvr) EnterRawMode() error {
+ return r.writeMsg(NewMessage(T_RAW, nil))
+}
+
+func (r *RemoteSvr) ExitRawMode() error {
+ return r.writeMsg(NewMessage(T_ERAW, nil))
+}
+
+func (r *RemoteSvr) writeLoop() {
+ defer r.Close()
+
+loop:
+ for {
+ select {
+ case ctx, ok := <-r.writeChan:
+ if !ok {
+ break
+ }
+ n, err := ctx.msg.WriteTo(r.conn)
+ ctx.reply <- &writeReply{n, err}
+ case <-r.stopChan:
+ break loop
+ }
+ }
+}
+
+func (r *RemoteSvr) Close() error {
+ if atomic.CompareAndSwapInt32(&r.closed, 0, 1) {
+ close(r.stopChan)
+ r.conn.Close()
+ }
+ return nil
+}
+
+func (r *RemoteSvr) readLoop(buf *bufio.Reader) {
+ defer r.Close()
+ for {
+ m, err := ReadMessage(buf)
+ if err != nil {
+ break
+ }
+ switch m.Type {
+ case T_EOF:
+ atomic.StoreInt32(&r.eof, 1)
+ select {
+ case r.reciveChan <- struct{}{}:
+ default:
+ }
+ case T_DATA:
+ r.dataBufM.Lock()
+ r.dataBuf.Write(m.Data)
+ r.dataBufM.Unlock()
+ select {
+ case r.reciveChan <- struct{}{}:
+ default:
+ }
+ case T_WIDTH_REPORT:
+ r.GotReportWidth(m.Data)
+ case T_ISTTY_REPORT:
+ r.GotIsTerminal(m.Data)
+ }
+ }
+}
+
+func (r *RemoteSvr) GotIsTerminal(data []byte) {
+ if binary.BigEndian.Uint16(data) == 0 {
+ r.isTerminal = false
+ } else {
+ r.isTerminal = true
+ }
+}
+
+func (r *RemoteSvr) GotReportWidth(data []byte) {
+ atomic.StoreInt32(&r.width, int32(binary.BigEndian.Uint16(data)))
+ if r.funcWidthChan != nil {
+ r.funcWidthChan()
+ }
+}
+
+func (r *RemoteSvr) GetWidth() int {
+ return int(atomic.LoadInt32(&r.width))
+}
+
+// -----------------------------------------------------------------------------
+
+type Message struct {
+ Type MsgType
+ Data []byte
+}
+
+func ReadMessage(r io.Reader) (*Message, error) {
+ m := new(Message)
+ var length int32
+ if err := binary.Read(r, binary.BigEndian, &length); err != nil {
+ return nil, err
+ }
+ if err := binary.Read(r, binary.BigEndian, &m.Type); err != nil {
+ return nil, err
+ }
+ m.Data = make([]byte, int(length)-2)
+ if _, err := io.ReadFull(r, m.Data); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func NewMessage(t MsgType, data []byte) *Message {
+ return &Message{t, data}
+}
+
+func (m *Message) WriteTo(w io.Writer) (int, error) {
+ buf := bytes.NewBuffer(make([]byte, 0, len(m.Data)+2+4))
+ binary.Write(buf, binary.BigEndian, int32(len(m.Data)+2))
+ binary.Write(buf, binary.BigEndian, m.Type)
+ buf.Write(m.Data)
+ n, err := buf.WriteTo(w)
+ return int(n), err
+}
+
+// -----------------------------------------------------------------------------
+
+type RemoteCli struct {
+ conn net.Conn
+ raw RawMode
+ receiveChan chan struct{}
+ inited int32
+ isTerminal *bool
+
+ data bytes.Buffer
+ dataM sync.Mutex
+}
+
+func NewRemoteCli(conn net.Conn) (*RemoteCli, error) {
+ r := &RemoteCli{
+ conn: conn,
+ receiveChan: make(chan struct{}),
+ }
+ return r, nil
+}
+
+func (r *RemoteCli) MarkIsTerminal(is bool) {
+ r.isTerminal = &is
+}
+
+func (r *RemoteCli) init() error {
+ if !atomic.CompareAndSwapInt32(&r.inited, 0, 1) {
+ return nil
+ }
+
+ if err := r.reportIsTerminal(); err != nil {
+ return err
+ }
+
+ if err := r.reportWidth(); err != nil {
+ return err
+ }
+
+ // register sig for width changed
+ DefaultOnWidthChanged(func() {
+ r.reportWidth()
+ })
+ return nil
+}
+
+func (r *RemoteCli) writeMsg(m *Message) error {
+ r.dataM.Lock()
+ _, err := m.WriteTo(r.conn)
+ r.dataM.Unlock()
+ return err
+}
+
+func (r *RemoteCli) Write(b []byte) (int, error) {
+ m := NewMessage(T_DATA, b)
+ r.dataM.Lock()
+ _, err := m.WriteTo(r.conn)
+ r.dataM.Unlock()
+ return len(b), err
+}
+
+func (r *RemoteCli) reportWidth() error {
+ screenWidth := GetScreenWidth()
+ data := make([]byte, 2)
+ binary.BigEndian.PutUint16(data, uint16(screenWidth))
+ msg := NewMessage(T_WIDTH_REPORT, data)
+
+ if err := r.writeMsg(msg); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (r *RemoteCli) reportIsTerminal() error {
+ var isTerminal bool
+ if r.isTerminal != nil {
+ isTerminal = *r.isTerminal
+ } else {
+ isTerminal = DefaultIsTerminal()
+ }
+ data := make([]byte, 2)
+ if isTerminal {
+ binary.BigEndian.PutUint16(data, 1)
+ } else {
+ binary.BigEndian.PutUint16(data, 0)
+ }
+ msg := NewMessage(T_ISTTY_REPORT, data)
+ if err := r.writeMsg(msg); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (r *RemoteCli) readLoop() {
+ buf := bufio.NewReader(r.conn)
+ for {
+ msg, err := ReadMessage(buf)
+ if err != nil {
+ break
+ }
+ switch msg.Type {
+ case T_ERAW:
+ r.raw.Exit()
+ case T_RAW:
+ r.raw.Enter()
+ case T_DATA:
+ os.Stdout.Write(msg.Data)
+ }
+ }
+}
+
+func (r *RemoteCli) ServeBy(source io.Reader) error {
+ if err := r.init(); err != nil {
+ return err
+ }
+
+ go func() {
+ defer r.Close()
+ for {
+ n, _ := io.Copy(r, source)
+ if n == 0 {
+ break
+ }
+ }
+ }()
+ defer r.raw.Exit()
+ r.readLoop()
+ return nil
+}
+
+func (r *RemoteCli) Close() {
+ r.writeMsg(NewMessage(T_EOF, nil))
+}
+
+func (r *RemoteCli) Serve() error {
+ return r.ServeBy(os.Stdin)
+}
+
+func ListenRemote(n, addr string, cfg *Config, h func(*Instance), onListen ...func(net.Listener) error) error {
+ ln, err := net.Listen(n, addr)
+ if err != nil {
+ return err
+ }
+ if len(onListen) > 0 {
+ if err := onListen[0](ln); err != nil {
+ return err
+ }
+ }
+ for {
+ conn, err := ln.Accept()
+ if err != nil {
+ break
+ }
+ go func() {
+ defer conn.Close()
+ rl, err := HandleConn(*cfg, conn)
+ if err != nil {
+ return
+ }
+ h(rl)
+ }()
+ }
+ return nil
+}
+
+func HandleConn(cfg Config, conn net.Conn) (*Instance, error) {
+ r, err := NewRemoteSvr(conn)
+ if err != nil {
+ return nil, err
+ }
+ r.HandleConfig(&cfg)
+
+ rl, err := NewEx(&cfg)
+ if err != nil {
+ return nil, err
+ }
+ return rl, nil
+}
+
+func DialRemote(n, addr string) error {
+ conn, err := net.Dial(n, addr)
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ cli, err := NewRemoteCli(conn)
+ if err != nil {
+ return err
+ }
+ return cli.Serve()
+}
diff --git a/vendor/github.com/chzyer/readline/runebuf.go b/vendor/github.com/chzyer/readline/runebuf.go
new file mode 100644
index 000000000..d95df1e36
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/runebuf.go
@@ -0,0 +1,629 @@
+package readline
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+ "strconv"
+ "strings"
+ "sync"
+)
+
+type runeBufferBck struct {
+ buf []rune
+ idx int
+}
+
+type RuneBuffer struct {
+ buf []rune
+ idx int
+ prompt []rune
+ w io.Writer
+
+ hadClean bool
+ interactive bool
+ cfg *Config
+
+ width int
+
+ bck *runeBufferBck
+
+ offset string
+
+ lastKill []rune
+
+ sync.Mutex
+}
+
+func (r *RuneBuffer) pushKill(text []rune) {
+ r.lastKill = append([]rune{}, text...)
+}
+
+func (r *RuneBuffer) OnWidthChange(newWidth int) {
+ r.Lock()
+ r.width = newWidth
+ r.Unlock()
+}
+
+func (r *RuneBuffer) Backup() {
+ r.Lock()
+ r.bck = &runeBufferBck{r.buf, r.idx}
+ r.Unlock()
+}
+
+func (r *RuneBuffer) Restore() {
+ r.Refresh(func() {
+ if r.bck == nil {
+ return
+ }
+ r.buf = r.bck.buf
+ r.idx = r.bck.idx
+ })
+}
+
+func NewRuneBuffer(w io.Writer, prompt string, cfg *Config, width int) *RuneBuffer {
+ rb := &RuneBuffer{
+ w: w,
+ interactive: cfg.useInteractive(),
+ cfg: cfg,
+ width: width,
+ }
+ rb.SetPrompt(prompt)
+ return rb
+}
+
+func (r *RuneBuffer) SetConfig(cfg *Config) {
+ r.Lock()
+ r.cfg = cfg
+ r.interactive = cfg.useInteractive()
+ r.Unlock()
+}
+
+func (r *RuneBuffer) SetMask(m rune) {
+ r.Lock()
+ r.cfg.MaskRune = m
+ r.Unlock()
+}
+
+func (r *RuneBuffer) CurrentWidth(x int) int {
+ r.Lock()
+ defer r.Unlock()
+ return runes.WidthAll(r.buf[:x])
+}
+
+func (r *RuneBuffer) PromptLen() int {
+ r.Lock()
+ width := r.promptLen()
+ r.Unlock()
+ return width
+}
+
+func (r *RuneBuffer) promptLen() int {
+ return runes.WidthAll(runes.ColorFilter(r.prompt))
+}
+
+func (r *RuneBuffer) RuneSlice(i int) []rune {
+ r.Lock()
+ defer r.Unlock()
+
+ if i > 0 {
+ rs := make([]rune, i)
+ copy(rs, r.buf[r.idx:r.idx+i])
+ return rs
+ }
+ rs := make([]rune, -i)
+ copy(rs, r.buf[r.idx+i:r.idx])
+ return rs
+}
+
+func (r *RuneBuffer) Runes() []rune {
+ r.Lock()
+ newr := make([]rune, len(r.buf))
+ copy(newr, r.buf)
+ r.Unlock()
+ return newr
+}
+
+func (r *RuneBuffer) Pos() int {
+ r.Lock()
+ defer r.Unlock()
+ return r.idx
+}
+
+func (r *RuneBuffer) Len() int {
+ r.Lock()
+ defer r.Unlock()
+ return len(r.buf)
+}
+
+func (r *RuneBuffer) MoveToLineStart() {
+ r.Refresh(func() {
+ if r.idx == 0 {
+ return
+ }
+ r.idx = 0
+ })
+}
+
+func (r *RuneBuffer) MoveBackward() {
+ r.Refresh(func() {
+ if r.idx == 0 {
+ return
+ }
+ r.idx--
+ })
+}
+
+func (r *RuneBuffer) WriteString(s string) {
+ r.WriteRunes([]rune(s))
+}
+
+func (r *RuneBuffer) WriteRune(s rune) {
+ r.WriteRunes([]rune{s})
+}
+
+func (r *RuneBuffer) WriteRunes(s []rune) {
+ r.Refresh(func() {
+ tail := append(s, r.buf[r.idx:]...)
+ r.buf = append(r.buf[:r.idx], tail...)
+ r.idx += len(s)
+ })
+}
+
+func (r *RuneBuffer) MoveForward() {
+ r.Refresh(func() {
+ if r.idx == len(r.buf) {
+ return
+ }
+ r.idx++
+ })
+}
+
+func (r *RuneBuffer) IsCursorInEnd() bool {
+ r.Lock()
+ defer r.Unlock()
+ return r.idx == len(r.buf)
+}
+
+func (r *RuneBuffer) Replace(ch rune) {
+ r.Refresh(func() {
+ r.buf[r.idx] = ch
+ })
+}
+
+func (r *RuneBuffer) Erase() {
+ r.Refresh(func() {
+ r.idx = 0
+ r.pushKill(r.buf[:])
+ r.buf = r.buf[:0]
+ })
+}
+
+func (r *RuneBuffer) Delete() (success bool) {
+ r.Refresh(func() {
+ if r.idx == len(r.buf) {
+ return
+ }
+ r.pushKill(r.buf[r.idx : r.idx+1])
+ r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
+ success = true
+ })
+ return
+}
+
+func (r *RuneBuffer) DeleteWord() {
+ if r.idx == len(r.buf) {
+ return
+ }
+ init := r.idx
+ for init < len(r.buf) && IsWordBreak(r.buf[init]) {
+ init++
+ }
+ for i := init + 1; i < len(r.buf); i++ {
+ if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
+ r.pushKill(r.buf[r.idx : i-1])
+ r.Refresh(func() {
+ r.buf = append(r.buf[:r.idx], r.buf[i-1:]...)
+ })
+ return
+ }
+ }
+ r.Kill()
+}
+
+func (r *RuneBuffer) MoveToPrevWord() (success bool) {
+ r.Refresh(func() {
+ if r.idx == 0 {
+ return
+ }
+
+ for i := r.idx - 1; i > 0; i-- {
+ if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
+ r.idx = i
+ success = true
+ return
+ }
+ }
+ r.idx = 0
+ success = true
+ })
+ return
+}
+
+func (r *RuneBuffer) KillFront() {
+ r.Refresh(func() {
+ if r.idx == 0 {
+ return
+ }
+
+ length := len(r.buf) - r.idx
+ r.pushKill(r.buf[:r.idx])
+ copy(r.buf[:length], r.buf[r.idx:])
+ r.idx = 0
+ r.buf = r.buf[:length]
+ })
+}
+
+func (r *RuneBuffer) Kill() {
+ r.Refresh(func() {
+ r.pushKill(r.buf[r.idx:])
+ r.buf = r.buf[:r.idx]
+ })
+}
+
+func (r *RuneBuffer) Transpose() {
+ r.Refresh(func() {
+ if len(r.buf) == 1 {
+ r.idx++
+ }
+
+ if len(r.buf) < 2 {
+ return
+ }
+
+ if r.idx == 0 {
+ r.idx = 1
+ } else if r.idx >= len(r.buf) {
+ r.idx = len(r.buf) - 1
+ }
+ r.buf[r.idx], r.buf[r.idx-1] = r.buf[r.idx-1], r.buf[r.idx]
+ r.idx++
+ })
+}
+
+func (r *RuneBuffer) MoveToNextWord() {
+ r.Refresh(func() {
+ for i := r.idx + 1; i < len(r.buf); i++ {
+ if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
+ r.idx = i
+ return
+ }
+ }
+
+ r.idx = len(r.buf)
+ })
+}
+
+func (r *RuneBuffer) MoveToEndWord() {
+ r.Refresh(func() {
+ // already at the end, so do nothing
+ if r.idx == len(r.buf) {
+ return
+ }
+ // if we are at the end of a word already, go to next
+ if !IsWordBreak(r.buf[r.idx]) && IsWordBreak(r.buf[r.idx+1]) {
+ r.idx++
+ }
+
+ // keep going until at the end of a word
+ for i := r.idx + 1; i < len(r.buf); i++ {
+ if IsWordBreak(r.buf[i]) && !IsWordBreak(r.buf[i-1]) {
+ r.idx = i - 1
+ return
+ }
+ }
+ r.idx = len(r.buf)
+ })
+}
+
+func (r *RuneBuffer) BackEscapeWord() {
+ r.Refresh(func() {
+ if r.idx == 0 {
+ return
+ }
+ for i := r.idx - 1; i > 0; i-- {
+ if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
+ r.pushKill(r.buf[i:r.idx])
+ r.buf = append(r.buf[:i], r.buf[r.idx:]...)
+ r.idx = i
+ return
+ }
+ }
+
+ r.buf = r.buf[:0]
+ r.idx = 0
+ })
+}
+
+func (r *RuneBuffer) Yank() {
+ if len(r.lastKill) == 0 {
+ return
+ }
+ r.Refresh(func() {
+ buf := make([]rune, 0, len(r.buf)+len(r.lastKill))
+ buf = append(buf, r.buf[:r.idx]...)
+ buf = append(buf, r.lastKill...)
+ buf = append(buf, r.buf[r.idx:]...)
+ r.buf = buf
+ r.idx += len(r.lastKill)
+ })
+}
+
+func (r *RuneBuffer) Backspace() {
+ r.Refresh(func() {
+ if r.idx == 0 {
+ return
+ }
+
+ r.idx--
+ r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
+ })
+}
+
+func (r *RuneBuffer) MoveToLineEnd() {
+ r.Refresh(func() {
+ if r.idx == len(r.buf) {
+ return
+ }
+
+ r.idx = len(r.buf)
+ })
+}
+
+func (r *RuneBuffer) LineCount(width int) int {
+ if width == -1 {
+ width = r.width
+ }
+ return LineCount(width,
+ runes.WidthAll(r.buf)+r.PromptLen())
+}
+
+func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) {
+ r.Refresh(func() {
+ if reverse {
+ for i := r.idx - 1; i >= 0; i-- {
+ if r.buf[i] == ch {
+ r.idx = i
+ if prevChar {
+ r.idx++
+ }
+ success = true
+ return
+ }
+ }
+ return
+ }
+ for i := r.idx + 1; i < len(r.buf); i++ {
+ if r.buf[i] == ch {
+ r.idx = i
+ if prevChar {
+ r.idx--
+ }
+ success = true
+ return
+ }
+ }
+ })
+ return
+}
+
+func (r *RuneBuffer) isInLineEdge() bool {
+ if isWindows {
+ return false
+ }
+ sp := r.getSplitByLine(r.buf)
+ return len(sp[len(sp)-1]) == 0
+}
+
+func (r *RuneBuffer) getSplitByLine(rs []rune) []string {
+ return SplitByLine(r.promptLen(), r.width, rs)
+}
+
+func (r *RuneBuffer) IdxLine(width int) int {
+ r.Lock()
+ defer r.Unlock()
+ return r.idxLine(width)
+}
+
+func (r *RuneBuffer) idxLine(width int) int {
+ if width == 0 {
+ return 0
+ }
+ sp := r.getSplitByLine(r.buf[:r.idx])
+ return len(sp) - 1
+}
+
+func (r *RuneBuffer) CursorLineCount() int {
+ return r.LineCount(r.width) - r.IdxLine(r.width)
+}
+
+func (r *RuneBuffer) Refresh(f func()) {
+ r.Lock()
+ defer r.Unlock()
+
+ if !r.interactive {
+ if f != nil {
+ f()
+ }
+ return
+ }
+
+ r.clean()
+ if f != nil {
+ f()
+ }
+ r.print()
+}
+
+func (r *RuneBuffer) SetOffset(offset string) {
+ r.Lock()
+ r.offset = offset
+ r.Unlock()
+}
+
+func (r *RuneBuffer) print() {
+ r.w.Write(r.output())
+ r.hadClean = false
+}
+
+func (r *RuneBuffer) output() []byte {
+ buf := bytes.NewBuffer(nil)
+ buf.WriteString(string(r.prompt))
+ if r.cfg.EnableMask && len(r.buf) > 0 {
+ buf.Write([]byte(strings.Repeat(string(r.cfg.MaskRune), len(r.buf)-1)))
+ if r.buf[len(r.buf)-1] == '\n' {
+ buf.Write([]byte{'\n'})
+ } else {
+ buf.Write([]byte(string(r.cfg.MaskRune)))
+ }
+ if len(r.buf) > r.idx {
+ buf.Write(r.getBackspaceSequence())
+ }
+
+ } else {
+ for _, e := range r.cfg.Painter.Paint(r.buf, r.idx) {
+ if e == '\t' {
+ buf.WriteString(strings.Repeat(" ", TabWidth))
+ } else {
+ buf.WriteRune(e)
+ }
+ }
+ if r.isInLineEdge() {
+ buf.Write([]byte(" \b"))
+ }
+ }
+ // cursor position
+ if len(r.buf) > r.idx {
+ buf.Write(r.getBackspaceSequence())
+ }
+ return buf.Bytes()
+}
+
+func (r *RuneBuffer) getBackspaceSequence() []byte {
+ var sep = map[int]bool{}
+
+ var i int
+ for {
+ if i >= runes.WidthAll(r.buf) {
+ break
+ }
+
+ if i == 0 {
+ i -= r.promptLen()
+ }
+ i += r.width
+
+ sep[i] = true
+ }
+ var buf []byte
+ for i := len(r.buf); i > r.idx; i-- {
+ // move input to the left of one
+ buf = append(buf, '\b')
+ if sep[i] {
+ // up one line, go to the start of the line and move cursor right to the end (r.width)
+ buf = append(buf, "\033[A\r"+"\033["+strconv.Itoa(r.width)+"C"...)
+ }
+ }
+
+ return buf
+
+}
+
+func (r *RuneBuffer) Reset() []rune {
+ ret := runes.Copy(r.buf)
+ r.buf = r.buf[:0]
+ r.idx = 0
+ return ret
+}
+
+func (r *RuneBuffer) calWidth(m int) int {
+ if m > 0 {
+ return runes.WidthAll(r.buf[r.idx : r.idx+m])
+ }
+ return runes.WidthAll(r.buf[r.idx+m : r.idx])
+}
+
+func (r *RuneBuffer) SetStyle(start, end int, style string) {
+ if end < start {
+ panic("end < start")
+ }
+
+ // goto start
+ move := start - r.idx
+ if move > 0 {
+ r.w.Write([]byte(string(r.buf[r.idx : r.idx+move])))
+ } else {
+ r.w.Write(bytes.Repeat([]byte("\b"), r.calWidth(move)))
+ }
+ r.w.Write([]byte("\033[" + style + "m"))
+ r.w.Write([]byte(string(r.buf[start:end])))
+ r.w.Write([]byte("\033[0m"))
+ // TODO: move back
+}
+
+func (r *RuneBuffer) SetWithIdx(idx int, buf []rune) {
+ r.Refresh(func() {
+ r.buf = buf
+ r.idx = idx
+ })
+}
+
+func (r *RuneBuffer) Set(buf []rune) {
+ r.SetWithIdx(len(buf), buf)
+}
+
+func (r *RuneBuffer) SetPrompt(prompt string) {
+ r.Lock()
+ r.prompt = []rune(prompt)
+ r.Unlock()
+}
+
+func (r *RuneBuffer) cleanOutput(w io.Writer, idxLine int) {
+ buf := bufio.NewWriter(w)
+
+ if r.width == 0 {
+ buf.WriteString(strings.Repeat("\r\b", len(r.buf)+r.promptLen()))
+ buf.Write([]byte("\033[J"))
+ } else {
+ buf.Write([]byte("\033[J")) // just like ^k :)
+ if idxLine == 0 {
+ buf.WriteString("\033[2K")
+ buf.WriteString("\r")
+ } else {
+ for i := 0; i < idxLine; i++ {
+ io.WriteString(buf, "\033[2K\r\033[A")
+ }
+ io.WriteString(buf, "\033[2K\r")
+ }
+ }
+ buf.Flush()
+ return
+}
+
+func (r *RuneBuffer) Clean() {
+ r.Lock()
+ r.clean()
+ r.Unlock()
+}
+
+func (r *RuneBuffer) clean() {
+ r.cleanWithIdxLine(r.idxLine(r.width))
+}
+
+func (r *RuneBuffer) cleanWithIdxLine(idxLine int) {
+ if r.hadClean || !r.interactive {
+ return
+ }
+ r.hadClean = true
+ r.cleanOutput(r.w, idxLine)
+}
diff --git a/vendor/github.com/chzyer/readline/runes.go b/vendor/github.com/chzyer/readline/runes.go
new file mode 100644
index 000000000..a669bc48c
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/runes.go
@@ -0,0 +1,223 @@
+package readline
+
+import (
+ "bytes"
+ "unicode"
+ "unicode/utf8"
+)
+
+var runes = Runes{}
+var TabWidth = 4
+
+type Runes struct{}
+
+func (Runes) EqualRune(a, b rune, fold bool) bool {
+ if a == b {
+ return true
+ }
+ if !fold {
+ return false
+ }
+ if a > b {
+ a, b = b, a
+ }
+ if b < utf8.RuneSelf && 'A' <= a && a <= 'Z' {
+ if b == a+'a'-'A' {
+ return true
+ }
+ }
+ return false
+}
+
+func (r Runes) EqualRuneFold(a, b rune) bool {
+ return r.EqualRune(a, b, true)
+}
+
+func (r Runes) EqualFold(a, b []rune) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := 0; i < len(a); i++ {
+ if r.EqualRuneFold(a[i], b[i]) {
+ continue
+ }
+ return false
+ }
+
+ return true
+}
+
+func (Runes) Equal(a, b []rune) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := 0; i < len(a); i++ {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+ return true
+}
+
+func (rs Runes) IndexAllBckEx(r, sub []rune, fold bool) int {
+ for i := len(r) - len(sub); i >= 0; i-- {
+ found := true
+ for j := 0; j < len(sub); j++ {
+ if !rs.EqualRune(r[i+j], sub[j], fold) {
+ found = false
+ break
+ }
+ }
+ if found {
+ return i
+ }
+ }
+ return -1
+}
+
+// Search in runes from end to front
+func (rs Runes) IndexAllBck(r, sub []rune) int {
+ return rs.IndexAllBckEx(r, sub, false)
+}
+
+// Search in runes from front to end
+func (rs Runes) IndexAll(r, sub []rune) int {
+ return rs.IndexAllEx(r, sub, false)
+}
+
+func (rs Runes) IndexAllEx(r, sub []rune, fold bool) int {
+ for i := 0; i < len(r); i++ {
+ found := true
+ if len(r[i:]) < len(sub) {
+ return -1
+ }
+ for j := 0; j < len(sub); j++ {
+ if !rs.EqualRune(r[i+j], sub[j], fold) {
+ found = false
+ break
+ }
+ }
+ if found {
+ return i
+ }
+ }
+ return -1
+}
+
+func (Runes) Index(r rune, rs []rune) int {
+ for i := 0; i < len(rs); i++ {
+ if rs[i] == r {
+ return i
+ }
+ }
+ return -1
+}
+
+func (Runes) ColorFilter(r []rune) []rune {
+ newr := make([]rune, 0, len(r))
+ for pos := 0; pos < len(r); pos++ {
+ if r[pos] == '\033' && r[pos+1] == '[' {
+ idx := runes.Index('m', r[pos+2:])
+ if idx == -1 {
+ continue
+ }
+ pos += idx + 2
+ continue
+ }
+ newr = append(newr, r[pos])
+ }
+ return newr
+}
+
+var zeroWidth = []*unicode.RangeTable{
+ unicode.Mn,
+ unicode.Me,
+ unicode.Cc,
+ unicode.Cf,
+}
+
+var doubleWidth = []*unicode.RangeTable{
+ unicode.Han,
+ unicode.Hangul,
+ unicode.Hiragana,
+ unicode.Katakana,
+}
+
+func (Runes) Width(r rune) int {
+ if r == '\t' {
+ return TabWidth
+ }
+ if unicode.IsOneOf(zeroWidth, r) {
+ return 0
+ }
+ if unicode.IsOneOf(doubleWidth, r) {
+ return 2
+ }
+ return 1
+}
+
+func (Runes) WidthAll(r []rune) (length int) {
+ for i := 0; i < len(r); i++ {
+ length += runes.Width(r[i])
+ }
+ return
+}
+
+func (Runes) Backspace(r []rune) []byte {
+ return bytes.Repeat([]byte{'\b'}, runes.WidthAll(r))
+}
+
+func (Runes) Copy(r []rune) []rune {
+ n := make([]rune, len(r))
+ copy(n, r)
+ return n
+}
+
+func (Runes) HasPrefixFold(r, prefix []rune) bool {
+ if len(r) < len(prefix) {
+ return false
+ }
+ return runes.EqualFold(r[:len(prefix)], prefix)
+}
+
+func (Runes) HasPrefix(r, prefix []rune) bool {
+ if len(r) < len(prefix) {
+ return false
+ }
+ return runes.Equal(r[:len(prefix)], prefix)
+}
+
+func (Runes) Aggregate(candicate [][]rune) (same []rune, size int) {
+ for i := 0; i < len(candicate[0]); i++ {
+ for j := 0; j < len(candicate)-1; j++ {
+ if i >= len(candicate[j]) || i >= len(candicate[j+1]) {
+ goto aggregate
+ }
+ if candicate[j][i] != candicate[j+1][i] {
+ goto aggregate
+ }
+ }
+ size = i + 1
+ }
+aggregate:
+ if size > 0 {
+ same = runes.Copy(candicate[0][:size])
+ for i := 0; i < len(candicate); i++ {
+ n := runes.Copy(candicate[i])
+ copy(n, n[size:])
+ candicate[i] = n[:len(n)-size]
+ }
+ }
+ return
+}
+
+func (Runes) TrimSpaceLeft(in []rune) []rune {
+ firstIndex := len(in)
+ for i, r := range in {
+ if unicode.IsSpace(r) == false {
+ firstIndex = i
+ break
+ }
+ }
+ return in[firstIndex:]
+}
diff --git a/vendor/github.com/chzyer/readline/search.go b/vendor/github.com/chzyer/readline/search.go
new file mode 100644
index 000000000..52e8ff099
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/search.go
@@ -0,0 +1,164 @@
+package readline
+
+import (
+ "bytes"
+ "container/list"
+ "fmt"
+ "io"
+)
+
+const (
+ S_STATE_FOUND = iota
+ S_STATE_FAILING
+)
+
+const (
+ S_DIR_BCK = iota
+ S_DIR_FWD
+)
+
+type opSearch struct {
+ inMode bool
+ state int
+ dir int
+ source *list.Element
+ w io.Writer
+ buf *RuneBuffer
+ data []rune
+ history *opHistory
+ cfg *Config
+ markStart int
+ markEnd int
+ width int
+}
+
+func newOpSearch(w io.Writer, buf *RuneBuffer, history *opHistory, cfg *Config, width int) *opSearch {
+ return &opSearch{
+ w: w,
+ buf: buf,
+ cfg: cfg,
+ history: history,
+ width: width,
+ }
+}
+
+func (o *opSearch) OnWidthChange(newWidth int) {
+ o.width = newWidth
+}
+
+func (o *opSearch) IsSearchMode() bool {
+ return o.inMode
+}
+
+func (o *opSearch) SearchBackspace() {
+ if len(o.data) > 0 {
+ o.data = o.data[:len(o.data)-1]
+ o.search(true)
+ }
+}
+
+func (o *opSearch) findHistoryBy(isNewSearch bool) (int, *list.Element) {
+ if o.dir == S_DIR_BCK {
+ return o.history.FindBck(isNewSearch, o.data, o.buf.idx)
+ }
+ return o.history.FindFwd(isNewSearch, o.data, o.buf.idx)
+}
+
+func (o *opSearch) search(isChange bool) bool {
+ if len(o.data) == 0 {
+ o.state = S_STATE_FOUND
+ o.SearchRefresh(-1)
+ return true
+ }
+ idx, elem := o.findHistoryBy(isChange)
+ if elem == nil {
+ o.SearchRefresh(-2)
+ return false
+ }
+ o.history.current = elem
+
+ item := o.history.showItem(o.history.current.Value)
+ start, end := 0, 0
+ if o.dir == S_DIR_BCK {
+ start, end = idx, idx+len(o.data)
+ } else {
+ start, end = idx, idx+len(o.data)
+ idx += len(o.data)
+ }
+ o.buf.SetWithIdx(idx, item)
+ o.markStart, o.markEnd = start, end
+ o.SearchRefresh(idx)
+ return true
+}
+
+func (o *opSearch) SearchChar(r rune) {
+ o.data = append(o.data, r)
+ o.search(true)
+}
+
+func (o *opSearch) SearchMode(dir int) bool {
+ if o.width == 0 {
+ return false
+ }
+ alreadyInMode := o.inMode
+ o.inMode = true
+ o.dir = dir
+ o.source = o.history.current
+ if alreadyInMode {
+ o.search(false)
+ } else {
+ o.SearchRefresh(-1)
+ }
+ return true
+}
+
+func (o *opSearch) ExitSearchMode(revert bool) {
+ if revert {
+ o.history.current = o.source
+ o.buf.Set(o.history.showItem(o.history.current.Value))
+ }
+ o.markStart, o.markEnd = 0, 0
+ o.state = S_STATE_FOUND
+ o.inMode = false
+ o.source = nil
+ o.data = nil
+}
+
+func (o *opSearch) SearchRefresh(x int) {
+ if x == -2 {
+ o.state = S_STATE_FAILING
+ } else if x >= 0 {
+ o.state = S_STATE_FOUND
+ }
+ if x < 0 {
+ x = o.buf.idx
+ }
+ x = o.buf.CurrentWidth(x)
+ x += o.buf.PromptLen()
+ x = x % o.width
+
+ if o.markStart > 0 {
+ o.buf.SetStyle(o.markStart, o.markEnd, "4")
+ }
+
+ lineCnt := o.buf.CursorLineCount()
+ buf := bytes.NewBuffer(nil)
+ buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
+ buf.WriteString("\033[J")
+ if o.state == S_STATE_FAILING {
+ buf.WriteString("failing ")
+ }
+ if o.dir == S_DIR_BCK {
+ buf.WriteString("bck")
+ } else if o.dir == S_DIR_FWD {
+ buf.WriteString("fwd")
+ }
+ buf.WriteString("-i-search: ")
+ buf.WriteString(string(o.data)) // keyword
+ buf.WriteString("\033[4m \033[0m") // _
+ fmt.Fprintf(buf, "\r\033[%dA", lineCnt) // move prev
+ if x > 0 {
+ fmt.Fprintf(buf, "\033[%dC", x) // move forward
+ }
+ o.w.Write(buf.Bytes())
+}
diff --git a/vendor/github.com/chzyer/readline/std.go b/vendor/github.com/chzyer/readline/std.go
new file mode 100644
index 000000000..61d44b759
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/std.go
@@ -0,0 +1,197 @@
+package readline
+
+import (
+ "io"
+ "os"
+ "sync"
+ "sync/atomic"
+)
+
+var (
+ Stdin io.ReadCloser = os.Stdin
+ Stdout io.WriteCloser = os.Stdout
+ Stderr io.WriteCloser = os.Stderr
+)
+
+var (
+ std *Instance
+ stdOnce sync.Once
+)
+
+// global instance will not submit history automatic
+func getInstance() *Instance {
+ stdOnce.Do(func() {
+ std, _ = NewEx(&Config{
+ DisableAutoSaveHistory: true,
+ })
+ })
+ return std
+}
+
+// let readline load history from filepath
+// and try to persist history into disk
+// set fp to "" to prevent readline persisting history to disk
+// so the `AddHistory` will return nil error forever.
+func SetHistoryPath(fp string) {
+ ins := getInstance()
+ cfg := ins.Config.Clone()
+ cfg.HistoryFile = fp
+ ins.SetConfig(cfg)
+}
+
+// set auto completer to global instance
+func SetAutoComplete(completer AutoCompleter) {
+ ins := getInstance()
+ cfg := ins.Config.Clone()
+ cfg.AutoComplete = completer
+ ins.SetConfig(cfg)
+}
+
+// add history to global instance manually
+// raise error only if `SetHistoryPath` is set with a non-empty path
+func AddHistory(content string) error {
+ ins := getInstance()
+ return ins.SaveHistory(content)
+}
+
+func Password(prompt string) ([]byte, error) {
+ ins := getInstance()
+ return ins.ReadPassword(prompt)
+}
+
+// readline with global configs
+func Line(prompt string) (string, error) {
+ ins := getInstance()
+ ins.SetPrompt(prompt)
+ return ins.Readline()
+}
+
+type CancelableStdin struct {
+ r io.Reader
+ mutex sync.Mutex
+ stop chan struct{}
+ closed int32
+ notify chan struct{}
+ data []byte
+ read int
+ err error
+}
+
+func NewCancelableStdin(r io.Reader) *CancelableStdin {
+ c := &CancelableStdin{
+ r: r,
+ notify: make(chan struct{}),
+ stop: make(chan struct{}),
+ }
+ go c.ioloop()
+ return c
+}
+
+func (c *CancelableStdin) ioloop() {
+loop:
+ for {
+ select {
+ case <-c.notify:
+ c.read, c.err = c.r.Read(c.data)
+ select {
+ case c.notify <- struct{}{}:
+ case <-c.stop:
+ break loop
+ }
+ case <-c.stop:
+ break loop
+ }
+ }
+}
+
+func (c *CancelableStdin) Read(b []byte) (n int, err error) {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+ if atomic.LoadInt32(&c.closed) == 1 {
+ return 0, io.EOF
+ }
+
+ c.data = b
+ select {
+ case c.notify <- struct{}{}:
+ case <-c.stop:
+ return 0, io.EOF
+ }
+ select {
+ case <-c.notify:
+ return c.read, c.err
+ case <-c.stop:
+ return 0, io.EOF
+ }
+}
+
+func (c *CancelableStdin) Close() error {
+ if atomic.CompareAndSwapInt32(&c.closed, 0, 1) {
+ close(c.stop)
+ }
+ return nil
+}
+
+// FillableStdin is a stdin reader which can prepend some data before
+// reading into the real stdin
+type FillableStdin struct {
+ sync.Mutex
+ stdin io.Reader
+ stdinBuffer io.ReadCloser
+ buf []byte
+ bufErr error
+}
+
+// NewFillableStdin gives you FillableStdin
+func NewFillableStdin(stdin io.Reader) (io.ReadCloser, io.Writer) {
+ r, w := io.Pipe()
+ s := &FillableStdin{
+ stdinBuffer: r,
+ stdin: stdin,
+ }
+ s.ioloop()
+ return s, w
+}
+
+func (s *FillableStdin) ioloop() {
+ go func() {
+ for {
+ bufR := make([]byte, 100)
+ var n int
+ n, s.bufErr = s.stdinBuffer.Read(bufR)
+ if s.bufErr != nil {
+ if s.bufErr == io.ErrClosedPipe {
+ break
+ }
+ }
+ s.Lock()
+ s.buf = append(s.buf, bufR[:n]...)
+ s.Unlock()
+ }
+ }()
+}
+
+// Read will read from the local buffer and if no data, read from stdin
+func (s *FillableStdin) Read(p []byte) (n int, err error) {
+ s.Lock()
+ i := len(s.buf)
+ if len(p) < i {
+ i = len(p)
+ }
+ if i > 0 {
+ n := copy(p, s.buf)
+ s.buf = s.buf[:0]
+ cerr := s.bufErr
+ s.bufErr = nil
+ s.Unlock()
+ return n, cerr
+ }
+ s.Unlock()
+ n, err = s.stdin.Read(p)
+ return n, err
+}
+
+func (s *FillableStdin) Close() error {
+ s.stdinBuffer.Close()
+ return nil
+}
diff --git a/vendor/github.com/chzyer/readline/std_windows.go b/vendor/github.com/chzyer/readline/std_windows.go
new file mode 100644
index 000000000..b10f91bcb
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/std_windows.go
@@ -0,0 +1,9 @@
+// +build windows
+
+package readline
+
+func init() {
+ Stdin = NewRawReader()
+ Stdout = NewANSIWriter(Stdout)
+ Stderr = NewANSIWriter(Stderr)
+}
diff --git a/vendor/github.com/chzyer/readline/term.go b/vendor/github.com/chzyer/readline/term.go
new file mode 100644
index 000000000..ea5db9346
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/term.go
@@ -0,0 +1,123 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd os400 solaris
+
+// Package terminal provides support functions for dealing with terminals, as
+// commonly found on UNIX systems.
+//
+// Putting a terminal into raw mode is the most common requirement:
+//
+// oldState, err := terminal.MakeRaw(0)
+// if err != nil {
+// panic(err)
+// }
+// defer terminal.Restore(0, oldState)
+package readline
+
+import (
+ "io"
+ "syscall"
+)
+
+// State contains the state of a terminal.
+type State struct {
+ termios Termios
+}
+
+// IsTerminal returns true if the given file descriptor is a terminal.
+func IsTerminal(fd int) bool {
+ _, err := getTermios(fd)
+ return err == nil
+}
+
+// MakeRaw put the terminal connected to the given file descriptor into raw
+// mode and returns the previous state of the terminal so that it can be
+// restored.
+func MakeRaw(fd int) (*State, error) {
+ var oldState State
+
+ if termios, err := getTermios(fd); err != nil {
+ return nil, err
+ } else {
+ oldState.termios = *termios
+ }
+
+ newState := oldState.termios
+ // This attempts to replicate the behaviour documented for cfmakeraw in
+ // the termios(3) manpage.
+ newState.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON
+ // newState.Oflag &^= syscall.OPOST
+ newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
+ newState.Cflag &^= syscall.CSIZE | syscall.PARENB
+ newState.Cflag |= syscall.CS8
+
+ newState.Cc[syscall.VMIN] = 1
+ newState.Cc[syscall.VTIME] = 0
+
+ return &oldState, setTermios(fd, &newState)
+}
+
+// GetState returns the current state of a terminal which may be useful to
+// restore the terminal after a signal.
+func GetState(fd int) (*State, error) {
+ termios, err := getTermios(fd)
+ if err != nil {
+ return nil, err
+ }
+
+ return &State{termios: *termios}, nil
+}
+
+// Restore restores the terminal connected to the given file descriptor to a
+// previous state.
+func restoreTerm(fd int, state *State) error {
+ return setTermios(fd, &state.termios)
+}
+
+// ReadPassword reads a line of input from a terminal without local echo. This
+// is commonly used for inputting passwords and other sensitive data. The slice
+// returned does not include the \n.
+func ReadPassword(fd int) ([]byte, error) {
+ oldState, err := getTermios(fd)
+ if err != nil {
+ return nil, err
+ }
+
+ newState := oldState
+ newState.Lflag &^= syscall.ECHO
+ newState.Lflag |= syscall.ICANON | syscall.ISIG
+ newState.Iflag |= syscall.ICRNL
+ if err := setTermios(fd, newState); err != nil {
+ return nil, err
+ }
+
+ defer func() {
+ setTermios(fd, oldState)
+ }()
+
+ var buf [16]byte
+ var ret []byte
+ for {
+ n, err := syscall.Read(fd, buf[:])
+ if err != nil {
+ return nil, err
+ }
+ if n == 0 {
+ if len(ret) == 0 {
+ return nil, io.EOF
+ }
+ break
+ }
+ if buf[n-1] == '\n' {
+ n--
+ }
+ ret = append(ret, buf[:n]...)
+ if n < len(buf) {
+ break
+ }
+ }
+
+ return ret, nil
+}
diff --git a/vendor/github.com/chzyer/readline/term_bsd.go b/vendor/github.com/chzyer/readline/term_bsd.go
new file mode 100644
index 000000000..68b56ea6b
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/term_bsd.go
@@ -0,0 +1,29 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin dragonfly freebsd netbsd openbsd
+
+package readline
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+func getTermios(fd int) (*Termios, error) {
+ termios := new(Termios)
+ _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCGETA, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
+ if err != 0 {
+ return nil, err
+ }
+ return termios, nil
+}
+
+func setTermios(fd int, termios *Termios) error {
+ _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCSETA, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
+ if err != 0 {
+ return err
+ }
+ return nil
+}
diff --git a/vendor/github.com/chzyer/readline/term_linux.go b/vendor/github.com/chzyer/readline/term_linux.go
new file mode 100644
index 000000000..e3392b4ac
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/term_linux.go
@@ -0,0 +1,33 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package readline
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+// These constants are declared here, rather than importing
+// them from the syscall package as some syscall packages, even
+// on linux, for example gccgo, do not declare them.
+const ioctlReadTermios = 0x5401 // syscall.TCGETS
+const ioctlWriteTermios = 0x5402 // syscall.TCSETS
+
+func getTermios(fd int) (*Termios, error) {
+ termios := new(Termios)
+ _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
+ if err != 0 {
+ return nil, err
+ }
+ return termios, nil
+}
+
+func setTermios(fd int, termios *Termios) error {
+ _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
+ if err != 0 {
+ return err
+ }
+ return nil
+}
diff --git a/vendor/github.com/chzyer/readline/term_nosyscall6.go b/vendor/github.com/chzyer/readline/term_nosyscall6.go
new file mode 100644
index 000000000..df9233937
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/term_nosyscall6.go
@@ -0,0 +1,32 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build aix os400 solaris
+
+package readline
+
+import "golang.org/x/sys/unix"
+
+// GetSize returns the dimensions of the given terminal.
+func GetSize(fd int) (int, int, error) {
+ ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
+ if err != nil {
+ return 0, 0, err
+ }
+ return int(ws.Col), int(ws.Row), nil
+}
+
+type Termios unix.Termios
+
+func getTermios(fd int) (*Termios, error) {
+ termios, err := unix.IoctlGetTermios(fd, unix.TCGETS)
+ if err != nil {
+ return nil, err
+ }
+ return (*Termios)(termios), nil
+}
+
+func setTermios(fd int, termios *Termios) error {
+ return unix.IoctlSetTermios(fd, unix.TCSETSF, (*unix.Termios)(termios))
+}
diff --git a/vendor/github.com/chzyer/readline/term_unix.go b/vendor/github.com/chzyer/readline/term_unix.go
new file mode 100644
index 000000000..d3ea24244
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/term_unix.go
@@ -0,0 +1,24 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd
+
+package readline
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+type Termios syscall.Termios
+
+// GetSize returns the dimensions of the given terminal.
+func GetSize(fd int) (int, int, error) {
+ var dimensions [4]uint16
+ _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0)
+ if err != 0 {
+ return 0, 0, err
+ }
+ return int(dimensions[1]), int(dimensions[0]), nil
+}
diff --git a/vendor/github.com/chzyer/readline/term_windows.go b/vendor/github.com/chzyer/readline/term_windows.go
new file mode 100644
index 000000000..1290e00bc
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/term_windows.go
@@ -0,0 +1,171 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build windows
+
+// Package terminal provides support functions for dealing with terminals, as
+// commonly found on UNIX systems.
+//
+// Putting a terminal into raw mode is the most common requirement:
+//
+// oldState, err := terminal.MakeRaw(0)
+// if err != nil {
+// panic(err)
+// }
+// defer terminal.Restore(0, oldState)
+package readline
+
+import (
+ "io"
+ "syscall"
+ "unsafe"
+)
+
+const (
+ enableLineInput = 2
+ enableEchoInput = 4
+ enableProcessedInput = 1
+ enableWindowInput = 8
+ enableMouseInput = 16
+ enableInsertMode = 32
+ enableQuickEditMode = 64
+ enableExtendedFlags = 128
+ enableAutoPosition = 256
+ enableProcessedOutput = 1
+ enableWrapAtEolOutput = 2
+)
+
+var kernel32 = syscall.NewLazyDLL("kernel32.dll")
+
+var (
+ procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
+ procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
+ procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
+)
+
+type (
+ coord struct {
+ x short
+ y short
+ }
+ smallRect struct {
+ left short
+ top short
+ right short
+ bottom short
+ }
+ consoleScreenBufferInfo struct {
+ size coord
+ cursorPosition coord
+ attributes word
+ window smallRect
+ maximumWindowSize coord
+ }
+)
+
+type State struct {
+ mode uint32
+}
+
+// IsTerminal returns true if the given file descriptor is a terminal.
+func IsTerminal(fd int) bool {
+ var st uint32
+ r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
+ return r != 0 && e == 0
+}
+
+// MakeRaw put the terminal connected to the given file descriptor into raw
+// mode and returns the previous state of the terminal so that it can be
+// restored.
+func MakeRaw(fd int) (*State, error) {
+ var st uint32
+ _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
+ if e != 0 {
+ return nil, error(e)
+ }
+ raw := st &^ (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput)
+ _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(raw), 0)
+ if e != 0 {
+ return nil, error(e)
+ }
+ return &State{st}, nil
+}
+
+// GetState returns the current state of a terminal which may be useful to
+// restore the terminal after a signal.
+func GetState(fd int) (*State, error) {
+ var st uint32
+ _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
+ if e != 0 {
+ return nil, error(e)
+ }
+ return &State{st}, nil
+}
+
+// Restore restores the terminal connected to the given file descriptor to a
+// previous state.
+func restoreTerm(fd int, state *State) error {
+ _, _, err := syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(state.mode), 0)
+ return err
+}
+
+// GetSize returns the dimensions of the given terminal.
+func GetSize(fd int) (width, height int, err error) {
+ var info consoleScreenBufferInfo
+ _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&info)), 0)
+ if e != 0 {
+ return 0, 0, error(e)
+ }
+ return int(info.size.x), int(info.size.y), nil
+}
+
+// ReadPassword reads a line of input from a terminal without local echo. This
+// is commonly used for inputting passwords and other sensitive data. The slice
+// returned does not include the \n.
+func ReadPassword(fd int) ([]byte, error) {
+ var st uint32
+ _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
+ if e != 0 {
+ return nil, error(e)
+ }
+ old := st
+
+ st &^= (enableEchoInput)
+ st |= (enableProcessedInput | enableLineInput | enableProcessedOutput)
+ _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0)
+ if e != 0 {
+ return nil, error(e)
+ }
+
+ defer func() {
+ syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(old), 0)
+ }()
+
+ var buf [16]byte
+ var ret []byte
+ for {
+ n, err := syscall.Read(syscall.Handle(fd), buf[:])
+ if err != nil {
+ return nil, err
+ }
+ if n == 0 {
+ if len(ret) == 0 {
+ return nil, io.EOF
+ }
+ break
+ }
+ if buf[n-1] == '\n' {
+ n--
+ }
+ if n > 0 && buf[n-1] == '\r' {
+ n--
+ }
+ ret = append(ret, buf[:n]...)
+ if n < len(buf) {
+ break
+ }
+ }
+
+ return ret, nil
+}
diff --git a/vendor/github.com/chzyer/readline/terminal.go b/vendor/github.com/chzyer/readline/terminal.go
new file mode 100644
index 000000000..38413d0cf
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/terminal.go
@@ -0,0 +1,254 @@
+package readline
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "strings"
+ "sync"
+ "sync/atomic"
+)
+
+type Terminal struct {
+ m sync.Mutex
+ cfg *Config
+ outchan chan rune
+ closed int32
+ stopChan chan struct{}
+ kickChan chan struct{}
+ wg sync.WaitGroup
+ isReading int32
+ sleeping int32
+
+ sizeChan chan string
+}
+
+func NewTerminal(cfg *Config) (*Terminal, error) {
+ if err := cfg.Init(); err != nil {
+ return nil, err
+ }
+ t := &Terminal{
+ cfg: cfg,
+ kickChan: make(chan struct{}, 1),
+ outchan: make(chan rune),
+ stopChan: make(chan struct{}, 1),
+ sizeChan: make(chan string, 1),
+ }
+
+ go t.ioloop()
+ return t, nil
+}
+
+// SleepToResume will sleep myself, and return only if I'm resumed.
+func (t *Terminal) SleepToResume() {
+ if !atomic.CompareAndSwapInt32(&t.sleeping, 0, 1) {
+ return
+ }
+ defer atomic.StoreInt32(&t.sleeping, 0)
+
+ t.ExitRawMode()
+ ch := WaitForResume()
+ SuspendMe()
+ <-ch
+ t.EnterRawMode()
+}
+
+func (t *Terminal) EnterRawMode() (err error) {
+ return t.cfg.FuncMakeRaw()
+}
+
+func (t *Terminal) ExitRawMode() (err error) {
+ return t.cfg.FuncExitRaw()
+}
+
+func (t *Terminal) Write(b []byte) (int, error) {
+ return t.cfg.Stdout.Write(b)
+}
+
+// WriteStdin prefill the next Stdin fetch
+// Next time you call ReadLine() this value will be writen before the user input
+func (t *Terminal) WriteStdin(b []byte) (int, error) {
+ return t.cfg.StdinWriter.Write(b)
+}
+
+type termSize struct {
+ left int
+ top int
+}
+
+func (t *Terminal) GetOffset(f func(offset string)) {
+ go func() {
+ f(<-t.sizeChan)
+ }()
+ t.Write([]byte("\033[6n"))
+}
+
+func (t *Terminal) Print(s string) {
+ fmt.Fprintf(t.cfg.Stdout, "%s", s)
+}
+
+func (t *Terminal) PrintRune(r rune) {
+ fmt.Fprintf(t.cfg.Stdout, "%c", r)
+}
+
+func (t *Terminal) Readline() *Operation {
+ return NewOperation(t, t.cfg)
+}
+
+// return rune(0) if meet EOF
+func (t *Terminal) ReadRune() rune {
+ ch, ok := <-t.outchan
+ if !ok {
+ return rune(0)
+ }
+ return ch
+}
+
+func (t *Terminal) IsReading() bool {
+ return atomic.LoadInt32(&t.isReading) == 1
+}
+
+func (t *Terminal) KickRead() {
+ select {
+ case t.kickChan <- struct{}{}:
+ default:
+ }
+}
+
+func (t *Terminal) ioloop() {
+ t.wg.Add(1)
+ defer func() {
+ t.wg.Done()
+ close(t.outchan)
+ }()
+
+ var (
+ isEscape bool
+ isEscapeEx bool
+ isEscapeSS3 bool
+ expectNextChar bool
+ )
+
+ buf := bufio.NewReader(t.getStdin())
+ for {
+ if !expectNextChar {
+ atomic.StoreInt32(&t.isReading, 0)
+ select {
+ case <-t.kickChan:
+ atomic.StoreInt32(&t.isReading, 1)
+ case <-t.stopChan:
+ return
+ }
+ }
+ expectNextChar = false
+ r, _, err := buf.ReadRune()
+ if err != nil {
+ if strings.Contains(err.Error(), "interrupted system call") {
+ expectNextChar = true
+ continue
+ }
+ break
+ }
+
+ if isEscape {
+ isEscape = false
+ if r == CharEscapeEx {
+ // ^][
+ expectNextChar = true
+ isEscapeEx = true
+ continue
+ } else if r == CharO {
+ // ^]O
+ expectNextChar = true
+ isEscapeSS3 = true
+ continue
+ }
+ r = escapeKey(r, buf)
+ } else if isEscapeEx {
+ isEscapeEx = false
+ if key := readEscKey(r, buf); key != nil {
+ r = escapeExKey(key)
+ // offset
+ if key.typ == 'R' {
+ if _, _, ok := key.Get2(); ok {
+ select {
+ case t.sizeChan <- key.attr:
+ default:
+ }
+ }
+ expectNextChar = true
+ continue
+ }
+ }
+ if r == 0 {
+ expectNextChar = true
+ continue
+ }
+ } else if isEscapeSS3 {
+ isEscapeSS3 = false
+ if key := readEscKey(r, buf); key != nil {
+ r = escapeSS3Key(key)
+ }
+ if r == 0 {
+ expectNextChar = true
+ continue
+ }
+ }
+
+ expectNextChar = true
+ switch r {
+ case CharEsc:
+ if t.cfg.VimMode {
+ t.outchan <- r
+ break
+ }
+ isEscape = true
+ case CharInterrupt, CharEnter, CharCtrlJ, CharDelete:
+ expectNextChar = false
+ fallthrough
+ default:
+ t.outchan <- r
+ }
+ }
+
+}
+
+func (t *Terminal) Bell() {
+ fmt.Fprintf(t, "%c", CharBell)
+}
+
+func (t *Terminal) Close() error {
+ if atomic.SwapInt32(&t.closed, 1) != 0 {
+ return nil
+ }
+ if closer, ok := t.cfg.Stdin.(io.Closer); ok {
+ closer.Close()
+ }
+ close(t.stopChan)
+ t.wg.Wait()
+ return t.ExitRawMode()
+}
+
+func (t *Terminal) GetConfig() *Config {
+ t.m.Lock()
+ cfg := *t.cfg
+ t.m.Unlock()
+ return &cfg
+}
+
+func (t *Terminal) getStdin() io.Reader {
+ t.m.Lock()
+ r := t.cfg.Stdin
+ t.m.Unlock()
+ return r
+}
+
+func (t *Terminal) SetConfig(c *Config) error {
+ if err := c.Init(); err != nil {
+ return err
+ }
+ t.m.Lock()
+ t.cfg = c
+ t.m.Unlock()
+ return nil
+}
diff --git a/vendor/github.com/chzyer/readline/utils.go b/vendor/github.com/chzyer/readline/utils.go
new file mode 100644
index 000000000..0706dd4ec
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/utils.go
@@ -0,0 +1,311 @@
+package readline
+
+import (
+ "bufio"
+ "bytes"
+ "container/list"
+ "fmt"
+ "os"
+ "os/signal"
+ "strconv"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+ "unicode"
+)
+
+var (
+ isWindows = false
+)
+
+const (
+ CharLineStart = 1
+ CharBackward = 2
+ CharInterrupt = 3
+ CharDelete = 4
+ CharLineEnd = 5
+ CharForward = 6
+ CharBell = 7
+ CharCtrlH = 8
+ CharTab = 9
+ CharCtrlJ = 10
+ CharKill = 11
+ CharCtrlL = 12
+ CharEnter = 13
+ CharNext = 14
+ CharPrev = 16
+ CharBckSearch = 18
+ CharFwdSearch = 19
+ CharTranspose = 20
+ CharCtrlU = 21
+ CharCtrlW = 23
+ CharCtrlY = 25
+ CharCtrlZ = 26
+ CharEsc = 27
+ CharO = 79
+ CharEscapeEx = 91
+ CharBackspace = 127
+)
+
+const (
+ MetaBackward rune = -iota - 1
+ MetaForward
+ MetaDelete
+ MetaBackspace
+ MetaTranspose
+)
+
+// WaitForResume need to call before current process got suspend.
+// It will run a ticker until a long duration is occurs,
+// which means this process is resumed.
+func WaitForResume() chan struct{} {
+ ch := make(chan struct{})
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ ticker := time.NewTicker(10 * time.Millisecond)
+ t := time.Now()
+ wg.Done()
+ for {
+ now := <-ticker.C
+ if now.Sub(t) > 100*time.Millisecond {
+ break
+ }
+ t = now
+ }
+ ticker.Stop()
+ ch <- struct{}{}
+ }()
+ wg.Wait()
+ return ch
+}
+
+func Restore(fd int, state *State) error {
+ err := restoreTerm(fd, state)
+ if err != nil {
+ // errno 0 means everything is ok :)
+ if err.Error() == "errno 0" {
+ return nil
+ } else {
+ return err
+ }
+ }
+ return nil
+}
+
+func IsPrintable(key rune) bool {
+ isInSurrogateArea := key >= 0xd800 && key <= 0xdbff
+ return key >= 32 && !isInSurrogateArea
+}
+
+// translate Esc[X
+func escapeExKey(key *escapeKeyPair) rune {
+ var r rune
+ switch key.typ {
+ case 'D':
+ r = CharBackward
+ case 'C':
+ r = CharForward
+ case 'A':
+ r = CharPrev
+ case 'B':
+ r = CharNext
+ case 'H':
+ r = CharLineStart
+ case 'F':
+ r = CharLineEnd
+ case '~':
+ if key.attr == "3" {
+ r = CharDelete
+ }
+ default:
+ }
+ return r
+}
+
+// translate EscOX SS3 codes for up/down/etc.
+func escapeSS3Key(key *escapeKeyPair) rune {
+ var r rune
+ switch key.typ {
+ case 'D':
+ r = CharBackward
+ case 'C':
+ r = CharForward
+ case 'A':
+ r = CharPrev
+ case 'B':
+ r = CharNext
+ case 'H':
+ r = CharLineStart
+ case 'F':
+ r = CharLineEnd
+ default:
+ }
+ return r
+}
+
+type escapeKeyPair struct {
+ attr string
+ typ rune
+}
+
+func (e *escapeKeyPair) Get2() (int, int, bool) {
+ sp := strings.Split(e.attr, ";")
+ if len(sp) < 2 {
+ return -1, -1, false
+ }
+ s1, err := strconv.Atoi(sp[0])
+ if err != nil {
+ return -1, -1, false
+ }
+ s2, err := strconv.Atoi(sp[1])
+ if err != nil {
+ return -1, -1, false
+ }
+ return s1, s2, true
+}
+
+func readEscKey(r rune, reader *bufio.Reader) *escapeKeyPair {
+ p := escapeKeyPair{}
+ buf := bytes.NewBuffer(nil)
+ for {
+ if r == ';' {
+ } else if unicode.IsNumber(r) {
+ } else {
+ p.typ = r
+ break
+ }
+ buf.WriteRune(r)
+ r, _, _ = reader.ReadRune()
+ }
+ p.attr = buf.String()
+ return &p
+}
+
+// translate EscX to Meta+X
+func escapeKey(r rune, reader *bufio.Reader) rune {
+ switch r {
+ case 'b':
+ r = MetaBackward
+ case 'f':
+ r = MetaForward
+ case 'd':
+ r = MetaDelete
+ case CharTranspose:
+ r = MetaTranspose
+ case CharBackspace:
+ r = MetaBackspace
+ case 'O':
+ d, _, _ := reader.ReadRune()
+ switch d {
+ case 'H':
+ r = CharLineStart
+ case 'F':
+ r = CharLineEnd
+ default:
+ reader.UnreadRune()
+ }
+ case CharEsc:
+
+ }
+ return r
+}
+
+func SplitByLine(start, screenWidth int, rs []rune) []string {
+ var ret []string
+ buf := bytes.NewBuffer(nil)
+ currentWidth := start
+ for _, r := range rs {
+ w := runes.Width(r)
+ currentWidth += w
+ buf.WriteRune(r)
+ if currentWidth >= screenWidth {
+ ret = append(ret, buf.String())
+ buf.Reset()
+ currentWidth = 0
+ }
+ }
+ ret = append(ret, buf.String())
+ return ret
+}
+
+// calculate how many lines for N character
+func LineCount(screenWidth, w int) int {
+ r := w / screenWidth
+ if w%screenWidth != 0 {
+ r++
+ }
+ return r
+}
+
+func IsWordBreak(i rune) bool {
+ switch {
+ case i >= 'a' && i <= 'z':
+ case i >= 'A' && i <= 'Z':
+ case i >= '0' && i <= '9':
+ default:
+ return true
+ }
+ return false
+}
+
+func GetInt(s []string, def int) int {
+ if len(s) == 0 {
+ return def
+ }
+ c, err := strconv.Atoi(s[0])
+ if err != nil {
+ return def
+ }
+ return c
+}
+
+type RawMode struct {
+ state *State
+}
+
+func (r *RawMode) Enter() (err error) {
+ r.state, err = MakeRaw(GetStdin())
+ return err
+}
+
+func (r *RawMode) Exit() error {
+ if r.state == nil {
+ return nil
+ }
+ return Restore(GetStdin(), r.state)
+}
+
+// -----------------------------------------------------------------------------
+
+func sleep(n int) {
+ Debug(n)
+ time.Sleep(2000 * time.Millisecond)
+}
+
+// print a linked list to Debug()
+func debugList(l *list.List) {
+ idx := 0
+ for e := l.Front(); e != nil; e = e.Next() {
+ Debug(idx, fmt.Sprintf("%+v", e.Value))
+ idx++
+ }
+}
+
+// append log info to another file
+func Debug(o ...interface{}) {
+ f, _ := os.OpenFile("debug.tmp", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+ fmt.Fprintln(f, o...)
+ f.Close()
+}
+
+func CaptureExitSignal(f func()) {
+ cSignal := make(chan os.Signal, 1)
+ signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
+ go func() {
+ for range cSignal {
+ f()
+ }
+ }()
+}
diff --git a/vendor/github.com/chzyer/readline/utils_unix.go b/vendor/github.com/chzyer/readline/utils_unix.go
new file mode 100644
index 000000000..fc4949232
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/utils_unix.go
@@ -0,0 +1,83 @@
+// +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd os400 solaris
+
+package readline
+
+import (
+ "io"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+)
+
+type winsize struct {
+ Row uint16
+ Col uint16
+ Xpixel uint16
+ Ypixel uint16
+}
+
+// SuspendMe use to send suspend signal to myself, when we in the raw mode.
+// For OSX it need to send to parent's pid
+// For Linux it need to send to myself
+func SuspendMe() {
+ p, _ := os.FindProcess(os.Getppid())
+ p.Signal(syscall.SIGTSTP)
+ p, _ = os.FindProcess(os.Getpid())
+ p.Signal(syscall.SIGTSTP)
+}
+
+// get width of the terminal
+func getWidth(stdoutFd int) int {
+ cols, _, err := GetSize(stdoutFd)
+ if err != nil {
+ return -1
+ }
+ return cols
+}
+
+func GetScreenWidth() int {
+ w := getWidth(syscall.Stdout)
+ if w < 0 {
+ w = getWidth(syscall.Stderr)
+ }
+ return w
+}
+
+// ClearScreen clears the console screen
+func ClearScreen(w io.Writer) (int, error) {
+ return w.Write([]byte("\033[H"))
+}
+
+func DefaultIsTerminal() bool {
+ return IsTerminal(syscall.Stdin) && (IsTerminal(syscall.Stdout) || IsTerminal(syscall.Stderr))
+}
+
+func GetStdin() int {
+ return syscall.Stdin
+}
+
+// -----------------------------------------------------------------------------
+
+var (
+ widthChange sync.Once
+ widthChangeCallback func()
+)
+
+func DefaultOnWidthChanged(f func()) {
+ widthChangeCallback = f
+ widthChange.Do(func() {
+ ch := make(chan os.Signal, 1)
+ signal.Notify(ch, syscall.SIGWINCH)
+
+ go func() {
+ for {
+ _, ok := <-ch
+ if !ok {
+ break
+ }
+ widthChangeCallback()
+ }
+ }()
+ })
+}
diff --git a/vendor/github.com/chzyer/readline/utils_windows.go b/vendor/github.com/chzyer/readline/utils_windows.go
new file mode 100644
index 000000000..5bfa55dcc
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/utils_windows.go
@@ -0,0 +1,41 @@
+// +build windows
+
+package readline
+
+import (
+ "io"
+ "syscall"
+)
+
+func SuspendMe() {
+}
+
+func GetStdin() int {
+ return int(syscall.Stdin)
+}
+
+func init() {
+ isWindows = true
+}
+
+// get width of the terminal
+func GetScreenWidth() int {
+ info, _ := GetConsoleScreenBufferInfo()
+ if info == nil {
+ return -1
+ }
+ return int(info.dwSize.x)
+}
+
+// ClearScreen clears the console screen
+func ClearScreen(_ io.Writer) error {
+ return SetConsoleCursorPosition(&_COORD{0, 0})
+}
+
+func DefaultIsTerminal() bool {
+ return true
+}
+
+func DefaultOnWidthChanged(func()) {
+
+}
diff --git a/vendor/github.com/chzyer/readline/vim.go b/vendor/github.com/chzyer/readline/vim.go
new file mode 100644
index 000000000..bedf2c1a6
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/vim.go
@@ -0,0 +1,176 @@
+package readline
+
+const (
+ VIM_NORMAL = iota
+ VIM_INSERT
+ VIM_VISUAL
+)
+
+type opVim struct {
+ cfg *Config
+ op *Operation
+ vimMode int
+}
+
+func newVimMode(op *Operation) *opVim {
+ ov := &opVim{
+ cfg: op.cfg,
+ op: op,
+ }
+ ov.SetVimMode(ov.cfg.VimMode)
+ return ov
+}
+
+func (o *opVim) SetVimMode(on bool) {
+ if o.cfg.VimMode && !on { // turn off
+ o.ExitVimMode()
+ }
+ o.cfg.VimMode = on
+ o.vimMode = VIM_INSERT
+}
+
+func (o *opVim) ExitVimMode() {
+ o.vimMode = VIM_INSERT
+}
+
+func (o *opVim) IsEnableVimMode() bool {
+ return o.cfg.VimMode
+}
+
+func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, handled bool) {
+ rb := o.op.buf
+ handled = true
+ switch r {
+ case 'h':
+ t = CharBackward
+ case 'j':
+ t = CharNext
+ case 'k':
+ t = CharPrev
+ case 'l':
+ t = CharForward
+ case '0', '^':
+ rb.MoveToLineStart()
+ case '$':
+ rb.MoveToLineEnd()
+ case 'x':
+ rb.Delete()
+ if rb.IsCursorInEnd() {
+ rb.MoveBackward()
+ }
+ case 'r':
+ rb.Replace(readNext())
+ case 'd':
+ next := readNext()
+ switch next {
+ case 'd':
+ rb.Erase()
+ case 'w':
+ rb.DeleteWord()
+ case 'h':
+ rb.Backspace()
+ case 'l':
+ rb.Delete()
+ }
+ case 'p':
+ rb.Yank()
+ case 'b', 'B':
+ rb.MoveToPrevWord()
+ case 'w', 'W':
+ rb.MoveToNextWord()
+ case 'e', 'E':
+ rb.MoveToEndWord()
+ case 'f', 'F', 't', 'T':
+ next := readNext()
+ prevChar := r == 't' || r == 'T'
+ reverse := r == 'F' || r == 'T'
+ switch next {
+ case CharEsc:
+ default:
+ rb.MoveTo(next, prevChar, reverse)
+ }
+ default:
+ return r, false
+ }
+ return t, true
+}
+
+func (o *opVim) handleVimNormalEnterInsert(r rune, readNext func() rune) (t rune, handled bool) {
+ rb := o.op.buf
+ handled = true
+ switch r {
+ case 'i':
+ case 'I':
+ rb.MoveToLineStart()
+ case 'a':
+ rb.MoveForward()
+ case 'A':
+ rb.MoveToLineEnd()
+ case 's':
+ rb.Delete()
+ case 'S':
+ rb.Erase()
+ case 'c':
+ next := readNext()
+ switch next {
+ case 'c':
+ rb.Erase()
+ case 'w':
+ rb.DeleteWord()
+ case 'h':
+ rb.Backspace()
+ case 'l':
+ rb.Delete()
+ }
+ default:
+ return r, false
+ }
+
+ o.EnterVimInsertMode()
+ return
+}
+
+func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune) {
+ switch r {
+ case CharEnter, CharInterrupt:
+ o.ExitVimMode()
+ return r
+ }
+
+ if r, handled := o.handleVimNormalMovement(r, readNext); handled {
+ return r
+ }
+
+ if r, handled := o.handleVimNormalEnterInsert(r, readNext); handled {
+ return r
+ }
+
+ // invalid operation
+ o.op.t.Bell()
+ return 0
+}
+
+func (o *opVim) EnterVimInsertMode() {
+ o.vimMode = VIM_INSERT
+}
+
+func (o *opVim) ExitVimInsertMode() {
+ o.vimMode = VIM_NORMAL
+}
+
+func (o *opVim) HandleVim(r rune, readNext func() rune) rune {
+ if o.vimMode == VIM_NORMAL {
+ return o.HandleVimNormal(r, readNext)
+ }
+ if r == CharEsc {
+ o.ExitVimInsertMode()
+ return 0
+ }
+
+ switch o.vimMode {
+ case VIM_INSERT:
+ return r
+ case VIM_VISUAL:
+ }
+ return r
+}
diff --git a/vendor/github.com/chzyer/readline/windows_api.go b/vendor/github.com/chzyer/readline/windows_api.go
new file mode 100644
index 000000000..63f4f7b78
--- /dev/null
+++ b/vendor/github.com/chzyer/readline/windows_api.go
@@ -0,0 +1,152 @@
+// +build windows
+
+package readline
+
+import (
+ "reflect"
+ "syscall"
+ "unsafe"
+)
+
+var (
+ kernel = NewKernel()
+ stdout = uintptr(syscall.Stdout)
+ stdin = uintptr(syscall.Stdin)
+)
+
+type Kernel struct {
+ SetConsoleCursorPosition,
+ SetConsoleTextAttribute,
+ FillConsoleOutputCharacterW,
+ FillConsoleOutputAttribute,
+ ReadConsoleInputW,
+ GetConsoleScreenBufferInfo,
+ GetConsoleCursorInfo,
+ GetStdHandle CallFunc
+}
+
+type short int16
+type word uint16
+type dword uint32
+type wchar uint16
+
+type _COORD struct {
+ x short
+ y short
+}
+
+func (c *_COORD) ptr() uintptr {
+ return uintptr(*(*int32)(unsafe.Pointer(c)))
+}
+
+const (
+ EVENT_KEY = 0x0001
+ EVENT_MOUSE = 0x0002
+ EVENT_WINDOW_BUFFER_SIZE = 0x0004
+ EVENT_MENU = 0x0008
+ EVENT_FOCUS = 0x0010
+)
+
+type _KEY_EVENT_RECORD struct {
+ bKeyDown int32
+ wRepeatCount word
+ wVirtualKeyCode word
+ wVirtualScanCode word
+ unicodeChar wchar
+ dwControlKeyState dword
+}
+
+// KEY_EVENT_RECORD KeyEvent;
+// MOUSE_EVENT_RECORD MouseEvent;
+// WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
+// MENU_EVENT_RECORD MenuEvent;
+// FOCUS_EVENT_RECORD FocusEvent;
+type _INPUT_RECORD struct {
+ EventType word
+ Padding uint16
+ Event [16]byte
+}
+
+type _CONSOLE_SCREEN_BUFFER_INFO struct {
+ dwSize _COORD
+ dwCursorPosition _COORD
+ wAttributes word
+ srWindow _SMALL_RECT
+ dwMaximumWindowSize _COORD
+}
+
+type _SMALL_RECT struct {
+ left short
+ top short
+ right short
+ bottom short
+}
+
+type _CONSOLE_CURSOR_INFO struct {
+ dwSize dword
+ bVisible bool
+}
+
+type CallFunc func(u ...uintptr) error
+
+func NewKernel() *Kernel {
+ k := &Kernel{}
+ kernel32 := syscall.NewLazyDLL("kernel32.dll")
+ v := reflect.ValueOf(k).Elem()
+ t := v.Type()
+ for i := 0; i < t.NumField(); i++ {
+ name := t.Field(i).Name
+ f := kernel32.NewProc(name)
+ v.Field(i).Set(reflect.ValueOf(k.Wrap(f)))
+ }
+ return k
+}
+
+func (k *Kernel) Wrap(p *syscall.LazyProc) CallFunc {
+ return func(args ...uintptr) error {
+ var r0 uintptr
+ var e1 syscall.Errno
+ size := uintptr(len(args))
+ if len(args) <= 3 {
+ buf := make([]uintptr, 3)
+ copy(buf, args)
+ r0, _, e1 = syscall.Syscall(p.Addr(), size,
+ buf[0], buf[1], buf[2])
+ } else {
+ buf := make([]uintptr, 6)
+ copy(buf, args)
+ r0, _, e1 = syscall.Syscall6(p.Addr(), size,
+ buf[0], buf[1], buf[2], buf[3], buf[4], buf[5],
+ )
+ }
+
+ if int(r0) == 0 {
+ if e1 != 0 {
+ return error(e1)
+ } else {
+ return syscall.EINVAL
+ }
+ }
+ return nil
+ }
+
+}
+
+func GetConsoleScreenBufferInfo() (*_CONSOLE_SCREEN_BUFFER_INFO, error) {
+ t := new(_CONSOLE_SCREEN_BUFFER_INFO)
+ err := kernel.GetConsoleScreenBufferInfo(
+ stdout,
+ uintptr(unsafe.Pointer(t)),
+ )
+ return t, err
+}
+
+func GetConsoleCursorInfo() (*_CONSOLE_CURSOR_INFO, error) {
+ t := new(_CONSOLE_CURSOR_INFO)
+ err := kernel.GetConsoleCursorInfo(stdout, uintptr(unsafe.Pointer(t)))
+ return t, err
+}
+
+func SetConsoleCursorPosition(c *_COORD) error {
+ return kernel.SetConsoleCursorPosition(stdout, c.ptr())
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 224ca048b..1bbeb218b 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -68,6 +68,9 @@ github.com/cert-manager/cert-manager/pkg/apis/meta/v1
# github.com/cespare/xxhash/v2 v2.3.0
## explicit; go 1.11
github.com/cespare/xxhash/v2
+# github.com/chzyer/readline v1.5.1
+## explicit; go 1.15
+github.com/chzyer/readline
# github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.15.2
## explicit; go 1.18
github.com/cloudevents/sdk-go/observability/opencensus/v2/client