mirror of
https://github.com/neovim/neovim.git
synced 2026-02-05 19:37:21 +00:00
feat(ssh): SSH configuration parser #35027
This commit is contained in:
237
runtime/lua/vim/net/_ssh.lua
Normal file
237
runtime/lua/vim/net/_ssh.lua
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user