├── .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 | --------------------------------------------------------------------------------