├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── hello.cr ├── shard.yml ├── spec ├── completion_spec.cr └── spec_helper.cr └── src ├── completion.cr └── completion ├── macros.cr └── version.cr /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /libs/ 3 | /.crystal/ 4 | /.shards/ 5 | 6 | 7 | # Libraries don't need dependency lock 8 | # Dependencies will be locked in application that uses them 9 | /shard.lock 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Fatih Kadir Akın 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy Command Line Autocompletion Helper 2 | 3 | Completion is a Shell Completion Helper built on Crystal. It allows you to have completion muscle for your CLI apps. 4 | 5 | ## Installation 6 | 7 | Add this to your application's `shard.yml`: 8 | 9 | ```yaml 10 | dependencies: 11 | completion: 12 | github: f/completion 13 | ``` 14 | 15 | ![Completion](http://i.imgur.com/UVh97ZO.gif) 16 | 17 | ## Usage 18 | 19 | ### 1 Minute Start 20 | 21 | First, create a file with simple autocompletion logic. 22 | ```crystal 23 | # hello.cr 24 | require "completion" 25 | 26 | completion :where do |comp| 27 | comp.on(:where) do 28 | comp.reply ["world", "mars", "pluton"] 29 | end 30 | end 31 | ``` 32 | 33 | Build and try: 34 | 35 | ```bash 36 | crystal build hello.cr 37 | eval "$(./hello --completion --development)" 38 | 39 | ./hello 40 | world mars pluton 41 | ``` 42 | 43 | > **Note:** `--development` flag enables autocompletion for relative (`./hello`) completion. 44 | > While developing your application and testing your autocompletion feature, please 45 | > use `--development` flag beside `--completion` flag. 46 | > 47 | > When you are available to use your app globally (e.g. via Homebrew) you should 48 | > make your users to run `eval "$(yourapp --completion)"` command. 49 | 50 | ### Overview 51 | ```crystal 52 | require "completion" 53 | 54 | # [detected program] 55 | 56 | completion :action, :user, :remote do |comp| 57 | 58 | # When Program requested :action, reply the availables. 59 | comp.on(:action) do 60 | comp.reply ["pull", "push"] 61 | end 62 | 63 | # When Program requested :user, reply the availables. 64 | comp.on(:user) do 65 | comp.reply ["f", "sdogruyol", "askn"] 66 | end 67 | 68 | # When Program requested :remote, reply the availables with defined variables. 69 | comp.on(:remote) do 70 | comp.reply ["github/#{comp.values[:user]}", "bitbucket/#{comp.values[:user]}"] 71 | end 72 | 73 | # When all parameters finished, reply always... 74 | # It is `Dir.entries Dir.current` by default. 75 | comp.end do 76 | comp.reply ["--force"] 77 | end 78 | end 79 | ``` 80 | 81 | ### Changing Program Name 82 | 83 | It detects program name automatically. If you want to change it or you have problems with 84 | detection, you should set the first argument to program name. 85 | 86 | ```crystal 87 | require "completion" 88 | 89 | # myprogram 90 | completion "myprogram", :action, :remote, :suboption do |comp| 91 | # ... 92 | end 93 | ``` 94 | 95 | ### Defined Values, Last Word and Whole Line 96 | 97 | The first parameter of the block you give has `last_word`, `line` and `fragment` parameters. So you can make 98 | your parameters more dynamic. 99 | 100 | ```crystal 101 | completion :searchengine, :url do |comp| 102 | 103 | comp.on(:searchengine) do 104 | comp.reply ["google", "bing"] 105 | end 106 | 107 | comp.on(:url) do 108 | comp.reply ["#{comp.values[:searchengine]}.com/search", "#{comp.values[:searchengine]}.com/images"] 109 | end 110 | end 111 | ``` 112 | 113 | This will run as: 114 | 115 | ``` 116 | $ myapp 117 | google bing 118 | 119 | $ myapp goog 120 | google 121 | 122 | $ myapp google 123 | google.com/search google.com/images 124 | ``` 125 | 126 | ### End of Arguments 127 | 128 | You can define what to show when arguments are finished: 129 | 130 | ```crystal 131 | completion :first do |comp| 132 | comp.on(:first) do 133 | comp.reply ["any", "option"] 134 | end 135 | comp.end do 136 | comp.reply ["--force", "--prune"] 137 | end 138 | end 139 | ``` 140 | 141 | ### Concatting Replies 142 | 143 | You can reply more than one time. It will concat all of these. 144 | 145 | ```crystal 146 | completion :first do |comp| 147 | comp.on(:first) do 148 | comp.reply ["any", "option"] 149 | comp.reply ["other", "awesome"] 150 | comp.reply ["options", "to", "select"] 151 | end 152 | end 153 | ``` 154 | 155 | ## Integrating into `OptionParser` 156 | 157 | Completion can parse `OptionParser` arguments and it's very easy to integrate with. 158 | 159 | Simply use `complete_with` macro with the instance of `OptionParser`. It will automatically 160 | parse all the flags and add them to the suggestion list. 161 | 162 | ```crystal 163 | OptionParser.parse! do |parser| 164 | parser.banner = "Usage: salute [arguments]" 165 | parser.on("-u", "--upcase", "Upcases the sallute") { } 166 | parser.on("-t NAME", "--to=NAME", "Specifies the name to salute") { } 167 | parser.on("-h", "--help", "Show this help") { puts parser } 168 | 169 | # Just add this macro to the OptionParser block. 170 | complete_with parser 171 | end 172 | ``` 173 | 174 | It will run as: 175 | 176 | ``` 177 | $ myapp 178 | --help --to --upcase -h -t -u 179 | 180 | $ myapp -- 181 | --help --to --upcase 182 | 183 | $ myapp --help -- 184 | --help --to --upcase 185 | ``` 186 | 187 | ## Installation 188 | 189 | *(You should add these instructions to your project's README)* 190 | 191 | ```bash 192 | # Add this line to your .bashrc file. 193 | eval "$(yourapp --completion)" 194 | ``` 195 | 196 | # Examples 197 | 198 | Examples are here to show you how to make it more functional. 199 | 200 | ## Real World Examples 201 | 202 | | Project Name | Talk is cheap, show me the code | 203 | | ------------ | ------------------------------- | 204 | | [tlcr](https://github.com/porras/tlcr) | [src/tlcr/completion.cr](https://github.com/porras/tlcr/blob/master/src/tlcr/completion.cr#L28-L39) | 205 | | [cryload](https://github.com/sdogruyol/cryload) | [src/cryload/cli.cr](https://github.com/sdogruyol/cryload/blob/master/src/cryload/cli.cr#L36) | 206 | 207 | ## Branched Autocompletion 208 | 209 | This is how you can branch options and suboptions by using `values` parameter. 210 | 211 | ```crystal 212 | completion :action, :subaction, :subsubaction do |comp| 213 | comp.on(:action) do 214 | comp.reply ["pull", "log", "commit", "remote"] 215 | end 216 | 217 | comp.on(:subaction) do 218 | case comp.values[:action] 219 | when "pull" 220 | comp.reply ["origin", "upstream"] 221 | 222 | when "log" 223 | comp.reply ["HEAD", "master", "develop"] 224 | 225 | when "commit" 226 | comp.reply ["--amend", "-m", "-am"] 227 | end 228 | end 229 | 230 | comp.on(:subsubaction) do 231 | case comp.values[:subaction] 232 | when "origin" 233 | comp.reply ["origin/master", "origin/upstream", "origin/patch"] 234 | 235 | when "HEAD" 236 | comp.reply ["~1", "~2", "~3"] 237 | end 238 | end 239 | end 240 | ``` 241 | 242 | ## Remote Autocompletion 243 | 244 | You can make a remote autocompletion using `HTTP::Client`. 245 | 246 | ```crystal 247 | require "json" 248 | require "http/client" 249 | 250 | completion :repos do |comp| 251 | comp.on(:repos) do 252 | request = HTTP::Client.get "https://api.github.com/users/f/repos" 253 | repos = JSON.parse(request.body) 254 | repo_names = [] of JSON::Any 255 | repos.each {|repo| repo_names << repo["name"] } 256 | 257 | comp.reply repo_names 258 | end 259 | end 260 | ``` 261 | 262 | This will run as: 263 | 264 | ``` 265 | $ mygit c 266 | cards coffeepad completion cryload crystal-kemal-todo-list crystal-weekly 267 | ``` 268 | 269 | ## TODO 270 | 271 | - [ ] Add ZSH Support 272 | 273 | ## Contributing 274 | 275 | 1. Fork it ( https://github.com/f/completion/fork ) 276 | 2. Create your feature branch (git checkout -b my-new-feature) 277 | 3. Commit your changes (git commit -am 'Add some feature') 278 | 4. Push to the branch (git push origin my-new-feature) 279 | 5. Create a new Pull Request 280 | 281 | ## Contributors 282 | 283 | - [f](https://github.com/f) Fatih Kadir Akın - creator, maintainer 284 | 285 | > It's acutally a port of [`omelette`](http://github.com/f/omelette) package of Node. 286 | -------------------------------------------------------------------------------- /examples/hello.cr: -------------------------------------------------------------------------------- 1 | require "../src/completion" 2 | 3 | completion :where do |comp| 4 | comp.on(:where) do 5 | comp.reply ["world", "mars", "pluton"] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: completion 2 | version: 0.1.0 3 | 4 | authors: 5 | - Fatih Kadir Akın 6 | 7 | license: MIT 8 | -------------------------------------------------------------------------------- /spec/completion_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Completion do 4 | # TODO: Write tests 5 | 6 | it "works" do 7 | true.should eq(true) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/completion" 3 | -------------------------------------------------------------------------------- /src/completion.cr: -------------------------------------------------------------------------------- 1 | require "./completion/*" 2 | require "option_parser" 3 | 4 | module Completion 5 | 6 | class Completer 7 | 8 | getter last_word 9 | getter line 10 | getter fragment 11 | getter values 12 | 13 | def initialize(@program) 14 | cli_args = ARGV 15 | @fragments = [] of Symbol 16 | @values = {} of String|Symbol => String|Nil 17 | @end_of_arguments = ->{ reply Dir.entries Dir.current } 18 | @listeners = {} of Symbol => -> Void 19 | @install = cli_args.includes? "--completion" 20 | @compgen = cli_args.includes? "__compgen__" 21 | @line = "" 22 | 23 | if @compgen 24 | @comp_starts_at = cli_args.index "__compgen__" 25 | if @comp_starts_at 26 | starts = Int32.cast @comp_starts_at 27 | @fragment = cli_args[starts + 1].to_i 28 | @last_word = cli_args[starts + 2] 29 | @line = cli_args[starts + 3] 30 | end 31 | end 32 | 33 | if @install 34 | unless @compgen 35 | puts installer 36 | end 37 | exit 0 38 | end 39 | end 40 | 41 | def set_fragments(@fragments) 42 | end 43 | 44 | def on(fragment, &reply) 45 | @listeners[fragment] = reply 46 | end 47 | 48 | def end(&reply) 49 | @end_of_arguments = reply 50 | end 51 | 52 | def reply(results) 53 | puts results.join "\n" 54 | end 55 | 56 | def init 57 | if @compgen 58 | fragment = @fragment.as(Int32) 59 | begin 60 | completions = @listeners[@fragments[fragment-1]] 61 | 62 | @line.split(" ").each_index do |i| 63 | @values[@fragments[i-1]] = @line.split(" ").at(i) 64 | end 65 | rescue 66 | completions = @end_of_arguments 67 | end 68 | completions.call 69 | exit 0 70 | end 71 | end 72 | 73 | def installer 74 | completion = "__#{@program}_completion" 75 | @caller = @program 76 | @caller = "$1" if ARGV.includes? "--development" 77 | <<-SCRIPT 78 | ### #{@program} completion - begin. generated by f/completion ### 79 | if type complete &>/dev/null; then 80 | #{completion}() { 81 | COMPREPLY=( $(compgen -W '$(#{@caller} __compgen__ \"${COMP_CWORD}\" \"${COMP_WORDS[COMP_CWORD-1]}\" \"${COMP_LINE}\")' -- \"${COMP_WORDS[COMP_CWORD]}\") ) 82 | } 83 | complete -o bashdefault -o default -o nospace -F #{completion} #{@program} 84 | fi 85 | ### #{@program} completion - end ### 86 | SCRIPT 87 | end 88 | 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /src/completion/macros.cr: -------------------------------------------------------------------------------- 1 | macro completion(*fragments, &block) 2 | %completion = Completion::Completer.new File.basename $0 3 | %completion.set_fragments {{fragments}} 4 | {{block.args.first}} = %completion 5 | {{block.body}} 6 | %completion.init 7 | end 8 | 9 | macro complete_with(parser) 10 | %options = [] of String 11 | {{parser}}.to_s.split("\n").each do |v| 12 | res = v.match(/\s{4}(.*?)\s{2}/) 13 | if (res != nil) 14 | opts = res.not_nil![1].split(",").map do |e| 15 | e.split(/\s+/, 1).not_nil!.map do |v| 16 | %options << v.gsub(/^\s+|\s+$/, "").split(/\=|\s/)[0] 17 | end 18 | end 19 | end 20 | end 21 | 22 | %completion = Completion::Completer.new File.basename $0 23 | %completion.set_fragments [:__opts] 24 | %completion.on(:__opts) do 25 | %completion.reply %options 26 | end 27 | %completion.end do 28 | %completion.reply %options 29 | end 30 | %completion.init 31 | end 32 | 33 | macro completion(program, *fragments, &block) 34 | %completion = Completion::Completer.new {{program}} 35 | %completion.set_fragments {{fragments}} 36 | {{block.args.first}} = %completion 37 | {{block.body}} 38 | %completion.init 39 | end 40 | 41 | macro complete_with(program, parser) 42 | %options = [] of String 43 | {{parser}}.to_s.split("\n").each do |v| 44 | res = v.match(/\s{4}(.*?)\s{2}/) 45 | if (res != nil) 46 | opts = res.not_nil![1].split(",").map do |e| 47 | e.split(/\s+/, 1).not_nil!.map do |v| 48 | %options << v.gsub(/^\s+|\s+$/, "").split(/\=|\s/)[0] 49 | end 50 | end 51 | end 52 | end 53 | 54 | %completion = Completion::Completer.new {{program}} 55 | %completion.set_fragments [:__opts] 56 | %completion.on(:__opts) do 57 | %completion.reply %options 58 | end 59 | %completion.end do 60 | %completion.reply %options 61 | end 62 | %completion.init 63 | end 64 | -------------------------------------------------------------------------------- /src/completion/version.cr: -------------------------------------------------------------------------------- 1 | module Completion 2 | VERSION = "0.1.0" 3 | end 4 | --------------------------------------------------------------------------------