Commit 425f6114 authored by Craig Peterson's avatar Craig Peterson

initial implementation of caddyfile macros

parent 79072828
...@@ -54,6 +54,7 @@ type parser struct { ...@@ -54,6 +54,7 @@ type parser struct {
block ServerBlock // current server block being parsed block ServerBlock // current server block being parsed
validDirectives []string // a directive must be valid or it's an error validDirectives []string // a directive must be valid or it's an error
eof bool // if we encounter a valid EOF in a hard place eof bool // if we encounter a valid EOF in a hard place
definedMacros map[string][]Token
} }
func (p *parser) parseAll() ([]ServerBlock, error) { func (p *parser) parseAll() ([]ServerBlock, error) {
...@@ -95,6 +96,25 @@ func (p *parser) begin() error { ...@@ -95,6 +96,25 @@ func (p *parser) begin() error {
return nil return nil
} }
if ok, name := p.isMacro(); ok {
if p.definedMacros == nil {
p.definedMacros = map[string][]Token{}
}
if p.definedMacros[name] != nil {
p.Errf("redeclaration of previously declared macro %s", name)
}
// consume all tokens til matched close brace
tokens, err := p.macroTokens()
if err != nil {
return err
}
p.definedMacros[name] = tokens
// empty block keys so we don't save this block as a real server.
p.block.Keys = nil
return nil
}
return p.blockContents() return p.blockContents()
} }
...@@ -221,70 +241,75 @@ func (p *parser) doImport() error { ...@@ -221,70 +241,75 @@ func (p *parser) doImport() error {
if p.NextArg() { if p.NextArg() {
return p.Err("Import takes only one argument (glob pattern or file)") return p.Err("Import takes only one argument (glob pattern or file)")
} }
// splice out the import directive and its argument (2 tokens total)
tokensBefore := p.tokens[:p.cursor-1]
tokensAfter := p.tokens[p.cursor+1:]
var importedTokens []Token
// make path relative to Caddyfile rather than current working directory (issue #867) // first check macros. That is a simple, non-recursive replacement
// and then use glob to get list of matching filenames if p.definedMacros[importPattern] != nil {
absFile, err := filepath.Abs(p.Dispenser.filename) importedTokens = p.definedMacros[importPattern]
if err != nil {
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
}
var matches []string
var globPattern string
if !filepath.IsAbs(importPattern) {
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
} else { } else {
globPattern = importPattern // make path relative to Caddyfile rather than current working directory (issue #867)
} // and then use glob to get list of matching filenames
matches, err = filepath.Glob(globPattern) absFile, err := filepath.Abs(p.Dispenser.filename)
if err != nil {
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
}
if err != nil { var matches []string
return p.Errf("Failed to use import pattern %s: %v", importPattern, err) var globPattern string
} if !filepath.IsAbs(importPattern) {
if len(matches) == 0 { globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
if strings.Contains(globPattern, "*") {
log.Printf("[WARNING] No files matching import pattern: %s", importPattern)
} else { } else {
return p.Errf("File to import not found: %s", importPattern) globPattern = importPattern
} }
} matches, err = filepath.Glob(globPattern)
// splice out the import directive and its argument (2 tokens total)
tokensBefore := p.tokens[:p.cursor-1]
tokensAfter := p.tokens[p.cursor+1:]
// collect all the imported tokens
var importedTokens []Token
for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile)
if err != nil { if err != nil {
return err return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
} }
if len(matches) == 0 {
if strings.Contains(globPattern, "*") {
log.Printf("[WARNING] No files matching import pattern: %s", importPattern)
} else {
return p.Errf("File to import not found: %s", importPattern)
}
}
// collect all the imported tokens
var importLine int for _, importFile := range matches {
for i, token := range newTokens { newTokens, err := p.doSingleImport(importFile)
if token.Text == "import" { if err != nil {
importLine = token.Line return err
continue
} }
if token.Line == importLine {
var abs string var importLine int
if filepath.IsAbs(token.Text) { for i, token := range newTokens {
abs = token.Text if token.Text == "import" {
} else if !filepath.IsAbs(importFile) { importLine = token.Line
abs = filepath.Join(filepath.Dir(absFile), token.Text) continue
} else {
abs = filepath.Join(filepath.Dir(importFile), token.Text)
} }
newTokens[i] = Token{ if token.Line == importLine {
Text: abs, var abs string
Line: token.Line, if filepath.IsAbs(token.Text) {
File: token.File, abs = token.Text
} else if !filepath.IsAbs(importFile) {
abs = filepath.Join(filepath.Dir(absFile), token.Text)
} else {
abs = filepath.Join(filepath.Dir(importFile), token.Text)
}
newTokens[i] = Token{
Text: abs,
Line: token.Line,
File: token.File,
}
} }
} }
}
importedTokens = append(importedTokens, newTokens...) importedTokens = append(importedTokens, newTokens...)
}
} }
// splice the imported tokens in the place of the import statement // splice the imported tokens in the place of the import statement
...@@ -433,3 +458,45 @@ type ServerBlock struct { ...@@ -433,3 +458,45 @@ type ServerBlock struct {
Keys []string Keys []string
Tokens map[string][]Token Tokens map[string][]Token
} }
func (p *parser) isMacro() (bool, string) {
keys := p.block.Keys
// "macro foo {}" style
if len(keys) == 2 && keys[0] == "macro" {
return true, keys[1]
}
// (foo) style. What to do if more than one server key and some have ()?
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
return true, strings.TrimSuffix(keys[0][1:], ")")
}
return false, ""
}
// read and store everything in a block for later replay.
func (p *parser) macroTokens() ([]Token, error) {
// TODO: disallow imports in macros for simplicity at import time
// macro must have curlies.
err := p.openCurlyBrace()
if err != nil {
return nil, err
}
count := 1
tokens := []Token{}
for p.Next() {
if p.Val() == "}" {
count--
if count == 0 {
break
}
}
if p.Val() == "{" {
count++
}
tokens = append(tokens, p.tokens[p.cursor])
}
// make sure we're matched up
if count != 0 {
return nil, p.SyntaxErr("}")
}
return tokens, nil
}
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
package caddyfile package caddyfile
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
...@@ -514,3 +515,43 @@ func testParser(input string) parser { ...@@ -514,3 +515,43 @@ func testParser(input string) parser {
p := parser{Dispenser: NewDispenser("Caddyfile", buf)} p := parser{Dispenser: NewDispenser("Caddyfile", buf)}
return p return p
} }
func TestMacro(t *testing.T) {
for _, tst := range []string{"(common)", "macro common"} {
t.Run(tst, func(t *testing.T) {
p := testParser(fmt.Sprintf(`
%s {
gzip foo
errors stderr
}
http://example.com {
import common
}
`, tst))
blocks, err := p.parseAll()
if err != nil {
t.Fatal(err)
}
for _, b := range blocks {
t.Log(b.Keys)
t.Log(b.Tokens)
}
if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
}
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
}
if len(blocks[0].Tokens) != 2 {
t.Fatalf("Server block should have tokens from import")
}
if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Tokens["errors"][1].Text, "stderr"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
})
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment