@@ -85,23 +85,42 @@ func (m MatchBlocks) Flatten() map[string]any {
8585
8686func mergeIncludedBlocks (matchConditions map [string ]* MatchBlock , blocks MatchBlocks , curBlock string ) {
8787 for _ , block := range blocks {
88- // meaning:
89- // 1. curBlock == "", we can always add all subblocks
90- // 2. if block == "", we can add it to whatever current block is
91- // 3. in all other cases the block criteria must match, or we move on
92- if block .Criteria != curBlock && curBlock != "" && block .Criteria != "" {
88+ if block .Criteria == "" {
89+ // Default block: merge into the current block
90+ existing := matchConditions [curBlock ]
91+ if existing == nil {
92+ existing = & MatchBlock {
93+ Criteria : curBlock ,
94+ Params : map [string ]any {},
95+ Context : block .Context ,
96+ }
97+ matchConditions [curBlock ] = existing
98+ }
99+ if existing .Params == nil {
100+ existing .Params = map [string ]any {}
101+ }
102+ for k , v := range block .Params {
103+ if _ , ok := existing .Params [k ]; ! ok {
104+ existing .Params [k ] = v
105+ }
106+ }
93107 continue
94108 }
95109
96- var existing * MatchBlock
97- if block . Criteria == "" {
98- existing = matchConditions [ curBlock ]
99- } else {
100- existing := matchConditions [ block . Criteria ]
101- if existing == nil {
102- matchConditions [ block . Criteria ] = block
103- continue
110+ // Match block: always add to global map
111+ existing , ok := matchConditions [ block . Criteria ]
112+ if ! ok {
113+ // Create a new Match block
114+ existing = & MatchBlock {
115+ Criteria : block . Criteria ,
116+ Params : map [ string ] any {},
117+ Context : block . Context ,
104118 }
119+ matchConditions [block .Criteria ] = existing
120+ }
121+
122+ if existing .Params == nil {
123+ existing .Params = map [string ]any {}
105124 }
106125
107126 for k , v := range block .Params {
@@ -112,17 +131,147 @@ func mergeIncludedBlocks(matchConditions map[string]*MatchBlock, blocks MatchBlo
112131 }
113132}
114133
115- func ParseBlocks (rootPath string , globPathContent func (string ) (string , error )) (MatchBlocks , error ) {
116- content , err := globPathContent (rootPath )
134+ type (
135+ fileContentFunc func (string ) (content string , err error )
136+ globExpandFunc func (string ) (paths []string , err error )
137+ )
138+
139+ // ParseBlocks parses a single SSH config file and returns the match blocks.
140+ // The filePath should be the actual file path (not a glob pattern).
141+ // For Include directives with glob patterns, use ParseBlocksWithGlob instead.
142+ func ParseBlocks (filePath string , content string ) (MatchBlocks , error ) {
143+ curBlock := & MatchBlock {
144+ Criteria : "" ,
145+ Params : map [string ]any {},
146+ Context : Context {
147+ Path : filePath ,
148+ Range : llx .NewRange (),
149+ curLine : 1 ,
150+ },
151+ }
152+ matchConditions := map [string ]* MatchBlock {
153+ "" : curBlock ,
154+ }
155+
156+ lines := strings .Split (content , "\n " )
157+ for curLineIdx , textLine := range lines {
158+ l , err := ParseLine ([]rune (textLine ))
159+ if err != nil {
160+ return nil , err
161+ }
162+
163+ key := l .key
164+ if key == "" {
165+ continue
166+ }
167+
168+ // handle lower case entries and use proper ssh camel case
169+ if sshKey , ok := SSH_Keywords [strings .ToLower (key )]; ok {
170+ key = sshKey
171+ }
172+
173+ if key == "Include" {
174+ // Include directives are handled by ParseBlocksWithGlob
175+ // This function only parses single files
176+ log .Warn ().Str ("file" , filePath ).Msg ("Include directive found in single-file parser, use ParseBlocksWithGlob instead" )
177+ continue
178+ }
179+
180+ if key == "Match" {
181+ // wrap up context on the previous block
182+ curBlock .Context .Range = curBlock .Context .Range .AddLineRange (uint32 (curBlock .Context .curLine ), uint32 (curLineIdx ))
183+ curBlock .Context .curLine = curLineIdx
184+
185+ // This key is stored in the condition of each block and can be accessed there.
186+ condition := l .args
187+ if b , ok := matchConditions [condition ]; ok {
188+ curBlock = b
189+ } else {
190+ curBlock = & MatchBlock {
191+ Criteria : condition ,
192+ Params : map [string ]any {},
193+ Context : Context {
194+ curLine : curLineIdx + 1 ,
195+ Path : filePath ,
196+ Range : llx .NewRange (),
197+ },
198+ }
199+ matchConditions [condition ] = curBlock
200+ }
201+ continue
202+ }
203+
204+ setParam (curBlock .Params , key , l .args )
205+ }
206+
207+ keys := sortx .Keys (matchConditions )
208+ res := make ([]* MatchBlock , len (keys ))
209+ i := 0
210+ for _ , key := range keys {
211+ res [i ] = matchConditions [key ]
212+ i ++
213+ }
214+
215+ curBlock .Context .Range = curBlock .Context .Range .AddLineRange (uint32 (curBlock .Context .curLine ), uint32 (len (lines )))
216+
217+ return res , nil
218+ }
219+
220+ // ParseBlocksWithGlob parses SSH config files, expanding glob patterns in Include directives.
221+ // It expands globs and calls ParseBlocksWithGlobRecursive for each matched file individually,
222+ func ParseBlocksWithGlob (rootPath string , fileContent fileContentFunc , globExpand globExpandFunc ) (MatchBlocks , error ) {
223+ // First, expand the root path if it's a glob
224+ paths , err := globExpand (rootPath )
117225 if err != nil {
118226 return nil , err
119227 }
120228
229+ // If no paths matched, check if rootPath was a single file (not a glob) or return empty blocks
230+ if len (paths ) == 0 {
231+ // Check if rootPath contains a glob pattern
232+ hasGlob := strings .Contains (rootPath , "*" ) || strings .Contains (rootPath , "?" ) || strings .Contains (rootPath , "[" )
233+ if ! hasGlob {
234+ _ , err := fileContent (rootPath )
235+ if err != nil {
236+ return nil , err
237+ }
238+ }
239+ return MatchBlocks {}, nil
240+ }
241+
242+ // Parse each file individually and collect all blocks
243+ // Each file maintains its own context (path, line numbers)
244+ var allBlocks MatchBlocks
245+
246+ for i , path := range paths {
247+ content , err := fileContent (path )
248+ if err != nil {
249+ if i == 0 && (len (paths ) == 1 || path == rootPath ) {
250+ return nil , err
251+ }
252+ log .Warn ().Err (err ).Str ("path" , path ).Msg ("unable to read file" )
253+ continue
254+ }
255+
256+ blocks , err := ParseBlocksWithGlobRecursive (path , content , fileContent , globExpand )
257+ if err != nil {
258+ log .Warn ().Err (err ).Str ("path" , path ).Msg ("unable to parse file" )
259+ continue
260+ }
261+
262+ allBlocks = append (allBlocks , blocks ... )
263+ }
264+
265+ return allBlocks , nil
266+ }
267+
268+ // ParseBlocksWithGlobRecursive parses a single file and recursively handles Include directives.
269+ func ParseBlocksWithGlobRecursive (filePath string , content string , fileContent fileContentFunc , globExpand globExpandFunc ) (MatchBlocks , error ) {
121270 curBlock := & MatchBlock {
122271 Criteria : "" ,
123272 Params : map [string ]any {},
124273 Context : Context {
125- Path : rootPath ,
274+ Path : filePath ,
126275 Range : llx .NewRange (),
127276 curLine : 1 ,
128277 },
@@ -150,15 +299,31 @@ func ParseBlocks(rootPath string, globPathContent func(string) (string, error))
150299
151300 if key == "Include" {
152301 // FIXME: parse multi-keys properly
153- paths := strings .Split (l .args , " " )
302+ includePaths := strings .Split (l .args , " " )
154303
155- for _ , path := range paths {
156- subBlocks , err := ParseBlocks (path , globPathContent )
304+ for _ , includePath := range includePaths {
305+ // Expand glob pattern if present
306+ expandedPaths , err := globExpand (includePath )
157307 if err != nil {
158- log .Warn ().Err (err ).Msg ("unable to parse Include directive" )
308+ log .Warn ().Err (err ).Str ( "path" , includePath ). Msg ("unable to expand Include directive" )
159309 continue
160310 }
161- mergeIncludedBlocks (matchConditions , subBlocks , curBlock .Criteria )
311+
312+ // Parse each matched file individually
313+ for _ , expandedPath := range expandedPaths {
314+ subContent , err := fileContent (expandedPath )
315+ if err != nil {
316+ log .Warn ().Err (err ).Str ("path" , expandedPath ).Msg ("unable to read included file" )
317+ continue
318+ }
319+
320+ subBlocks , err := ParseBlocksWithGlobRecursive (expandedPath , subContent , fileContent , globExpand )
321+ if err != nil {
322+ log .Warn ().Err (err ).Str ("path" , expandedPath ).Msg ("unable to parse included file" )
323+ continue
324+ }
325+ mergeIncludedBlocks (matchConditions , subBlocks , curBlock .Criteria )
326+ }
162327 }
163328 continue
164329 }
@@ -179,7 +344,7 @@ func ParseBlocks(rootPath string, globPathContent func(string) (string, error))
179344 Params : map [string ]any {},
180345 Context : Context {
181346 curLine : curLineIdx + 1 ,
182- Path : rootPath ,
347+ Path : filePath ,
183348 Range : llx .NewRange (),
184349 },
185350 }
0 commit comments