diff options
| author | Siddhant Agarwal <68201519+siddhantdev@users.noreply.github.com> | 2025-08-18 09:10:28 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-17 20:40:28 -0700 |
| commit | 1d40f677760d3fbaaaac6292841f2f5426bf83bd (patch) | |
| tree | 152afdc38145063213b23194cdabbcea8e4bd1fb /runtime/lua/vim/net/_ssh.lua | |
| parent | 37119ad0d24f4fb6052a7b1bc8ae2081fed621a2 (diff) | |
| download | rneovim-1d40f677760d3fbaaaac6292841f2f5426bf83bd.tar.gz rneovim-1d40f677760d3fbaaaac6292841f2f5426bf83bd.tar.bz2 rneovim-1d40f677760d3fbaaaac6292841f2f5426bf83bd.zip | |
feat(ssh): SSH configuration parser #35027
Diffstat (limited to 'runtime/lua/vim/net/_ssh.lua')
| -rw-r--r-- | runtime/lua/vim/net/_ssh.lua | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/runtime/lua/vim/net/_ssh.lua b/runtime/lua/vim/net/_ssh.lua new file mode 100644 index 0000000000..3f18213244 --- /dev/null +++ b/runtime/lua/vim/net/_ssh.lua @@ -0,0 +1,237 @@ +-- Converted into Lua from https://github.com/cyjake/ssh-config +-- TODO (siddhantdev): deal with include directives + +local M = {} + +local whitespace_pattern = '%s' +local line_break_pattern = '[\r\n]' + +---@param param string +local function is_multi_value_directive(param) + local multi_value_directives = { + 'globalknownhostsfile', + 'host', + 'ipqos', + 'sendenv', + 'userknownhostsfile', + 'proxycommand', + 'match', + 'canonicaldomains', + } + + return vim.list_contains(multi_value_directives, param:lower()) +end + +---@param text string The ssh configuration which needs to be parsed +---@return string[] The parsed host names in the configuration +function M.parse_ssh_config(text) + local i = 1 + local line = 1 + + local function consume() + if i <= #text then + local char = text:sub(i, i) + i = i + 1 + return char + end + return nil + end + + local chr = consume() + + local function parse_spaces() + local spaces = '' + while chr and chr:match(whitespace_pattern) do + spaces = spaces .. chr + chr = consume() + end + return spaces + end + + local function parse_linebreaks() + local breaks = '' + while chr and chr:match(line_break_pattern) do + line = line + 1 + breaks = breaks .. chr + chr = consume() + end + return breaks + end + + local function parse_parameter_name() + local param = '' + while chr and not chr:match('[ \t=]') do + param = param .. chr + chr = consume() + end + return param + end + + local function parse_separator() + local sep = parse_spaces() + if chr == '=' then + sep = sep .. chr + chr = consume() + end + return sep .. parse_spaces() + end + + local function parse_value() + local val = {} + local quoted, escaped = false, false + + while chr and not chr:match(line_break_pattern) do + if escaped then + table.insert(val, chr == '"' and chr or '\\' .. chr) + escaped = false + elseif chr == '"' and (val == {} or quoted) then + quoted = not quoted + elseif chr == '\\' then + escaped = true + elseif chr == '#' and not quoted then + break + else + table.insert(val, chr) + end + chr = consume() + end + + if quoted or escaped then + error('Unexpected line break at line ' .. line) + end + + return vim.trim(table.concat(val)) + end + + local function parse_comment() + while chr and not chr:match(line_break_pattern) do + chr = consume() + end + end + + ---@return string[] + local function parse_multiple_values() + local results = {} + local val = {} + local quoted = false + local escaped = false + + while chr and not chr:match(line_break_pattern) do + if escaped then + table.insert(val, chr == '"' and chr or '\\' .. chr) + escaped = false + elseif chr == '"' then + quoted = not quoted + elseif chr == '\\' then + escaped = true + elseif quoted then + table.insert(val, chr) + elseif chr:match('[ \t=]') then + if val ~= {} then + table.insert(results, vim.trim(table.concat(val))) + val = {} + end + elseif chr == '#' and #results > 0 then + break + else + table.insert(val, chr) + end + chr = consume() + end + + if quoted or escaped then + error('Unexpected line break at line ' .. line) + end + + if val ~= {} then + table.insert(results, vim.trim(table.concat(val))) + end + + return results + end + + local function parse_directive() + local param = parse_parameter_name() + local multiple = is_multi_value_directive(param) + local _ = parse_separator() + local value = multiple and parse_multiple_values() or parse_value() + + local result = { + param = param, + value = value, + } + + return result + end + + local function parse_line() + local _ = parse_spaces() + if chr == '#' then + parse_comment() + return nil + end + local node = parse_directive() + local _ = parse_linebreaks() + + return node + end + + local hostnames = {} + + ---@param value string + local function is_valid(value) + return not (value:find('[?*!]') or vim.list_contains(hostnames, value)) + end + + while chr do + local node = parse_line() + if node then + -- This is done just to assign the type + node.value = node.value ---@type string[] + if node.param:lower() == 'match' and node.value then + local current = nil + for ind, val in ipairs(node.value) do + if val:lower() == 'host' and ind + 1 <= #node.value and is_valid(node.value[ind + 1]) then + current = node.value[ind + 1] + end + end + if current then + table.insert(hostnames, current) + end + elseif node.param:lower() == 'host' and node.value then + for _, value in ipairs(node.value) do + if is_valid(value) then + table.insert(hostnames, value) + end + end + end + end + end + + return hostnames +end + +---@param filename string +---@return string[] The hostnames configured in the file located at filename +function M.parse_config(filename) + local file = io.open(filename, 'r') + if not file then + error('Cannot read ssh configuration file') + end + local config_string = file:read('*a') + file:close() + + return M.parse_ssh_config(config_string) +end + +---@return string[] The hostnames configured in the ssh configuration file +--- located at "~/.ssh/config". +--- Note: This does not currently process `Include` directives in the +--- configuration file. +function M.get_hosts() + local config_path = vim.fs.normalize('~/.ssh/config') ---@type string + + return M.parse_config(config_path) +end + +return M |