Skip to content

Commit 490c933

Browse files
authored
Merge pull request #152 from ianlewis/133-feature-ini-file-example
test: add ini example test
2 parents 1939770 + 568c2d9 commit 490c933

6 files changed

Lines changed: 325 additions & 9 deletions

File tree

infix_example_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ func Calculate(root *lexparse.Node[*exprNode]) (float64, error) {
212212
}
213213
}
214214

215+
// Example_infixCalculator demonstrates an infix expression calculator
216+
// using a Pratt parser. It makes use of the ScanningLexer to tokenize
217+
// the input expression and builds an expression tree that is then evaluated
218+
// using the Calculate function.
215219
func Example_infixCalculator() {
216220
r := strings.NewReader(`6.1 * ( 2.8 + 3.2 ) / 7.6 - 2.4`)
217221

ini_example_test.go

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
// Copyright 2025 Ian Lewis
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package lexparse_test
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
"io"
22+
"regexp"
23+
"strings"
24+
25+
"github.com/ianlewis/lexparse"
26+
)
27+
28+
const (
29+
// lexINITypeIden represents an identifier token (key or section name).
30+
lexINITypeIden lexparse.TokenType = iota
31+
32+
// lexINITypeOper represents an operator token.
33+
lexINITypeOper
34+
35+
// lexINITypeValue represents a property value token.
36+
lexINITypeValue
37+
38+
// lexINITypeComment represents a comment token.
39+
lexINITypeComment
40+
)
41+
42+
type iniNodeType int
43+
44+
const (
45+
// iniNodeTypeRoot represents the root node of the INI parse tree.
46+
iniNodeTypeRoot iniNodeType = iota
47+
48+
// iniNodeTypeSection represents a section node in the INI parse tree.
49+
iniNodeTypeSection
50+
51+
// iniNodeTypeProperty represents a property node in the INI parse tree.
52+
iniNodeTypeProperty
53+
)
54+
55+
type iniNode struct {
56+
typ iniNodeType
57+
58+
// sectionName is only used for section nodes.
59+
sectionName string
60+
61+
// propertyName and propertyValue are only used for property nodes.
62+
propertyName string
63+
propertyValue string
64+
}
65+
66+
func (n *iniNode) String() string {
67+
switch n.typ {
68+
case iniNodeTypeRoot:
69+
return "root"
70+
case iniNodeTypeSection:
71+
return fmt.Sprintf("[%s]", n.sectionName)
72+
case iniNodeTypeProperty:
73+
return fmt.Sprintf("%s = %s", n.propertyName, n.propertyValue)
74+
default:
75+
return "<Unknown>"
76+
}
77+
}
78+
79+
var iniIdenRegexp = regexp.MustCompile(`^[A-Za-z0-9]+$`)
80+
81+
var (
82+
errINIIdentifier = errors.New("unexpected identifier")
83+
errINISectionName = errors.New("invalid section name")
84+
errINIPropertyName = errors.New("invalid property name")
85+
)
86+
87+
// lexINI is the initial lexer state for INI files.
88+
//
89+
//nolint:ireturn // returning the generic interface is needed to return the previous value.
90+
func lexINI(_ context.Context, lexer *lexparse.CustomLexer) (lexparse.LexState, error) {
91+
for {
92+
rn := lexer.Peek()
93+
switch rn {
94+
case ' ', '\t', '\r', '\n':
95+
lexer.Discard()
96+
case '[', ']', '=':
97+
return lexparse.LexStateFn(lexINIOper), nil
98+
case ';', '#':
99+
return lexparse.LexStateFn(lexINIComment), nil
100+
case lexparse.EOF:
101+
return nil, io.EOF
102+
default:
103+
return lexparse.LexStateFn(lexINIIden), nil
104+
}
105+
}
106+
}
107+
108+
// lexINIOper lexes an operator token.
109+
//
110+
//nolint:ireturn // returning the generic interface is needed to return the previous value.
111+
func lexINIOper(_ context.Context, lexer *lexparse.CustomLexer) (lexparse.LexState, error) {
112+
oper := lexer.NextRune()
113+
lexer.Emit(lexINITypeOper)
114+
115+
if oper == '=' {
116+
return lexparse.LexStateFn(lexINIValue), nil
117+
}
118+
119+
return lexparse.LexStateFn(lexINI), nil
120+
}
121+
122+
// lexINIIden lexes an identifier token (section name or property key).
123+
//
124+
//nolint:ireturn // returning the generic interface is needed to return the previous value.
125+
func lexINIIden(_ context.Context, lexer *lexparse.CustomLexer) (lexparse.LexState, error) {
126+
if next := lexer.Find([]string{"]", "="}); next != "" {
127+
lexer.Emit(lexINITypeIden)
128+
return lexparse.LexStateFn(lexINIOper), nil
129+
}
130+
131+
return nil, io.ErrUnexpectedEOF
132+
}
133+
134+
// lexINIValue lexes a property value token.
135+
//
136+
//nolint:ireturn // returning the generic interface is needed to return the previous value.
137+
func lexINIValue(_ context.Context, lexer *lexparse.CustomLexer) (lexparse.LexState, error) {
138+
lexer.Find([]string{";", "\n"})
139+
lexer.Emit(lexINITypeValue)
140+
141+
return lexparse.LexStateFn(lexINI), nil
142+
}
143+
144+
// lexINIComment lexes a comment token.
145+
//
146+
//nolint:ireturn // returning the generic interface is needed to return the previous value.
147+
func lexINIComment(_ context.Context, lexer *lexparse.CustomLexer) (lexparse.LexState, error) {
148+
lexer.Find([]string{"\n"})
149+
lexer.Emit(lexINITypeComment)
150+
151+
return lexparse.LexStateFn(lexINI), nil
152+
}
153+
154+
// iniTokenErr formats an error message with token context.
155+
func iniTokenErr(err error, t *lexparse.Token) error {
156+
return fmt.Errorf("%w: %q, line %d, column %d", err,
157+
t.Value, t.Start.Line, t.Start.Column)
158+
}
159+
160+
// parseINIInit is the initial parser state for INI files.
161+
func parseINIInit(_ context.Context, p *lexparse.Parser[*iniNode]) error {
162+
// Replace the root node with a new root node.
163+
_ = p.Replace(&iniNode{
164+
typ: iniNodeTypeRoot,
165+
})
166+
167+
// Create the empty section node for the global section.
168+
_ = p.Push(&iniNode{
169+
typ: iniNodeTypeSection,
170+
sectionName: "",
171+
})
172+
173+
p.PushState(lexparse.ParseStateFn(parseINI))
174+
175+
return nil
176+
}
177+
178+
// parseINI parses the top-level structure of an INI file.
179+
func parseINI(ctx context.Context, p *lexparse.Parser[*iniNode]) error {
180+
t := p.Peek(ctx)
181+
182+
switch t.Type {
183+
case lexINITypeOper:
184+
p.PushState(lexparse.ParseStateFn(parseSection))
185+
case lexINITypeIden:
186+
p.PushState(lexparse.ParseStateFn(parseProperty))
187+
case lexINITypeComment:
188+
_ = p.Next(ctx) // Discard comment
189+
p.PushState(lexparse.ParseStateFn(parseINI))
190+
case lexparse.TokenTypeEOF:
191+
return nil
192+
default:
193+
return iniTokenErr(errINIIdentifier, t)
194+
}
195+
196+
return nil
197+
}
198+
199+
// parseSection parses a section header.
200+
func parseSection(ctx context.Context, parser *lexparse.Parser[*iniNode]) error {
201+
openBracket := parser.Next(ctx)
202+
if openBracket.Type != lexINITypeOper || openBracket.Value != "[" {
203+
return iniTokenErr(errINIIdentifier, openBracket)
204+
}
205+
206+
sectionToken := parser.Next(ctx)
207+
if sectionToken.Type != lexINITypeIden {
208+
return iniTokenErr(errINIIdentifier, sectionToken)
209+
}
210+
211+
closeBracket := parser.Next(ctx)
212+
if closeBracket.Type != lexINITypeOper || closeBracket.Value != "]" {
213+
return iniTokenErr(errINIIdentifier, closeBracket)
214+
}
215+
216+
sectionName := strings.TrimSpace(sectionToken.Value)
217+
218+
// Validate the section name.
219+
if !iniIdenRegexp.MatchString(sectionName) {
220+
return iniTokenErr(errINISectionName, sectionToken)
221+
}
222+
223+
// Create a new node for the section and push it onto the parse tree.
224+
// The current node is now the new section node.
225+
_ = parser.Climb()
226+
_ = parser.Push(&iniNode{
227+
typ: iniNodeTypeSection,
228+
sectionName: sectionName,
229+
})
230+
231+
parser.PushState(lexparse.ParseStateFn(parseINI))
232+
233+
return nil
234+
}
235+
236+
// parseProperty parses a property key-value pair.
237+
func parseProperty(ctx context.Context, parser *lexparse.Parser[*iniNode]) error {
238+
keyToken := parser.Next(ctx)
239+
if keyToken.Type != lexINITypeIden {
240+
return iniTokenErr(errINIIdentifier, keyToken)
241+
}
242+
243+
keyName := strings.TrimSpace(keyToken.Value)
244+
245+
// Validate the property name.
246+
if !iniIdenRegexp.MatchString(keyName) {
247+
return iniTokenErr(errINIPropertyName, keyToken)
248+
}
249+
250+
eqToken := parser.Next(ctx)
251+
if eqToken.Type != lexINITypeOper || eqToken.Value != "=" {
252+
return iniTokenErr(errINIIdentifier, eqToken)
253+
}
254+
255+
valueToken := parser.Next(ctx)
256+
if valueToken.Type != lexINITypeValue {
257+
return iniTokenErr(errINIIdentifier, valueToken)
258+
}
259+
260+
// Create a new node for the property and add it to the current section.
261+
parser.Node(&iniNode{
262+
typ: iniNodeTypeProperty,
263+
propertyName: keyName,
264+
propertyValue: strings.TrimSpace(valueToken.Value),
265+
})
266+
267+
parser.PushState(lexparse.ParseStateFn(parseINI))
268+
269+
return nil
270+
}
271+
272+
// Example_iniParser demonstrates parsing a simple INI file. It does not support
273+
// nested sections, or escape sequences.
274+
func Example_iniParser() {
275+
r := strings.NewReader(`; last modified 1 April 2001 by John Doe
276+
[owner]
277+
name = John Doe
278+
organization = Acme Widgets Inc.
279+
280+
[database]
281+
; use IP address in case network name resolution is not working
282+
server = 192.0.2.62
283+
port = 143
284+
file = "payroll.dat"
285+
`)
286+
287+
// Produces a tree representation of the INI file.
288+
// Each child of the root node is a section node, which in turn
289+
// has property nodes as children. The global section is represented
290+
// as a section node with an empty name.
291+
tree, err := lexparse.LexParse(
292+
context.Background(),
293+
lexparse.NewCustomLexer(r, lexparse.LexStateFn(lexINI)),
294+
lexparse.ParseStateFn(parseINIInit),
295+
)
296+
if err != nil {
297+
panic(err)
298+
}
299+
300+
fmt.Print(tree)
301+
302+
// Output:
303+
// root (0:0)
304+
// ├── [] (0:0)
305+
// ├── [owner] (2:7)
306+
// │ ├── name = John Doe (3:7)
307+
// │ └── organization = Acme Widgets Inc. (4:15)
308+
// └── [database] (6:10)
309+
// ├── server = 192.0.2.62 (8:9)
310+
// ├── port = 143 (9:7)
311+
// └── file = "payroll.dat" (10:7)
312+
}

lexer.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ func (t Token) String() string {
7575
}
7676

7777
// Lexer is an interface that defines the methods for a lexer that tokenizes
78-
// input streams. It reads from an input stream and emits [Token]s.
78+
// input streams. It reads from an input stream and emits tokens.
7979
type Lexer interface {
8080
// NextToken returns the next token from the input. If there are no more
8181
// tokens, the context is canceled, or an error occurs, it returns a Token
82-
// with Type set to [TokenTypeEOF].
82+
// with Type set to TokenTypeEOF.
8383
NextToken(ctx context.Context) *Token
8484

8585
// Err returns the error encountered by the lexer, if any. If the error
86-
// encountered is [io.EOF], it will return nil.
86+
// encountered is io.EOF, it will return nil.
8787
Err() error
8888
}

parser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func (s *stack[V]) pop() ParseState[V] {
113113
// TokenSource is an interface that defines a source of tokens for the parser.
114114
type TokenSource interface {
115115
// NextToken returns the next token from the source. When tokens are
116-
// exhausted, it returns a Token with Type set to [TokenTypeEOF].
116+
// exhausted, it returns a Token with Type set to TokenTypeEOF.
117117
NextToken(ctx context.Context) *Token
118118
}
119119

scanner.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func NewScanningLexer(r io.Reader) *ScanningLexer {
7878
return &l
7979
}
8080

81-
// NextToken implements [Lexer.NextToken]. It returns the next token from
81+
// NextToken implements Lexer.NextToken. It returns the next token from
8282
// the input stream.
8383
func (l *ScanningLexer) NextToken(_ context.Context) *Token {
8484
if l.err != nil {
@@ -88,7 +88,7 @@ func (l *ScanningLexer) NextToken(_ context.Context) *Token {
8888
return l.newToken(TokenType(l.s.Scan()))
8989
}
9090

91-
// Err implements [Lexer.Err]. It returns the first error encountered by
91+
// Err implements Lexer.Err. It returns the first error encountered by
9292
// the lexer, if any.
9393
func (l *ScanningLexer) Err() error {
9494
return l.err

template_example_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func lexCode(_ context.Context, l *lexparse.CustomLexer) (lexparse.LexState, err
134134
case symbolRegexp.MatchString(string(rn)):
135135
return lexparse.LexStateFn(lexSymbol), nil
136136
default:
137-
return nil, fmt.Errorf("code: %w: %q; line: %d, column: %d", errRune,
137+
return nil, fmt.Errorf("%w: %q; line: %d, column: %d", errRune,
138138
rn, l.Pos().Line, l.Pos().Column)
139139
}
140140
}
@@ -529,8 +529,8 @@ func execNode(root *lexparse.Node[*tmplNode], data map[string]string, bldr *stri
529529
// (e.g. `{{ var }}`) with data values for those variables.
530530
//
531531
// LexParse is used to lex and parse the template into a parse tree. This tree
532-
// can be passed with a data map to the Execute function to interpret the template
533-
// and retrieve a final result.
532+
// can be passed with a data map to the Execute function to interpret the
533+
// template and retrieve a final result.
534534
//
535535
// This example includes some best practices for error handling, such as
536536
// including line and column numbers in error messages.

0 commit comments

Comments
 (0)