├── .gitignore
├── web
├── example
│ ├── test
│ │ ├── .gitignore
│ │ ├── Makefile
│ │ └── dummy.c
│ ├── README.md
│ ├── main.go
│ └── static
│ │ ├── style.css
│ │ ├── index.html
│ │ └── script.js
├── go.mod
├── go.sum
├── README.md
├── doc.go
└── handler.go
├── go.mod
├── go.sum
├── interrupt_test.go
├── LICENSE
├── doc.go
├── commands_test.go
├── io_test.go
├── lexer.go
├── grammar.y
├── parser_test.go
├── commands.go
├── gdb.go
├── README.md
└── grammar.go
/.gitignore:
--------------------------------------------------------------------------------
1 | /_tmp/
2 | y.output
3 |
--------------------------------------------------------------------------------
/web/example/test/.gitignore:
--------------------------------------------------------------------------------
1 | dummy
2 |
--------------------------------------------------------------------------------
/web/example/test/Makefile:
--------------------------------------------------------------------------------
1 | CFLAGS=-g
2 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/cyrus-and/gdb
2 |
3 | go 1.18
4 |
5 | require github.com/kr/pty v1.1.8
6 |
7 | require github.com/creack/pty v1.1.18 // indirect
8 |
--------------------------------------------------------------------------------
/web/example/test/dummy.c:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | int main(int argc, char *argv[])
4 | {
5 | int x;
6 |
7 | printf("Hello %s, write a number!\n", argv[1]);
8 | printf("x = ");
9 | scanf("%d", &x);
10 | printf("x * x = %d\n", x * x);
11 | return 0;
12 | }
13 |
--------------------------------------------------------------------------------
/web/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/cyrus-and/gdbweb
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/cyrus-and/gdb v0.0.0-20180213171718-0306a029f42f
7 | golang.org/x/net v0.7.0
8 | )
9 |
10 | require (
11 | github.com/creack/pty v1.1.18 // indirect
12 | github.com/kr/pty v1.1.8 // indirect
13 | )
14 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
2 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
3 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
4 | github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
5 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
6 |
--------------------------------------------------------------------------------
/web/example/README.md:
--------------------------------------------------------------------------------
1 | # web example
2 |
3 | This is a very simple example of how the `web` package can be used to build a web-based front end for GDB.
4 |
5 | 
6 |
7 | ## Usage
8 |
9 | 1. move into this directory;
10 |
11 | 2. compile the dummy test program with:
12 |
13 | ```
14 | make -C test/ dummy
15 | ```
16 | 3. start the server with:
17 |
18 | ```
19 | go run main.go
20 | ```
21 |
22 | then load `http://localhost:8080/` in your browser
23 |
24 | 4. load the test program using a relative path `test/dummy`;
25 |
26 | 5. the rest should be straightforward...
27 |
--------------------------------------------------------------------------------
/web/example/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cyrus-and/gdb/web"
5 | "log"
6 | "net/http"
7 | "path/filepath"
8 | )
9 |
10 | func main() {
11 | mux, err := web.NewHandler()
12 | if err != nil {
13 | log.Fatal(err)
14 | }
15 | // static assets
16 | mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
17 | http.ServeFile(w, req, filepath.Join("static", req.URL.Path[1:]))
18 | })
19 | // source files
20 | mux.HandleFunc("/source/", func(w http.ResponseWriter, req *http.Request) {
21 | http.ServeFile(w, req, req.URL.Path[len("/source"):])
22 | })
23 | log.Fatal(http.ListenAndServe("localhost:8080", mux))
24 | }
25 |
--------------------------------------------------------------------------------
/web/go.sum:
--------------------------------------------------------------------------------
1 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
2 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
3 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
4 | github.com/cyrus-and/gdb v0.0.0-20180213171718-0306a029f42f h1:EsEZxjb7ni/7s8POJenkO8wwbbrbOO4Omr8zAQG5tO8=
5 | github.com/cyrus-and/gdb v0.0.0-20180213171718-0306a029f42f/go.mod h1:9K120i9a8gTrRiYUpFXiLMBNH3yFnNs8J66+FWOtPYE=
6 | github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
7 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
8 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
9 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
10 |
--------------------------------------------------------------------------------
/web/example/static/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans;
3 | max-width: 1000px;
4 | margin: 0 auto;
5 | padding: 0 1em;
6 | }
7 |
8 | label {
9 | display: block;
10 | margin-top: 1em;
11 | }
12 |
13 | .toolbar {
14 | overflow: hidden;
15 | }
16 |
17 | .toolbar-button {
18 | float: right;
19 | margin-left: 0.5em;
20 | }
21 |
22 | .toolbar-text {
23 | overflow: hidden;
24 | display: block;
25 | }
26 |
27 | input[type=text] {
28 | width: 100%;
29 | box-sizing: border-box;
30 | }
31 |
32 | .window {
33 | font-family: monospace;
34 | white-space: pre-wrap;
35 | background: black;
36 | color: #0f0;
37 | padding: 0.5em;
38 | margin-bottom: 1em;
39 | }
40 |
41 | #source span {
42 | display: block;
43 | }
44 |
45 | .selected {
46 | background: #0f0;
47 | color: #000;
48 | }
49 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | # web
2 |
3 | Provides a simple web interface for the `gdb` module.
4 |
5 | Every new handler spawns a new GDB instance which is not meant to serve more than one client.
6 |
7 | Several URLs are available:
8 |
9 | - a WebSocket will be bound to `NotificationsUrl`, every notification sent by GDB will be delivered through this WebSocket as a JSON object;
10 |
11 | - a WebSocket will be bound to `TerminalUrl`: data sent through it will be delivered to the target program's input whereas data read from it will reflect the the target program's output;
12 |
13 | - POST requests are expected to `CommandsUrl` to send commands to GDB. Commands are represented by JSON arrays `[command, arguments...]`. Command responses are sent back as JSON objects in the response body;
14 |
15 | - POST requests are expected to `InterruptUrl` to send interrupts to GDB, no parameters are allowed.
16 |
17 | HTTP methods return 200 on success and 500 on error.
18 |
19 | ## Example
20 |
21 | A dummy example can be found in the [example](example/) directory.
22 |
--------------------------------------------------------------------------------
/web/doc.go:
--------------------------------------------------------------------------------
1 | // Package web provides a simple web interface for the gdb module.
2 | //
3 | // Every new handler spawns a new GDB instance which is not meant to serve more
4 | // than one client.
5 | //
6 | // Several URLs are available:
7 | //
8 | // - a WebSocket will be bound to NotificationsUrl, every notification sent by
9 | // GDB will be delivered through this WebSocket as a JSON object;
10 | //
11 | // - a WebSocket will be bound to TerminalUrl: data sent through it will be
12 | // delivered to the target program's input whereas data read from it will
13 | // reflect the the target program's output;
14 | //
15 | // - POST requests are expected to CommandsUrl to send commands to GDB. Commands
16 | // are represented by JSON arrays [command, arguments...]. Command responses
17 | // are sent back as JSON objects in the response body;
18 | //
19 | // - POST requests are expected to InterruptUrl to send interrupts to GDB, no
20 | // parameters are allowed.
21 | //
22 | // HTTP methods return 200 on success and 500 on error.
23 | package web
24 |
--------------------------------------------------------------------------------
/interrupt_test.go:
--------------------------------------------------------------------------------
1 | package gdb
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestInterrupt(t *testing.T) {
9 | // start gdb
10 | gdb, err := New(nil)
11 | if err != nil {
12 | t.Fatal(err)
13 | }
14 |
15 | // load and start gofmt (a program which will wait for input)
16 | _, err = gdb.Send("file-exec-and-symbols", "gofmt")
17 | if err != nil {
18 | t.Fatal(err)
19 | }
20 | _, err = gdb.Send("exec-run")
21 | if err != nil {
22 | t.Fatal(err)
23 | }
24 |
25 | interrupt := false
26 | go func() {
27 | // allow the program to start up
28 | time.Sleep(time.Second)
29 |
30 | // then interrupt the execution
31 | interrupt = true
32 | if err := gdb.Interrupt(); err != nil {
33 | t.Fatal(err)
34 | }
35 | }()
36 |
37 | // try to execute a command
38 | _, err = gdb.Send("gdb-version")
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 | if interrupt == false {
43 | t.Fatal("GDB should wait for the interrupt")
44 | }
45 |
46 | // exit gdb
47 | if err := gdb.Exit(); err != nil {
48 | t.Fatal(err)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022 Andrea Cardaci
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // Package gdb provides a convenient way to interact with the GDB/MI
2 | // interface. The methods offered by this module are very low level, the main
3 | // goals are:
4 | //
5 | // - avoid the tedious parsing of the MI2 line-based text interface;
6 | //
7 | // - bypass a known bug(https://sourceware.org/bugzilla/show_bug.cgi?id=8759)
8 | // which prevents to distinguish the target program's output from MI2 records.
9 | //
10 | // The objects returned as a result of the commands or as asynchronous
11 | // notifications are generic Go maps suitable to be converted to JSON format
12 | // with json.Unmarshal(). The fields present in such objects are blindly added
13 | // according to the records returned from GDB (see
14 | // https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Output-Syntax.html):
15 | // tuples are map[string]interface{} and lists are []interface{}. There are a
16 | // couple of exceptions to this:
17 | //
18 | // - the record class, where present, is represented by the "class" field;
19 | //
20 | // - the record type is represented using the "type" field as follows:
21 | // "+": "status"
22 | // "=": "notify"
23 | // "~": "console"
24 | // "@": "target"
25 | // "&": "log"
26 | //
27 | // - the optional result list is stored into a tuple under the "payload" field.
28 | package gdb
29 |
30 | //go:generate goyacc -o grammar.go grammar.y
31 |
--------------------------------------------------------------------------------
/web/example/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | gdb/web example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | |
22 |
23 |
(load and run a program)
24 |
25 |
(no source available)
26 |
Ready!
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/commands_test.go:
--------------------------------------------------------------------------------
1 | package gdb
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | var consoleRecordCount = 0
9 |
10 | func onNotification(t *testing.T, notification map[string]interface{}) {
11 | if notification["type"] != "console" {
12 | consoleRecordCount++
13 | }
14 | }
15 |
16 | func TestSend(t *testing.T) {
17 | var expected, result map[string]interface{}
18 |
19 | // start gdb
20 | gdb, err := New(func(notification map[string]interface{}) {
21 | onNotification(t, notification)
22 | })
23 | if err != nil {
24 | t.Fatal(err)
25 | }
26 |
27 | // try sending a command
28 | result, err = gdb.Send("gdb-version")
29 | expected = map[string]interface{}{"class": "done"}
30 | if err != nil {
31 | t.Fatal(err)
32 | }
33 | if !reflect.DeepEqual(result, expected) {
34 | t.Fatal(result, "!=", expected)
35 | }
36 |
37 | // gdb-version should generate some console records
38 | if consoleRecordCount == 0 {
39 | t.Fatal("no console records received")
40 | }
41 |
42 | // try sending a command with an argument containing a space
43 | result, err = gdb.Send("var-create", "foo", "@", "40 + 2")
44 | if err != nil {
45 | t.Fatal(err)
46 | }
47 | if class, hasClass := result["class"]; !hasClass || class != "done" {
48 | t.Fatal(result, "has not class 'done'")
49 | }
50 |
51 | // try sending a wrong command
52 | result, err = gdb.Send("foobarbaz")
53 | if err != nil {
54 | t.Fatal(err)
55 | }
56 | if class, hasClass := result["class"]; !hasClass || class != "error" {
57 | t.Fatal(result, "has not class 'error'")
58 | }
59 |
60 | // exit gdb
61 | if err := gdb.Exit(); err != nil {
62 | t.Fatal(err)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/io_test.go:
--------------------------------------------------------------------------------
1 | package gdb
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestIO(t *testing.T) {
9 | input := "package foo"
10 | expectedIn := fmt.Sprintf("%s\r\n", input) // not sure why \r\n...
11 | expectedOut := "package foo\r\n"
12 |
13 | // start gdb
14 | gdb, err := New(nil)
15 | if err != nil {
16 | t.Fatal(err)
17 | }
18 |
19 | // start processing the output
20 | done := make(chan bool)
21 | go func() {
22 | var n int
23 | buf := make([]byte, 1024)
24 |
25 | // read the first line
26 | n, err = gdb.Read(buf)
27 | if err != nil {
28 | t.Fatal(err)
29 | }
30 | if string(buf[:n]) != expectedIn {
31 | fmt.Printf("'%s'\n", buf[:n])
32 | fmt.Printf("'%s'\n", []byte(expectedIn))
33 | t.Fatal("should read back the input")
34 | }
35 |
36 | // read the second line
37 | n, err = gdb.Read(buf)
38 | if err != nil {
39 | t.Fatal(err)
40 | }
41 | if string(buf[:n]) != expectedOut {
42 | fmt.Printf("'%s'\n", buf[:n])
43 | fmt.Printf("'%s'\n", []byte(expectedOut))
44 | t.Fatal("should read the proper output")
45 | }
46 |
47 | // try another read
48 | n, err = gdb.Read(buf)
49 | if err == nil {
50 | t.Fatal("read should fail")
51 | }
52 |
53 | done <- true
54 | }()
55 |
56 | // load a program
57 | if _, err = gdb.Send("file-exec-file", "gofmt"); err != nil {
58 | t.Fatal(err)
59 | }
60 |
61 | // provide some input
62 | if _, err := gdb.Write([]byte(input)); err != nil {
63 | t.Fatal(err)
64 | }
65 | if _, err := gdb.Write([]byte("\n\x04")); err != nil {
66 | t.Fatal(err)
67 | }
68 |
69 | // run the program
70 | if _, err = gdb.Send("exec-run"); err != nil {
71 | t.Fatal(err)
72 | }
73 |
74 | // exit gdb
75 | if err := gdb.Exit(); err != nil {
76 | t.Fatal(err)
77 | }
78 |
79 | // wait for the output processing
80 | _ = <-done
81 | }
82 |
--------------------------------------------------------------------------------
/lexer.go:
--------------------------------------------------------------------------------
1 | package gdb
2 |
3 | type tokenType int
4 | type lexerState int
5 |
6 | const (
7 | normal lexerState = iota
8 | inQuotation
9 | inEscape
10 | )
11 |
12 | type token struct {
13 | tokenType tokenType
14 | value string
15 | }
16 |
17 | type parser struct {
18 | tokens <-chan token
19 | output map[string]interface{}
20 | }
21 |
22 | func lexer(input string) <-chan token { // no checks here...
23 | position := 0
24 | state := normal
25 | tokens := make(chan token)
26 | var value []byte
27 | go func() {
28 | for position < len(input) {
29 | next := input[position]
30 | switch state {
31 | case normal:
32 | switch next {
33 | case '^', '*', '+', '=', '~', '@', '&', ',', '{', '}', '[', ']':
34 | if value != nil {
35 | tokens <- token{tokenType(text), string(value)}
36 | value = nil
37 | }
38 | tokens <- token{tokenType(next), string(next)}
39 | case '"':
40 | state = inQuotation
41 | if value != nil {
42 | tokens <- token{tokenType(text), string(value)}
43 | value = nil
44 | }
45 | default:
46 | value = append(value, next)
47 | }
48 | case inQuotation:
49 | switch next {
50 | case '"':
51 | state = normal
52 | if value != nil {
53 | tokens <- token{tokenType(text), string(value)}
54 | } else {
55 | tokens <- token{tokenType(text), ""}
56 | }
57 | value = nil
58 | case '\\':
59 | state = inEscape
60 | default:
61 | value = append(value, next)
62 | }
63 | case inEscape:
64 | switch next {
65 | case 'a':
66 | next = '\a'
67 | case 'b':
68 | next = '\b'
69 | case 'f':
70 | next = '\f'
71 | case 'n':
72 | next = '\n'
73 | case 'r':
74 | next = '\r'
75 | case 't':
76 | next = '\t'
77 | case 'v':
78 | next = '\v'
79 | case '\\':
80 | next = '\\'
81 | case '\'':
82 | next = '\''
83 | case '"':
84 | next = '"'
85 | }
86 | value = append(value, next)
87 | state = inQuotation
88 | }
89 | position++
90 | }
91 | if value != nil {
92 | tokens <- token{tokenType(text), string(value)}
93 | value = nil
94 | }
95 | close(tokens)
96 | }()
97 | return tokens
98 | }
99 |
100 | func (p *parser) Lex(lval *yySymType) int {
101 | // fetch the next token
102 | token, ok := <-p.tokens
103 | if ok {
104 | // save the value and return the token type
105 | lval.text = token.value
106 | return int(token.tokenType)
107 | } else {
108 | return 0 // no more tokens
109 | }
110 | }
111 |
112 | func (p *parser) Error(err string) {
113 | // errors are GDB bugs if the grammar is correct
114 | panic(err)
115 | }
116 |
117 | func parseRecord(data string) map[string]interface{} {
118 | parser := parser{lexer(data), map[string]interface{}{}}
119 | yyParse(&parser)
120 | return parser.output
121 | }
122 |
--------------------------------------------------------------------------------
/web/handler.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/cyrus-and/gdb"
6 | "golang.org/x/net/websocket"
7 | "io"
8 | "io/ioutil"
9 | "log"
10 | "net/http"
11 | )
12 |
13 | const (
14 | NotificationsUrl = "/notifications"
15 | TerminalUrl = "/terminal"
16 | CommandsUrl = "/send"
17 | InterruptUrl = "/interrupt"
18 | )
19 |
20 | func die(err error, w http.ResponseWriter) {
21 | log.Print("### ", err)
22 | w.WriteHeader(http.StatusInternalServerError)
23 | }
24 |
25 | // NewHandler returns a new http.ServeMux which automatically spawn a new
26 | // instance of GDB and offers HTTP mount points to interact with. It is assumed
27 | // that only one client at a time access this multiplexer.
28 | func NewHandler() (*http.ServeMux, error) {
29 | handler := http.NewServeMux()
30 | notificationsChan := make(chan []byte)
31 |
32 | // start a new GDB instance
33 | gdb, err := gdb.New(func(notification map[string]interface{}) {
34 | // notifications are converted to JSON and sent through the channel
35 | notificationText, err := json.Marshal(notification)
36 | if err != nil {
37 | log.Fatal(err)
38 | }
39 | notificationsChan <- notificationText
40 | log.Print(">>> ", string(notificationText))
41 | })
42 | if err != nil {
43 | return nil, err
44 | }
45 |
46 | // notifications WebSocket
47 | handler.Handle(NotificationsUrl, websocket.Handler(func(ws *websocket.Conn) {
48 | // deliver the notifications through the WebSocket
49 | for notification := range notificationsChan {
50 | ws.Write(notification)
51 | }
52 | }))
53 |
54 | // terminal WebSocket
55 | handler.Handle(TerminalUrl, websocket.Handler(func(ws *websocket.Conn) {
56 | // copy GDB to WS and WS to GDB
57 | go io.Copy(gdb, ws)
58 | io.Copy(ws, gdb)
59 | }))
60 |
61 | // send command action
62 | handler.HandleFunc(CommandsUrl, func(w http.ResponseWriter, req *http.Request) {
63 | if req.Method != "POST" {
64 | log.Print("### invalid method")
65 | w.WriteHeader(http.StatusInternalServerError)
66 | return
67 | }
68 | data, err := ioutil.ReadAll(req.Body)
69 | if err != nil {
70 | die(err, w)
71 | return
72 | }
73 | log.Print("<<< ", string(data))
74 | command := []string{}
75 | err = json.Unmarshal(data, &command)
76 | if err != nil {
77 | die(err, w)
78 | return
79 | }
80 | result, err := gdb.Send(command[0], command[1:]...)
81 | if err != nil {
82 | die(err, w)
83 | return
84 | }
85 | reply, err := json.Marshal(result)
86 | if err != nil {
87 | die(err, w)
88 | return
89 | }
90 | io.WriteString(w, string(reply))
91 | log.Print(">>> ", string(reply))
92 | })
93 |
94 | // send interrupt action
95 | handler.HandleFunc(InterruptUrl, func(w http.ResponseWriter, req *http.Request) {
96 | if req.Method != "POST" {
97 | log.Print("### invalid method")
98 | w.WriteHeader(http.StatusInternalServerError)
99 | return
100 | }
101 | if err := gdb.Interrupt(); err != nil {
102 | die(err, w)
103 | return
104 | }
105 | log.Print("<<< interrupt")
106 | })
107 |
108 | return handler, nil
109 | }
110 |
--------------------------------------------------------------------------------
/grammar.y:
--------------------------------------------------------------------------------
1 | // -*- go -*-
2 | %{
3 | package gdb
4 |
5 | const (
6 | terminator = "(gdb) " // yes there's the trailing space
7 | typeKey = "type"
8 | classKey = "class"
9 | payloadKey = "payload"
10 | sequenceKey = "sequence"
11 | )
12 |
13 | // avoid DRY due to a poor lexer
14 | func newClassResult(typeString, class string, payload map[string]interface{}) map[string]interface{} {
15 | out := map[string]interface{}{
16 | typeKey: typeString,
17 | classKey: class,
18 | }
19 | if payload != nil {
20 | out[payloadKey] = payload
21 | }
22 | return out
23 | }
24 | %}
25 |
26 | %union{
27 | text string
28 | record map[string]interface{}
29 | class_result struct{class string; payload map[string]interface{}}
30 | result_pair struct{variable string; value interface{}}
31 | value interface{}
32 | list []interface{}
33 | }
34 |
35 | %token text
36 |
37 | %type text token
38 | %type result_list record async_record stream_record result_record tuple
39 | %type class_result
40 | %type result
41 | %type value
42 | %type list value_list tuple_list
43 |
44 | %%
45 |
46 | all:
47 | record { yylex.(*parser).output = $1 };
48 |
49 | record:
50 | async_record |
51 | stream_record |
52 | result_record;
53 |
54 | async_record:
55 | token '*' class_result { $$ = newClassResult("exec", $3.class, $3.payload) } |
56 | token '+' class_result { $$ = newClassResult("status", $3.class, $3.payload) } |
57 | token '+' text ',' tuple { $$ = newClassResult("status", $3, $5) } |
58 | token '=' class_result { $$ = newClassResult("notify", $3.class, $3.payload) };
59 |
60 | class_result:
61 | text ',' result_list { $$.class, $$.payload = $1, $3 } |
62 | text { $$.class, $$.payload = $1, nil };
63 |
64 | stream_record:
65 | '~' text { $$ = map[string]interface{}{typeKey: "console", payloadKey: $2} } |
66 | '@' text { $$ = map[string]interface{}{typeKey: "target", payloadKey: $2} } |
67 | '&' text { $$ = map[string]interface{}{typeKey: "log", payloadKey: $2} };
68 |
69 | result_record:
70 | token '^' class_result
71 | {
72 | $$ = map[string]interface{}{sequenceKey: $1, classKey: $3.class}
73 | if $3.payload != nil { $$[payloadKey] = $3.payload }
74 | };
75 |
76 | result_list:
77 | result_list ',' result { $$[$3.variable] = $3.value } |
78 | result { $$ = map[string]interface{}{$1.variable: $1.value} };
79 |
80 | token:
81 | { $$ = "" } |
82 | text { $$ = $1 }
83 |
84 | result:
85 | text '=' value { $$.variable, $$.value = $1, $3 };
86 |
87 | value:
88 | text { $$ = $1 } |
89 | tuple { $$ = $1 } |
90 | list { $$ = $1 };
91 |
92 | value_list:
93 | value_list ',' value { $$ = append($$, $3) } |
94 | value { $$ = []interface{}{$1} };
95 |
96 | tuple:
97 | '{' result_list '}' { $$ = $2 } |
98 | '{' '}' { $$ = map[string]interface{}{} };
99 |
100 | tuple_list:
101 | tuple_list ',' result { $$ = append($$, map[string]interface{}{$3.variable: $3.value}) } |
102 | result { $$ = []interface{}{map[string]interface{}{$1.variable: $1.value} } };
103 |
104 | list:
105 | '[' value_list ']' { $$ = $2 } |
106 | '[' tuple_list ']' { $$ = $2 } |
107 | '[' ']' { $$ = []interface{}{} };
108 |
--------------------------------------------------------------------------------
/parser_test.go:
--------------------------------------------------------------------------------
1 | package gdb
2 |
3 | import (
4 | "encoding/json"
5 | "reflect"
6 | "testing"
7 | )
8 |
9 | func checkParseRecord(t *testing.T, input, expected string) {
10 | inputObj := parseRecord(input)
11 | expectedObj := map[string]interface{}{}
12 | if err := json.Unmarshal([]byte(expected), &expectedObj); err != nil {
13 | t.Fatal(err)
14 | }
15 | if !reflect.DeepEqual(inputObj, expectedObj) {
16 | t.Fatal(inputObj, "!=", expectedObj)
17 | }
18 | }
19 |
20 | func TestParseRecord(t *testing.T) {
21 | // async record
22 | checkParseRecord(t, `*foo`, `{"type":"exec","class":"foo"}`)
23 | checkParseRecord(t, `+bar`, `{"type":"status","class":"bar"}`)
24 | checkParseRecord(t, `=baz`, `{"type":"notify","class":"baz"}`)
25 | checkParseRecord(t, `*foo,a="x"`, `{"type":"exec","class":"foo","payload":{"a":"x"}}`)
26 | checkParseRecord(t, `*foo,a="x",b="y"`, `{"type":"exec","class":"foo","payload":{"a":"x","b":"y"}}`)
27 | checkParseRecord(t, `+bar,a="x"`, `{"type":"status","class":"bar","payload":{"a":"x"}}`)
28 | checkParseRecord(t, `+bar,a="x",b="y"`, `{"type":"status","class":"bar","payload":{"a":"x","b":"y"}}`)
29 | checkParseRecord(t, `=baz,a="x"`, `{"type":"notify","class":"baz","payload":{"a":"x"}}`)
30 | checkParseRecord(t, `=baz,a="x",b="y"`, `{"type":"notify","class":"baz","payload":{"a":"x","b":"y"}}`)
31 |
32 | // stream record
33 | checkParseRecord(t, `~"foo"`, `{"type":"console","payload":"foo"}`)
34 | checkParseRecord(t, `@"bar"`, `{"type":"target","payload":"bar"}`)
35 | checkParseRecord(t, `&"baz"`, `{"type":"log","payload":"baz"}`)
36 |
37 | // result record
38 | checkParseRecord(t, `123^foo`, `{"sequence":"123","class":"foo"}`)
39 | checkParseRecord(t, `456^bar,a="x"`, `{"sequence":"456","class":"bar","payload":{"a":"x"}}`)
40 | checkParseRecord(t, `789^baz,a="x",b="y"`, `{"sequence":"789","class":"baz","payload":{"a":"x","b":"y"}}`)
41 |
42 | // complex record payload
43 | checkParseRecord(t, `000^foo,a={"b"="x","c"="y"}`, `{"sequence":"000","class":"foo","payload":{"a":{"b":"x","c":"y"}}}`)
44 | checkParseRecord(t, `000^foo,a=["b"="x","c"="y"]`, `{"sequence":"000","class":"foo","payload":{"a":[{"b":"x"},{"c":"y"}]}}`)
45 | checkParseRecord(t, `000^foo,a=["x","y"]`, `{"sequence":"000","class":"foo","payload":{"a":["x","y"]}}`)
46 | checkParseRecord(t, `000^foo,a=[[["x"]]]`, `{"sequence":"000","class":"foo","payload":{"a":[[["x"]]]}}`)
47 | checkParseRecord(t, `000^foo,a={b={c={x="y"}}}`, `{"sequence":"000","class":"foo","payload":{"a":{"b":{"c":{"x":"y"}}}}}`)
48 | checkParseRecord(t, `000^foo,a={}`, `{"sequence":"000","class":"foo","payload":{"a":{}}}`)
49 | checkParseRecord(t, `000^foo,a=[]`, `{"sequence":"000","class":"foo","payload":{"a":[]}}`)
50 |
51 | // escape sequences and strings in general
52 | checkParseRecord(t, `~""`, `{"type":"console","payload":""}`)
53 | checkParseRecord(t, `~"\b\f\n\r\t\""`, `{"type":"console","payload":"\b\f\n\r\t\""}`)
54 | checkParseRecord(t, `@""`, `{"type":"target","payload":""}`)
55 | checkParseRecord(t, `@"\b\f\n\r\t\""`, `{"type":"target","payload":"\b\f\n\r\t\""}`)
56 | checkParseRecord(t, `&""`, `{"type":"log","payload":""}`)
57 | checkParseRecord(t, `&"\b\f\n\r\t\""`, `{"type":"log","payload":"\b\f\n\r\t\""}`)
58 | checkParseRecord(t, `000^foo,a=""`, `{"sequence":"000","class":"foo","payload":{"a":""}}`)
59 | checkParseRecord(t, `000^bar,a="\b\f\n\r\t\""`, `{"sequence":"000","class":"bar","payload":{"a":"\b\f\n\r\t\""}}`)
60 | }
61 |
--------------------------------------------------------------------------------
/commands.go:
--------------------------------------------------------------------------------
1 | package gdb
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "errors"
7 | "fmt"
8 | "strconv"
9 | "strings"
10 | )
11 |
12 | // NotificationCallback is a callback used to report the notifications that GDB
13 | // send asynchronously through the MI2 interface. Responses to the commands are
14 | // not part of these notifications. The notification generic object contains the
15 | // notification sent by GDB.
16 | type NotificationCallback func(notification map[string]interface{})
17 |
18 | // Send issues a command to GDB. Operation is the name of the MI2 command to
19 | // execute without the leading "-" (this means that it is impossible send a CLI
20 | // command), arguments is an optional list of arguments, in GDB parlance the can
21 | // be: options, parameters or "--". It returns a generic object which represents
22 | // the reply of GDB or an error in case the command cannot be delivered to GDB.
23 | func (gdb *Gdb) Send(operation string, arguments ...string) (map[string]interface{}, error) {
24 | // atomically increase the sequence number and queue a pending command
25 | pending := make(chan map[string]interface{})
26 | gdb.mutex.Lock()
27 | sequence := strconv.FormatInt(gdb.sequence, 10)
28 | gdb.pending[sequence] = pending
29 | gdb.sequence++
30 | gdb.mutex.Unlock()
31 |
32 | // prepare the command
33 | buffer := bytes.NewBufferString(fmt.Sprintf("%s-%s", sequence, operation))
34 | for _, argument := range arguments {
35 | buffer.WriteByte(' ')
36 | // quote the argument only if needed because GDB interprets un/quoted
37 | // values differently in some contexts, e.g., when the value is a
38 | // number('1' vs '"1"') or an option ('--thread' vs '"--thread"')
39 | if strings.ContainsAny(argument, "\a\b\f\n\r\t\v\\'\" ") {
40 | argument = strconv.Quote(argument)
41 | }
42 | buffer.WriteString(argument)
43 | }
44 | buffer.WriteByte('\n')
45 |
46 | // send the command
47 | if _, err := gdb.stdin.Write(buffer.Bytes()); err != nil {
48 | return nil, err
49 | }
50 |
51 | // wait for a response
52 | result := <-pending
53 | gdb.mutex.Lock()
54 | delete(gdb.pending, sequence)
55 | gdb.mutex.Unlock()
56 | return result, nil
57 | }
58 |
59 | // CheckedSend works like Send, except that if the result returned by
60 | // gdb has class=error, CheckedSend returns a non-nil error value
61 | // (containing the gdb error message)
62 | func (gdb *Gdb) CheckedSend(operation string, arguments ...string) (map[string]interface{}, error) {
63 | result, err := gdb.Send(operation, arguments...)
64 |
65 | if err != nil {
66 | return nil, err
67 | }
68 |
69 | if result["class"] == "error" {
70 | if payload, isMap := result["payload"].(map[string]interface{}); isMap {
71 | if msg, isString := payload["msg"].(string); isString {
72 | return nil, errors.New(msg)
73 | }
74 | }
75 | // Class is error, but no message? Stringify the entire
76 | // result as the error message then
77 | return nil, errors.New("Unknown gdb error: " + fmt.Sprint(result["payload"]))
78 | }
79 |
80 | return result, nil
81 | }
82 |
83 | func (gdb *Gdb) recordReader() {
84 | scanner := bufio.NewScanner(gdb.stdout)
85 | for scanner.Scan() {
86 | // scan the GDB output one line at a time skipping the GDB terminator
87 | line := scanner.Text()
88 | if line == terminator {
89 | continue
90 | }
91 |
92 | // parse the line and distinguish between command result and
93 | // notification
94 | record := parseRecord(line)
95 | sequence, isResult := record[sequenceKey]
96 | if isResult {
97 | // if it is a result record remove the sequence field and complete
98 | // the pending command
99 | delete(record, sequenceKey)
100 | gdb.mutex.RLock()
101 | pending := gdb.pending[sequence.(string)]
102 | gdb.mutex.RUnlock()
103 | pending <- record
104 | } else {
105 | if gdb.onNotification != nil {
106 | gdb.onNotification(record)
107 | }
108 | }
109 | }
110 | if err := scanner.Err(); err != nil {
111 | panic(err)
112 | }
113 | gdb.recordReaderDone <- true
114 | }
115 |
--------------------------------------------------------------------------------
/gdb.go:
--------------------------------------------------------------------------------
1 | package gdb
2 |
3 | import (
4 | "github.com/kr/pty"
5 | "io"
6 | "os"
7 | "os/exec"
8 | "sync"
9 | )
10 |
11 | // Gdb represents a GDB instance. It implements the ReadWriter interface to
12 | // read/write data from/to the target program's TTY.
13 | type Gdb struct {
14 | io.ReadWriter
15 |
16 | ptm *os.File
17 | pts *os.File
18 | cmd *exec.Cmd
19 |
20 | mutex sync.RWMutex
21 | stdin io.WriteCloser
22 | stdout io.ReadCloser
23 |
24 | sequence int64
25 | pending map[string]chan map[string]interface{}
26 |
27 | onNotification NotificationCallback
28 |
29 | recordReaderDone chan bool
30 | }
31 |
32 | // New creates and starts a new GDB instance. onNotification if not nil is the
33 | // callback used to deliver to the client the asynchronous notifications sent by
34 | // GDB. It returns a pointer to the newly created instance handled or an error.
35 | func New(onNotification NotificationCallback) (*Gdb, error) {
36 | return NewCustom([]string{"gdb"}, onNotification)
37 | }
38 |
39 | // Like New, but allows to specify the GDB executable path and arguments.
40 | func NewCustom(gdbCmd []string, onNotification NotificationCallback) (*Gdb, error) {
41 | // open a new terminal (master and slave) for the target program, they are
42 | // both saved so that they are nore garbage collected after this function
43 | // ends
44 | ptm, pts, err := pty.Open()
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | // create GDB command
50 | gdbCmd = append(gdbCmd, "--nx", "--quiet", "--interpreter=mi2", "--tty", pts.Name())
51 | gdb, err := NewCmd(gdbCmd, onNotification)
52 |
53 | if err != nil {
54 | ptm.Close()
55 | pts.Close()
56 | return nil, err
57 | }
58 |
59 | gdb.ptm = ptm
60 | gdb.pts = pts
61 |
62 | return gdb, nil
63 | }
64 |
65 | // NewCmd creates a new GDB instance like New, but allows explicitely passing
66 | // the gdb command to run (including all arguments). cmd is passed as-is to
67 | // exec.Command, so the first element should be the command to run, and the
68 | // remaining elements should each contain a single argument.
69 | func NewCmd(cmd []string, onNotification NotificationCallback) (*Gdb, error) {
70 | gdb := Gdb{onNotification: onNotification}
71 |
72 | gdb.cmd = exec.Command(cmd[0], cmd[1:]...)
73 |
74 | // GDB standard input
75 | stdin, err := gdb.cmd.StdinPipe()
76 | if err != nil {
77 | return nil, err
78 | }
79 | gdb.stdin = stdin
80 |
81 | // GDB standard ouput
82 | stdout, err := gdb.cmd.StdoutPipe()
83 | if err != nil {
84 | return nil, err
85 | }
86 | gdb.stdout = stdout
87 |
88 | // start GDB
89 | if err := gdb.cmd.Start(); err != nil {
90 | return nil, err
91 | }
92 |
93 | // prepare the command interface
94 | gdb.sequence = 1
95 | gdb.pending = make(map[string]chan map[string]interface{})
96 |
97 | gdb.recordReaderDone = make(chan bool)
98 |
99 | // start the line reader
100 | go gdb.recordReader()
101 |
102 | return &gdb, nil
103 | }
104 |
105 | // Read reads a number of bytes from the target program's output.
106 | func (gdb *Gdb) Read(p []byte) (n int, err error) {
107 | return gdb.ptm.Read(p)
108 | }
109 |
110 | // Write writes a number of bytes to the target program's input.
111 | func (gdb *Gdb) Write(p []byte) (n int, err error) {
112 | return gdb.ptm.Write(p)
113 | }
114 |
115 | // Interrupt sends a signal (SIGINT) to GDB so it can stop the target program
116 | // and resume the processing of commands.
117 | func (gdb *Gdb) Interrupt() error {
118 | return gdb.cmd.Process.Signal(os.Interrupt)
119 | }
120 |
121 | // Exit sends the exit command to GDB and waits for the process to exit.
122 | func (gdb *Gdb) Exit() error {
123 | // send the exit command and wait for the GDB process
124 | if _, err := gdb.Send("gdb-exit"); err != nil {
125 | return err
126 | }
127 |
128 | // Wait for the recordReader to finish (due to EOF on the pipe).
129 | // If we do not wait for this, Wait() might clean up stdout
130 | // while recordReader is still using it, leading to
131 | // panic: read |0: bad file descriptor
132 | <-gdb.recordReaderDone
133 |
134 | if err := gdb.cmd.Wait(); err != nil {
135 | return err
136 | }
137 |
138 | // close the target program's terminal, since the lifetime of the terminal
139 | // is longer that the one of the targer program's instances reading from a
140 | // Gdb object ,i.e., the master side will never return EOF (at least on
141 | // Linux) so the only way to stop reading is to intercept the I/O error
142 | // caused by closing the terminal
143 | if gdb.ptm != nil {
144 | if err := gdb.ptm.Close(); err != nil {
145 | return err
146 | }
147 | }
148 | if gdb.pts != nil {
149 | if err := gdb.pts.Close(); err != nil {
150 | return err
151 | }
152 | }
153 |
154 | return nil
155 | }
156 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gdb
2 |
3 | Package `gdb` provides a convenient way to interact with the GDB/MI interface. The methods offered by this module are very low level, the main goals are:
4 |
5 | - avoid the tedious parsing of the MI2 line-based text interface;
6 |
7 | - bypass a [known bug][mi2-bug] which prevents to distinguish the target program's output from MI2 records.
8 |
9 | ## Web interface
10 |
11 | This package comes with an additional [HTTP/WebSocket interface](web/) which aims to provide a straightforward way to start developing web-based GDB front ends.
12 |
13 | A dummy example can be found in the [example folder](web/example).
14 |
15 | ## Example
16 |
17 | ```go
18 | package main
19 |
20 | import (
21 | "fmt"
22 | "github.com/cyrus-and/gdb"
23 | "io"
24 | "os"
25 | )
26 |
27 | func main() {
28 | // start a new instance and pipe the target output to stdout
29 | gdb, _ := gdb.New(nil)
30 | go io.Copy(os.Stdout, gdb)
31 |
32 | // evaluate an expression
33 | gdb.Send("var-create", "x", "@", "40 + 2")
34 | fmt.Println(gdb.Send("var-evaluate-expression", "x"))
35 |
36 | // load and run a program
37 | gdb.Send("file-exec-file", "wc")
38 | gdb.Send("exec-arguments", "-w")
39 | gdb.Write([]byte("This sentence has five words.\n\x04")) // EOT
40 | gdb.Send("exec-run")
41 |
42 | gdb.Exit()
43 | }
44 | ```
45 |
46 | ## Installation
47 |
48 | ```
49 | go get -u github.com/cyrus-and/gdb
50 | ```
51 |
52 | ## Documentation
53 |
54 | [GoDoc][godoc].
55 |
56 | ## Data representation
57 |
58 | The objects returned as a result of the commands or as asynchronous notifications are generic Go maps suitable to be converted to JSON format with `json.Marshal()`. The fields present in such objects are blindly added according to the records returned from GDB (see the [command syntax][mi2-syntax]): tuples are `map[string]interface{}` and lists are `[]interface{}`.
59 |
60 | Yet, some additional fields are added:
61 |
62 | - the record class, where present, is represented by the `"class"` field;
63 |
64 | - the record type is represented using the `"type"` field as follows:
65 | - `+`: `"status"`
66 | - `=`: `"notify"`
67 | - `~`: `"console"`
68 | - `@`: `"target"`
69 | - `&`: `"log"`
70 |
71 | - the optional result list is stored into a tuple under the `"payload"` field.
72 |
73 | For example, the notification:
74 |
75 | ```
76 | =thread-group-exited,id="i1",exit-code="0"
77 | ```
78 |
79 | becomes the Go map:
80 |
81 | ```go
82 | map[type:notify class:thread-group-exited payload:map[id:i1 exit-code:0]]
83 | ```
84 |
85 | which can be converted to JSON with `json.Marshal()` obtaining:
86 |
87 | ```json
88 | {
89 | "class": "thread-group-exited",
90 | "payload": {
91 | "exit-code": "0",
92 | "id": "i1"
93 | },
94 | "type": "notify"
95 | }
96 | ```
97 |
98 | ## Mac OS X
99 |
100 | ### Setting up GDB on Darwin
101 |
102 | To use this module is mandatory to have a working version of GDB installed, Mac OS X users may obtain a copy using [Homebrew][homebrew] for example, then they may need to give GDB permission to control other processes as described [here][gdb-on-mac].
103 |
104 | ### Issues
105 |
106 | The Mac OS X support, though, is partial and buggy due to the following issues.
107 |
108 | #### Pseudoterminals
109 |
110 | I/O operations on the target program happens through a pseudoterminal obtained using the [pty][pty] package which basically uses the `/dev/ptmx` on *nix systems to request new terminal instances.
111 |
112 | There are some unclear behaviors on Mac OS X. Calling `gdb.Write()` when the target program is not running is a no-op, on Linux instead writes are somewhat buffered and delivered later. Likewise, `gdb.Read()` may returns EOF even though there is actually data to read, a solution may be keep trying.
113 |
114 | #### Interrupt
115 |
116 | Sending a `SIGINT` signal to GDB has no effect on Mac OS X, on Linux instead this is equivalent to typing `^C`, so `gdb.Interrupt()` will not work.
117 |
118 | ## Development
119 |
120 | The [goyacc](https://pkg.go.dev/golang.org/x/tools/cmd/goyacc) tool is needed to generate the `grammar.go` file. Install it with:
121 |
122 | ```
123 | go install golang.org/x/tools/cmd/goyacc@latest
124 | ```
125 |
126 | After that use the following to update the `grammar.go` file:
127 |
128 | ```
129 | go generate -x
130 | ```
131 |
132 | ## Resources
133 |
134 | - [The `GDB/MI` Interface][gdb-mi]
135 |
136 | [mi2-bug]: https://sourceware.org/bugzilla/show_bug.cgi?id=8759
137 | [mi2-syntax]: https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Output-Syntax.html
138 | [godoc]: https://godoc.org/github.com/cyrus-and/gdb
139 | [homebrew]: http://brew.sh/
140 | [gdb-on-mac]: http://sourceware.org/gdb/wiki/BuildingOnDarwin
141 | [pty]: https://github.com/kr/pty
142 | [gdb-mi]: https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI.html
143 |
--------------------------------------------------------------------------------
/web/example/static/script.js:
--------------------------------------------------------------------------------
1 | function Client(host, onNotification, onOutput) {
2 | var notificationWs = new WebSocket('ws://' + host + '/notifications');
3 | var terminalWs = new WebSocket('ws://' + host + '/terminal');
4 |
5 | notificationWs.onmessage = function (event) {
6 | var object = JSON.parse(event.data);
7 | onNotification(object);
8 | };
9 |
10 | terminalWs.onmessage = function (event) {
11 | onOutput(event.data);
12 | };
13 |
14 | this.input = function (data) {
15 | terminalWs.send(data);
16 | };
17 |
18 | this.command = function (onReply) {
19 | var command = Array.prototype.slice.call(arguments).slice(1);
20 | var ajax = new XMLHttpRequest();
21 | ajax.open('POST', 'http://' + host + '/send');
22 | ajax.onreadystatechange = function () {
23 | if (ajax.readyState === 4 && ajax.status === 200){
24 | var object = JSON.parse(ajax.responseText);
25 | onReply(object);
26 | }
27 | };
28 | ajax.send(JSON.stringify(command));
29 | };
30 |
31 | this.interrupt = function (onReply) {
32 | var ajax = new XMLHttpRequest();
33 | ajax.open('POST', 'http://' + host + '/interrupt');
34 | ajax.onreadystatechange = function () {
35 | if (ajax.readyState === 4 && ajax.status === 200){
36 | onReply && onReply();
37 | }
38 | };
39 | ajax.send(null);
40 | };
41 | }
42 |
43 | ///
44 |
45 | var host = 'localhost:8080';
46 |
47 | function bind(id, action) {
48 | var elem = document.getElementById(id);
49 | elem.addEventListener('click', action);
50 | }
51 |
52 | function onNotification(object) {
53 | console.log(JSON.stringify(object, null, 4));
54 | if (object.type === 'exec') {
55 | var status = object.class;
56 | if (status === 'stopped') {
57 | status += ', ' + object.payload.reason;
58 | if (typeof object.payload.frame !== 'undefined') {
59 | if (typeof object.payload.frame.func !== 'undefined') {
60 | status += ', ' + object.payload.frame.func;
61 | }
62 | if (typeof object.payload.frame.file !== 'undefined') {
63 | status += ' ' + object.payload.frame.file;
64 | }
65 | if (typeof object.payload.frame.line !== 'undefined') {
66 | status += ':' + object.payload.frame.line;
67 | }
68 | if (typeof object.payload.frame.fullname !== 'undefined') {
69 | var filePath = object.payload.frame.fullname;
70 | var ajax = new XMLHttpRequest();
71 | ajax.open('POST', 'http://' + host + '/source' + filePath);
72 | ajax.onreadystatechange = function () {
73 | if (ajax.readyState === 4 && ajax.status == 200){
74 | var container = document.getElementById('source');
75 | container.innerHTML = '';
76 | var source = ajax.responseText;
77 | var lines = source.split('\n');
78 | for (var i = 0; i < lines.length; i++) {
79 | var line = document.createElement('span');
80 | line.id = i + 1;
81 | line.appendChild(document.createTextNode(lines[i] || ' '));
82 | container.appendChild(line);
83 | }
84 | var lineNumber = object.payload.frame.line;
85 | document.getElementById(lineNumber).className = 'selected';
86 | }
87 | };
88 | ajax.send(null);
89 | }
90 | }
91 | }
92 | document.getElementById('status').innerHTML = status;
93 | }
94 | }
95 |
96 | function onOutput(data) {
97 | document.getElementById('output').innerHTML += data;
98 | }
99 |
100 | var client = new Client(host, onNotification, onOutput);
101 |
102 | function dumpResult(object) {
103 | console.log(JSON.stringify(object, null, 4));
104 | var result = document.getElementById('result');
105 | if (object.class === 'error') {
106 | result.innerHTML = object.payload.msg;
107 | } else {
108 | result.innerHTML = 'Done';
109 | }
110 | }
111 |
112 | bind('load', function () {
113 | var fileName = document.getElementById('file').value;
114 | client.command(dumpResult, 'file-exec-and-symbols', fileName);
115 | });
116 |
117 | bind('run', function () {
118 | client.interrupt();
119 | document.getElementById('output').innerHTML = '';
120 | var argText = document.getElementById('arguments').value.trim();
121 | if (argText !== '') {
122 | var args = argText.split(/ +/);
123 | client.command.apply(null, [dumpResult, 'exec-arguments'].concat(args));
124 | }
125 | client.command(dumpResult, 'exec-run', '--start');
126 | });
127 |
128 | bind('continue', function () {
129 | client.command(dumpResult, 'exec-continue');
130 | });
131 |
132 | bind('step', function () {
133 | client.command(dumpResult, 'exec-next');
134 | });
135 |
136 | bind('interrupt', function () {
137 | client.interrupt();
138 | });
139 |
140 | bind('send', function () {
141 | var line = document.getElementById('line').value;
142 | client.input(line + '\n');
143 | });
144 |
145 | bind('eot', function () {
146 | client.input('\x04');
147 | });
148 |
--------------------------------------------------------------------------------
/grammar.go:
--------------------------------------------------------------------------------
1 | // Code generated by goyacc -o grammar.go grammar.y. DO NOT EDIT.
2 |
3 | //line grammar.y:3
4 | package gdb
5 |
6 | import __yyfmt__ "fmt"
7 |
8 | //line grammar.y:3
9 |
10 | const (
11 | terminator = "(gdb) " // yes there's the trailing space
12 | typeKey = "type"
13 | classKey = "class"
14 | payloadKey = "payload"
15 | sequenceKey = "sequence"
16 | )
17 |
18 | // avoid DRY due to a poor lexer
19 | func newClassResult(typeString, class string, payload map[string]interface{}) map[string]interface{} {
20 | out := map[string]interface{}{
21 | typeKey: typeString,
22 | classKey: class,
23 | }
24 | if payload != nil {
25 | out[payloadKey] = payload
26 | }
27 | return out
28 | }
29 |
30 | //line grammar.y:26
31 | type yySymType struct {
32 | yys int
33 | text string
34 | record map[string]interface{}
35 | class_result struct {
36 | class string
37 | payload map[string]interface{}
38 | }
39 | result_pair struct {
40 | variable string
41 | value interface{}
42 | }
43 | value interface{}
44 | list []interface{}
45 | }
46 |
47 | const text = 57346
48 |
49 | var yyToknames = [...]string{
50 | "$end",
51 | "error",
52 | "$unk",
53 | "text",
54 | "'*'",
55 | "'+'",
56 | "','",
57 | "'='",
58 | "'~'",
59 | "'@'",
60 | "'&'",
61 | "'^'",
62 | "'{'",
63 | "'}'",
64 | "'['",
65 | "']'",
66 | }
67 |
68 | var yyStatenames = [...]string{}
69 |
70 | const yyEofCode = 1
71 | const yyErrCode = 2
72 | const yyInitialStackSize = 16
73 |
74 | //line yacctab:1
75 | var yyExca = [...]int8{
76 | -1, 1,
77 | 1, -1,
78 | -2, 0,
79 | }
80 |
81 | const yyPrivate = 57344
82 |
83 | const yyLast = 59
84 |
85 | var yyAct = [...]int8{
86 | 27, 36, 26, 38, 47, 50, 31, 18, 37, 48,
87 | 28, 32, 25, 30, 51, 40, 44, 30, 49, 40,
88 | 20, 22, 23, 31, 28, 24, 28, 19, 21, 29,
89 | 41, 17, 35, 33, 34, 30, 11, 12, 16, 13,
90 | 10, 46, 45, 14, 15, 7, 8, 9, 1, 43,
91 | 52, 53, 42, 39, 5, 4, 3, 2, 6,
92 | }
93 |
94 | var yyPact = [...]int16{
95 | 36, -1000, -1000, -1000, -1000, -1000, 31, 40, 34, 27,
96 | -1000, 23, 24, 23, 23, -1000, -1000, -1000, -1000, 18,
97 | -1000, 5, -1000, -1000, 6, 22, -1, -1000, 3, -1000,
98 | 20, 6, 4, 16, -1000, -1000, -1000, -1000, -1000, -1000,
99 | 0, -1000, 2, -2, -1000, -1000, -1000, 3, 4, -1000,
100 | 6, -1000, -1000, -1000,
101 | }
102 |
103 | var yyPgo = [...]int8{
104 | 0, 58, 2, 57, 56, 55, 54, 3, 7, 0,
105 | 1, 53, 52, 49, 48,
106 | }
107 |
108 | var yyR1 = [...]int8{
109 | 0, 14, 3, 3, 3, 4, 4, 4, 4, 8,
110 | 8, 5, 5, 5, 6, 2, 2, 1, 1, 9,
111 | 10, 10, 10, 12, 12, 7, 7, 13, 13, 11,
112 | 11, 11,
113 | }
114 |
115 | var yyR2 = [...]int8{
116 | 0, 1, 1, 1, 1, 3, 3, 5, 3, 3,
117 | 1, 2, 2, 2, 3, 3, 1, 0, 1, 3,
118 | 1, 1, 1, 3, 1, 3, 2, 3, 1, 3,
119 | 3, 2,
120 | }
121 |
122 | var yyChk = [...]int16{
123 | -1000, -14, -3, -4, -5, -6, -1, 9, 10, 11,
124 | 4, 5, 6, 8, 12, 4, 4, 4, -8, 4,
125 | -8, 4, -8, -8, 7, 7, -2, -9, 4, -7,
126 | 13, 7, 8, -2, 14, -9, -10, 4, -7, -11,
127 | 15, 14, -12, -13, 16, -10, -9, 4, 7, 16,
128 | 7, 16, -10, -9,
129 | }
130 |
131 | var yyDef = [...]int8{
132 | 17, -2, 1, 2, 3, 4, 0, 0, 0, 0,
133 | 18, 0, 0, 0, 0, 11, 12, 13, 5, 10,
134 | 6, 10, 8, 14, 0, 0, 9, 16, 0, 7,
135 | 0, 0, 0, 0, 26, 15, 19, 20, 21, 22,
136 | 0, 25, 0, 0, 31, 24, 28, 20, 0, 29,
137 | 0, 30, 23, 27,
138 | }
139 |
140 | var yyTok1 = [...]int8{
141 | 1, 3, 3, 3, 3, 3, 3, 3, 3, 3,
142 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
143 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
144 | 3, 3, 3, 3, 3, 3, 3, 3, 11, 3,
145 | 3, 3, 5, 6, 7, 3, 3, 3, 3, 3,
146 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
147 | 3, 8, 3, 3, 10, 3, 3, 3, 3, 3,
148 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
149 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
150 | 3, 15, 3, 16, 12, 3, 3, 3, 3, 3,
151 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
152 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
153 | 3, 3, 3, 13, 3, 14, 9,
154 | }
155 |
156 | var yyTok2 = [...]int8{
157 | 2, 3, 4,
158 | }
159 |
160 | var yyTok3 = [...]int8{
161 | 0,
162 | }
163 |
164 | var yyErrorMessages = [...]struct {
165 | state int
166 | token int
167 | msg string
168 | }{}
169 |
170 | //line yaccpar:1
171 |
172 | /* parser for yacc output */
173 |
174 | var (
175 | yyDebug = 0
176 | yyErrorVerbose = false
177 | )
178 |
179 | type yyLexer interface {
180 | Lex(lval *yySymType) int
181 | Error(s string)
182 | }
183 |
184 | type yyParser interface {
185 | Parse(yyLexer) int
186 | Lookahead() int
187 | }
188 |
189 | type yyParserImpl struct {
190 | lval yySymType
191 | stack [yyInitialStackSize]yySymType
192 | char int
193 | }
194 |
195 | func (p *yyParserImpl) Lookahead() int {
196 | return p.char
197 | }
198 |
199 | func yyNewParser() yyParser {
200 | return &yyParserImpl{}
201 | }
202 |
203 | const yyFlag = -1000
204 |
205 | func yyTokname(c int) string {
206 | if c >= 1 && c-1 < len(yyToknames) {
207 | if yyToknames[c-1] != "" {
208 | return yyToknames[c-1]
209 | }
210 | }
211 | return __yyfmt__.Sprintf("tok-%v", c)
212 | }
213 |
214 | func yyStatname(s int) string {
215 | if s >= 0 && s < len(yyStatenames) {
216 | if yyStatenames[s] != "" {
217 | return yyStatenames[s]
218 | }
219 | }
220 | return __yyfmt__.Sprintf("state-%v", s)
221 | }
222 |
223 | func yyErrorMessage(state, lookAhead int) string {
224 | const TOKSTART = 4
225 |
226 | if !yyErrorVerbose {
227 | return "syntax error"
228 | }
229 |
230 | for _, e := range yyErrorMessages {
231 | if e.state == state && e.token == lookAhead {
232 | return "syntax error: " + e.msg
233 | }
234 | }
235 |
236 | res := "syntax error: unexpected " + yyTokname(lookAhead)
237 |
238 | // To match Bison, suggest at most four expected tokens.
239 | expected := make([]int, 0, 4)
240 |
241 | // Look for shiftable tokens.
242 | base := int(yyPact[state])
243 | for tok := TOKSTART; tok-1 < len(yyToknames); tok++ {
244 | if n := base + tok; n >= 0 && n < yyLast && int(yyChk[int(yyAct[n])]) == tok {
245 | if len(expected) == cap(expected) {
246 | return res
247 | }
248 | expected = append(expected, tok)
249 | }
250 | }
251 |
252 | if yyDef[state] == -2 {
253 | i := 0
254 | for yyExca[i] != -1 || int(yyExca[i+1]) != state {
255 | i += 2
256 | }
257 |
258 | // Look for tokens that we accept or reduce.
259 | for i += 2; yyExca[i] >= 0; i += 2 {
260 | tok := int(yyExca[i])
261 | if tok < TOKSTART || yyExca[i+1] == 0 {
262 | continue
263 | }
264 | if len(expected) == cap(expected) {
265 | return res
266 | }
267 | expected = append(expected, tok)
268 | }
269 |
270 | // If the default action is to accept or reduce, give up.
271 | if yyExca[i+1] != 0 {
272 | return res
273 | }
274 | }
275 |
276 | for i, tok := range expected {
277 | if i == 0 {
278 | res += ", expecting "
279 | } else {
280 | res += " or "
281 | }
282 | res += yyTokname(tok)
283 | }
284 | return res
285 | }
286 |
287 | func yylex1(lex yyLexer, lval *yySymType) (char, token int) {
288 | token = 0
289 | char = lex.Lex(lval)
290 | if char <= 0 {
291 | token = int(yyTok1[0])
292 | goto out
293 | }
294 | if char < len(yyTok1) {
295 | token = int(yyTok1[char])
296 | goto out
297 | }
298 | if char >= yyPrivate {
299 | if char < yyPrivate+len(yyTok2) {
300 | token = int(yyTok2[char-yyPrivate])
301 | goto out
302 | }
303 | }
304 | for i := 0; i < len(yyTok3); i += 2 {
305 | token = int(yyTok3[i+0])
306 | if token == char {
307 | token = int(yyTok3[i+1])
308 | goto out
309 | }
310 | }
311 |
312 | out:
313 | if token == 0 {
314 | token = int(yyTok2[1]) /* unknown char */
315 | }
316 | if yyDebug >= 3 {
317 | __yyfmt__.Printf("lex %s(%d)\n", yyTokname(token), uint(char))
318 | }
319 | return char, token
320 | }
321 |
322 | func yyParse(yylex yyLexer) int {
323 | return yyNewParser().Parse(yylex)
324 | }
325 |
326 | func (yyrcvr *yyParserImpl) Parse(yylex yyLexer) int {
327 | var yyn int
328 | var yyVAL yySymType
329 | var yyDollar []yySymType
330 | _ = yyDollar // silence set and not used
331 | yyS := yyrcvr.stack[:]
332 |
333 | Nerrs := 0 /* number of errors */
334 | Errflag := 0 /* error recovery flag */
335 | yystate := 0
336 | yyrcvr.char = -1
337 | yytoken := -1 // yyrcvr.char translated into internal numbering
338 | defer func() {
339 | // Make sure we report no lookahead when not parsing.
340 | yystate = -1
341 | yyrcvr.char = -1
342 | yytoken = -1
343 | }()
344 | yyp := -1
345 | goto yystack
346 |
347 | ret0:
348 | return 0
349 |
350 | ret1:
351 | return 1
352 |
353 | yystack:
354 | /* put a state and value onto the stack */
355 | if yyDebug >= 4 {
356 | __yyfmt__.Printf("char %v in %v\n", yyTokname(yytoken), yyStatname(yystate))
357 | }
358 |
359 | yyp++
360 | if yyp >= len(yyS) {
361 | nyys := make([]yySymType, len(yyS)*2)
362 | copy(nyys, yyS)
363 | yyS = nyys
364 | }
365 | yyS[yyp] = yyVAL
366 | yyS[yyp].yys = yystate
367 |
368 | yynewstate:
369 | yyn = int(yyPact[yystate])
370 | if yyn <= yyFlag {
371 | goto yydefault /* simple state */
372 | }
373 | if yyrcvr.char < 0 {
374 | yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval)
375 | }
376 | yyn += yytoken
377 | if yyn < 0 || yyn >= yyLast {
378 | goto yydefault
379 | }
380 | yyn = int(yyAct[yyn])
381 | if int(yyChk[yyn]) == yytoken { /* valid shift */
382 | yyrcvr.char = -1
383 | yytoken = -1
384 | yyVAL = yyrcvr.lval
385 | yystate = yyn
386 | if Errflag > 0 {
387 | Errflag--
388 | }
389 | goto yystack
390 | }
391 |
392 | yydefault:
393 | /* default state action */
394 | yyn = int(yyDef[yystate])
395 | if yyn == -2 {
396 | if yyrcvr.char < 0 {
397 | yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval)
398 | }
399 |
400 | /* look through exception table */
401 | xi := 0
402 | for {
403 | if yyExca[xi+0] == -1 && int(yyExca[xi+1]) == yystate {
404 | break
405 | }
406 | xi += 2
407 | }
408 | for xi += 2; ; xi += 2 {
409 | yyn = int(yyExca[xi+0])
410 | if yyn < 0 || yyn == yytoken {
411 | break
412 | }
413 | }
414 | yyn = int(yyExca[xi+1])
415 | if yyn < 0 {
416 | goto ret0
417 | }
418 | }
419 | if yyn == 0 {
420 | /* error ... attempt to resume parsing */
421 | switch Errflag {
422 | case 0: /* brand new error */
423 | yylex.Error(yyErrorMessage(yystate, yytoken))
424 | Nerrs++
425 | if yyDebug >= 1 {
426 | __yyfmt__.Printf("%s", yyStatname(yystate))
427 | __yyfmt__.Printf(" saw %s\n", yyTokname(yytoken))
428 | }
429 | fallthrough
430 |
431 | case 1, 2: /* incompletely recovered error ... try again */
432 | Errflag = 3
433 |
434 | /* find a state where "error" is a legal shift action */
435 | for yyp >= 0 {
436 | yyn = int(yyPact[yyS[yyp].yys]) + yyErrCode
437 | if yyn >= 0 && yyn < yyLast {
438 | yystate = int(yyAct[yyn]) /* simulate a shift of "error" */
439 | if int(yyChk[yystate]) == yyErrCode {
440 | goto yystack
441 | }
442 | }
443 |
444 | /* the current p has no shift on "error", pop stack */
445 | if yyDebug >= 2 {
446 | __yyfmt__.Printf("error recovery pops state %d\n", yyS[yyp].yys)
447 | }
448 | yyp--
449 | }
450 | /* there is no state on the stack with an error shift ... abort */
451 | goto ret1
452 |
453 | case 3: /* no shift yet; clobber input char */
454 | if yyDebug >= 2 {
455 | __yyfmt__.Printf("error recovery discards %s\n", yyTokname(yytoken))
456 | }
457 | if yytoken == yyEofCode {
458 | goto ret1
459 | }
460 | yyrcvr.char = -1
461 | yytoken = -1
462 | goto yynewstate /* try again in the same state */
463 | }
464 | }
465 |
466 | /* reduction by production yyn */
467 | if yyDebug >= 2 {
468 | __yyfmt__.Printf("reduce %v in:\n\t%v\n", yyn, yyStatname(yystate))
469 | }
470 |
471 | yynt := yyn
472 | yypt := yyp
473 | _ = yypt // guard against "declared and not used"
474 |
475 | yyp -= int(yyR2[yyn])
476 | // yyp is now the index of $0. Perform the default action. Iff the
477 | // reduced production is ε, $1 is possibly out of range.
478 | if yyp+1 >= len(yyS) {
479 | nyys := make([]yySymType, len(yyS)*2)
480 | copy(nyys, yyS)
481 | yyS = nyys
482 | }
483 | yyVAL = yyS[yyp+1]
484 |
485 | /* consult goto table to find next state */
486 | yyn = int(yyR1[yyn])
487 | yyg := int(yyPgo[yyn])
488 | yyj := yyg + yyS[yyp].yys + 1
489 |
490 | if yyj >= yyLast {
491 | yystate = int(yyAct[yyg])
492 | } else {
493 | yystate = int(yyAct[yyj])
494 | if int(yyChk[yystate]) != -yyn {
495 | yystate = int(yyAct[yyg])
496 | }
497 | }
498 | // dummy call; replaced with literal code
499 | switch yynt {
500 |
501 | case 1:
502 | yyDollar = yyS[yypt-1 : yypt+1]
503 | //line grammar.y:47
504 | {
505 | yylex.(*parser).output = yyDollar[1].record
506 | }
507 | case 5:
508 | yyDollar = yyS[yypt-3 : yypt+1]
509 | //line grammar.y:55
510 | {
511 | yyVAL.record = newClassResult("exec", yyDollar[3].class_result.class, yyDollar[3].class_result.payload)
512 | }
513 | case 6:
514 | yyDollar = yyS[yypt-3 : yypt+1]
515 | //line grammar.y:56
516 | {
517 | yyVAL.record = newClassResult("status", yyDollar[3].class_result.class, yyDollar[3].class_result.payload)
518 | }
519 | case 7:
520 | yyDollar = yyS[yypt-5 : yypt+1]
521 | //line grammar.y:57
522 | {
523 | yyVAL.record = newClassResult("status", yyDollar[3].text, yyDollar[5].record)
524 | }
525 | case 8:
526 | yyDollar = yyS[yypt-3 : yypt+1]
527 | //line grammar.y:58
528 | {
529 | yyVAL.record = newClassResult("notify", yyDollar[3].class_result.class, yyDollar[3].class_result.payload)
530 | }
531 | case 9:
532 | yyDollar = yyS[yypt-3 : yypt+1]
533 | //line grammar.y:61
534 | {
535 | yyVAL.class_result.class, yyVAL.class_result.payload = yyDollar[1].text, yyDollar[3].record
536 | }
537 | case 10:
538 | yyDollar = yyS[yypt-1 : yypt+1]
539 | //line grammar.y:62
540 | {
541 | yyVAL.class_result.class, yyVAL.class_result.payload = yyDollar[1].text, nil
542 | }
543 | case 11:
544 | yyDollar = yyS[yypt-2 : yypt+1]
545 | //line grammar.y:65
546 | {
547 | yyVAL.record = map[string]interface{}{typeKey: "console", payloadKey: yyDollar[2].text}
548 | }
549 | case 12:
550 | yyDollar = yyS[yypt-2 : yypt+1]
551 | //line grammar.y:66
552 | {
553 | yyVAL.record = map[string]interface{}{typeKey: "target", payloadKey: yyDollar[2].text}
554 | }
555 | case 13:
556 | yyDollar = yyS[yypt-2 : yypt+1]
557 | //line grammar.y:67
558 | {
559 | yyVAL.record = map[string]interface{}{typeKey: "log", payloadKey: yyDollar[2].text}
560 | }
561 | case 14:
562 | yyDollar = yyS[yypt-3 : yypt+1]
563 | //line grammar.y:71
564 | {
565 | yyVAL.record = map[string]interface{}{sequenceKey: yyDollar[1].text, classKey: yyDollar[3].class_result.class}
566 | if yyDollar[3].class_result.payload != nil {
567 | yyVAL.record[payloadKey] = yyDollar[3].class_result.payload
568 | }
569 | }
570 | case 15:
571 | yyDollar = yyS[yypt-3 : yypt+1]
572 | //line grammar.y:77
573 | {
574 | yyVAL.record[yyDollar[3].result_pair.variable] = yyDollar[3].result_pair.value
575 | }
576 | case 16:
577 | yyDollar = yyS[yypt-1 : yypt+1]
578 | //line grammar.y:78
579 | {
580 | yyVAL.record = map[string]interface{}{yyDollar[1].result_pair.variable: yyDollar[1].result_pair.value}
581 | }
582 | case 17:
583 | yyDollar = yyS[yypt-0 : yypt+1]
584 | //line grammar.y:81
585 | {
586 | yyVAL.text = ""
587 | }
588 | case 18:
589 | yyDollar = yyS[yypt-1 : yypt+1]
590 | //line grammar.y:82
591 | {
592 | yyVAL.text = yyDollar[1].text
593 | }
594 | case 19:
595 | yyDollar = yyS[yypt-3 : yypt+1]
596 | //line grammar.y:85
597 | {
598 | yyVAL.result_pair.variable, yyVAL.result_pair.value = yyDollar[1].text, yyDollar[3].value
599 | }
600 | case 20:
601 | yyDollar = yyS[yypt-1 : yypt+1]
602 | //line grammar.y:88
603 | {
604 | yyVAL.value = yyDollar[1].text
605 | }
606 | case 21:
607 | yyDollar = yyS[yypt-1 : yypt+1]
608 | //line grammar.y:89
609 | {
610 | yyVAL.value = yyDollar[1].record
611 | }
612 | case 22:
613 | yyDollar = yyS[yypt-1 : yypt+1]
614 | //line grammar.y:90
615 | {
616 | yyVAL.value = yyDollar[1].list
617 | }
618 | case 23:
619 | yyDollar = yyS[yypt-3 : yypt+1]
620 | //line grammar.y:93
621 | {
622 | yyVAL.list = append(yyVAL.list, yyDollar[3].value)
623 | }
624 | case 24:
625 | yyDollar = yyS[yypt-1 : yypt+1]
626 | //line grammar.y:94
627 | {
628 | yyVAL.list = []interface{}{yyDollar[1].value}
629 | }
630 | case 25:
631 | yyDollar = yyS[yypt-3 : yypt+1]
632 | //line grammar.y:97
633 | {
634 | yyVAL.record = yyDollar[2].record
635 | }
636 | case 26:
637 | yyDollar = yyS[yypt-2 : yypt+1]
638 | //line grammar.y:98
639 | {
640 | yyVAL.record = map[string]interface{}{}
641 | }
642 | case 27:
643 | yyDollar = yyS[yypt-3 : yypt+1]
644 | //line grammar.y:101
645 | {
646 | yyVAL.list = append(yyVAL.list, map[string]interface{}{yyDollar[3].result_pair.variable: yyDollar[3].result_pair.value})
647 | }
648 | case 28:
649 | yyDollar = yyS[yypt-1 : yypt+1]
650 | //line grammar.y:102
651 | {
652 | yyVAL.list = []interface{}{map[string]interface{}{yyDollar[1].result_pair.variable: yyDollar[1].result_pair.value}}
653 | }
654 | case 29:
655 | yyDollar = yyS[yypt-3 : yypt+1]
656 | //line grammar.y:105
657 | {
658 | yyVAL.list = yyDollar[2].list
659 | }
660 | case 30:
661 | yyDollar = yyS[yypt-3 : yypt+1]
662 | //line grammar.y:106
663 | {
664 | yyVAL.list = yyDollar[2].list
665 | }
666 | case 31:
667 | yyDollar = yyS[yypt-2 : yypt+1]
668 | //line grammar.y:107
669 | {
670 | yyVAL.list = []interface{}{}
671 | }
672 | }
673 | goto yystack /* stack new state and value */
674 | }
675 |
--------------------------------------------------------------------------------