├── .gitignore ├── LICENSE.txt ├── README.md ├── askbash.rb ├── bashrc-generator.bash ├── bashrc-generator.rb ├── completions ├── cargo.yml ├── jekyll.yml ├── mvn.yml ├── rustc.yml └── vagrant.yml ├── lib ├── completers │ ├── ExecCompleter.rb │ ├── FileCompleter.rb │ ├── FilesCSVCommaCompleter.rb │ ├── FilesCompleter.rb │ ├── PwdDirCompleter.rb │ └── RegexCompleter.rb ├── config-search.rb ├── core.rb ├── testtools.rb ├── tokenize.rb └── universal-utils.rb ├── releasenotes.md └── test ├── askdebug.sh ├── completions └── food.yml ├── core-test.rb └── data ├── space lab └── orbit └── wow ├── dang └── .gitkeep ├── duh └── day └── huh ├── ha └── ho /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /bin 3 | Session.vim 4 | *.log 5 | todo.md.html 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ari Kast 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | Version: 0.6.4 5 | 6 | Askbash makes it easy to define your own bash autocompletions (that thing that happens when you type a command and hit the tab key once or twice) using an intuitive yaml syntax. It also comes with a number of autocompletions pre-installed, which you can find in the completions/ directory. 7 | 8 | This means when you type a program name which matches one of the completers, such as "mvn", that you can tab-complete arguments to the program on the command line based on the definitions found in the corresponding completion file (in this case mvn.yml). 9 | 10 | How to install and use 11 | ============== 12 | 13 | *Note: these instructions are written using markdown. If you are reading this file "raw" then you'll see backtick markings around the code snippets -- these backticks are formatting and are not part of the code!* 14 | 15 | - First make sure Ruby 2.0+ is installed (tested against 2.0, probably also works with 1.9). 16 | 17 | - Download or clone this repo. This will be your Askbash installation. 18 | 19 | - Add this to your .bashrc (or .bash_profile as the case may be), substituting the real path to your Askbash installation: 20 | 21 | ```bash 22 | # export ASKBASH_DEBUG=1 23 | export ASKBASH_HOME=/your-actual-path/askbash 24 | source $ASKBASH_HOME/bashrc-generator.bash 25 | ``` 26 | 27 | - Restart or source your shell for it to take effect. 28 | You will now have a variable called $ASKBASH_HOME defined, and any completions found in the following places will be active (first match found wins) 29 | 30 | ```bash 31 | ~/.askbash/completions/*.yml 32 | $ASKBASH_HOME/completions/*.yml 33 | ``` 34 | 35 | That's it, you now have the full existing library of bash autocompletions working for you, and an easy way to write your own additional completions as needed. 36 | 37 | Writing your own static completer 38 | ========== 39 | 40 | Your custom autocompletions are driven from a set of yaml files. For example, suppose you have a fictitious command called "food". You might create this completion in a file called food.yml: 41 | 42 | ```yaml 43 | 'food ': 44 | 'fruit ': 45 | 'orange ': 46 | 'banana ': 47 | 'veg ': 48 | 'broccoli ': 49 | ``` 50 | 51 | After adding this file in ~/.askbash/completions/ and restarting your shell, you would now have autocompletion of "food " according to the static hierarchy defined in food.yml. You could now type "food f" and hit tab to complete the text to "food fruit ". You could then hit tab twice to get your next set of options, which would be "orange " and "banana ". 52 | 53 | Here we explicitly add spaces to our choices because we want a space to be added when these words complete, but you don't have to do this. You could also have a "multi-part" completion by not putting a space at the end: 54 | 55 | ```yaml 56 | 'food ': 57 | 'fruit ': 58 | 'orange ': 59 | 'banana ': 60 | '--seedless': 61 | '=true ': 62 | '=false ': 63 | 'veg ': 64 | 'broccoli ': 65 | ``` 66 | 67 | Here the --seedless does not end with a space because the intention is to continue with =true or =false without any spaces in between. 68 | 69 | Sometimes our intent is to select many options, for example "food fruit orange --seedless=true banana". In this case, we use yaml's "reference" syntax to loop back to another node like this: 70 | 71 | ```yaml 72 | 'food ': 73 | 'fruit ': &fruit 74 | 'orange ': *fruit 75 | 'banana ': *fruit 76 | '--seedless': 77 | '=true ': *fruit 78 | '=false ': *fruit 79 | 'veg ': 80 | 'broccoli ': 81 | ``` 82 | 83 | This "reference" syntax consists of an arbitrarily named anchor (here it is &fruit) followed by one or more references to it (in this case *fruit). Note how all nodes end with colons, but references occur after the colon. 84 | 85 | ### Dynamic completers 86 | 87 | Sometimes aspects of your completion hierarchy might be dynamic. For instance, perhaps in addition to =true and =false we also want to allow an arbitrary value here. In this case you'd use a Regex completer like this: 88 | 89 | ```yaml 90 | 'food ': 91 | 'fruit ': &fruit 92 | 'orange ': 93 | 'banana ': 94 | '--seedless': 95 | '=true ': *fruit 96 | '=false ': *fruit 97 | '.+ ': *fruit 98 | 'veg ': 99 | 'broccoli ': 100 | ``` 101 | 102 | There are many dynamic completers to do all sorts of things such as fill in a filename or list a running proc or execute an arbitrary bash command. Take a look at the $ASKBASH_HOME/lib/completers/ to see the available completers. Any of these completers can be used in your yml configuration; to use one, just use it in your *.yml in the same way we've used the Regex above and drop the "Completer.rb" suffix when refering to it. So to use FileCompleter.rb for instance, you would specify `` in your *.yml config. 103 | 104 | You can also of course easily write your own completer; just place it in lib/completers and then use it like any other, and make sure its name ends in "Config.rb". 105 | 106 | 107 | Writing your own dynamic completer 108 | ================================== 109 | 110 | See lib/completers/*.rb for examples of writing your own dynamic completer. You basically just need to extend a class and implement a few methods. 111 | You will also likely find it useful to enable debugging output, which can be done by setting an environment variable in your .bashrc (or .bash_profile as the case may be) like this: 112 | 113 | ```bash 114 | export ASKBASH_DEBUG=1 115 | ``` 116 | 117 | Then after restarting the shell, you'll find copious debugging info logged to $ASKBASH_HOME/askbash.log 118 | 119 | 120 | ### Some nit-picky syntax/naming rules which MUST be followed 121 | 122 | - If your Ruby completion class is called `` in the .yml config, then it must be defined in a Ruby class called AbcCompleter whose definition can be found in $ASKBASH_HOME/lib/completers/AbcCompleter.rb 123 | 124 | - If the Bash command you are autocompleting is called foobar, then: 125 | 126 | * You must define its completions in $ASKBASH_HOME/completions/foobar.yml or ~/.askbash/completions/foobar.yml 127 | 128 | * The top level node inside foobar.yml must be 'foobar ' 129 | 130 | - If you want spaces after your completions, then you need to quote them and include the space as part of the completion, eg: 131 | '-r ': 132 | 133 | - As seen above, each completion definition in your *.yml file must end with a colon, with the exception of references which occur after the colon (but the colon must still be present!) 134 | 135 | 136 | Examples 137 | ========= 138 | 139 | See `completions/*.yml` as well as `test/completions/*.yml` for examples 140 | -------------------------------------------------------------------------------- /askbash.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | ################################ 4 | ### the entry point which bash will call 5 | ### see the README.md for full instructions how to setup 6 | ################################ 7 | 8 | 9 | require_relative 'lib/core.rb' 10 | require_relative 'lib/config-search.rb' 11 | 12 | include ConfigSearch 13 | 14 | begin 15 | config = ARGV[0] 16 | #config = completionConfSearch( COMPLETION_CONF_DIRS ) 17 | 18 | textToComplete = ENV['COMP_LINE'] 19 | if textToComplete.nil? || textToComplete.strip.length == 0 20 | if ARGV.size > 1 21 | textToComplete = ARGV[1..-1].join ' ' 22 | end 23 | end 24 | 25 | log "attempting completion for #{textToComplete}" 26 | log "using config #{config}" 27 | 28 | ac = AutoCompleter.new(config, textToComplete) 29 | 30 | answer = ac.parse 31 | log "returned #{answer}" 32 | puts answer 33 | 34 | rescue StandardError => e 35 | if debugMode? 36 | raise e 37 | else 38 | log "problem autocompleting: #{e}" 39 | log e.backtrace 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /bashrc-generator.bash: -------------------------------------------------------------------------------- 1 | 2 | # dont include colon as a wordbreak char (otherwise wreaks havoc on completions like mvn eclipse:eclipse) 3 | # unfortunately this must be preset in the calling shell, meaning it could affect other completion programs which may rely on it 4 | export COMP_WORDBREAKS="${COMP_WORDBREAKS//:}" 5 | 6 | # the main inclusion here, finds all askbash completers and registers them with the shell 7 | eval "$($ASKBASH_HOME/bashrc-generator.rb)" 8 | 9 | # a handy utility for exploring bash's native completion behavior. lets you type askdebug and then uses the contents of test/askdebug.sh as the tab completion 10 | if [ "$ASKBASH_DEBUG" != "" ] && [ "$ASKBASH_DEBUG" != "0" ]; then 11 | complete -C "$ASKBASH_HOME/test/askdebug.sh" -o nospace askdebug 12 | fi 13 | -------------------------------------------------------------------------------- /bashrc-generator.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | ################################################ 4 | ### this generates entries to put in your .bashrc 5 | ### see the README.md for instructions on the easiest way to incorporate its output in your .bashrc or .bash_profile 6 | ################################################ 7 | 8 | 9 | require_relative 'lib/core.rb' 10 | require_relative 'lib/config-search.rb' 11 | 12 | include ConfigSearch 13 | 14 | allconfs = allCompletionConfs( COMPLETION_CONF_DIRS ) 15 | 16 | allconfs.each {|conf| 17 | #puts "complete -C \"$ASKBASH_HOME/askbash.rb\" -o nospace #{conf.command}" 18 | puts "complete -C \"$ASKBASH_HOME/askbash.rb #{conf.configfile}\" -o nospace #{conf.command}" 19 | } 20 | 21 | #puts allconfs 22 | 23 | -------------------------------------------------------------------------------- /completions/cargo.yml: -------------------------------------------------------------------------------- 1 | "cargo ": &cargo 2 | "-h ": 3 | "--help ": 4 | "-V ": 5 | "--version ": 6 | "--list ": 7 | "--verbose ": *cargo 8 | "-v ": *cargo 9 | "--quiet ": *cargo 10 | "-q ": *cargo 11 | 12 | "build ": &build 13 | "-h ": 14 | "--help ": 15 | "--package ": &package 16 | '.+ ': *build 17 | "-p ": *package 18 | '--jobs ': &jobs 19 | '([$].*)|(\d+) ': *build 20 | '-j ': *jobs 21 | '--lib ': *build 22 | '--bin ': 23 | '.+ ': *build 24 | '--example ': 25 | '.+ ': *build 26 | '--test ': 27 | '.+ ': *build 28 | '--bench ': 29 | '.+ ': *build 30 | '--release ': *build 31 | '--features ': 32 | '.+ ': *build 33 | '--no-default-features ': *build 34 | "--manifest-path ": 35 | ' ': *build 36 | "--target ": 37 | "TRIPLE ": *build 38 | "--verbose ": *build 39 | "-v ": *build 40 | "--quiet ": *build 41 | "-q ": *build 42 | 43 | "clean ": &clean 44 | "-h ": 45 | "--help ": 46 | "--package ": &clean_package 47 | '.+ ': *clean 48 | "-p ": *clean_package 49 | "--manifest-path ": 50 | ' ': *clean 51 | "--target ": 52 | "TRIPLE ": *clean 53 | "--verbose ": *clean 54 | "-v ": *clean 55 | "--quiet ": *clean 56 | "-q ": *clean 57 | 58 | "doc ": &doc 59 | "-h ": 60 | "--help ": 61 | "--open ": *doc 62 | '--features ': 63 | '.+ ': *doc 64 | '--jobs ': &doc_jobs 65 | '([$].*)|(\d+) ': *doc 66 | '-j ': *doc_jobs 67 | '--no-default-features ': *doc 68 | '--no-deps ': *doc 69 | "--manifest-path ": 70 | ' ': *doc 71 | "--package ": &doc_package 72 | '.+ ': *doc 73 | "-p ": *doc_package 74 | "--target ": 75 | "TRIPLE ": *doc 76 | "--verbose ": *doc 77 | "-v ": *doc 78 | "--quiet ": *doc 79 | "-q ": *doc 80 | 81 | "new ": &new 82 | "-h ": 83 | "--help ": 84 | "--vcs ": 85 | "git ": *new 86 | "hg ": *new 87 | "none ": *new 88 | "--bin ": *new 89 | "--name ": 90 | '.+ ': *new 91 | "--verbose ": *new 92 | "-v ": *new 93 | "--quiet ": *new 94 | "-q ": *new 95 | 96 | "run ": &run 97 | "-h ": 98 | "--help ": 99 | '--bin ': 100 | '.+ ': *run 101 | '--example ': 102 | '.+ ': *run 103 | '--jobs ': &run_jobs 104 | '([$].*)|(\d+) ': *run 105 | '-j ': *run_jobs 106 | '--release ': *run 107 | '--features ': 108 | '.+ ': *run 109 | '--no-default-features ': *run 110 | "--manifest-path ": 111 | ' ': *run 112 | "--target ": 113 | "TRIPLE ": *run 114 | "--verbose ": *run 115 | "-v ": *run 116 | "--quiet ": *run 117 | "-q ": *run 118 | 119 | "test ": &test 120 | "-h ": 121 | "--help ": 122 | '--lib ': *test 123 | '--bin ': 124 | '.+ ': *test 125 | '--example ': 126 | '.+ ': *test 127 | '--test ': 128 | '.+ ': *test 129 | '--bench ': 130 | '.+ ': *test 131 | '--no-run ': *test 132 | '--jobs ': &run_jobs 133 | '([$].*)|(\d+) ': *run 134 | '-j ': *run_jobs 135 | '--features ': 136 | '.+ ': *test 137 | '--no-default-features ': *test 138 | "--manifest-path ": 139 | ' ': *test 140 | "--package ": &test_package 141 | '.+ ': *test 142 | "-p ": *test_package 143 | '--release ': *test 144 | "--target ": 145 | "TRIPLE ": *test 146 | "--verbose ": *test 147 | "-v ": *test 148 | "--quiet ": *test 149 | "-q ": *test 150 | 151 | "bench ": &bench 152 | "-h ": 153 | "--help ": 154 | '--lib ': *bench 155 | '--bin ': 156 | '.+ ': *bench 157 | '--example ': 158 | '.+ ': *bench 159 | '--test ': 160 | '.+ ': *bench 161 | '--bench ': 162 | '.+ ': *bench 163 | '--no-run ': *bench 164 | '--jobs ': &run_jobs 165 | '([$].*)|(\d+) ': *run 166 | '-j ': *run_jobs 167 | '--features ': 168 | '.+ ': *bench 169 | '--no-default-features ': *bench 170 | "--manifest-path ": 171 | ' ': *bench 172 | "--package ": &bench_package 173 | '.+ ': *bench 174 | "-p ": *bench_package 175 | "--target ": 176 | "TRIPLE ": *bench 177 | "--verbose ": *bench 178 | "-v ": *bench 179 | "--quiet ": *bench 180 | "-q ": *bench 181 | 182 | "update ": &update 183 | "-h ": 184 | "--help ": 185 | '--aggressive ': *update 186 | '--precise ': 187 | '.+ ': *update 188 | "--package ": &update_package 189 | '.+ ': *update 190 | "-p ": *update_package 191 | "--verbose ": *update 192 | "-v ": *update 193 | "--quiet ": *update 194 | "-q ": *update 195 | 196 | "search ": &search 197 | "-h ": 198 | "--help ": 199 | '--host ': 200 | '.+ ': *search 201 | "--verbose ": *search 202 | "-v ": *search 203 | "--quiet ": *search 204 | "-q ": *search 205 | '.+ ': 206 | -------------------------------------------------------------------------------- /completions/jekyll.yml: -------------------------------------------------------------------------------- 1 | 'jekyll ': &jekyll 2 | '--version ': 3 | '-v ': 4 | '--help ': 5 | '-h ': 6 | '--trace ': *jekyll 7 | '-t ': *jekyll 8 | '--source ': &source 9 | ' ': *jekyll 10 | '-s ' : *source 11 | '--destination ': &destination 12 | ' ': *jekyll 13 | '-d ' : *destination 14 | '--safe ': *jekyll 15 | '--plugins ': &plugins 16 | ', ': *jekyll 17 | '-p ': *plugins 18 | '--layouts ': 19 | ' ': *jekyll 20 | 21 | 'build ': &build 22 | '--config ': 23 | ', ': *build 24 | '--future ': *build 25 | '--limit_posts ': 26 | '\d+ ': *build 27 | '--no-watch ': *build 28 | '--watch ': *build 29 | '-w ': *build 30 | '--force_polling ': *build 31 | '--lsi ' : *build 32 | '--drafts ': *build 33 | '-D ': *build 34 | '--unpublished ': *build 35 | '-q ': *build 36 | '--quiet ': *build 37 | '-V ' : *build 38 | '--verbose ': *build 39 | '-h ' : 40 | '--help ': 41 | '-v ' : 42 | '--version ': 43 | '-t ' : *build 44 | '--trace ': *build 45 | 'b ': *build 46 | 47 | 'docs ': &docs 48 | '--port ': &port 49 | '\d+ ': *docs 50 | '-P ': *port 51 | '--host ': &host 52 | '.+ ': *docs 53 | '-H ': *host 54 | '-h ': 55 | '--help ': 56 | '-v ': 57 | '--version ': 58 | '-t ': *docs 59 | '--trace ': *docs 60 | 61 | 'doctor ': &doctor 62 | '-h ': 63 | '--help ': 64 | '-v ': 65 | '--version ': 66 | '-t ': 67 | '--trace ': 68 | 'hyde ': *doctor 69 | 70 | 'help ': &help 71 | 72 | 'new ': &new 73 | '--force ': *new 74 | '--blank ': *new 75 | '-h ': 76 | '--help ': 77 | '-v ': 78 | '--version ': 79 | '-t ': *new 80 | '--trace ': *new 81 | 82 | 'serve ': &serve 83 | '--config ': 84 | ', ': *serve 85 | '--future ': *serve 86 | '--limit_posts ': 87 | '\d+ ': *serve 88 | '--no-watch ': *serve 89 | '--watch ': *serve 90 | '-w ': *serve 91 | '--force_polling ': *serve 92 | '--lsi ' : *serve 93 | '--drafts ': *serve 94 | '-D ': *serve 95 | '--unpublished ': *serve 96 | '-q ': *serve 97 | '--quiet ': *serve 98 | '-V ' : 99 | '--verbose ': 100 | '-h ' : 101 | '--help ': 102 | '-v ' : 103 | '--version ': 104 | '-t ' : *serve 105 | '--trace ': *serve 106 | '-B ' : *serve 107 | '--detach ': *serve 108 | '--port ': &serve-port 109 | '\d+ ': *serve 110 | '-P ': *serve-port 111 | '--host ': &serve-host 112 | '.+ ': *serve 113 | '-H ': *serve-host 114 | '--baseurl ': &baseurl 115 | '.+ ': *serve 116 | '-b ': *baseurl 117 | '--skip-initial-build ': *serve 118 | '-h ': 119 | '--help ': 120 | '-v ': 121 | '--version ': 122 | '-t ': 123 | '--trace ': *serve 124 | 125 | 'server ': *serve 126 | 's ': *serve 127 | 128 | -------------------------------------------------------------------------------- /completions/mvn.yml: -------------------------------------------------------------------------------- 1 | 'mvn ': &mvn 2 | ### top level flags 3 | '-am ': *mvn 4 | '--also-make ': *mvn 5 | 6 | '-amd ': *mvn 7 | '--also-make-dependents ': *mvn 8 | 9 | '-B ': *mvn 10 | '--batch-mode ': *mvn 11 | 12 | '-b ': &b 13 | '.+ ': *mvn 14 | '--builder ': *b 15 | 16 | '-C ': *mvn 17 | '--strict-checksums ': *mvn 18 | 19 | '-c ': *mvn 20 | '--lax-checksums ': *mvn 21 | 22 | '-cpu ': *mvn 23 | '--check-plugin-updates ': *mvn 24 | 25 | '-D ': &D 26 | 'maven.': 27 | 'test.skip=': 28 | 'true ': *mvn 29 | 'false ': *mvn 30 | 'surefire.debug="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 -Xnoagent -Djava.compiler=NONE" ': *mvn 31 | 'skipTests ': *mvn 32 | 'skipTests=': 33 | 'true ': *mvn 34 | 'false ': *mvn 35 | 'test=': 36 | '.+ ': *mvn 37 | 'surefire.': 38 | 'rerunFailingTestsCount=': 39 | '([$].*)|(\d+) ': *mvn 40 | 'jboss-as.': 41 | 'hostname=': 42 | '.+ ': *mvn 43 | 'port=': 44 | '([$].*)|(\d+) ': *mvn 45 | 'username=': 46 | '.+ ': *mvn 47 | 'password=': 48 | '.+ ': *mvn 49 | 'spring.profiles.active=': 50 | '.+ ': *mvn 51 | '-D': *D 52 | '--define ': *D 53 | 54 | '-e ': *mvn 55 | '--errors ': *mvn 56 | 57 | '-emp ': &emp 58 | '.+ ': *mvn 59 | '--encrypt-master-password ': *emp 60 | 61 | '-ep ': &ep 62 | '.+ ': *mvn 63 | '--encrypt-password ': *ep 64 | 65 | '-f ': &f 66 | '': *mvn 67 | '--file ': *f 68 | 69 | '-fae ': *mvn 70 | '--fail-at-end ': *mvn 71 | 72 | '-ff ': *mvn 73 | '--fail-fast ': *mvn 74 | 75 | '-fn ': *mvn 76 | '--fail-never ': *mvn 77 | 78 | '-gs ': &gs 79 | '.+ ': *mvn 80 | '--global-settings ': *gs 81 | 82 | '-h ': 83 | '--help ': 84 | 85 | '-l ': &l 86 | '': *mvn 87 | '--log-file ': *l 88 | 89 | '-llr ': *mvn 90 | '--legacy-local-repository ': *mvn 91 | 92 | '-N ': *mvn 93 | '--non-recursive ': *mvn 94 | 95 | '-npr ': *mvn 96 | '--no-plugin-registry ': *mvn 97 | 98 | '-npu ': *mvn 99 | '--no-plugin-updates ': *mvn 100 | 101 | '-nsu ': *mvn 102 | '--no-snapshot-updates ': *mvn 103 | 104 | '-o ': *mvn 105 | '--offline ': *mvn 106 | 107 | '-P ': &P 108 | '.+ ': *mvn 109 | '--activate-profiles ': *P 110 | 111 | '-pl ': &pl 112 | '.+ ': *mvn 113 | '--projects ': *pl 114 | 115 | '-q ': *mvn 116 | '--quiet ': *mvn 117 | 118 | '-rf ': &rf 119 | '.+ ': *mvn 120 | '--resume-from ': *rf 121 | 122 | '-s ': &s 123 | '.+ ': *mvn 124 | '--settings ': *s 125 | 126 | '-T ': &T 127 | '.+ ': *mvn 128 | '--threads ': *T 129 | 130 | '-t ': &t 131 | '.+ ': *mvn 132 | '--toolchains ': *t 133 | 134 | '-U ': *mvn 135 | '--update-snapshots ': *mvn 136 | 137 | '-up ': *mvn 138 | '--update-plugins ': *mvn 139 | 140 | '-V ': *mvn 141 | '--show-version ': *mvn 142 | 143 | '-v ': 144 | '--version ': 145 | 146 | '-X ': *mvn 147 | '--debug ': *mvn 148 | 149 | ### default lifecycles 150 | # clean lifecycle 151 | 'pre-clean ': *mvn 152 | 'clean ': *mvn 153 | 'post-clean ': *mvn 154 | 155 | # Default Lifecycle 156 | 'validate ': *mvn 157 | 'initialize ': *mvn 158 | 'generate-sources ': *mvn 159 | 'process-sources ': *mvn 160 | 'generate-resources ': *mvn 161 | 'process-resources ': *mvn 162 | 'compile ': *mvn 163 | 'process-classes ': *mvn 164 | 'generate-test-sources ': *mvn 165 | 'process-test-sources ': *mvn 166 | 'generate-test-resources ': *mvn 167 | 'process-test-resources ': *mvn 168 | 'test-compile ': *mvn 169 | 'process-test-classes ': *mvn 170 | 'test ': *mvn 171 | 'prepare-package ': *mvn 172 | 'package ': *mvn 173 | 'pre-integration-test ': *mvn 174 | 'integration-test ': *mvn 175 | 'post-integration-test ': *mvn 176 | 'verify ': *mvn 177 | 'install ': *mvn 178 | 'deploy ': *mvn 179 | 180 | # Site Lifecycle 181 | 'pre-site ': *mvn 182 | 'site ': *mvn 183 | 'post-site ': *mvn 184 | 'site-deploy ': *mvn 185 | 186 | # common goals 187 | 'archetype:': 188 | 'generate ': &archetype_generate 189 | '-D': 190 | 'archetypeGroupId=': 191 | '.+ ': *archetype_generate 192 | 'archetypeArtifactId=': 193 | 'maven-archetype-quickstart ': *archetype_generate 194 | '.+ ': *archetype_generate 195 | 'archetypeVersion=': 196 | '.+ ': *archetype_generate 197 | 'groupId=': 198 | '.+ ': *archetype_generate 199 | 'artifactId=': 200 | '.+ ': *archetype_generate 201 | 'interactiveMode=': 202 | 'false ': *archetype_generate 203 | 'true ': *archetype_generate 204 | 'eclipse:': 205 | 'eclipse ': *mvn 206 | 'help:': 207 | 'describe ': &help-describe 208 | '-D': 209 | 'plugin=': 210 | 'war ': *help-describe 211 | 'eclipse ': *help-describe 212 | '.+ ': *help-describe 213 | 'full=true ': *help-describe 214 | 'effective-settings ': 215 | 'effective-pom ': 216 | 'active-profiles ': 217 | 218 | 'compiler:': 219 | 'help ': &compiler_help 220 | '-Dgoal=compile ' : *compiler_help 221 | '-Ddetail ' : *compiler_help 222 | 223 | 'liquibase:': 224 | 'changelogSync ': *mvn 225 | 'changelogSyncSQL ': *mvn 226 | 'clearCheckSums ': *mvn 227 | 'dbDoc ': *mvn 228 | 'diff ': *mvn 229 | 'dropAll ': *mvn 230 | 'generateChangeLog ': *mvn 231 | 'help ': 232 | 'listLocks ': *mvn 233 | 'releaseLocks ': *mvn 234 | 'rollback ': *mvn 235 | 'rollbackSQL ': *mvn 236 | 'status ': *mvn 237 | 'tag ': *mvn 238 | 'update ': *mvn 239 | 'updateSQL ': *mvn 240 | 'updateTestingRollback ': *mvn 241 | 'futureRollbackSQL ': *mvn 242 | 'tomcat:' : 243 | 'deploy ': *mvn 244 | 'deploy-only ': *mvn 245 | 'exploded ': *mvn 246 | 'help ': 247 | '-Ddetail=true ' : 248 | 'info ': *mvn 249 | 'inplace ': *mvn 250 | 'list ': *mvn 251 | 'redeploy ': *mvn 252 | 'reload ': *mvn 253 | 'resources ': *mvn 254 | 'roles ': *mvn 255 | 'run ': *mvn 256 | 'run-war ': *mvn 257 | 'run-war-only ': *mvn 258 | 'sessions ': *mvn 259 | 'shutdown ': *mvn 260 | 'start ': *mvn 261 | 'stop ': *mvn 262 | 'undeploy ': *mvn 263 | 'jetty:' : 264 | 'run ': *mvn 265 | 'run-war ': *mvn 266 | 'config ': *mvn 267 | 'surefire:': 268 | 'help ': &surefire_help 269 | '-D': 270 | 'detail=true ': *surefire_help 271 | 'goal=': 272 | '.+ ': *surefire_help 273 | 'jboss-as:': 274 | 'redeploy ': *mvn 275 | 'sonar:sonar': *mvn 276 | 'site:': 277 | 'site ': *mvn 278 | 'stage ': *mvn 279 | 'versions:': 280 | 'display-dependency-updates ': *mvn 281 | 'display-plugin-updates ': *mvn 282 | 'display-property-updates ': *mvn 283 | 'set ': &versions_set 284 | '-D': 285 | 'artifactIdn=': 286 | '.+ ': *versions_set 287 | 'groupId=': 288 | '.+ ': *versions_set 289 | 'newVersion=': 290 | '.+ ': *versions_set 291 | 'newVersion=': 292 | '.+ ': *versions_set 293 | 'oldVersion=': 294 | '.+ ': *versions_set 295 | 'rulesUri=': 296 | '.+ ': *versions_set 297 | 'serverId=': 298 | '.+ ': *versions_set 299 | 'allowSnapshots=': 300 | 'true ': *versions_set 301 | 'false ': *versions_set 302 | 'generateBackupPoms=': 303 | 'true ': *versions_set 304 | 'false ': *versions_set 305 | 'processDependencies=': 306 | 'true ': *versions_set 307 | 'false ': *versions_set 308 | 'processParent=': 309 | 'true ': *versions_set 310 | 'false ': *versions_set 311 | 'processPlugins=': 312 | 'true ': *versions_set 313 | 'false ': *versions_set 314 | 'processProject=': 315 | 'true ': *versions_set 316 | 'false ': *versions_set 317 | 'updateMatchingVersions=': 318 | 'true ': *versions_set 319 | 'false ': *versions_set 320 | 'revert ': *mvn 321 | 'commit ': *mvn 322 | 'dependency:': 323 | 'tree ': *mvn 324 | 'analyze ': *mvn 325 | -------------------------------------------------------------------------------- /completions/rustc.yml: -------------------------------------------------------------------------------- 1 | "rustc ": &rustc 2 | "--help ": &help 3 | "-v ": 4 | "-h ": *help 5 | "-V ": 6 | "--version ": 7 | "--cfg ": 8 | '.+ ': *rustc 9 | "-L ": &L 10 | "KIND=": *L 11 | ' ': *rustc 12 | "-l ": &l 13 | "KIND=": *l 14 | "dylib ": *rustc 15 | "static ": *rustc 16 | "framework ": *rustc 17 | "--crate-type ": 18 | "bin " : *rustc 19 | "lib " : *rustc 20 | "rlib " : *rustc 21 | "dylib " : *rustc 22 | "staticlib " : *rustc 23 | "--crate-name ": 24 | '.+ ': *rustc 25 | "--emit ": &emit 26 | "asm" : 27 | ",": *emit 28 | " ": *rustc 29 | "llvm-bc" : 30 | ",": *emit 31 | " ": *rustc 32 | "llvm-ir" : 33 | ",": *emit 34 | " ": *rustc 35 | "obj" : 36 | ",": *emit 37 | " ": *rustc 38 | "link" : 39 | ",": *emit 40 | " ": *rustc 41 | "dep-info" : 42 | ",": *emit 43 | " ": *rustc 44 | "--print ": &print 45 | "crate-name" : 46 | ",": *print 47 | " ": *rustc 48 | "file-names" : 49 | ",": *print 50 | " ": *rustc 51 | "sysroot" : 52 | ",": *print 53 | " ": *rustc 54 | "-g ": *rustc 55 | "-O ": *rustc 56 | "-o ": 57 | ' ': *rustc 58 | "--out-dir ": 59 | ' ': *rustc 60 | "--explain ": *rustc 61 | "--test ": *rustc 62 | "--target ": 63 | "TRIPLE ": *rustc 64 | "--warn ": &warn 65 | "help ": 66 | '.+ ': *rustc 67 | "-W ": *warn 68 | "--allow ": &allow 69 | '.+ ': *rustc 70 | "-A ": *allow 71 | "--deny ": &deny 72 | '.+ ': *rustc 73 | "-D ": *deny 74 | "--forbid ": &forbid 75 | '.+ ': *rustc 76 | "-F ": *forbid 77 | "--codegen ": &codegen 78 | "help ": 79 | '.+ ': *rustc 80 | "-C ": *codegen 81 | "-v ": *rustc 82 | "--verbose ": *rustc 83 | "-Z ": 84 | "help ": 85 | 86 | -------------------------------------------------------------------------------- /completions/vagrant.yml: -------------------------------------------------------------------------------- 1 | 'vagrant ': 2 | '--version ': 3 | '-v ': 4 | '--help ': 5 | '-h ': 6 | 'help ': 7 | 'box ': 8 | 'add ': 9 | '--help ': 10 | '-h ': 11 | 'list ': 12 | '--help ': 13 | '-h ': 14 | 'outdated ': 15 | '--help ': 16 | '-h ': 17 | 'remove ': 18 | '--help ': 19 | '-h ': 20 | 'repackage ': 21 | '--help ': 22 | '-h ': 23 | 'update ': 24 | '--help ': 25 | '-h ': 26 | 27 | 'connect ': &connect 28 | '--disable-static-ip ': *connect 29 | '--static-ip ': 30 | '.+ ': *connect 31 | '--ssh ': *connect 32 | '--help ': 33 | '-h ': 34 | '.+ ': 35 | 36 | 'destroy ': &destroy 37 | '--force ': *destroy 38 | '-f ': *destroy 39 | '--help ': 40 | '-h ': 41 | '.+ ': 42 | 43 | 'docker-logs ': &docker-logs 44 | '--follow ': *docker-logs 45 | '--no-follow ': *docker-logs 46 | '--prefix ': *docker-logs 47 | '--no-prefix ': *docker-logs 48 | '--help ': 49 | '-h ': 50 | 51 | 'docker-run ': &docker-run 52 | '--detach ': *docker-run 53 | '--no-detach ': *docker-run 54 | '--tty ': *docker-run 55 | '--no-tty ': *docker-run 56 | '-t ': *docker-run 57 | '--help ': 58 | '-h ': 59 | '.+ ': 60 | 61 | 'global-status ': 62 | '--prune ': 63 | '--help ': 64 | '-h ': 65 | 66 | 'halt ': &halt 67 | '--force ': *halt 68 | '-f ': *halt 69 | '-h ': 70 | '--help ': 71 | '.+ ': 72 | 73 | 'init ': &init 74 | '--force ': *init 75 | '-f ': *init 76 | '--minimal ': *init 77 | '-m ': *init 78 | '--output ': 79 | '- ': *init 80 | ' ': *init 81 | '--help ': 82 | '-h ': 83 | '.+ ': 84 | 85 | 'list-commands ': 86 | '--help ': 87 | '-h ': 88 | 89 | 'login ': 90 | '--check ': 91 | '-c ': 92 | '--logout ': 93 | '-k ': 94 | '--help ': 95 | '-h ': 96 | 97 | 'package ': &package 98 | '--base ': 99 | '.+ ': *package 100 | '--output ': 101 | ' ': *package 102 | '--include ': &package_include 103 | # FILE... Additional files to package with the box 104 | ' ': *package_include 105 | '--vagrantfile ': 106 | ' ': *package 107 | '--help ': 108 | '-h ': 109 | '.+ ': 110 | 111 | 'plugin ': 112 | 'install ': 113 | '--help ': 114 | '-h ': 115 | 'license ': 116 | '--help ': 117 | '-h ': 118 | 'list ': 119 | '--help ': 120 | '-h ': 121 | 'uninstall ': 122 | '--help ': 123 | '-h ': 124 | 'update ': 125 | '--help ': 126 | '-h ': 127 | '--help ': 128 | '-h ': 129 | 130 | 'provision ': &provision 131 | '--parallel ': *provision 132 | '--no-parallel ': *provision 133 | '--provision-with ': 134 | '.+ ': *provision 135 | '--help ': 136 | '-h ': 137 | '.+ ': *provision 138 | 139 | 'rdp ': 140 | '--help ': 141 | '-h ': 142 | '.+ ': 143 | 144 | 'reload ': &reload 145 | '--provision ': *reload 146 | '--no-provision ': *reload 147 | '--provision-with ': 148 | '.+ ': *reload 149 | '--help ': 150 | '-h ': 151 | '.+ ': *reload 152 | 153 | 'resume ': 154 | '--help ': 155 | '-h ': 156 | '.+ ': 157 | 158 | 'rsync ': 159 | '--help ': 160 | '-h ': 161 | 162 | 'rsync-auto ': 163 | '--help ': 164 | '-h ': 165 | 166 | 'share ': &share 167 | '--disable-http ': *share 168 | '--domain ': 169 | '.+ ': *share 170 | '--http ': 171 | '([$].*)|(\d+) ': *share 172 | '--https ': 173 | '([$].*)|(\d+) ': *share 174 | '--name ': 175 | '.+ ': *share 176 | '--ssh ': *share 177 | '--ssh-no-password ': *share 178 | '--ssh-port ': 179 | '([$].*)|(\d+) ': *share 180 | '--ssh-once ': *share 181 | '--help ': 182 | '-h ': 183 | 184 | 'ssh ': &ssh 185 | '--command ': *ssh 186 | '-c ': *ssh 187 | '--plain ': *ssh 188 | '-p ': *ssh 189 | '.+ ': 190 | '--help ': 191 | '-h ': 192 | 193 | 'ssh-config ': &ssh-config 194 | '--host ': *ssh-config 195 | '.+ ': *ssh-config 196 | '--help ': 197 | '-h ': 198 | 199 | 'status ': 200 | '.+ ': 201 | '--help ': 202 | '-h ': 203 | 204 | 'suspend ': 205 | '.+ ': 206 | '--help ': 207 | '-h ': 208 | 209 | 'up ': &up 210 | '--provision ': *up 211 | '--no-provision ': *up 212 | '--provision-with ': 213 | '.+ ': *up 214 | '--destroy-on-error ': *up 215 | '--no-destroy-on-error ': *up 216 | '--parallel ': *up 217 | '--no-parallel ': *up 218 | '--provider ': 219 | '.+ ': *up 220 | '--help ': 221 | '-h ': 222 | '.+ ': 223 | 224 | 'version ': 225 | '--help ': 226 | '-h ': 227 | 228 | -------------------------------------------------------------------------------- /lib/completers/ExecCompleter.rb: -------------------------------------------------------------------------------- 1 | #################### 2 | ### this class executes its rawchoice in a bash shell and returns the results 3 | ### the variable $ASKBASH_TOKEN will be available in the shell 4 | #################### 5 | 6 | class ExecCompleter < DynamicCompleter 7 | def deriveChoices(rawchoice, token) 8 | log "deriveChoices '#{token}'" 9 | cmd = "export ASKBASH_TOKEN=#{token}; #{rawchoice}" 10 | answer = runShellCommand(cmd) 11 | if ! rawchoice.nil? && ! answer.nil? && answer.length > 0 && rawchoice =~ /.*\s$/ 12 | answer.map! {|a| 13 | "#{a} " 14 | } 15 | end 16 | answer 17 | end 18 | end 19 | 20 | -------------------------------------------------------------------------------- /lib/completers/FileCompleter.rb: -------------------------------------------------------------------------------- 1 | ################### 2 | ### this completer is for completing a local file path 3 | ################### 4 | 5 | class FileCompleter < DynamicCompleter 6 | def deriveChoices(rawChoice, token) 7 | if ! token.nil? && token.length > 0 && token[-1] == " " 8 | return [ token ] 9 | end 10 | 11 | tok = if token.nil? then "" else token.strip end 12 | log "about to glob '#{tok}'" 13 | answer = Dir.glob("#{tok}*") 14 | answer.map! {|a| 15 | if File.directory?(a) 16 | "#{a}/" 17 | else 18 | "#{a} " 19 | end 20 | } 21 | if answer.length < 1 && File.exist?(tok) 22 | answer = [token] 23 | end 24 | 25 | answer.map! {|a| 26 | a.gsub(/([^\\])(\s)(?!$)/, '\\1' + '\\\\' + '\\2') 27 | } 28 | log "glob returned #{answer}" 29 | return answer 30 | end 31 | 32 | def abbreviate(rawChoice, choice, token) 33 | File.basename(choice.strip) 34 | end 35 | 36 | end 37 | 38 | 39 | -------------------------------------------------------------------------------- /lib/completers/FilesCSVCommaCompleter.rb: -------------------------------------------------------------------------------- 1 | ################### 2 | ### DEPRECATED - this class remains purely for legacy purposes 3 | ### instead, use an ordinary FilesCompleter and specify comma as its separator in your yaml like this: ", " 4 | ### this completer is for completing one or more local file paths separated by a comma 5 | ################### 6 | require_relative 'FilesCompleter.rb' 7 | 8 | 9 | class FilesCSVCommaCompleter < FilesCompleter 10 | 11 | def separator(rawChoice) 12 | ',' 13 | end 14 | 15 | end 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/completers/FilesCompleter.rb: -------------------------------------------------------------------------------- 1 | ################### 2 | ### this completer is for completing one or more local file paths separated by an arbitrary token separator 3 | ################### 4 | require_relative 'FileCompleter.rb' 5 | 6 | 7 | class FilesCompleter < DynamicCompleter 8 | 9 | def initialize(choiceTree) 10 | @tree = choiceTree 11 | @fc = FileCompleter.new(choiceTree) 12 | end 13 | 14 | def deriveChoices(rawChoice, token) 15 | if ! token.nil? && token.length > 0 && token[-1] == " " 16 | return [ token ] 17 | end 18 | sep = separator(rawChoice) 19 | toks = token.split(sep) 20 | if(toks.size > 1) 21 | prefix = toks[0..-2].join(sep) 22 | prefix += sep 23 | tok = toks[-1] 24 | answer = @fc.deriveChoices(rawChoice, tok) 25 | if(!answer.nil?) 26 | answer.map! {|a| 27 | prefix + a 28 | } 29 | end 30 | else 31 | answer = @fc.deriveChoices(rawChoice, token) 32 | end 33 | 34 | return answer 35 | end 36 | 37 | def abbreviate(rawChoice, choice, token) 38 | toks = choice.split(separator(rawChoice)) 39 | tok = toks[-1] 40 | File.basename(tok.strip) 41 | end 42 | 43 | def separator(rawChoice) 44 | if rawChoice.nil? || rawChoice.length == 0 45 | return ',' 46 | else 47 | return rawChoice.strip 48 | end 49 | end 50 | 51 | end 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/completers/PwdDirCompleter.rb: -------------------------------------------------------------------------------- 1 | ############# 2 | ### matches dirs from the pwd 3 | ############# 4 | 5 | class PwdDirCompleter < DynamicCompleter 6 | 7 | def deriveChoices(choice, token) 8 | Dir.pwd.sub(/^\//, '').split('/').map { |s| "#{s} " } 9 | end 10 | 11 | end 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/completers/RegexCompleter.rb: -------------------------------------------------------------------------------- 1 | ############# 2 | ### matches by treating choice as a regex for the token. if the regex accepts the token, then the token itself is returned as a choice 3 | ############# 4 | 5 | class RegexCompleter < DynamicCompleter 6 | 7 | def deriveChoices(choice, token) 8 | if token.nil? 9 | [] 10 | elsif match = token.match( /^(#{choice})$/) 11 | log "regex consumed #{choice} from #{token}" 12 | [token] 13 | else 14 | log "regex did not match #{choice} to #{token}" 15 | [] 16 | end 17 | end 18 | 19 | end 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/config-search.rb: -------------------------------------------------------------------------------- 1 | ######################## 2 | ### manages the process of locating completion config files 3 | ######################## 4 | 5 | module ConfigSearch 6 | 7 | COMPLETION_CONF_DIRS = [ 8 | "#{ENV['HOME']}/.askbash", 9 | ENV['ASKBASH_HOME'], 10 | # File.dirname(__FILE__) 11 | ] 12 | 13 | CompletionConf = Struct.new(:command, :configfile) 14 | 15 | def allCompletionConfs(locations) 16 | answer = [] 17 | dupcheck = {} 18 | 19 | locations.each {|loc| 20 | completionDir = "#{loc}/completions" 21 | log "searching for completion configs in #{completionDir}" 22 | confs = Dir.glob("#{completionDir}/*.yml") 23 | if ! confs.nil? 24 | confs.each { |c| 25 | cmd = File.basename(c) 26 | cmd = cmd[0, cmd.length - '.yml'.length] 27 | if dupcheck[cmd].nil? 28 | dupcheck[cmd]=true 29 | log "mapping #{cmd} -> #{c}" 30 | answer.push CompletionConf.new(cmd, c) 31 | end 32 | } 33 | end 34 | } 35 | return answer 36 | end 37 | 38 | def completionConfSearch(locations) 39 | compline = ENV['COMP_LINE'].split 40 | prog = compline[0] 41 | log "loading completions for #{prog}" 42 | locations.each {|loc| 43 | file = "#{loc}/completions/#{prog}.yml" 44 | if ! loc.nil? && File.exist?(file) 45 | return file 46 | else 47 | log "not found: #{file}" 48 | end 49 | } 50 | return nil 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /lib/core.rb: -------------------------------------------------------------------------------- 1 | ##################################################################### 2 | ### this is the main program which gathers and outputs the completion 3 | ##################################################################### 4 | 5 | require 'yaml' 6 | 7 | require_relative 'universal-utils.rb' 8 | require_relative 'tokenize.rb' 9 | 10 | include UniversalUtils 11 | 12 | 13 | class String 14 | def tokenize 15 | a = Assembly.new self 16 | a.parse 17 | end 18 | end 19 | 20 | TrailCrumb = Struct.new(:completer, :node, :consumed) 21 | 22 | class AbstractCompleter 23 | def initialize(choiceTree) 24 | @tree = choiceTree 25 | end 26 | 27 | def deriveChoices(choice, token) 28 | [choice] 29 | end 30 | 31 | # this method is used for long completions when tab is hit twice and multiple possible continuations are shown -- in this case it's nicer to display just the continuation rather than the entire full form choice 32 | # for example if you've typed "/usr/bin/m" and the possible continuations are ["/usr/bin/mysql", "/usr/bin/mongodb"] then on screen its nicer to just show ["mysql", "mongodb"] 33 | def abbreviate(rawChoice, choice, token) 34 | choice 35 | end 36 | 37 | def rawChoiceConsumesToken?(choice, token) 38 | expansion = deriveChoices(choice, token) 39 | if expansion.nil? 40 | log "expansion was nil using choice '#{choice}' and token '#{token}'" 41 | end 42 | expansion.each {|exp| 43 | if ! token.nil? 44 | remainder = derivedChoiceConsumesToken?(exp, token) 45 | if ! remainder.nil? 46 | log "- #{exp} consumed from #{token} with remainder '#{remainder}'" 47 | return remainder 48 | end 49 | end 50 | } 51 | #log "'#{token}' was not consumed in #{expansion}" 52 | return nil 53 | end 54 | 55 | # this method is for consuming the existing tokens to the left of the current token 56 | def derivedChoiceConsumesToken?(derivedChoice, token) 57 | if token.nil? 58 | return nil 59 | end 60 | if token.start_with? derivedChoice 61 | tt = token.dup 62 | tt.slice! derivedChoice 63 | return tt 64 | else 65 | return nil 66 | end 67 | end 68 | 69 | # this method is for filtering matching choices based on the current incomplete token 70 | # how does it differ from the derivedChoiceConsumesToken? method? 71 | # if choice is "red" and token is "r", then this method will return true, indicating that "red" is indeed a potential completion of "r" 72 | # the derivedChoiceConsumesToken? method on the other hand would return nil (ie false) because "red" cannot consume the token "r" 73 | def isChoicePotentialCompletion(derivedChoice, token) 74 | if token.nil? 75 | return true 76 | end 77 | derivedChoice.start_with? token 78 | end 79 | 80 | def self.isMyType(choice) 81 | self.class.name == self.parseTypeName(choice) 82 | end 83 | 84 | def self.parseTypeName(rawChoice) 85 | if (! rawChoice.nil?) && (match = rawChoice.match(/^<([A-Z].*)>(.*)\s*$/i)) 86 | answer = "#{match.captures[0]}Completer" 87 | log "#{rawChoice} is of type #{answer}" 88 | return answer 89 | else 90 | answer = StaticCompleter.name 91 | log "#{rawChoice} defaulted to type #{answer}" 92 | return answer 93 | end 94 | end 95 | 96 | # this returns the "data" of a choice -- for a normal static choice its the choice itself, but for a dynamic choice it's everything after the <> 97 | # so if the choice node is "fruit " this will return "fruit " 98 | # but if the choice node is "123 " then this returns "123" 99 | def self.content(rawChoice) 100 | if match = rawChoice.match(/^<([A-Z].*)>(.*)\s*$/i) 101 | return match.captures[1] 102 | else 103 | return rawChoice 104 | end 105 | end 106 | 107 | def self.isMultiPart(choice) 108 | ! (choice =~ /\s$/) 109 | end 110 | 111 | end 112 | 113 | class StaticCompleter < AbstractCompleter 114 | end 115 | 116 | class DynamicCompleter < AbstractCompleter 117 | end 118 | 119 | 120 | class ChoiceTree 121 | attr_accessor :currentNode 122 | 123 | def initialize(configfile) 124 | @conf = YAML::load_file(configfile) 125 | @conf.freeze 126 | @currentNode = @conf 127 | @completerCache = {} 128 | @consumptionTrail = [] 129 | end 130 | 131 | def completerFactory(classname) 132 | answer = @completerCache[classname] 133 | if answer.nil? 134 | comp = completerClassFromString(classname) 135 | answer = comp.new self 136 | @completerCache[classname] = answer 137 | end 138 | 139 | answer 140 | end 141 | 142 | def categorizeChoices 143 | answer = [[],[]] 144 | @currentNode.keys.each{|k| 145 | if DynamicCompleter.isMyType k 146 | answer[1].push k 147 | else 148 | answer[0].push k 149 | end 150 | } 151 | return answer 152 | end 153 | 154 | # returns nil when not consumed, otherwise returns a string indicating what remained after consumption (will typically be an empty string, indicating that the token was fully consumed) 155 | # TODO: consider just always returning what remains, so instead of nil you'd return the full token to indicate non-consumption 156 | # purpose: every time you hit tab to invoke auto-completion, this ruby script is invoked from scratch, so this program essentially must operate statelessly. Thus each time you must walk through the yaml completion tree from the beginning to re-figure out where in the tree you currently are. This method is used as part of that tree navigation, so basically we start from root node and then recursively look for subnodes that can consume our token stack. Once we cant consume any more then we've reached the current node and we then switch over to the matchingCandidates method to suggest to the user potential continuations from here 157 | def consume?(token) 158 | log "trying to consume '#{token}'" 159 | 160 | if @currentNode.nil? 161 | log "failed to fully consume '#{token}' from nil location reached via #{@consumptionTrail} " 162 | return nil 163 | end 164 | 165 | if @currentNode.length > 0 166 | # for efficiency, first handle the quick n easy static cases 167 | simpleC, dynamicC = categorizeChoices 168 | 169 | remainder = consumeChoices?(simpleC, token) 170 | return remainder if ! remainder.nil? 171 | 172 | remainder = consumeChoices?(dynamicC, token) 173 | return remainder if ! remainder.nil? 174 | end 175 | log "failed to consume '#{token}' from location " 176 | log @currentNode.keys 177 | return nil 178 | end 179 | 180 | def consumeChoices?(choices, token) 181 | choices.each {|k| 182 | comp = completer k 183 | remainder = comp.rawChoiceConsumesToken?(comp.class.content(k), token) 184 | if ! remainder.nil? 185 | log "was at #{@currentNode.keys}" 186 | log "trying to move to #{k}" 187 | @currentNode = @currentNode[k] 188 | log "about to truncate '#{remainder}' from the end of '#{token}'" 189 | cIndex = [(token.length - remainder.length - 1), 0].max 190 | consumed = token[0..cIndex] 191 | @consumptionTrail.push TrailCrumb.new(comp, @currentNode, consumed) 192 | if ! @currentNode.nil? 193 | log "now at #{@currentNode.keys}" 194 | else 195 | log "now at the end of the road" 196 | end 197 | return remainder 198 | end 199 | } 200 | return nil 201 | end 202 | 203 | # only relevant for multi-part completions, eg: 204 | # color: 205 | # =green: 206 | # =blue: 207 | # here when completing =green or =blue we want to know that this completion is really a continuation of color 208 | # so this method would return "color" in this case 209 | def completionPrefix() 210 | index = @consumptionTrail.length() -1 211 | answer = '' 212 | loop do 213 | break if index < 0 214 | node = @consumptionTrail[index] 215 | if ! node.completer.kind_of?(StaticCompleter) 216 | log "parent was not StaticCompleter #{node.completer}" 217 | break 218 | end 219 | if ! AbstractCompleter.isMultiPart(node.consumed) 220 | log "parent was not multi-part: #{node.consumed}" 221 | break 222 | end 223 | log "gathering from parent #{node.consumed}" 224 | answer.insert(0, node.consumed) 225 | index -= 1 226 | end 227 | return answer 228 | end 229 | 230 | # this is used to generate potential continuations for the text you've typed so far 231 | def matchingCandidates(token) 232 | candidates(@currentNode, token) 233 | end 234 | 235 | def candidates(node, token) 236 | answer = [] 237 | abbrevAnswer = [] 238 | if node.kind_of?(Hash) && ! node.keys.nil? 239 | 240 | node.keys.each {|candidate| 241 | compltr = completer candidate 242 | nodeContent = compltr.class.content(candidate) 243 | log "deriving all choices for token '#{token}' having node content #{nodeContent}" 244 | addCandidates = compltr.deriveChoices(nodeContent, token) 245 | 246 | log "about to select matches for '#{token}' from #{addCandidates}" 247 | addCandidates.select! {|c| 248 | compltr.isChoicePotentialCompletion(c, token) 249 | } 250 | answer.concat addCandidates 251 | 252 | #now that we've generated the matching candidates, lets also create abbreviations for them 253 | addCandidates.each{|c| 254 | abbr = compltr.abbreviate(nodeContent, c, token) 255 | if ! abbr.nil? 256 | abbrevAnswer.push abbr 257 | end 258 | } 259 | } 260 | end 261 | log "derived choices to #{answer}" 262 | 263 | # when there are multiple continuations, we prefer to show them abbreviated when possible 264 | # but theres a special case when all of the continuation choices share any common prefix then bash immediately prints the common prefix 265 | # thus in that case we need to show the full form so that this behavior does not wipe out what we've typed so far 266 | # (in other words, we must accept that this behavior will occur so we therefore tack on what's been typed already to each choice so that it gets preserved) 267 | if answer.length > 1 && ! shareACommonPrefix?(abbrevAnswer) 268 | answer = abbrevAnswer 269 | log "abbreviated to #{answer}" 270 | else 271 | prefix = completionPrefix() 272 | if prefix.length > 0 273 | answer.map!{|c| 274 | "#{prefix}#{c}" 275 | } 276 | end 277 | log "special case: could not abbreviate" 278 | end 279 | return answer 280 | 281 | end 282 | 283 | def shareACommonPrefix?(arr) 284 | if arr.nil? || arr.length < 1 285 | return false 286 | end 287 | 288 | firstLetter = nil 289 | arr.each {|s| 290 | if firstLetter.nil? 291 | firstLetter = s[0] 292 | else 293 | if firstLetter != s[0] 294 | return false 295 | end 296 | end 297 | } 298 | return true 299 | end 300 | 301 | # fetches an appropriate Completer subclass to interpret the given choice node 302 | def completer(choice) 303 | if choice.nil? 304 | completerFactory(StaticCompleter.name) 305 | else 306 | comp = completerFactory( AbstractCompleter.parseTypeName choice ) 307 | log "using ruby class #{comp.to_s}" 308 | comp 309 | end 310 | end 311 | 312 | def rollback() 313 | if @consumptionTrail.length < 1 314 | return 315 | end 316 | 317 | log "rolling back from #{@currentNode}" 318 | @consumptionTrail.pop 319 | @currentNode = @consumptionTrail[-1].node 320 | log "rolled back to #{@currentNode}" 321 | end 322 | 323 | end 324 | 325 | class AutoCompleter 326 | def initialize(configfile, rawinput) 327 | log "parsing rawinput #{rawinput}" 328 | @conf = ChoiceTree.new(configfile) 329 | if rawinput.nil? || rawinput.length <=1 330 | rawinput = '' 331 | end 332 | @rawinput = rawinput 333 | @input = rawinput.tokenize.map! {|i| "#{i} "} 334 | #@input = rawinput.split.map! {|i| "#{i} "} 335 | 336 | ### strip space from @input if needed to match @rawinput 337 | if @rawinput[-1] != " " 338 | @input[-1].strip! 339 | end 340 | @tokenIndex = 0 341 | end 342 | 343 | # the main processing loop for the whole program 344 | def parse 345 | @input.reverse! 346 | loop do 347 | if @input.empty? 348 | break 349 | end 350 | t = @input.pop 351 | t.freeze 352 | remainder = @conf.consume?(t) 353 | if remainder.nil? 354 | log "+ #{t} not consumed" 355 | return @conf.matchingCandidates(t) 356 | elsif remainder.strip.length > 0 357 | @input.push remainder 358 | end 359 | end 360 | return @conf.matchingCandidates(nil) 361 | end 362 | 363 | end 364 | -------------------------------------------------------------------------------- /lib/testtools.rb: -------------------------------------------------------------------------------- 1 | module TestTools 2 | 3 | def expectedIs(a, b) 4 | b.sort! 5 | output = AutoCompleter.new(@config, a).parse.sort! 6 | if output != b 7 | puts caller 8 | puts "for input: #{a}" 9 | puts "#{output} != #{b}" 10 | puts ">>> Test FAILED <<<" 11 | exit 12 | end 13 | end 14 | 15 | def expectedContains(a, b) 16 | output = AutoCompleter.new(@config, a).parse 17 | if ! output.member? b 18 | puts caller 19 | puts "for input: #{a}" 20 | puts "#{output} did not contain '#{b}'" 21 | puts ">>> Test FAILED <<<" 22 | exit 23 | end 24 | end 25 | 26 | def expectedLength(a, b) 27 | output = AutoCompleter.new(@config, a).parse 28 | if output.length != b 29 | puts caller 30 | puts "for input: #{a}" 31 | puts "#{output} did not meet expected length of #{b}" 32 | puts ">>> Test FAILED <<<" 33 | exit 34 | end 35 | end 36 | 37 | def expectedContainsNot(a, b) 38 | output = AutoCompleter.new(@config, a).parse 39 | if output.member? b 40 | puts caller 41 | puts "for input: #{a}" 42 | puts "#{output} should not contain '#{b}'" 43 | puts ">>> Test FAILED <<<" 44 | exit 45 | end 46 | end 47 | 48 | def testArraysEqual(a, b) 49 | a.sort! 50 | b.sort! 51 | if a != b 52 | puts caller 53 | puts "#{a} != #{b}" 54 | puts ">>> Test FAILED <<<" 55 | exit 56 | end 57 | end 58 | 59 | def testArrayContains(a, b) 60 | if ! a.member? b 61 | puts caller 62 | puts "#{a} did not contain #{b}" 63 | puts ">>> Test FAILED <<<" 64 | exit 65 | end 66 | end 67 | 68 | 69 | end 70 | -------------------------------------------------------------------------------- /lib/tokenize.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | class Assembly 4 | 5 | attr_accessor :parsers, :text, :result 6 | 7 | def initialize(text) 8 | @text = text 9 | @parsers = [] 10 | @result = [] 11 | end 12 | 13 | def makeView 14 | AssemblyView.new self 15 | end 16 | 17 | def pushTerm(term) 18 | if term.nil? || term =~ /^\s*$/ 19 | return 20 | end 21 | @result.push term 22 | end 23 | 24 | def parse 25 | graph = TokenStateGraph.new 26 | graph.parse self 27 | 28 | return result 29 | end 30 | end 31 | 32 | class AssemblyView 33 | 34 | attr_accessor :consumedIndex, :assembly 35 | 36 | def initialize(assembly, consumedIndex=0, currentWordStart=consumedIndex) 37 | @assembly = assembly 38 | @consumedIndex = consumedIndex 39 | @currentWordStart = currentWordStart 40 | end 41 | 42 | def consume(numOfChars) 43 | @consumedIndex += numOfChars 44 | end 45 | 46 | def unconsume(numOfChars) 47 | @consumedIndex -= numOfChars 48 | end 49 | 50 | def consumable(idx=@consumedIndex) 51 | @assembly.text.slice(idx, @assembly.text.length - idx) 52 | end 53 | 54 | def isFinished(idx=@consumedIndex) 55 | idx >= @assembly.text.length 56 | end 57 | 58 | def consumed(idx=@consumedIndex) 59 | @assembly.text.slice(0, idx) 60 | end 61 | 62 | def peek(idx=0) 63 | if consumable.nil? || consumable.length == 0 64 | return "" 65 | end 66 | answer = consumable[idx] 67 | if consumable.length > 1 && answer == '\\' 68 | answer += consumable[idx + 1] 69 | log "escaped char found: '#{answer}'" 70 | end 71 | 72 | return answer 73 | end 74 | 75 | def lookAhead(idx) 76 | return consumable.slice(0,idx) 77 | end 78 | 79 | def popChar 80 | answer = peek 81 | consume answer.length 82 | end 83 | 84 | def peekWord 85 | @assembly.text.slice(@currentWordStart, @consumedIndex - @currentWordStart) 86 | end 87 | 88 | def popWord 89 | answer = peekWord 90 | log "popping word length #{answer.length}: #{answer}" 91 | @currentWordStart = @consumedIndex 92 | return answer 93 | end 94 | 95 | def copy 96 | AssemblyView.new(@assembly, @consumedIndex, @currentWordStart) 97 | end 98 | end 99 | 100 | class TokenStateGraph 101 | 102 | def initialize 103 | @default = nil 104 | 105 | #note that a simple '.*' regex is not adequate because we need to allow for presence of \' 106 | @apostrophe = PairedToken.new("'") 107 | 108 | #note that a simple ".*" regex is not adequate because we need to allow for presence of \" 109 | @quote = PairedToken.new('"') 110 | 111 | @space = RegexToken.new('\s+') 112 | 113 | #note that this needs to go char by char to allow for things like: wow' this is 'interesting 114 | @word = OneAtATimeToken.new('\S') 115 | 116 | #@default.allowedTransitions = [@apostrope, @quote, @space, @word] 117 | #@apostrophe.allowedTransitions = [] 118 | #@quote.allowedTransitions = [] 119 | #@space.allowedTransitions = @default.allowedTransitions 120 | #@word.allowedTransitions = @default.allowedTransitions 121 | 122 | #@stateStack = [] 123 | #@stateStack.push @default 124 | 125 | @current = @default 126 | end 127 | 128 | def parse(assembly) 129 | log "starting scanner/tokenize of #{assembly.text}" 130 | view = assembly.makeView 131 | 132 | loop do 133 | if view.isFinished 134 | assembly.pushTerm(view.popWord) 135 | break 136 | end 137 | 138 | if (howMany = @apostrophe.accepts(view)) > 0 139 | view.consume howMany 140 | @current = @apostrophe 141 | next 142 | elsif(howMany = @quote.accepts(view)) > 0 143 | view.consume howMany 144 | @current = @quote 145 | next 146 | elsif (howMany = @space.accepts(view)) > 0 147 | if @current != @space 148 | assembly.pushTerm(view.popWord) 149 | end 150 | view.consume howMany 151 | #throw away the empty space word 152 | view.popWord 153 | @current = @space 154 | next 155 | elsif (howMany = @word.accepts(view)) > 0 156 | view.consume howMany 157 | @current = @word 158 | next 159 | end 160 | log("unable to parse further, got as far as #{view.consumed}<<>>#{view.consumable}") 161 | break 162 | end 163 | log "finished scanner/tokenize phase" 164 | log "----------------------------" 165 | end 166 | end 167 | 168 | class GenericToken 169 | attr_accessor :allowedTransitions 170 | end 171 | 172 | class PairedToken < GenericToken 173 | # a regex describing which character(s) permit this state change to happen 174 | def initialize(startChar, endChar = startChar) 175 | @startChar = startChar 176 | @endChar = endChar 177 | end 178 | 179 | def accepts(assemblyView) 180 | answer = 0 181 | c = assemblyView.peek 182 | if c != @startChar 183 | return 0 184 | else 185 | answer += c.length 186 | end 187 | 188 | loop do 189 | if assemblyView.isFinished(answer) 190 | log "#{@startChar}.*#{@endChar} accepted #{assemblyView.lookAhead(answer)}" 191 | log "word is #{assemblyView.peekWord}" 192 | return answer 193 | end 194 | c = assemblyView.peek(answer) 195 | if c == @endChar || c.nil? 196 | if ! c.nil? 197 | answer += c.length 198 | end 199 | log "#{@startChar}.*#{@endChar} accepted #{assemblyView.lookAhead(answer)}" 200 | log "word is #{assemblyView.peekWord}" 201 | return answer 202 | else 203 | answer += c.length 204 | end 205 | end 206 | log "#{@startChar}.*#{@endChar} accepted #{assemblyView.lookAhead(answer)}" 207 | log "word is #{assemblyView.peekWord}" 208 | return answer 209 | end 210 | end 211 | 212 | class RegexToken < GenericToken 213 | def initialize(regex) 214 | @regex = regex 215 | end 216 | 217 | def accepts(assemblyView) 218 | if match = assemblyView.consumable.match( /^(#{@regex})/ ) 219 | answer = match.captures[0] 220 | log "'#{@regex}' accepted #{answer}" 221 | return answer.length 222 | else 223 | return 0 224 | end 225 | end 226 | end 227 | 228 | class OneAtATimeToken < GenericToken 229 | def initialize(regex) 230 | @regex = regex 231 | end 232 | 233 | def accepts(assemblyView) 234 | c = assemblyView.peek 235 | if c =~ /#{@regex}/ 236 | log "'#{@regex}' accepted #{c}" 237 | return c.length 238 | else 239 | return 0 240 | end 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /lib/universal-utils.rb: -------------------------------------------------------------------------------- 1 | module UniversalUtils 2 | 3 | def debugMode? 4 | ENV["ASKBASH_DEBUG"] 5 | end 6 | 7 | def log(msg) 8 | debug = debugMode? 9 | if debug 10 | logfile = 'askbash.log' 11 | if ! ENV['ASKBASH_HOME'].nil? 12 | logfile = "#{ENV['ASKBASH_HOME']}/#{logfile}" 13 | end 14 | open(logfile, 'a') { |f| 15 | if debug == "2" 16 | f.puts caller[2], msg 17 | elsif debug.to_i > 2 18 | f.puts caller, msg 19 | else 20 | f.puts msg 21 | end 22 | } 23 | end 24 | end 25 | 26 | def completerClassFromString(str) 27 | completerClassname = "#{str}" 28 | file = "#{File.dirname(__FILE__)}/completers/#{completerClassname}.rb" 29 | #load class on demand. ruby file must be named same as the ruby class + ".rb" 30 | if File.exist? file 31 | require_relative file 32 | else 33 | log "#{file} not found while instantiating #{completerClassname}" 34 | end 35 | classFromString completerClassname 36 | end 37 | 38 | def classFromString(str) 39 | str.split('::').inject(Object) do |mod, class_name| 40 | mod.const_get(class_name) 41 | end 42 | end 43 | 44 | def runShellCommand(cmd) 45 | log "abt to exec #{cmd}" 46 | answer = `#{cmd}`.split 47 | log "got answer #{answer}" 48 | #TODO: handle error condition 49 | return answer 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /releasenotes.md: -------------------------------------------------------------------------------- 1 | release notes for version 0.6.4 2 | - accomodate spaces in $ASKBASH_HOME path if necessary (still not a recommended practice though) 3 | 4 | This project intends to adhere to semantic versioning after 1.0.0, but not prior to this. 5 | -------------------------------------------------------------------------------- /test/askdebug.sh: -------------------------------------------------------------------------------- 1 | #echo dang 2 | #echo duh 3 | #echo wow 4 | #echo "$@"; 5 | #echo eclipse:eclipse 6 | echo :eclipse 7 | -------------------------------------------------------------------------------- /test/completions/food.yml: -------------------------------------------------------------------------------- 1 | 'food ': &food 2 | 'fruit ': &fruit 3 | 'orange ': *fruit 4 | 'banana ': *fruit 5 | 'strawberry ': *fruit 6 | 'grape ': 7 | 'green ': 8 | 'red ': 9 | 'grapefruit ' : 10 | 'ruby red ': 11 | 'yellow ': 12 | '--seedless': &seedless 13 | '=true ': *food 14 | '=false ': *food 15 | 'veg ': 16 | '-maybe ': &m 17 | 'avocado ': 18 | 'tomato ': 19 | '-m ': *m 20 | '-certain ': &c 21 | 'broccoli ': 22 | '"brussel sprouts" ': *food 23 | 'asparagus ': 24 | '-c ': *c 25 | 'candy ': 26 | 'skittles:red ': 27 | 'skittles:yellow ': 28 | 'booze:': 29 | 'booze ': 30 | 'meat ': 31 | 'dairy:': 32 | 'cow=': 33 | '.+ ': *m 34 | '-f ': 35 | ' echo $ASKBASH_TOKEN* ': *food 36 | '-r ': 37 | '': *food 38 | '--color ': 39 | 'red ': *food 40 | 'white ': *food 41 | 'blue ': *food 42 | '.+ ': *m 43 | 'other ': 44 | -------------------------------------------------------------------------------- /test/core-test.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | # for use with bash autocompletion 4 | # 5 | # to setup, run this in bash: 6 | # ./askbash.bash 7 | # 8 | # then to use: mycmdtotriggercompletion [tab] 9 | # 10 | # to enable debug logging, run this in your shell to write debug stmts to askbash.log: 11 | # export ASKBASH_DEBUG=1 12 | # to include line numbers use this: 13 | # export ASKBASH_DEBUG=2 14 | 15 | 16 | require_relative '../lib/core.rb' 17 | require_relative '../lib/testtools.rb' 18 | 19 | ENV["ASKBASH_DEBUG"] = "1" 20 | 21 | @config = File.dirname(__FILE__) + '/completions/food.yml' 22 | puts "using config #{@config}" 23 | 24 | include TestTools 25 | 26 | ################################################# 27 | 28 | # test the tokenizer 29 | testArrayContains("wow 'this is' handy".tokenize, "'this is'") 30 | testArrayContains('wow "this is " handy'.tokenize, '"this is "') 31 | testArrayContains('wow this\ is handy'.tokenize, 'this\ is') 32 | testArrayContains("wow 'this is' handy".tokenize, "handy") 33 | testArrayContains("wow 'this is' handy ".tokenize, "handy") 34 | testArrayContains(" wow 'this is' handy ".tokenize, "wow") 35 | testArrayContains("wow 'this is".tokenize, "'this is") 36 | testArraysEqual 'food fruit banana '.split, 'food fruit banana '.tokenize 37 | 38 | # expand space at end 39 | expectedIs 'food -r', ['-r '] 40 | # multiple matching options work 41 | expectedIs 'food veg -m', ['-m ', '-maybe '] 42 | # reference to another completion node 43 | expectedContains 'food fruit banana ', 'banana ' 44 | # differentiate between choices where one name is a substring of the other 45 | expectedContains 'food fruit grape ', 'green ' 46 | expectedContains 'food fruit grapefruit ', 'yellow ' 47 | expectedContains 'food fruit grape', 'grape ' 48 | # ability to break into multiple parts and retrieve previous parts when needed 49 | expectedIs 'food fruit --seedless', ['--seedless=false ', '--seedless=true '] 50 | # ability to consume a partial match in a multi-part completer 51 | expectedIs 'food fruit --seedless=f', ['--seedless=false '] 52 | # dont crash if extra input isnt parsed 53 | expectedIs 'food fruit grape green hare krishna', [] 54 | 55 | # test multilevel matches with repeated string 56 | expectedIs 'food booze:', ['booze:booze '] 57 | expectedIs 'food booze:b', ['booze:booze '] 58 | 59 | 60 | 61 | # should try to match a known color 62 | expectedIs 'food --color r', ['red '] 63 | # but should still accept an unknown color too 64 | expectedContains 'food --color violet ', 'avocado ' 65 | # and should not show the regex choice 66 | expectedLength 'food --color ', 3 67 | # Regex choice still should not show even if its part of a multilevel choice 68 | expectedIs 'food dairy:cow=', [] 69 | 70 | # space should still be respected at the end of a regex 71 | #expectedContains 'food --color violet', 'violet ' 72 | 73 | # direct shell exec completer 74 | expectedIs 'food -f data/wo', ['data/wow '] 75 | 76 | 77 | # file completer drills into a dir 78 | expectedIs 'food -r data/wow', ['data/wow/'] 79 | # file completer can terminate anywhere on the path 80 | expectedContains 'food -r wow ', 'fruit ' 81 | # file completer works with several matches sharing a common prefix (in which case it must return the full match, not the abbreviated, due to a bug in bash) 82 | expectedIs 'food -r data/wow/d', ['data/wow/dang/', 'data/wow/duh/'] 83 | # file completer drills into a dir showing abbreviated matches when possible 84 | expectedIs 'food -r data/wow/', ['dang','duh','huh'] 85 | # file completer returns full match when only one match found 86 | expectedIs 'food -r data/wow/h', ['data/wow/huh/'] 87 | # file completer matches file 88 | expectedIs 'food -r data/wow/huh/ho', ['data/wow/huh/ho '] 89 | # file completer handles spaces in file name 90 | expectedIs 'food -r data/spac', ['data/space\ lab/'] 91 | expectedIs 'food -r data/space\ lab/', ['data/space\ lab/orbit '] 92 | expectedIs 'food -r data/space\ lab/o', ['data/space\ lab/orbit '] 93 | 94 | puts "SUCCESS! All Tests PASSED" 95 | -------------------------------------------------------------------------------- /test/data/space lab/orbit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arikast/askbash/abfdf7733acd6f5240570d0c5d3b680166642cad/test/data/space lab/orbit -------------------------------------------------------------------------------- /test/data/wow/dang/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arikast/askbash/abfdf7733acd6f5240570d0c5d3b680166642cad/test/data/wow/dang/.gitkeep -------------------------------------------------------------------------------- /test/data/wow/duh/day: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arikast/askbash/abfdf7733acd6f5240570d0c5d3b680166642cad/test/data/wow/duh/day -------------------------------------------------------------------------------- /test/data/wow/huh/ha: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arikast/askbash/abfdf7733acd6f5240570d0c5d3b680166642cad/test/data/wow/huh/ha -------------------------------------------------------------------------------- /test/data/wow/huh/ho: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arikast/askbash/abfdf7733acd6f5240570d0c5d3b680166642cad/test/data/wow/huh/ho --------------------------------------------------------------------------------