mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 11:28:22 +00:00
238 lines
5.6 KiB
Lua
238 lines
5.6 KiB
Lua
-- 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
|