cmd: Add caddy fmt command. (#3090)

This takes the config file as input and formats it.
Prints the result to stdout. Can write changes to
file if `--write` flag is passed.

Fixes #3020

Signed-off-by: Vaibhav <vrongmeal@gmail.com>
This commit is contained in:
Vaibhav 2020-02-29 22:42:16 +05:30 committed by GitHub
parent e717028f83
commit 5fe69ac4ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 346 additions and 0 deletions

View file

@ -0,0 +1,137 @@
// 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 (
"bytes"
"io"
"unicode"
)
// Format formats a Caddyfile to conventional standards.
func Format(body []byte) []byte {
reader := bytes.NewReader(body)
result := new(bytes.Buffer)
var (
commented,
quoted,
escaped,
block,
environ,
lineBegin bool
firstIteration = true
prev,
curr,
next rune
err error
)
for {
prev = curr
curr = next
if curr < 0 {
break
}
next, _, err = reader.ReadRune()
if err != nil {
if err == io.EOF {
next = -1
} else {
panic(err)
}
}
if firstIteration {
firstIteration = false
lineBegin = true
continue
}
if quoted {
if escaped {
escaped = false
} else {
if curr == '\\' {
escaped = true
}
if curr == '"' {
quoted = false
}
}
if curr == '\n' {
quoted = false
}
} else if commented {
if curr == '\n' {
commented = false
}
} else {
if curr == '"' {
quoted = true
}
if curr == '#' {
commented = true
}
if curr == '}' {
if environ {
environ = false
} else if block {
block = false
}
}
if curr == '{' {
if unicode.IsSpace(next) {
block = true
if !unicode.IsSpace(prev) {
result.WriteRune(' ')
}
} else {
environ = true
}
}
if lineBegin {
if curr == ' ' || curr == '\t' {
continue
} else {
lineBegin = false
if block {
result.WriteRune('\t')
}
}
} else {
if prev == '{' &&
(curr == ' ' || curr == '\t') &&
(next != '\n' && next != '\r') {
curr = '\n'
}
}
}
if curr == '\n' {
lineBegin = true
}
result.WriteRune(curr)
}
return result.Bytes()
}

View file

@ -0,0 +1,161 @@
// 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 (
"testing"
)
func TestFormatBasicIndentation(t *testing.T) {
input := []byte(`
a
b
c {
d
}
e { f
}
`)
expected := []byte(`
a
b
c {
d
}
e {
f
}
`)
testFormat(t, input, expected)
}
func TestFormatBasicSpacing(t *testing.T) {
input := []byte(`
a{
b
}
c{ d
}
`)
expected := []byte(`
a {
b
}
c {
d
}
`)
testFormat(t, input, expected)
}
func TestFormatEnvironmentVariable(t *testing.T) {
input := []byte(`
{$A}
b {
{$C}
}
d { {$E}
}
`)
expected := []byte(`
{$A}
b {
{$C}
}
d {
{$E}
}
`)
testFormat(t, input, expected)
}
func TestFormatComments(t *testing.T) {
input := []byte(`
# a "\n"
# b {
c
}
d {
e # f
# g
}
h { # i
}
`)
expected := []byte(`
# a "\n"
# b {
c
}
d {
e # f
# g
}
h {
# i
}
`)
testFormat(t, input, expected)
}
func TestFormatQuotesAndEscapes(t *testing.T) {
input := []byte(`
"a \"b\" #c
d
e {
"f"
}
g { "h"
}
`)
expected := []byte(`
"a \"b\" #c
d
e {
"f"
}
g {
"h"
}
`)
testFormat(t, input, expected)
}
func testFormat(t *testing.T, input, expected []byte) {
output := Format(input)
if string(output) != string(expected) {
t.Errorf("Expected:\n%s\ngot:\n%s", string(output), string(expected))
}
}

View file

@ -34,6 +34,7 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/mholt/certmagic" "github.com/mholt/certmagic"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -538,6 +539,35 @@ func cmdValidateConfig(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func cmdFormatConfig(fl Flags) (int, error) {
// Default path of file is Caddyfile
formatCmdConfigFile := fl.Arg(0)
if formatCmdConfigFile == "" {
formatCmdConfigFile = "Caddyfile"
}
formatCmdWriteFlag := fl.Bool("write")
input, err := ioutil.ReadFile(formatCmdConfigFile)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err)
}
output := caddyfile.Format(input)
if formatCmdWriteFlag {
err = ioutil.WriteFile(formatCmdConfigFile, output, 0644)
if err != nil {
return caddy.ExitCodeFailedStartup, nil
}
} else {
fmt.Print(string(output))
}
return caddy.ExitCodeSuccess, nil
}
func cmdHelp(fl Flags) (int, error) { func cmdHelp(fl Flags) (int, error) {
const fullDocs = `Full documentation is available at: const fullDocs = `Full documentation is available at:
https://caddyserver.com/docs/command-line` https://caddyserver.com/docs/command-line`

View file

@ -242,6 +242,24 @@ provisioning stages.`,
}(), }(),
}) })
RegisterCommand(Command{
Name: "fmt",
Func: cmdFormatConfig,
Usage: "[--write] [<path>]",
Short: "Formats a Caddyfile",
Long: `
Formats the Caddyfile by adding proper indentation and spaces to improve
human readability. It prints the result to stdout.
If --write is specified, the output will be written to the config file
directly instead of printing it.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("format", flag.ExitOnError)
fs.Bool("write", false, "Over-write the output to specified file")
return fs
}(),
})
} }
// RegisterCommand registers the command cmd. // RegisterCommand registers the command cmd.