Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

z.lua解析 #28

Open
BruceChen7 opened this issue Dec 8, 2020 · 0 comments
Open

z.lua解析 #28

BruceChen7 opened this issue Dec 8, 2020 · 0 comments

Comments

@BruceChen7
Copy link
Owner

BruceChen7 commented Dec 8, 2020

参考资料

这是一个用lua来执行快速跳转的工具,来替代z.sh,执行速度快。

分析

eval "$(lua /path/to/z.lua  --init bash once enhanced)"   # BASH 初始化

我们执行$(lua /path/to/z.lua --init bash once enanced) 输出的脚本是

ZLUA_SCRIPT="/usr/local/bin/z.lua"
ZLUA_LUAEXE="/usr/bin/lua"

_zlua() {
        local arg_mode=""
        local arg_type=""
        local arg_subdir=""
        local arg_inter=""
        local arg_strip=""
        if [ "$1" = "--add" ]; then
                shift
                _ZL_RANDOM="$RANDOM" "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" --add "$@"
                return
        elif [ "$1" = "--complete" ]; then
                shift
                "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" --complete "$@"
                return
        fi
        while [ "$1" ]; do
                case "$1" in
                        -l) local arg_mode="-l" ;;
                        -e) local arg_mode="-e" ;;
                        -x) local arg_mode="-x" ;;
                        -t) local arg_type="-t" ;;
                        -r) local arg_type="-r" ;;
                        -c) local arg_subdir="-c" ;;
                        -s) local arg_strip="-s" ;;
                        -i) local arg_inter="-i" ;;
                        -I) local arg_inter="-I" ;;
                        -h|--help) local arg_mode="-h" ;;
                        --purge) local arg_mode="--purge" ;;
                        *) break ;;
                esac
                shift
        done
        if [ "$arg_mode" = "-h" ] || [ "$arg_mode" = "--purge" ]; then
                "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" $arg_mode
        elif [ "$arg_mode" = "-l" ] || [ "$#" -eq 0 ]; then
                "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" -l $arg_subdir $arg_type $arg_strip "$@"
        elif [ -n "$arg_mode" ]; then
                "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" $arg_mode $arg_subdir $arg_type $arg_inter "$@"
        else
                local zdest=$("$ZLUA_LUAEXE" "$ZLUA_SCRIPT" --cd $arg_type $arg_subdir $arg_inter "$@")
                if [ -n "$zdest" ] && [ -d "$zdest" ]; then
                        if [ -z "$_ZL_CD" ]; then
                                builtin cd "$zdest"
                        else
                                $_ZL_CD "$zdest"
                        fi
                        if [ -n "$_ZL_ECHO" ]; then pwd; fi
                fi
        fi
}
# alias ${_ZL_CMD:-z}='_zlua 2>&1'
alias ${_ZL_CMD:-z}='_zlua'

_zlua_precmd() {
    [ "$_ZL_PREVIOUS_PWD" = "$PWD" ] && return
    _ZL_PREVIOUS_PWD="$PWD"
    (_zlua --add "$PWD" 2> /dev/null &)
}
case "$PROMPT_COMMAND" in
        *_zlua_precmd*) ;;
        *) PROMPT_COMMAND="_zlua_precmd${PROMPT_COMMAND:+;$PROMPT_COMMAND}" ;;
esac

if [ -n "$BASH_VERSION" ]; then
        complete -o filenames -C '_zlua --complete "$COMP_LINE"' ${_ZL_CMD:-z}
fi

_zlua
_zlua是一段胶水代码,其做了两件事情

  • hook住cd命令,在切换cd命令的时候,执行_zlua_precmd,保存当前的PWD,如果当前的PWD和之前的不一样,调用z_lua来保存到数据中
  • hook住tab补全

PROMPT_COMMAND就是说每执行一个命令前,PROMPT_COMMAND 里面先执行,然后执行PROMPT

**_zlua --add **

首先定义了z.lua脚本的位置和解释器的位置。然后定义shell函数_z_lua。看下最关键的信息:

# 如果是使用bash
if [ -n "$BASH_VERSION" ]; then
        #使用complete来不全文件列表
		#-C用来产生候选项的命令,会调用一个sub shell来执行
        complete -o filenames -C '_zlua --complete "$COMP_LINE"' ${_ZL_CMD:-z}
fi

这里可以查看$COMP_LINE的含义,这里的概念programmable completion.这里我们可以理解成,当我们在终端打z xxx tab键的时候, 这个变量$COMP_LINE就是我们打入的字符串,接着, 叫做我们可以看_zlua,实际上直接跳到

"$ZLUA_LUAEXE" "$ZLUA_SCRIPT" --complete "$@"

然后开始执行z.lua的逻辑,

z.lua的complete逻辑

进入z.lua的逻辑是,在命令行传入了--complete的和 "$@",

function main(argv):
   ...
   elseif options['--complete'] then                          
      local line = args[1] and args[1] or ''                 
      local head = line:sub(Z_CMD:len()+1):gsub('^%s+', '')  
      local M = z_match({head}, Z_METHOD, Z_SUBDIR)          
      -- 打印所有的item                                      
      for _, item in pairs(M) do                             
          print(item.name)                                   
      end
   ....
end

z.lua添加path

这部分逻辑比较清晰:

  • 首先查看当前路径是否是HOME目录,如果是,skip
  • 是否是指定忽略的目录(比如.git等),忽略
  • 然后看是否已经存在了该路径,如果存在了路径,将会更新路径的rank
  • 最后将新的数据保存到data_file
function z_add(path)
	local paths = {}
	local count = 0
	if type(path) == 'table' then
			paths = path
	elseif type(path) == 'string' then
			paths[1] = path -- 第一次执行放入第一个位置
	end
	if table.length(paths) == 0 then
			return false
	end
	-- 获取HOME目录
	local H = os.getenv('HOME')
	local M = data_load(DATA_FILE)

	local nc = os.getenv('_ZL_NO_CHECK')
	if nc == nil or nc == '' or nc == '0' then
			-- 执行数据检查
			M = data_filter(M)
	end
	-- 插入当前工作目录, insert paths
	for _, path in pairs(paths) do
		  if os.path.isdir(path) and os.path.isabs(path) then
				local skip = false
				local test = path
				path = os.path.norm(path)
				-- check ignore
				if windows then
					if path:len() == 3 and path:sub(2, 2) == ':' then
							local tail = path:sub(3, 3)
							if tail == '/' or tail == '\\' then
									skip = true
							end
					end
					test = os.path.norm(path:lower())
				else
					--如果是HOME目录,那么直接忽略
					if H == path then
							skip = true
					end
				end
				-- check exclude
				if not skip then
					for _, exclude in ipairs(Z_EXCLUDE) do
						-- 目录是以排除目录开头,那么直接忽略
						if test:startswith(exclude) then
								skip = true
								break
						end
					end
				end

				if not skip then                    
					 --更新了数据                    
					 M = data_insert(M, path)
					 count = count + 1
				end                            
		  end
	end
end

注意这里有更新数据文件中path的逻辑,这部分逻辑在data_insert()中体现,这里由一个数据老化的概念

  • 总的访问次数超过了5000次(默认),那么 * 0.9后清楚不足1,清除这个path
 function data_insert(M, filename)
     local i = 1
     local sumscore = 0
     for i = 1, #M do
         local item = M[i]
         sumscore = sumscore + item.rank
     end
     if sumscore >= MAX_AGE then
         local X = {}
         for i = 1, #M do
             local item = M[i]
             item.rank = item.rank * 0.9
			 -- 清除不足1的rank
             if item.rank >= 1.0 then
                 table.insert(X, item)
             end
         end
         M = X
     end
     local nocase = path_case_insensitive()
     local name = filename
     local key = nocase and string.lower(name) or name
     local find = false
     local current = os.time()
     for i = 1, #M do
         local item = M[i]
         if not nocase then
             if name == item.name then
                 item.rank = item.rank + 1
                 item.time = current
                 find = true
                 break
             end
         else
		 	 -- 存在,则rank + 1
			 -- 更新访问时间戳
             if key == string.lower(item.name) then
                 item.rank = item.rank + 1
                 item.time = current
                 find = true
                 break
             end
	     end
     end
	 --初始化时frecent为rank
	 if not find then
     	local item = {}
     	item.name = name
     	item.rank = 1
     	item.time = current
     	item.frecent = item.rank
     	table.insert(M, item)
 	 end
 return M

注意这里的frecent值默认时rank的值,也就是1,我们发现在切换工作目录时,也就是调用data_insert时,并没有frecent值进行更新,只是在不存在该路径的时候,给了一个默认值,那么什么时候更新这个frecent值呢,在match的时候。

z_match

function z_match(patterns, method, subdir)
    patterns = patterns ~= nil and patterns or {}
    -- 默认支持几种匹配的pattern
    method = method ~= nil and method or 'frecent'
    subdir = subdir ~= nil and subdir or false
    local M = data_load(DATA_FILE)
    -- 从数据集中选择匹配的path
	M = data_select(M, patterns, false)
	M = data_filter(M)
	if Z_MATCHNAME then
		local N = data_select(M, patterns, true)
		N = data_filter(N)
		if #N > 0 then
			M = N
		end
	end
    -- 在匹配时,进行对frecent更新
	M = data_update_frecent(M)
    -- 匹配算法如果时按照时间
	if method == 'time' then
		current = os.time()
		for _, item in pairs(M) do
			item.score = item.time - current
		end
	elseif method == 'rank' then
        --根据分值来排名
		for _, item in pairs(M) do
			item.score = item.rank
		end
	else
		for _, item in pairs(M) do
            -- 默认使用frecent值
		    item.score = item.frecent
		end
	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 = {}
			--获取满足当前目录中的子目录,并过滤
			for _, item in pairs(M) do
				if os.path.subdir(pwd, item.name) then
					table.insert(N, item)
				end
			end
			M = N
		end
		if Z_SKIPPWD then
			local N = {}
			local key = windows and string.lower(pwd) or pwd
			for _, item in pairs(M) do
				local match = false
				local name = windows and string.lower(item.name) or item.name
				if name ~= key then
						table.insert(N, item)
				end
			end
			M = N
		end
	end
	return M
end

match的逻辑比较简单

  • 根据输入的pattern来匹配在数据文件中的路径
  • 过滤了不存在的路径
  • 如果是设置匹配最后文件名,再次过滤
  • 对命中的路径,更新frecent值
  • 根据匹配的算法,来计算score
    • 时间
    • rank值
    • frecent值
  • 根据score值进行排序
  • 如果打开了子目录模式
    • 当前选中的路径如果当前工作目录中的子目录 ,那么选中该路径

这里涉及到更新frecent的值

  • 如果在1个小时之内访问,frecent为当前rank * 4
  • 如果1天值之内访问,frecent为rank * 2
  • 如果小于604800,frecent值为rank * 0.5
  • 否则frecent值为rank * 0.25

什么时候更新rank值呢?在我们改变我们的工作目录,就会将rank + 1,总结起来,如果我们输入的path pattern,匹配到相关路径,那么frecent值 将会改变,如果是我们切换到了该path,则rank值会增加。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant