├── .gitignore
├── Makefile
├── README.md
├── bin
└── .gitkeep
└── vargs.go
/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/vargs
2 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | all:
3 | go build -o ./bin/vargs
4 |
5 |
6 | .PHONY: all
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vargs
2 |
3 | Open given files / Call vim function with given strings using Terminal API (xargs for vim)
4 |
5 | ## Usage
6 |
7 | ```
8 | vargs [-0] [-s {separators (comma-separated)}] [-I {replstr}] [-t] [{vim terminal-api arguments (default: "drop")} ...]
9 | ```
10 |
11 | `vargs` sends given arguments to Vim using Terminal API.
12 | If no arguments were given, `"drop"` is specified implicitly (default).
13 |
14 | See below about Vim Terminal API.
15 |
16 | * [Vim help (English)](https://vim-jp.org/vimdoc-en/terminal.html#terminal-api)
17 | * [Vim help (Japanese)](https://vim-jp.org/vimdoc-ja/terminal.html#terminal-api)
18 |
19 | #### Options
20 |
21 | ```
22 | -0 Change separator to NUL character. This is same as "-s nul"
23 | -I string
24 | If this replacement string was given, replace arguments by this with each item
25 | -s string
26 | Change separators with these comma-separated values (available values are "space", "tab", "newline", "nul") (default "newline")
27 | -t Print JSON command to standard error before printing with escape sequence (verbose print)
28 | ```
29 |
30 | ## Why you created this?
31 |
32 | I wanted a way to quickly open selected file using file fuzzy finder UI, like [gof](https://github.com/mattn/gof).
33 | I sent [a pull request](https://github.com/mattn/gof/pull/14) to gof that adds `-t` option to open selected file using Vim Terminal API.
34 | After that, I got an idea that creating generic command for Vim Terminal API.
35 |
36 | Vim Terminal API is very handy to communicate from processes inside Vim terminal window to outer Vim.
37 |
38 | Other Vim plugin examples using Terminal API:
39 |
40 | * [sync-term-cwd.vim](https://github.com/tyru/sync-term-cwd.vim)
41 | * Sync Vim current directory with current directory of bash/zsh
42 | * [tapi-reg.vim](https://github.com/tyru/tapi-reg.vim)
43 | * Access Vim clipboard from bash/zsh
44 |
45 |
46 | ## Examples
47 |
48 | ### Open the selected file (peco, gof)
49 |
50 | Using peco
51 |
52 | ```
53 | $ ls | peco | vargs
54 | ```
55 |
56 | Using gof (recurse subdirectories)
57 |
58 | ```
59 | $ gof | vargs
60 | ```
61 |
62 | However, [`gof` already has `-t` option](https://github.com/mattn/gof/pull/14) to open in Vim :smirk:
63 |
64 | ```
65 | $ gof -t
66 | ```
67 |
68 | ### Open all files under repository
69 |
70 | ```
71 | $ find . -path ./.git -prune -o -type f | vargs
72 | ```
73 |
74 | More safe way...
75 |
76 | ```
77 | $ find . -path ./.git -prune -o -type f -print0 | vargs -0
78 | ```
79 |
80 | ### Open project under $GOPATH
81 |
82 | ```
83 | $ ls -d $GOPATH/src/github.com/*/* | peco | vargs
84 | ```
85 |
86 | You may prefer [project-guide.vim](https://github.com/tyru/project-guide.vim).
87 |
--------------------------------------------------------------------------------
/bin/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tyru/vargs/5c7c8e8c47f772b6b5c5e4133f8a41f0907ed7c8/bin/.gitkeep
--------------------------------------------------------------------------------
/vargs.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "context"
6 | "encoding/json"
7 | "flag"
8 | "fmt"
9 | "io"
10 | "os"
11 | "strings"
12 | "unicode/utf8"
13 | )
14 |
15 | var (
16 | nulTerminated = flag.Bool("0", false, "Change separator to NUL character. This is same as \"-s nul\"")
17 | separators = flag.String("s", "newline", "Change separators with these comma-separated values (available values are \"space\", \"tab\", \"newline\", \"nul\")")
18 | replaceStr = flag.String("I", "", "If this replacement string was given, replace arguments by this with each item")
19 | // for compatibility with xargs, name it as -t
20 | verbose = flag.Bool("t", false, "Print JSON command to standard error before printing with escape sequence (verbose print)")
21 | )
22 |
23 | func main() {
24 | flag.Parse()
25 |
26 | if os.Getenv("VIM_TERMINAL") == "" {
27 | fmt.Fprintln(os.Stderr, "[error] you are running vargs outside vim.")
28 | os.Exit(1)
29 | }
30 |
31 | if *nulTerminated {
32 | *separators = "nul"
33 | }
34 |
35 | var msg []string
36 | if flag.NArg() == 0 {
37 | if *replaceStr == "" {
38 | msg = []string{"drop"}
39 | } else {
40 | msg = []string{"drop", *replaceStr}
41 | }
42 | } else {
43 | msg = flag.Args()
44 | }
45 | existsReplaceStr := false
46 | for _, m := range msg {
47 | if strings.Contains(m, *replaceStr) {
48 | existsReplaceStr = true
49 | break
50 | }
51 | }
52 | if !existsReplaceStr {
53 | fmt.Fprintln(os.Stderr, "warning: -I {replstr} option was specified but no {replstr} in arguments")
54 | }
55 | buildMsg := makeMsgBuilder(msg)
56 |
57 | ctx, cancel := context.WithCancel(context.Background())
58 | ch := make(chan string)
59 | done := make(chan bool)
60 | var senderErr error
61 | go func() {
62 | for {
63 | select {
64 | case item, open := <-ch:
65 | if !open {
66 | done <- true
67 | return
68 | }
69 | newMsg := buildMsg(item)
70 | b, err := json.Marshal(newMsg)
71 | if err != nil {
72 | senderErr = err
73 | cancel()
74 | return
75 | }
76 | cmd := string(b)
77 | if *verbose {
78 | fmt.Fprintln(os.Stderr, cmd)
79 | }
80 | fmt.Printf("\x1b]51;%s\x07", cmd)
81 | }
82 | }
83 | }()
84 |
85 | sepRunes, err := convertSeparators(*separators)
86 | if err != nil {
87 | fmt.Fprintln(os.Stderr, err)
88 | os.Exit(1)
89 | }
90 | err = readEach(ctx, os.Stdin, sepRunes, ch)
91 | <-done
92 |
93 | if err != nil {
94 | fmt.Fprintln(os.Stderr, err)
95 | }
96 | if senderErr != nil {
97 | fmt.Fprintln(os.Stderr, senderErr)
98 | }
99 | if err != nil || senderErr != nil {
100 | os.Exit(1)
101 | }
102 | }
103 |
104 | func makeMsgBuilder(msg []string) func(string) []string {
105 | if *replaceStr == "" {
106 | newMsg := make([]string, len(msg)+1)
107 | copy(newMsg, msg)
108 | return func(item string) []string {
109 | newMsg[len(msg)] = item
110 | return newMsg
111 | }
112 | } else {
113 | template := make([]string, len(msg))
114 | newMsg := make([]string, len(msg))
115 | copy(template, msg)
116 | return func(item string) []string {
117 | for i := range template {
118 | newMsg[i] = strings.ReplaceAll(template[i], *replaceStr, item)
119 | }
120 | return newMsg
121 | }
122 | }
123 | }
124 |
125 | func convertSeparators(separators string) ([]rune, error) {
126 | runes := make([]rune, 0, 5)
127 | for _, s := range strings.Split(separators, ",") {
128 | switch s {
129 | case "space":
130 | runes = append(runes, ' ')
131 | case "tab":
132 | runes = append(runes, '\t')
133 | case "newline":
134 | runes = append(runes, '\r', '\n')
135 | case "nul":
136 | runes = append(runes, '\x00')
137 | default:
138 | return nil, fmt.Errorf("unknown separator '%s'", s)
139 | }
140 | }
141 | return runes, nil
142 | }
143 |
144 | func readEach(ctx context.Context, r io.Reader, separators []rune, dst chan string) error {
145 | scanner := bufio.NewScanner(r)
146 | isSep := func(r rune) bool {
147 | for _, s := range separators {
148 | if s == r {
149 | return true
150 | }
151 | }
152 | return false
153 | }
154 | scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
155 | return scan(isSep, data, atEOF)
156 | })
157 | for scanner.Scan() {
158 | select {
159 | case <-ctx.Done():
160 | close(dst)
161 | return nil
162 | case dst <- scanner.Text():
163 | }
164 | }
165 | close(dst)
166 | return scanner.Err()
167 | }
168 |
169 | // https://golang.org/src/bufio/scan.go?s=13096:13174#L380
170 | func scan(isSep func(rune) bool, data []byte, atEOF bool) (advance int, token []byte, err error) {
171 | // Skip separators.
172 | start := 0
173 | for width := 0; start < len(data); start += width {
174 | var r rune
175 | r, width = utf8.DecodeRune(data[start:])
176 | if !isSep(r) {
177 | break
178 | }
179 | }
180 | // Scan until non-separator character, marking end of word.
181 | for width, i := 0, start; i < len(data); i += width {
182 | var r rune
183 | r, width = utf8.DecodeRune(data[i:])
184 | if isSep(r) {
185 | return i + width, data[start:i], nil
186 | }
187 | }
188 | // If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
189 | if atEOF && len(data) > start {
190 | return len(data), data[start:], nil
191 | }
192 | // Request more data.
193 | return start, nil, nil
194 | }
195 |
--------------------------------------------------------------------------------