1
0
mirror of https://github.com/skywind3000/z.lua synced 2026-03-15 04:09:49 +00:00

commit z.lua

This commit is contained in:
skywind3000
2018-03-21 20:06:08 +08:00
parent a5cf95f2da
commit 21f4f0ba8b

941
z.lua Normal file
View File

@@ -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