diff --git a/!init.lua b/!init.lua new file mode 100644 index 0000000..2b265a0 --- /dev/null +++ b/!init.lua @@ -0,0 +1,22 @@ +-- Note: This happens in both .init.lua and !init.lua because older Cmder +-- versions don't know about !init.lua. + +-- Get the parent path of this script. +local parent_path = debug.getinfo(1, "S").source:match[[^@?(.*[\/])[^\/]-$]] + +-- Extend package.path with modules directory, if not already present, to allow +-- using require() with them. +local modules_path = parent_path.."modules/?.lua" +if not package.path:find(modules_path, 1, true--[[plain]]) then + package.path = modules_path..";"..package.path +end + +-- Explicitly set the completions dir, in case something (such as Cmder) +-- manually loads completion scripts with them being in a Clink script path. +if os.setenv then + local completions_path = parent_path.."completions" + local env = os.getenv("CLINK_COMPLETIONS_DIR") or "" + if not env:find(completions_path, 1, true--[[plain]]) then + os.setenv("CLINK_COMPLETIONS_DIR", env .. (#env > 0 and ";" or "") .. completions_path) + end +end diff --git a/.init.lua b/.init.lua index 406a789..2b265a0 100644 --- a/.init.lua +++ b/.init.lua @@ -1,3 +1,22 @@ --- The line below extends package.path with modules --- directory to allow to require them -package.path = debug.getinfo(1, "S").source:match[[^@?(.*[\/])[^\/]-$]] .."modules/?.lua;".. package.path \ No newline at end of file +-- Note: This happens in both .init.lua and !init.lua because older Cmder +-- versions don't know about !init.lua. + +-- Get the parent path of this script. +local parent_path = debug.getinfo(1, "S").source:match[[^@?(.*[\/])[^\/]-$]] + +-- Extend package.path with modules directory, if not already present, to allow +-- using require() with them. +local modules_path = parent_path.."modules/?.lua" +if not package.path:find(modules_path, 1, true--[[plain]]) then + package.path = modules_path..";"..package.path +end + +-- Explicitly set the completions dir, in case something (such as Cmder) +-- manually loads completion scripts with them being in a Clink script path. +if os.setenv then + local completions_path = parent_path.."completions" + local env = os.getenv("CLINK_COMPLETIONS_DIR") or "" + if not env:find(completions_path, 1, true--[[plain]]) then + os.setenv("CLINK_COMPLETIONS_DIR", env .. (#env > 0 and ";" or "") .. completions_path) + end +end diff --git a/.luacheckrc b/.luacheckrc index 7d7f685..bc43fbc 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -3,5 +3,22 @@ return { files = { spec = { std = "+busted" }, }, - globals = { "clink", "rl_state", "rl", "settings", "log", "path" } + globals = { + "clink", + "error", + "log", + "os.getcwd", + "os.globfiles", + "os.isdir", + "os.setenv", + "path", + "pause", + "rl", + "rl_state", + "settings", + "string.explode", + "string.matchlen", + "unicode.fromcodepage", + "unicode.iter", + } } diff --git a/completions/adb.lua b/completions/adb.lua new file mode 100644 index 0000000..ebc9d3e --- /dev/null +++ b/completions/adb.lua @@ -0,0 +1,405 @@ +--- adb.lua, Android ADB completion for Clink. +-- @compatible Android SDK Platform-tools v31.0.3 (ADB v1.0.41) +-- @author Goldie Lin +-- @date 2021-08-27 +-- @see [Clink](https://github.com/chrisant996/clink) +-- @usage +-- Place it in "%LocalAppData%\clink\" if installed globally, +-- or "ConEmu/ConEmu/clink/" if you used portable ConEmu & Clink. +-- + +-- luacheck: no unused args +-- luacheck: ignore clink rl_state + +local function dump(o) -- luacheck: ignore + if type(o) == 'table' then + local s = '{ ' + local prefix = "" + for k, v in pairs(o) do + if type(k) ~= 'number' then k = '"'..k..'"' end + s = s..prefix..'['..k..']="'..dump(v)..'"' + prefix = ', ' + end + return s..' }' + else + return tostring(o) + end +end + +local function generate_matches(command, pattern) + local f = io.popen('2>nul '..command) + if f then + local matches = {} + for line in f:lines() do + if line ~= 'List of devices attached' then + table.insert(matches, line:match(pattern)) + end + end + f:close() + return matches + end +end + +local function serialno_parser(word) + return clink.argmatcher():addarg({generate_matches('adb devices', '^(%w+)%s+.*$')}) +end + +local function transportid_parser(word) + return clink.argmatcher():addarg({generate_matches('adb devices -l', '^.*%s+transport_id:(%d+)%s*.*$')}) +end + +local null_parser = clink.argmatcher():nofiles() + +local devices_parser = clink.argmatcher() +:nofiles() +:addflags( + "-l" +) + +local reconnect_parser = clink.argmatcher() +:nofiles() +:addarg({ + "device", + "offline" +}) + +local networking_options_parser = clink.argmatcher() +:addflags( + "--list", + "--no-rebind", + "--remove", + "--remove-all" +) + +local mdns_parser = clink.argmatcher() +:nofiles() +:addarg({ + "check", + "services" +}) + +local push_parser = clink.argmatcher() +:addflags( + "--sync", + "-n", + "-z", + "-Z" +) + +local pull_parser = clink.argmatcher() +:addflags( + "-a", + "-z", + "-Z" +) + +local sync_parser = clink.argmatcher() +:addflags( + "-n", + "-l", + "-z", + "-Z" +) +:addarg({ + "all", + "data", + "odm", + "oem", + "product_services", + "product", + "system", + "system_ext", + "vendor" +}) + +local shell_bu_backup_parser = clink.argmatcher() +:addflags( + "-f", + "-all", + "-apk", + "-noapk", + "-obb", + "-noobb", + "-shared", + "-noshared", + "-system", + "-nosystem", + "-keyvalue", + "-nokeyvalue" +) +local backup_parser = shell_bu_backup_parser + +local shell_bu_parser = clink.argmatcher() +:addarg({ + "backup" .. shell_bu_backup_parser, + "restore" +}) + +local shell_parser = clink.argmatcher() +:addflags( + "-e", + "-n", + "-T", + "-t", + "-x" +) +:addarg({ + "bu" .. shell_bu_parser +}) + +local install_parser = clink.argmatcher() +:addflags( + "-l", + "-r", + "-t", + "-s", + "-d", + "-g", + "--abi", + "--instant", + "--no-streaming", + "--streaming", + "--fastdeploy", + "--no-fastdeploy", + "--force-agent", + "--date-check-agent", + "--version-check-agent", + "--local-agent" +) + +local install_multiple_parser = clink.argmatcher() +:addflags( + "-l", + "-r", + "-t", + "-s", + "-d", + "-p", + "-g", + "--abi", + "--instant", + "--no-streaming", + "--streaming", + "--fastdeploy", + "--no-fastdeploy", + "--force-agent", + "--date-check-agent", + "--version-check-agent", + "--local-agent" +) + +local install_multi_package_parser = clink.argmatcher() +:addflags( + "-l", + "-r", + "-t", + "-s", + "-d", + "-p", + "-g", + "--abi", + "--instant", + "--no-streaming", + "--streaming", + "--fastdeploy", + "--no-fastdeploy", + "--force-agent", + "--date-check-agent", + "--version-check-agent", + "--local-agent" +) + +local uninstall_parser = clink.argmatcher() +:addflags( + "-k" +) + +local logcat_format_parser = clink.argmatcher() +:nofiles() +:addarg({ + "brief", + "help", + "long", + "process", + "raw", + "tag", + "thread", + "threadtime", + "time", + "color", + "descriptive", + "epoch", + "monotonic", + "printable", + "uid", + "usec", + "UTC", + "year", + "zone" +}) + +local logcat_buffer_parser = clink.argmatcher() +:nofiles() +:addarg({ + "default", -- default = main,system,crash + "all", + "main", + "radio", + "events", + "system", + "crash", + "security", + "kernel" +}) + +local logcat_parser = clink.argmatcher() +:nofiles() +:addflags( + "-s", + "-f", + "--file", + "-r", + "--rotate-kbytes", + "-n", + "--rotate-count", + "--id", + "-v" .. logcat_format_parser, + "--format" .. logcat_format_parser, + "-D", + "--dividers", + "-c", + "--clear", + "-d", + "-e", + "--regex", + "-m", + "--max-count", + "--print", + "-t", + "-T", + "-g", + "--buffer-size", + "-G", + "--buffer-size=", + "-L", + "--last", + "-b" .. logcat_buffer_parser, + "--buffer" .. logcat_buffer_parser, + "-B", + "--binary", + "-S", + "--statistics", + "-p", + "--prune", + "-P", + "--prune=", + "--pid", + "--wrap" +) +:addarg({ + "*:V", + "*:D", + "*:I", + "*:W", + "*:E", + "*:F", + "*:S", +}) + +local remount_parser = clink.argmatcher() +:nofiles() +:addflags( + "-R" +) + +local reboot_parser = clink.argmatcher() +:nofiles() +:addarg({ + "bootloader", + "recovery", + "sideload", + "sideload-auto-reboot", + "edl" +}) + +clink.argmatcher("adb") +:addflags( + "-a", + "-d", + "-e", + "-s" .. serialno_parser, + "-p", + "-t" .. transportid_parser, + "-H", + "-P", + "-L" +) +:addarg({ + "help" .. null_parser, + "version" .. null_parser, + "devices" .. devices_parser, + "connect" .. null_parser, + "disconnect" .. null_parser, + "pair" .. null_parser, + "reconnect" .. reconnect_parser, + "ppp", + "forward" .. networking_options_parser, + "reverse" .. networking_options_parser, + "mdns" .. mdns_parser, + "push" .. push_parser, + "pull" .. pull_parser, + "sync" .. sync_parser, + "shell" .. shell_parser, + "emu", + "install" .. install_parser, + "install-multiple" .. install_multiple_parser, + "install-multi-package" .. install_multi_package_parser, + "uninstall" .. uninstall_parser, + "backup" .. backup_parser, + "restore", + "bugreport", + "jdwp" .. null_parser, + "logcat" .. logcat_parser, + "disable-verity" .. null_parser, + "enable-verity" .. null_parser, + "keygen", + "wait-for-device" .. null_parser, + "wait-for-recovery" .. null_parser, + "wait-for-rescue" .. null_parser, + "wait-for-sideload" .. null_parser, + "wait-for-bootloader" .. null_parser, + "wait-for-disconnect" .. null_parser, + "wait-for-any-device" .. null_parser, + "wait-for-any-recovery" .. null_parser, + "wait-for-any-rescue" .. null_parser, + "wait-for-any-sideload" .. null_parser, + "wait-for-any-bootloader" .. null_parser, + "wait-for-any-disconnect" .. null_parser, + "wait-for-usb-device" .. null_parser, + "wait-for-usb-recovery" .. null_parser, + "wait-for-usb-rescue" .. null_parser, + "wait-for-usb-sideload" .. null_parser, + "wait-for-usb-bootloader" .. null_parser, + "wait-for-usb-disconnect" .. null_parser, + "wait-for-local-device" .. null_parser, + "wait-for-local-recovery" .. null_parser, + "wait-for-local-rescue" .. null_parser, + "wait-for-local-sideload" .. null_parser, + "wait-for-local-bootloader" .. null_parser, + "wait-for-local-disconnect" .. null_parser, + "get-state" .. null_parser, + "get-serialno" .. null_parser, + "get-devpath" .. null_parser, + "remount" .. remount_parser, + "reboot-bootloader" .. null_parser, + "reboot" .. reboot_parser, + "sideload", + "root" .. null_parser, + "unroot" .. null_parser, + "usb" .. null_parser, + "tcpip", + "start-server" .. null_parser, + "kill-server" .. null_parser, + "attach" .. null_parser, + "detach" .. null_parser +}) diff --git a/completions/attrib.lua b/completions/attrib.lua new file mode 100644 index 0000000..b0bfe87 --- /dev/null +++ b/completions/attrib.lua @@ -0,0 +1,105 @@ +-------------------------------------------------------------------------------- +-- Usage: +-- +-- Argmatcher for ATTRIB. Uses delayinit to support localized help text. + +-------------------------------------------------------------------------------- +local clink_version = require('clink_version') +if not clink_version.supports_argmatcher_delayinit then + print("attrib.lua argmatcher requires a newer version of Clink; please upgrade.") + return +end + +-------------------------------------------------------------------------------- +local function add_pending(pending, flags, descriptions, hideflags) + if pending then + table.insert(flags, pending.flag) + table.insert(hideflags, pending.flag:lower()) + local desc = pending.desc:gsub('[ .]+$', '') + descriptions[pending.flag] = { desc } + end +end + +-------------------------------------------------------------------------------- +local function make_desc(lhs, rhs) + if rhs:match('^[A-Z][a-z ]') then + rhs = rhs:sub(1, 1):lower() .. rhs:sub(2) + end + return lhs .. rhs:gsub('[ .]+$', '') +end + +-------------------------------------------------------------------------------- +local inited + +-------------------------------------------------------------------------------- +local function delayinit(argmatcher) + if inited then + return + end + inited = true + + local f = io.popen('attrib /?') + if not f then + return + end + + local flags = {} + local descriptions = {} + local hideflags = {} + local pending + + local section = 'header' + for line in f:lines() do + if unicode.fromcodepage then + line = unicode.fromcodepage(line) + end + if section == 'attrs' then + local attr, desc = line:match('^ +([A-Z]) +([^ ].+)$') + if attr then + table.insert(flags, '+'..attr) + table.insert(flags, '-'..attr) + table.insert(hideflags, '+'..attr:lower()) + table.insert(hideflags, '-'..attr:lower()) + descriptions['+'..attr] = { make_desc('Set ', desc) } + descriptions['-'..attr] = { make_desc('Clear ', desc) } + elseif line:match('^ +%[') then + section = 'flags' + end + elseif section == 'flags' then + local indent, flag, pad, desc = line:match('^( +)(/[^ ]+)( +)([^ ].*)$') + if flag then + add_pending(pending, flags, descriptions, hideflags) + pending = {} + pending.indent = #indent + #flag + #pad + pending.flag = flag + pending.desc = desc:gsub(' +$', '') + elseif pending then + indent, desc = line:match('^( +)([^ ].*)$') + if indent and #indent == (pending.indent or 0) then + pending.desc = pending.desc .. ' ' .. desc:gsub(' +$', '') + else + add_pending(pending, flags, descriptions, hideflags) + pending = nil + end + else + add_pending(pending, flags, descriptions, hideflags) + pending = nil + end + elseif section == 'header' then + if line:match('^ +%+ +') then + section = 'attrs' + end + end + end + add_pending(pending, flags, descriptions, hideflags) + + f:close() + + argmatcher:addflags(flags) + argmatcher:addflags(hideflags) + argmatcher:adddescriptions(descriptions) + argmatcher:hideflags(hideflags) +end + +-------------------------------------------------------------------------------- +clink.argmatcher('attrib'):setdelayinit(delayinit) diff --git a/completions/curl.lua b/completions/curl.lua new file mode 100644 index 0000000..bf89fe5 --- /dev/null +++ b/completions/curl.lua @@ -0,0 +1,7 @@ +local clink_version = require('clink_version') +if not clink_version.supports_argmatcher_delayinit then + print("curl.lua argmatcher requires a newer version of Clink; please upgrade.") + return +end + +require('help_parser').make('curl', '--help all', 'curl') diff --git a/completions/dir.lua b/completions/dir.lua new file mode 100644 index 0000000..9100841 --- /dev/null +++ b/completions/dir.lua @@ -0,0 +1,154 @@ +-------------------------------------------------------------------------------- +-- Usage: +-- +-- Argmatcher for DIR. Uses delayinit to support localized help text. + +-------------------------------------------------------------------------------- +local clink_version = require('clink_version') +if not clink_version.supports_argmatcher_delayinit then + print("dir.lua argmatcher requires a newer version of Clink; please upgrade.") + return +end + +local mcf = require('multicharflags') + +-------------------------------------------------------------------------------- +local function add_pending(pending, flags, descriptions, hideflags) + if pending then + local main = pending.flag:lower() + local alt = pending.flag + table.insert(flags, { flag=main }) + if main ~= alt then + table.insert(flags, { flag=alt }) + table.insert(hideflags, alt) + end + local desc = pending.desc:gsub('%.+$', '') + descriptions[main] = { desc } + if pending.charflags then + local arg = mcf.addcharflagsarg(clink.argmatcher(), pending.charflags) + main = main .. ':' + alt = alt .. ':' + table.insert(flags, { flag=main, arg=arg }) + if main ~= alt then + table.insert(flags, { flag=alt, arg=arg }) + table.insert(hideflags, alt) + end + descriptions[main] = { pending.display, desc } + end + end +end + +-------------------------------------------------------------------------------- +local inited + +-------------------------------------------------------------------------------- +local function add_charflags(pending, indent, line) + if indent < pending.indent then + return + end + + local lhs, rhs = line:match('^([^ ] [^ ].+ +)([^ ] [^ ].+)$') + if not lhs then + lhs = line:match('^([^ ] [^ ].+)$') + end + if not lhs then + return + end + + for _, x in ipairs({lhs, rhs}) do + local c, desc = x:match('^([^ ]) ([^ ].+)$') + if not c then + break + end + local i = (_ > 1 or c == '-') and (#pending.charflags + 1) or (#pending.charflags / 2 + 1) + table.insert(pending.charflags, i, { c:lower(), desc }) + end + + return true +end + +-------------------------------------------------------------------------------- +local function delayinit(argmatcher) + if inited then + return + end + inited = true + + local file = io.popen('dir /?') + if not file then + return + end + + local flags = {} + local descriptions = {} + local hideflags = {} + local pending + + local section = 'header' + for line in file:lines() do + if unicode.fromcodepage then + line = unicode.fromcodepage(line) + end + if section == 'header' and line:match('^ +/') then + section = 'flags' + end + if section == 'flags' then + local add + local indent, flag, pad, desc = line:match('^( +)(/[^ ]+)( +)([^ ].*)$') + + if flag then + add_pending(pending, flags, descriptions, hideflags) + pending = {} + pending.indent = #indent + #flag + #pad + pending.flag = flag + pending.desc = desc:gsub(' +$', '') + elseif pending then + indent, desc = line:match('^( +)([^ ].*)$') + if indent and #indent == (pending.indent or 0) then + pending.desc = pending.desc .. ' ' .. desc:gsub(' +$', '') + elseif indent and #indent >= 2 and #indent < 8 then + local display + display, pad, desc = desc:match('^([^ ]+)( +)([^ ].+)$') + indent = #indent + #display + #pad + if display and indent >= pending.indent then + pending.display = display + pending.charflags = pending.charflags or { caseless=true } + add = not add_charflags(pending, indent, desc) + else + add = true + end + elseif indent and pending.charflags then + add = not add_charflags(pending, #indent, desc) + else + add = true + end + else + add = true + end + + if add then + add_pending(pending, flags, descriptions, hideflags) + pending = nil + end + end + end + add_pending(pending, flags, descriptions, hideflags) + + file:close() + + local actual_flags = {} + for _, f in ipairs(flags) do + if f.arg then + table.insert(actual_flags, f.flag .. f.arg) + else + table.insert(actual_flags, f.flag) + end + end + + argmatcher:addflags(actual_flags) + argmatcher:adddescriptions(descriptions) + argmatcher:hideflags(hideflags) +end + +-------------------------------------------------------------------------------- +clink.argmatcher('dir'):setdelayinit(delayinit) diff --git a/completions/doskey.lua b/completions/doskey.lua new file mode 100644 index 0000000..82b2cbd --- /dev/null +++ b/completions/doskey.lua @@ -0,0 +1,22 @@ +require('arghelper') + +local function exe_matches_all(word, word_index, line_state, match_builder) -- luacheck: no unused args + match_builder:addmatch({ match="all", display="\x1b[1mALL" }) + match_builder:addmatch({ match="cmd.exe", display="\x1b[1mCMD.EXE" }) + match_builder:addmatches(clink.filematches("")) +end + +local function exe_matches(word, word_index, line_state, match_builder) -- luacheck: no unused args + match_builder:addmatch({ match="cmd.exe", display="\x1b[1mCMD.EXE" }) + match_builder:addmatches(clink.filematches("")) +end + +-- luacheck: no max line length +clink.argmatcher("doskey") +:_addexflags({ + {"/reinstall", "Installs a new copy of Doskey"}, + {"/macros", "Display all Doskey macros for the current executable"}, + {"/macros:"..clink.argmatcher():addarg(exe_matches_all), "Display all Doskey macros for the named executable ('ALL' for all executables)"}, + {"/exename="..clink.argmatcher():addarg(exe_matches), "Specifies the executable"}, + {"/macrofile=", "Specifies a file of macros to install"}, +}) diff --git a/completions/fastboot.lua b/completions/fastboot.lua new file mode 100644 index 0000000..5920eb9 --- /dev/null +++ b/completions/fastboot.lua @@ -0,0 +1,267 @@ +--- fastboot.lua, Android Fastboot completion for Clink. +-- @compatible Android SDK Platform-tools v31.0.3 +-- @author Goldie Lin +-- @date 2021-08-27 +-- @see [Clink](https://github.com/chrisant996/clink) +-- @usage +-- Place it in "%LocalAppData%\clink\" if installed globally, +-- or "ConEmu/ConEmu/clink/" if you used portable ConEmu & Clink. +-- + +-- luacheck: no unused args +-- luacheck: ignore clink rl_state + +local function dump(o) -- luacheck: ignore + if type(o) == 'table' then + local s = '{ ' + local prefix = "" + for k, v in pairs(o) do + if type(k) ~= 'number' then k = '"'..k..'"' end + s = s..prefix..'['..k..']="'..dump(v)..'"' + prefix = ', ' + end + return s..' }' + else + return tostring(o) + end +end + +local function generate_matches(command, pattern) + local f = io.popen('2>nul '..command) + if f then + local matches = {} + for line in f:lines() do + if line ~= 'List of devices attached' then + table.insert(matches, line:match(pattern)) + end + end + f:close() + return matches + end +end + +local function serialno_parser(word) + return clink.argmatcher():addarg({generate_matches('fastboot devices', '^(%w+)%s+.*$')}) +end + +local null_parser = clink.argmatcher():nofiles() + +local flashing_parser = clink.argmatcher() +:nofiles() +:addarg({ + "lock", + "unlock", + "lock_critical", + "unlock_critical", + "lock_bootloader", + "unlock_bootloader", + "get_unlock_ability", + "get_unlock_bootloader_nonce" +}) + +local partitions = { + "devinfo", + "splash", + "keystore", + "ssd", + "frp", + "misc", + "aboot", + "abl", + "abl_a", + "abl_b", + "boot", + "boot_a", + "boot_b", + "recovery", + "cache", + "persist", + "userdata", + "system", + "system_a", + "system_b", + "vendor", + "vendor_a", + "vendor_b" +} + +local partitions_parser = clink.argmatcher() +:addarg(partitions) + +local partitions_nofile_parser = clink.argmatcher() +:nofiles() +:addarg(partitions) + +local variables_parser = clink.argmatcher() +:nofiles() +:addarg({ + "all", + "serialno", + "product", + "secure", + "unlocked", + "variant", + "kernel", + "version-baseband", + "version-bootloader", + "charger-screen-enabled", + "off-mode-charge", + "battery-soc-ok", + "battery-voltage", + "slot-count", + "current-slot", + "has-slot:boot", + "has-slot:modem", + "has-slot:system", + "slot-retry-count:a", + "slot-retry-count:b", + "slot-successful:a", + "slot-successful:b", + "slot-unbootable:a", + "slot-unbootable:b" +}) + +local slots = { + "a", + "b" +} + +local slot_types = { + "all", + "other" +} + +local slots_full = {} +for _, i in ipairs(slots) do + table.insert(slots_full, i) +end +for _, i in ipairs(slot_types) do + table.insert(slots_full, i) +end + +local slots_parser = clink.argmatcher() +:nofiles() +:addarg(slots) + +local slot_types_parser = clink.argmatcher() +:nofiles() +:addarg(slots_full) + +local fs_options = { + "casefold", + "compress", + "projid" +} + +local fs_options_parser = clink.argmatcher() +:addarg(fs_options) + +local flash_raw_parser = clink.argmatcher() +:addarg({ + "boot" +}) + +local devices_parser = clink.argmatcher() +:nofiles() +:addflags( + "-l" +) + +local reboot_parser = clink.argmatcher() +:nofiles() +:addarg({ + "bootloader", + "emergency" +}) + +local oem_parser = clink.argmatcher() +:nofiles() +:addarg({ + "lock", + "unlock", + "device-info", + "select-display-panel", + "enable-charger-screen", + "disable-charger-screen" +}) + +local gsi_parser = clink.argmatcher() +:nofiles() +:addarg({ + "wipe", + "disable" +}) + +local snapshotupdate_parser = clink.argmatcher() +:nofiles() +:addarg({ + "cancel", + "merge" +}) + +clink.argmatcher("fastboot") +:addflags( + "-w", + "-u", + "-s" .. serialno_parser, + "--dtb", + "-c", + "--cmdline", + "-i", + "-h", + "--help", + "-b", + "--base", + "--kernel-offset", + "--ramdisk-offset", + "--tags-offset", + "--dtb-offset", + "-n", + "--page-size", + "--header-version", + "--os-version", + "--os-patch-level", + "-S", + "--slot" .. slot_types_parser, + "-a" .. slots_parser, + "--set-active=" .. slots_parser, + "--skip-secondary", + "--skip-reboot", + "--disable-verity", + "--disable-verification", + "--fs-options=" .. fs_options_parser, + "--wipe-and-use-fbe", + "--unbuffered", + "--force", + "-v", + "--verbose", + "--version" +) +:addarg({ + "help" .. null_parser, + "update", + "flashall" .. null_parser, + "flashing" .. flashing_parser, + "flash" .. partitions_parser, + "erase" .. partitions_nofile_parser, + "format" .. partitions_nofile_parser, + "getvar" .. variables_parser, + "set_active" .. slots_parser, + "boot", + "flash:raw" .. flash_raw_parser, + "devices" .. devices_parser, + "continue" .. null_parser, + "reboot" .. reboot_parser, + "reboot-bootloader" .. null_parser, + "oem" .. oem_parser, + "gsi" .. gsi_parser, + "wipe-super" .. null_parser, + "create-logical-partition", + "delete-logical-partition", + "resize-logical-partition", + "snapshot-update" .. snapshotupdate_parser, + "fetch" .. partitions_nofile_parser, + "stage", + "get_staged", +}) + diff --git a/completions/findstr.lua b/completions/findstr.lua new file mode 100644 index 0000000..3148dba --- /dev/null +++ b/completions/findstr.lua @@ -0,0 +1,30 @@ +require('arghelper') + +local dir_matcher = clink.argmatcher():addarg(clink.dirmatches) +local file_matcher = clink.argmatcher():addarg({ + { match="/", display="/ (console)" }, + clink.filematches +}) + +-- luacheck: no max line length +clink.argmatcher("findstr") +:_addexflags({ + {"/b", "Matches pattern if at the beginning of a line"}, + {"/e", "Matches pattern if at the end of a line"}, + {"/l", "Uses search strings literally"}, + {"/r", "Uses search strings as regular expressions (default)"}, + {"/s", "Search in subdirectories also"}, + {"/i", "Case insensitive search"}, + {"/x", "Prints lines that match exactly"}, + {"/v", "Prints only lines that do not contain a match"}, + {"/n", "Prints the line number before each line that matches"}, + {"/m", "Prints only the filename if a file contains a match"}, + {"/o", "Prints character offset before each matching line"}, + {"/p", "Skips files with non-printable characters"}, + {"/offline", "Do not skip files with offline attribute set"}, + {"/a:"..clink.argmatcher():addarg({fromhistory=true, "attr"}), "hexattr", "Specifies color attribute with two hex digits"}, + {"/f:"..file_matcher, "file", "Reads file list from the specified file (/ stands for console)"}, + {"/c:"..clink.argmatcher():addarg("search_string"), "string", "Uses specified string as literal search string"}, + {"/g:"..file_matcher, "file", "Gets search strings from the specified file (/ stands for console)"}, + {"/d:"..dir_matcher, "dir[;dir...]", "Search a semicolon delimited list of directories"}, +}) diff --git a/completions/grep.lua b/completions/grep.lua new file mode 100644 index 0000000..8ed4fd9 --- /dev/null +++ b/completions/grep.lua @@ -0,0 +1,7 @@ +local clink_version = require('clink_version') +if not clink_version.supports_argmatcher_delayinit then + print("grep.lua argmatcher requires a newer version of Clink; please upgrade.") + return +end + +require('help_parser').make('grep', '--help', 'gnu') diff --git a/completions/less.lua b/completions/less.lua new file mode 100644 index 0000000..f46771f --- /dev/null +++ b/completions/less.lua @@ -0,0 +1,99 @@ +-------------------------------------------------------------------------------- +-- Clink argmatcher for LESS +-- +-- Info: http://www.greenwoodsoftware.com/less +-- Repo: https://github.com/gwsw/less.git + +local clink_version = require('clink_version') +if not clink_version.supports_argmatcher_delayinit then + print("less.lua argmatcher requires a newer version of Clink; please upgrade.") + return +end + +local inited + +local function init(argmatcher) + if inited then + return + end + inited = true + + local file = io.popen('less --help') + if not file then + return + end + + local section = 'header' + local flags = {} + local descriptions = {} + local pending + + for line in file:lines() do + line = line:gsub('.\x08', '') + if section == 'header' then + if line:match('^ +OPTIONS') then + section = 'options' + end + elseif section == 'options' then + if line:find('^ *%-%-%-%-%-%-%-%-%-%-') then + section = 'done' + elseif pending then + local desc = line:match('^[ \t]+([^ \t].*)$') + if desc then + local args + desc = desc:gsub('%.+$', '') + for _,f in ipairs(pending) do + local display = f:match('%=(.+)$') or f:match('( .+)$') + if display then + if not args then + if display:find('file') then + args = clink.argmatcher():addarg(clink.filematches) + else + args = clink.argmatcher():addarg({fromhistory=true}) + end + end + f = f:sub(1, #f - #display) + if args then + table.insert(flags, f .. args) + else + table.insert(flags, f) + end + descriptions[f] = { display, desc } + else + table.insert(flags, f) + descriptions[f] = { desc } + end + end + end + pending = nil + elseif line:match('^ +%-') then + line = line:gsub('^ +', '') + while true do + local f = line:match('^(%-.-) ') or line:match('^(%-%-[^ ]+)$') + if not f then + break + end + line = line:sub(#f + 1):gsub('^[ .]+', '') + + pending = pending or {} + f = f:gsub(' +$', '') + table.insert(pending, f) + end + end + else -- luacheck: ignore 542 + -- Nothing to do. + end + end + + file:close() + + argmatcher:addflags(flags) + argmatcher:adddescriptions(descriptions) +end + +local a = clink.argmatcher('less') +if a.setdelayinit then + a:setdelayinit(init) +else + init(a) +end diff --git a/completions/premake5.lua b/completions/premake5.lua new file mode 100644 index 0000000..cc8e807 --- /dev/null +++ b/completions/premake5.lua @@ -0,0 +1,131 @@ +-------------------------------------------------------------------------------- +-- Usage: +-- +-- Clink argmatcher for Premake5. Generates completions for Premake5 by getting +-- a list of available commands and flags from Premake5. +-- +-- Uses argmatcher:setdelayinit() to dynamically (re-)initialize the argmatcher +-- based on the current directory. +-- +-- https://premake.github.io/ + +if not clink then + -- Probably getting loaded and run inside of Premake5 itself; bail out. + return +end + +local clink_version = require('clink_version') +if not clink_version.supports_argmatcher_delayinit then + print("premake5.lua argmatcher requires a newer version of Clink; please upgrade.") + return +end + +local prev_cwd = "" + +local function delayinit(argmatcher) + -- If the current directory is the same, the argmatcher is already + -- initialized. + local cwd = os.getcwd() + if prev_cwd == cwd then + return + end + + -- Reset the argmatcher and update the current directory. + argmatcher:reset() + prev_cwd = cwd + + -- Invoke 'premake5 --help' and parse its output to collect the available + -- flags and arguments. + local actions + local flags = {} + local descriptions = {} + local pending_link + local values + local placeholder + local r = io.popen('premake5.exe --help 2>nul') + + -- The output from premake5 follows this layout: + -- + -- ... ignore lines until ... + -- OPTIONS ... + -- --flag Description + -- --etc ... + -- ACTIONS + -- action Description + -- etc ... + -- ... stop once a non-indented line is reached. + -- + -- Additionally, some flags follow this layout: + -- + -- --flag=value Description of flag + -- value Description of value + -- etc ... + -- ... until another --flag line, or ACTIONS. + -- + for line in r:lines() do + if actions then + -- A non-blank, non-indented line ends the actions. + if #line > 0 and line:sub(1,1) ~= ' ' then + break + end + -- Parsing an action. + local action, description = line:match('^ ([^ ]+) +(.+)$') + if action then + table.insert(actions, { match=action, type="arg", description=description }) + end + elseif line:find('^ACTIONS') then + -- An 'ACTIONS' line starts the actions section. + actions = {} + elseif values and line:match('^ ') then + -- Add a value to the values table for the pending_link. + local value, description = line:match('^ ([^ ]+) +(.+)$') + if value then + table.insert(values, { match=value, type="arg", description=description }) + end + else + -- Not a value line, so if there's a pending_link then it's + -- finished; add the pending values to it. + if pending_link then + pending_link:addarg(#values > 0 and values or placeholder) + pending_link = nil + values = nil + placeholder = nil + end + -- Parse a flag line. + local flag, value, description + flag, description = line:match('^[ ]+(%-%-[^ =]+) +(.+)$') + if not flag then + flag, value, description = line:match('^[ ]+(%-%-[^ =]+=)([^ ]+) +(.+)$') + end + -- If the line defines a flag, process the flag. + if flag then + if description then + description = description:gsub('; one of:$', '') + end + -- Add the flag. + if value then + pending_link = clink.argmatcher() + table.insert(flags, flag..pending_link) + descriptions[flag] = { value, description } + -- Prepare placeholder value. + values = {} + placeholder = { match=value, type="arg", description=description } + else + descriptions[flag] = description + table.insert(flags, flag) + end + end + end + end + + r:close() + + argmatcher:addarg(actions or {}) + argmatcher:addflags(flags) + argmatcher:adddescriptions(descriptions) +end + +local matcher = clink.argmatcher('premake5') +if matcher.setdelayinit then + matcher:setdelayinit(delayinit) +end diff --git a/completions/robocopy.lua b/completions/robocopy.lua new file mode 100644 index 0000000..209f4f2 --- /dev/null +++ b/completions/robocopy.lua @@ -0,0 +1,124 @@ +-------------------------------------------------------------------------------- +-- Clink argmatcher for Robocopy. +-- Uses delayinit to parse the Robocopy help text. + +local clink_version = require('clink_version') +if not clink_version.supports_argmatcher_delayinit then + print("robocopy.lua argmatcher requires a newer version of Clink; please upgrade.") + return +end + +require('arghelper') +local mcf = require('multicharflags') + +local function sentence_casing(text) + if unicode.iter then + for str in unicode.iter(text) do -- luacheck: ignore 512 + return clink.upper(str) .. text:sub(#str + 1) + end + return text + else + return clink.upper(text:sub(1,1)) .. text:sub(2) + end +end + +local function delayinit(argmatcher) + local r = io.popen('robocopy.exe /??? 2>nul') + if not r then + return + end + + local flags = {} + local hideflags = {} + local descriptions = {} + + local function add_match(flag, disp, desc, linked) + local altflag = flag:lower() + if flag == altflag then + altflag = nil + end + desc = sentence_casing(desc) + if linked then + table.insert(flags, flag..linked) + if altflag then + table.insert(flags, altflag..linked) + table.insert(hideflags, altflag) + end + else + table.insert(flags, flag) + if altflag then + table.insert(flags, altflag) + table.insert(hideflags, altflag) + end + end + if disp then + descriptions[flag] = { disp, desc } + else + descriptions[flag] = { desc } + end + end + + local rashcnet_chars = { + nosort=true, + caseless=true, + { 'R', 'Read-only' }, + { 'A', 'Archive' }, + { 'S', 'System' }, + { 'H', 'Hidden' }, + { 'C', 'Compressed' }, + { 'N', 'Not content indexed' }, + { 'E', 'Encrypted' }, + { 'T', 'Temporary' }, + } + local rashcneto_chars = { + nosort=true, + caseless=true, + } + for _, x in ipairs(rashcnet_chars) do + table.insert(rashcneto_chars, x) + end + table.insert(rashcneto_chars, { 'O', 'Offline' }) + + local rashcnet = mcf.addcharflagsarg(clink.argmatcher(), rashcnet_chars) + local rashcneto = mcf.addcharflagsarg(clink.argmatcher(), rashcneto_chars) + + for line in r:lines() do + if unicode.fromcodepage then + line = unicode.fromcodepage(line) + end + local f,d = line:match('^ *(/[^ ]+) :: (.+)$') + if f then + local a,b = f:match('^(.-)%[:(.+)%]$') + if a then + add_match(a, nil, d) + add_match(a..':', b, d) + else + a,b = f:match('^([^:]+:)(.+)$') + if not a then + a,b = f:match('^([^ ]+)( .+)$') + end + if a then + if a == "/A-:" or a == "/A+:" then + -- TODO: Clink can't do completions for /A+: yet. + add_match(a, b, d, rashcnet) + elseif a == "/IA:" or a == "/XA:" then + add_match(a, b, d, rashcneto) + else + add_match(a, b, d) + end + else + add_match(f, nil, d) + end + end + end + end + + r:close() + + argmatcher:addflags(flags) + argmatcher:hideflags(hideflags) + argmatcher:adddescriptions(descriptions) + return true +end + +clink.argmatcher('robocopy'):setdelayinit(delayinit) diff --git a/completions/scrcpy.lua b/completions/scrcpy.lua new file mode 100644 index 0000000..1c93af5 --- /dev/null +++ b/completions/scrcpy.lua @@ -0,0 +1,245 @@ +--- scrcpy.lua, Genymobile's scrcpy completion for Clink. +-- @compatible scrcpy v1.21 +-- @author Goldie Lin +-- @date 2021-12-12 +-- @see [Clink](https://github.com/chrisant996/clink) +-- @see [scrcpy](https://github.com/Genymobile/scrcpy) +-- @usage +-- Place it in "%LocalAppData%\clink\" if installed globally, +-- or "ConEmu/ConEmu/clink/" if you used portable ConEmu & Clink. +-- + +-- luacheck: no unused args +-- luacheck: ignore clink rl_state + +local function dump(o) -- luacheck: ignore + if type(o) == 'table' then + local s = '{ ' + local prefix = "" + for k, v in pairs(o) do + if type(k) ~= 'number' then k = '"'..k..'"' end + s = s..prefix..'['..k..']="'..dump(v)..'"' + prefix = ', ' + end + return s..' }' + else + return tostring(o) + end +end + +local function generate_matches(command, pattern) + local f = io.popen('2>nul '..command) + if f then + local matches = {} + for line in f:lines() do + if line ~= 'List of devices attached' then + table.insert(matches, line:match(pattern)) + end + end + f:close() + return matches + end +end + +local function serialno_parser(word) + return clink.argmatcher():addarg({generate_matches('adb devices', '^(%w+)%s+.*$')}) +end + +local null_parser = clink.argmatcher():nofiles() + +local bitrate_parser = clink.argmatcher() +:nofiles() +:addarg({ + "8000000" .. null_parser, + "8000K" .. null_parser, + "8M" .. null_parser +}) + +local crop_parser = clink.argmatcher() +:nofiles() +:addarg({ + "720:1280:50:50" .. null_parser +}) + +local display_parser = clink.argmatcher() +:nofiles() +:addarg({ + "0" .. null_parser +}) + +local lockvideoorientation_parser = clink.argmatcher() +:nofiles() +:addarg({ + "unlocked" .. null_parser, + "initial" .. null_parser, + "0" .. null_parser, + "1" .. null_parser, + "2" .. null_parser, + "3" .. null_parser +}) + +local maxfps_parser = clink.argmatcher() +:nofiles() +:addarg({ + "60" .. null_parser +}) + +local maxsize_parser = clink.argmatcher() +:nofiles() +:addarg({ + "0" .. null_parser +}) + +local portnumber_parser = clink.argmatcher() +:nofiles() +:addarg({ + "27183" .. null_parser +}) + +local pushtarget_parser = clink.argmatcher() +:nofiles() +:addarg({ + "/sdcard/" +}) + +local recordformat_parser = clink.argmatcher() +:nofiles() +:addarg({ + "mp4" .. null_parser, + "mkv" .. null_parser +}) + +local renderdriver_parser = clink.argmatcher() +:nofiles() +:addarg({ + "direct3d" .. null_parser, + "metal" .. null_parser, + "opengl" .. null_parser, + "opengles" .. null_parser, + "opengles2" .. null_parser, + "software" .. null_parser +}) + +local rotation_parser = clink.argmatcher() +:nofiles() +:addarg({ + "0" .. null_parser, + "1" .. null_parser, + "2" .. null_parser, + "3" .. null_parser +}) + +local shortcutmod_parser = clink.argmatcher() +:nofiles() +:addarg({ + "lalt,lsuper" .. null_parser, + "lctrl" .. null_parser, + "rctrl" .. null_parser, + "lalt" .. null_parser, + "ralt" .. null_parser, + "lsuper" .. null_parser, + "rsuper" .. null_parser +}) + +local verbosity_parser = clink.argmatcher() +:nofiles() +:addarg({ + "debug" .. null_parser, + "info" .. null_parser, + "warn" .. null_parser, + "error" .. null_parser +}) + +local windowx_parser = clink.argmatcher() +:nofiles() +:addflags({ + "-1" .. null_parser +}) + +local windowy_parser = clink.argmatcher() +:nofiles() +:addflags({ + "-1" .. null_parser +}) + +local windowwidth_parser = clink.argmatcher() +:nofiles() +:addarg({ + "0" .. null_parser +}) + +local windowheight_parser = clink.argmatcher() +:nofiles() +:addarg({ + "0" .. null_parser +}) + +clink.argmatcher() +:nofiles() +:addflags( + "--always-on-top" .. null_parser, + "-b" .. bitrate_parser, + "--bit-rate" .. bitrate_parser, + "--codec-options", + "--crop" .. crop_parser, + "--disable-screensaver" .. null_parser, + "--display" .. display_parser, + "--display-buffer", + "--encoder", + "--force-adb-forward" .. null_parser, + "--forward-all-clicks" .. null_parser, + "-f" .. null_parser, + "--fullscreen" .. null_parser, + "-K" .. null_parser, + "--hid-keyboard" .. null_parser, + "-h" .. null_parser, + "--help" .. null_parser, + "--legacy-paste" .. null_parser, + "--lock-video-orientation" .. lockvideoorientation_parser, + "--max-fps" .. maxfps_parser, + "-m" .. maxsize_parser, + "--max-size" .. maxsize_parser, + "--no-clipboard-autosync" .. null_parser, + "-n" .. null_parser, + "--no-control" .. null_parser, + "-N" .. null_parser, + "--no-display" .. null_parser, + "--no-key-repeat" .. null_parser, + "--no-mipmaps" .. null_parser, + "-p" .. portnumber_parser, + "--port" .. portnumber_parser, + "--power-off-on-close" .. null_parser, + "--prefer-text" .. null_parser, + "--push-target" .. pushtarget_parser, + "--raw-key-events" .. null_parser, + "-r", + "--record", + "--record-format" .. recordformat_parser, + "--render-driver" .. renderdriver_parser, + "--render-expired-frames" .. null_parser, + "--rotation" .. rotation_parser, + "-s" .. serialno_parser, + "--serial" .. serialno_parser, + "--shortcut-mod" .. shortcutmod_parser, + "-S" .. null_parser, + "--turn-screen-off" .. null_parser, + "-t" .. null_parser, + "--show-touches" .. null_parser, + "--tunnel-host", + "--tunnel-port", + "--v4l2-sink", + "--v4l2-buffer", + "-V" .. verbosity_parser, + "--verbosity" .. verbosity_parser, + "-v" .. null_parser, + "--version" .. null_parser, + "-w" .. null_parser, + "--stay-awake" .. null_parser, + "--tcpip", + "--window-borderless" .. null_parser, + "--window-title", + "--window-x" .. windowx_parser, + "--window-y" .. windowy_parser, + "--window-width" .. windowwidth_parser, + "--window-height" .. windowheight_parser +) diff --git a/completions/sed.lua b/completions/sed.lua new file mode 100644 index 0000000..9fdfc52 --- /dev/null +++ b/completions/sed.lua @@ -0,0 +1,7 @@ +local clink_version = require('clink_version') +if not clink_version.supports_argmatcher_delayinit then + print("sed.lua argmatcher requires a newer version of Clink; please upgrade.") + return +end + +require('help_parser').make('sed', '--help', 'gnu') diff --git a/completions/sudo.lua b/completions/sudo.lua new file mode 100644 index 0000000..7e7b975 --- /dev/null +++ b/completions/sudo.lua @@ -0,0 +1,7 @@ +local clink_version = require('clink_version') +if not clink_version.supports_argmatcher_chaincommand then + print("sudo.lua argmatcher requires a newer version of Clink; please upgrade.") + return +end + +clink.argmatcher("sudo"):chaincommand() diff --git a/completions/winget.lua b/completions/winget.lua new file mode 100644 index 0000000..7db0b29 --- /dev/null +++ b/completions/winget.lua @@ -0,0 +1,364 @@ +require("arghelper") + +-------------------------------------------------------------------------------- +-- It would have been great to simply use the "winget complete" command. +-- But it doesn't provide completions for lots of things. + +-------------------------------------------------------------------------------- +-- Helper functions. + +local function winget_complete(command) + local matches = {} + local winget = os.getenv("USERPROFILE") + if winget then + winget = '"'..path.join(winget, "AppData\\Local\\Microsoft\\WindowsApps\\winget.exe")..'"' + local f = io.popen('2>nul '..winget..' complete --word="" --commandline="winget '..command..' " --position='..tostring(9 + #command)) -- luacheck: no max line length + if f then + for line in f:lines() do + table.insert(matches, line) + end + f:close() + end + end + return matches +end + +local function complete_export_source() + return winget_complete("export --source") +end + +-------------------------------------------------------------------------------- +-- Parsers for linking. + +local add_source_matches = clink.argmatcher():addarg() +local arch_matches = clink.argmatcher():addarg({fromhistory=true}) +local command_matches = clink.argmatcher():addarg({fromhistory=true}) +local count_matches = clink.argmatcher():addarg({fromhistory=true, 10, 20, 40}) +local file_matches = clink.argmatcher():addarg(clink.filematches) +local header_matches = clink.argmatcher():addarg({fromhistory=true}) +local id_matches = clink.argmatcher():addarg({fromhistory=true}) +local locale_matches = clink.argmatcher():addarg({fromhistory=true}) +local location_matches = clink.argmatcher():addarg(clink.dirmatches) +local moniker_matches = clink.argmatcher():addarg({fromhistory=true}) +local name_matches = clink.argmatcher():addarg({fromhistory=true}) +local override_matches = clink.argmatcher():addarg({fromhistory=true}) +local productcode_matches = clink.argmatcher():addarg({fromhistory=true}) +local query_matches = clink.argmatcher():addarg({fromhistory=true}) +local scope_matches = clink.argmatcher():addarg({fromhistory=true}) +local setting_name_matches = clink.argmatcher():addarg({fromhistory=true}) +local source_matches = clink.argmatcher():addarg({complete_export_source}) +local tag_matches = clink.argmatcher():addarg({fromhistory=true}) +local type_matches = clink.argmatcher():addarg({"Microsoft.PreIndexed.Package"}) +local url_matches = clink.argmatcher():addarg() +local version_matches = clink.argmatcher():addarg() + +-------------------------------------------------------------------------------- +-- Factored flag definitions. + +local common_flags = { + { hide=true, "--verbose-logs" }, + { hide=true, "--no-vt" }, + { hide=true, "--rainbow" }, + { hide=true, "--retro" }, + { "-?" }, + { "--help" }, +} + +local query_flags = { + { hide=true, "-q"..query_matches }, + { "--query"..query_matches, " query", "" }, + { "--id"..id_matches, " id", "" }, + { "--name"..name_matches, " name", "" }, + { "--moniker"..moniker_matches, " moniker", "" }, + { "--tag"..tag_matches, " tag", "" }, + { "--command"..command_matches, " command", "" }, + { hide=true, "-n"..count_matches, "" }, + { "--count"..count_matches, " count", "" }, + { hide=true, "-e" }, + { "--exact" }, +} + +local source_flags = { + { hide=true, "-s"..source_matches }, + { "--source"..source_matches, " source", "" }, +} + +-------------------------------------------------------------------------------- +-- Command parsers. + +local export_parser = clink.argmatcher():_addexflags({ + opteq=true, + { hide=true, "-o"..file_matches }, + { "--output"..file_matches, " file", "" }, + source_flags, + { hide=true, "--include-versions" }, + { hide=true, "--accept-source-agreements" }, + common_flags, +}) +:addarg(clink.filematches) +:nofiles() + +local features_parser = clink.argmatcher():_addexflags({ + common_flags, +}) +:nofiles() + +local hash_parser = clink.argmatcher():_addexflags({ + opteq=true, + { hide=true, "-f"..file_matches }, + { "--file"..file_matches, " file", ""}, + { hide=true, "-m" }, + { "--msix" }, + common_flags, +}) +:addarg(clink.filematches) +:nofiles() + +local help_parser = clink.argmatcher():addarg({ + "export", + "features", + "hash", + "help", + "import", + "info", + "install", + "list", + "search", + "settings", + "show", + "source", + "uninstall", + "upgrade", + "validate", +}) +:nofiles() + +local import_parser = clink.argmatcher():_addexflags({ + opteq=true, + { hide=true, "-i"..file_matches }, + { "--import-file"..file_matches, " file", "" }, + { "--ignore-unavailable" }, + { "--ignore-versions" }, + { hide=true, "--accept-package-agreements" }, + { hide=true, "--accept-source-agreements" }, + common_flags, +}) +:addarg(clink.filematches) +:nofiles() + +local info_parser = clink.argmatcher():_addexflags({ + common_flags, +}) +:nofiles() + +local install_parser = clink.argmatcher():_addexflags({ + opteq=true, + query_matches, + { hide=true, "-m"..file_matches }, + { "--manifest"..file_matches, " file", "" }, + { hide=true, "-v"..version_matches }, + { "--version"..version_matches, " version", "" }, + source_flags, + { "--scope"..scope_matches, " scope", "" }, + { hide=true, "-a"..arch_matches }, + { "--architecture"..arch_matches, " arch", "" }, + { hide=true, "-i" }, + { "--interactive" }, + { hide=true, "-h" }, + { "--silent" }, + { "--locale"..locale_matches, " locale", "" }, + { hide=true, "-o"..file_matches }, + { "--log"..file_matches, " file", "" }, + { "--override"..override_matches, " string", "" }, + { hide=true, "-l"..location_matches }, + { "--location"..location_matches, " location", "" }, + { "--force" }, + { "--accept-package-agreements" }, + { "--accept-source-agreements" }, + { "--header"..header_matches, " header", "" }, + { hide=true, "-r"..file_matches }, + { "--rename"..file_matches, " file", "" }, + common_flags, +}) +:addarg(query_matches) +:nofiles() + +local list_parser = clink.argmatcher():_addexflags({ + opteq=true, + query_flags, + source_flags, + { hide=true, "--accept-source-agreements" }, + { "--header"..header_matches, " header", "" }, + common_flags, +}) +:addarg(query_matches) +:nofiles() + +local search_parser = list_parser + +local settings_parser = clink.argmatcher():_addexflags({ + { "--enable"..setting_name_matches, " setting", "" }, + { "--disable"..setting_name_matches, " setting", "" }, +}) +:addarg(setting_name_matches) +:nofiles() + +local show_parser = clink.argmatcher():_addexflags({ + opteq=true, + query_flags, + { hide=true, "-m"..file_matches }, + { "--manifest"..file_matches, " file", "" }, + source_flags, + { hide=true, "-v"..version_matches }, + { "--version"..version_matches, " version", "" }, + { "--versions" }, + { "--header"..header_matches, " header", "" }, + { "--accept-source-agreements" }, + common_flags, +}) +:addarg(query_matches) +:nofiles() + +local source_add_parser = clink.argmatcher():_addexflags({ + { hide=true, "-n"..add_source_matches }, + { "--name"..add_source_matches, " name", "" }, + { hide=true, "-a"..url_matches }, + { "--arg"..url_matches, " url", "" }, + { hide=true, "-t"..type_matches }, + { "--type"..type_matches, " type", "" }, + common_flags, +}) +:addarg(name_matches) +:nofiles() + +local source_list_parser = clink.argmatcher():_addexflags({ + { hide=true, "-n"..source_matches }, + { "--name"..source_matches, " name", "" }, + common_flags, +}) +:addarg(name_matches) +:nofiles() + +local source_update_parser = clink.argmatcher():_addexflags({ + { hide=true, "-n"..source_matches }, + { "--name"..source_matches, " name", "" }, + common_flags, +}) +:addarg(name_matches) +:nofiles() + +local source_remove_parser = clink.argmatcher():_addexflags({ + { hide=true, "-n"..source_matches }, + { "--name"..source_matches, " name", "" }, + common_flags, +}) +:addarg(name_matches) +:nofiles() + +local source_reset_parser = clink.argmatcher():_addexflags({ + { "--force" }, + common_flags, +}) +:nofiles() + +local source_export_parser = clink.argmatcher():_addexflags({ + common_flags, +}) +:addarg(source_matches) +:nofiles() + +local source_parser = clink.argmatcher():_addexflags({ + opteq=true, + common_flags, +}) +:addarg({ + "add"..source_add_parser, + "list"..source_list_parser, + "update"..source_update_parser, + "remove"..source_remove_parser, + "reset"..source_reset_parser, + "export"..source_export_parser, +}) +:nofiles() + +local uninstall_parser = clink.argmatcher():_addexflags({ + opteq=true, + query_flags, + { hide=true, "-m"..file_matches }, + { "--manifest"..file_matches, " file", "" }, + { "--product-code"..productcode_matches, " code", "" }, + { hide=true, "-v"..version_matches }, + { "--version"..version_matches, " version", "" }, + { hide=true, "-i" }, + { "--interactive" }, + { hide=true, "-h" }, + { "--silent" }, + { "--purge" }, + { "--preserve" }, + { hide=true, "-o"..file_matches }, + { "--log"..file_matches, " file", "" }, + { "--accept-source-agreements" }, + { "--header"..header_matches, " header", "" }, + common_flags, +}) +:addarg(query_matches) +:nofiles() + +local upgrade_parser = clink.argmatcher():_addexflags({ + opteq=true, + query_flags, + { hide=true, "-m"..file_matches }, + { "--manifest"..file_matches, " file", "" }, + { hide=true, "-v"..version_matches }, + { "--version"..version_matches, " version", "" }, + { hide=true, "-i" }, + { "--interactive" }, + { hide=true, "-h" }, + { "--silent" }, + { "--purge" }, + { hide=true, "-o"..file_matches }, + { "--log"..file_matches, " file", "" }, + { "--override"..override_matches, " string", "" }, + { hide=true, "-l"..location_matches }, + { "--location"..location_matches, " location", "" }, + { "--force" }, + { "--accept-package-agreements" }, + { "--accept-source-agreements" }, + { "--header"..header_matches, " header", "" }, + { "--all" }, + { "--include-unknown" }, + common_flags, +}) +:addarg(query_matches) +:nofiles() + +local validate_parser = clink.argmatcher():_addexflags({ + opteq=true, + { "--manifest"..file_matches, " file", "" }, + common_flags +}) +:addarg(clink.filematches) +:nofiles() + +-------------------------------------------------------------------------------- +-- Define the winget argmatcher. + +local winget_parser = { + "export" .. export_parser, + "features" .. features_parser, + "hash" .. hash_parser, + "help" .. help_parser, + "import" .. import_parser, + "info" .. info_parser, + "install" .. install_parser, + "list" .. list_parser, + "search" .. search_parser, + "settings" .. settings_parser, + "show" .. show_parser, + "source" .. source_parser, + "uninstall" .. uninstall_parser, + "upgrade" .. upgrade_parser, + "validate" .. validate_parser, +} + +clink.argmatcher("winget"):addarg(winget_parser):addflags("--version", "--info", "--help") diff --git a/completions/xcopy.lua b/completions/xcopy.lua new file mode 100644 index 0000000..3e14ca3 --- /dev/null +++ b/completions/xcopy.lua @@ -0,0 +1,7 @@ +local clink_version = require('clink_version') +if not clink_version.supports_argmatcher_delayinit then + print("xcopy.lua argmatcher requires a newer version of Clink; please upgrade.") + return +end + +require('help_parser').make('xcopy', '/?') diff --git a/git.lua b/git.lua index b282224..5c9f250 100644 --- a/git.lua +++ b/git.lua @@ -1,6 +1,6 @@ -- preamble: common routines -local path = require('path') +local path_module = require('path') local git = require('gitutil') local matchers = require('matchers') local w = require('tables').wrap @@ -22,6 +22,8 @@ local dir_matches = clink.dirmatches or matchers.dirs local files_parser = parser({file_matches}) local dirs_parser = parser({dir_matches}) +local looping_files_parser = clink.argmatcher and clink.argmatcher():addarg(clink.filematches):loop() + --- -- Lists remote branches based on packed-refs file from git directory -- @param string [dir] Directory where to search file for @@ -50,12 +52,53 @@ local function list_remote_branches(dir) local git_dir = dir or git.get_git_common_dir() if not git_dir then return w() end - return w(path.list_files(git_dir..'/refs/remotes', '/*', + return w(path_module.list_files(git_dir..'/refs/remotes', '/*', --[[recursive=]]true, --[[reverse_separator=]]true)) :concat(list_packed_refs(git_dir)) :sort():dedupe() end +local function list_git_status_files(token, flags) -- luacheck: no unused args + local result = w() + local git_dir = git.get_git_common_dir() + if git_dir then + local f = io.popen("git status --porcelain "..(flags or "").." 2>nul") + if f then + if string.matchlen then + --[[ + token = path.normalise(token) + --]] + for line in f:lines() do + line = line:match("^.. (.+)$") + if line then + line = path.normalise(line) + --[[ + -- TODO: Maybe use match display filtering to show the number of files in each dir? + local mlen = string.matchlen(line, token) + if mlen < 0 then + table.insert(result, { match = line, type = "file" }) + else + local dir = path.getdirectory(line:sub(1, mlen)) + local child = line:sub(mlen + 1):match("^([^/\\]*[/\\]?)") + local m = dir and path.join(dir, child) or child + local isdir = m:sub(-1):find("[/\\]") + table.insert(result, { match = m, type = (isdir and "dir" or "file") }) + end + --]] + table.insert(result, line) + end + end + else + for line in f:lines() do + table.insert(result, line:sub(4)) + end + end + f:close() + end + end + return result +end + --- -- Lists local branches for git repo in git_dir directory. -- @@ -65,7 +108,7 @@ local function list_local_branches(dir) local git_dir = dir or git.get_git_common_dir() if not git_dir then return w() end - local result = w(path.list_files(git_dir..'/refs/heads', '/*', + local result = w(path_module.list_files(git_dir..'/refs/heads', '/*', --[[recursive=]]true, --[[reverse_separator=]]true)) return result @@ -104,7 +147,7 @@ local function get_git_aliases() end -- Function to generate completions for alias -local function alias(token) +local function alias(token) -- luacheck: no unused args local res = w() local aliases = get_git_aliases() @@ -153,12 +196,12 @@ local function local_or_remote_branches(token) end) end -local function checkout_spec_generator_deprecated(token) - local files = matchers.files(token) - :filter(function(file) - return path.is_real_dir(file) - end) +local function add_spec_generator(token) + return list_git_status_files(token, "-uall") +end +local function checkout_spec_generator_deprecated(token) + local files = list_git_status_files(token, "-uno") local git_dir = git.get_git_common_dir() local local_branches = branches(token) @@ -212,7 +255,7 @@ local function checkout_spec_generator(token) local git_dir = git.get_git_common_dir() - local files = matchers.files(token) + local files = list_git_status_files(token, "-uno") local local_branches = branches(token) local remote_branches = list_remote_branches(git_dir) :filter(function(branch) @@ -240,7 +283,7 @@ local function checkout_spec_generator(token) if clink_version.supports_query_rl_var and rl.isvariabletrue('colored-stats') then star = color.get_clink_color('color.git.star')..star..color.get_clink_color('color.filtered') end - return files + return files:map(function(file) return '\x1b[m'..file end) :concat(local_branches:map(function(branch) return { match=branch } end)) :concat(predicted_branches:map(function(branch) return { match=branch, display=star..branch } end)) :concat(remote_branches:map(function(branch) return { match=branch } end)) @@ -292,9 +335,9 @@ local function push_branch_spec(token) -- TODO: show remote branches only for remote that has been specified as previous argument local b = w(clink.find_dirs(git_dir..'/refs/remotes/*')) - :filter(function(remote) return path.is_real_dir(remote) end) + :filter(function(remote) return path_module.is_real_dir(remote) end) :reduce({}, function(result, remote) - return w(path.list_files(git_dir..'/refs/remotes/'..remote, '/*', + return w(path_module.list_files(git_dir..'/refs/remotes/'..remote, '/*', --[[recursive=]]true, --[[reverse_separator=]]true)) :filter(function(remote_branch) return clink.is_match(remote_branch_spec, remote_branch) @@ -305,7 +348,10 @@ local function push_branch_spec(token) -- setup display filter to prevent display '+' symbol in completion list if clink_version.supports_display_filter_description then b = b:map(function(branch) - return { match=(plus_prefix and '+'..local_branch_spec or local_branch_spec)..':'..branch, display=branch } + return { + match=(plus_prefix and '+'..local_branch_spec or local_branch_spec)..':'..branch, + display=branch + } end) clink.ondisplaymatches(function () return b @@ -391,12 +437,12 @@ local function concept_guides() local r = io.popen("git help -g 2>nul") if r then local matches = {} - local color = "\x1b[1m" + local sgr = "\x1b[1m" local mark = " \x1b[22;32m*" for line in r:lines() do local guide, desc = line:match("^ ([^ ]+) +(.+)$") if guide then - table.insert(matches, { match=guide, display=color..guide..mark, description="Guide: "..desc } ) + table.insert(matches, { match=guide, display=sgr..guide..mark, description="Guide: "..desc } ) end end r:close() @@ -412,14 +458,14 @@ local function all_commands() if r then local matches = {} local prefix = "Command: " - local color = "" + local sgr = "" for line in r:lines() do local command, desc = line:match("^ ([^ ]+) +(.+)$") if command then - table.insert(matches, { match=command, display=color..command, description=prefix..desc } ) + table.insert(matches, { match=command, display=sgr..command, description=prefix..desc } ) elseif line == "Command aliases" then prefix = "Alias: " - color = "\x1b["..settings.get("color.doskey").."m" + sgr = "\x1b["..settings.get("color.doskey").."m" end end r:close() @@ -429,6 +475,9 @@ local function all_commands() return {} end +-- luacheck: push +-- luacheck: no max line length + local mergesubtree_arg = parser({dir_matches}) local placeholder_required_arg = parser({}) -- Note: All these separate fromhistory parsers are necessary in order to @@ -785,7 +834,7 @@ local merge_flags = { -- Command parsers. local add_parser = parser() -:addarg(file_matches) +:addarg(add_spec_generator) :_addexflags({ "-n", "--dry-run", "-v", "--verbose", @@ -1177,7 +1226,7 @@ local help_parser = parser() { "--web", "Display manual page for the command in HTML format" }, }) if help_parser.setdelayinit then - help_parser:addarg({delayinit=function (argmatcher) + help_parser:addarg({delayinit=function (argmatcher) -- luacheck: no unused args local matches = all_commands() or {} local guides = concept_guides() or {} for _,g in ipairs(guides) do @@ -1196,7 +1245,7 @@ local log_parser = parser() :_addexflags(commit_formatting_flags) local merge_parser = parser() -:addarg(branches) +:addarg(local_or_remote_branches) :_addexflags({ "--commit", "--no-commit", "--edit", "-e", "--no-edit", @@ -1827,6 +1876,8 @@ local git_flags = { { "--no-optional-locks", "Do not perform optional operations that require locks" }, } +-- luacheck: pop + -- Initialize the argmatcher. This may be called repeatedly. local function init(argmatcher, full_init) -- When doing a full init, must reset in order to maintain the sort order. @@ -1844,6 +1895,8 @@ local function init(argmatcher, full_init) local linked = linked_parsers[x[1]] if linked then table.insert(commands, { x[1]..linked, x[2] }) + elseif looping_files_parser then + table.insert(commands, x..looping_files_parser) else table.insert(commands, x) end @@ -1869,6 +1922,8 @@ local function init(argmatcher, full_init) local linked = linked_parsers[x] if linked then table.insert(commands, x..linked) + elseif looping_files_parser then + table.insert(commands, x..looping_files_parser) else table.insert(commands, x) end diff --git a/modules/arghelper.lua b/modules/arghelper.lua index 1885f02..b48155a 100644 --- a/modules/arghelper.lua +++ b/modules/arghelper.lua @@ -14,7 +14,9 @@ -- "-a", -- Adds match "-a". -- { "-b" }, -- Adds match "-b". -- { "-c", "Use colors" }, -- Adds match "-c" and description "Use colors". --- { "-d", " date", "List newer than date" }, -- Adds string "-d", arginfo " date", and description "List newer than date". +-- { "-d", " date", "List newer than date" }, +-- -- Adds string "-d", arginfo " date", and +-- description "List newer than date". -- { -- Nested table, following the same format. -- { "-e" }, -- { "-f" }, @@ -182,7 +184,7 @@ if not tmp._addexflags or not tmp._addexarg then end if elm.hide then local name = arglinked and arg._key or arg - table.insert(hide, arg) + table.insert(hide, name) end elseif t == "function" then table.insert(list, arg) @@ -198,7 +200,7 @@ if not tmp._addexflags or not tmp._addexarg then local function build_lists(tbl) local list = {} - local descriptions = (not ARGHELPER_DISABLE_DESCRIPTIONS) and {} + local descriptions = (not ARGHELPER_DISABLE_DESCRIPTIONS) and {} -- luacheck: no global local hide = {} if type(tbl) ~= "table" then pause('table expected.') @@ -244,7 +246,7 @@ end -- If nothing was missing, then no interop functions got added, and the meta -- table doesn't need to be modified. -for _,_ in pairs(interop) do +for _,_ in pairs(interop) do -- luacheck: ignore 512 local old_index = meta.__index meta.__index = function(parser, key) local value = rawget(interop, key) diff --git a/modules/clink_version.lua b/modules/clink_version.lua index b3fc5ef..5001608 100644 --- a/modules/clink_version.lua +++ b/modules/clink_version.lua @@ -14,5 +14,7 @@ exports.supports_display_filter_description = (clink_version_encoded >= 10010012 exports.supports_color_settings = (clink_version_encoded >= 10010009) exports.supports_query_rl_var = (clink_version_encoded >= 10010009) exports.supports_path_toparent = (clink_version_encoded >= 10010020) +exports.supports_argmatcher_delayinit = (clink_version_encoded >= 10030010) +exports.supports_argmatcher_chaincommand = (clink_version_encoded >= 10030013) return exports diff --git a/modules/help_parser.lua b/modules/help_parser.lua new file mode 100644 index 0000000..43010e3 --- /dev/null +++ b/modules/help_parser.lua @@ -0,0 +1,648 @@ +-------------------------------------------------------------------------------- +-- This exports a table containing functions that delayinit argmatchers can use +-- to parse help text output from programs. +-- +-- local help_parser = require('help_parser') +-- +-- help_parser.run(argmatcher, 'gnu', 'grep --help') +-- help_parser.run(argmatcher, 'basic', 'findstr /?', { slashes=true }) +-- +-- help_parser.make('curl "--help all" curl') +-- help_parser.make('xcopy /?') +-- +-- function run(argmatcher, parser, command, config) +-- +-- Runs parser on the command output to initialize argmatcher. +-- +-- argmatcher The argmatcher to be initialized. +-- parser Can be a parser function, or the name of a built-in parser. +-- Built-in parsers are: 'basic', 'curl', 'gnu'. +-- command The command whose output to parse as help text, OR a table +-- of lines of help text output to be parsed. +-- config Optional table with configuration modes. +-- { +-- case=nil, -- Smart case; add lower case flags if +-- -- all flags are upper case. +-- case=1, -- Force adding lower case copies of +-- -- flags. +-- case=2, -- Add flags with verbatim casing. +-- slashes=true, -- Add slash copies of minus flags +-- -- (add /x for -x, etc). +-- } +-- +-- function make(command, help_flag, parser, config, closure) +-- +-- Makes a delayinit argmatcher for the command. A completion script can +-- be a single line, using this: +-- +-- require('help_parser').make('xcopy', '/?') +-- +-- command Command for which to make an argmatcher. +-- help_flag The command line args to append to get the help text for +-- parsing into an argmatcher. +-- parser Optional parser to use (see run()). +-- config Optional table with configuration modes (see run()). +-- closure Optional function to call on completion of the argmatcher, +-- called as closure(argmatcher). +-- +-- +-- Parser functions: +-- +-- function parser(context, flags, descriptions, hideflags, line) +-- +-- context A container table for use by the parser function. +-- flags Use add_pending() to update flags. +-- descriptions Use add_pending() to update descriptions. +-- hideflags A table of flag names to hide (table of strings). +-- line The help text line to be parsed. +-- +-- function add_pending(context, flags, descriptions, hideflags, pending) +-- context Pass the context table from the parser() function. +-- flags Pass the flags table from the parser() function. +-- descriptions Pass the descriptions table from the parser() function. +-- hideflags Pass the hideflags table from the parser() function. +-- pending The flag(s) to add. Scheme: +-- { +-- flag="-x", -- Flag to add. +-- has_arg=true, -- Whether the flag has an arg. +-- display=" file", -- Arg info label to show. +-- desc="Description text.", -- Description. +-- } + +-------------------------------------------------------------------------------- +if not clink then + -- E.g. some unit test systems will run this module *outside* of Clink. + return +end +local clink_version = require('clink_version') +if not clink_version.supports_argmatcher_delayinit then + return +end + +-------------------------------------------------------------------------------- +local function sentence_casing(text) + if unicode.iter then + for str in unicode.iter(text) do -- luacheck: ignore 512 + return clink.upper(str) .. text:sub(#str + 1) + end + return text + else + return clink.upper(text:sub(1,1)) .. text:sub(2) + end +end + +-------------------------------------------------------------------------------- +local _file_keywords = { 'file', 'files', 'filename', 'glob' } +local _dir_keywords = { 'dir', 'dirs' } + +for _, k in ipairs(_file_keywords) do + _file_keywords[' <' .. k .. '>'] = true + _file_keywords['<' .. k .. '>'] = true + _file_keywords[' ' .. k] = true + _file_keywords[k] = true + _file_keywords[' ' .. k:upper()] = true + _file_keywords[k:upper()] = true +end + +for _, k in ipairs(_dir_keywords) do + _dir_keywords[' <' .. k .. '>'] = true + _dir_keywords['<' .. k .. '>'] = true + _dir_keywords[' ' .. k] = true + _dir_keywords[k] = true + _dir_keywords[' ' .. k:upper()] = true + _dir_keywords[k:upper()] = true +end + +local function is_file_arg(display) + return _file_keywords[display] or display:find('file') +end + +local function is_dir_arg(display) + return _dir_keywords[display] +end + +-------------------------------------------------------------------------------- +local function add_pending(context, flags, descriptions, hideflags, pending) -- luacheck: no unused args + if not pending.flag then + return + end + + if pending.has_arg and pending.display then + if not pending.argmatcher then + if not pending.flag:match('[:=]$') and not pending.display:match('^[ \t]') then -- luacheck: ignore 542 + -- -x or -x[n] or -Tn or etc. Argmatchers must be separated + -- from flag by : or = or space. So, no argmatcher. + else + local args = clink.argmatcher() + if is_file_arg(pending.display) then + args:addarg(clink.filematches) + elseif is_dir_arg(pending.display) then + args:addarg(clink.dirmatches) + else + args:addarg({fromhistory=true}) + end + pending.argmatcher = args + end + end + pending.args = pending.argmatcher + else + pending.args = nil + end + + table.insert(flags, { flag=pending.flag, args=pending.args }) + + pending.desc = (pending.desc or ''):gsub('%.+$', '') + if pending.display then + descriptions[pending.flag] = { pending.display, pending.desc } + else + descriptions[pending.flag] = { pending.desc } + end +end + +-------------------------------------------------------------------------------- +local function earlier_gap(a, b) + local r + if not a or not a.ofs then + r = b + elseif not b or not b.ofs then + r = a + elseif a.ofs <= b.ofs then + r = a + else + r = b + end + return r and r.ofs and r or nil +end + +-------------------------------------------------------------------------------- +local function find_flag_gap(line, allow_no_gap) + local colon = { len=3, ofs=line:find(' : ') } + local spaces = { len=2, ofs=line:find(' ') } + local tab = { len=1, ofs=line:find('\t') } + + local gap = earlier_gap(earlier_gap(colon, spaces), tab) + if gap then + return gap + end + + local space = { len=1, ofs=line:find(' ') } + if not space.ofs then + if allow_no_gap then + return { len=0, ofs=#line + 1 } + else + return + end + end + + if not line:find('[ \t][-/][^ \t/]', space.ofs) then + return space + end +end + +-------------------------------------------------------------------------------- +-- The basic parser recognizes lines like: +-- +-- /A Description of /A. +-- -A Description of -A. +-- --foo Description of --foo. +-- -x file Description of -x with file argument. +-- +-- It strips bracketed stuff like /OFF[LINE]. +-- It ignores /nnn, /nnnn, -nnn, and -nnnn. +-- +-- Recognizes many variations of file and dir arg types. +-- Other arg types use fromhistory=true. +-- +local function basic_parser(context, flags, descriptions, hideflags, line) + local indent,f = line:match('^([ \t]*)([-/].+)$') + local pad,d + if f then + local gap = find_flag_gap(f, true--[[allow_no_gap]]) + if gap then + d = f:sub(gap.ofs + gap.len):gsub('^[ \t]+', '') + f = f:sub(1, gap.ofs - 1):gsub('[ \t]+$', '') + pad = line:sub(#indent + #f + 1, #line - #d) + else + f = nil + end + end + if not f and context.pending.desc then + indent,d = line:match('^([ \t]+)([^ \t].*)$') + if indent then + if #context.pending.desc == 0 then + context.pending.indent = #indent + elseif #indent ~= context.pending.indent then + indent = nil + d = nil + end + if d then + if #context.pending.desc > 0 then + context.pending.desc = context.pending.desc .. ' ' + end + context.pending.desc = context.pending.desc .. d:gsub('[ \t]+$', '') + end + end + end + + -- Too much indent can't be a flag. + if indent and #indent > 8 then + f = nil + end + + -- Add pending flag. + if context.pending.flag and (f or not d) then + if not context.pending.display then + local display = context.pending.flag:match('^[-/][A-Z].*([cnx]+)$') + if display then + context.pending.display = context.pending.flag:sub(0 - #display) + context.pending.flag = context.pending.flag:sub(1, #context.pending.flag - #display) + end + end + add_pending(context, flags, descriptions, hideflags, context.pending) + context.pending = {} + end + + if f then + -- Skip various things. + f = f:gsub('[ \t]+$', '') + local pd = f:match('(%[n%])$') + if not pd then + f = f:gsub('%[.*%]$', '') + end + d = d:gsub('[ \t]+$', '') + + -- Set pending flag. + local x, y = f:match('^([^ \t]+)([ \t].+)$') + if x then + f = x + context.pending.display = y + else + local delim = f:find('[:=]') + local bracket = f:find('%[') + if delim and (not bracket or delim < bracket) then + x, y = f:match('^([^ \t]+[:=])(.+)$') + elseif bracket and (not delim or bracket < delim) then + x, y = f:match('^([^%[]+)(%[.+)$') + end + if x then + f = x + context.pending.display = y + end + end + + if f:match('^[-/]nnnn?$') or f:match('^//') then + return + end + + context.pending.flag = f + context.pending.desc = sentence_casing(d) + context.pending.indent = #indent + #f + #pad + context.pending.has_arg = context.pending.display and true + end +end + +-------------------------------------------------------------------------------- +-- The curl parser recognizes this layout: +-- +-- ... ignore lines unless they start with at least 1 space ... +-- -a, --aardvark description +-- --bumblebee description +-- +-- Arguments are indicated by angle brackets and apply to all of the flags +-- listed on the line of help text. +-- +-- -a, --aardvark description +-- +-- Recognizes many variations of file, and dir arg types. +-- Other arg types use fromhistory=true. +-- +local function curl_parser(context, flags, descriptions, hideflags, line) + -- Parse if the line declares one or more flags. + local s = line:match('^ +(%-.+)$') + if not s then + return + end + + -- Look for gap between flags and description. + local gap = find_flag_gap(s) + if not gap then + return + end + + local pending = {} + + -- Parse description. + pending.desc = s:sub(gap.ofs + gap.len):match('^ *([^ ].*)$') + if not pending.desc then + return + end + s = s:sub(1, gap.ofs - 1):gsub(' +$', '') + pending.desc = pending.desc:gsub('%.+$', '') + pending.desc = pending.desc:gsub('^: ', '') + pending.desc = sentence_casing(pending.desc) + + -- Parse flag arguments. + pending.display = s:match(' <.+>$') + if pending.display then + s = s:sub(1, #s - #pending.display) + pending.has_arg = true + end + + -- Add flags. + local list = string.explode(s, ',') + for _,f in ipairs(list) do + f = f:match('^ *([^ ].*)$') + if f then + pending.flag = f + add_pending(context, flags, descriptions, hideflags, pending) + end + end +end + +-------------------------------------------------------------------------------- +-- The GNU parser recognizes this layout: +-- +-- ... ignore lines unless they start with at least 2 spaces ... +-- -a... description +-- -a... +-- description which could be +-- more than one line +-- +-- Some lines define more than one flag, delimited by commas: +-- +-- -b, --bar, etc description +-- -b, --bar, etc ... +-- description +-- +-- Some flags accept arguments, and follow these layouts: +-- +-- --abc[=X] Defines --abc and --abc=X. +-- --def=Y Defines --def=Y. +-- -g, --gg=Z Define -g and --gg= with required Z arg. +-- -j Z Define -j with required Z arg. +-- -k[Z] Define -k with optional Z arg, with no space. +-- --color[=WHEN], <-- Notice the `,` +-- --colour[=WHEN] +-- +-- Some flags have a predefined list of args: +-- +-- --foo=XYZ description +-- XYZ is 'a', 'b', or 'c' +-- +-- Recognizes many variations of file and dir arg types. +-- Other arg types use fromhistory=true. +-- +-- Special exception: +-- +-- A minus sign followed by an arbitrary number isn't representable as a +-- flag in Clink. +-- +local function gnu_parser(context, flags, descriptions, hideflags, line) + local x = line:match('^ +([^ ].+)$') + if x then + if context.arg_line_missing_desc then + -- The line is a description. + if context.desc then + context.desc = context.desc .. ' ' + end + context.desc = (context.desc or '') .. x + else + -- The line is an arg list. + if context.pending and context.pending.expect_args then + local words = string.explode(line, ' ,') + if clink.upper(words[1]) == words[1] and words[2] == 'is' then + local arglist = {} + for _,w in ipairs(words) do + local arg = w:match("^'(.*)'$") + if arg then + table.insert(arglist, arg) + end + end + context.pending.argmatcher = clink.argmatcher():addarg(arglist) + context.pending.expect_args = nil + end + end + end + else + -- Add any pending flags. + if context.pending then + if context.desc then + context.desc = context.desc:gsub('%.+$', '') + context.desc = context.desc:gsub(';$', '') + context.desc = sentence_casing(context.desc) + context.pending.desc = context.desc + context.desc = nil + end + for _,f in ipairs(context.pending) do + if f.flag == '-NUM' then -- luacheck: ignore 542 + -- Clink can't represent minus followed by any number. + else + context.pending.flag = f.flag + context.pending.has_arg = f.has_arg or (f.has_arg == nil and context.pending.arginfo) + local display = f.display + if not display and context.pending.has_arg then + display = context.pending.arginfo + end + if not display then + context.pending.display = nil + elseif f.flag:match('[:=]$') then + context.pending.display = display:gsub('^[ \t]', '') + else + context.pending.display = ' ' .. display:gsub('^[ \t]', '') + end + add_pending(context, flags, descriptions, hideflags, context.pending) + end + end + context.pending = {} + context.arg_line_missing_desc = nil + end + -- Parse if the line declares one or more flags. + local s = line:match('^ +(%-.+)$') + if s then + if context.carryover then + s = context.carryover .. ' ' .. s + context.carryover = nil + end + local gap = find_flag_gap(s) + if not gap and s:find(',$') then + context.carryover = s + else + if gap then + context.arg_line_missing_desc = false + context.desc = s:sub(gap.ofs + gap.len):match('^[ \t]*([^ \t].*)$') + s = s:sub(1, gap.ofs - 1) + else + context.arg_line_missing_desc = true + end + -- All flags on a single line share one argmatcher. + local d + local list = string.explode(s, ',') + context.pending.expect_args = true + for _,f in ipairs(list) do + f = f:match('^ *([^ ].*)$') + if f then + if f:find('%[=') then + -- Add two flags. + f,d = f:match('^([^[]+)%[=(.*)%]$') + if f then + local feq = f .. '=' + context.pending.arginfo = d + table.insert(context.pending, { flag=f, has_arg=false }) + table.insert(context.pending, { flag=feq, has_arg=true, display=d }) + end + elseif f:find('%[') then + -- Add a flag with just an arginfo hint. + local arginfo + f,arginfo = f:match('^([^[]+)(.*)$') + if f then + table.insert(context.pending, { flag=f, has_arg=false, display=arginfo }) + end + elseif f:find('=') then + -- Add a flag with an arg. + f,d = f:match('^([^=]+=)(.*)$') + if f then + context.pending.arginfo = d + table.insert(context.pending, { flag=f, has_arg=true, display=d }) + end + elseif f:find(' ') then + -- Add a flag with an arg. + f,d = f:match('^([^ ]+)( .*)$') + if f then + context.pending.arginfo = d + table.insert(context.pending, { flag=f, has_arg=true, display=d }) + end + else + -- Add a flag verbatim. + table.insert(context.pending, { flag=f }) + end + end + end + end + end + end +end + +-------------------------------------------------------------------------------- +local _parsers = { + ['basic'] = basic_parser, + ['curl'] = curl_parser, + ['gnu'] = gnu_parser, +} + +-------------------------------------------------------------------------------- +local function run(argmatcher, parser, command, config) + if type(parser) ~= "function" then + parser = parser and _parsers[parser:lower()] or basic_parser + end + + local flags = {} + local descriptions = {} + local hideflags = {} + local context = { pending={} } + config = config or {} + + if type(command) == "table" then + if not command[1] then + return + end + for _, line in ipairs(command) do + parser(context, flags, descriptions, hideflags, line) + end + else + local r = io.popen('2>nul ' .. command) + if not r then + return + end + + for line in r:lines() do + if unicode.fromcodepage then + line = unicode.fromcodepage(line) + end + parser(context, flags, descriptions, hideflags, line) + end + r:close() + end + parser(context, flags, descriptions, hideflags, "") + + if config.slashes then + local slashes = {} + for _, f in ipairs(flags) do + if f.flag:match('^%-[^-]') then + local sf = f.flag:gsub('^%-', '/') + table.insert(slashes, { flag=sf, args=f.args }) + descriptions[sf] = descriptions[f.flag] + end + end + for _, sf in ipairs(slashes) do + table.insert(flags, sf) + end + end + + local caseless + if config.case == 1 then + -- Caseless: Explicitly forcing caseless. + caseless = true + elseif config.case == nil then + -- Smart case: Caseless if all flags are upper case. + caseless = true + for _, f in ipairs(flags) do + local lower = clink.lower(f.flag) + if lower == f.flag then + local upper = clink.upper(f.flag) + if upper ~= f.flag then + caseless = false + break + end + end + end + end + + local actual_flags = {} + + if caseless then + for _, f in ipairs(flags) do + local lower = clink.lower(f.flag) + if f.flag ~= lower then + if f.args then + table.insert(actual_flags, lower .. f.args) + else + table.insert(actual_flags, lower) + end + table.insert(hideflags, lower) + end + end + end + + for _, f in ipairs(flags) do + if f.args then + table.insert(actual_flags, f.flag .. f.args) + else + table.insert(actual_flags, f.flag) + end + end + + argmatcher:addflags(actual_flags) + argmatcher:adddescriptions(descriptions) + argmatcher:hideflags(hideflags) +end + +-------------------------------------------------------------------------------- +local _inited = {} +local function make(command, help_flag, parser, config, closure) + clink.argmatcher(command):setdelayinit(function (argmatcher) + if not _inited[command] then + _inited[command] = true + run(argmatcher, parser, help_flag and (command .. ' ' .. help_flag) or command, config) + if type(closure) == "function" then + closure(argmatcher) + end + end + end) +end + +-------------------------------------------------------------------------------- +return { + run=run, + make=make, + add_pending=add_pending, +} diff --git a/modules/matchers.lua b/modules/matchers.lua index 728cf2f..d0f209d 100644 --- a/modules/matchers.lua +++ b/modules/matchers.lua @@ -5,6 +5,10 @@ local exports = {} local path = require('path') local w = require('tables').wrap +-- A function to generate directory matches. +-- +-- local matchers = require("matchers") +-- clink.argmatcher():addarg(matchers.dirs) exports.dirs = function(word) -- Strip off any path components that may be on text. local prefix = "" @@ -34,6 +38,10 @@ exports.dirs = function(word) return matches end +-- A function to generate file matches. +-- +-- local matchers = require("matchers") +-- clink.argmatcher():addarg(matchers.files) exports.files = function (word) if clink_version.supports_display_filter_description then local matches = w(clink.filematches(word)) @@ -64,26 +72,25 @@ exports.files = function (word) return matches end -exports.create_dirs_matcher = function (dir_pattern, show_dotfiles) - return function (token) - return w(clink.find_dirs(dir_pattern)) - :filter(function(dir) - return clink.is_match(token, dir) and (path.is_real_dir(dir) or show_dotfiles) - end ) - end -end - -exports.create_files_matcher = function (file_pattern) - return function (token) - return w(clink.find_files(file_pattern)) - :filter(function(file) - -- Filter out '.' and '..' entries as well - return clink.is_match(token, file) and path.is_real_dir(file) - end ) +-- Returns a function that generates matches for the specified wildcards. +-- +-- local matchers = require("matchers") +-- clink.argmatcher():addarg(matchers.ext_files("*.json")) +exports.ext_files = function (...) + local wildcards = {...} + + if clink.argmatcher then + return function (word) + local matches = clink.dirmatches(word.."*") + for _, wild in ipairs(wildcards) do + for _, m in ipairs(clink.filematches(word..wild)) do + table.insert(matches, m) + end + end + return matches + end end -end -exports.ext_files = function (extension) return function (word) -- Strip off any path components that may be on text. @@ -93,35 +100,52 @@ exports.ext_files = function (extension) prefix = word:sub(1, i) end - -- dir matches. - local dirmatches = w(clink.find_dirs(word.."*", true)) + -- Find directories. + local matches = w(clink.find_dirs(word.."*", true)) :filter(function (dir) - return clink.is_match(word, prefix..dir) and - (include_dots or path.is_real_dir(dir)) + return clink.is_match(word, prefix..dir) and path.is_real_dir(dir) end) :map(function(dir) return prefix..dir end) - -- extension matches. (e.g. *.dll) - local dllmatches = w(clink.find_files(word..extension, true)) - :filter(function (file) - return clink.is_match(word, prefix..file) - end) - :map(function(file) - return prefix..file - end) - - for _,v in ipairs(dirmatches) do - table.insert(dllmatches, v) + -- Find wildcard matches (e.g. *.dll). + for _, wild in ipairs(wildcards) do + local filematches = w(clink.find_files(word..wild, true)) + :filter(function (file) + return clink.is_match(word, prefix..file) + end) + :map(function(file) + return prefix..file + end) + matches = matches:concat(filematches) end -- Tell readline that matches are files and it will do magic. - if #dllmatches ~= 0 then + if #matches ~= 0 then clink.matches_are_files() end - return dllmatches + return matches + end +end + +exports.create_dirs_matcher = function (dir_pattern, show_dotfiles) + return function (token) + return w(clink.find_dirs(dir_pattern)) + :filter(function(dir) + return clink.is_match(token, dir) and (path.is_real_dir(dir) or show_dotfiles) + end ) + end +end + +exports.create_files_matcher = function (file_pattern) + return function (token) + return w(clink.find_files(file_pattern)) + :filter(function(file) + -- Filter out '.' and '..' entries as well + return clink.is_match(token, file) and path.is_real_dir(file) + end ) end end diff --git a/modules/multicharflags.lua b/modules/multicharflags.lua new file mode 100644 index 0000000..ed824b8 --- /dev/null +++ b/modules/multicharflags.lua @@ -0,0 +1,202 @@ +-------------------------------------------------------------------------------- +-- Helpers to add multi-character flags to an argmatcher. +-- +-- This makes it easy to add flags like `dir /o:nge` and be able to provide +-- completions on the fly even after `dir /o:ng` has been typed. +-- +-- local mcf = require('multicharflags') +-- +-- local sortflags = clink.argmatcher() +-- mcf.addcharflagsarg(sortflags, { +-- { 'n', 'By name (alphabetic)' }, +-- { 'e', 'By extension (alphabetic)' }, +-- { 'g', 'Group directories first' }, +-- { 's', 'By size (smallest first)' }, +-- { 'd', 'By name (alphabetic)' }, +-- { '-', 'Prefix to reverse order' }, +-- }) +-- clink.argmatcher('dir'):addflags('/o:'..sortflags) +-- +-- The exported functions are: +-- +-- local mcf = require('multicharflags') +-- +-- mcf.addcharflagsarg(argmatcher, chars_table) +-- This adds an arg to argmatcher, for the character flags listed in +-- chars_table (each table element is a sub-table with two fields, the +-- character and its description). It returns argmatcher. +-- +-- mcf.setcharflagsclassifier(argmatcher, chars_table) +-- This makes and sets a classifier for the character flags. This is +-- for specialized scenarios, and is not normally needed because +-- addcharflagsarg() automatically does this. +-- +-- mcf.makecharflagsclassifier(chars_table) +-- Makes a character flags classifier. This is for specialized +-- scenarios, and is not normally needed because addcharflagsarg() +-- automatically does this. + +if not clink then + -- E.g. some unit test systems will run this module *outside* of Clink. + return +end + +-------------------------------------------------------------------------------- +local function index_chars_table(t) + if not t.indexed then + if t.caseless then + for _, m in ipairs(t) do + t[m[1]:lower()] = true + t[m[1]:upper()] = true + end + else + for _, m in ipairs(t) do + t[m[1]] = true + end + end + t.indexed = true + end +end + +local function compound_matches_func(chars, word, -- luacheck: no unused args, no unused + word_index, line_state, builder, user_data) -- luacheck: no unused + local info = line_state:getwordinfo(word_index) + if not info then + return {} + end + + -- local used = {} + local available = {} + + if chars.caseless then + for _, m in ipairs(chars) do + available[m[1]:lower()] = true + available[m[1]:upper()] = true + end + else + for _, m in ipairs(chars) do + available[m[1]] = true + end + end + + word = line_state:getline():sub(info.offset, line_state:getcursor() - 1) + + for _, m in ipairs(chars) do + if chars.caseless then + local l = m[1]:lower() + local u = m[1]:upper() + if word:find(l, 1, true--[[plain]]) or word:find(u, 1, true--[[plain]]) then + -- used[l] = true + -- used[u] = true + available[l] = false + available[u] = false + end + else + local c = m[1] + if word:find(c, 1, true--[[plain]]) then + -- used[c] = true + available[c] = false + end + end + end + + local last_c + if #word > 0 then + last_c = word:sub(-1) + else + last_c = line_state:getline():sub(info.offset - 1, info.offset - 1) + end + available['+'] = chars['+'] and last_c ~= '+' and last_c ~= '-' + available['-'] = chars['-'] and last_c ~= '+' and last_c ~= '-' + + local matches = { nosort=true } + for _, m in ipairs(chars) do + local c = m[1] + if available[c] then + table.insert(matches, { match=word..c, display='\x1b[m'..c, description=m[2], suppressappend=true }) + end + end + + if builder.setvolatile then + builder:setvolatile() + elseif clink._reset_generate_matches then + clink._reset_generate_matches() + end + + return matches +end + +local function get_bad_color() + local bad = settings.get('color.unrecognized') + if not bad or bad == '' then + bad = '91' + end + return bad +end + +local function compound_classifier(chars, arg_index, -- luacheck: no unused args + word, word_index, line_state, classifications) + local info = line_state:getwordinfo(word_index) + if not info then + return + end + + local bad + local good = settings.get('color.arg') + + for i = 1, #word do + local c = word:sub(i, i) + if chars[c] then + classifications:applycolor(info.offset + i - 1, 1, good) + else + bad = bad or get_bad_color() + classifications:applycolor(info.offset + i - 1, 1, bad) + end + end + + if chars['+'] then + local plus_pos = info.offset + info.length + if line_state:getline():sub(plus_pos, plus_pos) == '+' then + classifications:applycolor(plus_pos, 1, good) + end + end +end + +local function make_char_flags_classifier(chars) + local function classifier_func(...) + compound_classifier(chars, ...) + end + return classifier_func +end + +local function set_char_flags_classifier(argmatcher, chars) + if argmatcher.setclassifier then + argmatcher:setclassifier(make_char_flags_classifier(chars)) + end + return argmatcher +end + +local function add_char_flags_arg(argmatcher, chars, ...) + index_chars_table(chars) + + local matches_func = function (...) + return compound_matches_func(chars, ...) + end + + local t = { matches_func } + if chars['+'] then + t.loopchars = '+' + end + + argmatcher:addarg(t, ...) + set_char_flags_classifier(argmatcher, chars) + + return argmatcher +end + +-------------------------------------------------------------------------------- +return { + addcharflagsarg = add_char_flags_arg, + makecharflagsclassifier = make_char_flags_classifier, + setcharflagsclassifier = set_char_flags_classifier, +} diff --git a/msbuild.lua b/msbuild.lua new file mode 100644 index 0000000..6af8a45 --- /dev/null +++ b/msbuild.lua @@ -0,0 +1,247 @@ +-------------------------------------------------------------------------------- +-- Usage: +-- +-- This builds an argmatcher for MSBUILD. +-- +-- It also defines a global msbuild_parser_data table which contains two tables +-- that other scripts can use to add MSBUILD flags to their own argmatchers: +-- +-- msbuild_parser_data.exflags +-- msbuild_parser_data.hideflags +-- Table of flags for :_addexflags() and :hideflags(), to add all flag +-- forms (/, -, --) and hide short form flags and all -- flags. +-- +-- msbuild_parser_data.exflags_onlyslash +-- msbuild_parser_data.hideflags_onlyslash +-- Table of flags for :_addexflags() and :hideflags(), to add only / flags +-- and hide short form flags. +-- +-- msbuild_parser_data.exflags_onlyminus +-- msbuild_parser_data.hideflags_onlyminus +-- Table of flags for :_addexflags() and :hideflags(), to add only - flags +-- and hide short form flags. +-- +-- msbuild_parser_data.exflags_onlyminusminus +-- msbuild_parser_data.hideflags_onlyminusminus +-- Table of flags for :_addexflags() and :hideflags(), to add only -- flags +-- and hide short form flags. +-- +-- Because of the global msbuild_parser_data table, this script should be +-- located in a normal script directory, not in a completions subdirectory. + +local clink_version = require('clink_version') +if not clink_version.new_api then + return +end + +--[[ +// vim: set et: +--]] +require('arghelper') + +-- luacheck: no max line length + +-- This is a global so that other scripts can add the tables into their own +-- argmatchers, e.g. for use with scripts that wrap msbuild with additional +-- functionality. + +-- luacheck: globals msbuild_parser_data +msbuild_parser_data = {} + +local binlog = clink.argmatcher():addarg({ fromhistory=true }) +local codes = clink.argmatcher():addarg({ fromhistory=true }) +local clparams = clink.argmatcher():_addexarg({ + { 'PerformanceSummary', 'Show time spent in tasks, targets and projects' }, + { 'Summary', 'Show error and warning summary at the end' }, + { 'NoSummary', 'Don\'t show error and warning summary at the end' }, + { 'ErrorsOnly', 'Show only errors' }, + { 'WarningsOnly', 'Show only warnings' }, + { 'NoItemAndPropertyList', 'Don\'t show list of items and properties at the start of each project build' }, + { 'ShowCommandLine', 'Show TaskCommandLineEvent message' }, + { 'ShowTimestamp', 'Display the Timestamp as a prefix to any message' }, + { 'ShowEventId', 'Show eventId for started events, finished events, and message' }, + { 'ForceNoAlign', 'Does not align the text to the size of the console buffe' }, + { 'DisableConsoleColor', 'Use the default console colors for all logging messages' }, + { 'DisableMPLogging', ' Disable the multiprocessor logging style of output when running in non-multiprocessor mode' }, + { 'EnableMPLogging', 'Enable the multiprocessor logging style even when running in non-multiprocessor mode. This logging style is on by default' }, + { 'ForceConsoleColor', 'Use ANSI console colors even if console does not support i' }, + { 'Verbosity', 'overrides the -verbosity setting for this logger' }, +}) +local cpus = clink.argmatcher():addarg({ fromhistory=true, '2', '3', '4', '6', '8', '10' }) +local dlparams = clink.argmatcher():addarg({ fromhistory=true }) +local flparams = clink.argmatcher():addarg({ fromhistory=true }) +local exts = clink.argmatcher():addarg({ fromhistory=true }) +local filelist = clink.argmatcher():addarg(clink.filematches) +local files = clink.argmatcher():addarg(clink.filematches) +local logger = clink.argmatcher():addarg({ fromhistory=true }) +local neqv = clink.argmatcher():addarg({ fromhistory=true }) +local schema = clink.argmatcher():addarg({ fromhistory=true }) +local targets = clink.argmatcher():addarg({ fromhistory=true, "clean" }) +local tf = clink.argmatcher():addarg({ 'true', 'false' }) +local tver = clink.argmatcher():addarg({ fromhistory=true }) +local vlevel = clink.argmatcher():addarg({ + 'q', 'm', 'n', 'd', 'diag', + 'quiet', 'minimal', 'normal', 'detailed', 'diagnostic', +}):hideflags({ + 'q', 'm', 'n', 'd', 'diag', +}) + +local displays = { + [binlog] = 'params', + [codes] = 'code[;...]', + [clparams] = 'params', + [cpus] = 'num', + [dlparams] = 'params', + [flparams] = 'params', + [exts] = '.ext[;...]', + [filelist] = 'file[;...]', + [files] = 'file', + [logger] = 'logger', + [neqv] = 'n=v[;...]', + [schema] = 'schema', + [targets] = 'target[;...]', + [tf] = 'True|False', + [tver] = 'version', + [vlevel] = 'level', +} + +local source = { + { { 't:', 'target:', targets }, 'Build these targets in this project' }, + { { 'p:', 'property:', neqv }, 'Set or override these project-level properties' }, + { { 'm', 'maxCpuCount' }, 'Build with concurrent processes, up to the number of processors on the computer' }, + { { 'm:', 'maxCpuCount:', cpus }, 'Specify the maximum number of concurrent processes to build with' }, + { { 'tv:', 'toolsVersion:', tver }, 'Override version of the MSBuild toolset to use during build' }, + { { 'v:', 'verbosity:', vlevel }, 'Display this amount of information to the event log' }, + { { 'clp:', 'consoleLoggerParameters:', clparams }, 'Parameters to console logger' }, + { { 'noConLog', 'noConsoleLogger' }, 'Disable the default console logger and do not log events to the console' }, + { { 'fl1', 'fileLogger1', + 'fl2', 'fileLogger2', + 'fl3', 'fileLogger3', + 'fl4', 'fileLogger4', + 'fl5', 'fileLogger5', + 'fl6', 'fileLogger6', + 'fl7', 'fileLogger7', + 'fl8', 'fileLogger8', + 'fl9', 'fileLogger9', + 'fl', 'fileLogger' }, 'Logs the build output to a file' }, + { { 'flp1', 'fileLoggerParameters1', + 'flp2', 'fileLoggerParameters2', + 'flp3', 'fileLoggerParameters3', + 'flp4', 'fileLoggerParameters4', + 'flp5', 'fileLoggerParameters5', + 'flp6', 'fileLoggerParameters6', + 'flp7', 'fileLoggerParameters7', + 'flp8', 'fileLoggerParameters8', + 'flp9', 'fileLoggerParameters9', + 'flp', 'fileLoggerParameters', flparams }, 'Provide extra parameters for file loggers' }, + { { 'dl:', 'distributedLogger:', dlparams }, 'Use this logger(s) to log events from MSBuild, one instance per node' }, + { { 'distributedFileLogger' }, 'Log build output to one log file per MSBuild node' }, + { { 'l:', 'logger:', logger }, 'Use this logger(s) to log events from MSBuild' }, + { { 'bl', 'binaryLogger' }, 'Use a compressed binary log file (see https://aka.ms/msbuild/binlog)' }, + { { 'bl:', 'binaryLogger:', binlog }, 'Use a compressed binary log file (see https://aka.ms/msbuild/binlog)' }, + { { 'err', 'warnAsError' }, 'Treat warning codes as errors' }, + { { 'err:', 'warnAsError:', codes }, 'List of warning codes to treat as errors' }, + { { 'noWarn', 'warnAsMessage' }, 'Treat warning codes as low importance messages' }, + { { 'noWarn:', 'warnAsMessage:', codes }, 'List of warning codes to treat as low importance messages' }, + { { 'val', 'validate' }, 'Validate the project against the default schema' }, + { { 'val:', 'validate:', schema }, 'Validate the project against the specified schema (e.g. xsd file)' }, + { { 'ignore:', 'ignoreProjectExtensions:', exts }, 'List of extensions to ignore when determining which project file to build' }, + { { 'nr:', 'nodeReuse:', tf }, 'Enable or disable the reuse of MSBuild nodes after the build completes' }, + { { 'pp', 'preprocess' }, 'Write to stdout an aggregated project file by inlining all the files that would be imported, with their boundaries marked' }, + { { 'pp:', 'preprocess:', files }, 'Write an aggregated project file by inlining all the files that would be imported, with their boundaries marked' }, + { { 'ts', 'targets' }, 'List the available targets without executing the actual build process' }, + { { 'ts:', 'targets:', files }, 'Write to the specified file a list of available targets without executing the actual build process' }, + { { 'ds', 'detailedSummary' }, 'Show detailed information at the end of the build' }, + { { 'ds:', 'detailedSummary:', tf }, 'Indicates whether to show detailed information at the end of the build' }, + { { 'r', 'restore' }, 'Runs a target named Restore prior to building other targets and uses the latest restored build logic for them' }, + { { 'r:', 'restore:', tf }, 'Indicates whether to run a target named Restore prior to building other targets and uses the latest restored build logic for them' }, + { { 'rp:', 'restoreProperty:', neqv }, 'Set or overide project-level properties only during restore and do not use properties specified with -property' }, + { { 'profileEvaluation:', files }, 'Profiles MSBuild evaluation and writes the result to the specified file (.md ext for markdown format)' }, + { { 'interactive' }, 'Actions in the build are allowed to interact with the user' }, + { { 'interactive:', tf }, 'Indicates whether actions in the build are allowed to interact with the user' }, + { { 'isolate', 'isolateProjects' }, 'Build each project in isolation' }, + { { 'isolate:', 'isolateProjects:', tf }, 'Indicates whether to build each project in isolation' }, + { { 'irc:', 'inputResultsCaches:', filelist }, 'Semicolon separated list of input cache files that MSBuild will read build results from' }, + { { 'orc:', 'outputResultsCache:', files }, 'Output cache file where MSBuild will write the contents of its build result caches at the end of the build' }, + { { 'graph', 'graphBuild' }, 'Construct and build a project graph' }, + { { 'graph:', 'graphBuild:', tf }, 'Indicates whether to construct and build a project graph' }, + { { 'low', 'lowPriority' }, 'Causes MSBuild to run at low process priority' }, + { { 'low:', 'lowPriority:', tf }, 'True or False causes MSBuild to run at low process priority or not' }, + { { 'noAutoRsp', 'noAutoResponse' }, 'Do not auto-include any MSBuild.rsp files' }, + { { 'noLogo' }, 'Do not display the startup banner and copyright message' }, + { { 'ver', 'version' }, 'Display version information only' }, + { { 'h', 'help' }, 'Display help' }, + { { '?' }, 'Display help' }, +} + +msbuild_parser_data.exflags = {} +msbuild_parser_data.exflags_onlyslash = {} +msbuild_parser_data.exflags_onlyminus = {} +msbuild_parser_data.exflags_onlyminusminus = {} +msbuild_parser_data.hideflags = {} +msbuild_parser_data.hideflags_onlyslash = {} +msbuild_parser_data.hideflags_onlyminus = {} +msbuild_parser_data.hideflags_onlyminusminus = {} + +local function make_exflag(flag, linked, desc) + local exflag = {} + + -- Flag. + if flag:sub(-1) ~= ':' then + linked = nil + end + table.insert(exflag, linked and flag..linked or flag) + + -- Arg info. + local display = linked and displays[linked] + if display then + desc = desc or '' + table.insert(exflag, display) + end + + -- Description. + if desc then + table.insert(exflag, desc) + end + + return exflag +end + +for _,e in ipairs(source) do + local flags = e[1] + local desc = e[2] + + local num = #flags + local linked = type(flags[num]) == 'table' and flags[num] + if linked then + num = num - 1 + end + + local hidenum = num - 1 + for i = 1, hidenum do + table.insert(msbuild_parser_data.hideflags, '/'..flags[i]) + table.insert(msbuild_parser_data.hideflags_onlyslash, '/'..flags[i]) + table.insert(msbuild_parser_data.hideflags, '-'..flags[i]) + table.insert(msbuild_parser_data.hideflags_onlyminus, '-'..flags[i]) + table.insert(msbuild_parser_data.hideflags, '--'..flags[i]) + table.insert(msbuild_parser_data.hideflags_onlyminusminus, '--'..flags[i]) + end + table.insert(msbuild_parser_data.hideflags, '--'..flags[num]) + + local exflag + for i = 1, num do + exflag = make_exflag('/'..flags[i], linked, desc) + table.insert(msbuild_parser_data.exflags, exflag) + table.insert(msbuild_parser_data.exflags_onlyslash, exflag) + exflag = make_exflag('-'..flags[i], linked, desc) + table.insert(msbuild_parser_data.exflags, exflag) + table.insert(msbuild_parser_data.exflags_onlyminus, exflag) + exflag = make_exflag('--'..flags[i], linked, desc) + table.insert(msbuild_parser_data.exflags, exflag) + table.insert(msbuild_parser_data.exflags_onlyminusminus, exflag) + end +end + +clink.argmatcher('msbuild') +:_addexflags(msbuild_parser_data.exflags) +:hideflags(msbuild_parser_data.hideflags) diff --git a/vagrant.lua b/vagrant.lua index e016952..c91e390 100644 --- a/vagrant.lua +++ b/vagrant.lua @@ -36,7 +36,7 @@ end local function delete_ruby_comment(line) if line == nil then return nil end local index = string.find(line, '#') - if (not (index == nil) and index > 0) then + if index and index > 0 then return string.sub(line, 0, index-1) end return line diff --git a/yarn.lua b/yarn.lua index d630d40..ac53899 100644 --- a/yarn.lua +++ b/yarn.lua @@ -7,6 +7,8 @@ function JSON:assert () end -- luacheck: no unused args local w = require('tables').wrap local matchers = require('matchers') +local color = require('color') +local clink_version = require('clink_version') --- -- Queries config options value using 'yarn config get' call @@ -48,12 +50,13 @@ local function global_modules(token) return global_modules_matcher(token) end --- A function that matches all files in bin folder. See #74 for rationale -local bins = matchers.create_files_matcher('node_modules/.bin/*.') +-------------------------------------------------------------------------------- +-- Let `yarn run` match files in bin folder (see #74 for why) and package.json +-- scripts. When using newer versions of Clink, it is also able to colorize the +-- matches. -- Reads package.json in current directory and extracts all "script" commands defined -local function scripts(token) -- luacheck: no unused args - +local function package_scripts() -- Read package.json first local package_json = io.open('package.json') -- If there is no such file, then close handle and return @@ -63,8 +66,7 @@ local function scripts(token) -- luacheck: no unused args local package_contents = package_json:read("*a") package_json:close() - local package_scripts = JSON:decode(package_contents).scripts - return w(package_scripts):keys() + return JSON:decode(package_contents).scripts end local parser = clink.arg.new_parser @@ -72,6 +74,55 @@ local parser = clink.arg.new_parser -- end preamble +local yarn_run_matches +local yarn_run_matches_parser +if not clink_version.supports_display_filter_description then + yarn_run_matches = function () + local bins = w(matchers.create_files_matcher('node_modules/.bin/*.')('')) + return bins:concat(w(package_scripts()):keys()) + end + yarn_run_matches_parser = parser({yarn_run_matches}) +else + settings.add('color.yarn.module', 'bright green', 'Color for yarn run local module', + 'Used when Clink displays yarn run local module completions.') + settings.add('color.yarn.script', 'bright blue', 'Color for yarn run project.json script', + 'Used when Clink displays yarn run project.json script completions.') + + yarn_run_matches = function () + local bin_matches = w(os.globfiles('node_modules/.bin/*.')) + local bin_index = {} + for _, m in ipairs(bin_matches) do + bin_index[m] = true + end + bin_matches = bin_matches:map(function (m) + return { match=m } + end) + + local scripts = w(package_scripts()) + + if clink_version.supports_query_rl_var and rl.isvariabletrue('colored-stats') then + clink.ondisplaymatches(function (matches) + local bc = color.get_clink_color('color.yarn.module') + local sc = color.get_clink_color('color.yarn.script') + return w(matches):map(function (match) + local m = match.match + if scripts[m] then + match.display = sc..m + elseif bin_index[m] then + match.display = bc..m + end + return match + end) + end) + end + + return bin_matches:concat(scripts:keys()) + end + + yarn_run_matches_parser = clink.argmatcher():addarg({yarn_run_matches}) +end + + local add_parser = parser( "--dev", "-D", "--exact", "-E", @@ -124,7 +175,7 @@ local yarn_parser = parser({ "--tag" ), "remove"..parser({modules}), - "run"..parser({bins, scripts}), + "run"..yarn_run_matches_parser, "self-update", "tag"..parser({"add", "ls", "rm"}), "team"..parser({"add", "create", "destroy", "ls", "rm"}), @@ -139,8 +190,7 @@ local yarn_parser = parser({ ), "versions", "why"..parser({modules}), - bins, - scripts + yarn_run_matches, }, "-h", "-v",