// package asciiscan is used to parse plaintext files into structured data package asciiscan import ( "bufio" "bytes" "fmt" "io" "os" "strconv" "strings" ) var hashComment = []byte("#") type Scanner struct { bufio *bufio.Scanner closer io.Closer // If we've peeked, there will be items here buffered []string } func New(filename string) (*Scanner, error) { f, err := os.Open(filename) if err != nil { return nil, err } return &Scanner{ bufio: bufio.NewScanner(f), closer: f, }, nil } func (s *Scanner) Bufio() *bufio.Scanner { return s.bufio } func (s *Scanner) Close() error { return s.closer.Close() } func (s *Scanner) ConsumeString() (string, error) { if len(s.buffered) > 0 { out, buffered := s.buffered[0], s.buffered[1:] s.buffered = buffered return out, nil } for s.bufio.Scan() { line := s.bufio.Bytes() if len(line) == 0 || line[0] == hashComment[0] { // Most .dat files use # for comments continue } comment := bytes.Index(line, hashComment) if comment > 0 { line = line[0:comment] } return string(bytes.TrimRight(line, "\r\n\t ")), nil } err := s.bufio.Err() if err == nil { return "", io.EOF } return "", err } // It's common for properties to be specified as "foo : bar". Parse them out. func ConsumeProperty(s string) (string, string) { if !IsProperty(s) { return "", "" } parts := strings.SplitN(s, ":", 2) return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) } // Check to see if the line looks like a property (contains a colon character). func IsProperty(s string) bool { return strings.Contains(s, ":") } // Checks if the next line might be a property, without reading it func (s *Scanner) PeekProperty() (bool, error) { str, err := s.ConsumeString() if err != nil { return false, err } s.buffered = append(s.buffered, str) return IsProperty(str), nil } func (s *Scanner) ConsumeProperty() (string, string, error) { str, err := s.ConsumeString() if err != nil { return "", "", err } if !IsProperty(str) { return "", "", fmt.Errorf("Not a property: %q", str) } k, v := ConsumeProperty(str) return k, v, nil } func (s *Scanner) ConsumeInt() (int, error) { str, err := s.ConsumeString() if err != nil { return 0, err } return strconv.Atoi(str) } func (s *Scanner) ConsumeBool() (bool, error) { integer, err := s.ConsumeInt() if err != nil { return false, err } return (integer > 0), nil } // Reads a list of non-property lines, skipping any that match the given strings func (s *Scanner) ConsumeStringList(skip ...string) ([]string, error) { skipper := make(map[string]bool, len(skip)) for _, str := range skip { skipper[str] = true } var out []string for { isProp, err := s.PeekProperty() if err != nil { return nil, err } // The object list is terminated by the first property if isProp { break } str, err := s.ConsumeString() if err != nil { return nil, err } if !skipper[str] { out = append(out, str) } } return out, nil } func (s *Scanner) ConsumeIntPtr(to *int) error { val, err := s.ConsumeInt() if err != nil { return err } *to = val return nil } func (s *Scanner) ConsumeBoolPtr(to *bool) error { val, err := s.ConsumeBool() if err != nil { return err } *to = val return nil } func (s *Scanner) ConsumeIntPtrs(ptrs ...*int) error { for _, ptr := range ptrs { if err := s.ConsumeIntPtr(ptr); err != nil { return err } } return nil } func (s *Scanner) ConsumeBoolPtrs(ptrs ...*bool) error { for _, ptr := range ptrs { if err := s.ConsumeBoolPtr(ptr); err != nil { return err } } return nil }