#! /usr/bin/env lua
--=====================================================================
--
-- z.lua - a cd command that learns, by skywind 2018-2022
-- Licensed under MIT license.
--
-- Version 1.8.17, Last Modified: 2022/09/29 15:47
--
-- * 10x faster than fasd and autojump, 3x faster than z.sh
-- * available for posix shells: bash, zsh, sh, ash, dash, busybox
-- * available for fish shell, power shell and windows cmd
-- * compatible with lua 5.1, 5.2 and 5.3+
--
-- USE:
--     * z foo        # cd to most frecent dir matching foo
--     * z foo bar    # cd to most frecent dir matching foo and bar
--     * z -r foo     # cd to highest ranked dir matching foo
--     * z -t foo     # cd to most recently accessed dir matching foo
--     * z -l foo     # list matches instead of cd
--     * z -c foo     # restrict matches to subdirs of $PWD
--     * z -e foo     # echo the best match, don't cd
--     * z -x path    # remove path from history
--     * z -i foo     # cd with interactive selection
--     * z -I foo     # cd with interactive selection using fzf
--     * z -b foo     # cd to the parent directory starting with foo
--     * z -b foo bar # replace foo with bar in cwd and cd there
--
-- Bash Install:
--     * put something like this in your .bashrc:
--         eval "$(lua /path/to/z.lua --init bash)"
--
-- Bash Enhanced Mode:
--     * put something like this in your .bashrc:
--         eval "$(lua /path/to/z.lua --init bash enhanced)"
--
-- Bash fzf tab completion Mode:
--     * put something like this in your .bashrc:
--         eval "$(lua /path/to/z.lua --init bash fzf)"
--
-- Zsh Install:
--     * put something like this in your .zshrc:
--         eval "$(lua /path/to/z.lua --init zsh)"
--
-- Posix Shell Install:
--     * put something like this in your .profile:
--         eval "$(lua /path/to/z.lua --init posix)"
--
-- Fish Shell Install:
--     * put something like this in your config file:
--         lua /path/to/z.lua --init fish | source
--
-- Power Shell Install:
--     * put something like this in your config file:
--         Invoke-Expression (& {
--           (lua /path/to/z.lua --init powershell) -join "`n" })
--
-- Windows Install (with Clink):
--     * copy z.lua and z.cmd to clink's home directory
--     * Add clink's home to %PATH% (z.cmd can be called anywhere)
--     * Ensure that "lua" can be called in %PATH%
--
-- Windows Cmder Install:
--     * copy z.lua and z.cmd to cmder/vendor
--     * Add cmder/vendor to %PATH%
--     * Ensure that "lua" can be called in %PATH%
--
-- Windows WSL-1:
--     * Install lua-filesystem module before init z.lua:
--         sudo apt-get install lua-filesystem
--
-- Configure (optional):
--   set $_ZL_CMD in .bashrc/.zshrc to change the command (default z).
--   set $_ZL_DATA in .bashrc/.zshrc to change the datafile (default ~/.zlua).
--   set $_ZL_NO_PROMPT_COMMAND if you're handling PROMPT_COMMAND yourself.
--   set $_ZL_EXCLUDE_DIRS to a comma separated list of dirs to exclude.
--   set $_ZL_ADD_ONCE to 1 to update database only if $PWD changed.
--   set $_ZL_CD to specify your own cd command
--   set $_ZL_ECHO to 1 to display new directory name after cd.
--   set $_ZL_MAXAGE to define a aging threshold (default is 5000).
--   set $_ZL_MATCH_MODE to 1 to enable enhanced matching mode.
--   set $_ZL_NO_CHECK to 1 to disable path validation. z --purge to clear.
--   set $_ZL_USE_LFS to 1 to use lua-filesystem package
--   set $_ZL_HYPHEN to 1 to stop treating hyphen as a regexp keyword
--
--=====================================================================


-----------------------------------------------------------------------
-- Module Header
-----------------------------------------------------------------------
local modname = 'z'
local MM = {}
_G[modname] = MM
package.loaded[modname] = MM  --return modname
setmetatable(MM, {__index = _G})

if _ENV ~= nil then
	_ENV[modname] = MM
else
	setfenv(1, MM)
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
local utils = {}
os.path = {}
os.argv = arg ~= nil and arg or {}
os.path.sep = windows and '\\' or '/'


-----------------------------------------------------------------------
-- Global Variable
-----------------------------------------------------------------------
MAX_AGE = 5000
DATA_FILE = '~/.zlua'
PRINT_MODE = '<stdout>'
PWD = ''
Z_METHOD = 'frecent'
Z_SUBDIR = false
Z_INTERACTIVE = 0
Z_EXCLUDE = {}
Z_CMD = 'z'
Z_MATCHMODE = 0
Z_MATCHNAME = false
Z_SKIPPWD = false
Z_HYPHEN = false

os.LOG_NAME = os.getenv('_ZL_LOG_NAME')


-----------------------------------------------------------------------
-- string lib
-----------------------------------------------------------------------
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)
	else
		aRecord[1] = ''
	end
	return aRecord
end

function string:startswith(text)
	local size = text:len()
	if self:sub(1, size) == text then
		return true
	end
	return false
end

function string:endswith(text)
	return text == "" or self:sub(-#text) == text
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 key == '' then
		return self:len(), 0
	end
	local length = self:len()
	local start, ends = self:reverse():find(key:reverse(), 1, true)
	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
-----------------------------------------------------------------------
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


-----------------------------------------------------------------------
-- write log
-----------------------------------------------------------------------
function os.log(text)
	if not os.LOG_NAME then
		return
	end
	local fp = io.open(os.LOG_NAME, 'a')
	if not fp then
		return
	end
	local date = "[" .. os.date('%Y-%m-%d %H:%M:%S') .. "] "
	fp:write(date .. text .. "\n")
	fp:close()
end


-----------------------------------------------------------------------
-- ffi optimize (luajit has builtin ffi module)
-----------------------------------------------------------------------
os.native = {}
os.native.status, os.native.ffi =  pcall(require, "ffi")
if os.native.status then
	local ffi = os.native.ffi
	if windows then
		ffi.cdef[[
		int GetFullPathNameA(const char *name, uint32_t size, char *out, char **name);
		int ReplaceFileA(const char *dstname, const char *srcname, void *, uint32_t, void *, void *);
		uint32_t GetTickCount(void);
		uint32_t GetFileAttributesA(const char *name);
		uint32_t GetCurrentDirectoryA(uint32_t size, char *ptr);
		uint32_t GetShortPathNameA(const char *longname, char *shortname, uint32_t size);
		uint32_t GetLongPathNameA(const char *shortname, char *longname, uint32_t size);
		]]
		local kernel32 = ffi.load('kernel32.dll')
		local buffer = ffi.new('char[?]', 4100)
		local INVALID_FILE_ATTRIBUTES = 0xffffffff
		local FILE_ATTRIBUTE_DIRECTORY = 0x10
		os.native.kernel32 = kernel32
		function os.native.GetFullPathName(name)
			local hr = kernel32.GetFullPathNameA(name, 4096, buffer, nil)
			return (hr > 0) and ffi.string(buffer, hr) or nil
		end
		function os.native.ReplaceFile(replaced, replacement)
			local hr = kernel32.ReplaceFileA(replaced, replacement, nil, 2, nil, nil)
			return (hr ~= 0) and true or false
		end
		function os.native.GetTickCount()
			return kernel32.GetTickCount()
		end
		function os.native.GetFileAttributes(name)
			return kernel32.GetFileAttributesA(name)
		end
		function os.native.GetLongPathName(name)
			local hr = kernel32.GetLongPathNameA(name, buffer, 4096)
			return (hr ~= 0) and ffi.string(buffer, hr) or nil
		end
		function os.native.GetShortPathName(name)
			local hr = kernel32.GetShortPathNameA(name, buffer, 4096)
			return (hr ~= 0) and ffi.string(buffer, hr) or nil
		end
		function os.native.GetRealPathName(name)
			local short = os.native.GetShortPathName(name)
			if short then
				return os.native.GetLongPathName(short)
			end
			return nil
		end
		function os.native.exists(name)
			local attr = os.native.GetFileAttributes(name)
			return attr ~= INVALID_FILE_ATTRIBUTES
		end
		function os.native.isdir(name)
			local attr = os.native.GetFileAttributes(name)
			local isdir = FILE_ATTRIBUTE_DIRECTORY
			if attr == INVALID_FILE_ATTRIBUTES then
				return false
			end
			return (attr % (2 * isdir)) >= isdir
		end
		function os.native.getcwd()
			local hr = kernel32.GetCurrentDirectoryA(4096, buffer)
			if hr <= 0 then return nil end
			return ffi.string(buffer, hr)
		end
	else
		ffi.cdef[[
		typedef struct { long tv_sec; long tv_usec; } timeval;
		int gettimeofday(timeval *tv, void *tz);
		int access(const char *name, int mode);
		char *realpath(const char *path, char *resolve);
		char *getcwd(char *buf, size_t size);
		]]
		local timeval = ffi.new('timeval[?]', 1)
		local buffer = ffi.new('char[?]', 4100)
		function os.native.gettimeofday()
			local hr = ffi.C.gettimeofday(timeval, nil)
			local sec = tonumber(timeval[0].tv_sec)
			local usec = tonumber(timeval[0].tv_usec)
			return sec + (usec * 0.000001)
		end
		function os.native.access(name, mode)
			return ffi.C.access(name, mode)
		end
		function os.native.realpath(name)
			local path = ffi.C.realpath(name, buffer)
			return (path ~= nil) and ffi.string(buffer) or nil
		end
		function os.native.getcwd()
			local hr = ffi.C.getcwd(buffer, 4099)
			return hr ~= nil and ffi.string(buffer) or nil
		end
	end
	function os.native.tickcount()
		if windows then
			return os.native.GetTickCount()
		else
			return math.floor(os.native.gettimeofday() * 1000)
		end
	end
	os.native.init = true
end


-----------------------------------------------------------------------
-- get current path
-----------------------------------------------------------------------
function os.pwd()
	if os.native and os.native.getcwd then
		local hr = os.native.getcwd()
		if hr then return hr end
	end
	if os.getcwd then
		return os.getcwd()
	end
	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


-----------------------------------------------------------------------
-- 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 os.native and os.native.GetFullPathName then
		local test = os.native.GetFullPathName(path)
		if test then return test end
	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)
		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
			test = os.call('realpath -s \'' .. path .. '\' 2> /dev/null')
			if test ~= nil and test ~= '' then
				return test
			end
			test = os.call('realpath \'' .. path .. '\' 2> /dev/null')
			if test ~= nil and test ~= '' then
				return test
			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\''
			local s = string.format(s, path)
			test = os.call(s)
			if test ~= nil and test ~= '' then
				return test
			end
		end
		for _, python in pairs({'python3', 'python2', 'python'}) 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
				test = os.call(s)
				if test ~= nil and test ~= '' then
					return test
				end
			end
		end
	end
	return os.path.absolute(path)
end


-----------------------------------------------------------------------
-- dir exists
-----------------------------------------------------------------------
function os.path.isdir(pathname)
	if pathname == '/' then
		return true
	elseif pathname == '' then
		return false
	elseif windows then
		if pathname == '\\' then
			return true
		end
	end
	if os.native and os.native.isdir then
		return os.native.isdir(pathname)
	end
	if clink and os.isdir then
		return os.isdir(pathname)
	end
	local name = pathname
	if (not name:endswith('/')) and (not name:endswith('\\')) then
		name = name .. os.path.sep
	end
	return os.path.exists(name)
end


-----------------------------------------------------------------------
-- file or path exists
-----------------------------------------------------------------------
function os.path.exists(name)
	if name == '/' then
		return true
	end
	if os.native and os.native.exists then
		return os.native.exists(name)
	end
	local ok, err, code = os.rename(name, name)
	if not ok then
		if code == 13 or code == 17 then
			return true
		elseif code == 30 then
			local f = io.open(name,"r")
			if f ~= nil then
				io.close(f)
				return true
			end
		elseif name:sub(-1) == '/' and code == 20 and (not windows) then
			local test = name .. '.'
			ok, err, code = os.rename(test, test)
			if code == 16 or code == 13 or code == 22 then
				return true
			end
		end
		return false
	end
	return true
end


-----------------------------------------------------------------------
-- is absolute path
-----------------------------------------------------------------------
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


-----------------------------------------------------------------------
-- normalize path
-----------------------------------------------------------------------
function os.path.norm(pathname)
	if windows then
		pathname = pathname:gsub('\\', '/')
	end
	if windows then
		pathname = pathname:gsub('/', '\\')
	end
	return 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 pos ~= 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 and drive ~= '' 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
-----------------------------------------------------------------------
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


-----------------------------------------------------------------------
-- check single name element
-----------------------------------------------------------------------
function os.path.single(path)
	if string.match(path, '/') then
		return false
	end
	if windows then
		if string.match(path, '\\') then
			return false
		end
	end
	return true
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


-----------------------------------------------------------------------
-- search executable
-----------------------------------------------------------------------
function os.path.search(name)
end


-----------------------------------------------------------------------
-- get lua executable
-----------------------------------------------------------------------
function os.interpreter()
	if os.argv == nil then
		io.stderr:write("cannot get arguments (arg), recompiled your lua\n")
		return nil
	end
	local lua = os.argv[-1]
	if lua == nil then
		io.stderr:write("cannot get executable name, recompiled your lua\n")
	end
	if os.path.single(lua) then
		local path = os.path.which(lua)
		if not os.path.isabs(path) then
			return os.path.abspath(path)
		end
		return path
	end
	return os.path.abspath(lua)
end


-----------------------------------------------------------------------
-- get script name
-----------------------------------------------------------------------
function os.scriptname()
	if os.argv == nil then
		io.stderr:write("cannot get arguments (arg), recompiled your lua\n")
		return nil
	end
	local script = os.argv[0]
	if script == nil then
		io.stderr:write("cannot get script name, recompiled your lua\n")
	end
	return os.path.abspath(script)
end


-----------------------------------------------------------------------
-- get environ
-----------------------------------------------------------------------
function os.environ(name, default)
	local value = os.getenv(name)
	if os.envmap ~= nil and type(os.envmap) == 'table' then
		local t = os.envmap[name]
		value = (t ~= nil and type(t) == 'string') and t or value
	end
	if value == nil then
		return default
	elseif type(default) == 'boolean' then
		value = value:lower()
		if value == '0' or value == '' or value == 'no' then
			return false
		elseif value == 'false' or value == 'n' or value == 'f' then
			return false
		else
			return true
		end
	elseif type(default) == 'number' then
		value = tonumber(value)
		if value == nil then
			return default
		else
			return value
		end
	elseif type(default) == 'string' then
		return value
	elseif type(default) == 'table' then
		return value:sep(',')
	end
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 arg ~= '' then
			if head ~= '-' then
				break
			end
			if arg == '-' then
				options['-'] = ''
			elseif arg == '--' then
				options['-'] = '-'
			elseif arg:match('^-%d+$') then
				options['-'] = arg:sub(2)
			else
				local part = arg:split('=')
				options[part[1]] = part[2] ~= nil and part[2] or ''
			end
		end
		index = index + 1
	end
	while index <= count do
		table.insert(args, argv[index])
		index = index + 1
	end
	return options, args
end


-----------------------------------------------------------------------
-- generate random seed
-----------------------------------------------------------------------
function math.random_init()
	-- random seed from os.time()
	local seed = tostring(os.time() * 1000)
	seed = seed .. tostring(math.random(99999999))
	if os.argv ~= nil then
		for _, key in ipairs(os.argv) do
			seed = seed .. '/' .. key
		end
	end
	local ppid = os.getenv('PPID')
	seed = (ppid ~= nil) and (seed .. '/' .. ppid) or seed
	-- random seed from socket.gettime()
	local status, socket = pcall(require, 'socket')
	if status then
		seed = seed .. tostring(socket.gettime())
	end
	-- random seed from _ZL_RANDOM
	local rnd = os.getenv('_ZL_RANDOM')
	if rnd ~= nil then
		seed = seed .. rnd
	end
	seed = seed .. tostring(os.clock() * 10000000)
	if os.native and os.native.tickcount then
		seed = seed .. tostring(os.native.tickcount())
	end
	local number = 0
	for i = 1, seed:len() do
		local k = string.byte(seed:sub(i, i))
		number = ((number * 127) % 0x7fffffff) + k
	end
	math.randomseed(number)
end


-----------------------------------------------------------------------
-- math random string
-----------------------------------------------------------------------
function math.random_string(N)
	local text = ''
	for i = 1, N do
		local k = math.random(0, 26 * 2 + 10 - 1)
		if k < 26 then
			text = text .. string.char(0x41 + k)
		elseif k < 26 * 2 then
			text = text .. string.char(0x61 + k - 26)
		elseif k < 26 * 2 + 10 then
			text = text .. string.char(0x30 + k - 26 * 2)
		else
		end
	end
	return text
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()
	local 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]
			part[2] = part[2]:gsub(",", ".")
			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)
	math.random_init()
	while true do
		tmpname = filename .. '.' .. tostring(os.time())
		if os.native and os.native.tickcount then
			local key = os.native.tickcount() % 1000
			tmpname = tmpname .. string.format('%03d', key)
			tmpname = tmpname .. math.random_string(5)
		else
			tmpname = tmpname .. math.random_string(8)
		end
		if not os.path.exists(tmpname) then
			-- print('tmpname: '..tmpname)
			break
		end
	end
	if windows then
		if os.native and os.native.ReplaceFile then
			fp = io.open(tmpname, 'w')
		else
			fp = io.open(filename, 'w')
			tmpname = nil
		end
	else
		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
		if windows then
			local ok, err, code = os.rename(tmpname, filename)
			if not ok then
				os.native.ReplaceFile(filename, tmpname)
			end
		else
			os.rename(tmpname, filename)
		end
		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
		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
			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 database
-----------------------------------------------------------------------
function data_file_set(name)
	DATA_FILE = name
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, matchlast)
	local pos = 1
	local i = 0
	local matchlast = matchlast ~= nil and matchlast or false
	for i = 1, #patterns do
		local pat = patterns[i]
		local start, endup = pathname:find(pat, pos)
		if start == nil or endup == nil then
			return false
		end
		pos = endup + 1
	end
	if matchlast and #patterns > 0 then
		local last = ''
		local index = #patterns
		local pat = patterns[index]
		if not windows then
			last = string.match(pathname, ".*(/.*)")
		else
			last = string.match(pathname, ".*([/\\].*)")
		end
		if last then
			local start, endup = last:find(pat, 1)
			if start == nil or endup == nil then
				return false
			end
		end
	end
	return true
end


-----------------------------------------------------------------------
-- select matched pathnames
-----------------------------------------------------------------------
function data_select(M, patterns, matchlast)
	local N = {}
	local i = 1
	local pats = {}
	for i = 1, #patterns do
		local p = patterns[i]
		if Z_HYPHEN then
			p = p:gsub('-', '%%-')
		end
		table.insert(pats, case_insensitive_pattern(p))
	end
	for i = 1, #M do
		local item = M[i]
		if path_match(item.name, pats, matchlast) 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)
	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
				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
				if windows then
					if os.native and os.native.GetRealPathName then
						local ts = os.native.GetRealPathName(path)
						if ts then
							path = ts
						end
					end
				end
				M = data_insert(M, path)
				count = count + 1
			end
		end
	end
	if count > 0 then
		data_save(DATA_FILE, M)
	end
	return true
end


-----------------------------------------------------------------------
-- remove path
-----------------------------------------------------------------------
function z_remove(path)
	local paths = {}
	local count = 0
	local remove = {}
	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)
	local X = {}
	M = data_filter(M)
	local insensitive = path_case_insensitive()
	for _, path in pairs(paths) do
		path = os.path.abspath(path)
		if not insensitive then
			remove[path] = 1
		else
			remove[path:lower()] = 1
		end
	end
	for i = 1, #M do
		local item = M[i]
		if not insensitive then
			if not remove[item.name] then
				table.insert(X, item)
			end
		else
			if not remove[item.name:lower()] then
				table.insert(X, item)
			end
		end
	end
	data_save(DATA_FILE, X)
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, 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
	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)
	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


-----------------------------------------------------------------------
-- pretty print
-----------------------------------------------------------------------
function z_print(M, weight, number)
	local N = {}
	local maxsize = 9
	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
	local fp = io.stdout
	if PRINT_MODE == '<stdout>' then
		fp = io.stdout
	elseif PRINT_MODE == '<stderr>' then
		fp = io.stderr
	else
		fp = io.open(PRINT_MODE, 'w')
	end
	for i = #N, 1, -1 do
		local record = N[i]
		local line = record.score
		while true do
			local tail = line:sub(-1, -1)
			if tail ~= '0' and tail ~= '.' then
				break
			end
			line = line:sub(1, -2)
			if tail == '.' then
				break
			end
		end
		local dx = maxsize - line:len()
		if dx > 0 then
			line = line .. string.rep(' ', dx)
		end
		if weight then
			line = line .. '  ' .. record.name
		else
			line = record.name
		end
		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 fp ~= nil then
			fp:write(line .. '\n')
		end
	end
	if PRINT_MODE:sub(1, 1) ~= '<' then
		if fp ~= nil then fp:close() 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 == '~' or last == '~/' then
		return os.path.expand('~')
	elseif windows and last == '~\\' then
		return os.path.expand('~')
	end
	if os.path.isabs(last) and os.path.isdir(last) then
		local size = #patterns
		if size <= 1 then
			return os.path.norm(last)
		elseif last ~= '/' and last ~= '\\' then
			return os.path.norm(last)
		end
	end
	local M = z_match(patterns, Z_METHOD, Z_SUBDIR)
	if M == nil then
		return nil
	end
	if #M == 0 then
		return nil
	elseif #M == 1 then
		return M[1].name
	elseif Z_INTERACTIVE == 0 then
		return M[1].name
	end
	if os.environ('_ZL_INT_SORT', false) then
		table.sort(M, function (a, b) return a.name < b.name end)
	end
	local retval = nil
	if Z_INTERACTIVE == 1 then
		PRINT_MODE = '<stderr>'
		z_print(M, true, true)
		io.stderr:write('> ')
		io.stderr:flush()
		local input = io.read('*l')
		if input == nil or input == '' then
			return nil
		end
		local index = tonumber(input)
		if index == nil then
			return nil
		end
		if index < 1 or index > #M then
			return nil
		end
		retval = M[index].name
	elseif Z_INTERACTIVE == 2 then
		local fzf = os.environ('_ZL_FZF', 'fzf')
		local tmpname = '/tmp/zlua.txt'
		local cmd = '--nth 2.. --reverse --info=inline --tac '
		local flag = os.environ('_ZL_FZF_FLAG', '')
		flag = (flag == '' or flag == nil) and '+s -e' or flag
		cmd = ((fzf == '') and 'fzf' or fzf)  .. ' ' .. cmd .. ' ' .. flag
		if not windows then
			tmpname = os.tmpname()
			local height = os.environ('_ZL_FZF_HEIGHT', '35%')
			if height ~= nil and height ~= '' and height ~= '0' then
				cmd = cmd .. ' --height ' .. height
			end
			cmd = cmd .. ' < "' .. tmpname .. '"'
		else
			tmpname = os.tmpname():gsub('\\', ''):gsub('%.', '')
			tmpname = os.environ('TMP', '') .. '\\zlua_' .. tmpname .. '.txt'
			cmd = 'type "' .. tmpname .. '" | ' .. cmd
		end
		PRINT_MODE = tmpname
		z_print(M, true, false)
		retval = os.call(cmd)
		-- io.stderr:write('<'..cmd..'>\n')
		os.remove(tmpname)
		if retval == '' or retval == nil then
			return nil
		end
		local pos = retval:find(' ')
		if not pos then
			return nil
		end
		retval = retval:sub(pos, -1):gsub('^%s*', '')
	end
	return (retval ~= '' and retval or nil)
end


-----------------------------------------------------------------------
-- purge invalid paths
-----------------------------------------------------------------------
function z_purge()
	local M = data_load(DATA_FILE)
	local N = data_filter(M)
	local x = #M
	local y = #N
	if x == y then
		return x, y
	end
	data_save(DATA_FILE, N)
	return x, y
end


-----------------------------------------------------------------------
-- find_vcs_root
-----------------------------------------------------------------------
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 and args[1]:sub(1, 2) == '..' then
		local size = args[1]:len() - 1
		if args[1]:match('^%.%.+$') then
			size = args[1]:len() - 1
		elseif args[1]:match('^%.%.%d+$') then
			size = tonumber(args[1]:sub(3))
		else
			return nil
		end
		local path = pwd
		for index = 1, size do
			path = os.path.join(path, '..')
		end
		return os.path.normpath(path)
	elseif nargs == 1 then
		pwd = os.path.split(pwd)
		local test = windows and pwd:gsub('\\', '/') or pwd
		local key = windows and args[1]:lower() or args[1]
		if not key:match('%u') then
			test = test:lower()
		end
		local pos, ends = test:rfind('/' .. key)
		if pos then
			ends = test:find('/', pos + key:len() + 1, true)
			ends = ends and ends or test:len()
			return os.path.normpath(pwd:sub(1, ends))
		elseif windows and test:startswith(key) then
			ends = test:find('/', key:len(), true)
			ends = ends and ends or test:len()
			return os.path.normpath(pwd:sub(1, ends))
		end
		pos = test:rfind(key)
		if pos then
			ends = test:find('/', pos + key:len(), true)
			ends = ends and ends or test:len()
			return os.path.normpath(pwd:sub(1, ends))
		end
		return nil
	elseif nargs == 2 then
		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)
		local newpath = lhs .. dst .. rhs
		if os.path.isdir(newpath) then
			return newpath
		end

		-- Get rid of the entire path component that matched `src`.
		lhs = lhs:gsub("[^/]*$", "")
		rhs = rhs:gsub("^[^/]*", "")
		return z_cd({lhs, dst, rhs})
		-- In the future, it would make sense to have `z -b -c from to to2`
		-- to z_cd({lhs, dst[1], dst[2]}). Without `-c`, we probably still
		-- want to support only 2 argumets.
	end

	io.stderr:write("Error: " .. Z_CMD .. " -b takes at most 2 arguments.\n")
	return nil
end


-----------------------------------------------------------------------
-- cd minus: "z -", "z --", "z -2"
-----------------------------------------------------------------------
function cd_minus(args, options)
	Z_SKIPPWD = true
	local M = z_match({}, 'time', Z_SUBDIR)
	local size = #M
	if options['-'] == '-' then
		for i, item in ipairs(M) do
			if i > 10 then break end
			io.stderr:write(' ' .. tostring(i - 1) .. '  ' .. item.name .. '\n')
		end
	else
		local level = 0
		local num = options['-']
		if num and num ~= '' then
			level = tonumber(num)
		end
		if level >= 0 and level < size then
			return M[level + 1].name
		end
	end
	return nil
end


-----------------------------------------------------------------------
-- cd breadcrumbs: z -b -i, z -b -I
-----------------------------------------------------------------------
function cd_breadcrumbs(pwd, interactive)
	local pwd = (pwd == nil or pwd == '') and os.pwd() or pwd
	local pwd = os.path.normpath(pwd)
	local path, _ = os.path.split(pwd)
	local elements = {}
	local interactive = interactive and interactive or 1
	local fullname = os.environ('_ZL_FULL_PATH', false)
	while true do
		local head, name = os.path.split(path)
		if head == path  then		-- reached root
			table.insert(elements, {head, head})
			break
		elseif name ~= '' then
			table.insert(elements, {name, path})
		else
			break
		end
		path = head
	end
	local tmpname = '/tmp/zlua.txt'
	local fp = io.stderr
	if interactive == 2 then
		if not windows then
			tmpname = os.tmpname()
		else
			tmpname = os.tmpname():gsub('\\', ''):gsub('%.', '')
			tmpname = os.environ('TMP', '') .. '\\zlua_' .. tmpname .. '.txt'
		end
		fp = io.open(tmpname, 'w')
	end
	-- print table
	local maxsize = string.len(tostring(#elements))
	for i = #elements, 1, -1 do
		local item = elements[i]
		local name = item[1]
		local text = string.rep(' ', maxsize - string.len(i)) .. tostring(i)
		text = text .. ': ' .. (fullname and item[2] or item[1])
		fp:write(text .. '\n')
	end
	if fp ~= io.stderr then
		fp:close()
	end
	local retval = ''
	-- select from stdin or fzf
	if interactive == 1 then
		io.stderr:write('> ')
		io.stderr:flush()
		retval = io.read('*l')
	elseif interactive == 2 then
		local fzf = os.environ('_ZL_FZF', 'fzf')
		local cmd = '--reverse --info=inline --tac '
		local flag = os.environ('_ZL_FZF_FLAG', '')
		flag = (flag == '' or flag == nil) and '+s -e' or flag
		cmd = ((fzf == '') and 'fzf' or fzf) .. ' ' .. cmd .. ' ' .. flag
		if not windows then
			local height = os.environ('_ZL_FZF_HEIGHT', '35%')
			if height ~= nil and height ~= '' and height ~= '0' then
				cmd = cmd .. ' --height ' .. height
			end
			cmd = cmd .. '< "' .. tmpname .. '"'
		else
			cmd = 'type "' .. tmpname .. '" | ' .. cmd
		end
		retval = os.call(cmd)
		os.remove(tmpname)
		if retval == '' or retval == nil then
			return nil
		end
		local pos = retval:find(':')
		if not pos then
			return nil
		end
		retval = retval:sub(1, pos - 1):gsub('^%s*', '')
	end
	local index = tonumber(retval)
	if index == nil or index < 1 or index > #elements then
		return nil
	end
	return elements[index][2]
end


-----------------------------------------------------------------------
-- main entry
-----------------------------------------------------------------------
function main(argv)
	local options, args = os.getopt(argv)
	os.log("main()")
	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 true then
		os.log("options: " .. dump(options))
		os.log("args: " .. dump(args))
	end
	if options['-c'] then
		Z_SUBDIR = true
	end
	if options['-r'] then
		Z_METHOD = 'rank'
	elseif options['-t'] then
		Z_METHOD = 'time'
	end
	if options['-i'] then
		Z_INTERACTIVE = 1
	elseif options['-I'] then
		Z_INTERACTIVE = 2
	end
	if options['--cd'] or options['-e'] then
		local path = ''
		if options['-b'] then
			if #args > 0 or Z_INTERACTIVE == 0 then
				path = cd_backward(args, options)
			else
				path = cd_breadcrumbs('', Z_INTERACTIVE)
			end
		elseif options['-'] then
			path = cd_minus(args, options)
		elseif #args == 0 then
			path = nil
		else
			path = z_cd(args)
			if path == nil and Z_MATCHMODE ~= 0 then
				local last = args[#args]
				if os.path.isdir(last) then
					path = os.path.abspath(last)
					path = os.path.norm(path)
				end
			end
		end
		if path ~= nil then
			io.write(path .. (options['-e'] and "\n" or ""))
		end
	elseif options['--add'] then
		-- print('data: ' .. DATA_FILE)
		z_add(args)
	elseif options['-x'] then
		z_remove(args)
	elseif options['--purge'] then
		local src, dst = z_purge()
		local fp = io.stderr
		fp:write('purge: ' .. tostring(src) .. ' record(s) remaining, ')
		fp:write(tostring(src - dst) .. ' invalid record(s) removed.\n')
	elseif options['--init'] then
		local opts = {}
		for _, key in ipairs(args) do
			opts[key] = 1
		end
		if windows then
			z_windows_init(opts)
		elseif opts.fish then
			z_fish_init(opts)
		elseif opts.powershell then
		       z_windows_init(opts)
		else
			z_shell_init(opts)
		end
	elseif options['-l'] then
		local M = z_match(args and args or {}, Z_METHOD, Z_SUBDIR)
		if options['-s'] then
			z_print(M, false, false)
		else
			z_print(M, true, false)
		end
	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)
		for _, item in pairs(M) do
			print(item.name)
		end
	elseif options['--help'] or options['-h'] then
		z_help()
	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_DIRS')
	local _zl_cmd = os.getenv('_ZL_CMD')
	local _zl_matchname = os.getenv('_ZL_MATCH_NAME')
	local _zl_skippwd = os.getenv('_ZL_SKIP_PWD')
	local _zl_matchmode = os.getenv('_ZL_MATCH_MODE')
	local _zl_hyphen = os.getenv('_ZL_HYPHEN')
	if _zl_data ~= nil and _zl_data ~= "" then
		if windows then
			DATA_FILE = _zl_data
		else
			-- avoid windows environments affect cygwin & msys
			if not string.match(_zl_data, '^%a:[/\\]') then
				DATA_FILE = _zl_data
			end
		end
	end
	if _zl_maxage ~= nil and _zl_maxage ~= "" then
		_zl_maxage = tonumber(_zl_maxage)
		if _zl_maxage ~= nil and _zl_maxage > 0 then
			MAX_AGE = _zl_maxage
		end
	end
	if _zl_exclude ~= nil and _zl_exclude ~= "" then
		local part = _zl_exclude:split(',')
		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
			table.insert(Z_EXCLUDE, name)
		end
	end
	if _zl_cmd ~= nil and _zl_cmd ~= '' then
		Z_CMD = _zl_cmd
	end
	if _zl_matchname ~= nil then
		local m = string.lower(_zl_matchname)
		if (m == '1' or m == 'yes' or m == 'true' or m == 't') then
			Z_MATCHNAME = true
		end
	end
	if _zl_skippwd ~= nil then
		local m = string.lower(_zl_skippwd)
		if (m == '1' or m == 'yes' or m == 'true' or m == 't') then
			Z_SKIPPWD = true
		end
	end
	if _zl_matchmode ~= nil then
		local m = tonumber(_zl_matchmode)
		Z_MATCHMODE = m
		if (m == 1) then
			Z_MATCHNAME = true
			Z_SKIPPWD = true
		end
	end
	if _zl_hyphen ~= nil then
		local m = string.lower(_zl_hyphen)
		if (m == '1' or m == 'yes' or m == 'true' or m == 't') then
			Z_HYPHEN = true
		end
	end
end


-----------------------------------------------------------------------
-- initialize clink hooks
-----------------------------------------------------------------------
function z_clink_init()
	local once = os.environ("_ZL_ADD_ONCE", false)
	local _zl_clink_prompt_priority = os.environ('_ZL_CLINK_PROMPT_PRIORITY', 99)
	local previous = ''
	function z_add_to_database()
		pwd = clink.get_cwd()
		if once then
			if previous == pwd then
				return
			end
			previous = pwd
		end
		z_add(clink.get_cwd())
	end
	clink.prompt.register_filter(z_add_to_database, _zl_clink_prompt_priority)
	function z_match_completion(word)
		local M = z_match({word}, Z_METHOD, Z_SUBDIR)
		for _, item in pairs(M) do
			clink.add_match(item.name)
		end
		return {}
	end
	local z_parser = clink.arg.new_parser()
	z_parser:set_arguments({ z_match_completion })
	z_parser:set_flags("-c", "-r", "-i", "--cd", "-e", "-b", "--add", "-x", "--purge",
		"--init", "-l", "-s", "--complete", "--help", "-h")
	clink.arg.register_parser("z", z_parser)
end


-----------------------------------------------------------------------
-- shell scripts
-----------------------------------------------------------------------
local script_zlua = [[
_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'
]]

local script_init_bash = [[
case "$PROMPT_COMMAND" in
	*_zlua?--add*) ;;
	*) PROMPT_COMMAND="(_zlua --add \"\$(command pwd 2>/dev/null)\" &)${PROMPT_COMMAND:+;$PROMPT_COMMAND}" ;;
esac
]]

local script_init_bash_fast = [[
case "$PROMPT_COMMAND" in
	*_zlua?--add*) ;;
	*) PROMPT_COMMAND="(_zlua --add \"\$PWD\" &)${PROMPT_COMMAND:+;$PROMPT_COMMAND}" ;;
esac
]]

local script_init_bash_once = [[
_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
]]

local script_init_posix = [[
case "$PS1" in
	*_zlua?--add*) ;;
	*) PS1="\$(_zlua --add \"\$(command pwd 2>/dev/null)\" &)$PS1"
esac
]]

local script_init_posix_once = [[
_zlua_precmd() {
    [ "$_ZL_PREVIOUS_PWD" = "$PWD" ] && return
    _ZL_PREVIOUS_PWD="$PWD"
    (_zlua --add "$PWD" 2> /dev/null &)
}
case "$PS1" in
	*_zlua_precmd*) ;;
	*) PS1="\$(_zlua_precmd)$PS1"
esac
]]

local script_init_zsh = [[
_zlua_precmd() {
	(_zlua --add "${PWD:a}" &)
}
typeset -ga precmd_functions
[ -n "${precmd_functions[(r)_zlua_precmd]}" ] || {
	precmd_functions[$(($#precmd_functions+1))]=_zlua_precmd
}
]]

local script_init_zsh_once = [[
_zlua_precmd() {
	(_zlua --add "${PWD:a}" &)
}
typeset -ga chpwd_functions
[ -n "${chpwd_functions[(r)_zlua_precmd]}" ] || {
	chpwd_functions[$(($#chpwd_functions+1))]=_zlua_precmd
}
]]

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

local script_fzf_complete_bash = [[
if [ "$TERM" != "dumb" ] && command -v fzf >/dev/null 2>&1; then
	# To redraw line after fzf closes (printf '\e[5n')
	bind '"\e[0n": redraw-current-line'
	_zlua_fzf_complete() {
		local selected=$(_zlua -l "${COMP_WORDS[@]:1}" | sed "s|$HOME|\~|" | $zlua_fzf | sed 's/^[0-9,.]* *//')
		if [ -n "$selected" ]; then
			COMPREPLY=( "$selected" )
		fi
		printf '\e[5n'
	}
	complete -o bashdefault -o nospace -F _zlua_fzf_complete ${_ZL_CMD:-z}
fi
]]

local script_complete_zsh = [[
_zlua_zsh_tab_completion() {
	# tab completion
	(( $+compstate )) && compstate[insert]=menu # no expand
	local -a tmp=(${(f)"$(_zlua --complete "${words/_zlua/z}")"})
	_describe "directory" tmp -U
}
if [ "${+functions[compdef]}" -ne 0 ]; then
	compdef _zlua_zsh_tab_completion _zlua 2> /dev/null
	compdef ${_ZL_CMD:-z}=_zlua
fi
]]


-----------------------------------------------------------------------
-- initialize bash/zsh
----------------------------------------------------------------------
function z_shell_init(opts)
	print('ZLUA_SCRIPT="' .. os.scriptname() .. '"')
	print('ZLUA_LUAEXE="' .. os.interpreter() .. '"')
	print('')
	if not opts.posix then
		print(script_zlua)
	elseif not opts.legacy then
		local script = script_zlua:gsub('builtin ', '')
		print(script)
	else
		local script = script_zlua:gsub('local ', ''):gsub('builtin ', '')
		print(script)
	end

	local prompt_hook = (not os.environ("_ZL_NO_PROMPT_COMMAND", false))
	local once = os.environ("_ZL_ADD_ONCE", false) or opts.once ~= nil

	if opts.clean ~= nil then
		prompt_hook = false
	end

	if opts.bash ~= nil then
		if prompt_hook then
			if once then
				print(script_init_bash_once)
			elseif opts.fast then
				print(script_init_bash_fast)
			else
				print(script_init_bash)
			end
		end
		print(script_complete_bash)
		if opts.fzf ~= nil then
			fzf_cmd = "fzf --nth 2.. --reverse --info=inline --tac "
			local height = os.environ('_ZL_FZF_HEIGHT', '35%')
			if height ~= nil and height ~= '' and height ~= '0' then
				fzf_cmd = fzf_cmd .. ' --height ' .. height .. ' '
			end
			local flag = os.environ('_ZL_FZF_FLAG', '')
			flag = (flag == '' or flag == nil) and '+s -e' or flag
			fzf_cmd = fzf_cmd .. ' ' .. flag .. ' '
			print('zlua_fzf="' .. fzf_cmd .. '"')
			print(script_fzf_complete_bash)
		end
	elseif opts.zsh ~= nil then
		if prompt_hook then
			print(once and script_init_zsh_once or script_init_zsh)
		end
		print(script_complete_zsh)
	elseif opts.posix ~= nil then
		if prompt_hook then
			local script = script_init_posix
			if once then script = script_init_posix_once end
			if opts.legacy then
				script = script:gsub('%&%)', ')')
			end
			print(script)
		end
	else
		if prompt_hook then
			print('if [ -n "$BASH_VERSION" ]; then')
			if opts.once then
				print(script_init_bash_once)
			elseif opts.fast then
				print(script_init_bash_fast)
			else
				print(script_init_bash)
			end
			print(script_complete_bash)
			print('elif [ -n "$ZSH_VERSION" ]; then')
			print(once and script_init_zsh_once or script_init_zsh)
			-- print(script_complete_zsh)
			print('else')
			print(once and script_init_posix_once or script_init_posix)
			print('builtin() { cd "$2"; }')
			print('fi')
		end
	end
	if opts.enhanced ~= nil then
		print('export _ZL_MATCH_MODE=1')
	end
	if opts.nc then
		print('export _ZL_NO_CHECK=1')
	end
	if opts.echo then
		print('_ZL_ECHO=1')
	end
end


-----------------------------------------------------------------------
-- Fish shell init
-----------------------------------------------------------------------
local script_zlua_fish = [[
function _zlua
	set -l arg_mode ""
	set -l arg_type ""
	set -l arg_subdir ""
	set -l arg_inter ""
	set -l arg_strip ""
	function _zlua_call; eval (string escape -- $argv); end
	if test "$argv[1]" = "--add"
		set -e argv[1]
		set -x _ZL_RANDOM (random)
		_zlua_call "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" --add $argv
		return
	else if test "$argv[1]" = "--complete"
		set -e argv[1]
		_zlua_call "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" --complete $argv
		return
	end
	while true
		switch "$argv[1]"
			case "-l"; set arg_mode "-l"
			case "-e"; set arg_mode "-e"
			case "-x"; set arg_mode "-x"
			case "-t"; set arg_type "-t"
			case "-r"; set arg_type "-r"
			case "-c"; set arg_subdir "-c"
			case "-s"; set arg_strip "-s"
			case "-i"; set arg_inter "-i"
			case "-I"; set arg_inter "-I"
			case "-h"; set arg_mode "-h"
			case "--help"; set arg_mode "-h"
			case "--purge"; set arg_mode "--purge"
			case '*'; break
		end
		set -e argv[1]
	end
	if test "$arg_mode" = "-h"
		_zlua_call "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" -h
	else if test "$arg_mode" = "--purge"
		_zlua_call "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" --purge
	else if test "$arg_mode" = "-l"
		_zlua_call "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" -l $arg_subdir $arg_type $arg_strip $argv
	else if test (count $argv) -eq 0
		_zlua_call "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" -l $arg_subdir $arg_type $arg_strip $argv
	else if test -n "$arg_mode"
		_zlua_call "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" $arg_mode $arg_subdir $arg_type $arg_inter $argv
	else
		set -l dest (_zlua_call "$ZLUA_LUAEXE" "$ZLUA_SCRIPT" --cd $arg_type $arg_subdir $arg_inter $argv)
		if test -n "$dest" -a -d "$dest"
			if test -z "$_ZL_CD"
				builtin cd "$dest"
			else
				_zlua_call "$_ZL_CD" "$dest"
			end
			if test -n "$_ZL_ECHO"; pwd; end
		end
	end
end

if test -z "$_ZL_CMD"; set -x _ZL_CMD z; end
alias "$_ZL_CMD"=_zlua
]]

script_init_fish = [[
function _zlua_precmd --on-event fish_prompt
	_zlua --add "$PWD" 2> /dev/null &
end
]]

script_init_fish_once = [[
function _zlua_precmd --on-variable PWD
	_zlua --add "$PWD" 2> /dev/null &
end
]]

script_complete_fish = [[
function _z_complete
	eval "$_ZL_CMD" --complete (commandline -t)
end

complete -c $_ZL_CMD -f -a '(_z_complete)'
complete -c $_ZL_CMD -s 'r' -d 'cd to highest ranked dir matching'
complete -c $_ZL_CMD -s 'i' -d 'cd with interactive selection'
complete -c $_ZL_CMD -s 'I' -d 'cd with interactive selection using fzf'
complete -c $_ZL_CMD -s 't' -d 'cd to most recently accessed dir matching'
complete -c $_ZL_CMD -s 'l' -d 'list matches instead of cd'
complete -c $_ZL_CMD -s 'c' -d 'restrict matches to subdirs of $PWD'
complete -c $_ZL_CMD -s 'e' -d 'echo the best match, don''t cd'
complete -c $_ZL_CMD -s 'b' -d 'jump backwards to given dir or to project root'
complete -c $_ZL_CMD -s 'x' -x -d 'remove path from history' -a '(_z_complete)'
]]


function z_fish_init(opts)
	print('set -x ZLUA_SCRIPT "' .. os.scriptname() .. '"')
	print('set -x ZLUA_LUAEXE "' .. os.interpreter() .. '"')
	local once = (os.getenv("_ZL_ADD_ONCE") ~= nil) or opts.once ~= nil
	local prompt_hook = (not os.environ("_ZL_NO_PROMPT_COMMAND", false))
	if opts.clean ~= nil then
		prompt_hook = false
	end
	print(script_zlua_fish)
	if prompt_hook then
		if once then
			print(script_init_fish_once)
		else
			print(script_init_fish)
		end
	end
	print(script_complete_fish)
	if opts.enhanced ~= nil then
		print('set -x _ZL_MATCH_MODE 1')
	end
	if opts.echo then
		print('set -g _ZL_ECHO 1')
	end
	if opts.nc then
		print('set -x _ZL_NO_CHECK 1')
	end
end


-----------------------------------------------------------------------
-- windows .cmd script
-----------------------------------------------------------------------
local script_init_cmd = [[
set "MatchType=-n"
set "StrictSub=-n"
set "RunMode=-n"
set "StripMode="
set "InterMode="
if /i not "%_ZL_LUA_EXE%"=="" (
	set "LuaExe=%_ZL_LUA_EXE%"
)
:parse
if /i "%1"=="-r" (
	set "MatchType=-r"
	shift /1
	goto parse
)
if /i "%1"=="-t" (
	set "MatchType=-t"
	shift /1
	goto parse
)
if /i "%1"=="-c" (
	set "StrictSub=-c"
	shift /1
	goto parse
)
if /i "%1"=="-l" (
	set "RunMode=-l"
	shift /1
	goto parse
)
if /i "%1"=="-e" (
	set "RunMode=-e"
	shift /1
	goto parse
)
if /i "%1"=="-x" (
	set "RunMode=-x"
	shift /1
	goto parse
)
if /i "%1"=="--add" (
	set "RunMode=--add"
	shift /1
	goto parse
)
if "%1"=="-i" (
	set "InterMode=-i"
	shift /1
	goto parse
)
if "%1"=="-I" (
	set "InterMode=-I"
	shift /1
	goto parse
)
if /i "%1"=="-s" (
	set "StripMode=-s"
	shift /1
	goto parse
)
if /i "%1"=="-h" (
	call "%LuaExe%" "%LuaScript%" -h
	goto end
)
if /i "%1"=="--purge" (
	call "%LuaExe%" "%LuaScript%" --purge
	goto end
)
:check
if /i "%1"=="" (
	set "RunMode=-l"
)
for /f "delims=" %%i in ('cd') do set "PWD=%%i"
if /i "%RunMode%"=="-n" (
	for /f "delims=" %%i in ('call "%LuaExe%" "%LuaScript%" --cd %MatchType% %StrictSub% %InterMode% %*') do set "NewPath=%%i"
	if not "!NewPath!"=="" (
		if exist !NewPath!\nul (
			if /i not "%_ZL_ECHO%"=="" (
				echo !NewPath!
			)
			pushd !NewPath!
			pushd !NewPath!
			endlocal
			goto popdir
		)
	)
)	else (
	call "%LuaExe%" "%LuaScript%" "%RunMode%" %MatchType% %StrictSub% %InterMode% %StripMode% %*
)
goto end
:popdir
popd
setlocal
set "NewPath=%CD%"
set "CDCmd=cd /d"
if /i not "%_ZL_CD%"=="" (
	set "CDCmd=%_ZL_CD%"
)
endlocal & popd & %CDCmd% "%NewPath%"
:end
]]


-----------------------------------------------------------------------
-- powershell
-----------------------------------------------------------------------
local script_zlua_powershell = [[
function global:_zlua {
	$arg_mode = ""
	$arg_type = ""
	$arg_subdir = ""
	$arg_inter = ""
	$arg_strip = ""
	if ($args[0] -eq "--add") {
		$_, $rest = $args
		$env:_ZL_RANDOM = Get-Random
		& $script:ZLUA_LUAEXE $script:ZLUA_SCRIPT --add $rest
		return
	} elseif ($args[0] -eq "--complete") {
		$_, $rest = $args
		& $script:ZLUA_LUAEXE $script:ZLUA_SCRIPT --complete $rest
		return
	} elseif ($args[0] -eq "--update") {
		$str_pwd = ([string] $PWD)
		if ((!$env:_ZL_ADD_ONCE) -or
			($env:_ZL_ADD_ONCE -and ($script:_zlua_previous -ne $str_pwd))) {
			$script:_zlua_previous = $str_pwd
			_zlua --add $str_pwd
		}
		return
	}
	:loop while ($args) {
		switch -casesensitive ($args[0]) {
			"-l" { $arg_mode = "-l"; break }
			"-e" { $arg_mode = "-e"; break }
			"-x" { $arg_mode = "-x"; break }
			"-t" { $arg_type = "-t"; break }
			"-r" { $arg_type = "-r"; break }
			"-c" { $arg_subdir="-c"; break }
			"-s" { $arg_strip="-s"; break }
			"-i" { $arg_inter="-i"; break }
			"-I" { $arg_inter="-I"; break }
			"-h" { $arg_mode="-h"; break }
			"--help" { $arg_mode="-h"; break }
			"--purge" { $arg_mode="--purge"; break }
			Default { break loop }
		}
		$_, $args = $args
		if (!$args) { break loop }
	}
	$env:PWD = ([string] $PWD)
	if ($arg_mode -eq "-h" -or $arg_mode -eq "--purge") {
		& $script:ZLUA_LUAEXE $script:ZLUA_SCRIPT $arg_mode
	} elseif ($arg_mode -eq "-l" -or $args.Length -eq 0) {
		& $script:ZLUA_LUAEXE $script:ZLUA_SCRIPT -l $arg_subdir $arg_type $arg_strip $args
	} elseif ($arg_mode -ne "") {
		& $script:ZLUA_LUAEXE $script:ZLUA_SCRIPT $arg_mode $arg_subdir $arg_type $arg_inter $args
	} else {
		$dest = & $script:ZLUA_LUAEXE $script:ZLUA_SCRIPT --cd $arg_type $arg_subdir $arg_inter $args
		if ($dest) {
			if ($env:_ZL_CD) { & $env:_ZL_CD "$dest" }
			else { & "Push-Location" "$dest" }
			if ($env:_ZL_ECHO) { Write-Host $PWD }
		}
	}
}

if ($env:_ZL_CMD) { Set-Alias $env:_ZL_CMD _zlua -Scope Global }
else { Set-Alias z _zlua -Scope Global }
]]

local script_init_powershell = [[
if (!$env:_ZL_NO_PROMPT_COMMAND -and (!$global:_zlua_inited)) {
	$script:_zlua_orig_prompt = ([ref] $function:prompt)
	$global:_zlua_inited = $True
	function global:prompt {
		& $script:_zlua_orig_prompt.value
		_zlua --update
	}
}
]]


-----------------------------------------------------------------------
-- initialize cmd/powershell
-----------------------------------------------------------------------
function z_windows_init(opts)
	local prompt_hook = (not os.environ("_ZL_NO_PROMPT_COMMAND", false))
	if opts.clean ~= nil then
		prompt_hook = false
	end
	if opts.powershell ~= nil then
		print('$script:ZLUA_LUAEXE = "' .. os.interpreter() .. '"')
		print('$script:ZLUA_SCRIPT = "' .. os.scriptname() .. '"')
		print(script_zlua_powershell)
		if opts.enhanced ~= nil then
			print('$env:_ZL_MATCH_MODE = 1')
		end
		if opts.once ~= nil then
			print('$env:_ZL_ADD_ONCE = 1')
		end
		if opts.echo ~= nil then
			print('$env:_ZL_ECHO = 1')
		end
		if opts.nc ~= nil then
			print('$env:_ZL_NO_CHECK = 1')
		end
		if prompt_hook then
			print(script_init_powershell)
		end
	else
		print('@echo off')
		print('setlocal EnableDelayedExpansion')
		print('set "LuaExe=' .. os.interpreter() .. '"')
		print('set "LuaScript=' .. os.scriptname() .. '"')
		print(script_init_cmd)
		if opts.newline then
			print('echo.')
		end
	end
end


-----------------------------------------------------------------------
-- help
-----------------------------------------------------------------------
function z_help()
	local cmd = Z_CMD .. ' '
	print(cmd .. 'foo        # cd to most frecent dir matching foo')
	print(cmd .. 'foo bar    # cd to most frecent dir matching foo and bar')
	print(cmd .. '-r foo     # cd to highest ranked dir matching foo')
	print(cmd .. '-t foo     # cd to most recently accessed dir matching foo')
	print(cmd .. '-l foo     # list matches instead of cd')
	print(cmd .. '-c foo     # restrict matches to subdirs of $PWD')
	print(cmd .. '-e foo     # echo the best match, don\'t cd')
	print(cmd .. '-x path    # remove path from history')
	print(cmd .. '-i foo     # cd with interactive selection')
	print(cmd .. '-I foo     # cd with interactive selection using fzf')
	print(cmd .. '-b foo     # cd to the parent directory starting with foo')
	print(cmd .. '-b foo bar # replace foo with bar in cwd and cd there')
end


-----------------------------------------------------------------------
-- LFS optimize
-----------------------------------------------------------------------
os.lfs = {}
os.lfs.enable = os.getenv('_ZL_USE_LFS')
os.lfs.enable = '1'
if os.lfs.enable ~= nil then
	local m = string.lower(os.lfs.enable)
	if (m == '1' or m == 'yes' or m == 'true' or m == 't') then
		os.lfs.status, os.lfs.pkg = pcall(require, 'lfs')
		if os.lfs.status then
			local lfs = os.lfs.pkg
			os.path.exists = function (name)
				return lfs.attributes(name) and true or false
			end
			os.path.isdir = function (name)
				local mode = lfs.attributes(name)
				if not mode then
					return false
				end
				return (mode.mode == 'directory') and true or false
			end
		end
	end
end


-----------------------------------------------------------------------
-- program entry
-----------------------------------------------------------------------
if not pcall(debug.getlocal, 4, 1) then
	-- main script
	z_init()
	if windows and type(clink) == 'table' and clink.prompt ~= nil then
		z_clink_init()
	else
		main()
	end
end

-- vim: set ts=4 sw=4 tw=0 noet :

