diff --git a/README.cn.md b/README.cn.md index 51d7186..e2f4d0b 100644 --- a/README.cn.md +++ b/README.cn.md @@ -20,7 +20,8 @@ z.lua 是一个快速路径切换工具,它会跟踪你在 shell 下访问过 - 新增:环境变量 "$_ZL_ADD_ONCE" 设成 1 的话性仅当前路径改变时才更新数据库。 - 新增:增强匹配模式,将环境变量 "$_ZL_MATCH_MODE" 设置成 1 可以启用。 - 新增:交互选择模式,如果有多个匹配结果的话,跳转前允许你进行选择。 -- 新增:支持 fzf 来进行多结果筛选,见后面文档。 +- 新增:支持 fzf 来进行多结果筛选,见后面文档。 +- 新增:快速跳转到父目录,代替反复 “cd ../../.." 。 ## Examples @@ -248,6 +249,66 @@ PS:如果你使用 Fish shell,需要 2.7.0 以上才支持该功能。 PS:你可以使用 `$_ZL_FZF` 环境变量来精确指明 fzf 的可执行路径,默认的话就是 fzf。如果你使用 Fish shell,需要 2.7.0 以上才支持该功能。 + +## 快速回到父目录 + +`"-b"` 选项可以快速回到某一级父目录,避免重复的输入 "cd ../../.."。 + +- **(没有参数)** `cd` 到项目根目录: + + 使用 `"z -b"` 后面不跟任何参数,z.lua 会寻找当前项目的 checkout 目录(有 `.git`/`.hg`/`.svn` 的地方) 然后 `cd` 过去。 + +- **(单个参数)** `cd` 到离当前目录最近的以关键字开头的父目录: + + 假设你在 `/home/user/project/src/org/main/site/utils/file/reader/whatever` 然后你想快速回到 `site` 目录, + + 只需要输入:`z -b site` + + 实际上,可简化为 `z -b <开头的几个字母>` 比如 `z -b s` or `z -b si`。 + + 如果当前存在多级父目录同时包含你输入的关键词,`z -b xxx` 会将你到离你最近的那一层父目录。 + +- **(两个参数)** 将当前路径中的第一个关键词替换为第二个关键词。 + +为了使用简便,我们继续将 `z -b` 取个别名成 `zb`: + +```bash +# 一直向上退到项目根目录(就是里面有一个 .git 目录的地方) +~/github/lorem/src/public$ zb + => cd ~/github/lorem + +# cd 到第一个以 g 开头的父目录 +~/github/vimium/src/public$ zb g + => cd ~/github + +# 将 jekyll 替换为 ghost +~/github/jekyll/test$ zb jekyll ghost + => cd ~/github/ghost/test +``` + +向后跳转同样也支持环境变量 `$_ZL_ECHO`(用来显示跳转结果),这样为搭配其他工具提供了可能性(并不需要改变当前工作目录): + + +```bash +# 假设我们位于 ~/github/vim/src/libvterm +# 打开 $_ZL_ECHO 用于在每次跳转后调用一次 pwd 显示当前目录 +$ _ZL_ECHO=1 + +# 看看我项目根目录(有 .git 那个)目录里有什么? +$ ls -l `zb` + => ls -l ~/github/vim + +# 检查 "<项目根目录>/logs" 下面的日志 +$ tail -f `zb`/logs/error.log + => tail -f ~/github/vim/logs/error.log + +# 查看一下某一级父目录里有些啥 +$ ls -l `zb git` + => ls -l ~/github + +``` + + ## Tips 推荐一些常用的命令别名: @@ -256,6 +317,7 @@ PS:你可以使用 `$_ZL_FZF` 环境变量来精确指明 fzf 的可执行路 alias zc='z -c' # 严格匹配当前路径的子路径 alias zz='z -i' # 使用交互式选择模式 alias zf='z -I' # 使用 fzf 对多个结果进行选择 +alias zb='z -b' # 快速回到父目录 ``` diff --git a/README.md b/README.md index 231da5e..4e225a9 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ For example, `z foo bar` would match `/foo/bar` but not `/bar/foo`. - New "$_ZL_ADD_ONCE" to allow updating database only if `$PWD` changed. - Enhanced matching mode with "$_ZL_MATCH_MODE" set to 1. - Interactive selection enables you to choose where to go before cd. -- Support fzf for selecting from multiple results. +- Support fzf for selecting from multiple results (optional). +- Quickly go back to a parent directory instead of typing "cd ../../..". ## Examples @@ -241,7 +242,7 @@ From version 1.1.0, a new option `"-I"` will allow you to use fzf to select when When we use `"z -I vim"`,12 paths contains keyword "vim" has been matched and ordered by their frecent value, the higher frecent comes with the higher rank. Then without cd to the highest ranked path, z.lua passes all the candidates to fzf. And you can use fzf to select where you want to go, or ESC to quit. -Of course, you can always give more keywords to `z` command to match your destination precisely. This feature provide you another way to do that. +Of course, you can always give more keywords to `z` command to match your destination precisely. `"z -I"` is similar to `"z -i"`, but use fzf. Both `"-i"` and `"-I"` provide you another way for path navigation. Usually, `z -I` can be aliased to `zf` (z + fuzzy finder) for convenience. If there are only one path matched, `z -I` will jump to it directly, fzf will only be invoked for multiple matches. @@ -249,6 +250,62 @@ Usually, `z -I` can be aliased to `zf` (z + fuzzy finder) for convenience. If th NOTE: For fish shell, this feature requires fish 2.7.0 or above. You can specify fzf executable in `$_ZL_FZF` environment variable, `"fzf"` will be called by default. +## Jump Backwards + +New option `"-b"` can quickly go back to a specific parent directory in bash instead of typing "cd ../../.." redundantly. + +- **(No argument)** `cd` into the project root: + + Use `z -b` with no argument, it will look for the project (checkout) directory (the one with `.git`/`.hg`/`.svn` in it) and then `cd` into it. + +- **(One argument)** `cd` into the closest parent having its name begin with whatever the value you passed in: + + If you are in this path `/home/user/project/src/org/main/site/utils/file/reader/whatever` and you want to go to `site` directory quickly, + + then just type: `z -b site` + + In fact, You can simply type `z -b ` like `z -b s` or `z -b si`. + If there are more than one directories with same name up in the hierarchy, `z -b` will take you to the closest. + +- **(Two arguments)** replace the first value with the second one (in the current path). + +Let's start by alising `z -b` to `zb`: + +```bash +# go all the way up to the project root (in this case, the one that has .git in it) +~/github/lorem/src/public$ zb + => cd ~/github/lorem + +# cd into to the first parent directory named g* +~/github/vimium/src/public$ zb g + => cd ~/github + +# substitute jekyll with ghost +~/github/jekyll/test$ zb jekyll ghost + => cd ~/github/ghost/test +``` + +Backward jumping can also be used with `$_ZL_ECHO` option (echo $pwd), which makes it possible to combine them with other tools (without actually changing the working directory): + +```bash +# Assuming we are in ~/github/vim/src/libvterm +# Enable $_ZL_ECHO to emit a pwd command after cd +$ _ZL_ECHO=1 + +# see what's in my project root +$ ls -l `zb` + => ls -l ~/github/vim + +# check log in "/logs" +$ tail -f `zb`/logs/error.log + => tail -f ~/github/vim/logs/error.log + +# list some parent directory +$ ls -l `zb git` + => ls -l ~/github + +``` + ## Tips @@ -258,6 +315,7 @@ Recommended aliases you may find useful: alias zc='z -c' # restrict matches to subdirs of $PWD alias zz='z -i' # cd with interactive selection alias zf='z -I' # use fzf to select in multiple matches +alias zb='z -b' # quickly cd to the parent directory ``` @@ -309,6 +367,8 @@ awk -F '\t' '{print $2 "|" $1 "|" 0}' $FN >> ~/.zlua ## History +- 1.3.0 (2019-02-04): Backward jumping, prevent "cd ../../.." repeatly. +- 1.2.0 (2019-02-03): Upgrading string lib and path lib. - 1.1.0 (2019-02-02): New option '-I' to use fzf to select from multiple matches. - 1.0.0 (2019-02-01): Fixed minor issues and make it stable. - 0.5.0 (2019-01-21): supports fish shell (Daniel Lewan). @@ -319,12 +379,13 @@ awk -F '\t' '{print $2 "|" $1 "|" 0}' $FN >> ~/.zlua - 0.1.0 (2018-04-30): supports windows cmd, cmder and conemu. - 0.0.0 (2018-03-21): initial commit, compatible with original z.sh. -## Credit +## Thanks -Releated projects: +Thanks to @rupa for inspiring me to start this project. +Thanks to @vigneshwaranr and @shyiko for inspiring me the backward jumping. +Thanks to @TeddyDD for fish shell porting. -- [rupa/z](https://github.com/rupa/z): origin z.sh implementation -- [JannesMeyer/z.ps](https://github.com/JannesMeyer/z.ps): z for powershell +And many others. ## License diff --git a/test_path.lua b/test_path.lua new file mode 100644 index 0000000..2465ffb --- /dev/null +++ b/test_path.lua @@ -0,0 +1,193 @@ +local zmod = require('z') +local windows = os.path.sep == '\\' + +----------------------------------------------------------------------- +-- logo +----------------------------------------------------------------------- +function print_title(text) + print(string.rep('-', 72)) + print('-- '.. text) + print(string.rep('-', 72)) +end + + +----------------------------------------------------------------------- +-- os.path.normpath +----------------------------------------------------------------------- +print_title('os.path.normpath') + +function assert_posix(path, result) + local x = os.path.normpath(path) + print('[test] normpath: ('..path..') -> (' .. result .. ')') + if x:gsub('\\', '/') ~= result then + print('failed: "' .. x .. '" != "'..result.. '"') + os.exit() + else + print('passed') + print() + end +end + +function assert_windows(path, result) + local x = os.path.normpath(path) + print('[test] normpath: ('..path..') -> (' .. result .. ')') + if x ~= result then + print('failed: "' .. x .. '" != "'..result.. '"') + os.exit() + else + print('passed') + print() + end +end + +assert_posix("", ".") +assert_posix("/", "/") +assert_posix("///", "/") +assert_posix("///foo/.//bar//", "/foo/bar") +assert_posix("///foo/.//bar//.//..//.//baz", "/foo/baz") +assert_posix("///..//./foo/.//bar", "/foo/bar") + +if windows then + assert_windows('A//////././//.//B', 'A\\B') + assert_windows('A/./B', 'A\\B') + assert_windows('A/foo/../B', 'A\\B') + assert_windows('C:A//B', 'C:A\\B') + assert_windows('D:A/./B', 'D:A\\B') + assert_windows('e:A/foo/../B', 'e:A\\B') + assert_windows('C:///A//B', 'C:\\A\\B') + assert_windows('D:///A/./B', 'D:\\A\\B') + assert_windows('e:///A/foo/../B', 'e:\\A\\B') + assert_windows('..', '..') + assert_windows('.', '.') + assert_windows('', '.') + assert_windows('/', '\\') + assert_windows('c:/', 'c:\\') + assert_windows('/../.././..', '\\') + assert_windows('c:/../../..', 'c:\\') + assert_windows('../.././..', '..\\..\\..') + assert_windows('K:../.././..', 'K:..\\..\\..') + assert_windows('C:////a/b', 'C:\\a\\b') +end + +print() + + +----------------------------------------------------------------------- +-- os.path.join +----------------------------------------------------------------------- +print_title('os.path.join') + +function assert_join_posix(segments, result, isnt) + print('[test] join: '..zmod.dump(segments)..' -> (' .. result .. ')') + local path = '' + for _, item in ipairs(segments) do + path = os.path.join(path, item) + end + if windows and (not isnt) then + path = path:gsub('\\', '/') + end + if path ~= result then + print('failed: "' .. path .. '"') + os.exit() + else + print('passed') + end +end + +function assert_join_windows(segments, result) + assert_join_posix(segments, result, 1) +end + +assert_join_posix({"/foo", "bar", "/bar", "baz"}, "/bar/baz") +assert_join_posix({"/foo", "bar", "baz"}, "/foo/bar/baz") +assert_join_posix({"/foo/", "bar/", "baz/"}, "/foo/bar/baz/") + +if windows then + assert_join_windows({""}, '') + assert_join_windows({"", "", ""}, '') + assert_join_windows({"a"}, 'a') + assert_join_windows({"/a"}, '/a') + assert_join_windows({"\\a"}, '\\a') + assert_join_windows({"a:"}, 'a:') + assert_join_windows({"a:", "\\b"}, 'a:\\b') + assert_join_windows({"a", "\\b"}, '\\b') + assert_join_windows({"a", "b", "c"}, 'a\\b\\c') + assert_join_windows({"a\\", "b", "c"}, 'a\\b\\c') + assert_join_windows({"a", "b\\", "c"}, 'a\\b\\c') + assert_join_windows({"a", "b", "\\c"}, '\\c') + assert_join_windows({"d:\\", "\\pleep"}, 'd:\\pleep') + assert_join_windows({"d:\\", "a", "b"}, 'd:\\a\\b') + + assert_join_windows({'', 'a'}, 'a') + assert_join_windows({'', '', '', '', 'a'}, 'a') + assert_join_windows({'a', ''}, 'a\\') + assert_join_windows({'a', '', '', '', ''}, 'a\\') + assert_join_windows({'a\\', ''}, 'a\\') + assert_join_windows({'a\\', '', '', '', ''}, 'a\\') + assert_join_windows({'a/', ''}, 'a/') + + assert_join_windows({'a/b', 'x/y'}, 'a/b\\x/y') + assert_join_windows({'/a/b', 'x/y'}, '/a/b\\x/y') + assert_join_windows({'/a/b/', 'x/y'}, '/a/b/x/y') + assert_join_windows({'c:', 'x/y'}, 'c:x/y') + assert_join_windows({'c:a/b', 'x/y'}, 'c:a/b\\x/y') + assert_join_windows({'c:a/b/', 'x/y'}, 'c:a/b/x/y') + assert_join_windows({'c:/', 'x/y'}, 'c:/x/y') + assert_join_windows({'c:/a/b', 'x/y'}, 'c:/a/b\\x/y') + assert_join_windows({'c:/a/b/', 'x/y'}, 'c:/a/b/x/y') + + assert_join_windows({'a/b', '/x/y'}, '/x/y') + assert_join_windows({'/a/b', '/x/y'}, '/x/y') + assert_join_windows({'c:', '/x/y'}, 'c:/x/y') + assert_join_windows({'c:a/b', '/x/y'}, 'c:/x/y') + assert_join_windows({'c:/', '/x/y'}, 'c:/x/y') + assert_join_windows({'c:/a/b', '/x/y'}, 'c:/x/y') + + assert_join_windows({'c:', 'C:x/y'}, 'C:x/y') + assert_join_windows({'c:a/b', 'C:x/y'}, 'C:a/b\\x/y') + assert_join_windows({'c:/', 'C:x/y'}, 'C:/x/y') + assert_join_windows({'c:/a/b', 'C:x/y'}, 'C:/a/b\\x/y') + + for _, x in ipairs({'', 'a/b', '/a/b', 'c:', 'c:a/b', 'c:/', 'c:/a/b'}) do + for _, y in ipairs({'d:', 'd:x/y', 'd:/', 'd:/x/y'}) do + assert_join_windows({x, y}, y) + end + end +end + +print() + + +----------------------------------------------------------------------- +-- os.path.split +----------------------------------------------------------------------- +print_title('os.path.split') +function assert_split(path, sep1, sep2) + print('[test] split: "' .. path ..'" -> ("' .. sep1 .. '", "' .. sep2 .. '")') + local x, y = os.path.split(path) + if x ~= sep1 or y ~= sep2 then + print('failed: ("'..x..'", "'..y..'")') + os.exit() + else + print('passed') + end +end + +assert_split("", "", "") +assert_split(".", "", ".") +assert_split("/foo/bar", "/foo", "bar") +assert_split("/", "/", "") +assert_split("foo", "", "foo") +assert_split("////foo", "////", "foo") +assert_split("//foo//bar", "//foo", "bar") + +if windows then + assert_split("c:\\foo\\bar", 'c:\\foo', 'bar') + assert_split("\\\\conky\\mountpoint\\foo\\bar", '\\\\conky\\mountpoint\\foo', 'bar') + assert_split("c:\\", "c:\\", '') + assert_split("c:/", "c:/", '') + assert_split("c:test", "c:", 'test') + assert_split("c:", "c:", '') + -- assert_split("\\\\conky\\mountpoint\\", "\\\\conky\\mountpoint\\", '') +end + diff --git a/z.lua b/z.lua index c644369..a3d216f 100755 --- a/z.lua +++ b/z.lua @@ -4,7 +4,7 @@ -- z.lua - z.sh implementation in lua, by skywind 2018, 2019 -- Licensed under MIT license. -- --- Version 1.1.0, Last Modified: 2019/02/02 14:51 +-- Version 1.3.0, Last Modified: 2019/02/04 00:06 -- -- * 10x times faster than fasd and autojump -- * 3x times faster than rupa/z @@ -87,6 +87,7 @@ local in_module = pcall(debug.getlocal, 4, 1) and true or false local utils = {} os.path = {} os.argv = arg ~= nil and arg or {} +os.path.sep = windows and '\\' or '/' ----------------------------------------------------------------------- @@ -110,7 +111,7 @@ os.LOG_NAME = os.getenv('_ZL_LOG_NAME') ----------------------------------------------------------------------- --- split string +-- string lib ----------------------------------------------------------------------- function string:split(sSeparator, nMax, bRegexp) assert(sSeparator ~= '') @@ -120,23 +121,21 @@ function string:split(sSeparator, nMax, bRegexp) local bPlain = not bRegexp nMax = nMax or -1 local nField, nStart = 1, 1 - local nFirst,nLast = self:find(sSeparator, nStart, bPlain) + local nFirst, nLast = self:find(sSeparator, nStart, bPlain) while nFirst and nMax ~= 0 do - aRecord[nField] = self:sub(nStart, nFirst-1) - nField = nField+1 - nStart = nLast+1 - nFirst,nLast = self:find(sSeparator, nStart, bPlain) - nMax = nMax-1 + aRecord[nField] = self:sub(nStart, nFirst - 1) + nField = nField + 1 + nStart = nLast + 1 + nFirst, nLast = self:find(sSeparator, nStart, bPlain) + nMax = nMax - 1 end aRecord[nField] = self:sub(nStart) + else + aRecord[1] = '' end return aRecord end - ------------------------------------------------------------------------ --- string starts with ------------------------------------------------------------------------ function string:startswith(text) local size = text:len() if self:sub(1, size) == text then @@ -145,6 +144,52 @@ function string:startswith(text) return false end +function string:lstrip() + if self == nil then return nil end + local s = self:gsub('^%s+', '') + return s +end + +function string:rstrip() + if self == nil then return nil end + local s = self:gsub('%s+$', '') + return s +end + +function string:strip() + return self:lstrip():rstrip() +end + +function string:rfind(key) + if keyword == '' then + return self:len(), 0 + end + local length = self:len() + local start, ends = self:reverse():find(key:reverse()) + if start == nil then + return nil + end + return (length - ends + 1), (length - start + 1) +end + +function string:join(parts) + if parts == nil or #parts == 0 then + return '' + end + local size = #parts + local text = '' + local index = 1 + while index <= size do + if index == 1 then + text = text .. parts[index] + else + text = text .. self .. parts[index] + end + index = index + 1 + end + return text +end + ----------------------------------------------------------------------- -- table size @@ -297,15 +342,28 @@ end ----------------------------------------------------------------------- --- get absolute path +-- absolute path (simulated) +----------------------------------------------------------------------- +function os.path.absolute(path) + local pwd = os.pwd() + return os.path.normpath(os.path.join(pwd, path)) +end + + +----------------------------------------------------------------------- +-- absolute path (system call, can fall back to os.path.absolute) ----------------------------------------------------------------------- function os.path.abspath(path) + if path == '' then path = '.' end if windows then local script = 'FOR /f "delims=" %%i IN ("%s") DO @echo %%~fi' local script = string.format(script, path) local script = 'cmd.exe /C ' .. script .. ' 2> nul' local output = os.call(script) - return output:gsub('%s$', '') + local test = output:gsub('%s$', '') + if test ~= nil and test ~= '' then + return test + end else local test = os.path.which('realpath') if test ~= nil and test ~= '' then @@ -314,15 +372,15 @@ function os.path.abspath(path) return test end end - if os.path.isdir(path) then - if os.path.exists('/bin/sh') and os.path.exists('/bin/pwd') then - local cmd = "/bin/sh -c 'cd \"" ..path .."\"; /bin/pwd'" - test = os.call(cmd) + if os.path.isdir(path) then + if os.path.exists('/bin/sh') and os.path.exists('/bin/pwd') then + local cmd = "/bin/sh -c 'cd \"" ..path .."\"; /bin/pwd'" + test = os.call(cmd) if test ~= nil and test ~= '' then return test end - end - end + end + end local test = os.path.which('perl') if test ~= nil and test ~= '' then local s = 'perl -MCwd -e "print Cwd::realpath(\\$ARGV[0])" \'%s\'' @@ -342,6 +400,7 @@ function os.path.abspath(path) end end end + return os.path.absolute(path) end @@ -379,20 +438,20 @@ end ----------------------------------------------------------------------- -- is absolute path ----------------------------------------------------------------------- -function os.path.isabs(pathname) - local h1 = pathname:sub(1, 1) - if windows then - local h2 = pathname:sub(2, 2) - local h3 = pathname:sub(3, 3) - if h1 == '/' or h1 == '\\' then - return true - end - if h2 == ':' and (h3 == '/' or h3 == '\\') then - return true - end - elseif h1 == '/' then +function os.path.isabs(path) + if path == nil or path == '' then + return false + elseif path:sub(1, 1) == '/' then return true end + if windows then + local head = path:sub(1, 1) + if head == '\\' then + return true + elseif path:match('^%a:[/\\]') ~= nil then + return true + end + end return false end @@ -411,6 +470,165 @@ function os.path.norm(pathname) end +----------------------------------------------------------------------- +-- normalize . and .. +----------------------------------------------------------------------- +function os.path.normpath(path) + if os.path.sep ~= '/' then + path = path:gsub('\\', '/') + end + path = path:gsub('/+', '/') + local srcpath = path + local basedir = '' + local isabs = false + if windows and path:sub(2, 2) == ':' then + basedir = path:sub(1, 2) + path = path:sub(3, -1) + end + if path:sub(1, 1) == '/' then + basedir = basedir .. '/' + isabs = true + path = path:sub(2, -1) + end + local parts = path:split('/') + local output = {} + for _, path in ipairs(parts) do + if path == '.' or path == '' then + elseif path == '..' then + local size = #output + if size == 0 then + if not isabs then + table.insert(output, '..') + end + elseif output[size] == '..' then + table.insert(output, '..') + else + table.remove(output, size) + end + else + table.insert(output, path) + end + end + path = basedir .. string.join('/', output) + if windows then path = path:gsub('/', '\\') end + return path == '' and '.' or path +end + + +----------------------------------------------------------------------- +-- join two path +----------------------------------------------------------------------- +function os.path.join(path1, path2) + if path1 == nil or path1 == '' then + if path2 == nil or path2 == '' then + return '' + else + return path2 + end + elseif path2 == nil or path2 == '' then + local head = path1:sub(-1, -1) + if head == '/' or (windows and head == '\\') then + return path1 + end + return path1 .. os.path.sep + elseif os.path.isabs(path2) then + if windows then + local head = path2:sub(1, 1) + if head == '/' or head == '\\' then + if path1:match('^%a:') then + return path1:sub(1, 2) .. path2 + end + end + end + return path2 + elseif windows then + local d1 = path1:match('^%a:') and path1:sub(1, 2) or '' + local d2 = path2:match('^%a:') and path2:sub(1, 2) or '' + if d1 ~= '' then + if d2 ~= '' then + if d1:lower() == d2:lower() then + return d2 .. os.path.join(path1:sub(3), path2:sub(3)) + else + return path2 + end + end + elseif d2 ~= '' then + return path2 + end + end + local postsep = true + local len1 = path1:len() + local len2 = path2:len() + if path1:sub(-1, -1) == '/' then + postsep = false + elseif windows then + if path1:sub(-1, -1) == '\\' then + postsep = false + elseif len1 == 2 and path1:sub(2, 2) == ':' then + postsep = false + end + end + if postsep then + return path1 .. os.path.sep .. path2 + else + return path1 .. path2 + end +end + + +----------------------------------------------------------------------- +-- split +----------------------------------------------------------------------- +function os.path.split(path) + if path == '' then + return '', '' + end + local pos = path:rfind('/') + if os.path.sep == '\\' then + local p2 = path:rfind('\\') + if pos == nil and p2 ~= nil then + pos = p2 + elseif p1 ~= nil and p2 ~= nil then + pos = (pos < p2) and pos or p2 + end + if path:match('^%a:[/\\]') and pos == nil then + return path:sub(1, 2), path:sub(3) + end + end + if pos == nil then + if windows then + local drive = path:match('^%a:') and path:sub(1, 2) or '' + if drive ~= '' then + return path:sub(1, 2), path:sub(3) + end + end + return '', path + elseif pos == 1 then + return path:sub(1, 1), path:sub(2) + elseif windows then + local drive = path:match('^%a:') and path:sub(1, 2) or '' + if pos == 3 then + return path:sub(1, 3), path:sub(4) + end + end + local head = path:sub(1, pos) + local tail = path:sub(pos + 1) + if not windows then + local test = string.rep('/', head:len()) + if head ~= test then + head = head:gsub('/+$', '') + end + else + local t1 = string.rep('/', head:len()) + local t2 = string.rep('\\', head:len()) + if head ~= t1 and head ~= t2 then + head = head:gsub('[/\\]+$', '') + end + end + return head, tail +end + + ----------------------------------------------------------------------- -- check subdir ----------------------------------------------------------------------- @@ -1017,6 +1235,9 @@ function z_match(patterns, method, subdir) end table.sort(M, function (a, b) return a.score > b.score end) local pwd = (PWD == nil or PWD == '') and os.getenv('PWD') or PWD + if pwd == nil or pwd == '' then + pwd = os.pwd() + end if pwd ~= '' and pwd ~= nil then if subdir then local N = {} @@ -1187,9 +1408,70 @@ end ----------------------------------------------------------------------- --- cd to parent directories which contains keyword +-- find_vcs_root ----------------------------------------------------------------------- -function cd_backward(args, options) +function find_vcs_root(path) + local markers = os.getenv('_ZL_ROOT_MARKERS') + local markers = markers and markers or '.git,.svn,.hg,.root' + local markers = string.split(markers, ',') + path = os.path.absolute(path) + while true do + for _, marker in ipairs(markers) do + local test = os.path.join(path, marker) + if os.path.exists(test) then + return path + end + end + local parent, _ = os.path.split(path) + if path == parent then break end + path = parent + end + return nil +end + + +----------------------------------------------------------------------- +-- cd to parent directories which contains keyword +-- #args == 0 -> returns to vcs root +-- #args == 1 -> returns to parent dir starts with args[1] +-- #args == 2 -> returns string.replace($PWD, args[1], args[2]) +----------------------------------------------------------------------- +function cd_backward(args, options, pwd) + local nargs = #args + local pwd = (pwd ~= nil) and pwd or os.pwd() + if nargs == 0 then + return find_vcs_root(pwd) + elseif nargs == 1 then + local test = windows and pwd:gsub('\\', '/') or pwd + local key = '/' .. args[1] + if not key:match('%u') then + test = test:lower() + end + local pos, _ = test:rfind(key) + if not pos then + return nil + end + local ends = test:find('/', pos + key:len()) + if not ends then + ends = test:len() + end + local path = pwd:sub(1, (not ends) and test:len() or ends) + return os.path.normpath(path) + else + local test = windows and pwd:gsub('\\', '/') or pwd + local src = args[1] + local dst = args[2] + if not src:match('%u') then + test = test:lower() + end + local start, ends = test:rfind(src) + if not start then + return pwd + end + local lhs = pwd:sub(1, start - 1) + local rhs = pwd:sub(ends + 1) + return lhs .. dst .. rhs + end end @@ -1247,7 +1529,7 @@ function main(argv) elseif options['-d'] then path = cd_detour(args, options) elseif #args == 0 then - path = os.path.expand('~') + path = nil else path = z_cd(args) if path == nil and Z_MATCHMODE ~= 0 then diff --git a/z.lua.plugin.zsh b/z.lua.plugin.zsh index 72210ad..1110586 100644 --- a/z.lua.plugin.zsh +++ b/z.lua.plugin.zsh @@ -27,5 +27,6 @@ eval "$($ZLUA_EXEC $ZLUA_SCRIPT --init zsh once enhanced)" alias zz='z -i' alias zc='z -c' alias zf='z -I' +alias zb='z -b' alias zzc='zz -c'