neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

_ssh.lua (5722B)


      1 -- Converted into Lua from https://github.com/cyjake/ssh-config
      2 -- TODO (siddhantdev): deal with include directives
      3 
      4 local M = {}
      5 
      6 local whitespace_pattern = '%s'
      7 local line_break_pattern = '[\r\n]'
      8 
      9 ---@param param string
     10 local function is_multi_value_directive(param)
     11  local multi_value_directives = {
     12    'globalknownhostsfile',
     13    'host',
     14    'ipqos',
     15    'sendenv',
     16    'userknownhostsfile',
     17    'proxycommand',
     18    'match',
     19    'canonicaldomains',
     20  }
     21 
     22  return vim.list_contains(multi_value_directives, param:lower())
     23 end
     24 
     25 ---@param text string The ssh configuration which needs to be parsed
     26 ---@return string[] The parsed host names in the configuration
     27 function M.parse_ssh_config(text)
     28  local i = 1
     29  local line = 1
     30 
     31  local function consume()
     32    if i <= #text then
     33      local char = text:sub(i, i)
     34      i = i + 1
     35      return char
     36    end
     37    return nil
     38  end
     39 
     40  local chr = consume()
     41 
     42  local function parse_spaces()
     43    local spaces = ''
     44    while chr and chr:match(whitespace_pattern) do
     45      spaces = spaces .. chr
     46      chr = consume()
     47    end
     48    return spaces
     49  end
     50 
     51  local function parse_linebreaks()
     52    local breaks = ''
     53    while chr and chr:match(line_break_pattern) do
     54      line = line + 1
     55      breaks = breaks .. chr
     56      chr = consume()
     57    end
     58    return breaks
     59  end
     60 
     61  local function parse_parameter_name()
     62    local param = ''
     63    while chr and not chr:match('[ \t=]') do
     64      param = param .. chr
     65      chr = consume()
     66    end
     67    return param
     68  end
     69 
     70  local function parse_separator()
     71    local sep = parse_spaces()
     72    if chr == '=' then
     73      sep = sep .. chr
     74      chr = consume()
     75    end
     76    return sep .. parse_spaces()
     77  end
     78 
     79  local function parse_value()
     80    local val = {}
     81    local quoted, escaped = false, false
     82 
     83    while chr and not chr:match(line_break_pattern) do
     84      if escaped then
     85        table.insert(val, chr == '"' and chr or '\\' .. chr)
     86        escaped = false
     87      elseif chr == '"' and (val == {} or quoted) then
     88        quoted = not quoted
     89      elseif chr == '\\' then
     90        escaped = true
     91      elseif chr == '#' and not quoted then
     92        break
     93      else
     94        table.insert(val, chr)
     95      end
     96      chr = consume()
     97    end
     98 
     99    if quoted or escaped then
    100      error('Unexpected line break at line ' .. line)
    101    end
    102 
    103    return vim.trim(table.concat(val))
    104  end
    105 
    106  local function parse_comment()
    107    while chr and not chr:match(line_break_pattern) do
    108      chr = consume()
    109    end
    110  end
    111 
    112  ---@return string[]
    113  local function parse_multiple_values()
    114    local results = {}
    115    local val = {}
    116    local quoted = false
    117    local escaped = false
    118 
    119    while chr and not chr:match(line_break_pattern) do
    120      if escaped then
    121        table.insert(val, chr == '"' and chr or '\\' .. chr)
    122        escaped = false
    123      elseif chr == '"' then
    124        quoted = not quoted
    125      elseif chr == '\\' then
    126        escaped = true
    127      elseif quoted then
    128        table.insert(val, chr)
    129      elseif chr:match('[ \t=]') then
    130        if val ~= {} then
    131          table.insert(results, vim.trim(table.concat(val)))
    132          val = {}
    133        end
    134      elseif chr == '#' and #results > 0 then
    135        break
    136      else
    137        table.insert(val, chr)
    138      end
    139      chr = consume()
    140    end
    141 
    142    if quoted or escaped then
    143      error('Unexpected line break at line ' .. line)
    144    end
    145 
    146    if val ~= {} then
    147      table.insert(results, vim.trim(table.concat(val)))
    148    end
    149 
    150    return results
    151  end
    152 
    153  local function parse_directive()
    154    local param = parse_parameter_name()
    155    local multiple = is_multi_value_directive(param)
    156    local _ = parse_separator()
    157    local value = multiple and parse_multiple_values() or parse_value()
    158 
    159    local result = {
    160      param = param,
    161      value = value,
    162    }
    163 
    164    return result
    165  end
    166 
    167  local function parse_line()
    168    local _ = parse_spaces()
    169    if chr == '#' then
    170      parse_comment()
    171      return nil
    172    end
    173    local node = parse_directive()
    174    local _ = parse_linebreaks()
    175 
    176    return node
    177  end
    178 
    179  local hostnames = {}
    180 
    181  ---@param value string
    182  local function is_valid(value)
    183    return not (value:find('[?*!]') or vim.list_contains(hostnames, value))
    184  end
    185 
    186  while chr do
    187    local node = parse_line()
    188    if node then
    189      -- This is done just to assign the type
    190      node.value = node.value ---@type string[]
    191      if node.param:lower() == 'match' and node.value then
    192        local current = nil
    193        for ind, val in ipairs(node.value) do
    194          if val:lower() == 'host' and ind + 1 <= #node.value and is_valid(node.value[ind + 1]) then
    195            current = node.value[ind + 1]
    196          end
    197        end
    198        if current then
    199          table.insert(hostnames, current)
    200        end
    201      elseif node.param:lower() == 'host' and node.value then
    202        for _, value in ipairs(node.value) do
    203          if is_valid(value) then
    204            table.insert(hostnames, value)
    205          end
    206        end
    207      end
    208    end
    209  end
    210 
    211  return hostnames
    212 end
    213 
    214 ---@param filename string
    215 ---@return string[] The hostnames configured in the file located at filename
    216 function M.parse_config(filename)
    217  local file = io.open(filename, 'r')
    218  if not file then
    219    error('Cannot read ssh configuration file')
    220  end
    221  local config_string = file:read('*a')
    222  file:close()
    223 
    224  return M.parse_ssh_config(config_string)
    225 end
    226 
    227 ---@return string[] The hostnames configured in the ssh configuration file
    228 ---                 located at "~/.ssh/config".
    229 ---                 Note: This does not currently process `Include` directives in the
    230 ---                 configuration file.
    231 function M.get_hosts()
    232  local config_path = vim.fs.normalize('~/.ssh/config') ---@type string
    233 
    234  return M.parse_config(config_path)
    235 end
    236 
    237 return M