├── scripts └── test ├── .editorconfig ├── test ├── helper.fish └── cd.completion.fish ├── conf.d └── cd.fish ├── LICENSE ├── README.md ├── completions └── cd.fish └── functions └── __plugin_cd.fish /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env fish 2 | 3 | set -l SOURCE_DIR (cd ( dirname (status --current-filename) ); and pwd) 4 | 5 | eval $SOURCE_DIR/../test/cd.completion.fish 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | -------------------------------------------------------------------------------- /test/helper.fish: -------------------------------------------------------------------------------- 1 | set -l fish_tank /usr/local/share/fish-tank/tank.fish 2 | if not test -e $fish_tank 3 | echo 'Install fish-tank for running the tests (https://github.com/terlar/fish-tank)' 4 | git clone --depth 1 https://github.com/terlar/fish-tank.git /tmp/fish-tank 5 | cd /tmp/fish-tank/ 6 | make install 7 | end 8 | 9 | source $fish_tank 10 | 11 | set -U tank_reporter spec 12 | 13 | 14 | -------------------------------------------------------------------------------- /conf.d/cd.fish: -------------------------------------------------------------------------------- 1 | set SOURCE_DIR (dirname (status -f)) 2 | 3 | if not functions -q __wrapped_cd 4 | functions -c cd __wrapped_cd 5 | functions -e cd 6 | end 7 | 8 | functions -c __plugin_cd cd 9 | 10 | function _cd_uninstall --on-event cd_uninstall 11 | functions -e cd 12 | 13 | if functions -q __wrapped_cd 14 | functions -c __wrapped_cd cd 15 | functions -e __wrapped_cd 16 | end 17 | 18 | functions -e __plugin_cd 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jianming Qu 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 | [![][travis-badge]][travis-link] 2 | ![][license-badge] 3 | 4 |
5 | 6 | 7 | 8 |
9 |
10 | 11 | # cd 12 | 13 | Plugin for [Oh My Fish][omf-link]. 14 | 15 | Package cd provides a new `cd` command to help you change the current working directory fast. It's a wrapper directive of the built-in `cd` command with many useful features and powerful completions. 16 | 17 | ## Features: 18 | 19 | - Multi-dot navigation: `cd ....` 20 | - Plus / minus navigation: `cd -` `cd -3` `cd +2` 21 | - Full `$CDPATH` support 22 | 23 | ## Install 24 | 25 | ```fish 26 | $ omf install cd 27 | ``` 28 | 29 | 30 | ## Usage 31 | 32 | ```fish 33 | $ cd .../foo # <=> cd ../../foo 34 | $ cd ... # <=> cd ../.. 35 | $ cd .../foo/.../bar # <=> cd ../../foo/../../bar 36 | ``` 37 | 38 | ```fish 39 | $ cd # ~ 40 | $ cd ~/foo # ~/foo 41 | $ cd ~/bar # ~/bar 42 | $ cd - # ~/foo ;; ( Equal to `cd -1` ) 43 | ``` 44 | 45 | ```fish 46 | $ pwd # ~/a 47 | $ cd ~/b # ~/b ;; ( dirstack: a ) 48 | $ cd ~/c # ~/c ;; ( dirstack: b a ) 49 | $ cd ~/d # ~/d ;; ( dirstack: c b a ) 50 | $ cd -2 # ~/b ;; ( dirstack: d c a ) 51 | $ cd +1 # ~/c ;; ( dirstack: b d a ) 52 | $ cd +0 # ~/a ;; ( dirstack: c b d ) 53 | $ cd -0 # ~/a ;; ( dirstack: c b d ) 54 | ``` 55 | 56 | # License 57 | 58 | [MIT][mit] © [Jianming Qu](https://jmqu.tech) 59 | 60 | 61 | [mit]: http://opensource.org/licenses/MIT 62 | [author]: http://github.com/sancoder-q 63 | [omf-link]: https://www.github.com/oh-my-fish/oh-my-fish 64 | 65 | [license-badge]: https://img.shields.io/badge/license-MIT-007EC7.svg?style=flat-square 66 | [travis-badge]: http://img.shields.io/travis/sancoder-q/plugin-cd.svg?style=flat-square 67 | [travis-link]: https://travis-ci.org/sancoder-q/plugin-cd 68 | -------------------------------------------------------------------------------- /completions/cd.fish: -------------------------------------------------------------------------------- 1 | function __complete_omf_cd 2 | function __resolve_dot_path 3 | echo $argv | \ 4 | sed -e 's@^\.$@:@;s@^\.\([^\.]\)@:\1@g;s@\([^\.]\)\.$@\1:@g' \ 5 | -e 's@\([^\.]\)\.\([^\.]\)@\1:\2@g' \ 6 | -e 's@\([^\.]\)\.\([^\.]\)@\1:\2@g' \ 7 | -e 's@\.\{2\}\(\.*\)@::\1@g' \ 8 | -e 's@\.@\/\.\.@g' \ 9 | -e 's@:@\.@g' 10 | end 11 | 12 | function __resolve_home_path 13 | echo $argv |\ 14 | sed -e "s@^~@$HOME@" 15 | end 16 | 17 | function __resolve_fancy_path 18 | __resolve_dot_path (__resolve_home_path $argv) 19 | end 20 | 21 | function __get_basepath 22 | set -l __base (echo $argv | rev | cut -d/ -s -f2- | rev) 23 | if string match -qr '^/' "$argv"; and test -z $__base 24 | echo '/' 25 | else 26 | echo $__base 27 | end 28 | end 29 | 30 | function __get_filename 31 | echo $argv | rev | cut -d/ -f1 | rev 32 | end 33 | 34 | function __filter_directory 35 | while read -l __name 36 | if test -d "$argv/$__name" 37 | echo "$__name/" 38 | end 39 | end 40 | end 41 | 42 | function __list_and_filter 43 | if test -d $argv[1] 44 | command ls -a $argv[1] | __filter_directory $argv[1] | \ 45 | command grep -e (echo $argv[2] | sed -e 's@\.@\\\.@g' -e 's@^@\^@') 46 | end 47 | end 48 | 49 | function __list_all_candidate 50 | 51 | set -l __basepath (__get_basepath $argv) 52 | set -l __filename (__get_filename $argv) 53 | 54 | if test -n "$__basepath" 55 | set __candidate (__list_and_filter $__basepath $__filename) 56 | else 57 | set __candidate (__list_and_filter . $__filename) 58 | end 59 | 60 | if test -n "$CDPATH" 61 | for cdpath in $CDPATH 62 | if not string match -qr '^/' $argv 63 | set -l resolved_cdpath (__resolve_home_path $cdpath) 64 | if test -d $resolved_cdpath 65 | set __candidate $__candidate (__list_and_filter "$resolved_cdpath/$__basepath" $__filename) 66 | end 67 | end 68 | end 69 | end 70 | 71 | for __c in $__candidate 72 | if test "$__c" = './'; or test "$__c" = '../' 73 | else if test -z "$__c" 74 | else 75 | echo $__c 76 | end 77 | end 78 | end 79 | 80 | function __list_all 81 | 82 | set -l token $argv 83 | 84 | if test "$token" = "~" 85 | echo "~/" 86 | return 0 87 | end 88 | 89 | set -l basepath (__get_basepath $token) 90 | set -l resolved_path (__resolve_fancy_path $token) 91 | 92 | if test -d $resolved_path; and string match -qr '[^/]$' $resolved_path 93 | echo "$token/" 94 | end 95 | 96 | if test -z "$basepath" 97 | __list_all_candidate $resolved_path 98 | else if test "$basepath" = '/' 99 | __list_all_candidate $resolved_path | sed "s@^@/@" 100 | else 101 | __list_all_candidate $resolved_path | sed "s@^@$basepath/@" 102 | end 103 | end 104 | 105 | __list_all $argv | sort -u 106 | end 107 | 108 | function __fish_complete_plugin_cd -d "Completions for the plugin-cd command" 109 | # Start 110 | set -l token (commandline -ct) 111 | __complete_omf_cd $token 112 | end 113 | 114 | complete -c cd -e 115 | complete -f -c cd -a "(__fish_complete_plugin_cd)" 116 | -------------------------------------------------------------------------------- /functions/__plugin_cd.fish: -------------------------------------------------------------------------------- 1 | # SYNOPSIS 2 | # cd [DIRECTORY|DOTS] 3 | # 4 | # Description 5 | # cd changes the current working directory. It's a proxy directive of the buildin 6 | # cd command with an alias of going to the upper directory. 7 | # 8 | # Usage 9 | # cd - # return to the last directory 10 | # cd -1 # equal to cd - 11 | # cd -2 # navigate to the second top directory from dirstack 12 | # cd +2 # navigate to the second bottom directory from dirstack 13 | # 14 | # Examples 15 | # cd .../foo <=> cd ../../foo 16 | # cd ... <=> cd ../.. 17 | # cd .../foo/.../bar <=> cd ../../foo/../../bar 18 | 19 | function __plugin_cd -d "plugin-cd" -a fancy_path 20 | 21 | # private: 22 | function __fish_cd 23 | if functions -q __wrapped_cd 24 | __wrapped_cd $argv 25 | else 26 | builtin cd $argv 27 | end 28 | end 29 | 30 | function __empty_cd -S 31 | __fish_cd 32 | set -l output_status $status 33 | 34 | __update_pwd 35 | 36 | return $output_status 37 | end 38 | 39 | function __hyphen_cd -S 40 | __fish_cd $OLDPWD 41 | set -l output_status $status 42 | 43 | __update_pwd 44 | 45 | return $output_status 46 | end 47 | 48 | function __fancy_cd -S 49 | set extract_from_right (echo $fancy_path | sed -n 's/^+\([0-9]*\)$/\1/g p') 50 | set extract_from_left (echo $fancy_path | sed -n 's/^-\([0-9]*\)$/\1/g p') 51 | 52 | if test -n "$extract_from_right" -o -n "$extract_from_left" 53 | # Generate current stack 54 | set -l stack (command pwd) $dirstack 55 | 56 | if test -n "$extract_from_right" 57 | if test $extract_from_right -gt (math (count $stack) - 1) 58 | echo "no such entry in dir stack" 59 | return 1 60 | end 61 | 62 | set extract_from_left (math (count $stack) - 1 - $extract_from_right) 63 | end 64 | 65 | if test $extract_from_left -gt (math (count $stack) - 1) 66 | echo "no such entry in dir stack" 67 | return 1 68 | else 69 | if test $extract_from_left -gt 0 70 | set stack $stack[(math $extract_from_left + 1)] $stack 71 | set -e stack[(math $extract_from_left + 2)] 72 | end 73 | 74 | # now reconstruct dirstack and change directory 75 | set -g dirstack $stack[2..(count $stack)] 76 | __fish_cd $stack[1] 77 | set -l output_status $status 78 | 79 | __update_pwd 80 | 81 | return $output_status 82 | end 83 | else 84 | set -l normal_path (echo $fancy_path | sed -e 's@^\.$@:@;s@^\.\([^\.]\)@:\1@g;s@\([^\.]\)\.$@\1:@g' -e 's@\([^\.]\)\.\([^\.]\)@\1:\2@g' -e 's@\([^\.]\)\.\([^\.]\)@\1:\2@g' -e 's@\.\{2\}\(\.*\)@::\1@g' -e 's@\.@\/\.\.@g' -e 's@:@\.@g') 85 | 86 | __fish_cd $normal_path 87 | set -l output_status $status 88 | 89 | __update_pwd 90 | 91 | return $output_status 92 | end 93 | end 94 | 95 | function __update_dirstack -S 96 | if contains $old_pwd $dirstack 97 | # If the from directory is existed in stack, remove it 98 | set -e dirstack[(contains -i $old_pwd $dirstack)] 99 | end 100 | # Add the from directory to the head of stack 101 | set -g dirstack $old_pwd $dirstack 102 | 103 | if contains (command pwd) $dirstack 104 | # If the to directory is existed in stack, remove it 105 | set -e dirstack[(contains -i (command pwd) $dirstack)] 106 | end 107 | end 108 | 109 | function __update_pwd -S 110 | if test $old_pwd != (command pwd) 111 | set -xg OLDPWD $old_pwd 112 | __update_dirstack 113 | end 114 | end 115 | 116 | # body: 117 | set -l old_pwd (pwd) 118 | 119 | switch "$fancy_path" 120 | case '' 121 | __empty_cd 122 | case '-' 123 | __hyphen_cd 124 | case '*' 125 | __fancy_cd 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/cd.completion.fish: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/fish 2 | 3 | source (dirname (status -f))/../completions/cd.fish 4 | set _test_path /tmp/omf-cd 5 | 6 | function before 7 | mkdir -p "$_test_path" 8 | mkdir -p "$_test_path/.a/tm_start_folder" 9 | mkdir -p "$_test_path/a" 10 | mkdir -p "$_test_path/b" 11 | mkdir -p "$_test_path/b/b" 12 | mkdir -p "$_test_path/b/b1" 13 | mkdir -p "$_test_path/b/b2" 14 | mkdir -p "$_test_path/c" 15 | mkdir -p "$_test_path/c/~/h1" 16 | mkdir -p "$_test_path/c/~/h1/h1.1" 17 | mkdir -p "$_test_path/c/~/h2" 18 | 19 | set -e CDPATH 20 | set HOME $_test_path/c/~ 21 | end 22 | 23 | function after 24 | set -e HOME 25 | rm -rf $_test_path 26 | end 27 | 28 | function suite_relative_path 29 | 30 | function test_empty_should_return_current_directories 31 | cd $_test_path 32 | set -l subject (__complete_omf_cd "") 33 | assert_equal ".a/ a/ b/ c/" $subject 34 | end 35 | 36 | function test_one_dot_only_should_return_directories_stating_with_a_dot 37 | cd $_test_path 38 | assert_equal "./ .a/" (__complete_omf_cd ".") 39 | end 40 | 41 | function test_two_dot_only_should_return_a_slash 42 | cd $_test_path 43 | assert_equal "../" (__complete_omf_cd "..") 44 | end 45 | 46 | function test_three_dot_only_should_return_a_slash 47 | cd $_test_path 48 | assert_equal ".../" (__complete_omf_cd "...") 49 | end 50 | 51 | function test_continous_slashs_should_not_change_the_results 52 | cd $_test_path 53 | assert_equal ".//./ .//.a/" (__complete_omf_cd ".//.") 54 | end 55 | 56 | function test_one_dot_ending_with_slash_should_return_current_directories 57 | cd $_test_path 58 | assert_equal "./.a/ ./a/ ./b/ ./c/" (__complete_omf_cd "./") 59 | end 60 | 61 | function test_two_dot_with_slash_should_return_parent_directories 62 | cd $_test_path/a 63 | assert_equal "../.a/ ../a/ ../b/ ../c/" (__complete_omf_cd "../") 64 | end 65 | 66 | function test_empty_path_should_return_empty 67 | cd $_test_path 68 | assert_empty (__complete_omf_cd "a/") 69 | end 70 | end 71 | 72 | function suite_absolute_path 73 | 74 | function test_root_slash_should_return_root_directories 75 | cd $_test_path 76 | set -l subject (__complete_omf_cd "/") 77 | assert (contains '/tmp/' $subject) 78 | assert (contains '/etc/' $subject) 79 | end 80 | 81 | function test_empty_should_return_current_directories 82 | cd $_test_path 83 | set -l subject (__complete_omf_cd "$_test_path/") 84 | assert_equal "$_test_path/.a/ $_test_path/a/ $_test_path/b/ $_test_path/c/" $subject 85 | end 86 | 87 | function test_one_dot_only_should_return_directories_stating_with_a_dot 88 | cd $_test_path 89 | assert_equal "$_test_path/./ $_test_path/.a/" (__complete_omf_cd "$_test_path/.") 90 | end 91 | 92 | function test_two_dot_only_should_return_a_slash 93 | cd $_test_path 94 | assert_equal "$_test_path/../" (__complete_omf_cd "$_test_path/..") 95 | end 96 | 97 | function test_three_dot_only_should_return_a_slash 98 | cd $_test_path 99 | assert_equal "$_test_path/.../" (__complete_omf_cd "$_test_path/...") 100 | end 101 | 102 | function test_one_dot_ending_with_slash_should_return_current_directories 103 | cd $_test_path 104 | assert_equal "$_test_path/./.a/ $_test_path/./a/ $_test_path/./b/ $_test_path/./c/" \ 105 | (__complete_omf_cd "$_test_path/./") 106 | end 107 | 108 | function test_two_dot_with_slash_should_return_parent_directories 109 | cd $_test_path 110 | assert_equal "$_test_path/a/../.a/ $_test_path/a/../a/ $_test_path/a/../b/ $_test_path/a/../c/" \ 111 | (__complete_omf_cd "$_test_path/a/../") 112 | end 113 | 114 | function test_empty_path_should_return_empty 115 | cd $_test_path 116 | assert_empty (__complete_omf_cd "$_test_path/a/") 117 | end 118 | 119 | function test_one_swung_should_return_a_slash 120 | cd $_test_path 121 | assert_equal "~/" (__complete_omf_cd "~") 122 | end 123 | 124 | function test_one_swung_with_slash_should_return_home_directories 125 | cd $_test_path 126 | assert_equal "~/h1/ ~/h2/" (__complete_omf_cd "~/") 127 | end 128 | 129 | function test_path_start_with_swung_should_return_correct_directories 130 | cd $_test_path 131 | assert_equal "~/h1/h1.1/" (__complete_omf_cd "~/h1/") 132 | end 133 | end 134 | 135 | function suite_with_cdpath 136 | function test_single_cdpath_different_with_current_should_return_merged_directories 137 | cd $_test_path 138 | set -g CDPATH "$_test_path/b" 139 | set -l subject (__complete_omf_cd "") 140 | set -e CDPATH 141 | assert_equal ".a/ a/ b/ b1/ b2/ c/" $subject 142 | end 143 | 144 | function test_single_cdpath_with_slash_should_not_make_differences 145 | cd $_test_path 146 | set -g CDPATH "$_test_path/b/" 147 | set -l subject (__complete_omf_cd "") 148 | set -e CDPATH 149 | assert_equal ".a/ a/ b/ b1/ b2/ c/" $subject 150 | end 151 | 152 | function test_single_cdpath_same_with_current_should_return_current_directories 153 | cd $_test_path 154 | set -g CDPATH "$_test_path" 155 | set -l subject (__complete_omf_cd "") 156 | set -e CDPATH 157 | assert_equal ".a/ a/ b/ c/" $subject 158 | end 159 | 160 | function test_single_cdpath_with_root_should_not_return_directories_in_cdpath 161 | cd $_test_path 162 | set -g CDPATH "$_test_path/.a" 163 | set -l subject (__complete_omf_cd "/tm") 164 | set -e CDPATH 165 | assert (not contains '/tm_start_folder/' $subject) 166 | end 167 | 168 | function test_multiple_cdpath_should_return_correct_directories 169 | cd $_test_path 170 | set -g CDPATH "$_test_path/b" "$_test_path/c/~" 171 | set -l subject (__complete_omf_cd "") 172 | set -e CDPATH 173 | assert_equal ".a/ a/ b/ b1/ b2/ c/ h1/ h2/" $subject 174 | end 175 | 176 | function test_multiple_cdpath_include_swung_should_return_correct_directories 177 | cd $_test_path 178 | set -g CDPATH "$_test_path/b" "~" 179 | set -l subject (__complete_omf_cd "") 180 | set -e CDPATH 181 | assert_equal ".a/ a/ b/ b1/ b2/ c/ h1/ h2/" $subject 182 | end 183 | 184 | function test_multiple_cdpath_with_path_should_return_correct_directories 185 | cd $_test_path 186 | set -g CDPATH "$_test_path/b" "$_test_path/c/~" 187 | set -l subject (__complete_omf_cd "b") 188 | set -e CDPATH 189 | assert_equal "b/ b1/ b2/" $subject 190 | end 191 | end 192 | 193 | if not set -q tank_running 194 | source (dirname (status -f))/helper.fish 195 | before 196 | tank_run 197 | after 198 | end 199 | --------------------------------------------------------------------------------