├── .gitignore ├── MIT-LICENSE ├── README.md ├── doc ├── list.png ├── show.png └── which.png ├── shy └── test ├── fixtures ├── .hidden │ └── plugin4 ├── plugin1.sh ├── plugin2.sh └── plugin3.sh ├── shy.bats └── test_helper.bash /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014 Aaron Royer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shy - minimal shell "plugins" 2 | 3 | Shy is a small tool for managing shell (bash/zsh) confiuration that is split into several files. A "plugin" is merely a file with plain old shell aliases, functions, variables, or anything else - they work the same whether or not Shy is installed. 4 | 5 | Using Shy allows you to do the following: 6 | 7 | * Organize your shell config into groups of related functionality (plugins) in a lightweight way 8 | * Quickly find where things (functions, aliases, variables) are defined 9 | * Open your editor with the appropriate file, to make a quick change 10 | * And (not much) more! 11 | 12 | [Installation](#installation) 13 | [Usage](#usage) 14 | [Why Use This?](#why) 15 | [Command Reference](#command-reference) 16 | [Advanced Configuration](#advanced-configuration) 17 | [Credits](#credits) 18 | 19 | 20 | ## Installation 21 | 22 | 1. Download the [shy script](https://raw2.github.com/aaronroyer/shy/master/shy) and place it somewhere on your PATH (make sure it is executable) 23 | 1. Add ```eval "$(shy init)"``` in .bashrc (or .zshrc if using zsh), somewhere after configuring your PATH 24 | 1. Add some plugins! See [usage](#usage) for more info. 25 | 26 | 27 | ## Usage 28 | 29 | Once you're installed then you can used ```shy load``` to load plugin files. You will usually do this in your .bashrc/.zshrc file. Make sure Shy is initialized before loading plugins. 30 | 31 | ``` 32 | shy load ~/path/to/plugin.sh 33 | ``` 34 | Or maybe something like 35 | 36 | ``` 37 | for plugin in ~/.shell_plugins/* do 38 | shy load $plugin 39 | done 40 | ``` 41 | 42 | Shy will source the files as normal, but also record all of the aliases, functions, and variables first defined in the file. 43 | 44 | Now you can view a list of your plugins. The name of a plugin is the base file name with any extension removed. 45 | 46 | ![Shy printing loaded plugins](doc/list.png) 47 | 48 | You can examine the details of a plugin. 49 | 50 | ![Shy printing the contents of a plugin](doc/show.png) 51 | 52 | Use ```which``` if you want to know where something is defined. 53 | 54 | ``` 55 | $ shy which glb 56 | glb is a function in the plugin git 57 | $ shy which gd 58 | gs is an alias in the plugin git 59 | ``` 60 | 61 | You can open a plugin (or anything else) in your EDITOR. 62 | 63 | ``` 64 | $ shy edit git 65 | # (opens the git plugin source file in your editor) 66 | 67 | $ shy edit gs 68 | # (opens the git plugin source file, where the alias gs is defined) 69 | ``` 70 | 71 | 72 | ## Why Use This? 73 | 74 | You should try Shy if you like to maintain your own shell config, want to split things into separate files, want to be able to keep track of it all and make tweaks with ease, and don't want something heavy to do it. 75 | 76 | If you want lots of crazy/awesome power-user features you might like something like [composure](https://github.com/erichs/composure) instead. If you use zsh and just want to dump a ton of functionality that someone else wrote into your shell and be done with it then you could use [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh). 77 | 78 | I prefer to keep a set of often-used tools that I've put together myself (with little bits I've stolen) and manage them with simple tools. 79 | 80 | 81 | ## Command Reference 82 | 83 | #### shy load 84 | 85 | Load a plugin file. This simply sources the file and Shy records the aliases, functions, and variables first defined in the file. 86 | 87 | This should normally be used in your shell configuration files (like .bashrc or .zshrc) and not interactively. 88 | 89 | --- 90 | 91 | #### shy list 92 | 93 | List the plugins that that have previously been loaded with ```load```. Names of plugins are the plugin file names with any extension removed. 94 | 95 | For example: 96 | 97 | ```~/.shell_plugins/git.sh``` is a plugin named ```git``` 98 | 99 | ```~/.shell_plugins/rails``` is a plugin named ```rails``` 100 | 101 | --- 102 | 103 | #### shy show \ 104 | 105 | Show the details for the plugin. This prints the path of the plugin file, and lists the aliases, functions, and variables defined in the plugin. 106 | 107 | --- 108 | 109 | #### shy edit \ 110 | 111 | Open a plugin file in your editor. When given a plugin name, the plugin file is opened. When given the name of an alias, function, or variable the plugin file where that item is defined is opened in the editor. 112 | 113 | To find an editor to use, Shy first checks the variable ```SHY_EDITOR```, then ```EDITOR```, then falls back on ``vim```. 114 | 115 | --- 116 | 117 | #### shy which \ 118 | 119 | Print what plugin an alias, function, or variable is defined in. 120 | 121 | --- 122 | 123 | #### shy help \[\\] 124 | 125 | Prints help for a specific command, or general help with no command name given. 126 | 127 | 128 | ## Advanced Configuration 129 | 130 | If you want everything to work even if Shy is not installed (like if you sync your dotfiles but don't sync Shy along with it) then you can add a fallback function that simply sources your plugin files. This way your shell works the same even if ```shy``` isn't on your ```PATH```. 131 | 132 | ``` 133 | if which shy &> /dev/null; then 134 | eval "$(shy init)" 135 | else 136 | shy() { [ "$1" = 'load' ] && source "$2"; } 137 | fi 138 | ``` 139 | 140 | ### Environment Variables 141 | 142 | ```SHY_EDITOR``` - sets the editor to use with Shy 143 | 144 | ```SHY_DEBUG``` - prints debug info to standard out when loading plugins 145 | 146 | ```SHY_CACHE_DIR``` - Shy caches plugins in ```~/.shy_plugin_cache```for faster startup; this overrides the default location 147 | 148 | ```SHY_NO_COLOR``` - Set this to anything not empty to prevent Shy from coloring any output 149 | 150 | 151 | ## Credits 152 | 153 | Shy uses [Bats](https://github.com/sstephenson/bats) for automated testing. You should check it out if you're doing anything non-trivial with Bash. 154 | 155 | ## License 156 | 157 | Shy is MIT licensed 158 | -------------------------------------------------------------------------------- /doc/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronroyer/shy/3367bb3a9b84f7e73b33fb209d4493a0e985bda9/doc/list.png -------------------------------------------------------------------------------- /doc/show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronroyer/shy/3367bb3a9b84f7e73b33fb209d4493a0e985bda9/doc/show.png -------------------------------------------------------------------------------- /doc/which.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronroyer/shy/3367bb3a9b84f7e73b33fb209d4493a0e985bda9/doc/which.png -------------------------------------------------------------------------------- /shy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Installation: Make this file is executable and add it to your PATH somewhere. 4 | # Then place the following in your shell rc file. 5 | # 6 | # eval "$(shy init)" 7 | # 8 | # 9 | # shy 0.0.2 10 | # Usage: shy [] 11 | # 12 | # Shy enables minimal shell plugin management 13 | # 14 | # Most useful commands: 15 | # load Load a plugin file 16 | # list List the plugins that have been loaded 17 | # show Show the contents of a plugin 18 | # edit Open a plugin file with your editor 19 | # which Show which plugin a function, alias, or variable belongs to 20 | # 21 | # Use `shy help ' for information on a specific command. 22 | # Full documentation: https://github.com/aaronroyer/shy#readme 23 | set -e 24 | 25 | if [ -t 1 ] && [ -z "$SHY_NO_COLOR" ]; then 26 | c_reset=$(tput sgr0) 27 | c_bold=$(tput bold) 28 | c_blue=$(tput setaf 4) 29 | c_white=$(tput setaf 7) 30 | fi 31 | 32 | ### Utilities 33 | 34 | warn() { 35 | echo "shy: $@" 1>&2; 36 | } 37 | 38 | die() { 39 | warn "$@" 40 | exit 1 41 | } 42 | 43 | debug() { 44 | [ -n "$SHY_DEBUG" ] && warn "$@" 45 | return 0 46 | } 47 | 48 | # Get data for a plugin with the given name 49 | get_plugin_data() { 50 | local name=$1 51 | echo $SHY_PLUGIN_DATA | tr '|' '\n' | grep "^${name};" 52 | } 53 | 54 | plugin_field() { 55 | echo $1 | cut -d ';' -f $2 56 | } 57 | 58 | plugin_name() { plugin_field "$1" 1; } 59 | plugin_file() { plugin_field "$1" 2; } 60 | plugin_aliases() { plugin_field "$1" 3 | tr ':' '\n'; } 61 | plugin_functions() { plugin_field "$1" 4 | tr ':' '\n'; } 62 | plugin_variables() { plugin_field "$1" 5 | tr ':' '\n'; } 63 | 64 | realpath() { 65 | local native=$(type -p grealpath realpath | head -1) 66 | if [ -n "$native" ]; then 67 | $native "$1" 68 | else 69 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" 70 | fi 71 | } 72 | 73 | # Returns 0 if an exact match is included in a list, non-zero otherwise. 74 | includes() { 75 | echo "$1" | grep -q "^$2$" 76 | } 77 | 78 | # Takes a semicolon-delimited list and prints it with a nice title. Prints 79 | # nothing if the list is empty. 80 | fancy_list() { 81 | local items="$1" title="$2" 82 | [ -z "$items" ] && return 83 | echo -e "\n${c_bold}${c_blue}== ${c_white}$title${c_blue} ==${c_reset}" 84 | echo "$items" 85 | } 86 | 87 | singularize_type() { 88 | case "$1" in 89 | aliases) echo alias ;; 90 | functions) echo function ;; 91 | variables) echo variable ;; 92 | esac 93 | } 94 | 95 | # Takes the name of an item (function, alias, or variable) and echos the names 96 | # of any plugins it is included in, if any, followed by the type of item. The 97 | # plugin name and type of item are separated by a colon. 98 | # 99 | # Example 100 | # find_item my_func 101 | # => my_plugin:function 102 | find_item() { 103 | local item_name="$1" 104 | for plugin_name in $(list_plugins); do 105 | local plugin=$(get_plugin_data $plugin_name) 106 | for item_type in aliases functions variables; do 107 | if includes "$(plugin_${item_type} "$plugin")" $item_name; then 108 | echo "$plugin_name:$(singularize_type $item_type)" 109 | return 110 | fi 111 | done 112 | done 113 | return 1 114 | } 115 | 116 | plugin_cache_dir() { 117 | if [ -n "$SHY_CACHE_DIR" ]; then 118 | echo "$SHY_CACHE_DIR" 119 | return 120 | fi 121 | 122 | echo "$HOME/.shy_plugin_cache" 123 | } 124 | 125 | initialize_cache() { 126 | local cache_dir=$(plugin_cache_dir) 127 | if [ ! -d "$cache_dir" ]; then 128 | mkdir "$cache_dir" || { warn "Could not create cache directory: $cache_dir"; return 1; } 129 | fi 130 | } 131 | 132 | plugin_name_for_path() { 133 | basename "${1%.*}" 134 | } 135 | 136 | cache_file_path() { 137 | local plugin_name="$(plugin_name_for_path "$1")" 138 | echo "$(plugin_cache_dir)/$plugin_name" 139 | } 140 | 141 | write_cache() { 142 | local plugin_path="$1" plugin_content="$2" 143 | initialize_cache || return 1 144 | local plugin_cache_file=$(cache_file_path "$plugin_path") 145 | echo "$plugin_content" > "$plugin_cache_file" 146 | } 147 | 148 | read_cache() { 149 | local plugin_path="$1" 150 | [ -d "$(plugin_cache_dir)" ] || return 1 151 | local plugin_cache_file=$(cache_file_path "$plugin_path") 152 | [ -f "$plugin_cache_file" ] || return 1 153 | [ "$plugin_path" -nt "$plugin_cache_file" ] && return 1 154 | cat "$plugin_cache_file" 155 | } 156 | 157 | # Prints comments from this file starting at the given line number and stopping 158 | # when the next line does not start with a comment. 159 | print_comments_starting_at_line() { 160 | cat "$BASH_SOURCE" | tail -n "+$1" | awk '{if (/^#/) print; else exit}' | cut -c3- 161 | echo '' 162 | } 163 | 164 | ### Top-level functions 165 | 166 | # Usage: shy list 167 | # 168 | # Lists the names of plugins that have been previously loaded with 169 | # `shy load '. The name of a plugin is the file name with 170 | # any extension removed. 171 | list_plugins() { 172 | echo $SHY_PLUGIN_DATA | tr '|' '\n' | cut -d ';' -f 1 173 | } 174 | 175 | # Usage: shy show 176 | # 177 | # Prints information about the plugin with the given name, including the 178 | # name and file path, as well as the functions, aliases, and variables 179 | # defined in the plugin. 180 | show_plugin() { 181 | local plugin_data=$(get_plugin_data $1) 182 | [ -z "$plugin_data" ] && die "Unknown plugin name: $1" 183 | local name source_file aliases funcs vars 184 | name=$(plugin_name "$plugin_data") 185 | source_file=$(plugin_file "$plugin_data") 186 | aliases=$(plugin_aliases "$plugin_data") 187 | funcs=$(plugin_functions "$plugin_data") 188 | vars=$(plugin_variables "$plugin_data") 189 | 190 | echo "${c_bold}Plugin name:${c_reset} $name" 191 | echo "${c_bold}Source file:${c_reset} $source_file" 192 | fancy_list "$aliases" 'Aliases' 193 | fancy_list "$funcs" 'Functions' 194 | fancy_list "$vars" 'Variables' 195 | echo '' 196 | } 197 | 198 | # Usage: shy edit 199 | # 200 | # Opens a plugin file in your editor. 201 | # 202 | # If the name of a plugin is given, the file for the plugin with with that 203 | # name is opened. 204 | # 205 | # If the names of a function, alias, or variable defined in a function is 206 | # given, then the plugin file containing the item is opened. 207 | # 208 | # The editor used will be chosen from the SHY_EDITOR environment variable 209 | # or the EDITOR environment variable, in that order. If neither is set then 210 | # vi is used. 211 | edit_plugin() { 212 | local item_name="$1" 213 | local plugin_data=$(get_plugin_data $item_name) 214 | if [ -z "$plugin_data" ]; then 215 | local plugin_name_and_item_type=$(find_item $item_name) 216 | if [ -n "$plugin_name_and_item_type" ]; then 217 | plugin_data=$(get_plugin_data ${plugin_name_and_item_type%:*}) 218 | fi 219 | fi 220 | [ -z "$plugin_data" ] && die "Unknown plugin, function, alias, or variable: $1" 221 | 222 | local editor=$SHY_EDITOR 223 | [ -z "$editor" ] && editor=$EDITOR 224 | [ -z "$editor" ] && editor=vi 225 | 226 | $editor $(plugin_file "$plugin_data") 227 | } 228 | 229 | # Usage: shy which 230 | # 231 | # Finds which plugin a given function, alias, or variable is defined in. 232 | which_plugin() { 233 | local item_name="$1" 234 | local plugin_name_and_item_type=$(find_item $item_name) 235 | if [ -n "$plugin_name_and_item_type" ]; then 236 | local plugin_name=${plugin_name_and_item_type%:*} 237 | local item_type=${plugin_name_and_item_type##*:} 238 | local article=a 239 | [ "$item_type" = "alias" ] && article=an 240 | echo "$item_name is $article $item_type in the plugin $plugin_name" 241 | else 242 | echo "($item_name not found in any plugin)" >&2 243 | return 1 244 | fi 245 | } 246 | 247 | # Usage: shy help [] 248 | # 249 | # Prints help information. 250 | # 251 | # With no command name given, basic commands and usage are printed. 252 | # 253 | # If a command name is given, then usage for that command is printed. 254 | print_help() { 255 | local cmd="$1" 256 | if [ -n "$cmd" ]; then 257 | local doc_first_line=$(grep -n "^# Usage: shy $cmd" "$BASH_SOURCE") 258 | if [ -n "$doc_first_line" ]; then 259 | local doc_first_line_number=$(echo "$doc_first_line" | cut -d ':' -f 1) 260 | print_comments_starting_at_line $doc_first_line_number 261 | else 262 | die "No such command '$cmd'" 263 | fi 264 | else 265 | print_comments_starting_at_line 9 266 | fi 267 | } 268 | 269 | # (Documentation comments for load command included here, even though it is defined 270 | # in the init section, so that it is parseable with the rest of the top-level commands) 271 | 272 | # Usage: shy load 273 | # 274 | # Loads a plugin from the given file. 275 | # 276 | # A plugin is merely a file with normal shell code. When loaded, Shy sources the 277 | # file and records the names of the functions, aliases, and environment variables 278 | # defined (for the first time) while sourcing the file. 279 | 280 | # Usage: shy init 281 | # 282 | # Prints Shy code that must be sourced in the interactive shell environment. 283 | # 284 | # This should generally only be used in the following initial configuration of 285 | # Shy, which should be placed in your shell's rc file, after the main shy 286 | # executable is placed on the PATH. 287 | # 288 | # eval "$(shy init)" 289 | print_initialization() { 290 | cat <&2 338 | fi 339 | ;; 340 | 341 | _err) 342 | shift 343 | [ -n "\$@" ] && { echo "\$@" >&2; } 344 | return 1 345 | ;; 346 | 347 | _pathify) 348 | echo \$2 | tr "\$IFS" ':' | sed 's/:\$//' 349 | ;; 350 | 351 | _aliases) 352 | local raw_aliases 353 | if [ -n "\$ZSH_VERSION" ]; then 354 | raw_aliases="\$(alias)" 355 | else 356 | raw_aliases="\$(alias | cut -d ' ' -f 2-)" 357 | fi 358 | shy _pathify "\$(echo "\$raw_aliases" | cut -d '=' -f 1 | sort)" 359 | ;; 360 | 361 | _functions) 362 | local funcs 363 | if [ -n "\$ZSH_VERSION" ]; then 364 | funcs="\$(print -l \${(ok)functions})" 365 | else 366 | funcs="\$(typeset -F | cut -d ' ' -f 3)" 367 | fi 368 | shy _pathify "\$funcs" 369 | ;; 370 | 371 | _variables) 372 | shy _pathify "\$(env | cut -d '=' -f 1 | sort)" 373 | ;; 374 | 375 | # Capture names of aliases, functions, and variables in the current environment 376 | _capture-env) 377 | echo "\$(shy _aliases);\$(shy _functions);\$(shy _variables)" 378 | ;; 379 | 380 | # Run the given command command and detect additions to the environment that occurred 381 | _detect-env-additions) 382 | local before after additions additions_for_type 383 | before=\$(shy _capture-env) 384 | shift 385 | "\$@" 386 | after=\$(shy _capture-env) 387 | # TODO clean this up 388 | for n in 1 2 3; do 389 | additions_for_type=\$(comm -13 <(echo \$before | cut -d ';' -f "\$n" | tr ':' '\n') <(echo \$after | cut -d ';' -f "\$n" | tr ':' '\n')) 390 | additions="\${additions}\$(shy _pathify "\$additions_for_type")" 391 | [ "\$n" -lt "3" ] && additions="\$additions;" 392 | done 393 | # Ugh, figure out a way to not use this? 394 | SHY_TMP_DATA="\$additions" 395 | ;; 396 | 397 | *) 398 | command shy "\$@" 399 | ;; 400 | 401 | esac 402 | } 403 | EOS 404 | } 405 | 406 | if [ -z "$1" ]; then 407 | print_help 408 | else 409 | case "$1" in 410 | init) print_initialization ;; 411 | 412 | list) list_plugins ;; 413 | show|edit|which) ${1}_plugin "$2" ;; 414 | help) shift; print_help "$@" ;; 415 | 416 | _realpath|_read_cache|_write_cache) 417 | cmd=${1:1} 418 | shift 419 | $cmd "$@" ;; 420 | 421 | esac 422 | fi 423 | -------------------------------------------------------------------------------- /test/fixtures/.hidden/plugin4: -------------------------------------------------------------------------------- 1 | alias plugin4_alias1='echo "plugin4 alias1"' 2 | 3 | plugin4_func1() { 4 | echo 'plugin4 func1' 5 | } 6 | 7 | export PLUGIN4_VAR1='plugin4 var1' 8 | -------------------------------------------------------------------------------- /test/fixtures/plugin1.sh: -------------------------------------------------------------------------------- 1 | alias plugin1_alias1='echo "plugin1 alias1"' 2 | alias plugin1_alias2='echo "plugin1 alias2"' 3 | 4 | plugin1_func1() { 5 | echo 'plugin1 func1' 6 | } 7 | 8 | plugin1_func2() { 9 | echo 'plugin1 func2' 10 | } 11 | 12 | export PLUGIN1_VAR1='plugin1 var1' 13 | export PLUGIN1_VAR2='plugin1 var2' 14 | -------------------------------------------------------------------------------- /test/fixtures/plugin2.sh: -------------------------------------------------------------------------------- 1 | alias plugin2_alias1='echo "plugin2 alias1"' 2 | 3 | plugin2_func1() { 4 | echo 'plugin2 func1' 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/plugin3.sh: -------------------------------------------------------------------------------- 1 | alias plugin3_alias1='echo "plugin3 alias1"' 2 | 3 | export PLUGIN3_VAR1='plugin3 var1' 4 | -------------------------------------------------------------------------------- /test/shy.bats: -------------------------------------------------------------------------------- 1 | load test_helper 2 | 3 | @test "_detect-env-additions detects added aliases, functions, and variables from sourced files" { 4 | shy _detect-env-additions source "$FIXTURES_DIR/plugin1.sh" 5 | assert_equal "plugin1_alias1:plugin1_alias2;plugin1_func1:plugin1_func2;PLUGIN1_VAR1:PLUGIN1_VAR2" "$SHY_TMP_DATA" 6 | 7 | shy _detect-env-additions source "$FIXTURES_DIR/plugin2.sh" 8 | assert_equal "plugin2_alias1;plugin2_func1;" "$SHY_TMP_DATA" 9 | } 10 | 11 | @test "_write_cache writes plugin cache for plugin file path" { 12 | local plugin_file='/Users/aaron/.shell.d/plugin2.sh' 13 | local plugin_content='plugin2_alias1;plugin2_func1;' 14 | local cached_plugin_file="$SHY_CACHE_DIR/plugin2" 15 | [ -d "$SHY_CACHE_DIR" ] && rmdir "$SHY_CACHE_DIR" 16 | 17 | run shy _write_cache $plugin_file "$plugin_content" 18 | assert_success 19 | [ -d "$SHY_CACHE_DIR" ] 20 | 21 | assert_equal $plugin_content "$(cat $cached_plugin_file)" 22 | } 23 | 24 | @test "_read_cache reads plugin cache for plugin file path, if fresh" { 25 | local plugin_file="$BATS_TMPDIR/plugin2.sh" 26 | cat "$FIXTURES_DIR/plugin1.sh" > $plugin_file 27 | local plugin_content='plugin2_alias1;plugin2_func1;' 28 | local cached_plugin_file="$SHY_CACHE_DIR/plugin2" 29 | echo $plugin_content > "$cached_plugin_file" 30 | 31 | make_modified_in_past $plugin_file 32 | touch $cached_plugin_file 33 | 34 | run shy _read_cache $plugin_file 35 | assert_success 36 | assert_output $plugin_content 37 | } 38 | 39 | @test "_read_cache does not read plugin cache for plugin file path, if stale" { 40 | local plugin_file="$BATS_TMPDIR/plugin2.sh" 41 | cat "$FIXTURES_DIR/plugin1.sh" > $plugin_file 42 | local plugin_content='plugin2_alias1;plugin2_func1;' 43 | local cached_plugin_file="$SHY_CACHE_DIR/plugin2" 44 | echo $plugin_content > "$cached_plugin_file" 45 | 46 | touch $plugin_file 47 | make_modified_in_past $cached_plugin_file 48 | 49 | run shy _read_cache $plugin_file 50 | assert_failure 51 | assert_output "" 52 | } 53 | 54 | @test "load saves plugin information for first plugin" { 55 | local plugin_file="$FIXTURES_DIR/plugin1.sh" 56 | shy load $plugin_file 57 | assert_equal "plugin1;$plugin_file;plugin1_alias1:plugin1_alias2;plugin1_func1:plugin1_func2;PLUGIN1_VAR1:PLUGIN1_VAR2" "$SHY_PLUGIN_DATA" 58 | } 59 | 60 | @test "load saves plugin information for additional plugins" { 61 | load_plugin plugin1 62 | local plugin_file="$FIXTURES_DIR/plugin2.sh" 63 | local new_plugin_data="$SHY_PLUGIN_DATA|plugin2;$plugin_file;plugin2_alias1;plugin2_func1;" 64 | 65 | shy load $plugin_file 66 | assert_equal "$new_plugin_data" "$SHY_PLUGIN_DATA" 67 | 68 | plugin_file="$FIXTURES_DIR/plugin3.sh" 69 | new_plugin_data="$SHY_PLUGIN_DATA|plugin3;$plugin_file;plugin3_alias1;;PLUGIN3_VAR1" 70 | 71 | shy load $plugin_file 72 | assert_equal "$new_plugin_data" "$SHY_PLUGIN_DATA" 73 | } 74 | 75 | @test "plugins do not need a file extension (and work in hidden directories)" { 76 | local plugin_file="$FIXTURES_DIR/.hidden/plugin4" 77 | shy load $plugin_file 78 | assert_equal "plugin4;$plugin_file;plugin4_alias1;plugin4_func1;PLUGIN4_VAR1" "$SHY_PLUGIN_DATA" 79 | } 80 | 81 | @test "load returns failure status without a file name" { 82 | run shy load 83 | assert_failure "Usage: shy load PLUGIN_NAME" 84 | } 85 | 86 | @test "load returns failure status with nonexistent file" { 87 | local bogus_file="$FIXTURES_DIR/file_that_does_not_exist.sh" 88 | run shy load "$bogus_file" 89 | assert_failure "shy: file does not exist: $bogus_file" 90 | } 91 | 92 | @test "load reads data from cache if it exists and is fresh" { 93 | local plugin_file="$FIXTURES_DIR/plugin1.sh" 94 | local cached_plugin_content='cached_alias1;cached_func1;' 95 | local cached_plugin_file="$SHY_CACHE_DIR/plugin1" 96 | echo $cached_plugin_content > "$cached_plugin_file" 97 | 98 | make_modified_in_past $plugin_file 99 | touch $cached_plugin_file 100 | 101 | shy load $plugin_file 102 | assert_equal "plugin1;$plugin_file;$cached_plugin_content" "$SHY_PLUGIN_DATA" 103 | } 104 | 105 | @test "load does not read data from cache if it exists and is stale" { 106 | local plugin_file="$FIXTURES_DIR/plugin1.sh" 107 | local cached_plugin_content='cached_alias1;cached_func1;' 108 | local cached_plugin_file="$SHY_CACHE_DIR/plugin1" 109 | echo $cached_plugin_content > "$cached_plugin_file" 110 | 111 | make_modified_in_past $cached_plugin_file 112 | touch $plugin_file 113 | 114 | shy load $plugin_file 115 | assert_equal "plugin1;$plugin_file;plugin1_alias1:plugin1_alias2;plugin1_func1:plugin1_func2;PLUGIN1_VAR1:PLUGIN1_VAR2" "$SHY_PLUGIN_DATA" 116 | } 117 | 118 | @test "load writes to cache if not reading from cache" { 119 | local plugin_file="$FIXTURES_DIR/plugin1.sh" 120 | local cached_plugin_file="$SHY_CACHE_DIR/plugin1" 121 | rmdir "$SHY_CACHE_DIR" 122 | shy load $plugin_file 123 | [ -d "$SHY_CACHE_DIR" ] 124 | [ -f "$cached_plugin_file" ] 125 | assert_equal "plugin1_alias1:plugin1_alias2;plugin1_func1:plugin1_func2;PLUGIN1_VAR1:PLUGIN1_VAR2" "$(cat $cached_plugin_file)" 126 | } 127 | 128 | @test "list lists all loaded plugin names" { 129 | load_plugins plugin1 plugin2 plugin3 130 | 131 | run shy list 132 | assert_success 133 | assert_output "plugin1 134 | plugin2 135 | plugin3" 136 | } 137 | 138 | @test "show prints info for a plugin" { 139 | load_plugins plugin1 plugin2 plugin3 140 | run shy show plugin1 141 | assert_success 142 | assert_output "Plugin name: plugin1 143 | Source file: $FIXTURES_DIR/plugin1.sh 144 | 145 | == Aliases == 146 | plugin1_alias1 147 | plugin1_alias2 148 | 149 | == Functions == 150 | plugin1_func1 151 | plugin1_func2 152 | 153 | == Variables == 154 | PLUGIN1_VAR1 155 | PLUGIN1_VAR2" 156 | } 157 | 158 | @test "show returns failure status for nonexistent plugin" { 159 | load_plugins plugin1 plugin2 plugin3 160 | run shy show bogus_plugin 161 | assert_failure 162 | assert_output "shy: Unknown plugin name: bogus_plugin" 163 | } 164 | 165 | @test "edit opens editor for plugin" { 166 | load_plugins plugin1 plugin2 plugin3 167 | # echo the file path to make sure we get the right one in the output 168 | export SHY_EDITOR=echo 169 | run shy edit plugin1 170 | assert_success 171 | assert_output "$FIXTURES_DIR/plugin1.sh" 172 | } 173 | 174 | @test "edit opens editor with plugin containing a function" { 175 | load_plugins plugin1 plugin2 plugin3 176 | export SHY_EDITOR=echo 177 | run shy edit plugin1_func1 178 | assert_success 179 | assert_output "$FIXTURES_DIR/plugin1.sh" 180 | } 181 | 182 | @test "edit opens editor with plugin containing an alias" { 183 | load_plugins plugin1 plugin2 plugin3 184 | export SHY_EDITOR=echo 185 | run shy edit plugin2_alias1 186 | assert_success 187 | assert_output "$FIXTURES_DIR/plugin2.sh" 188 | } 189 | 190 | @test "edit opens editor with plugin containing a variable" { 191 | load_plugins plugin1 plugin2 plugin3 192 | export SHY_EDITOR=echo 193 | run shy edit PLUGIN3_VAR1 194 | assert_success 195 | assert_output "$FIXTURES_DIR/plugin3.sh" 196 | } 197 | 198 | @test "edit returns failure status for nonexistent plugin or item" { 199 | load_plugins plugin1 plugin2 plugin3 200 | export SHY_EDITOR=echo 201 | run shy edit bogus_plugin 202 | assert_failure 203 | assert_output "shy: Unknown plugin, function, alias, or variable: bogus_plugin" 204 | } 205 | 206 | @test "which finds where an alias is defined" { 207 | load_plugins plugin1 plugin2 plugin3 208 | run shy which plugin3_alias1 209 | assert_success 210 | assert_output "plugin3_alias1 is an alias in the plugin plugin3" 211 | } 212 | 213 | @test "which finds where an function is defined" { 214 | load_plugins plugin1 plugin2 plugin3 215 | run shy which plugin2_func1 216 | assert_success 217 | assert_output "plugin2_func1 is a function in the plugin plugin2" 218 | } 219 | 220 | @test "which finds where a variable is defined" { 221 | load_plugins plugin1 plugin2 plugin3 222 | run shy which PLUGIN1_VAR1 223 | assert_success 224 | assert_output "PLUGIN1_VAR1 is a variable in the plugin plugin1" 225 | } 226 | 227 | @test "which returns failure status and error message when nothing is found" { 228 | load_plugins plugin1 plugin2 plugin3 229 | run shy which bogus_something_or_other 230 | assert_failure 231 | assert_output "(bogus_something_or_other not found in any plugin)" 232 | } 233 | -------------------------------------------------------------------------------- /test/test_helper.bash: -------------------------------------------------------------------------------- 1 | # Much of this is inspired by or straight stolen from rbenv's tests 2 | # https://github.com/sstephenson/rbenv/blob/master/test/test_helper.bash 3 | 4 | # Guard against bats executing this twice 5 | if [ -z "$TEST_PATH_INITIALIZED" ]; then 6 | export TEST_PATH_INITIALIZED=true 7 | 8 | PATH=/usr/bin:/bin:/usr/sbin:/sbin 9 | PATH="$(dirname $BATS_TEST_DIRNAME):$PATH" 10 | 11 | export FIXTURES_DIR="$BATS_TEST_DIRNAME/fixtures" 12 | export SHY_CACHE_DIR="$BATS_TMPDIR/.shy_plugin_cache" 13 | fi 14 | 15 | eval "$(shy init)" 16 | 17 | setup() { 18 | unset SHY_PLUGIN_DATA 19 | unset SHY_TMP_DATA 20 | 21 | if [ -d "$SHY_CACHE_DIR" ]; then 22 | find "$SHY_CACHE_DIR" -type f -exec rm {} \; 23 | else 24 | mkdir "$SHY_CACHE_DIR" 25 | fi 26 | } 27 | 28 | flunk() { 29 | { if [ "$#" -eq 0 ]; then cat - 30 | else echo "$@" 31 | fi 32 | } | sed "s:${BATS_TEST_DIRNAME}:TEST_DIR:g" >&2 33 | return 1 34 | } 35 | 36 | assert_success() { 37 | if [ "$status" -ne 0 ]; then 38 | flunk "command failed with exit status $status" 39 | elif [ "$#" -gt 0 ]; then 40 | assert_output "$1" 41 | fi 42 | } 43 | 44 | assert_failure() { 45 | if [ "$status" -eq 0 ]; then 46 | flunk "expected failed exit status" 47 | elif [ "$#" -gt 0 ]; then 48 | assert_output "$1" 49 | fi 50 | } 51 | 52 | assert_equal() { 53 | if [ "$1" != "$2" ]; then 54 | { echo "expected: $1" 55 | echo "actual: $2" 56 | } | flunk 57 | fi 58 | } 59 | 60 | assert_output() { 61 | local expected 62 | if [ $# -eq 0 ]; then expected="$(cat -)" 63 | else expected="$1" 64 | fi 65 | assert_equal "$expected" "$output" 66 | } 67 | 68 | load_plugins() { 69 | for arg in "$@"; do 70 | shy load "$FIXTURES_DIR/$arg.sh" 71 | done 72 | } 73 | 74 | load_plugin() { 75 | load_plugins "$@" 76 | } 77 | 78 | make_modified_in_past() { 79 | local current_timestamp=$(date +%Y%m%d%H%M) 80 | local past_timestamp=$(expr $current_timestamp - 10) 81 | touch -t $past_timestamp "$1" 82 | } 83 | --------------------------------------------------------------------------------