Skip to content

Commit 9318c47

Browse files
scr-oathSheridan C Rawlins
andauthored
Fix the case where reading a line has spaces in it (#43)
* Fix the case where reading a line has spaces in it **NOTE**: There doesn't seem to be any way of doing it with easier std libs other than bufio Buffer or Scanner, but this is meant to be general. therefore, it copies part of fmt.Scanln without tokenizing on space. * Add some comments. Co-authored-by: Sheridan C Rawlins <scr@ouryahoo.com>
1 parent f59cba6 commit 9318c47

File tree

3 files changed

+109
-3
lines changed

3 files changed

+109
-3
lines changed

io/rune-reader.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package io
2+
3+
import (
4+
"io"
5+
"strings"
6+
"unicode/utf8"
7+
)
8+
9+
//UnbufferedRuneReader doesn't attempt to buffer the underlying reader, but does buffer enough to decode utf8 runes.
10+
type UnbufferedRuneReader struct {
11+
reader io.Reader
12+
buf [utf8.UTFMax]byte // used only inside ReadRune
13+
single [1]byte // to read one byte
14+
}
15+
16+
//ReadLine reads a line from any reader.
17+
func ReadLine(reader io.Reader) (string, error) {
18+
rr := ToRuneReader(reader)
19+
var sb strings.Builder
20+
var r rune
21+
var err error
22+
for r, _, err = rr.ReadRune(); err == nil && r != '\n'; r, _, err = rr.ReadRune() {
23+
sb.WriteRune(r)
24+
}
25+
if err == io.EOF && sb.Len() > 0 {
26+
err = nil
27+
}
28+
return sb.String(), err
29+
}
30+
31+
//ToRuneReader Converts reader to an io.RuneReader
32+
func ToRuneReader(reader io.Reader) io.RuneReader {
33+
if ret, ok := reader.(io.RuneReader); ok {
34+
return ret
35+
}
36+
return &UnbufferedRuneReader{
37+
reader: reader,
38+
}
39+
}
40+
41+
func (u *UnbufferedRuneReader) readByte() (b byte, err error) {
42+
n, err := io.ReadFull(u.reader, u.single[:])
43+
if n != 1 {
44+
return 0, err
45+
}
46+
return u.single[0], err
47+
}
48+
49+
//ReadRune reads a single rune, and returns the rune, its byte-length, and possibly an error.
50+
// see the code for fmt.Scanln - which is not public, but which does, but tokenizing on space, which is not desirable.
51+
// The implementation in fmt.Scanln also implements io.RuneScanner, which is not needed here as newlines are discarded.
52+
func (u *UnbufferedRuneReader) ReadRune() (r rune, size int, err error) {
53+
u.buf[0], err = u.readByte()
54+
if err != nil {
55+
return
56+
}
57+
if u.buf[0] < utf8.RuneSelf { // fast check for common ASCII case
58+
r = rune(u.buf[0])
59+
size = 1
60+
return
61+
}
62+
var n int
63+
for n = 1; !utf8.FullRune(u.buf[:n]); n++ {
64+
u.buf[n], err = u.readByte()
65+
if err != nil {
66+
if err == io.EOF {
67+
err = nil
68+
break
69+
}
70+
return
71+
}
72+
}
73+
r, size = utf8.DecodeRune(u.buf[:n])
74+
return
75+
}

io/wrappers.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,7 @@ func IOReaderRead(L *lua.LState) int {
227227
L.Push(lua.LString(data))
228228
return 1
229229
case 'l':
230-
var line lua.LString
231-
_, err := fmt.Fscanln(reader, &line)
230+
line, err := ReadLine(reader)
232231
if err == io.EOF {
233232
L.Push(lua.LNil)
234233
return 1
@@ -237,7 +236,7 @@ func IOReaderRead(L *lua.LState) int {
237236
L.RaiseError("%v", err)
238237
return 0
239238
}
240-
L.Push(line)
239+
L.Push(lua.LString(line))
241240
return 1
242241
}
243242
}

strings/test/test_api.lua

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,36 @@ function TestFields(t)
8282
assert(fields[2] == "b", string.format("%s ~= 'b'", fields[2]))
8383
assert(fields[3] == "c", string.format("%s ~= 'c'", fields[3]))
8484
assert(fields[4] == "d", string.format("%s ~= 'd'", fields[3]))
85+
end
86+
87+
function assert_equal(expected, actual)
88+
assert(expected == actual, string.format([[expected "%s": got "%s"]], expected, actual))
89+
end
90+
91+
function TestReadLine(t)
92+
t:Run("single line", function(t)
93+
local reader = strings.new_reader("single line\n")
94+
local line = reader:read("*l")
95+
assert_equal("single line", line)
96+
line = reader:read("*l")
97+
assert(not line, line)
98+
end)
99+
100+
t:Run("single line no newline", function(t)
101+
local reader = strings.new_reader("single line")
102+
local line = reader:read("*l")
103+
assert_equal("single line", line)
104+
line = reader:read("*l")
105+
assert(not line, line)
106+
end)
107+
108+
t:Run("two lines with spaces in first", function(t)
109+
local reader = strings.new_reader("foo bar\nbaz\n")
110+
local line = reader:read("*l")
111+
assert_equal("foo bar", line)
112+
line = reader:read("*l")
113+
assert_equal("baz", line)
114+
line = reader:read("*l")
115+
assert(not line, line)
116+
end)
85117
end

0 commit comments

Comments
 (0)