mirror of
https://github.com/caddyserver/caddy.git
synced 2024-11-29 12:16:16 +08:00
3dcc34d341
When we append a token to the new dispenser, we need to consume it in the parent, too; otherwise it gets scanned twice, which in this case messed up the nesting count which got decremented once too many times.
376 lines
11 KiB
Go
Executable File
376 lines
11 KiB
Go
Executable File
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package caddyfile
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
|
// except that it can do so with some notion of structure. An empty
|
|
// Dispenser is invalid; call NewDispenser to make a proper instance.
|
|
type Dispenser struct {
|
|
tokens []Token
|
|
cursor int
|
|
nesting int
|
|
}
|
|
|
|
// NewDispenser returns a Dispenser filled with the given tokens.
|
|
func NewDispenser(tokens []Token) *Dispenser {
|
|
return &Dispenser{
|
|
tokens: tokens,
|
|
cursor: -1,
|
|
}
|
|
}
|
|
|
|
// Next loads the next token. Returns true if a token
|
|
// was loaded; false otherwise. If false, all tokens
|
|
// have been consumed.
|
|
func (d *Dispenser) Next() bool {
|
|
if d.cursor < len(d.tokens)-1 {
|
|
d.cursor++
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Prev moves to the previous token. It does the inverse
|
|
// of Next(), except this function may decrement the cursor
|
|
// to -1 so that the next call to Next() points to the
|
|
// first token; this allows dispensing to "start over". This
|
|
// method returns true if the cursor ends up pointing to a
|
|
// valid token.
|
|
func (d *Dispenser) Prev() bool {
|
|
if d.cursor > -1 {
|
|
d.cursor--
|
|
return d.cursor > -1
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NextArg loads the next token if it is on the same
|
|
// line and if it is not a block opening (open curly
|
|
// brace). Returns true if an argument token was
|
|
// loaded; false otherwise. If false, all tokens on
|
|
// the line have been consumed except for potentially
|
|
// a block opening. It handles imported tokens
|
|
// correctly.
|
|
func (d *Dispenser) NextArg() bool {
|
|
if !d.nextOnSameLine() {
|
|
return false
|
|
}
|
|
if d.Val() == "{" {
|
|
// roll back; a block opening is not an argument
|
|
d.cursor--
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// nextOnSameLine advances the cursor if the next
|
|
// token is on the same line of the same file.
|
|
func (d *Dispenser) nextOnSameLine() bool {
|
|
if d.cursor < 0 {
|
|
d.cursor++
|
|
return true
|
|
}
|
|
if d.cursor >= len(d.tokens) {
|
|
return false
|
|
}
|
|
if d.cursor < len(d.tokens)-1 &&
|
|
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
|
|
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
|
|
d.cursor++
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NextLine loads the next token only if it is not on the same
|
|
// line as the current token, and returns true if a token was
|
|
// loaded; false otherwise. If false, there is not another token
|
|
// or it is on the same line. It handles imported tokens correctly.
|
|
func (d *Dispenser) NextLine() bool {
|
|
if d.cursor < 0 {
|
|
d.cursor++
|
|
return true
|
|
}
|
|
if d.cursor >= len(d.tokens) {
|
|
return false
|
|
}
|
|
if d.cursor < len(d.tokens)-1 &&
|
|
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
|
|
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
|
|
d.cursor++
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NextBlock can be used as the condition of a for loop
|
|
// to load the next token as long as it opens a block or
|
|
// is already in a block nested more than initialNestingLevel.
|
|
// In other words, a loop over NextBlock() will iterate
|
|
// all tokens in the block assuming the next token is an
|
|
// open curly brace, until the matching closing brace.
|
|
// The open and closing brace tokens for the outer-most
|
|
// block will be consumed internally and omitted from
|
|
// the iteration.
|
|
//
|
|
// Proper use of this method looks like this:
|
|
//
|
|
// for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
// }
|
|
//
|
|
// However, in simple cases where it is known that the
|
|
// Dispenser is new and has not already traversed state
|
|
// by a loop over NextBlock(), this will do:
|
|
//
|
|
// for d.NextBlock(0) {
|
|
// }
|
|
//
|
|
// As with other token parsing logic, a loop over
|
|
// NextBlock() should be contained within a loop over
|
|
// Next(), as it is usually prudent to skip the initial
|
|
// token.
|
|
func (d *Dispenser) NextBlock(initialNestingLevel int) bool {
|
|
if d.nesting > initialNestingLevel {
|
|
if !d.Next() {
|
|
return false // should be EOF error
|
|
}
|
|
if d.Val() == "}" {
|
|
d.nesting--
|
|
}
|
|
return d.nesting > initialNestingLevel
|
|
}
|
|
if !d.nextOnSameLine() { // block must open on same line
|
|
return false
|
|
}
|
|
if d.Val() != "{" {
|
|
d.cursor-- // roll back if not opening brace
|
|
return false
|
|
}
|
|
d.Next() // consume open curly brace
|
|
if d.Val() == "}" {
|
|
return false // open and then closed right away
|
|
}
|
|
d.nesting++
|
|
return true
|
|
}
|
|
|
|
// Nesting returns the current nesting level. Necessary
|
|
// if using NextBlock()
|
|
func (d *Dispenser) Nesting() int {
|
|
return d.nesting
|
|
}
|
|
|
|
// Val gets the text of the current token. If there is no token
|
|
// loaded, it returns empty string.
|
|
func (d *Dispenser) Val() string {
|
|
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
|
return ""
|
|
}
|
|
return d.tokens[d.cursor].Text
|
|
}
|
|
|
|
// Line gets the line number of the current token.
|
|
// If there is no token loaded, it returns 0.
|
|
func (d *Dispenser) Line() int {
|
|
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
|
return 0
|
|
}
|
|
return d.tokens[d.cursor].Line
|
|
}
|
|
|
|
// File gets the filename where the current token originated.
|
|
func (d *Dispenser) File() string {
|
|
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
|
return ""
|
|
}
|
|
return d.tokens[d.cursor].File
|
|
}
|
|
|
|
// Args is a convenience function that loads the next arguments
|
|
// (tokens on the same line) into an arbitrary number of strings
|
|
// pointed to in targets. If there are not enough argument tokens
|
|
// available to fill targets, false is returned and the remaining
|
|
// targets are left unchanged. If all the targets are filled,
|
|
// then true is returned.
|
|
func (d *Dispenser) Args(targets ...*string) bool {
|
|
for i := 0; i < len(targets); i++ {
|
|
if !d.NextArg() {
|
|
return false
|
|
}
|
|
*targets[i] = d.Val()
|
|
}
|
|
return true
|
|
}
|
|
|
|
// AllArgs is like Args, but if there are more argument tokens
|
|
// available than there are targets, false is returned. The
|
|
// number of available argument tokens must match the number of
|
|
// targets exactly to return true.
|
|
func (d *Dispenser) AllArgs(targets ...*string) bool {
|
|
if !d.Args(targets...) {
|
|
return false
|
|
}
|
|
if d.NextArg() {
|
|
d.Prev()
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// RemainingArgs loads any more arguments (tokens on the same line)
|
|
// into a slice and returns them. Open curly brace tokens also indicate
|
|
// the end of arguments, and the curly brace is not included in
|
|
// the return value nor is it loaded.
|
|
func (d *Dispenser) RemainingArgs() []string {
|
|
var args []string
|
|
for d.NextArg() {
|
|
args = append(args, d.Val())
|
|
}
|
|
return args
|
|
}
|
|
|
|
// NewFromNextTokens returns a new dispenser with a copy of
|
|
// the tokens from the current token until the end of the
|
|
// "directive" whether that be to the end of the line or
|
|
// the end of a block that starts at the end of the line;
|
|
// in other words, until the end of the segment.
|
|
func (d *Dispenser) NewFromNextTokens() *Dispenser {
|
|
tkns := []Token{d.Token()}
|
|
for d.NextArg() {
|
|
tkns = append(tkns, d.Token())
|
|
}
|
|
var openedBlock bool
|
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
if !openedBlock {
|
|
// because NextBlock() consumes the initial open
|
|
// curly brace, we rewind here to append it, since
|
|
// our case is special in that we want to include
|
|
// all the tokens including surrounding curly braces
|
|
// for a new dispenser to have
|
|
d.Prev()
|
|
tkns = append(tkns, d.Token())
|
|
d.Next()
|
|
openedBlock = true
|
|
}
|
|
tkns = append(tkns, d.Token())
|
|
}
|
|
if openedBlock {
|
|
// include closing brace accordingly
|
|
tkns = append(tkns, d.Token())
|
|
// since NewFromNextTokens is intended to consume the entire
|
|
// directive, we must call Next() here and consume the closing
|
|
// curly brace
|
|
d.Next()
|
|
}
|
|
return NewDispenser(tkns)
|
|
}
|
|
|
|
// Token returns the current token.
|
|
func (d *Dispenser) Token() Token {
|
|
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
|
return Token{}
|
|
}
|
|
return d.tokens[d.cursor]
|
|
}
|
|
|
|
// Reset sets d's cursor to the beginning, as
|
|
// if this was a new and unused dispenser.
|
|
func (d *Dispenser) Reset() {
|
|
d.cursor = -1
|
|
d.nesting = 0
|
|
}
|
|
|
|
// ArgErr returns an argument error, meaning that another
|
|
// argument was expected but not found. In other words,
|
|
// a line break or open curly brace was encountered instead of
|
|
// an argument.
|
|
func (d *Dispenser) ArgErr() error {
|
|
if d.Val() == "{" {
|
|
return d.Err("Unexpected token '{', expecting argument")
|
|
}
|
|
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
|
|
}
|
|
|
|
// SyntaxErr creates a generic syntax error which explains what was
|
|
// found and what was expected.
|
|
func (d *Dispenser) SyntaxErr(expected string) error {
|
|
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
|
|
return errors.New(msg)
|
|
}
|
|
|
|
// EOFErr returns an error indicating that the dispenser reached
|
|
// the end of the input when searching for the next token.
|
|
func (d *Dispenser) EOFErr() error {
|
|
return d.Errf("Unexpected EOF")
|
|
}
|
|
|
|
// Err generates a custom parse-time error with a message of msg.
|
|
func (d *Dispenser) Err(msg string) error {
|
|
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
|
|
return errors.New(msg)
|
|
}
|
|
|
|
// Errf is like Err, but for formatted error messages
|
|
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
|
return d.Err(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Delete deletes the current token and returns the updated slice
|
|
// of tokens. The cursor is not advanced to the next token.
|
|
// Because deletion modifies the underlying slice, this method
|
|
// should only be called if you have access to the original slice
|
|
// of tokens and/or are using the slice of tokens outside this
|
|
// Dispenser instance. If you do not re-assign the slice with the
|
|
// return value of this method, inconsistencies in the token
|
|
// array will become apparent (or worse, hide from you like they
|
|
// did me for 3 and a half freaking hours late one night).
|
|
func (d *Dispenser) Delete() []Token {
|
|
if d.cursor >= 0 && d.cursor <= len(d.tokens)-1 {
|
|
d.tokens = append(d.tokens[:d.cursor], d.tokens[d.cursor+1:]...)
|
|
d.cursor--
|
|
}
|
|
return d.tokens
|
|
}
|
|
|
|
// numLineBreaks counts how many line breaks are in the token
|
|
// value given by the token index tknIdx. It returns 0 if the
|
|
// token does not exist or there are no line breaks.
|
|
func (d *Dispenser) numLineBreaks(tknIdx int) int {
|
|
if tknIdx < 0 || tknIdx >= len(d.tokens) {
|
|
return 0
|
|
}
|
|
return strings.Count(d.tokens[tknIdx].Text, "\n")
|
|
}
|
|
|
|
// isNewLine determines whether the current token is on a different
|
|
// line (higher line number) than the previous token. It handles imported
|
|
// tokens correctly. If there isn't a previous token, it returns true.
|
|
func (d *Dispenser) isNewLine() bool {
|
|
if d.cursor < 1 {
|
|
return true
|
|
}
|
|
if d.cursor > len(d.tokens)-1 {
|
|
return false
|
|
}
|
|
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
|
|
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
|
|
}
|