1// Copyright 2015 Light Code Labs, LLC 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 15package caddyfile 16 17import ( 18 "errors" 19 "fmt" 20 "io" 21 "strings" 22) 23 24// Dispenser is a type that dispenses tokens, similarly to a lexer, 25// except that it can do so with some notion of structure and has 26// some really convenient methods. 27type Dispenser struct { 28 filename string 29 tokens []Token 30 cursor int 31 nesting int 32} 33 34// NewDispenser returns a Dispenser, ready to use for parsing the given input. 35func NewDispenser(filename string, input io.Reader) Dispenser { 36 tokens, _ := allTokens(input) // ignoring error because nothing to do with it 37 return Dispenser{ 38 filename: filename, 39 tokens: tokens, 40 cursor: -1, 41 } 42} 43 44// NewDispenserTokens returns a Dispenser filled with the given tokens. 45func NewDispenserTokens(filename string, tokens []Token) Dispenser { 46 return Dispenser{ 47 filename: filename, 48 tokens: tokens, 49 cursor: -1, 50 } 51} 52 53// Next loads the next token. Returns true if a token 54// was loaded; false otherwise. If false, all tokens 55// have been consumed. 56func (d *Dispenser) Next() bool { 57 if d.cursor < len(d.tokens)-1 { 58 d.cursor++ 59 return true 60 } 61 return false 62} 63 64// NextArg loads the next token if it is on the same 65// line. Returns true if a token was loaded; false 66// otherwise. If false, all tokens on the line have 67// been consumed. It handles imported tokens correctly. 68func (d *Dispenser) NextArg() bool { 69 if d.cursor < 0 { 70 d.cursor++ 71 return true 72 } 73 if d.cursor >= len(d.tokens) { 74 return false 75 } 76 if d.cursor < len(d.tokens)-1 && 77 d.tokens[d.cursor].File == d.tokens[d.cursor+1].File && 78 d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line { 79 d.cursor++ 80 return true 81 } 82 return false 83} 84 85// NextLine loads the next token only if it is not on the same 86// line as the current token, and returns true if a token was 87// loaded; false otherwise. If false, there is not another token 88// or it is on the same line. It handles imported tokens correctly. 89func (d *Dispenser) NextLine() bool { 90 if d.cursor < 0 { 91 d.cursor++ 92 return true 93 } 94 if d.cursor >= len(d.tokens) { 95 return false 96 } 97 if d.cursor < len(d.tokens)-1 && 98 (d.tokens[d.cursor].File != d.tokens[d.cursor+1].File || 99 d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) { 100 d.cursor++ 101 return true 102 } 103 return false 104} 105 106// NextBlock can be used as the condition of a for loop 107// to load the next token as long as it opens a block or 108// is already in a block. It returns true if a token was 109// loaded, or false when the block's closing curly brace 110// was loaded and thus the block ended. Nested blocks are 111// not supported. 112func (d *Dispenser) NextBlock() bool { 113 if d.nesting > 0 { 114 d.Next() 115 if d.Val() == "}" { 116 d.nesting-- 117 return false 118 } 119 return true 120 } 121 if !d.NextArg() { // block must open on same line 122 return false 123 } 124 if d.Val() != "{" { 125 d.cursor-- // roll back if not opening brace 126 return false 127 } 128 d.Next() 129 if d.Val() == "}" { 130 // Open and then closed right away 131 return false 132 } 133 d.nesting++ 134 return true 135} 136 137// Val gets the text of the current token. If there is no token 138// loaded, it returns empty string. 139func (d *Dispenser) Val() string { 140 if d.cursor < 0 || d.cursor >= len(d.tokens) { 141 return "" 142 } 143 return d.tokens[d.cursor].Text 144} 145 146// Line gets the line number of the current token. If there is no token 147// loaded, it returns 0. 148func (d *Dispenser) Line() int { 149 if d.cursor < 0 || d.cursor >= len(d.tokens) { 150 return 0 151 } 152 return d.tokens[d.cursor].Line 153} 154 155// File gets the filename of the current token. If there is no token loaded, 156// it returns the filename originally given when parsing started. 157func (d *Dispenser) File() string { 158 if d.cursor < 0 || d.cursor >= len(d.tokens) { 159 return d.filename 160 } 161 if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" { 162 return tokenFilename 163 } 164 return d.filename 165} 166 167// Args is a convenience function that loads the next arguments 168// (tokens on the same line) into an arbitrary number of strings 169// pointed to in targets. If there are fewer tokens available 170// than string pointers, the remaining strings will not be changed 171// and false will be returned. If there were enough tokens available 172// to fill the arguments, then true will be returned. 173func (d *Dispenser) Args(targets ...*string) bool { 174 enough := true 175 for i := 0; i < len(targets); i++ { 176 if !d.NextArg() { 177 enough = false 178 break 179 } 180 *targets[i] = d.Val() 181 } 182 return enough 183} 184 185// RemainingArgs loads any more arguments (tokens on the same line) 186// into a slice and returns them. Open curly brace tokens also indicate 187// the end of arguments, and the curly brace is not included in 188// the return value nor is it loaded. 189func (d *Dispenser) RemainingArgs() []string { 190 var args []string 191 192 for d.NextArg() { 193 if d.Val() == "{" { 194 d.cursor-- 195 break 196 } 197 args = append(args, d.Val()) 198 } 199 200 return args 201} 202 203// ArgErr returns an argument error, meaning that another 204// argument was expected but not found. In other words, 205// a line break or open curly brace was encountered instead of 206// an argument. 207func (d *Dispenser) ArgErr() error { 208 if d.Val() == "{" { 209 return d.Err("Unexpected token '{', expecting argument") 210 } 211 return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val()) 212} 213 214// SyntaxErr creates a generic syntax error which explains what was 215// found and what was expected. 216func (d *Dispenser) SyntaxErr(expected string) error { 217 msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected) 218 return errors.New(msg) 219} 220 221// EOFErr returns an error indicating that the dispenser reached 222// the end of the input when searching for the next token. 223func (d *Dispenser) EOFErr() error { 224 return d.Errf("Unexpected EOF") 225} 226 227// Err generates a custom parse-time error with a message of msg. 228func (d *Dispenser) Err(msg string) error { 229 msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg) 230 return errors.New(msg) 231} 232 233// Errf is like Err, but for formatted error messages 234func (d *Dispenser) Errf(format string, args ...interface{}) error { 235 return d.Err(fmt.Sprintf(format, args...)) 236} 237 238// numLineBreaks counts how many line breaks are in the token 239// value given by the token index tknIdx. It returns 0 if the 240// token does not exist or there are no line breaks. 241func (d *Dispenser) numLineBreaks(tknIdx int) int { 242 if tknIdx < 0 || tknIdx >= len(d.tokens) { 243 return 0 244 } 245 return strings.Count(d.tokens[tknIdx].Text, "\n") 246} 247 248// isNewLine determines whether the current token is on a different 249// line (higher line number) than the previous token. It handles imported 250// tokens correctly. If there isn't a previous token, it returns true. 251func (d *Dispenser) isNewLine() bool { 252 if d.cursor < 1 { 253 return true 254 } 255 if d.cursor > len(d.tokens)-1 { 256 return false 257 } 258 return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File || 259 d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line 260} 261