From 21f4f0ba8b69ff083a0682589a5c71b0eeac0d5c Mon Sep 17 00:00:00 2001 From: skywind3000 Date: Wed, 21 Mar 2018 20:06:08 +0800 Subject: [PATCH] commit z.lua --- z.lua | 941 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 941 insertions(+) create mode 100644 z.lua diff --git a/z.lua b/z.lua new file mode 100644 index 0000000..5630df5 --- /dev/null +++ b/z.lua @@ -0,0 +1,941 @@ +--===================================================================== +-- +-- zm.lua - z.sh implementation for lua +-- +-- Created by skywind on 2018/03/19 +-- Last Modified: 2018/03/19 11:11:47 +-- +--===================================================================== + + +----------------------------------------------------------------------- +-- Module Header +----------------------------------------------------------------------- +local modname = 'z' +local M = {} +_G[modname] = M +package.loaded[modname] = M --return modname +setmetatable(M,{__index = _G}) + +if _ENV ~= nil then + _ENV[modname] = M +else + setfenv(1, M) +end + + +----------------------------------------------------------------------- +-- Environment +----------------------------------------------------------------------- +local windows = package.config:sub(1, 1) ~= '/' and true or false +local in_module = pcall(debug.getlocal, 4, 1) and true or false +os.path = {} +os.argv = arg ~= nil and arg or {} + + +----------------------------------------------------------------------- +-- Global Variable +----------------------------------------------------------------------- +MAX_AGE = 5000 +DATA_FILE = '~/.zlua' +PRINT_TO_STDERR = false +PWD = '' +Z_METHOD = 'frecent' +Z_SUBDIR = false +Z_EXCLUDE = {} + + +----------------------------------------------------------------------- +-- split string +----------------------------------------------------------------------- +function string:split(sSeparator, nMax, bRegexp) + assert(sSeparator ~= '') + assert(nMax == nil or nMax >= 1) + local aRecord = {} + if self:len() > 0 then + local bPlain = not bRegexp + nMax = nMax or -1 + local nField, nStart = 1, 1 + 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 + end + aRecord[nField] = self:sub(nStart) + end + return aRecord +end + + +----------------------------------------------------------------------- +-- string starts with +----------------------------------------------------------------------- +function string:startswith(text) + local size = text:len() + if self:sub(1, size) == text then + return true + end + return false +end + + +----------------------------------------------------------------------- +-- table size +----------------------------------------------------------------------- +function table.length(T) + local count = 0 + if T == nil then return 0 end + for _ in pairs(T) do count = count + 1 end + return count +end + + +----------------------------------------------------------------------- +-- print table +----------------------------------------------------------------------- +function dump(o) + if type(o) == 'table' then + local s = '{ ' + for k,v in pairs(o) do + if type(k) ~= 'number' then k = '"'..k..'"' end + s = s .. '['..k..'] = ' .. dump(v) .. ',' + end + return s .. '} ' + else + return tostring(o) + end +end + + +----------------------------------------------------------------------- +-- print table +----------------------------------------------------------------------- +function printT(table, level) + key = "" + local func = function(table, level)end + func = function(table, level) + level = level or 1 + local indent = "" + for i = 1, level do + indent = indent.." " + end + + if key ~= "" then + print(indent..key.." ".."=".." ".."{") + else + print(indent .. "{") + end + + key = "" + for k, v in pairs(table) do + if type(v) == "table" then + key = k + func(v, level + 1) + else + local content = string.format("%s%s = %s", indent .. " ",tostring(k), tostring(v)) + print(content) + end + end + print(indent .. "}") + end + func(table, level) +end + + +----------------------------------------------------------------------- +-- invoke command and retrive output +----------------------------------------------------------------------- +function os.call(command) + local fp = io.popen(command) + if fp == nil then + return nil + end + local line = fp:read('*l') + fp:close() + return line +end + + +----------------------------------------------------------------------- +-- get current path +----------------------------------------------------------------------- +function os.pwd() + if windows then + local fp = io.popen('cd') + if fp == nil then + return '' + end + local line = fp:read('*l') + fp:close() + return line + else + local fp = io.popen('pwd') + if fp == nil then + return '' + end + local line = fp:read('*l') + fp:close() + return line + end +end + + +----------------------------------------------------------------------- +-- which executable +----------------------------------------------------------------------- +function os.path.which(exename) + local path = os.getenv('PATH') + if windows then + paths = ('.;' .. path):split(';') + else + paths = path:split(':') + end + for _, path in pairs(paths) do + if not windows then + local name = path .. '/' .. exename + if os.path.exists(name) then + return name + end + else + for _, ext in pairs({'.exe', '.cmd', '.bat'}) do + local name = path .. '\\' .. exename .. ext + if path == '.' then + name = exename .. ext + end + if os.path.exists(name) then + return name + end + end + end + end + return nil +end + + +----------------------------------------------------------------------- +-- get absolute path +----------------------------------------------------------------------- +function os.path.abspath(path) + if windows then + local script = 'FOR /F %%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 + else + local test = os.path.which('realpath') + if test ~= nil and test ~= '' then + return os.call('realpath \'' .. path .. '\'') + end + local test = os.path.which('perl') + if test ~= nil and test ~= '' then + local s = 'perl -MCwd -e "print Cwd::realpath(\\$ARGV[0])" \'%s\'' + local s = string.format(s, path) + return os.call(s) + end + for _, python in pairs({'python', 'python2', 'python3'}) do + local s = 'sys.stdout.write(os.path.abspath(sys.argv[1]))' + local s = '-c "import os, sys;' .. s .. '" \'' .. path .. '\'' + local s = python .. ' ' .. s + local test = os.path.which(python) + if test ~= nil and test ~= '' then + return os.call(s) + end + end + end +end + + +----------------------------------------------------------------------- +-- dir exists +----------------------------------------------------------------------- +function os.path.isdir(pathname) + local name = pathname .. '/' + local ok, err, code = os.rename(name, name) + if not ok then + if code == 13 then + return true + end + return false + end + return true +end + + +----------------------------------------------------------------------- +-- file or path exists +----------------------------------------------------------------------- +function os.path.exists(name) + local ok, err, code = os.rename(name, name) + if not ok then + if code == 13 then + return true + end + return false + end + return true +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 + return true + end + return false +end + + +----------------------------------------------------------------------- +-- normalize path +----------------------------------------------------------------------- +function os.path.norm(pathname) + if windows then + pathname = pathname:gsub('\\', '/') + end + if windows then + pathname = pathname:gsub('/', '\\') + end + return pathname +end + + +----------------------------------------------------------------------- +-- check subdir +----------------------------------------------------------------------- +function os.path.subdir(basename, subname) + if windows then + basename = basename:gsub('\\', '/') + subname = subname:gsub('\\', '/') + basename = basename:lower() + subname = subname:lower() + end + local last = basename:sub(-1, -1) + if last ~= '/' then + basename = basename .. '/' + end + if subname:find(basename, 0, true) == 1 then + return true + end + return false +end + + +----------------------------------------------------------------------- +-- expand user home +----------------------------------------------------------------------- +function os.path.expand(pathname) + if not pathname:find('~') then + return pathname + end + local home = '' + if windows then + home = os.getenv('USERPROFILE') + else + home = os.getenv('HOME') + end + if pathname == '~' then + return home + end + local head = pathname:sub(1, 2) + if windows then + if head == '~/' or head == '~\\' then + return home .. '\\' .. pathname:sub(3, -1) + end + elseif head == '~/' then + return home .. '/' .. pathname:sub(3, -1) + end + return pathname +end + + +----------------------------------------------------------------------- +-- get lua executable +----------------------------------------------------------------------- +function os.interpreter() + if arg == nil then + return nil + end + local lua = arg[-1] + if lua == 'lua' then + return os.path.which('lua') + end + return os.path.abspath(lua) +end + + +----------------------------------------------------------------------- +-- get script name +----------------------------------------------------------------------- +function os.scriptname() + if arg == nil then + return nil + end + local script = arg[0] + return os.path.abspath(script) +end + + +----------------------------------------------------------------------- +-- parse option +----------------------------------------------------------------------- +function os.getopt(argv) + local args = {} + local options = {} + argv = argv ~= nil and argv or os.argv + if argv == nil then + return nil, nil + elseif (#argv) == 0 then + return options, args + end + local count = #argv + local index = 1 + while index <= count do + local arg = argv[index] + local head = arg:sub(1, 1) + if head ~= '-' then + break + end + index = index + 1 + local part = arg:split('=') + options[part[1]] = part[2] ~= nil and part[2] or '' + end + while index <= count do + table.insert(args, argv[index]) + index = index + 1 + end + return options, args +end + + +----------------------------------------------------------------------- +-- returns true for path is insensitive +----------------------------------------------------------------------- +function path_case_insensitive() + if windows then + return true + end + local eos = os.getenv('OS') + eos = eos ~= nil and eos or '' + eos = eos:lower() + if eos:sub(1, 7) == 'windows' then + return true + end + return false +end + + +----------------------------------------------------------------------- +-- load and split data +----------------------------------------------------------------------- +function data_load(filename) + local M = {} + local N = {} + local insensitive = path_case_insensitive() + fp = io.open(os.path.expand(filename), 'r') + if fp == nil then + return {} + end + for line in fp:lines() do + local part = string.split(line, '|') + local item = {} + if part and part[1] and part[2] and part[3] then + local key = insensitive and part[1]:lower() or part[1] + item.name = part[1] + item.rank = tonumber(part[2]) + item.time = tonumber(part[3]) + 0 + item.frecent = item.rank + if string.len(part[3]) < 12 then + if item.rank ~= nil and item.time ~= nil then + if N[key] == nil then + table.insert(M, item) + N[key] = 1 + end + end + end + end + end + fp:close() + return M +end + + +----------------------------------------------------------------------- +-- save data +----------------------------------------------------------------------- +function data_save(filename, M) + local fp = nil + local tmpname = nil + local i + filename = os.path.expand(filename) + if windows then + fp = io.open(filename, 'w') + else + tmpname = os.tmpname() + fp = io.open(tmpname, 'w') + end + if fp == nil then + return false + end + for i = 1, #M do + local item = M[i] + local text = item.name .. '|' .. item.rank .. '|' .. item.time + fp:write(text .. '\n') + end + fp:close() + if tmpname ~= nil then + os.rename(tmpname, filename) + os.remove(tmpname) + end + return true +end + + +----------------------------------------------------------------------- +-- filter out bad dirname +----------------------------------------------------------------------- +function data_filter(M) + local N = {} + local i + M = M ~= nil and M or {} + for i = 1, #M do + local item = M[i] + if os.path.isdir(item.name) then + table.insert(N, item) + end + end + return N +end + + +----------------------------------------------------------------------- +-- insert item +----------------------------------------------------------------------- +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 + if item.rank >= 1.0 then + table.insert(X, item) + end + end + 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 + if key == string.lower(item.name) then + item.rank = item.rank + 1 + item.time = current + find = true + break + end + end + end + 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 +end + + +----------------------------------------------------------------------- +-- change pattern +----------------------------------------------------------------------- +function case_insensitive_pattern(pattern) + -- find an optional '%' (group 1) followed by any character (group 2) + local p = pattern:gsub("(%%?)(.)", function(percent, letter) + + if percent ~= "" or not letter:match("%a") then + -- if the '%' matched, or `letter` is not a letter, return "as is" + return percent .. letter + else + -- else, return a case-insensitive character class of the matched letter + return string.format("[%s%s]", letter:lower(), letter:upper()) + end + end) + return p +end + + +----------------------------------------------------------------------- +-- pathmatch +----------------------------------------------------------------------- +function path_match(pathname, patterns) + local pos = 1 + local i = 0 + for i = 1, #patterns do + local pat = patterns[i] + start, endup = pathname:find(pat, pos) + if start == nil or endup == nil then + return false + end + pos = endup + 1 + end + return true +end + + +----------------------------------------------------------------------- +-- select matched pathnames +----------------------------------------------------------------------- +function data_select(M, patterns) + local N = {} + local i = 1 + local pats = {} + for i = 1, #patterns do + local p = patterns[i] + table.insert(pats, case_insensitive_pattern(p)) + end + for i = 1, #M do + local item = M[i] + if path_match(item.name, pats) then + table.insert(N, item) + end + end + return N +end + + +----------------------------------------------------------------------- +-- update frecent +----------------------------------------------------------------------- +function data_update_frecent(M) + local current = os.time() + local i + for i = 1, #M do + local item = M[i] + local dx = current - item.time + if dx < 3600 then + item.frecent = item.rank * 4 + elseif dx < 86400 then + item.frecent = item.rank * 2 + elseif dx < 604800 then + item.frecent = item.rank * 0.5 + else + item.frecent = item.rank * 0.25 + end + end + return M +end + + +----------------------------------------------------------------------- +-- add path +----------------------------------------------------------------------- +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 + local H = os.getenv('HOME') + local M = data_load(DATA_FILE) + M = data_filter(M) + -- 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 + 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 + if count > 0 then + data_save(DATA_FILE, M) + end + return true +end + + +----------------------------------------------------------------------- +-- match method: frecent, rank, time +----------------------------------------------------------------------- +function z_match(patterns, method, subdir) + patterns = patterns ~= nil and patterns or {} + method = method ~= nil and method or 'frecent' + subdir = subdir ~= nil and subdir or false + local M = data_load(DATA_FILE) + M = data_select(M, patterns) + M = data_filter(M) + 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 + item.score = item.frecent + end + end + table.sort(M, function (a, b) return a.score > b.score end) + if subdir then + local pwd = (PWD == nil or PWD == '') and os.getenv('PWD') or PWD + if pwd ~= '' and pwd ~= nil 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 + end + return M +end + + +----------------------------------------------------------------------- +-- pretty print +----------------------------------------------------------------------- +function z_print(M, number) + local N = {} + local maxsize = 10 + local numsize = string.len(tostring(#M)) + for _, item in pairs(M) do + local record = {} + record.score = string.format('%.2f', item.score) + record.name = item.name + table.insert(N, record) + if record.score:len() > maxsize then + maxsize = record.score:len() + end + end + for i = #N, 1, -1 do + local record = N[i] + local line = record.score + local dx = maxsize - line:len() + if dx > 0 then + line = line .. string.rep(' ', dx) + end + line = line .. ' ' .. record.name + if number then + local head = tostring(i) + if head:len() < numsize then + head = string.rep(' ', numsize - head:len()) .. head + end + line = head .. ': ' .. line + end + if not PRINT_TO_STDERR then + print(line) + else + io.stderr:write(line .. '\n') + end + end +end + + +----------------------------------------------------------------------- +-- calculate jump dir +----------------------------------------------------------------------- +function z_cd(patterns) + if patterns == nil then + return nil + end + if #patterns == 0 then + return nil + end + local last = patterns[#patterns] + if last == '~' then + return os.path.expand('~') + end + if os.path.isabs(last) and os.path.isdir(last) then + return last + end + local M = z_match(patterns, Z_METHOD, Z_SUBDIR) + if M == nil then + return nil + end + if #M == 0 then + return nil + end + return M[1].name +end + + + + +----------------------------------------------------------------------- +-- main entry +----------------------------------------------------------------------- +function main(argv) + local options, args = os.getopt(argv) + if options == nil then + return false + elseif table.length(args) == 0 and table.length(options) == 0 then + print(os.argv[0] .. ': missing arguments') + help = os.argv[-1] .. ' ' .. os.argv[0] .. ' --help' + print('Try \'' .. help .. '\' for more information') + return false + end + if false then + print("options: ") + printT(options) + print("args: ") + printT(args) + end + if options['-c'] ~= nil then + Z_SUBDIR = true + end + if options['-r'] ~= nil then + Z_METHOD = 'rank' + elseif options['-t'] ~= nil then + Z_METHOD = 'time' + end + if options['--cd'] ~= nil then + local path = '' + if #args == 0 then + path = os.path.expand('~') + else + path = z_cd(args) + end + if path ~= nil then + io.write(path) + end + elseif options['--add'] ~= nil then + z_add(args) + elseif options['-l'] ~= nil then + local M = z_match(args ~= nil and args or {}, Z_METHOD, Z_SUBDIR) + z_print(M) + elseif options['--complete'] ~= nil then + local M = z_match(args ~= nil and args or {}, Z_METHOD, Z_SUBDIR) + for _, item in pairs(M) do + print(item.name) + end + end + return true +end + + +----------------------------------------------------------------------- +-- initialize from environment variable +----------------------------------------------------------------------- +function z_init() + local _zl_data = os.getenv('_ZL_DATA') + local _zl_maxage = os.getenv('_ZL_MAXAGE') + local _zl_exclude = os.getenv('_ZL_EXCLUDE') + if _zl_data ~= nil then + DATA_FILE = _zl_data + end + if _zl_maxage ~= nil then + _zl_maxage = tonumber(_zl_maxage) + if _zl_maxage ~= nil then + MAX_AGE = _zl_maxage + end + end + if _zl_exclude ~= nil then + local part = _zl_exclude:split(windows and ';' or ':') + local insensitive = path_case_insensitive() + for _, name in ipairs(part) do + if insensitive then + name = name:lower() + end + if windows then + name = os.path.norm(name) + end + Z_EXCLUDE[name] = 1 + end + end +end + + +----------------------------------------------------------------------- +-- initialize clink hooks +----------------------------------------------------------------------- +function z_clink_init() + function z_add_to_database() + z_add(clink.get_cwd()) + end + clink.prompt.register_filter(z_add_to_database, 99) +end + + +----------------------------------------------------------------------- +-- testing case +----------------------------------------------------------------------- +if not pcall(debug.getlocal, 4, 1) then + -- main script + z_init() + if clink ~= nil and type(clink) == 'table' and clink.prompt ~= nil then + z_clink_init() + else + main() + end +end + +