Skip to content

Commit

Permalink
Add replace mode
Browse files Browse the repository at this point in the history
  • Loading branch information
tamayika committed Nov 25, 2018
1 parent 95fcc3b commit 0babcd3
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 8 deletions.
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,50 @@ gaq is the cli tool to query ast node.
Typical usage is
cat <go file path> | gaq <Query>
cat <go file path> | gaq -m replace <Query> <Replace command>
Please see details at https://github.com/tamayika/gaq
Usage:
gaq <Query> [flags]
Flags:
-h, --help help for gaq
-f, --format string Output format, 'text' or 'pos'. Default is 'text' (default "text")
-h, --help help for gaq
-m, --mode string Execution mode, 'filter' or 'replace'. Default is 'filter' (default "filter")
--version version for gaq
```

#### Filter mode

Default mode is `filter`.

For example, `File > Ident` query filters package name in `main.go`

```
$ cat main.go | gaq "File > Ident"
main
```

#### Replace mode

You can replace matched node text by `replace` mode.

For example, below command exports functions except `main` function.

```
$ cat main.go | gaq -m replace "FuncDecl > Ident:not([Name='main'])" -- sed -e "s/^\(.\)/\U\1/"
```

In `replace` mode, below sequence is executed for each matched node

1. command is spawned
2. gaq passes node text as stdin
3. wait command exit
4. replace node text by command output

You can use any tool which gets input from stdin and puts result to stdout, `sed`, `awk`, `tr` etc.

# Query Specfication

Heavily inspired by CSS Selector.
Expand Down
67 changes: 60 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package main

import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"

"github.com/spf13/cobra"
"github.com/tamayika/gaq/pkg/gaq"
Expand All @@ -30,8 +34,45 @@ func printPos(nodes []ast.Node) {
}
}

func replaceByCommand(source []byte, fset *token.FileSet, nodes []ast.Node, commands []string) []byte {
ret := []byte{}
var lastNode ast.Node
for _, node := range nodes {
pos := fset.Position(node.Pos())
end := fset.Position(node.End())
nodeText := source[pos.Offset:end.Offset]
var stderr bytes.Buffer
cmd := exec.Command(commands[0], commands[1:]...)
cmd.Stderr = &stderr
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatalf("Cannot get stdin pipe. %v", err)
}
io.WriteString(stdin, string(nodeText))
stdin.Close()
replacedText, err := cmd.Output()
if err != nil {
log.Fatalf("Command failed.\nerr: %v\nstderr: %s\nnodeText: %s", err, strings.TrimSuffix(string(stderr.String()), "\n"), string(nodeText))
}
if lastNode == nil {
ret = append(ret, source[:pos.Offset]...)
} else {
ret = append(ret, source[fset.Position(lastNode.End()).Offset:pos.Offset]...)
}
ret = append(ret, replacedText...)
lastNode = node
}
if lastNode == nil {
ret = source
} else {
ret = append(ret, source[fset.Position(lastNode.End()).Offset:]...)
}
return ret
}

func main() {
var format string
var mode string

rootCmd := &cobra.Command{
Use: "gaq <Query>",
Expand All @@ -40,9 +81,10 @@ func main() {
Typical usage is
cat <go file path> | gaq <Query>
cat <go file path> | gaq -m replace <Query> <Replace command>
Please see details at https://github.com/tamayika/gaq`,
Args: cobra.ExactArgs(1),
Args: cobra.MinimumNArgs(1),
Version: version,
Run: func(cmd *cobra.Command, args []string) {
data, err := ioutil.ReadAll(os.Stdin)
Expand All @@ -58,17 +100,28 @@ Please see details at https://github.com/tamayika/gaq`,

q := query.MustParse(args[0])
nodes := node.QuerySelectorAll(q)
switch format {
case "text":
printText(data, fset, nodes)
case "pos":
printPos(nodes)
switch mode {
case "filter":
switch format {
case "text":
printText(data, fset, nodes)
case "pos":
printPos(nodes)
default:
log.Fatalf("Format: %s is not supported.", format)
}
case "replace":
if len(args) < 2 {
log.Fatalf("One or more command and args are expected in replace mode.")
}
fmt.Println(string(replaceByCommand(data, fset, nodes, args[1:])))
default:
log.Fatalf("Format: %s is not supported.", format)
log.Fatalf("Mode: %s is not supported.", mode)
}
},
}
rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "text", "Output format, 'text' or 'pos'. Default is 'text'")
rootCmd.PersistentFlags().StringVarP(&mode, "mode", "m", "filter", "Execution mode, 'filter' or 'replace'. Default is 'filter'")
rootCmd.SetVersionTemplate(`{{printf "%s" .Version}}`)
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
Expand Down

0 comments on commit 0babcd3

Please sign in to comment.