전체 코드 정리
This commit is contained in:
parent
bcd906c850
commit
8ebf91bf19
|
@ -120,6 +120,5 @@ $RECYCLE.BIN/
|
|||
*.lnk
|
||||
|
||||
build/
|
||||
vendor/
|
||||
|
||||
cpu_ctrl
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
PROJECT_NAME := cpu_ctrl
|
||||
PKG := "amuz.es/src/infra/$(PROJECT_NAME)"
|
||||
PKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/)
|
||||
GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go)
|
||||
BUILD=`date +%FT%T%z`
|
||||
IS_DIRTY=$(shell sh -c '[[ `git diff --shortstat 2> /dev/null | tail -n1` != "" ]] && echo "-dirty"')
|
||||
VERSION?=$(shell git describe --exact-match 2> /dev/null || echo "`git symbolic-ref HEAD 2> /dev/null | cut -b 12-`-`git log --pretty=format:\"%h\" -1`")$(IS_DIRTY)
|
||||
DIST_NAME:= $(PROJECT_NAME).cpio.gz
|
||||
UTILS :=$(shell find cmd -type d -maxdepth 1 -mindepth 1|sed -e 's/cmd\///g')
|
||||
|
||||
.PHONY: all vgo build
|
||||
|
||||
all: build
|
||||
|
||||
lint: ## Lint the files
|
||||
@golint -set_exit_status ${PKG_LIST}
|
||||
|
||||
test: ## Run unittests
|
||||
@go test -short ${PKG_LIST}
|
||||
|
||||
race: vgo ## Run data race detector
|
||||
@go test -race -short ${PKG_LIST}
|
||||
|
||||
msan: vgo ## Run memory sanitizer
|
||||
@go test -msan -short ${PKG_LIST}
|
||||
|
||||
vgo.lock: ## Get the dependencies
|
||||
@echo -n ensure 'vgo' dependency resolver..
|
||||
@go get -u golang.org/x/vgo
|
||||
@echo done
|
||||
@echo -n resolving dependencies..
|
||||
@${GOPATH}/bin/vgo mod -fix -vendor -sync
|
||||
@echo done
|
||||
@touch vgo.lock
|
||||
|
||||
vgo: vgo.lock
|
||||
|
||||
build: vgo ## Build the binary file
|
||||
@echo -n building..
|
||||
@go build -ldflags "-w -s -X main.version=${VERSION} -X main.buildDate=${BUILD}" -o "${PROJECT_NAME}" .
|
||||
@echo done
|
||||
|
||||
build_util: vgo ## Build the binary file
|
||||
@echo building..
|
||||
@$(foreach UTIL,$(UTILS),printf "=> $(UTIL):" ;\
|
||||
go build -ldflags "-w -s -X main.version=${VERSION} -X main.buildDate=${BUILD}" -o "cmd/$(UTIL)/$(UTIL)" $(PKG)/cmd/$(UTIL) ;\
|
||||
echo done ;)
|
||||
@echo done
|
||||
|
||||
clean: ## Remove previous build
|
||||
@$(foreach UTIL,$(UTILS),rm -f "cmd/$(UTIL)/$(UTIL)";)
|
||||
@rm -f "$(PROJECT_NAME)"
|
||||
@rm -rf build
|
||||
|
||||
version: ## Display version
|
||||
@echo $(VERSION)
|
||||
|
||||
help: ## Display this help screen
|
||||
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
dist: build build_util
|
||||
@rm -rf build
|
||||
@mkdir -p build
|
||||
@mv -f "$(PROJECT_NAME)" "build/$(PROJECT_NAME)"
|
||||
@$(foreach UTIL,$(UTILS),mv -f "cmd/$(UTIL)/$(UTIL)" "build/$(UTIL)";)
|
||||
@echo -n archieving..
|
||||
@pushd build 1> /dev/null 2>&1;find . -mindepth 1 ! -name $(DIST_NAME) | cpio --quiet -o |gzip > $(DIST_NAME) ;popd 1>/dev/null 2>&1
|
||||
@echo " build/$(DIST_NAME)"
|
|
@ -0,0 +1,168 @@
|
|||
package main // import "amuz.es/src/infra/cpu_ctrl"
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"fmt"
|
||||
zlog "amuz.es/src/infra/goutils/logger/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"go.uber.org/multierr"
|
||||
"amuz.es/src/infra/goutils/logger/rotater"
|
||||
"amuz.es/src/infra/goutils/handler"
|
||||
"amuz.es/src/infra/cpu_ctrl/daemon"
|
||||
"amuz.es/src/infra/cpu_ctrl/producer"
|
||||
"amuz.es/src/infra/cpu_ctrl/consumer"
|
||||
"errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func finalCloser() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.(error).Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 로그 초기화
|
||||
func initLogger() func() {
|
||||
// 로깅설정
|
||||
formatter := zapcore.NewConsoleEncoder(zlog.LogCommonFormat)
|
||||
|
||||
// 전역 로거 초기화
|
||||
var err error
|
||||
logger, err = zlog.Init(
|
||||
true,
|
||||
formatter,
|
||||
name,
|
||||
"Stderr",
|
||||
"",
|
||||
nil,
|
||||
zap.DebugLevel,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// 로깅종료 및 exitcode 설정
|
||||
return func() {}
|
||||
}
|
||||
|
||||
// 애플리케이션이 종료를 위해 대기하는 부분
|
||||
func initContext(handler *handler.Handler) (func(), func()) {
|
||||
exitSignal := make(chan os.Signal, 1)
|
||||
// return waiter
|
||||
return func() {
|
||||
daemon.NotifyDaemon(daemon.DaemonStarted)
|
||||
// 시그널 처리
|
||||
signal.Notify(exitSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
|
||||
for {
|
||||
select {
|
||||
case <-handler.Done():
|
||||
logger.Info("self destruct to close this application")
|
||||
return
|
||||
case initialErr := <-handler.Error():
|
||||
// 복구불가능한 에러(들) 모아서 넘겨주는 부분
|
||||
merged := initialErr
|
||||
logger.Error("main: ", initialErr)
|
||||
for {
|
||||
select {
|
||||
case anotherErr := <-handler.Error():
|
||||
merged = multierr.Append(merged, anotherErr)
|
||||
logger.Error("main: ", anotherErr)
|
||||
default:
|
||||
// panic을 발생시켜 컨텍스트를 에러를 전달한다.
|
||||
panic(merged)
|
||||
}
|
||||
}
|
||||
case sysSignal := <-exitSignal:
|
||||
//handle signal
|
||||
switch sysSignal {
|
||||
case syscall.SIGUSR1:
|
||||
rotater.Rotate()
|
||||
default:
|
||||
logger.Info(sysSignal.String(), " received")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}, // return closer
|
||||
func() {
|
||||
daemon.NotifyDaemon(daemon.DaemonStopping)
|
||||
logger.Info("main: main context waiting..")
|
||||
// http 서버 기다린다.
|
||||
handler.GracefulWait()
|
||||
close(exitSignal)
|
||||
}
|
||||
}
|
||||
|
||||
// 메인 웹서버 초기화
|
||||
func initProcessor(handler *handler.Handler) func() {
|
||||
|
||||
FanoutSpeed := func(sender <-chan producer.FanspeedInfo, receivers ...chan<- producer.FanspeedInfo) {
|
||||
defer func() {
|
||||
for _, receiver := range receivers {
|
||||
close(receiver)
|
||||
}
|
||||
if err := recover(); err != nil {
|
||||
handler.NotifyError(err.(error))
|
||||
}
|
||||
}()
|
||||
for speed := range sender {
|
||||
for _, receiver := range receivers {
|
||||
select {
|
||||
case receiver <- speed:
|
||||
default:
|
||||
logger.Warn("Some Fanspeed consumer blocked!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FanoutTempeture := func(sender <-chan producer.TempetureInfo, receivers ...chan<- producer.TempetureInfo) {
|
||||
defer func() {
|
||||
|
||||
for _, receiver := range receivers {
|
||||
close(receiver)
|
||||
}
|
||||
if err := recover(); err != nil {
|
||||
handler.NotifyError(err.(error))
|
||||
}
|
||||
}()
|
||||
for tempeture := range sender {
|
||||
for _, receiver := range receivers {
|
||||
select {
|
||||
case receiver <- tempeture:
|
||||
default:
|
||||
logger.Warn("Some Tempeture consumer blocked!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processorCount := producer.GetProcessorCount()
|
||||
if processorCount == 0 {
|
||||
panic(errors.New("cpu not found!"))
|
||||
}
|
||||
|
||||
tempetureInfoChan, fanspeedChan := producer.AggregateProcessorChannel(
|
||||
handler,
|
||||
*SampleInterval, processorCount,
|
||||
*P, *I, *D,
|
||||
*SetPoint,
|
||||
)
|
||||
|
||||
fanController := consumer.NewFanControl(processorCount, *SampleInterval, handler)
|
||||
metricLogger := consumer.NewInfluxMetric((*InfluxHost).String(), processorCount, handler)
|
||||
|
||||
|
||||
handler.IncreaseWait()
|
||||
go fanController.StartControl()
|
||||
handler.IncreaseWait()
|
||||
go metricLogger.StartLogging()
|
||||
|
||||
go FanoutTempeture(tempetureInfoChan, metricLogger.TempetureConsumer())
|
||||
go FanoutSpeed(fanspeedChan, fanController.Consumer(), metricLogger.FanSpeedConsumer())
|
||||
|
||||
return func() {}
|
||||
}
|
|
@ -1,16 +1,13 @@
|
|||
package consumer
|
||||
|
||||
import (
|
||||
"amuz.es/src/infra/cpu_ctrl/util"
|
||||
"amuz.es/src/infra/cpu_ctrl/processor"
|
||||
"amuz.es/src/infra/cpu_ctrl/logger"
|
||||
"github.com/influxdata/influxdb/client/v2"
|
||||
"time"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
influxLogger = logger.NewLogger("influx")
|
||||
"amuz.es/src/infra/goutils/handler"
|
||||
"amuz.es/src/infra/cpu_ctrl/producer"
|
||||
"github.com/influxdata/influxdb/client/v2"
|
||||
zlog "amuz.es/src/infra/goutils/logger/zap"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type data struct {
|
||||
|
@ -21,29 +18,31 @@ type data struct {
|
|||
type influxMetric struct {
|
||||
host string
|
||||
processorCount int
|
||||
handler util.Handler
|
||||
fanSpeedConsumer chan processor.FanspeedInfo
|
||||
tempetureConsumer chan processor.TempetureInfo
|
||||
handler *handler.Handler
|
||||
fanSpeedConsumer chan producer.FanspeedInfo
|
||||
tempetureConsumer chan producer.TempetureInfo
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
type InfluxMetric interface {
|
||||
FanSpeedConsumer() chan<- processor.FanspeedInfo
|
||||
TempetureConsumer() chan<- processor.TempetureInfo
|
||||
FanSpeedConsumer() chan<- producer.FanspeedInfo
|
||||
TempetureConsumer() chan<- producer.TempetureInfo
|
||||
StartLogging()
|
||||
}
|
||||
|
||||
func NewInfluxMetric(host string, processorCount int, handler util.Handler) InfluxMetric {
|
||||
func NewInfluxMetric(host string, processorCount int, handler *handler.Handler) InfluxMetric {
|
||||
return &influxMetric{
|
||||
host: host,
|
||||
processorCount: processorCount,
|
||||
handler: handler,
|
||||
fanSpeedConsumer: make(chan processor.FanspeedInfo, processorCount),
|
||||
tempetureConsumer: make(chan processor.TempetureInfo, processorCount),
|
||||
fanSpeedConsumer: make(chan producer.FanspeedInfo, processorCount),
|
||||
tempetureConsumer: make(chan producer.TempetureInfo, processorCount),
|
||||
logger: zlog.New(nil, "influx"),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *influxMetric) FanSpeedConsumer() chan<- processor.FanspeedInfo { return m.fanSpeedConsumer }
|
||||
func (m *influxMetric) TempetureConsumer() chan<- processor.TempetureInfo { return m.tempetureConsumer }
|
||||
func (m *influxMetric) FanSpeedConsumer() chan<- producer.FanspeedInfo { return m.fanSpeedConsumer }
|
||||
func (m *influxMetric) TempetureConsumer() chan<- producer.TempetureInfo { return m.tempetureConsumer }
|
||||
|
||||
func (m *influxMetric) StartLogging() {
|
||||
defer m.handler.DecreaseWait()
|
||||
|
@ -53,17 +52,15 @@ func (m *influxMetric) StartLogging() {
|
|||
m.handler.NotifyError(err.(error))
|
||||
}
|
||||
}()
|
||||
defer close(m.fanSpeedConsumer)
|
||||
defer close(m.tempetureConsumer)
|
||||
|
||||
defer influxLogger.Info("Metric logging stopped")
|
||||
influxLogger.Info("Metric logging started")
|
||||
defer m.logger.Info("Metric logging stopped")
|
||||
m.logger.Info("Metric logging started")
|
||||
|
||||
var influxDbConn client.Client
|
||||
for {
|
||||
conn, err := client.NewUDPClient(client.UDPConfig{Addr: m.host,})
|
||||
if err != nil {
|
||||
influxLogger.Error(err)
|
||||
m.logger.Error(err)
|
||||
} else {
|
||||
influxDbConn = conn
|
||||
break
|
||||
|
@ -81,15 +78,23 @@ func (m *influxMetric) StartLogging() {
|
|||
metricData := make([]data, m.processorCount)
|
||||
sendData := make([]data, m.processorCount)
|
||||
|
||||
go func() {
|
||||
for changedSpeed := range m.fanSpeedConsumer {
|
||||
metricData[changedSpeed.Id].FanSpeed = changedSpeed.FanSpeed
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for changedTempeture := range m.tempetureConsumer {
|
||||
metricData[changedTempeture.Id].Tempeture = changedTempeture.Tempeture
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker:
|
||||
copy(sendData, metricData)
|
||||
go m.sendPoint(influxDbConn, sendData)
|
||||
case changedSpeed := <-m.fanSpeedConsumer:
|
||||
metricData[changedSpeed.Id].FanSpeed = changedSpeed.FanSpeed
|
||||
case changedTempeture := <-m.tempetureConsumer:
|
||||
metricData[changedTempeture.Id].Tempeture = changedTempeture.Tempeture
|
||||
case <-m.handler.Done():
|
||||
return
|
||||
}
|
||||
|
@ -104,7 +109,7 @@ func (m *influxMetric) sendPoint(
|
|||
if point, err := m.getPoint(id, data, at); err == nil {
|
||||
pointList = append(pointList, point)
|
||||
} else {
|
||||
influxLogger.Debugf("id %d err %s", id, err)
|
||||
m.logger.Debugf("id %d err %s", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,12 +123,12 @@ func (m *influxMetric) sendPoint(
|
|||
Precision: "s",
|
||||
})
|
||||
if err != nil {
|
||||
influxLogger.Warn(err)
|
||||
m.logger.Warn(err)
|
||||
return
|
||||
}
|
||||
batchPoint.AddPoints(pointList)
|
||||
if err := influxDbConn.Write(batchPoint); err != nil {
|
||||
influxLogger.Warn(err)
|
||||
m.logger.Warn(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,39 +3,40 @@ package consumer
|
|||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"amuz.es/src/infra/cpu_ctrl/util"
|
||||
"time"
|
||||
"amuz.es/src/infra/cpu_ctrl/processor"
|
||||
"amuz.es/src/infra/cpu_ctrl/logger"
|
||||
zlog "amuz.es/src/infra/goutils/logger/zap"
|
||||
"bytes"
|
||||
"amuz.es/src/infra/goutils/handler"
|
||||
"amuz.es/src/infra/cpu_ctrl/producer"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
fanspeedLogger = logger.NewLogger("fanspeed")
|
||||
)
|
||||
var ()
|
||||
|
||||
type fanControl struct {
|
||||
processorCount int
|
||||
handler util.Handler
|
||||
fanSpeedConsumer chan processor.FanspeedInfo
|
||||
handler *handler.Handler
|
||||
fanSpeedConsumer chan producer.FanspeedInfo
|
||||
sampleDuration time.Duration
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
type FanControl interface {
|
||||
Consumer() chan<- processor.FanspeedInfo
|
||||
Consumer() chan<- producer.FanspeedInfo
|
||||
StartControl()
|
||||
}
|
||||
|
||||
func NewFanControl(processorCount int, sampleDuration time.Duration, handler util.Handler) FanControl {
|
||||
func NewFanControl(processorCount int, sampleDuration time.Duration, handler *handler.Handler) FanControl {
|
||||
return &fanControl{
|
||||
processorCount: processorCount,
|
||||
handler: handler,
|
||||
fanSpeedConsumer: make(chan processor.FanspeedInfo, processorCount),
|
||||
fanSpeedConsumer: make(chan producer.FanspeedInfo, processorCount),
|
||||
sampleDuration: sampleDuration,
|
||||
logger: zlog.New(nil, "fanspeed"),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *fanControl) Consumer() chan<- processor.FanspeedInfo { return c.fanSpeedConsumer }
|
||||
func (c *fanControl) Consumer() chan<- producer.FanspeedInfo { return c.fanSpeedConsumer }
|
||||
|
||||
func (c *fanControl) StartControl() {
|
||||
defer c.handler.DecreaseWait()
|
||||
|
@ -45,8 +46,8 @@ func (c *fanControl) StartControl() {
|
|||
}
|
||||
}()
|
||||
defer close(c.fanSpeedConsumer)
|
||||
defer fanspeedLogger.Info("Fan control stopped")
|
||||
fanspeedLogger.Info("Fan control started")
|
||||
defer c.logger.Info("Fan control stopped")
|
||||
c.logger.Info("Fan control started")
|
||||
|
||||
ticker := time.Tick(c.sampleDuration)
|
||||
pastFanSpeedList, newFanSpeedList := make([]int, c.processorCount), make([]int, c.processorCount)
|
||||
|
@ -83,7 +84,7 @@ func (c *fanControl) applyFanspeed(newFanSpeedList []int) {
|
|||
buf.WriteString(fmt.Sprintf("0x%x", item))
|
||||
buf.WriteRune(' ')
|
||||
}
|
||||
fanspeedLogger.Infof("Commit fan speed with %s", buf.String())
|
||||
c.logger.Infof("Commit fan speed with %s", buf.String())
|
||||
}
|
||||
|
||||
func compareFanSpeed(old, new []int) bool {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
module amuz.es/src/infra/cpu_ctrl
|
||||
|
||||
require (
|
||||
amuz.es/src/infra/goutils v0.1.0
|
||||
github.com/StackExchange/wmi v0.0.0-20180412205111-cdffdb33acae
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf
|
||||
github.com/coreos/go-systemd v0.0.0-20180525142239-a4887aeaa186
|
||||
github.com/davecgh/go-spew v1.1.0
|
||||
github.com/fastly/go-utils v0.0.0-20170926143046-88bf4bc30a29
|
||||
github.com/go-ole/go-ole v1.2.1
|
||||
github.com/influxdata/influxdb v1.5.4
|
||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869
|
||||
github.com/jonboulle/clockwork v0.1.0
|
||||
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc
|
||||
github.com/lestrrat-go/file-rotatelogs v0.0.0-20180607094457-00616292e771
|
||||
github.com/lestrrat-go/strftime v0.0.0-20180414112801-59966ecb6d84
|
||||
github.com/pkg/errors v0.8.0
|
||||
github.com/pmezard/go-difflib v1.0.0
|
||||
github.com/shirou/gopsutil v0.0.0-20180702150040-1c49dd8c6f1e
|
||||
github.com/stretchr/testify v1.2.2
|
||||
github.com/tebeka/strftime v0.0.0-20140926081919-3f9c7761e312
|
||||
go.uber.org/atomic v1.3.2
|
||||
go.uber.org/multierr v1.1.0
|
||||
go.uber.org/zap v1.8.0
|
||||
golang.org/x/sys v0.0.0-20180704094941-151529c776cd
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
)
|
|
@ -0,0 +1,75 @@
|
|||
amuz.es/src/infra/goutils v0.1.0/go.mod h1:yMrniY0O2X+1YLkLJnw2rdPej+nu3rQpxFITU8h1iao=
|
||||
amuz.es/src/infra/goutils/handler v0.0.0-20180612161152-d9d96073e8bd/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY=
|
||||
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/StackExchange/wmi v0.0.0-20180412205111-cdffdb33acae h1:Bqpru5NELaHtO/p7+TwRSKXWAMng4BCFBqVJ2eU8gpk=
|
||||
github.com/StackExchange/wmi v0.0.0-20180412205111-cdffdb33acae/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/coreos/go-systemd v0.0.0-20180525142239-a4887aeaa186 h1:d3qD/+gm+uFN6SEjkWwnI0m55lJ1TKk1sqwKlmGvOxI=
|
||||
github.com/coreos/go-systemd v0.0.0-20180525142239-a4887aeaa186/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/daemon v0.0.0-20180525142239-a4887aeaa186/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew/spew v0.0.0-20180221232628-8991bc29aa16/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
github.com/fastly/go-utils v0.0.0-20170926143046-88bf4bc30a29/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw=
|
||||
github.com/fastly/go-utils/strftime v0.0.0-20170926143046-88bf4bc30a29/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf/proto v0.0.0-20180622174009-9eb2c01ac278/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
github.com/influxdata/influxdb v1.5.4 h1:Mk3papmtopxk9N397Y5ldgkf8RWxzNigCnTlfuljS7s=
|
||||
github.com/influxdata/influxdb v1.5.4/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
|
||||
github.com/influxdata/influxdb/client v0.0.0-20180704104005-ef4e525546f5/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
github.com/influxdata/influxdb/client/v2 v2.0.0-20180704104005-ef4e525546f5/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
|
||||
github.com/lestrrat-go/file-rotatelogs v0.0.0-20180607094457-00616292e771 h1:OXGHg/CH8uOFy8qYz6amoQn92DF3/axESXbJ9HUkYkI=
|
||||
github.com/lestrrat-go/file-rotatelogs v0.0.0-20180607094457-00616292e771/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
|
||||
github.com/lestrrat-go/strftime v0.0.0-20180414112801-59966ecb6d84/go.mod h1:RMlXygAD3c48Psmr06d2G75L4E4xxzxkIe/+ppX9eAU=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/onsi/ginkgo v1.5.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib/difflib v0.0.0-20160110105554-792786c7400a/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
github.com/shirou/gopsutil v0.0.0-20180702150040-1c49dd8c6f1e h1:m0TaMtpFKX0wfsW8+G4/TlTr8AF8jscvNI3TOzEldtQ=
|
||||
github.com/shirou/gopsutil v0.0.0-20180702150040-1c49dd8c6f1e/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/gopsutil/cpu v0.0.0-20180702150040-1c49dd8c6f1e/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify/assert v0.0.0-20180609115518-f35b8ab0b5a2/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
github.com/tebeka/strftime v0.0.0-20140926081919-3f9c7761e312/go.mod h1:o6CrSUtupq/A5hylbvAsdydn0d5yokJExs8VVdx4wwI=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.8.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8 h1:h7zdf0RiEvWbYBKIx4b+q41xoUVnMmvsGZnIVE5syG8=
|
||||
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto/ssh v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
golang.org/x/crypto/ssh/terminal v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
golang.org/x/net v0.0.0-20180702212446-ed29d75add3d h1:B2RL9y12DFXBWEdHqZW1ts6ymJLN0FdBwL2mOY5zbCs=
|
||||
golang.org/x/net v0.0.0-20180702212446-ed29d75add3d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net/html v0.0.0-20180702212446-ed29d75add3d/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
golang.org/x/net/html/charset v0.0.0-20180702212446-ed29d75add3d/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync/errgroup v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
golang.org/x/sys v0.0.0-20180704094941-151529c776cd h1:KJQ1+cZBZLUUFD04lHVLuma4B5xAJP3LRGuqw08fV2E=
|
||||
golang.org/x/sys v0.0.0-20180704094941-151529c776cd/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys/unix v0.0.0-20180704094941-151529c776cd/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text/encoding v0.0.0-20180629073911-c0fe8dde8a10/go.mod h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3 h1:AFxeG48hTWHhDTQDk/m2gorfVHUEa9vo3tp3D7TzwjI=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
109
logger/logger.go
109
logger/logger.go
|
@ -1,109 +0,0 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/x-cray/logrus-prefixed-formatter"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = logrus.StandardLogger()
|
||||
rotaters []*lumberjack.Logger
|
||||
logDir string
|
||||
formatter = prefixed.TextFormatter{}
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
FileName string
|
||||
MaxSizeMb int
|
||||
MaxBackup int
|
||||
MaxDay int
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Debugf(format string, args ...interface{})
|
||||
Infof(format string, args ...interface{})
|
||||
Printf(format string, args ...interface{})
|
||||
Warnf(format string, args ...interface{})
|
||||
Warningf(format string, args ...interface{})
|
||||
Errorf(format string, args ...interface{})
|
||||
Fatalf(format string, args ...interface{})
|
||||
Panicf(format string, args ...interface{})
|
||||
Debug(args ...interface{})
|
||||
Info(args ...interface{})
|
||||
Print(args ...interface{})
|
||||
Warn(args ...interface{})
|
||||
Warning(args ...interface{})
|
||||
Error(args ...interface{})
|
||||
Fatal(args ...interface{})
|
||||
Panic(args ...interface{})
|
||||
Debugln(args ...interface{})
|
||||
Infoln(args ...interface{})
|
||||
Println(args ...interface{})
|
||||
Warnln(args ...interface{})
|
||||
Warningln(args ...interface{})
|
||||
Errorln(args ...interface{})
|
||||
Fatalln(args ...interface{})
|
||||
Panicln(args ...interface{})
|
||||
}
|
||||
|
||||
func init() {
|
||||
logger.Formatter = &formatter
|
||||
}
|
||||
func LoggerIsStd() bool {
|
||||
return !formatter.DisableColors
|
||||
}
|
||||
func InitLogger(verbose bool, logDirArg string, config *Config) {
|
||||
logDir = logDirArg
|
||||
colorSupport, writer := NewLogWriter(config)
|
||||
formatter.DisableColors = !colorSupport
|
||||
logger.Out = writer
|
||||
if verbose {
|
||||
logger.Level = logrus.DebugLevel
|
||||
} else {
|
||||
logger.Level = logrus.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
func NewLogger(prefix string) Logger {
|
||||
return logrus.NewEntry(logger).WithField("prefix", prefix)
|
||||
}
|
||||
func NewLogWriter(config *Config) (bool, io.Writer) {
|
||||
switch config.FileName {
|
||||
case "Stdout":
|
||||
return true, os.Stdout
|
||||
case "Stderr":
|
||||
return true, os.Stderr
|
||||
default:
|
||||
logpath := config.FileName
|
||||
if logDir != "" {
|
||||
logpath = path.Join(logDir, config.FileName)
|
||||
}
|
||||
logger.Info(" Attention!! log writes to ", config.FileName)
|
||||
rotater := &lumberjack.Logger{
|
||||
Filename: logpath,
|
||||
MaxSize: config.MaxSizeMb, // megabytes
|
||||
MaxBackups: config.MaxBackup,
|
||||
MaxAge: config.MaxDay, //days
|
||||
LocalTime: true,
|
||||
Compress: true,
|
||||
}
|
||||
rotaters = append(rotaters, rotater)
|
||||
return false, rotater
|
||||
}
|
||||
}
|
||||
|
||||
func RotateLogger() {
|
||||
if len(rotaters) == 0 {
|
||||
return
|
||||
}
|
||||
logger.Info("rotating logger")
|
||||
for _, rotater := range rotaters {
|
||||
rotater.Rotate()
|
||||
}
|
||||
logger.Info("rotated")
|
||||
}
|
205
main.go
205
main.go
|
@ -1,173 +1,84 @@
|
|||
package main
|
||||
package main // import "amuz.es/src/infra/cpu_ctrl"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"amuz.es/src/infra/cpu_ctrl/daemon"
|
||||
"amuz.es/src/infra/cpu_ctrl/logger"
|
||||
"amuz.es/src/infra/cpu_ctrl/processor"
|
||||
"amuz.es/src/infra/cpu_ctrl/util"
|
||||
"amuz.es/src/infra/cpu_ctrl/consumer"
|
||||
|
||||
"context"
|
||||
"os"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
"runtime"
|
||||
"amuz.es/src/infra/goutils/handler"
|
||||
)
|
||||
|
||||
const (
|
||||
name = "cpu_ctrl"
|
||||
)
|
||||
|
||||
// command argument
|
||||
var (
|
||||
app = kingpin.New("cpu_ctrl", "Interactive CPU fan controller").Author("Sangbum Kim")
|
||||
verbose = app.Flag("verbose", "Enable verbose mode.").Short('v').Bool()
|
||||
// 아래 부분은 컴파일시 하드코딩됨
|
||||
buildDate, version string
|
||||
|
||||
app = kingpin.New(name, fmt.Sprintf("%s - metric collector", name)).Author("Amazing from here")
|
||||
P = app.Flag("proportional-gain", "Set proportional gain value.").Short('p').Default("1.0").Float64()
|
||||
I = app.Flag("integral-gain", "Set integral gain value.").Short('i').Default("0.4").Float64()
|
||||
D = app.Flag("derivative-gain", "Set derivative gain value.").Short('d').Default("2.0").Float64()
|
||||
SetPoint = app.Flag("set-point", "Set pointe tempeture").Short('t').Default("40.0").Float64()
|
||||
SampleInterval = app.Flag("interval", "Set sampling interval").Short('s').Default("1s").Duration()
|
||||
InfluxHost = app.Flag("influx-host", "Set influx host").Short('h').Default("db:8089").TCP()
|
||||
verbose = app.Flag("verbose", "Enable verbose mode.").Short('v').Bool()
|
||||
|
||||
log = logger.NewLogger("cpu_ctrl")
|
||||
// 아래 부분은 실행될때 평가됨
|
||||
start = time.Now().Local()
|
||||
|
||||
logger = zap.L().Sugar()
|
||||
)
|
||||
|
||||
func init() {
|
||||
app.Version("0.3")
|
||||
defer finalCloser()
|
||||
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
// command 설정 로드
|
||||
app.Version(version)
|
||||
if _, err := app.Parse(os.Args[1:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
logger.InitLogger(*verbose, "", &logger.Config{FileName: "Stderr"})
|
||||
|
||||
setMaxProcs()
|
||||
}
|
||||
|
||||
func setMaxProcs() {
|
||||
// TODO(vmarmol): Consider limiting if we have a CPU mask in effect.
|
||||
// Allow as many threads as we have cores unless the user specified a value.
|
||||
var numProcs int
|
||||
// if *maxProcs < 1 {
|
||||
numProcs = runtime.NumCPU()
|
||||
// } else {
|
||||
// numProcs = *maxProcs
|
||||
// }
|
||||
runtime.GOMAXPROCS(numProcs)
|
||||
|
||||
// Check if the setting was successful.
|
||||
actualNumProcs := runtime.GOMAXPROCS(0)
|
||||
if actualNumProcs != numProcs {
|
||||
log.Printf("Specified max procs of %v but using %v\n", numProcs, actualNumProcs)
|
||||
}
|
||||
}
|
||||
|
||||
func FanoutSpeed(sender <-chan processor.FanspeedInfo, handler util.Handler, receivers ...chan<- processor.FanspeedInfo) {
|
||||
defer handler.DecreaseWait()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
handler.NotifyError(err.(error))
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case tempeture := <-sender:
|
||||
for _, receiver := range receivers {
|
||||
select {
|
||||
case receiver <- tempeture:
|
||||
default:
|
||||
log.Warn("Some Fanspeed consumer blocked!")
|
||||
}
|
||||
}
|
||||
runtime.Gosched()
|
||||
|
||||
case <-handler.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FanoutTempeture(sender <-chan processor.TempetureInfo, handler util.Handler, receivers ...chan<- processor.TempetureInfo) {
|
||||
defer handler.DecreaseWait()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
handler.NotifyError(err.(error))
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case tempeture := <-sender:
|
||||
for _, receiver := range receivers {
|
||||
select {
|
||||
case receiver <- tempeture:
|
||||
default:
|
||||
log.Warn("Some Tempeture consumer blocked!")
|
||||
}
|
||||
}
|
||||
runtime.Gosched()
|
||||
|
||||
case <-handler.Done():
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, err.(error).Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 엔트리 포인트
|
||||
func main() {
|
||||
defer finalCloser()
|
||||
|
||||
var (
|
||||
processorCount = processor.GetProcessorCount()
|
||||
processors []processor.Processor
|
||||
exitSignal = make(chan os.Signal, 1)
|
||||
handler = util.NewHandler()
|
||||
sampleDuration = time.Second
|
||||
// 로그 초기화
|
||||
loggerCleanup := initLogger()
|
||||
defer loggerCleanup()
|
||||
|
||||
// 컨텍스트 생성
|
||||
// handler내부에서도 로거를 이용하기 때문에 로깅부분 밑에 위치해야 된다.
|
||||
mainHandler := handler.NewHandler(context.Background())
|
||||
|
||||
// hello ;-)
|
||||
logger.Infof(
|
||||
"%s(%s), built: %s",
|
||||
name,
|
||||
version,
|
||||
buildDate,
|
||||
)
|
||||
log.Infof("Cpu fan controller")
|
||||
|
||||
if processorCount == 0 {
|
||||
handler.NotifyError(errors.New("cpu not found!"))
|
||||
}
|
||||
var (
|
||||
tempetureChannel = make(chan processor.TempetureInfo,1)
|
||||
fanspeedChannel = make(chan processor.FanspeedInfo,1)
|
||||
)
|
||||
processors = make([]processor.Processor, 0, processorCount)
|
||||
for i := 0; i < processorCount; i++ {
|
||||
if info, err := processor.NewProcessorInfo(handler, i, sampleDuration,
|
||||
*P, *I, *D, *SetPoint, 0x64, 0x4,
|
||||
tempetureChannel, fanspeedChannel,
|
||||
);
|
||||
err != nil {
|
||||
handler.NotifyError(err)
|
||||
} else {
|
||||
processors = append(processors, info)
|
||||
handler.IncreaseWait()
|
||||
go info.StartMonitoring()
|
||||
}
|
||||
}
|
||||
// 컨텍스트 띄우기
|
||||
processorCleanup := initProcessor(mainHandler)
|
||||
defer processorCleanup()
|
||||
|
||||
fanController := consumer.NewFanControl(processorCount, sampleDuration, handler)
|
||||
metricLogger := consumer.NewInfluxMetric("db:8089", processorCount, handler)
|
||||
// 뭔가 문제가 나거나 signal이 발생할 때 까지 애플리케이션이 종료로 빠지지 않게 잡아주는 부분
|
||||
contextWatcher, contextCloser := initContext(mainHandler)
|
||||
defer contextCloser()
|
||||
|
||||
|
||||
handler.IncreaseWait()
|
||||
go FanoutTempeture(tempetureChannel, handler, metricLogger.TempetureConsumer())
|
||||
defer close(tempetureChannel)
|
||||
|
||||
handler.IncreaseWait()
|
||||
go FanoutSpeed(fanspeedChannel, handler, fanController.Consumer(), metricLogger.FanSpeedConsumer())
|
||||
defer close(fanspeedChannel)
|
||||
|
||||
handler.IncreaseWait()
|
||||
go fanController.StartControl()
|
||||
handler.IncreaseWait()
|
||||
go metricLogger.StartLogging()
|
||||
|
||||
signal.Notify(exitSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
daemon.NotifyDaemon(daemon.DaemonStarted)
|
||||
defer daemon.NotifyDaemon(daemon.DaemonStopping)
|
||||
defer close(exitSignal)
|
||||
defer handler.GracefullWait()
|
||||
|
||||
select {
|
||||
case <-handler.Done():
|
||||
log.Infoln("Service request to close this application")
|
||||
case err := <-handler.Error():
|
||||
log.Errorf("%s", err)
|
||||
case sysSignal := <-exitSignal:
|
||||
log.Warnf("SYSCALL! %s", sysSignal.String())
|
||||
}
|
||||
// ok
|
||||
logger.Info("bootstrapped application ", time.Since(start), " ", time.Now())
|
||||
contextWatcher()
|
||||
logger.Info("served duration: ", time.Since(start))
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package producer
|
||||
|
||||
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
)
|
||||
|
||||
// 블럭되지 않는 큐체널
|
||||
func NewTempetureQueue() (chan<- TempetureInfo, <-chan TempetureInfo) {
|
||||
send := make(chan TempetureInfo, 1)
|
||||
receive := make(chan TempetureInfo, 1)
|
||||
go manageTempetureQueue(send, receive)
|
||||
return send, receive
|
||||
}
|
||||
|
||||
func manageTempetureQueue(send <-chan TempetureInfo, receive chan<- TempetureInfo) {
|
||||
queue := list.New()
|
||||
defer close(receive)
|
||||
for {
|
||||
if front := queue.Front(); front == nil {
|
||||
if value, ok := <-send; ok {
|
||||
queue.PushBack(value)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
select {
|
||||
case receive <- front.Value.(TempetureInfo):
|
||||
queue.Remove(front)
|
||||
case value, ok := <-send:
|
||||
if ok {
|
||||
queue.PushBack(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package producer
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
)
|
||||
|
||||
// 블럭되지 않는 큐체널
|
||||
func NewFanspeedQueue() (chan<- FanspeedInfo, <-chan FanspeedInfo) {
|
||||
send := make(chan FanspeedInfo, 1)
|
||||
receive := make(chan FanspeedInfo, 1)
|
||||
go manageFanspeedQueue(send, receive)
|
||||
return send, receive
|
||||
}
|
||||
|
||||
func manageFanspeedQueue(send <-chan FanspeedInfo, receive chan<- FanspeedInfo) {
|
||||
queue := list.New()
|
||||
defer close(receive)
|
||||
for {
|
||||
if front := queue.Front(); front == nil {
|
||||
if value, ok := <-send; ok {
|
||||
queue.PushBack(value)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
select {
|
||||
case receive <- front.Value.(FanspeedInfo):
|
||||
queue.Remove(front)
|
||||
case value, ok := <-send:
|
||||
if ok {
|
||||
queue.PushBack(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package processor
|
||||
package producer
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
|
@ -9,16 +9,18 @@ import (
|
|||
"time"
|
||||
"sync"
|
||||
"path"
|
||||
"amuz.es/src/infra/cpu_ctrl/logger"
|
||||
zlog "amuz.es/src/infra/goutils/logger/zap"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"amuz.es/src/infra/cpu_ctrl/pid"
|
||||
"amuz.es/src/infra/cpu_ctrl/util"
|
||||
"amuz.es/src/infra/goutils/handler"
|
||||
"runtime"
|
||||
"context"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type processor struct {
|
||||
handler util.Handler
|
||||
handler *handler.Handler
|
||||
id int
|
||||
tempeturePath string
|
||||
tempeture float64
|
||||
|
@ -29,6 +31,7 @@ type processor struct {
|
|||
fanMaxSpeed int
|
||||
fanMinSpeed int
|
||||
fanSpeedChanged chan<- FanspeedInfo
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
type TempetureInfo struct {
|
||||
|
@ -55,10 +58,6 @@ type Processor interface {
|
|||
normalizeFanspeed(float64) (int)
|
||||
}
|
||||
|
||||
var (
|
||||
log = logger.NewLogger("processor")
|
||||
)
|
||||
|
||||
func GetProcessorCount() (maxcpu int) {
|
||||
stat, err := cpu.Info()
|
||||
if err != nil {
|
||||
|
@ -77,7 +76,7 @@ func GetProcessorCount() (maxcpu int) {
|
|||
}
|
||||
|
||||
func NewProcessorInfo(
|
||||
handler util.Handler,
|
||||
handler *handler.Handler,
|
||||
processorId int,
|
||||
sampleDuration time.Duration,
|
||||
P, I, D,
|
||||
|
@ -109,6 +108,7 @@ func NewProcessorInfo(
|
|||
fanSpeedChanged: fanSpeedChanged,
|
||||
fanMaxSpeed: maxNoob,
|
||||
fanMinSpeed: minNoob,
|
||||
logger: zlog.New(nil, "processor"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
@ -119,7 +119,6 @@ func (p *processor) FanMaxSpeed() int { return p.fanMaxSpeed }
|
|||
func (p *processor) FanMinSpeed() int { return p.fanMinSpeed }
|
||||
|
||||
func (p *processor) StartMonitoring() {
|
||||
defer p.handler.DecreaseWait()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
p.handler.NotifyError(err.(error))
|
||||
|
@ -128,8 +127,8 @@ func (p *processor) StartMonitoring() {
|
|||
|
||||
tempeturePathGlob := path.Join(p.tempeturePath, "temp?_input")
|
||||
ticker := time.Tick(p.sampleDuration)
|
||||
defer log.Infof("Processor %d monitor stopped", p.id)
|
||||
log.Infof("Processor %d monitor started with %s", p.id, p.sampleDuration)
|
||||
defer p.logger.Infof("Processor %d monitor stopped", p.id)
|
||||
p.logger.Infof("Processor %d monitor started with %s", p.id, p.sampleDuration)
|
||||
|
||||
for {
|
||||
select {
|
||||
|
@ -155,7 +154,7 @@ func (p *processor) StartMonitoring() {
|
|||
}
|
||||
}
|
||||
p.tempeture = highestTemp
|
||||
log.Debugf("processor %d : tempeture changed %f", p.id, highestTemp)
|
||||
p.logger.Debugf("processor %d : tempeture changed %f", p.id, highestTemp)
|
||||
|
||||
/*
|
||||
SELECT mean("noob") as noob FROM "processor_cooling_fanspeed" WHERE $timeFilter GROUP BY "processor", time(5s) fill(previous) CREATE CONTINUOUS QUERY "processor_cooling_fanspeed_5s" ON "core" BEGIN SELECT max("noob") AS "mean_noob" INTO "processor_cooling_fanspeed_5s" FROM "processor_cooling_fanspeed" GROUP BY "processor", time(5s) fill(previous) END
|
||||
|
@ -173,7 +172,7 @@ func (p *processor) StartMonitoring() {
|
|||
}
|
||||
}
|
||||
p.fanSpeed = fanspeed
|
||||
log.Debugf("processor %d : fan changed 0x%x", p.id, fanspeed)
|
||||
p.logger.Debugf("processor %d : fan changed 0x%x", p.id, fanspeed)
|
||||
case <-p.handler.Done():
|
||||
return
|
||||
}
|
||||
|
@ -224,3 +223,43 @@ func (p *processor) normalizeFanspeed(response float64) (adjusted int) {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
func AggregateProcessorChannel(
|
||||
mainHandler *handler.Handler,
|
||||
sampleDuration time.Duration,
|
||||
processorCount int,
|
||||
P, I, D, SetPoint float64,
|
||||
) (<-chan TempetureInfo, <-chan FanspeedInfo) {
|
||||
mainHandler.IncreaseWait()
|
||||
aggHandler := handler.NewHandler(context.Background())
|
||||
|
||||
var (
|
||||
fin, fout = NewFanspeedQueue()
|
||||
tin, tout = NewTempetureQueue()
|
||||
)
|
||||
|
||||
go func() {
|
||||
<-aggHandler.Done()
|
||||
aggHandler.GracefulWait()
|
||||
close(tin)
|
||||
close(fin)
|
||||
mainHandler.DecreaseWait()
|
||||
}()
|
||||
|
||||
processors := make([]Processor, 0, processorCount)
|
||||
for i := 0; i < processorCount; i++ {
|
||||
if info, err := NewProcessorInfo(aggHandler, i, sampleDuration,
|
||||
P, I, D, SetPoint,
|
||||
0x64, 0x4,
|
||||
tin, fin,
|
||||
);
|
||||
err != nil {
|
||||
aggHandler.NotifyError(err)
|
||||
} else {
|
||||
processors = append(processors, info)
|
||||
aggHandler.IncreaseWait()
|
||||
go info.StartMonitoring()
|
||||
}
|
||||
}
|
||||
return tout, fout
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"context"
|
||||
"amuz.es/src/infra/cpu_ctrl/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logger.NewLogger("util")
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
errorChan chan error
|
||||
ctx context.Context
|
||||
canceler context.CancelFunc
|
||||
waiter *sync.WaitGroup
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
NotifyError(err error)
|
||||
Error() <-chan error
|
||||
Done() <-chan struct{}
|
||||
GracefullWait()
|
||||
IncreaseWait()
|
||||
DecreaseWait()
|
||||
}
|
||||
|
||||
func NewHandler() Handler {
|
||||
ctx, canceler := context.WithCancel(context.Background())
|
||||
return &handler{
|
||||
ctx: ctx,
|
||||
canceler: canceler,
|
||||
waiter: &sync.WaitGroup{},
|
||||
errorChan: make(chan error,1),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) NotifyError(err error) { h.errorChan <- err }
|
||||
func (h *handler) Error() <-chan error { return h.errorChan }
|
||||
func (h *handler) Done() <-chan struct{} { return h.ctx.Done() }
|
||||
func (h *handler) GracefullWait() {
|
||||
if h.ctx.Err() == nil {
|
||||
h.canceler()
|
||||
}
|
||||
h.waiter.Wait()
|
||||
close(h.errorChan)
|
||||
|
||||
for remainError := range h.errorChan {
|
||||
log.Errorf("%s", remainError)
|
||||
}
|
||||
}
|
||||
func (h *handler) IncreaseWait() { h.waiter.Add(1) }
|
||||
func (h *handler) DecreaseWait() { h.waiter.Done() }
|
|
@ -0,0 +1,48 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"context"
|
||||
"log"
|
||||
)
|
||||
|
||||
// 자식들을 기다리는 context waiter
|
||||
type Handler struct {
|
||||
errorChan chan error
|
||||
ctx context.Context
|
||||
canceler context.CancelFunc
|
||||
waiter *sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewHandler(ctx context.Context) *Handler {
|
||||
ctx, canceler := context.WithCancel(ctx)
|
||||
return &Handler{
|
||||
ctx: ctx,
|
||||
canceler: canceler,
|
||||
waiter: &sync.WaitGroup{},
|
||||
errorChan: make(chan error, 5),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) NotifyError(err error) { h.errorChan <- err }
|
||||
func (h *Handler) Error() <-chan error { return h.errorChan }
|
||||
func (h *Handler) Done() <-chan struct{} { return h.ctx.Done() }
|
||||
func (h *Handler) GracefulWait() {
|
||||
if h.ctx.Err() == nil {
|
||||
h.canceler()
|
||||
}
|
||||
|
||||
h.waiter.Wait()
|
||||
close(h.errorChan)
|
||||
|
||||
for remainError := range h.errorChan {
|
||||
log.Println("remain errors ", remainError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) IncreaseWait() {
|
||||
h.waiter.Add(1)
|
||||
}
|
||||
func (h *Handler) DecreaseWait() {
|
||||
h.waiter.Done()
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package logger
|
||||
|
||||
import "io"
|
||||
|
||||
type WriteSyncer interface {
|
||||
io.WriteCloser
|
||||
Sync() error
|
||||
}
|
||||
|
||||
type RotateSyncer interface {
|
||||
WriteSyncer
|
||||
SetOnClose(func())
|
||||
Rotate() error
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package rotater
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"log"
|
||||
"amuz.es/src/infra/goutils/logger"
|
||||
"github.com/lestrrat-go/file-rotatelogs"
|
||||
)
|
||||
|
||||
var loggers RotateSyncerSet
|
||||
|
||||
func NewLogWriter(FileName string, logDir string, options ...Option) (logger.RotateSyncer, error) {
|
||||
switch FileName {
|
||||
case "Stdout":
|
||||
return NewLocked(os.Stdout), nil
|
||||
case "Stderr":
|
||||
return NewLocked(os.Stderr), nil
|
||||
default:
|
||||
logpath := FileName
|
||||
if logDir != "" {
|
||||
logpath = filepath.Join(logDir, FileName)
|
||||
}
|
||||
logpath,_ = filepath.Abs(logpath)
|
||||
log.Println(" Attention!! log writes to ", logpath)
|
||||
options=append(options,rotatelogs.WithLinkName(logpath))
|
||||
if logWriter, err := NewRotater(logpath+".%Y%m%d", options...); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
loggers.Store(logWriter)
|
||||
logWriter.SetOnClose(func() { loggers.Delete(logWriter) })
|
||||
return logWriter, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Rotate() {
|
||||
loggers.Range(func(rotater logger.RotateSyncer) {
|
||||
rotater.Sync()
|
||||
rotater.Rotate()
|
||||
})
|
||||
log.Println("rotated")
|
||||
}
|
||||
|
||||
func Close() {
|
||||
loggers.Range(func(rotater logger.RotateSyncer) {
|
||||
rotater.Sync()
|
||||
rotater.Close()
|
||||
})
|
||||
log.Println("end of log")
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package rotater
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"amuz.es/src/infra/goutils/logger"
|
||||
)
|
||||
|
||||
/**
|
||||
logger set
|
||||
*/
|
||||
type RotateSyncerSet struct {
|
||||
storage sync.Map
|
||||
}
|
||||
|
||||
func (s *RotateSyncerSet) Delete(key logger.RotateSyncer) {
|
||||
s.storage.Delete(key)
|
||||
}
|
||||
func (s *RotateSyncerSet) Exist(key logger.RotateSyncer) (ok bool) {
|
||||
_, ok = s.storage.Load(key)
|
||||
return
|
||||
}
|
||||
func (s *RotateSyncerSet) SetNx(key logger.RotateSyncer) (bool) {
|
||||
_, exist := s.storage.LoadOrStore(key, 0)
|
||||
return !exist
|
||||
}
|
||||
func (s *RotateSyncerSet) Range(f func(key logger.RotateSyncer)) {
|
||||
s.storage.Range(s.rangeWrap(f))
|
||||
}
|
||||
func (s *RotateSyncerSet) Store(key logger.RotateSyncer) {
|
||||
s.storage.Store(key, 0)
|
||||
}
|
||||
func (s *RotateSyncerSet) rangeWrap(f func(key logger.RotateSyncer)) func(key, value interface{}) bool {
|
||||
ok := true
|
||||
return func(key, value interface{}) bool {
|
||||
f(key.(logger.RotateSyncer))
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RotateSyncerSet) Len() int {
|
||||
var count uint64
|
||||
s.Range(func(conn logger.RotateSyncer) {
|
||||
atomic.AddUint64(&count, 1)
|
||||
})
|
||||
return int(count)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package rotater
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"amuz.es/src/infra/goutils/logger"
|
||||
)
|
||||
|
||||
type LockedWriteSyncer struct {
|
||||
sync.Mutex
|
||||
ws logger.WriteSyncer
|
||||
}
|
||||
|
||||
// Lock wraps a WriteSyncer in a mutex to make it safe for concurrent use. In
|
||||
// particular, *os.Files must be locked before use.
|
||||
func NewLocked(ws logger.WriteSyncer) logger.RotateSyncer {
|
||||
if lws, ok := ws.(*LockedWriteSyncer); ok {
|
||||
// no need to layer on another lock
|
||||
return lws
|
||||
}
|
||||
return &LockedWriteSyncer{ws: ws}
|
||||
}
|
||||
|
||||
func (s *LockedWriteSyncer) Write(bs []byte) (int, error) {
|
||||
s.Lock()
|
||||
n, err := s.ws.Write(bs)
|
||||
s.Unlock()
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (s *LockedWriteSyncer) Sync() error {
|
||||
s.Lock()
|
||||
err := s.ws.Sync()
|
||||
s.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *LockedWriteSyncer) SetOnClose(closeFunc func()) {}
|
||||
func (r *LockedWriteSyncer) Rotate() (err error) { return }
|
||||
func (r *LockedWriteSyncer) Close() (err error) { return }
|
|
@ -0,0 +1,51 @@
|
|||
package rotater
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"amuz.es/src/infra/goutils/logger"
|
||||
"github.com/lestrrat-go/file-rotatelogs"
|
||||
)
|
||||
|
||||
type Option = rotatelogs.Option
|
||||
|
||||
type rotateSyncer struct {
|
||||
setOnceOnclose *sync.Once
|
||||
onClose func()
|
||||
*rotatelogs.RotateLogs
|
||||
}
|
||||
|
||||
func NewRotater(filename string, options ...Option) (logger.RotateSyncer, error) {
|
||||
if rotateLogger, err := rotatelogs.New(filename, options...); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &rotateSyncer{
|
||||
setOnceOnclose: &sync.Once{},
|
||||
RotateLogs: rotateLogger,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
func (r *rotateSyncer) SetOnClose(closeFunc func()) {
|
||||
r.setOnceOnclose.Do(func() {
|
||||
r.onClose = closeFunc
|
||||
})
|
||||
}
|
||||
|
||||
func (r *rotateSyncer) Rotate() error {
|
||||
return r.RotateLogs.Rotate()
|
||||
}
|
||||
func (r *rotateSyncer) Close() error {
|
||||
defer func() {
|
||||
if r.onClose != nil {
|
||||
r.onClose()
|
||||
}
|
||||
}()
|
||||
return r.RotateLogs.Close()
|
||||
}
|
||||
|
||||
func (r *rotateSyncer) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *rotateSyncer) Write(bs []byte) (int, error) {
|
||||
return s.RotateLogs.Write(bs)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package zap
|
||||
|
||||
import "go.uber.org/zap/zapcore"
|
||||
|
||||
var LogCommonFormat = zapcore.EncoderConfig{
|
||||
TimeKey: "ts",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||
EncodeDuration: zapcore.StringDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package zap
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
"amuz.es/src/infra/goutils/logger"
|
||||
"amuz.es/src/infra/goutils/logger/rotater"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultWriter logger.RotateSyncer
|
||||
defaultErrorOutputOptions []zap.Option
|
||||
nopCloser = func() (err error) { return }
|
||||
)
|
||||
|
||||
func init() {
|
||||
zap.RedirectStdLog(zap.L())
|
||||
}
|
||||
|
||||
func replaceGlobalLogger(newOne *zap.Logger) {
|
||||
zap.ReplaceGlobals(newOne)
|
||||
zap.RedirectStdLog(newOne)
|
||||
}
|
||||
|
||||
func Init(
|
||||
verbose bool,
|
||||
formatter zapcore.Encoder,
|
||||
mainLogName, logFilename, logDir string,
|
||||
rotateOption []rotater.Option,
|
||||
logLevel zapcore.Level,
|
||||
additionalOptions ...zap.Option,
|
||||
) (*zap.SugaredLogger, error) {
|
||||
level := zap.NewAtomicLevelAt(logLevel)
|
||||
if defaultWriter, err := rotater.NewLogWriter(logFilename, logDir, rotateOption...); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defaultErrorOutputOptions = []zap.Option{zap.ErrorOutput(defaultWriter)}
|
||||
options := defaultErrorOutputOptions
|
||||
if verbose {
|
||||
options = append(options, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.PanicLevel)))
|
||||
}
|
||||
// reset log option slice
|
||||
options = append(options, additionalOptions...)
|
||||
|
||||
log := initLogger(defaultWriter, mainLogName, formatter, level, options...)
|
||||
|
||||
replaceGlobalLogger(log)
|
||||
return log.Sugar(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func New(parent *zap.SugaredLogger, moduleName string, options ...zap.Option) *zap.SugaredLogger {
|
||||
var subLogger *zap.Logger
|
||||
if parent == nil {
|
||||
subLogger = zap.L().Named(moduleName)
|
||||
} else {
|
||||
subLogger = parent.Desugar().Named(moduleName)
|
||||
}
|
||||
|
||||
subLogger.WithOptions(options...)
|
||||
|
||||
return subLogger.Sugar()
|
||||
}
|
||||
|
||||
func NewOtherLogger(
|
||||
formatter zapcore.Encoder,
|
||||
moduleName, logFilename, logDir string,
|
||||
rotateOption []rotater.Option,
|
||||
logLevel zapcore.Level,
|
||||
fields ...zapcore.Field,
|
||||
) (logger *zap.SugaredLogger, closer func() error, err error) {
|
||||
loglevel := zap.NewAtomicLevelAt(logLevel)
|
||||
logWriter, err := rotater.NewLogWriter(logFilename, logDir, rotateOption...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
core := zapcore.NewCore(formatter, logWriter, loglevel)
|
||||
closer = logWriter.Close
|
||||
logger = zap.New(core, defaultErrorOutputOptions...).
|
||||
Named(moduleName).With(fields...).Sugar()
|
||||
return
|
||||
}
|
||||
|
||||
func NewOtherLoggerWithOption(
|
||||
formatter zapcore.Encoder,
|
||||
moduleName, logFilename, logDir string,
|
||||
rotateOption []rotater.Option,
|
||||
logLevel zapcore.Level,
|
||||
options []zap.Option,
|
||||
fields ...zapcore.Field,
|
||||
) (logger *zap.SugaredLogger, closer func() error, err error) {
|
||||
loglevel := zap.NewAtomicLevelAt(logLevel)
|
||||
logWriter, err := rotater.NewLogWriter(logFilename, logDir, rotateOption...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
core := zapcore.NewCore(formatter, logWriter, loglevel)
|
||||
closer = logWriter.Close
|
||||
options = append(defaultErrorOutputOptions, options...)
|
||||
logger = zap.New(core, options...).
|
||||
Named(moduleName).With(fields...).Sugar()
|
||||
return
|
||||
}
|
||||
func initLogger(
|
||||
writer zapcore.WriteSyncer,
|
||||
moduleName string,
|
||||
formatter zapcore.Encoder,
|
||||
level zap.AtomicLevel,
|
||||
options ...zap.Option,
|
||||
) *zap.Logger {
|
||||
core := zapcore.NewCore(formatter, writer, level)
|
||||
return zap.New(core, options...).Named(moduleName)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package zap
|
||||
|
||||
import (
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
type zapWrappedSyncer struct {
|
||||
zapcore.WriteSyncer
|
||||
}
|
||||
|
||||
func (r *zapWrappedSyncer) SetOnClose(closeFunc func()) {}
|
||||
func (r *zapWrappedSyncer) Rotate() (err error) { return }
|
||||
func (r *zapWrappedSyncer) Close() (err error) { return }
|
||||
func (r *zapWrappedSyncer) Sync() error { return r.WriteSyncer.Sync() }
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Stack Exchange
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,6 @@
|
|||
wmi
|
||||
===
|
||||
|
||||
Package wmi provides a WQL interface to Windows WMI.
|
||||
|
||||
Note: It interfaces with WMI on the local machine, therefore it only runs on Windows.
|
|
@ -0,0 +1,260 @@
|
|||
// +build windows
|
||||
|
||||
package wmi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
"github.com/go-ole/go-ole/oleutil"
|
||||
)
|
||||
|
||||
// SWbemServices is used to access wmi. See https://msdn.microsoft.com/en-us/library/aa393719(v=vs.85).aspx
|
||||
type SWbemServices struct {
|
||||
//TODO: track namespace. Not sure if we can re connect to a different namespace using the same instance
|
||||
cWMIClient *Client //This could also be an embedded struct, but then we would need to branch on Client vs SWbemServices in the Query method
|
||||
sWbemLocatorIUnknown *ole.IUnknown
|
||||
sWbemLocatorIDispatch *ole.IDispatch
|
||||
queries chan *queryRequest
|
||||
closeError chan error
|
||||
lQueryorClose sync.Mutex
|
||||
}
|
||||
|
||||
type queryRequest struct {
|
||||
query string
|
||||
dst interface{}
|
||||
args []interface{}
|
||||
finished chan error
|
||||
}
|
||||
|
||||
// InitializeSWbemServices will return a new SWbemServices object that can be used to query WMI
|
||||
func InitializeSWbemServices(c *Client, connectServerArgs ...interface{}) (*SWbemServices, error) {
|
||||
//fmt.Println("InitializeSWbemServices: Starting")
|
||||
//TODO: implement connectServerArgs as optional argument for init with connectServer call
|
||||
s := new(SWbemServices)
|
||||
s.cWMIClient = c
|
||||
s.queries = make(chan *queryRequest)
|
||||
initError := make(chan error)
|
||||
go s.process(initError)
|
||||
|
||||
err, ok := <-initError
|
||||
if ok {
|
||||
return nil, err //Send error to caller
|
||||
}
|
||||
//fmt.Println("InitializeSWbemServices: Finished")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Close will clear and release all of the SWbemServices resources
|
||||
func (s *SWbemServices) Close() error {
|
||||
s.lQueryorClose.Lock()
|
||||
if s == nil || s.sWbemLocatorIDispatch == nil {
|
||||
s.lQueryorClose.Unlock()
|
||||
return fmt.Errorf("SWbemServices is not Initialized")
|
||||
}
|
||||
if s.queries == nil {
|
||||
s.lQueryorClose.Unlock()
|
||||
return fmt.Errorf("SWbemServices has been closed")
|
||||
}
|
||||
//fmt.Println("Close: sending close request")
|
||||
var result error
|
||||
ce := make(chan error)
|
||||
s.closeError = ce //Race condition if multiple callers to close. May need to lock here
|
||||
close(s.queries) //Tell background to shut things down
|
||||
s.lQueryorClose.Unlock()
|
||||
err, ok := <-ce
|
||||
if ok {
|
||||
result = err
|
||||
}
|
||||
//fmt.Println("Close: finished")
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SWbemServices) process(initError chan error) {
|
||||
//fmt.Println("process: starting background thread initialization")
|
||||
//All OLE/WMI calls must happen on the same initialized thead, so lock this goroutine
|
||||
runtime.LockOSThread()
|
||||
defer runtime.LockOSThread()
|
||||
|
||||
err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
|
||||
if err != nil {
|
||||
oleCode := err.(*ole.OleError).Code()
|
||||
if oleCode != ole.S_OK && oleCode != S_FALSE {
|
||||
initError <- fmt.Errorf("ole.CoInitializeEx error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
defer ole.CoUninitialize()
|
||||
|
||||
unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator")
|
||||
if err != nil {
|
||||
initError <- fmt.Errorf("CreateObject SWbemLocator error: %v", err)
|
||||
return
|
||||
} else if unknown == nil {
|
||||
initError <- ErrNilCreateObject
|
||||
return
|
||||
}
|
||||
defer unknown.Release()
|
||||
s.sWbemLocatorIUnknown = unknown
|
||||
|
||||
dispatch, err := s.sWbemLocatorIUnknown.QueryInterface(ole.IID_IDispatch)
|
||||
if err != nil {
|
||||
initError <- fmt.Errorf("SWbemLocator QueryInterface error: %v", err)
|
||||
return
|
||||
}
|
||||
defer dispatch.Release()
|
||||
s.sWbemLocatorIDispatch = dispatch
|
||||
|
||||
// we can't do the ConnectServer call outside the loop unless we find a way to track and re-init the connectServerArgs
|
||||
//fmt.Println("process: initialized. closing initError")
|
||||
close(initError)
|
||||
//fmt.Println("process: waiting for queries")
|
||||
for q := range s.queries {
|
||||
//fmt.Printf("process: new query: len(query)=%d\n", len(q.query))
|
||||
errQuery := s.queryBackground(q)
|
||||
//fmt.Println("process: s.queryBackground finished")
|
||||
if errQuery != nil {
|
||||
q.finished <- errQuery
|
||||
}
|
||||
close(q.finished)
|
||||
}
|
||||
//fmt.Println("process: queries channel closed")
|
||||
s.queries = nil //set channel to nil so we know it is closed
|
||||
//TODO: I think the Release/Clear calls can panic if things are in a bad state.
|
||||
//TODO: May need to recover from panics and send error to method caller instead.
|
||||
close(s.closeError)
|
||||
}
|
||||
|
||||
// Query runs the WQL query using a SWbemServices instance and appends the values to dst.
|
||||
//
|
||||
// dst must have type *[]S or *[]*S, for some struct type S. Fields selected in
|
||||
// the query must have the same name in dst. Supported types are all signed and
|
||||
// unsigned integers, time.Time, string, bool, or a pointer to one of those.
|
||||
// Array types are not supported.
|
||||
//
|
||||
// By default, the local machine and default namespace are used. These can be
|
||||
// changed using connectServerArgs. See
|
||||
// http://msdn.microsoft.com/en-us/library/aa393720.aspx for details.
|
||||
func (s *SWbemServices) Query(query string, dst interface{}, connectServerArgs ...interface{}) error {
|
||||
s.lQueryorClose.Lock()
|
||||
if s == nil || s.sWbemLocatorIDispatch == nil {
|
||||
s.lQueryorClose.Unlock()
|
||||
return fmt.Errorf("SWbemServices is not Initialized")
|
||||
}
|
||||
if s.queries == nil {
|
||||
s.lQueryorClose.Unlock()
|
||||
return fmt.Errorf("SWbemServices has been closed")
|
||||
}
|
||||
|
||||
//fmt.Println("Query: Sending query request")
|
||||
qr := queryRequest{
|
||||
query: query,
|
||||
dst: dst,
|
||||
args: connectServerArgs,
|
||||
finished: make(chan error),
|
||||
}
|
||||
s.queries <- &qr
|
||||
s.lQueryorClose.Unlock()
|
||||
err, ok := <-qr.finished
|
||||
if ok {
|
||||
//fmt.Println("Query: Finished with error")
|
||||
return err //Send error to caller
|
||||
}
|
||||
//fmt.Println("Query: Finished")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SWbemServices) queryBackground(q *queryRequest) error {
|
||||
if s == nil || s.sWbemLocatorIDispatch == nil {
|
||||
return fmt.Errorf("SWbemServices is not Initialized")
|
||||
}
|
||||
wmi := s.sWbemLocatorIDispatch //Should just rename in the code, but this will help as we break things apart
|
||||
//fmt.Println("queryBackground: Starting")
|
||||
|
||||
dv := reflect.ValueOf(q.dst)
|
||||
if dv.Kind() != reflect.Ptr || dv.IsNil() {
|
||||
return ErrInvalidEntityType
|
||||
}
|
||||
dv = dv.Elem()
|
||||
mat, elemType := checkMultiArg(dv)
|
||||
if mat == multiArgTypeInvalid {
|
||||
return ErrInvalidEntityType
|
||||
}
|
||||
|
||||
// service is a SWbemServices
|
||||
serviceRaw, err := oleutil.CallMethod(wmi, "ConnectServer", q.args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := serviceRaw.ToIDispatch()
|
||||
defer serviceRaw.Clear()
|
||||
|
||||
// result is a SWBemObjectSet
|
||||
resultRaw, err := oleutil.CallMethod(service, "ExecQuery", q.query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result := resultRaw.ToIDispatch()
|
||||
defer resultRaw.Clear()
|
||||
|
||||
count, err := oleInt64(result, "Count")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
enumProperty, err := result.GetProperty("_NewEnum")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer enumProperty.Clear()
|
||||
|
||||
enum, err := enumProperty.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enum == nil {
|
||||
return fmt.Errorf("can't get IEnumVARIANT, enum is nil")
|
||||
}
|
||||
defer enum.Release()
|
||||
|
||||
// Initialize a slice with Count capacity
|
||||
dv.Set(reflect.MakeSlice(dv.Type(), 0, int(count)))
|
||||
|
||||
var errFieldMismatch error
|
||||
for itemRaw, length, err := enum.Next(1); length > 0; itemRaw, length, err = enum.Next(1) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := func() error {
|
||||
// item is a SWbemObject, but really a Win32_Process
|
||||
item := itemRaw.ToIDispatch()
|
||||
defer item.Release()
|
||||
|
||||
ev := reflect.New(elemType)
|
||||
if err = s.cWMIClient.loadEntity(ev.Interface(), item); err != nil {
|
||||
if _, ok := err.(*ErrFieldMismatch); ok {
|
||||
// We continue loading entities even in the face of field mismatch errors.
|
||||
// If we encounter any other error, that other error is returned. Otherwise,
|
||||
// an ErrFieldMismatch is returned.
|
||||
errFieldMismatch = err
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mat != multiArgTypeStructPtr {
|
||||
ev = ev.Elem()
|
||||
}
|
||||
dv.Set(reflect.Append(dv, ev))
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
//fmt.Println("queryBackground: Finished")
|
||||
return errFieldMismatch
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
// +build windows
|
||||
|
||||
package wmi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWbemQuery(t *testing.T) {
|
||||
s, err := InitializeSWbemServices(DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatalf("InitializeSWbemServices: %s", err)
|
||||
}
|
||||
|
||||
var dst []Win32_Process
|
||||
q := CreateQuery(&dst, "WHERE name='lsass.exe'")
|
||||
errQuery := s.Query(q, &dst)
|
||||
if errQuery != nil {
|
||||
t.Fatalf("Query1: %s", errQuery)
|
||||
}
|
||||
count := len(dst)
|
||||
if count < 1 {
|
||||
t.Fatal("Query1: no results found for lsass.exe")
|
||||
}
|
||||
//fmt.Printf("dst[0].ProcessID=%d\n", dst[0].ProcessId)
|
||||
|
||||
q2 := CreateQuery(&dst, "WHERE name='svchost.exe'")
|
||||
errQuery = s.Query(q2, &dst)
|
||||
if errQuery != nil {
|
||||
t.Fatalf("Query2: %s", errQuery)
|
||||
}
|
||||
count = len(dst)
|
||||
if count < 1 {
|
||||
t.Fatal("Query2: no results found for svchost.exe")
|
||||
}
|
||||
//for index, item := range dst {
|
||||
// fmt.Printf("dst[%d].ProcessID=%d\n", index, item.ProcessId)
|
||||
//}
|
||||
errClose := s.Close()
|
||||
if errClose != nil {
|
||||
t.Fatalf("Close: %s", errClose)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWbemQueryNamespace(t *testing.T) {
|
||||
s, err := InitializeSWbemServices(DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatalf("InitializeSWbemServices: %s", err)
|
||||
}
|
||||
var dst []MSFT_NetAdapter
|
||||
q := CreateQuery(&dst, "")
|
||||
errQuery := s.Query(q, &dst, nil, "root\\StandardCimv2")
|
||||
if errQuery != nil {
|
||||
t.Fatalf("Query: %s", errQuery)
|
||||
}
|
||||
count := len(dst)
|
||||
if count < 1 {
|
||||
t.Fatal("Query: no results found for MSFT_NetAdapter in root\\StandardCimv2")
|
||||
}
|
||||
errClose := s.Close()
|
||||
if errClose != nil {
|
||||
t.Fatalf("Close: %s", errClose)
|
||||
}
|
||||
}
|
||||
|
||||
// Run using: go test -run TestWbemMemory -timeout 60m
|
||||
func TestWbemMemory(t *testing.T) {
|
||||
s, err := InitializeSWbemServices(DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatalf("InitializeSWbemServices: %s", err)
|
||||
}
|
||||
start := time.Now()
|
||||
limit := 500000
|
||||
fmt.Printf("Benchmark Iterations: %d (Memory should stabilize around 7MB after ~3000)\n", limit)
|
||||
var privateMB, allocMB, allocTotalMB float64
|
||||
for i := 0; i < limit; i++ {
|
||||
privateMB, allocMB, allocTotalMB = WbemGetMemoryUsageMB(s)
|
||||
if i%100 == 0 {
|
||||
privateMB, allocMB, allocTotalMB = WbemGetMemoryUsageMB(s)
|
||||
fmt.Printf("Time: %4ds Count: %5d Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", time.Now().Sub(start)/time.Second, i, privateMB, allocMB, allocTotalMB)
|
||||
}
|
||||
}
|
||||
errClose := s.Close()
|
||||
if errClose != nil {
|
||||
t.Fatalf("Close: %s", err)
|
||||
}
|
||||
fmt.Printf("Final Time: %4ds Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", time.Now().Sub(start)/time.Second, privateMB, allocMB, allocTotalMB)
|
||||
}
|
||||
|
||||
func WbemGetMemoryUsageMB(s *SWbemServices) (float64, float64, float64) {
|
||||
runtime.ReadMemStats(&mMemoryUsageMB)
|
||||
errGetMemoryUsageMB = s.Query(qGetMemoryUsageMB, &dstGetMemoryUsageMB)
|
||||
if errGetMemoryUsageMB != nil {
|
||||
fmt.Println("ERROR GetMemoryUsage", errGetMemoryUsageMB)
|
||||
return 0, 0, 0
|
||||
}
|
||||
return float64(dstGetMemoryUsageMB[0].WorkingSetPrivate) / MB, float64(mMemoryUsageMB.Alloc) / MB, float64(mMemoryUsageMB.TotalAlloc) / MB
|
||||
}
|
||||
|
||||
//Run all benchmarks (should run for at least 60s to get a stable number):
|
||||
//go test -run=NONE -bench=Version -benchtime=120s
|
||||
|
||||
//Individual benchmarks:
|
||||
//go test -run=NONE -bench=NewVersion -benchtime=120s
|
||||
func BenchmarkNewVersion(b *testing.B) {
|
||||
s, err := InitializeSWbemServices(DefaultClient)
|
||||
if err != nil {
|
||||
b.Fatalf("InitializeSWbemServices: %s", err)
|
||||
}
|
||||
var dst []Win32_OperatingSystem
|
||||
q := CreateQuery(&dst, "")
|
||||
for n := 0; n < b.N; n++ {
|
||||
errQuery := s.Query(q, &dst)
|
||||
if errQuery != nil {
|
||||
b.Fatalf("Query%d: %s", n, errQuery)
|
||||
}
|
||||
count := len(dst)
|
||||
if count < 1 {
|
||||
b.Fatalf("Query%d: no results found for Win32_OperatingSystem", n)
|
||||
}
|
||||
}
|
||||
errClose := s.Close()
|
||||
if errClose != nil {
|
||||
b.Fatalf("Close: %s", errClose)
|
||||
}
|
||||
}
|
||||
|
||||
//go test -run=NONE -bench=OldVersion -benchtime=120s
|
||||
func BenchmarkOldVersion(b *testing.B) {
|
||||
var dst []Win32_OperatingSystem
|
||||
q := CreateQuery(&dst, "")
|
||||
for n := 0; n < b.N; n++ {
|
||||
errQuery := Query(q, &dst)
|
||||
if errQuery != nil {
|
||||
b.Fatalf("Query%d: %s", n, errQuery)
|
||||
}
|
||||
count := len(dst)
|
||||
if count < 1 {
|
||||
b.Fatalf("Query%d: no results found for Win32_OperatingSystem", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type MSFT_NetAdapter struct {
|
||||
Name string
|
||||
InterfaceIndex int
|
||||
DriverDescription string
|
||||
}
|
|
@ -0,0 +1,490 @@
|
|||
// +build windows
|
||||
|
||||
/*
|
||||
Package wmi provides a WQL interface for WMI on Windows.
|
||||
|
||||
Example code to print names of running processes:
|
||||
|
||||
type Win32_Process struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func main() {
|
||||
var dst []Win32_Process
|
||||
q := wmi.CreateQuery(&dst, "")
|
||||
err := wmi.Query(q, &dst)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for i, v := range dst {
|
||||
println(i, v.Name)
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
package wmi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
"github.com/go-ole/go-ole/oleutil"
|
||||
)
|
||||
|
||||
var l = log.New(os.Stdout, "", log.LstdFlags)
|
||||
|
||||
var (
|
||||
ErrInvalidEntityType = errors.New("wmi: invalid entity type")
|
||||
// ErrNilCreateObject is the error returned if CreateObject returns nil even
|
||||
// if the error was nil.
|
||||
ErrNilCreateObject = errors.New("wmi: create object returned nil")
|
||||
lock sync.Mutex
|
||||
)
|
||||
|
||||
// S_FALSE is returned by CoInitializeEx if it was already called on this thread.
|
||||
const S_FALSE = 0x00000001
|
||||
|
||||
// QueryNamespace invokes Query with the given namespace on the local machine.
|
||||
func QueryNamespace(query string, dst interface{}, namespace string) error {
|
||||
return Query(query, dst, nil, namespace)
|
||||
}
|
||||
|
||||
// Query runs the WQL query and appends the values to dst.
|
||||
//
|
||||
// dst must have type *[]S or *[]*S, for some struct type S. Fields selected in
|
||||
// the query must have the same name in dst. Supported types are all signed and
|
||||
// unsigned integers, time.Time, string, bool, or a pointer to one of those.
|
||||
// Array types are not supported.
|
||||
//
|
||||
// By default, the local machine and default namespace are used. These can be
|
||||
// changed using connectServerArgs. See
|
||||
// http://msdn.microsoft.com/en-us/library/aa393720.aspx for details.
|
||||
//
|
||||
// Query is a wrapper around DefaultClient.Query.
|
||||
func Query(query string, dst interface{}, connectServerArgs ...interface{}) error {
|
||||
if DefaultClient.SWbemServicesClient == nil {
|
||||
return DefaultClient.Query(query, dst, connectServerArgs...)
|
||||
}
|
||||
return DefaultClient.SWbemServicesClient.Query(query, dst, connectServerArgs...)
|
||||
}
|
||||
|
||||
// A Client is an WMI query client.
|
||||
//
|
||||
// Its zero value (DefaultClient) is a usable client.
|
||||
type Client struct {
|
||||
// NonePtrZero specifies if nil values for fields which aren't pointers
|
||||
// should be returned as the field types zero value.
|
||||
//
|
||||
// Setting this to true allows stucts without pointer fields to be used
|
||||
// without the risk failure should a nil value returned from WMI.
|
||||
NonePtrZero bool
|
||||
|
||||
// PtrNil specifies if nil values for pointer fields should be returned
|
||||
// as nil.
|
||||
//
|
||||
// Setting this to true will set pointer fields to nil where WMI
|
||||
// returned nil, otherwise the types zero value will be returned.
|
||||
PtrNil bool
|
||||
|
||||
// AllowMissingFields specifies that struct fields not present in the
|
||||
// query result should not result in an error.
|
||||
//
|
||||
// Setting this to true allows custom queries to be used with full
|
||||
// struct definitions instead of having to define multiple structs.
|
||||
AllowMissingFields bool
|
||||
|
||||
// SWbemServiceClient is an optional SWbemServices object that can be
|
||||
// initialized and then reused across multiple queries. If it is null
|
||||
// then the method will initialize a new temporary client each time.
|
||||
SWbemServicesClient *SWbemServices
|
||||
}
|
||||
|
||||
// DefaultClient is the default Client and is used by Query, QueryNamespace
|
||||
var DefaultClient = &Client{}
|
||||
|
||||
// Query runs the WQL query and appends the values to dst.
|
||||
//
|
||||
// dst must have type *[]S or *[]*S, for some struct type S. Fields selected in
|
||||
// the query must have the same name in dst. Supported types are all signed and
|
||||
// unsigned integers, time.Time, string, bool, or a pointer to one of those.
|
||||
// Array types are not supported.
|
||||
//
|
||||
// By default, the local machine and default namespace are used. These can be
|
||||
// changed using connectServerArgs. See
|
||||
// http://msdn.microsoft.com/en-us/library/aa393720.aspx for details.
|
||||
func (c *Client) Query(query string, dst interface{}, connectServerArgs ...interface{}) error {
|
||||
dv := reflect.ValueOf(dst)
|
||||
if dv.Kind() != reflect.Ptr || dv.IsNil() {
|
||||
return ErrInvalidEntityType
|
||||
}
|
||||
dv = dv.Elem()
|
||||
mat, elemType := checkMultiArg(dv)
|
||||
if mat == multiArgTypeInvalid {
|
||||
return ErrInvalidEntityType
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
|
||||
if err != nil {
|
||||
oleCode := err.(*ole.OleError).Code()
|
||||
if oleCode != ole.S_OK && oleCode != S_FALSE {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer ole.CoUninitialize()
|
||||
|
||||
unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator")
|
||||
if err != nil {
|
||||
return err
|
||||
} else if unknown == nil {
|
||||
return ErrNilCreateObject
|
||||
}
|
||||
defer unknown.Release()
|
||||
|
||||
wmi, err := unknown.QueryInterface(ole.IID_IDispatch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wmi.Release()
|
||||
|
||||
// service is a SWbemServices
|
||||
serviceRaw, err := oleutil.CallMethod(wmi, "ConnectServer", connectServerArgs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := serviceRaw.ToIDispatch()
|
||||
defer serviceRaw.Clear()
|
||||
|
||||
// result is a SWBemObjectSet
|
||||
resultRaw, err := oleutil.CallMethod(service, "ExecQuery", query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result := resultRaw.ToIDispatch()
|
||||
defer resultRaw.Clear()
|
||||
|
||||
count, err := oleInt64(result, "Count")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
enumProperty, err := result.GetProperty("_NewEnum")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer enumProperty.Clear()
|
||||
|
||||
enum, err := enumProperty.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enum == nil {
|
||||
return fmt.Errorf("can't get IEnumVARIANT, enum is nil")
|
||||
}
|
||||
defer enum.Release()
|
||||
|
||||
// Initialize a slice with Count capacity
|
||||
dv.Set(reflect.MakeSlice(dv.Type(), 0, int(count)))
|
||||
|
||||
var errFieldMismatch error
|
||||
for itemRaw, length, err := enum.Next(1); length > 0; itemRaw, length, err = enum.Next(1) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := func() error {
|
||||
// item is a SWbemObject, but really a Win32_Process
|
||||
item := itemRaw.ToIDispatch()
|
||||
defer item.Release()
|
||||
|
||||
ev := reflect.New(elemType)
|
||||
if err = c.loadEntity(ev.Interface(), item); err != nil {
|
||||
if _, ok := err.(*ErrFieldMismatch); ok {
|
||||
// We continue loading entities even in the face of field mismatch errors.
|
||||
// If we encounter any other error, that other error is returned. Otherwise,
|
||||
// an ErrFieldMismatch is returned.
|
||||
errFieldMismatch = err
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mat != multiArgTypeStructPtr {
|
||||
ev = ev.Elem()
|
||||
}
|
||||
dv.Set(reflect.Append(dv, ev))
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return errFieldMismatch
|
||||
}
|
||||
|
||||
// ErrFieldMismatch is returned when a field is to be loaded into a different
|
||||
// type than the one it was stored from, or when a field is missing or
|
||||
// unexported in the destination struct.
|
||||
// StructType is the type of the struct pointed to by the destination argument.
|
||||
type ErrFieldMismatch struct {
|
||||
StructType reflect.Type
|
||||
FieldName string
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (e *ErrFieldMismatch) Error() string {
|
||||
return fmt.Sprintf("wmi: cannot load field %q into a %q: %s",
|
||||
e.FieldName, e.StructType, e.Reason)
|
||||
}
|
||||
|
||||
var timeType = reflect.TypeOf(time.Time{})
|
||||
|
||||
// loadEntity loads a SWbemObject into a struct pointer.
|
||||
func (c *Client) loadEntity(dst interface{}, src *ole.IDispatch) (errFieldMismatch error) {
|
||||
v := reflect.ValueOf(dst).Elem()
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
f := v.Field(i)
|
||||
of := f
|
||||
isPtr := f.Kind() == reflect.Ptr
|
||||
if isPtr {
|
||||
ptr := reflect.New(f.Type().Elem())
|
||||
f.Set(ptr)
|
||||
f = f.Elem()
|
||||
}
|
||||
n := v.Type().Field(i).Name
|
||||
if !f.CanSet() {
|
||||
return &ErrFieldMismatch{
|
||||
StructType: of.Type(),
|
||||
FieldName: n,
|
||||
Reason: "CanSet() is false",
|
||||
}
|
||||
}
|
||||
prop, err := oleutil.GetProperty(src, n)
|
||||
if err != nil {
|
||||
if !c.AllowMissingFields {
|
||||
errFieldMismatch = &ErrFieldMismatch{
|
||||
StructType: of.Type(),
|
||||
FieldName: n,
|
||||
Reason: "no such struct field",
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
defer prop.Clear()
|
||||
|
||||
if prop.Value() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch val := prop.Value().(type) {
|
||||
case int8, int16, int32, int64, int:
|
||||
v := reflect.ValueOf(val).Int()
|
||||
switch f.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
f.SetInt(v)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
f.SetUint(uint64(v))
|
||||
default:
|
||||
return &ErrFieldMismatch{
|
||||
StructType: of.Type(),
|
||||
FieldName: n,
|
||||
Reason: "not an integer class",
|
||||
}
|
||||
}
|
||||
case uint8, uint16, uint32, uint64:
|
||||
v := reflect.ValueOf(val).Uint()
|
||||
switch f.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
f.SetInt(int64(v))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
f.SetUint(v)
|
||||
default:
|
||||
return &ErrFieldMismatch{
|
||||
StructType: of.Type(),
|
||||
FieldName: n,
|
||||
Reason: "not an integer class",
|
||||
}
|
||||
}
|
||||
case string:
|
||||
switch f.Kind() {
|
||||
case reflect.String:
|
||||
f.SetString(val)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
iv, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetInt(iv)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
uv, err := strconv.ParseUint(val, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetUint(uv)
|
||||
case reflect.Struct:
|
||||
switch f.Type() {
|
||||
case timeType:
|
||||
if len(val) == 25 {
|
||||
mins, err := strconv.Atoi(val[22:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val = val[:22] + fmt.Sprintf("%02d%02d", mins/60, mins%60)
|
||||
}
|
||||
t, err := time.Parse("20060102150405.000000-0700", val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Set(reflect.ValueOf(t))
|
||||
}
|
||||
}
|
||||
case bool:
|
||||
switch f.Kind() {
|
||||
case reflect.Bool:
|
||||
f.SetBool(val)
|
||||
default:
|
||||
return &ErrFieldMismatch{
|
||||
StructType: of.Type(),
|
||||
FieldName: n,
|
||||
Reason: "not a bool",
|
||||
}
|
||||
}
|
||||
case float32:
|
||||
switch f.Kind() {
|
||||
case reflect.Float32:
|
||||
f.SetFloat(float64(val))
|
||||
default:
|
||||
return &ErrFieldMismatch{
|
||||
StructType: of.Type(),
|
||||
FieldName: n,
|
||||
Reason: "not a Float32",
|
||||
}
|
||||
}
|
||||
default:
|
||||
if f.Kind() == reflect.Slice {
|
||||
switch f.Type().Elem().Kind() {
|
||||
case reflect.String:
|
||||
safeArray := prop.ToArray()
|
||||
if safeArray != nil {
|
||||
arr := safeArray.ToValueArray()
|
||||
fArr := reflect.MakeSlice(f.Type(), len(arr), len(arr))
|
||||
for i, v := range arr {
|
||||
s := fArr.Index(i)
|
||||
s.SetString(v.(string))
|
||||
}
|
||||
f.Set(fArr)
|
||||
}
|
||||
case reflect.Uint8:
|
||||
safeArray := prop.ToArray()
|
||||
if safeArray != nil {
|
||||
arr := safeArray.ToValueArray()
|
||||
fArr := reflect.MakeSlice(f.Type(), len(arr), len(arr))
|
||||
for i, v := range arr {
|
||||
s := fArr.Index(i)
|
||||
s.SetUint(reflect.ValueOf(v).Uint())
|
||||
}
|
||||
f.Set(fArr)
|
||||
}
|
||||
default:
|
||||
return &ErrFieldMismatch{
|
||||
StructType: of.Type(),
|
||||
FieldName: n,
|
||||
Reason: fmt.Sprintf("unsupported slice type (%T)", val),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
typeof := reflect.TypeOf(val)
|
||||
if typeof == nil && (isPtr || c.NonePtrZero) {
|
||||
if (isPtr && c.PtrNil) || (!isPtr && c.NonePtrZero) {
|
||||
of.Set(reflect.Zero(of.Type()))
|
||||
}
|
||||
break
|
||||
}
|
||||
return &ErrFieldMismatch{
|
||||
StructType: of.Type(),
|
||||
FieldName: n,
|
||||
Reason: fmt.Sprintf("unsupported type (%T)", val),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errFieldMismatch
|
||||
}
|
||||
|
||||
type multiArgType int
|
||||
|
||||
const (
|
||||
multiArgTypeInvalid multiArgType = iota
|
||||
multiArgTypeStruct
|
||||
multiArgTypeStructPtr
|
||||
)
|
||||
|
||||
// checkMultiArg checks that v has type []S, []*S for some struct type S.
|
||||
//
|
||||
// It returns what category the slice's elements are, and the reflect.Type
|
||||
// that represents S.
|
||||
func checkMultiArg(v reflect.Value) (m multiArgType, elemType reflect.Type) {
|
||||
if v.Kind() != reflect.Slice {
|
||||
return multiArgTypeInvalid, nil
|
||||
}
|
||||
elemType = v.Type().Elem()
|
||||
switch elemType.Kind() {
|
||||
case reflect.Struct:
|
||||
return multiArgTypeStruct, elemType
|
||||
case reflect.Ptr:
|
||||
elemType = elemType.Elem()
|
||||
if elemType.Kind() == reflect.Struct {
|
||||
return multiArgTypeStructPtr, elemType
|
||||
}
|
||||
}
|
||||
return multiArgTypeInvalid, nil
|
||||
}
|
||||
|
||||
func oleInt64(item *ole.IDispatch, prop string) (int64, error) {
|
||||
v, err := oleutil.GetProperty(item, prop)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer v.Clear()
|
||||
|
||||
i := int64(v.Val)
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// CreateQuery returns a WQL query string that queries all columns of src. where
|
||||
// is an optional string that is appended to the query, to be used with WHERE
|
||||
// clauses. In such a case, the "WHERE" string should appear at the beginning.
|
||||
func CreateQuery(src interface{}, where string) string {
|
||||
var b bytes.Buffer
|
||||
b.WriteString("SELECT ")
|
||||
s := reflect.Indirect(reflect.ValueOf(src))
|
||||
t := s.Type()
|
||||
if s.Kind() == reflect.Slice {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() != reflect.Struct {
|
||||
return ""
|
||||
}
|
||||
var fields []string
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
fields = append(fields, t.Field(i).Name)
|
||||
}
|
||||
b.WriteString(strings.Join(fields, ", "))
|
||||
b.WriteString(" FROM ")
|
||||
b.WriteString(t.Name())
|
||||
b.WriteString(" " + where)
|
||||
return b.String()
|
||||
}
|
|
@ -0,0 +1,544 @@
|
|||
// +build windows
|
||||
|
||||
package wmi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ole "github.com/go-ole/go-ole"
|
||||
"github.com/go-ole/go-ole/oleutil"
|
||||
)
|
||||
|
||||
func TestQuery(t *testing.T) {
|
||||
var dst []Win32_Process
|
||||
q := CreateQuery(&dst, "")
|
||||
err := Query(q, &dst)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldMismatch(t *testing.T) {
|
||||
type s struct {
|
||||
Name string
|
||||
HandleCount uint32
|
||||
Blah uint32
|
||||
}
|
||||
var dst []s
|
||||
err := Query("SELECT Name, HandleCount FROM Win32_Process", &dst)
|
||||
if err == nil || err.Error() != `wmi: cannot load field "Blah" into a "uint32": no such struct field` {
|
||||
t.Error("Expected err field mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrings(t *testing.T) {
|
||||
printed := false
|
||||
f := func() {
|
||||
var dst []Win32_Process
|
||||
zeros := 0
|
||||
q := CreateQuery(&dst, "")
|
||||
for i := 0; i < 5; i++ {
|
||||
err := Query(q, &dst)
|
||||
if err != nil {
|
||||
t.Fatal(err, q)
|
||||
}
|
||||
for _, d := range dst {
|
||||
v := reflect.ValueOf(d)
|
||||
for j := 0; j < v.NumField(); j++ {
|
||||
f := v.Field(j)
|
||||
if f.Kind() != reflect.String {
|
||||
continue
|
||||
}
|
||||
s := f.Interface().(string)
|
||||
if len(s) > 0 && s[0] == '\u0000' {
|
||||
zeros++
|
||||
if !printed {
|
||||
printed = true
|
||||
j, _ := json.MarshalIndent(&d, "", " ")
|
||||
t.Log("Example with \\u0000:\n", string(j))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println("iter", i, "zeros:", zeros)
|
||||
}
|
||||
if zeros > 0 {
|
||||
t.Error("> 0 zeros")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Disabling GC")
|
||||
debug.SetGCPercent(-1)
|
||||
f()
|
||||
fmt.Println("Enabling GC")
|
||||
debug.SetGCPercent(100)
|
||||
f()
|
||||
}
|
||||
|
||||
func TestNamespace(t *testing.T) {
|
||||
var dst []Win32_Process
|
||||
q := CreateQuery(&dst, "")
|
||||
err := QueryNamespace(q, &dst, `root\CIMV2`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dst = nil
|
||||
err = QueryNamespace(q, &dst, `broken\nothing`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateQuery(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Count int
|
||||
}
|
||||
var dst []TestStruct
|
||||
output := "SELECT Name, Count FROM TestStruct WHERE Count > 2"
|
||||
tests := []interface{}{
|
||||
&dst,
|
||||
dst,
|
||||
TestStruct{},
|
||||
&TestStruct{},
|
||||
}
|
||||
for i, test := range tests {
|
||||
if o := CreateQuery(test, "WHERE Count > 2"); o != output {
|
||||
t.Error("bad output on", i, o)
|
||||
}
|
||||
}
|
||||
if CreateQuery(3, "") != "" {
|
||||
t.Error("expected empty string")
|
||||
}
|
||||
}
|
||||
|
||||
// Run using: go test -run TestMemoryWMISimple -timeout 60m
|
||||
func _TestMemoryWMISimple(t *testing.T) {
|
||||
start := time.Now()
|
||||
limit := 500000
|
||||
fmt.Printf("Benchmark Iterations: %d (Memory should stabilize around 7MB after ~3000)\n", limit)
|
||||
var privateMB, allocMB, allocTotalMB float64
|
||||
//var dst []Win32_PerfRawData_PerfDisk_LogicalDisk
|
||||
//q := CreateQuery(&dst, "")
|
||||
for i := 0; i < limit; i++ {
|
||||
privateMB, allocMB, allocTotalMB = GetMemoryUsageMB()
|
||||
if i%1000 == 0 {
|
||||
//privateMB, allocMB, allocTotalMB = GetMemoryUsageMB()
|
||||
fmt.Printf("Time: %4ds Count: %5d Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", time.Now().Sub(start)/time.Second, i, privateMB, allocMB, allocTotalMB)
|
||||
}
|
||||
//Query(q, &dst)
|
||||
}
|
||||
//privateMB, allocMB, allocTotalMB = GetMemoryUsageMB()
|
||||
fmt.Printf("Final Time: %4ds Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", time.Now().Sub(start)/time.Second, privateMB, allocMB, allocTotalMB)
|
||||
}
|
||||
|
||||
func _TestMemoryWMIConcurrent(t *testing.T) {
|
||||
if testing.Short() {
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
limit := 50000
|
||||
fmt.Println("Total Iterations:", limit)
|
||||
fmt.Println("No panics mean it succeeded. Other errors are OK. Memory should stabilize after ~1500 iterations.")
|
||||
runtime.GOMAXPROCS(2)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
for i := 0; i < limit; i++ {
|
||||
if i%500 == 0 {
|
||||
privateMB, allocMB, allocTotalMB := GetMemoryUsageMB()
|
||||
fmt.Printf("Time: %4ds Count: %4d Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", time.Now().Sub(start)/time.Second, i, privateMB, allocMB, allocTotalMB)
|
||||
}
|
||||
var dst []Win32_PerfRawData_PerfDisk_LogicalDisk
|
||||
q := CreateQuery(&dst, "")
|
||||
err := Query(q, &dst)
|
||||
if err != nil {
|
||||
fmt.Println("ERROR disk", err)
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
for i := 0; i > -limit; i-- {
|
||||
//if i%500 == 0 {
|
||||
// fmt.Println(i)
|
||||
//}
|
||||
var dst []Win32_OperatingSystem
|
||||
q := CreateQuery(&dst, "")
|
||||
err := Query(q, &dst)
|
||||
if err != nil {
|
||||
fmt.Println("ERROR OS", err)
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
//privateMB, allocMB, allocTotalMB := GetMemoryUsageMB()
|
||||
//fmt.Printf("Final Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", privateMB, allocMB, allocTotalMB)
|
||||
}
|
||||
|
||||
var lockthread sync.Mutex
|
||||
var refcount1 int32
|
||||
var refcount2 int32
|
||||
var refcount3 int32
|
||||
|
||||
// Test function showing memory leak in unknown.QueryInterface call on Server2016/Windows10
|
||||
func getRSS(url string, xmlhttp *ole.IDispatch, MinimalTest bool) (int, error) {
|
||||
|
||||
// call using url,nil to see memory leak
|
||||
if xmlhttp == nil {
|
||||
//Initialize inside loop if not passed in from outer section
|
||||
lockthread.Lock()
|
||||
defer lockthread.Unlock()
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
|
||||
if err != nil {
|
||||
oleCode := err.(*ole.OleError).Code()
|
||||
if oleCode != ole.S_OK && oleCode != S_FALSE {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
defer ole.CoUninitialize()
|
||||
|
||||
//fmt.Println("CreateObject Microsoft.XMLHTTP")
|
||||
unknown, err := oleutil.CreateObject("Microsoft.XMLHTTP")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() { refcount1 += xmlhttp.Release() }()
|
||||
|
||||
//Memory leak occurs here
|
||||
xmlhttp, err = unknown.QueryInterface(ole.IID_IDispatch)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() { refcount2 += xmlhttp.Release() }()
|
||||
//Nothing below this really matters. Can be removed if you want a tighter loop
|
||||
}
|
||||
|
||||
//fmt.Printf("Download %s\n", url)
|
||||
openRaw, err := oleutil.CallMethod(xmlhttp, "open", "GET", url, false)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer openRaw.Clear()
|
||||
|
||||
if MinimalTest {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
//Initiate http request
|
||||
sendRaw, err := oleutil.CallMethod(xmlhttp, "send", nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer sendRaw.Clear()
|
||||
state := -1 // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState
|
||||
for state != 4 {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
stateRaw := oleutil.MustGetProperty(xmlhttp, "readyState")
|
||||
state = int(stateRaw.Val)
|
||||
stateRaw.Clear()
|
||||
}
|
||||
|
||||
responseXMLRaw := oleutil.MustGetProperty(xmlhttp, "responseXml")
|
||||
responseXML := responseXMLRaw.ToIDispatch()
|
||||
defer responseXMLRaw.Clear()
|
||||
itemsRaw := oleutil.MustCallMethod(responseXML, "selectNodes", "/rdf:RDF/item")
|
||||
items := itemsRaw.ToIDispatch()
|
||||
defer itemsRaw.Clear()
|
||||
lengthRaw := oleutil.MustGetProperty(items, "length")
|
||||
defer lengthRaw.Clear()
|
||||
length := int(lengthRaw.Val)
|
||||
|
||||
/* This just bloats the TotalAlloc and slows the test down. Doesn't effect Private Working Set
|
||||
for n := 0; n < length; n++ {
|
||||
itemRaw := oleutil.MustGetProperty(items, "item", n)
|
||||
item := itemRaw.ToIDispatch()
|
||||
title := oleutil.MustCallMethod(item, "selectSingleNode", "title").ToIDispatch()
|
||||
|
||||
//fmt.Println(oleutil.MustGetProperty(title, "text").ToString())
|
||||
textRaw := oleutil.MustGetProperty(title, "text")
|
||||
textRaw.ToString()
|
||||
|
||||
link := oleutil.MustCallMethod(item, "selectSingleNode", "link").ToIDispatch()
|
||||
//fmt.Println(" " + oleutil.MustGetProperty(link, "text").ToString())
|
||||
textRaw2 := oleutil.MustGetProperty(link, "text")
|
||||
textRaw2.ToString()
|
||||
|
||||
textRaw2.Clear()
|
||||
link.Release()
|
||||
textRaw.Clear()
|
||||
title.Release()
|
||||
itemRaw.Clear()
|
||||
}
|
||||
*/
|
||||
return length, nil
|
||||
}
|
||||
|
||||
// Testing go-ole/oleutil
|
||||
// Run using: go test -run TestMemoryOLE -timeout 60m
|
||||
// Code from https://github.com/go-ole/go-ole/blob/master/example/msxml/rssreader.go
|
||||
func _TestMemoryOLE(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Error(r)
|
||||
}
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
limit := 50000000
|
||||
url := "http://localhost/slashdot.xml" //http://rss.slashdot.org/Slashdot/slashdot"
|
||||
fmt.Printf("Benchmark Iterations: %d (Memory should stabilize around 8MB to 12MB after ~2k full or 250k minimal)\n", limit)
|
||||
|
||||
//On Server 2016 or Windows 10 changing leakMemory=true will cause it to leak ~1.5MB per 10000 calls to unknown.QueryInterface
|
||||
leakMemory := true
|
||||
|
||||
////////////////////////////////////////
|
||||
//Start outer section
|
||||
var unknown *ole.IUnknown
|
||||
var xmlhttp *ole.IDispatch
|
||||
if !leakMemory {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
|
||||
if err != nil {
|
||||
oleCode := err.(*ole.OleError).Code()
|
||||
if oleCode != ole.S_OK && oleCode != S_FALSE {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
defer ole.CoUninitialize()
|
||||
|
||||
//fmt.Println("CreateObject Microsoft.XMLHTTP")
|
||||
unknown, err = oleutil.CreateObject("Microsoft.XMLHTTP")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer unknown.Release()
|
||||
|
||||
//Memory leak starts here
|
||||
xmlhttp, err = unknown.QueryInterface(ole.IID_IDispatch)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer xmlhttp.Release()
|
||||
}
|
||||
//End outer section
|
||||
////////////////////////////////////////
|
||||
|
||||
totalItems := uint64(0)
|
||||
for i := 0; i < limit; i++ {
|
||||
if i%2000 == 0 {
|
||||
privateMB, allocMB, allocTotalMB := GetMemoryUsageMB()
|
||||
fmt.Printf("Time: %4ds Count: %7d Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB %7d/%7d\n", time.Now().Sub(start)/time.Second, i, privateMB, allocMB, allocTotalMB, refcount1, refcount2)
|
||||
}
|
||||
//This should use less than 10MB for 1 million iterations if xmlhttp was initialized above
|
||||
//On Server 2016 or Windows 10 changing leakMemory=true above will cause it to leak ~1.5MB per 10000 calls to unknown.QueryInterface
|
||||
count, err := getRSS(url, xmlhttp, true) //last argument is for Minimal test. Doesn't effect leak just overall allocations/time
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
totalItems += uint64(count)
|
||||
}
|
||||
privateMB, allocMB, allocTotalMB := GetMemoryUsageMB()
|
||||
fmt.Printf("Final totalItems: %d Private Memory: %5.1fMB MemStats.Alloc: %4.1fMB MemStats.TotalAlloc: %5.1fMB\n", totalItems, privateMB, allocMB, allocTotalMB)
|
||||
}
|
||||
|
||||
const MB = 1024 * 1024
|
||||
|
||||
var (
|
||||
mMemoryUsageMB runtime.MemStats
|
||||
errGetMemoryUsageMB error
|
||||
dstGetMemoryUsageMB []Win32_PerfRawData_PerfProc_Process
|
||||
filterProcessID = fmt.Sprintf("WHERE IDProcess = %d", os.Getpid())
|
||||
qGetMemoryUsageMB = CreateQuery(&dstGetMemoryUsageMB, filterProcessID)
|
||||
)
|
||||
|
||||
func GetMemoryUsageMB() (float64, float64, float64) {
|
||||
runtime.ReadMemStats(&mMemoryUsageMB)
|
||||
//errGetMemoryUsageMB = nil //Query(qGetMemoryUsageMB, &dstGetMemoryUsageMB) float64(dstGetMemoryUsageMB[0].WorkingSetPrivate)
|
||||
errGetMemoryUsageMB = Query(qGetMemoryUsageMB, &dstGetMemoryUsageMB)
|
||||
if errGetMemoryUsageMB != nil {
|
||||
fmt.Println("ERROR GetMemoryUsage", errGetMemoryUsageMB)
|
||||
return 0, 0, 0
|
||||
}
|
||||
return float64(dstGetMemoryUsageMB[0].WorkingSetPrivate) / MB, float64(mMemoryUsageMB.Alloc) / MB, float64(mMemoryUsageMB.TotalAlloc) / MB
|
||||
}
|
||||
|
||||
type Win32_PerfRawData_PerfProc_Process struct {
|
||||
IDProcess uint32
|
||||
WorkingSetPrivate uint64
|
||||
}
|
||||
|
||||
type Win32_Process struct {
|
||||
CSCreationClassName string
|
||||
CSName string
|
||||
Caption *string
|
||||
CommandLine *string
|
||||
CreationClassName string
|
||||
CreationDate *time.Time
|
||||
Description *string
|
||||
ExecutablePath *string
|
||||
ExecutionState *uint16
|
||||
Handle string
|
||||
HandleCount uint32
|
||||
InstallDate *time.Time
|
||||
KernelModeTime uint64
|
||||
MaximumWorkingSetSize *uint32
|
||||
MinimumWorkingSetSize *uint32
|
||||
Name string
|
||||
OSCreationClassName string
|
||||
OSName string
|
||||
OtherOperationCount uint64
|
||||
OtherTransferCount uint64
|
||||
PageFaults uint32
|
||||
PageFileUsage uint32
|
||||
ParentProcessId uint32
|
||||
PeakPageFileUsage uint32
|
||||
PeakVirtualSize uint64
|
||||
PeakWorkingSetSize uint32
|
||||
Priority uint32
|
||||
PrivatePageCount uint64
|
||||
ProcessId uint32
|
||||
QuotaNonPagedPoolUsage uint32
|
||||
QuotaPagedPoolUsage uint32
|
||||
QuotaPeakNonPagedPoolUsage uint32
|
||||
QuotaPeakPagedPoolUsage uint32
|
||||
ReadOperationCount uint64
|
||||
ReadTransferCount uint64
|
||||
SessionId uint32
|
||||
Status *string
|
||||
TerminationDate *time.Time
|
||||
ThreadCount uint32
|
||||
UserModeTime uint64
|
||||
VirtualSize uint64
|
||||
WindowsVersion string
|
||||
WorkingSetSize uint64
|
||||
WriteOperationCount uint64
|
||||
WriteTransferCount uint64
|
||||
}
|
||||
|
||||
type Win32_PerfRawData_PerfDisk_LogicalDisk struct {
|
||||
AvgDiskBytesPerRead uint64
|
||||
AvgDiskBytesPerRead_Base uint32
|
||||
AvgDiskBytesPerTransfer uint64
|
||||
AvgDiskBytesPerTransfer_Base uint32
|
||||
AvgDiskBytesPerWrite uint64
|
||||
AvgDiskBytesPerWrite_Base uint32
|
||||
AvgDiskQueueLength uint64
|
||||
AvgDiskReadQueueLength uint64
|
||||
AvgDiskSecPerRead uint32
|
||||
AvgDiskSecPerRead_Base uint32
|
||||
AvgDiskSecPerTransfer uint32
|
||||
AvgDiskSecPerTransfer_Base uint32
|
||||
AvgDiskSecPerWrite uint32
|
||||
AvgDiskSecPerWrite_Base uint32
|
||||
AvgDiskWriteQueueLength uint64
|
||||
Caption *string
|
||||
CurrentDiskQueueLength uint32
|
||||
Description *string
|
||||
DiskBytesPerSec uint64
|
||||
DiskReadBytesPerSec uint64
|
||||
DiskReadsPerSec uint32
|
||||
DiskTransfersPerSec uint32
|
||||
DiskWriteBytesPerSec uint64
|
||||
DiskWritesPerSec uint32
|
||||
FreeMegabytes uint32
|
||||
Frequency_Object uint64
|
||||
Frequency_PerfTime uint64
|
||||
Frequency_Sys100NS uint64
|
||||
Name string
|
||||
PercentDiskReadTime uint64
|
||||
PercentDiskReadTime_Base uint64
|
||||
PercentDiskTime uint64
|
||||
PercentDiskTime_Base uint64
|
||||
PercentDiskWriteTime uint64
|
||||
PercentDiskWriteTime_Base uint64
|
||||
PercentFreeSpace uint32
|
||||
PercentFreeSpace_Base uint32
|
||||
PercentIdleTime uint64
|
||||
PercentIdleTime_Base uint64
|
||||
SplitIOPerSec uint32
|
||||
Timestamp_Object uint64
|
||||
Timestamp_PerfTime uint64
|
||||
Timestamp_Sys100NS uint64
|
||||
}
|
||||
|
||||
type Win32_OperatingSystem struct {
|
||||
BootDevice string
|
||||
BuildNumber string
|
||||
BuildType string
|
||||
Caption *string
|
||||
CodeSet string
|
||||
CountryCode string
|
||||
CreationClassName string
|
||||
CSCreationClassName string
|
||||
CSDVersion *string
|
||||
CSName string
|
||||
CurrentTimeZone int16
|
||||
DataExecutionPrevention_Available bool
|
||||
DataExecutionPrevention_32BitApplications bool
|
||||
DataExecutionPrevention_Drivers bool
|
||||
DataExecutionPrevention_SupportPolicy *uint8
|
||||
Debug bool
|
||||
Description *string
|
||||
Distributed bool
|
||||
EncryptionLevel uint32
|
||||
ForegroundApplicationBoost *uint8
|
||||
FreePhysicalMemory uint64
|
||||
FreeSpaceInPagingFiles uint64
|
||||
FreeVirtualMemory uint64
|
||||
InstallDate time.Time
|
||||
LargeSystemCache *uint32
|
||||
LastBootUpTime time.Time
|
||||
LocalDateTime time.Time
|
||||
Locale string
|
||||
Manufacturer string
|
||||
MaxNumberOfProcesses uint32
|
||||
MaxProcessMemorySize uint64
|
||||
MUILanguages *[]string
|
||||
Name string
|
||||
NumberOfLicensedUsers *uint32
|
||||
NumberOfProcesses uint32
|
||||
NumberOfUsers uint32
|
||||
OperatingSystemSKU uint32
|
||||
Organization string
|
||||
OSArchitecture string
|
||||
OSLanguage uint32
|
||||
OSProductSuite uint32
|
||||
OSType uint16
|
||||
OtherTypeDescription *string
|
||||
PAEEnabled *bool
|
||||
PlusProductID *string
|
||||
PlusVersionNumber *string
|
||||
PortableOperatingSystem bool
|
||||
Primary bool
|
||||
ProductType uint32
|
||||
RegisteredUser string
|
||||
SerialNumber string
|
||||
ServicePackMajorVersion uint16
|
||||
ServicePackMinorVersion uint16
|
||||
SizeStoredInPagingFiles uint64
|
||||
Status string
|
||||
SuiteMask uint32
|
||||
SystemDevice string
|
||||
SystemDirectory string
|
||||
SystemDrive string
|
||||
TotalSwapSpaceSize *uint64
|
||||
TotalVirtualMemorySize uint64
|
||||
TotalVisibleMemorySize uint64
|
||||
Version string
|
||||
WindowsDirectory string
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,25 @@
|
|||
# Go's `text/template` package with newline elision
|
||||
|
||||
This is a fork of Go 1.4's [text/template](http://golang.org/pkg/text/template/) package with one addition: a backslash immediately after a closing delimiter will delete all subsequent newlines until a non-newline.
|
||||
|
||||
eg.
|
||||
|
||||
```
|
||||
{{if true}}\
|
||||
hello
|
||||
{{end}}\
|
||||
```
|
||||
|
||||
Will result in:
|
||||
|
||||
```
|
||||
hello\n
|
||||
```
|
||||
|
||||
Rather than:
|
||||
|
||||
```
|
||||
\n
|
||||
hello\n
|
||||
\n
|
||||
```
|
|
@ -0,0 +1,406 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package template implements data-driven templates for generating textual output.
|
||||
|
||||
To generate HTML output, see package html/template, which has the same interface
|
||||
as this package but automatically secures HTML output against certain attacks.
|
||||
|
||||
Templates are executed by applying them to a data structure. Annotations in the
|
||||
template refer to elements of the data structure (typically a field of a struct
|
||||
or a key in a map) to control execution and derive values to be displayed.
|
||||
Execution of the template walks the structure and sets the cursor, represented
|
||||
by a period '.' and called "dot", to the value at the current location in the
|
||||
structure as execution proceeds.
|
||||
|
||||
The input text for a template is UTF-8-encoded text in any format.
|
||||
"Actions"--data evaluations or control structures--are delimited by
|
||||
"{{" and "}}"; all text outside actions is copied to the output unchanged.
|
||||
Actions may not span newlines, although comments can.
|
||||
|
||||
Once parsed, a template may be executed safely in parallel.
|
||||
|
||||
Here is a trivial example that prints "17 items are made of wool".
|
||||
|
||||
type Inventory struct {
|
||||
Material string
|
||||
Count uint
|
||||
}
|
||||
sweaters := Inventory{"wool", 17}
|
||||
tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
|
||||
if err != nil { panic(err) }
|
||||
err = tmpl.Execute(os.Stdout, sweaters)
|
||||
if err != nil { panic(err) }
|
||||
|
||||
More intricate examples appear below.
|
||||
|
||||
Actions
|
||||
|
||||
Here is the list of actions. "Arguments" and "pipelines" are evaluations of
|
||||
data, defined in detail below.
|
||||
|
||||
*/
|
||||
// {{/* a comment */}}
|
||||
// A comment; discarded. May contain newlines.
|
||||
// Comments do not nest and must start and end at the
|
||||
// delimiters, as shown here.
|
||||
/*
|
||||
|
||||
{{pipeline}}
|
||||
The default textual representation of the value of the pipeline
|
||||
is copied to the output.
|
||||
|
||||
{{if pipeline}} T1 {{end}}
|
||||
If the value of the pipeline is empty, no output is generated;
|
||||
otherwise, T1 is executed. The empty values are false, 0, any
|
||||
nil pointer or interface value, and any array, slice, map, or
|
||||
string of length zero.
|
||||
Dot is unaffected.
|
||||
|
||||
{{if pipeline}} T1 {{else}} T0 {{end}}
|
||||
If the value of the pipeline is empty, T0 is executed;
|
||||
otherwise, T1 is executed. Dot is unaffected.
|
||||
|
||||
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
|
||||
To simplify the appearance of if-else chains, the else action
|
||||
of an if may include another if directly; the effect is exactly
|
||||
the same as writing
|
||||
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
|
||||
|
||||
{{range pipeline}} T1 {{end}}
|
||||
The value of the pipeline must be an array, slice, map, or channel.
|
||||
If the value of the pipeline has length zero, nothing is output;
|
||||
otherwise, dot is set to the successive elements of the array,
|
||||
slice, or map and T1 is executed. If the value is a map and the
|
||||
keys are of basic type with a defined order ("comparable"), the
|
||||
elements will be visited in sorted key order.
|
||||
|
||||
{{range pipeline}} T1 {{else}} T0 {{end}}
|
||||
The value of the pipeline must be an array, slice, map, or channel.
|
||||
If the value of the pipeline has length zero, dot is unaffected and
|
||||
T0 is executed; otherwise, dot is set to the successive elements
|
||||
of the array, slice, or map and T1 is executed.
|
||||
|
||||
{{template "name"}}
|
||||
The template with the specified name is executed with nil data.
|
||||
|
||||
{{template "name" pipeline}}
|
||||
The template with the specified name is executed with dot set
|
||||
to the value of the pipeline.
|
||||
|
||||
{{with pipeline}} T1 {{end}}
|
||||
If the value of the pipeline is empty, no output is generated;
|
||||
otherwise, dot is set to the value of the pipeline and T1 is
|
||||
executed.
|
||||
|
||||
{{with pipeline}} T1 {{else}} T0 {{end}}
|
||||
If the value of the pipeline is empty, dot is unaffected and T0
|
||||
is executed; otherwise, dot is set to the value of the pipeline
|
||||
and T1 is executed.
|
||||
|
||||
Arguments
|
||||
|
||||
An argument is a simple value, denoted by one of the following.
|
||||
|
||||
- A boolean, string, character, integer, floating-point, imaginary
|
||||
or complex constant in Go syntax. These behave like Go's untyped
|
||||
constants, although raw strings may not span newlines.
|
||||
- The keyword nil, representing an untyped Go nil.
|
||||
- The character '.' (period):
|
||||
.
|
||||
The result is the value of dot.
|
||||
- A variable name, which is a (possibly empty) alphanumeric string
|
||||
preceded by a dollar sign, such as
|
||||
$piOver2
|
||||
or
|
||||
$
|
||||
The result is the value of the variable.
|
||||
Variables are described below.
|
||||
- The name of a field of the data, which must be a struct, preceded
|
||||
by a period, such as
|
||||
.Field
|
||||
The result is the value of the field. Field invocations may be
|
||||
chained:
|
||||
.Field1.Field2
|
||||
Fields can also be evaluated on variables, including chaining:
|
||||
$x.Field1.Field2
|
||||
- The name of a key of the data, which must be a map, preceded
|
||||
by a period, such as
|
||||
.Key
|
||||
The result is the map element value indexed by the key.
|
||||
Key invocations may be chained and combined with fields to any
|
||||
depth:
|
||||
.Field1.Key1.Field2.Key2
|
||||
Although the key must be an alphanumeric identifier, unlike with
|
||||
field names they do not need to start with an upper case letter.
|
||||
Keys can also be evaluated on variables, including chaining:
|
||||
$x.key1.key2
|
||||
- The name of a niladic method of the data, preceded by a period,
|
||||
such as
|
||||
.Method
|
||||
The result is the value of invoking the method with dot as the
|
||||
receiver, dot.Method(). Such a method must have one return value (of
|
||||
any type) or two return values, the second of which is an error.
|
||||
If it has two and the returned error is non-nil, execution terminates
|
||||
and an error is returned to the caller as the value of Execute.
|
||||
Method invocations may be chained and combined with fields and keys
|
||||
to any depth:
|
||||
.Field1.Key1.Method1.Field2.Key2.Method2
|
||||
Methods can also be evaluated on variables, including chaining:
|
||||
$x.Method1.Field
|
||||
- The name of a niladic function, such as
|
||||
fun
|
||||
The result is the value of invoking the function, fun(). The return
|
||||
types and values behave as in methods. Functions and function
|
||||
names are described below.
|
||||
- A parenthesized instance of one the above, for grouping. The result
|
||||
may be accessed by a field or map key invocation.
|
||||
print (.F1 arg1) (.F2 arg2)
|
||||
(.StructValuedMethod "arg").Field
|
||||
|
||||
Arguments may evaluate to any type; if they are pointers the implementation
|
||||
automatically indirects to the base type when required.
|
||||
If an evaluation yields a function value, such as a function-valued
|
||||
field of a struct, the function is not invoked automatically, but it
|
||||
can be used as a truth value for an if action and the like. To invoke
|
||||
it, use the call function, defined below.
|
||||
|
||||
A pipeline is a possibly chained sequence of "commands". A command is a simple
|
||||
value (argument) or a function or method call, possibly with multiple arguments:
|
||||
|
||||
Argument
|
||||
The result is the value of evaluating the argument.
|
||||
.Method [Argument...]
|
||||
The method can be alone or the last element of a chain but,
|
||||
unlike methods in the middle of a chain, it can take arguments.
|
||||
The result is the value of calling the method with the
|
||||
arguments:
|
||||
dot.Method(Argument1, etc.)
|
||||
functionName [Argument...]
|
||||
The result is the value of calling the function associated
|
||||
with the name:
|
||||
function(Argument1, etc.)
|
||||
Functions and function names are described below.
|
||||
|
||||
Pipelines
|
||||
|
||||
A pipeline may be "chained" by separating a sequence of commands with pipeline
|
||||
characters '|'. In a chained pipeline, the result of the each command is
|
||||
passed as the last argument of the following command. The output of the final
|
||||
command in the pipeline is the value of the pipeline.
|
||||
|
||||
The output of a command will be either one value or two values, the second of
|
||||
which has type error. If that second value is present and evaluates to
|
||||
non-nil, execution terminates and the error is returned to the caller of
|
||||
Execute.
|
||||
|
||||
Variables
|
||||
|
||||
A pipeline inside an action may initialize a variable to capture the result.
|
||||
The initialization has syntax
|
||||
|
||||
$variable := pipeline
|
||||
|
||||
where $variable is the name of the variable. An action that declares a
|
||||
variable produces no output.
|
||||
|
||||
If a "range" action initializes a variable, the variable is set to the
|
||||
successive elements of the iteration. Also, a "range" may declare two
|
||||
variables, separated by a comma:
|
||||
|
||||
range $index, $element := pipeline
|
||||
|
||||
in which case $index and $element are set to the successive values of the
|
||||
array/slice index or map key and element, respectively. Note that if there is
|
||||
only one variable, it is assigned the element; this is opposite to the
|
||||
convention in Go range clauses.
|
||||
|
||||
A variable's scope extends to the "end" action of the control structure ("if",
|
||||
"with", or "range") in which it is declared, or to the end of the template if
|
||||
there is no such control structure. A template invocation does not inherit
|
||||
variables from the point of its invocation.
|
||||
|
||||
When execution begins, $ is set to the data argument passed to Execute, that is,
|
||||
to the starting value of dot.
|
||||
|
||||
Examples
|
||||
|
||||
Here are some example one-line templates demonstrating pipelines and variables.
|
||||
All produce the quoted word "output":
|
||||
|
||||
{{"\"output\""}}
|
||||
A string constant.
|
||||
{{`"output"`}}
|
||||
A raw string constant.
|
||||
{{printf "%q" "output"}}
|
||||
A function call.
|
||||
{{"output" | printf "%q"}}
|
||||
A function call whose final argument comes from the previous
|
||||
command.
|
||||
{{printf "%q" (print "out" "put")}}
|
||||
A parenthesized argument.
|
||||
{{"put" | printf "%s%s" "out" | printf "%q"}}
|
||||
A more elaborate call.
|
||||
{{"output" | printf "%s" | printf "%q"}}
|
||||
A longer chain.
|
||||
{{with "output"}}{{printf "%q" .}}{{end}}
|
||||
A with action using dot.
|
||||
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
|
||||
A with action that creates and uses a variable.
|
||||
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
|
||||
A with action that uses the variable in another action.
|
||||
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
|
||||
The same, but pipelined.
|
||||
|
||||
Functions
|
||||
|
||||
During execution functions are found in two function maps: first in the
|
||||
template, then in the global function map. By default, no functions are defined
|
||||
in the template but the Funcs method can be used to add them.
|
||||
|
||||
Predefined global functions are named as follows.
|
||||
|
||||
and
|
||||
Returns the boolean AND of its arguments by returning the
|
||||
first empty argument or the last argument, that is,
|
||||
"and x y" behaves as "if x then y else x". All the
|
||||
arguments are evaluated.
|
||||
call
|
||||
Returns the result of calling the first argument, which
|
||||
must be a function, with the remaining arguments as parameters.
|
||||
Thus "call .X.Y 1 2" is, in Go notation, dot.X.Y(1, 2) where
|
||||
Y is a func-valued field, map entry, or the like.
|
||||
The first argument must be the result of an evaluation
|
||||
that yields a value of function type (as distinct from
|
||||
a predefined function such as print). The function must
|
||||
return either one or two result values, the second of which
|
||||
is of type error. If the arguments don't match the function
|
||||
or the returned error value is non-nil, execution stops.
|
||||
html
|
||||
Returns the escaped HTML equivalent of the textual
|
||||
representation of its arguments.
|
||||
index
|
||||
Returns the result of indexing its first argument by the
|
||||
following arguments. Thus "index x 1 2 3" is, in Go syntax,
|
||||
x[1][2][3]. Each indexed item must be a map, slice, or array.
|
||||
js
|
||||
Returns the escaped JavaScript equivalent of the textual
|
||||
representation of its arguments.
|
||||
len
|
||||
Returns the integer length of its argument.
|
||||
not
|
||||
Returns the boolean negation of its single argument.
|
||||
or
|
||||
Returns the boolean OR of its arguments by returning the
|
||||
first non-empty argument or the last argument, that is,
|
||||
"or x y" behaves as "if x then x else y". All the
|
||||
arguments are evaluated.
|
||||
print
|
||||
An alias for fmt.Sprint
|
||||
printf
|
||||
An alias for fmt.Sprintf
|
||||
println
|
||||
An alias for fmt.Sprintln
|
||||
urlquery
|
||||
Returns the escaped value of the textual representation of
|
||||
its arguments in a form suitable for embedding in a URL query.
|
||||
|
||||
The boolean functions take any zero value to be false and a non-zero
|
||||
value to be true.
|
||||
|
||||
There is also a set of binary comparison operators defined as
|
||||
functions:
|
||||
|
||||
eq
|
||||
Returns the boolean truth of arg1 == arg2
|
||||
ne
|
||||
Returns the boolean truth of arg1 != arg2
|
||||
lt
|
||||
Returns the boolean truth of arg1 < arg2
|
||||
le
|
||||
Returns the boolean truth of arg1 <= arg2
|
||||
gt
|
||||
Returns the boolean truth of arg1 > arg2
|
||||
ge
|
||||
Returns the boolean truth of arg1 >= arg2
|
||||
|
||||
For simpler multi-way equality tests, eq (only) accepts two or more
|
||||
arguments and compares the second and subsequent to the first,
|
||||
returning in effect
|
||||
|
||||
arg1==arg2 || arg1==arg3 || arg1==arg4 ...
|
||||
|
||||
(Unlike with || in Go, however, eq is a function call and all the
|
||||
arguments will be evaluated.)
|
||||
|
||||
The comparison functions work on basic types only (or named basic
|
||||
types, such as "type Celsius float32"). They implement the Go rules
|
||||
for comparison of values, except that size and exact type are
|
||||
ignored, so any integer value, signed or unsigned, may be compared
|
||||
with any other integer value. (The arithmetic value is compared,
|
||||
not the bit pattern, so all negative integers are less than all
|
||||
unsigned integers.) However, as usual, one may not compare an int
|
||||
with a float32 and so on.
|
||||
|
||||
Associated templates
|
||||
|
||||
Each template is named by a string specified when it is created. Also, each
|
||||
template is associated with zero or more other templates that it may invoke by
|
||||
name; such associations are transitive and form a name space of templates.
|
||||
|
||||
A template may use a template invocation to instantiate another associated
|
||||
template; see the explanation of the "template" action above. The name must be
|
||||
that of a template associated with the template that contains the invocation.
|
||||
|
||||
Nested template definitions
|
||||
|
||||
When parsing a template, another template may be defined and associated with the
|
||||
template being parsed. Template definitions must appear at the top level of the
|
||||
template, much like global variables in a Go program.
|
||||
|
||||
The syntax of such definitions is to surround each template declaration with a
|
||||
"define" and "end" action.
|
||||
|
||||
The define action names the template being created by providing a string
|
||||
constant. Here is a simple example:
|
||||
|
||||
`{{define "T1"}}ONE{{end}}
|
||||
{{define "T2"}}TWO{{end}}
|
||||
{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
|
||||
{{template "T3"}}`
|
||||
|
||||
This defines two templates, T1 and T2, and a third T3 that invokes the other two
|
||||
when it is executed. Finally it invokes T3. If executed this template will
|
||||
produce the text
|
||||
|
||||
ONE TWO
|
||||
|
||||
By construction, a template may reside in only one association. If it's
|
||||
necessary to have a template addressable from multiple associations, the
|
||||
template definition must be parsed multiple times to create distinct *Template
|
||||
values, or must be copied with the Clone or AddParseTree method.
|
||||
|
||||
Parse may be called multiple times to assemble the various associated templates;
|
||||
see the ParseFiles and ParseGlob functions and methods for simple ways to parse
|
||||
related templates stored in files.
|
||||
|
||||
A template may be executed directly or through ExecuteTemplate, which executes
|
||||
an associated template identified by name. To invoke our example above, we
|
||||
might write,
|
||||
|
||||
err := tmpl.Execute(os.Stdout, "no data needed")
|
||||
if err != nil {
|
||||
log.Fatalf("execution failed: %s", err)
|
||||
}
|
||||
|
||||
or to invoke a particular template explicitly by name,
|
||||
|
||||
err := tmpl.ExecuteTemplate(os.Stdout, "T2", "no data needed")
|
||||
if err != nil {
|
||||
log.Fatalf("execution failed: %s", err)
|
||||
}
|
||||
|
||||
*/
|
||||
package template
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
)
|
||||
|
||||
func ExampleTemplate() {
|
||||
// Define a template.
|
||||
const letter = `
|
||||
Dear {{.Name}},
|
||||
{{if .Attended}}
|
||||
It was a pleasure to see you at the wedding.{{else}}
|
||||
It is a shame you couldn't make it to the wedding.{{end}}
|
||||
{{with .Gift}}Thank you for the lovely {{.}}.
|
||||
{{end}}
|
||||
Best wishes,
|
||||
Josie
|
||||
`
|
||||
|
||||
// Prepare some data to insert into the template.
|
||||
type Recipient struct {
|
||||
Name, Gift string
|
||||
Attended bool
|
||||
}
|
||||
var recipients = []Recipient{
|
||||
{"Aunt Mildred", "bone china tea set", true},
|
||||
{"Uncle John", "moleskin pants", false},
|
||||
{"Cousin Rodney", "", false},
|
||||
}
|
||||
|
||||
// Create a new template and parse the letter into it.
|
||||
t := template.Must(template.New("letter").Parse(letter))
|
||||
|
||||
// Execute the template for each recipient.
|
||||
for _, r := range recipients {
|
||||
err := t.Execute(os.Stdout, r)
|
||||
if err != nil {
|
||||
log.Println("executing template:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Dear Aunt Mildred,
|
||||
//
|
||||
// It was a pleasure to see you at the wedding.
|
||||
// Thank you for the lovely bone china tea set.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
//
|
||||
// Dear Uncle John,
|
||||
//
|
||||
// It is a shame you couldn't make it to the wedding.
|
||||
// Thank you for the lovely moleskin pants.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
//
|
||||
// Dear Cousin Rodney,
|
||||
//
|
||||
// It is a shame you couldn't make it to the wedding.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
)
|
||||
|
||||
// templateFile defines the contents of a template to be stored in a file, for testing.
|
||||
type templateFile struct {
|
||||
name string
|
||||
contents string
|
||||
}
|
||||
|
||||
func createTestDir(files []templateFile) string {
|
||||
dir, err := ioutil.TempDir("", "template")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
f, err := os.Create(filepath.Join(dir, file.name))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.WriteString(f, file.contents)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// Here we demonstrate loading a set of templates from a directory.
|
||||
func ExampleTemplate_glob() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T0.tmpl is a plain template file that just invokes T1.
|
||||
{"T0.tmpl", `T0 invokes T1: ({{template "T1"}})`},
|
||||
// T1.tmpl defines a template, T1 that invokes T2.
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
// T2.tmpl defines a template T2.
|
||||
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// T0.tmpl is the first name matched, so it becomes the starting template,
|
||||
// the value returned by ParseGlob.
|
||||
tmpl := template.Must(template.ParseGlob(pattern))
|
||||
|
||||
err := tmpl.Execute(os.Stdout, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("template execution: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// T0 invokes T1: (T1 invokes T2: (This is T2))
|
||||
}
|
||||
|
||||
// This example demonstrates one way to share some templates
|
||||
// and use them in different contexts. In this variant we add multiple driver
|
||||
// templates by hand to an existing bundle of templates.
|
||||
func ExampleTemplate_helpers() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T1.tmpl defines a template, T1 that invokes T2.
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
// T2.tmpl defines a template T2.
|
||||
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// Load the helpers.
|
||||
templates := template.Must(template.ParseGlob(pattern))
|
||||
// Add one driver template to the bunch; we do this with an explicit template definition.
|
||||
_, err := templates.Parse("{{define `driver1`}}Driver 1 calls T1: ({{template `T1`}})\n{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing driver1: ", err)
|
||||
}
|
||||
// Add another driver template.
|
||||
_, err = templates.Parse("{{define `driver2`}}Driver 2 calls T2: ({{template `T2`}})\n{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing driver2: ", err)
|
||||
}
|
||||
// We load all the templates before execution. This package does not require
|
||||
// that behavior but html/template's escaping does, so it's a good habit.
|
||||
err = templates.ExecuteTemplate(os.Stdout, "driver1", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("driver1 execution: %s", err)
|
||||
}
|
||||
err = templates.ExecuteTemplate(os.Stdout, "driver2", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("driver2 execution: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// Driver 1 calls T1: (T1 invokes T2: (This is T2))
|
||||
// Driver 2 calls T2: (This is T2)
|
||||
}
|
||||
|
||||
// This example demonstrates how to use one group of driver
|
||||
// templates with distinct sets of helper templates.
|
||||
func ExampleTemplate_share() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T0.tmpl is a plain template file that just invokes T1.
|
||||
{"T0.tmpl", "T0 ({{.}} version) invokes T1: ({{template `T1`}})\n"},
|
||||
// T1.tmpl defines a template, T1 that invokes T2. Note T2 is not defined
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// Load the drivers.
|
||||
drivers := template.Must(template.ParseGlob(pattern))
|
||||
|
||||
// We must define an implementation of the T2 template. First we clone
|
||||
// the drivers, then add a definition of T2 to the template name space.
|
||||
|
||||
// 1. Clone the helper set to create a new name space from which to run them.
|
||||
first, err := drivers.Clone()
|
||||
if err != nil {
|
||||
log.Fatal("cloning helpers: ", err)
|
||||
}
|
||||
// 2. Define T2, version A, and parse it.
|
||||
_, err = first.Parse("{{define `T2`}}T2, version A{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing T2: ", err)
|
||||
}
|
||||
|
||||
// Now repeat the whole thing, using a different version of T2.
|
||||
// 1. Clone the drivers.
|
||||
second, err := drivers.Clone()
|
||||
if err != nil {
|
||||
log.Fatal("cloning drivers: ", err)
|
||||
}
|
||||
// 2. Define T2, version B, and parse it.
|
||||
_, err = second.Parse("{{define `T2`}}T2, version B{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing T2: ", err)
|
||||
}
|
||||
|
||||
// Execute the templates in the reverse order to verify the
|
||||
// first is unaffected by the second.
|
||||
err = second.ExecuteTemplate(os.Stdout, "T0.tmpl", "second")
|
||||
if err != nil {
|
||||
log.Fatalf("second execution: %s", err)
|
||||
}
|
||||
err = first.ExecuteTemplate(os.Stdout, "T0.tmpl", "first")
|
||||
if err != nil {
|
||||
log.Fatalf("first: execution: %s", err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// T0 (second version) invokes T1: (T1 invokes T2: (T2, version B))
|
||||
// T0 (first version) invokes T1: (T1 invokes T2: (T2, version A))
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
)
|
||||
|
||||
// This example demonstrates a custom function to process template text.
|
||||
// It installs the strings.Title function and uses it to
|
||||
// Make Title Text Look Good In Our Template's Output.
|
||||
func ExampleTemplate_func() {
|
||||
// First we create a FuncMap with which to register the function.
|
||||
funcMap := template.FuncMap{
|
||||
// The name "title" is what the function will be called in the template text.
|
||||
"title": strings.Title,
|
||||
}
|
||||
|
||||
// A simple template definition to test our function.
|
||||
// We print the input text several ways:
|
||||
// - the original
|
||||
// - title-cased
|
||||
// - title-cased and then printed with %q
|
||||
// - printed with %q and then title-cased.
|
||||
const templateText = `
|
||||
Input: {{printf "%q" .}}
|
||||
Output 0: {{title .}}
|
||||
Output 1: {{title . | printf "%q"}}
|
||||
Output 2: {{printf "%q" . | title}}
|
||||
`
|
||||
|
||||
// Create a template, add the function map, and parse the text.
|
||||
tmpl, err := template.New("titleTest").Funcs(funcMap).Parse(templateText)
|
||||
if err != nil {
|
||||
log.Fatalf("parsing: %s", err)
|
||||
}
|
||||
|
||||
// Run the template to verify the output.
|
||||
err = tmpl.Execute(os.Stdout, "the go programming language")
|
||||
if err != nil {
|
||||
log.Fatalf("execution: %s", err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Input: "the go programming language"
|
||||
// Output 0: The Go Programming Language
|
||||
// Output 1: "The Go Programming Language"
|
||||
// Output 2: "The Go Programming Language"
|
||||
}
|
|
@ -0,0 +1,845 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/template/parse"
|
||||
)
|
||||
|
||||
// state represents the state of an execution. It's not part of the
|
||||
// template so that multiple executions of the same template
|
||||
// can execute in parallel.
|
||||
type state struct {
|
||||
tmpl *Template
|
||||
wr io.Writer
|
||||
node parse.Node // current node, for errors
|
||||
vars []variable // push-down stack of variable values.
|
||||
}
|
||||
|
||||
// variable holds the dynamic value of a variable such as $, $x etc.
|
||||
type variable struct {
|
||||
name string
|
||||
value reflect.Value
|
||||
}
|
||||
|
||||
// push pushes a new variable on the stack.
|
||||
func (s *state) push(name string, value reflect.Value) {
|
||||
s.vars = append(s.vars, variable{name, value})
|
||||
}
|
||||
|
||||
// mark returns the length of the variable stack.
|
||||
func (s *state) mark() int {
|
||||
return len(s.vars)
|
||||
}
|
||||
|
||||
// pop pops the variable stack up to the mark.
|
||||
func (s *state) pop(mark int) {
|
||||
s.vars = s.vars[0:mark]
|
||||
}
|
||||
|
||||
// setVar overwrites the top-nth variable on the stack. Used by range iterations.
|
||||
func (s *state) setVar(n int, value reflect.Value) {
|
||||
s.vars[len(s.vars)-n].value = value
|
||||
}
|
||||
|
||||
// varValue returns the value of the named variable.
|
||||
func (s *state) varValue(name string) reflect.Value {
|
||||
for i := s.mark() - 1; i >= 0; i-- {
|
||||
if s.vars[i].name == name {
|
||||
return s.vars[i].value
|
||||
}
|
||||
}
|
||||
s.errorf("undefined variable: %s", name)
|
||||
return zero
|
||||
}
|
||||
|
||||
var zero reflect.Value
|
||||
|
||||
// at marks the state to be on node n, for error reporting.
|
||||
func (s *state) at(node parse.Node) {
|
||||
s.node = node
|
||||
}
|
||||
|
||||
// doublePercent returns the string with %'s replaced by %%, if necessary,
|
||||
// so it can be used safely inside a Printf format string.
|
||||
func doublePercent(str string) string {
|
||||
if strings.Contains(str, "%") {
|
||||
str = strings.Replace(str, "%", "%%", -1)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// errorf formats the error and terminates processing.
|
||||
func (s *state) errorf(format string, args ...interface{}) {
|
||||
name := doublePercent(s.tmpl.Name())
|
||||
if s.node == nil {
|
||||
format = fmt.Sprintf("template: %s: %s", name, format)
|
||||
} else {
|
||||
location, context := s.tmpl.ErrorContext(s.node)
|
||||
format = fmt.Sprintf("template: %s: executing %q at <%s>: %s", location, name, doublePercent(context), format)
|
||||
}
|
||||
panic(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
// errRecover is the handler that turns panics into returns from the top
|
||||
// level of Parse.
|
||||
func errRecover(errp *error) {
|
||||
e := recover()
|
||||
if e != nil {
|
||||
switch err := e.(type) {
|
||||
case runtime.Error:
|
||||
panic(e)
|
||||
case error:
|
||||
*errp = err
|
||||
default:
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteTemplate applies the template associated with t that has the given name
|
||||
// to the specified data object and writes the output to wr.
|
||||
// If an error occurs executing the template or writing its output,
|
||||
// execution stops, but partial results may already have been written to
|
||||
// the output writer.
|
||||
// A template may be executed safely in parallel.
|
||||
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error {
|
||||
tmpl := t.tmpl[name]
|
||||
if tmpl == nil {
|
||||
return fmt.Errorf("template: no template %q associated with template %q", name, t.name)
|
||||
}
|
||||
return tmpl.Execute(wr, data)
|
||||
}
|
||||
|
||||
// Execute applies a parsed template to the specified data object,
|
||||
// and writes the output to wr.
|
||||
// If an error occurs executing the template or writing its output,
|
||||
// execution stops, but partial results may already have been written to
|
||||
// the output writer.
|
||||
// A template may be executed safely in parallel.
|
||||
func (t *Template) Execute(wr io.Writer, data interface{}) (err error) {
|
||||
defer errRecover(&err)
|
||||
value := reflect.ValueOf(data)
|
||||
state := &state{
|
||||
tmpl: t,
|
||||
wr: wr,
|
||||
vars: []variable{{"$", value}},
|
||||
}
|
||||
t.init()
|
||||
if t.Tree == nil || t.Root == nil {
|
||||
var b bytes.Buffer
|
||||
for name, tmpl := range t.tmpl {
|
||||
if tmpl.Tree == nil || tmpl.Root == nil {
|
||||
continue
|
||||
}
|
||||
if b.Len() > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&b, "%q", name)
|
||||
}
|
||||
var s string
|
||||
if b.Len() > 0 {
|
||||
s = "; defined templates are: " + b.String()
|
||||
}
|
||||
state.errorf("%q is an incomplete or empty template%s", t.Name(), s)
|
||||
}
|
||||
state.walk(value, t.Root)
|
||||
return
|
||||
}
|
||||
|
||||
// Walk functions step through the major pieces of the template structure,
|
||||
// generating output as they go.
|
||||
func (s *state) walk(dot reflect.Value, node parse.Node) {
|
||||
s.at(node)
|
||||
switch node := node.(type) {
|
||||
case *parse.ActionNode:
|
||||
// Do not pop variables so they persist until next end.
|
||||
// Also, if the action declares variables, don't print the result.
|
||||
val := s.evalPipeline(dot, node.Pipe)
|
||||
if len(node.Pipe.Decl) == 0 {
|
||||
s.printValue(node, val)
|
||||
}
|
||||
case *parse.IfNode:
|
||||
s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)
|
||||
case *parse.ListNode:
|
||||
for _, node := range node.Nodes {
|
||||
s.walk(dot, node)
|
||||
}
|
||||
case *parse.RangeNode:
|
||||
s.walkRange(dot, node)
|
||||
case *parse.TemplateNode:
|
||||
s.walkTemplate(dot, node)
|
||||
case *parse.TextNode:
|
||||
if _, err := s.wr.Write(node.Text); err != nil {
|
||||
s.errorf("%s", err)
|
||||
}
|
||||
case *parse.WithNode:
|
||||
s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, node.ElseList)
|
||||
default:
|
||||
s.errorf("unknown node: %s", node)
|
||||
}
|
||||
}
|
||||
|
||||
// walkIfOrWith walks an 'if' or 'with' node. The two control structures
|
||||
// are identical in behavior except that 'with' sets dot.
|
||||
func (s *state) walkIfOrWith(typ parse.NodeType, dot reflect.Value, pipe *parse.PipeNode, list, elseList *parse.ListNode) {
|
||||
defer s.pop(s.mark())
|
||||
val := s.evalPipeline(dot, pipe)
|
||||
truth, ok := isTrue(val)
|
||||
if !ok {
|
||||
s.errorf("if/with can't use %v", val)
|
||||
}
|
||||
if truth {
|
||||
if typ == parse.NodeWith {
|
||||
s.walk(val, list)
|
||||
} else {
|
||||
s.walk(dot, list)
|
||||
}
|
||||
} else if elseList != nil {
|
||||
s.walk(dot, elseList)
|
||||
}
|
||||
}
|
||||
|
||||
// isTrue reports whether the value is 'true', in the sense of not the zero of its type,
|
||||
// and whether the value has a meaningful truth value.
|
||||
func isTrue(val reflect.Value) (truth, ok bool) {
|
||||
if !val.IsValid() {
|
||||
// Something like var x interface{}, never set. It's a form of nil.
|
||||
return false, true
|
||||
}
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
truth = val.Len() > 0
|
||||
case reflect.Bool:
|
||||
truth = val.Bool()
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
truth = val.Complex() != 0
|
||||
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
|
||||
truth = !val.IsNil()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
truth = val.Int() != 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
truth = val.Float() != 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
truth = val.Uint() != 0
|
||||
case reflect.Struct:
|
||||
truth = true // Struct values are always true.
|
||||
default:
|
||||
return
|
||||
}
|
||||
return truth, true
|
||||
}
|
||||
|
||||
func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
|
||||
s.at(r)
|
||||
defer s.pop(s.mark())
|
||||
val, _ := indirect(s.evalPipeline(dot, r.Pipe))
|
||||
// mark top of stack before any variables in the body are pushed.
|
||||
mark := s.mark()
|
||||
oneIteration := func(index, elem reflect.Value) {
|
||||
// Set top var (lexically the second if there are two) to the element.
|
||||
if len(r.Pipe.Decl) > 0 {
|
||||
s.setVar(1, elem)
|
||||
}
|
||||
// Set next var (lexically the first if there are two) to the index.
|
||||
if len(r.Pipe.Decl) > 1 {
|
||||
s.setVar(2, index)
|
||||
}
|
||||
s.walk(elem, r.List)
|
||||
s.pop(mark)
|
||||
}
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
if val.Len() == 0 {
|
||||
break
|
||||
}
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
oneIteration(reflect.ValueOf(i), val.Index(i))
|
||||
}
|
||||
return
|
||||
case reflect.Map:
|
||||
if val.Len() == 0 {
|
||||
break
|
||||
}
|
||||
for _, key := range sortKeys(val.MapKeys()) {
|
||||
oneIteration(key, val.MapIndex(key))
|
||||
}
|
||||
return
|
||||
case reflect.Chan:
|
||||
if val.IsNil() {
|
||||
break
|
||||
}
|
||||
i := 0
|
||||
for ; ; i++ {
|
||||
elem, ok := val.Recv()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
oneIteration(reflect.ValueOf(i), elem)
|
||||
}
|
||||
if i == 0 {
|
||||
break
|
||||
}
|
||||
return
|
||||
case reflect.Invalid:
|
||||
break // An invalid value is likely a nil map, etc. and acts like an empty map.
|
||||
default:
|
||||
s.errorf("range can't iterate over %v", val)
|
||||
}
|
||||
if r.ElseList != nil {
|
||||
s.walk(dot, r.ElseList)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
|
||||
s.at(t)
|
||||
tmpl := s.tmpl.tmpl[t.Name]
|
||||
if tmpl == nil {
|
||||
s.errorf("template %q not defined", t.Name)
|
||||
}
|
||||
// Variables declared by the pipeline persist.
|
||||
dot = s.evalPipeline(dot, t.Pipe)
|
||||
newState := *s
|
||||
newState.tmpl = tmpl
|
||||
// No dynamic scoping: template invocations inherit no variables.
|
||||
newState.vars = []variable{{"$", dot}}
|
||||
newState.walk(dot, tmpl.Root)
|
||||
}
|
||||
|
||||
// Eval functions evaluate pipelines, commands, and their elements and extract
|
||||
// values from the data structure by examining fields, calling methods, and so on.
|
||||
// The printing of those values happens only through walk functions.
|
||||
|
||||
// evalPipeline returns the value acquired by evaluating a pipeline. If the
|
||||
// pipeline has a variable declaration, the variable will be pushed on the
|
||||
// stack. Callers should therefore pop the stack after they are finished
|
||||
// executing commands depending on the pipeline value.
|
||||
func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value reflect.Value) {
|
||||
if pipe == nil {
|
||||
return
|
||||
}
|
||||
s.at(pipe)
|
||||
for _, cmd := range pipe.Cmds {
|
||||
value = s.evalCommand(dot, cmd, value) // previous value is this one's final arg.
|
||||
// If the object has type interface{}, dig down one level to the thing inside.
|
||||
if value.Kind() == reflect.Interface && value.Type().NumMethod() == 0 {
|
||||
value = reflect.ValueOf(value.Interface()) // lovely!
|
||||
}
|
||||
}
|
||||
for _, variable := range pipe.Decl {
|
||||
s.push(variable.Ident[0], value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (s *state) notAFunction(args []parse.Node, final reflect.Value) {
|
||||
if len(args) > 1 || final.IsValid() {
|
||||
s.errorf("can't give argument to non-function %s", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final reflect.Value) reflect.Value {
|
||||
firstWord := cmd.Args[0]
|
||||
switch n := firstWord.(type) {
|
||||
case *parse.FieldNode:
|
||||
return s.evalFieldNode(dot, n, cmd.Args, final)
|
||||
case *parse.ChainNode:
|
||||
return s.evalChainNode(dot, n, cmd.Args, final)
|
||||
case *parse.IdentifierNode:
|
||||
// Must be a function.
|
||||
return s.evalFunction(dot, n, cmd, cmd.Args, final)
|
||||
case *parse.PipeNode:
|
||||
// Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored.
|
||||
return s.evalPipeline(dot, n)
|
||||
case *parse.VariableNode:
|
||||
return s.evalVariableNode(dot, n, cmd.Args, final)
|
||||
}
|
||||
s.at(firstWord)
|
||||
s.notAFunction(cmd.Args, final)
|
||||
switch word := firstWord.(type) {
|
||||
case *parse.BoolNode:
|
||||
return reflect.ValueOf(word.True)
|
||||
case *parse.DotNode:
|
||||
return dot
|
||||
case *parse.NilNode:
|
||||
s.errorf("nil is not a command")
|
||||
case *parse.NumberNode:
|
||||
return s.idealConstant(word)
|
||||
case *parse.StringNode:
|
||||
return reflect.ValueOf(word.Text)
|
||||
}
|
||||
s.errorf("can't evaluate command %q", firstWord)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
// idealConstant is called to return the value of a number in a context where
|
||||
// we don't know the type. In that case, the syntax of the number tells us
|
||||
// its type, and we use Go rules to resolve. Note there is no such thing as
|
||||
// a uint ideal constant in this situation - the value must be of int type.
|
||||
func (s *state) idealConstant(constant *parse.NumberNode) reflect.Value {
|
||||
// These are ideal constants but we don't know the type
|
||||
// and we have no context. (If it was a method argument,
|
||||
// we'd know what we need.) The syntax guides us to some extent.
|
||||
s.at(constant)
|
||||
switch {
|
||||
case constant.IsComplex:
|
||||
return reflect.ValueOf(constant.Complex128) // incontrovertible.
|
||||
case constant.IsFloat && !isHexConstant(constant.Text) && strings.IndexAny(constant.Text, ".eE") >= 0:
|
||||
return reflect.ValueOf(constant.Float64)
|
||||
case constant.IsInt:
|
||||
n := int(constant.Int64)
|
||||
if int64(n) != constant.Int64 {
|
||||
s.errorf("%s overflows int", constant.Text)
|
||||
}
|
||||
return reflect.ValueOf(n)
|
||||
case constant.IsUint:
|
||||
s.errorf("%s overflows int", constant.Text)
|
||||
}
|
||||
return zero
|
||||
}
|
||||
|
||||
func isHexConstant(s string) bool {
|
||||
return len(s) > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')
|
||||
}
|
||||
|
||||
func (s *state) evalFieldNode(dot reflect.Value, field *parse.FieldNode, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
s.at(field)
|
||||
return s.evalFieldChain(dot, dot, field, field.Ident, args, final)
|
||||
}
|
||||
|
||||
func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
s.at(chain)
|
||||
// (pipe).Field1.Field2 has pipe as .Node, fields as .Field. Eval the pipeline, then the fields.
|
||||
pipe := s.evalArg(dot, nil, chain.Node)
|
||||
if len(chain.Field) == 0 {
|
||||
s.errorf("internal error: no fields in evalChainNode")
|
||||
}
|
||||
return s.evalFieldChain(dot, pipe, chain, chain.Field, args, final)
|
||||
}
|
||||
|
||||
func (s *state) evalVariableNode(dot reflect.Value, variable *parse.VariableNode, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
|
||||
s.at(variable)
|
||||
value := s.varValue(variable.Ident[0])
|
||||
if len(variable.Ident) == 1 {
|
||||
s.notAFunction(args, final)
|
||||
return value
|
||||
}
|
||||
return s.evalFieldChain(dot, value, variable, variable.Ident[1:], args, final)
|
||||
}
|
||||
|
||||
// evalFieldChain evaluates .X.Y.Z possibly followed by arguments.
|
||||
// dot is the environment in which to evaluate arguments, while
|
||||
// receiver is the value being walked along the chain.
|
||||
func (s *state) evalFieldChain(dot, receiver reflect.Value, node parse.Node, ident []string, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
n := len(ident)
|
||||
for i := 0; i < n-1; i++ {
|
||||
receiver = s.evalField(dot, ident[i], node, nil, zero, receiver)
|
||||
}
|
||||
// Now if it's a method, it gets the arguments.
|
||||
return s.evalField(dot, ident[n-1], node, args, final, receiver)
|
||||
}
|
||||
|
||||
func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd parse.Node, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
s.at(node)
|
||||
name := node.Ident
|
||||
function, ok := findFunction(name, s.tmpl)
|
||||
if !ok {
|
||||
s.errorf("%q is not a defined function", name)
|
||||
}
|
||||
return s.evalCall(dot, function, cmd, name, args, final)
|
||||
}
|
||||
|
||||
// evalField evaluates an expression like (.Field) or (.Field arg1 arg2).
|
||||
// The 'final' argument represents the return value from the preceding
|
||||
// value of the pipeline, if any.
|
||||
func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, args []parse.Node, final, receiver reflect.Value) reflect.Value {
|
||||
if !receiver.IsValid() {
|
||||
return zero
|
||||
}
|
||||
typ := receiver.Type()
|
||||
receiver, _ = indirect(receiver)
|
||||
// Unless it's an interface, need to get to a value of type *T to guarantee
|
||||
// we see all methods of T and *T.
|
||||
ptr := receiver
|
||||
if ptr.Kind() != reflect.Interface && ptr.CanAddr() {
|
||||
ptr = ptr.Addr()
|
||||
}
|
||||
if method := ptr.MethodByName(fieldName); method.IsValid() {
|
||||
return s.evalCall(dot, method, node, fieldName, args, final)
|
||||
}
|
||||
hasArgs := len(args) > 1 || final.IsValid()
|
||||
// It's not a method; must be a field of a struct or an element of a map. The receiver must not be nil.
|
||||
receiver, isNil := indirect(receiver)
|
||||
if isNil {
|
||||
s.errorf("nil pointer evaluating %s.%s", typ, fieldName)
|
||||
}
|
||||
switch receiver.Kind() {
|
||||
case reflect.Struct:
|
||||
tField, ok := receiver.Type().FieldByName(fieldName)
|
||||
if ok {
|
||||
field := receiver.FieldByIndex(tField.Index)
|
||||
if tField.PkgPath != "" { // field is unexported
|
||||
s.errorf("%s is an unexported field of struct type %s", fieldName, typ)
|
||||
}
|
||||
// If it's a function, we must call it.
|
||||
if hasArgs {
|
||||
s.errorf("%s has arguments but cannot be invoked as function", fieldName)
|
||||
}
|
||||
return field
|
||||
}
|
||||
s.errorf("%s is not a field of struct type %s", fieldName, typ)
|
||||
case reflect.Map:
|
||||
// If it's a map, attempt to use the field name as a key.
|
||||
nameVal := reflect.ValueOf(fieldName)
|
||||
if nameVal.Type().AssignableTo(receiver.Type().Key()) {
|
||||
if hasArgs {
|
||||
s.errorf("%s is not a method but has arguments", fieldName)
|
||||
}
|
||||
return receiver.MapIndex(nameVal)
|
||||
}
|
||||
}
|
||||
s.errorf("can't evaluate field %s in type %s", fieldName, typ)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
var (
|
||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
||||
)
|
||||
|
||||
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
|
||||
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
|
||||
// as the function itself.
|
||||
func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
if args != nil {
|
||||
args = args[1:] // Zeroth arg is function name/node; not passed to function.
|
||||
}
|
||||
typ := fun.Type()
|
||||
numIn := len(args)
|
||||
if final.IsValid() {
|
||||
numIn++
|
||||
}
|
||||
numFixed := len(args)
|
||||
if typ.IsVariadic() {
|
||||
numFixed = typ.NumIn() - 1 // last arg is the variadic one.
|
||||
if numIn < numFixed {
|
||||
s.errorf("wrong number of args for %s: want at least %d got %d", name, typ.NumIn()-1, len(args))
|
||||
}
|
||||
} else if numIn < typ.NumIn()-1 || !typ.IsVariadic() && numIn != typ.NumIn() {
|
||||
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), len(args))
|
||||
}
|
||||
if !goodFunc(typ) {
|
||||
// TODO: This could still be a confusing error; maybe goodFunc should provide info.
|
||||
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
|
||||
}
|
||||
// Build the arg list.
|
||||
argv := make([]reflect.Value, numIn)
|
||||
// Args must be evaluated. Fixed args first.
|
||||
i := 0
|
||||
for ; i < numFixed && i < len(args); i++ {
|
||||
argv[i] = s.evalArg(dot, typ.In(i), args[i])
|
||||
}
|
||||
// Now the ... args.
|
||||
if typ.IsVariadic() {
|
||||
argType := typ.In(typ.NumIn() - 1).Elem() // Argument is a slice.
|
||||
for ; i < len(args); i++ {
|
||||
argv[i] = s.evalArg(dot, argType, args[i])
|
||||
}
|
||||
}
|
||||
// Add final value if necessary.
|
||||
if final.IsValid() {
|
||||
t := typ.In(typ.NumIn() - 1)
|
||||
if typ.IsVariadic() {
|
||||
t = t.Elem()
|
||||
}
|
||||
argv[i] = s.validateType(final, t)
|
||||
}
|
||||
result := fun.Call(argv)
|
||||
// If we have an error that is not nil, stop execution and return that error to the caller.
|
||||
if len(result) == 2 && !result[1].IsNil() {
|
||||
s.at(node)
|
||||
s.errorf("error calling %s: %s", name, result[1].Interface().(error))
|
||||
}
|
||||
return result[0]
|
||||
}
|
||||
|
||||
// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero.
|
||||
func canBeNil(typ reflect.Type) bool {
|
||||
switch typ.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// validateType guarantees that the value is valid and assignable to the type.
|
||||
func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Value {
|
||||
if !value.IsValid() {
|
||||
if typ == nil || canBeNil(typ) {
|
||||
// An untyped nil interface{}. Accept as a proper nil value.
|
||||
return reflect.Zero(typ)
|
||||
}
|
||||
s.errorf("invalid value; expected %s", typ)
|
||||
}
|
||||
if typ != nil && !value.Type().AssignableTo(typ) {
|
||||
if value.Kind() == reflect.Interface && !value.IsNil() {
|
||||
value = value.Elem()
|
||||
if value.Type().AssignableTo(typ) {
|
||||
return value
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
// Does one dereference or indirection work? We could do more, as we
|
||||
// do with method receivers, but that gets messy and method receivers
|
||||
// are much more constrained, so it makes more sense there than here.
|
||||
// Besides, one is almost always all you need.
|
||||
switch {
|
||||
case value.Kind() == reflect.Ptr && value.Type().Elem().AssignableTo(typ):
|
||||
value = value.Elem()
|
||||
if !value.IsValid() {
|
||||
s.errorf("dereference of nil pointer of type %s", typ)
|
||||
}
|
||||
case reflect.PtrTo(value.Type()).AssignableTo(typ) && value.CanAddr():
|
||||
value = value.Addr()
|
||||
default:
|
||||
s.errorf("wrong type for value; expected %s; got %s", typ, value.Type())
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
switch arg := n.(type) {
|
||||
case *parse.DotNode:
|
||||
return s.validateType(dot, typ)
|
||||
case *parse.NilNode:
|
||||
if canBeNil(typ) {
|
||||
return reflect.Zero(typ)
|
||||
}
|
||||
s.errorf("cannot assign nil to %s", typ)
|
||||
case *parse.FieldNode:
|
||||
return s.validateType(s.evalFieldNode(dot, arg, []parse.Node{n}, zero), typ)
|
||||
case *parse.VariableNode:
|
||||
return s.validateType(s.evalVariableNode(dot, arg, nil, zero), typ)
|
||||
case *parse.PipeNode:
|
||||
return s.validateType(s.evalPipeline(dot, arg), typ)
|
||||
case *parse.IdentifierNode:
|
||||
return s.evalFunction(dot, arg, arg, nil, zero)
|
||||
case *parse.ChainNode:
|
||||
return s.validateType(s.evalChainNode(dot, arg, nil, zero), typ)
|
||||
}
|
||||
switch typ.Kind() {
|
||||
case reflect.Bool:
|
||||
return s.evalBool(typ, n)
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return s.evalComplex(typ, n)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return s.evalFloat(typ, n)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return s.evalInteger(typ, n)
|
||||
case reflect.Interface:
|
||||
if typ.NumMethod() == 0 {
|
||||
return s.evalEmptyInterface(dot, n)
|
||||
}
|
||||
case reflect.String:
|
||||
return s.evalString(typ, n)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return s.evalUnsignedInteger(typ, n)
|
||||
}
|
||||
s.errorf("can't handle %s for arg of type %s", n, typ)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalBool(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
if n, ok := n.(*parse.BoolNode); ok {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetBool(n.True)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected bool; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalString(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
if n, ok := n.(*parse.StringNode); ok {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetString(n.Text)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected string; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalInteger(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
if n, ok := n.(*parse.NumberNode); ok && n.IsInt {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetInt(n.Int64)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected integer; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalUnsignedInteger(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
if n, ok := n.(*parse.NumberNode); ok && n.IsUint {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetUint(n.Uint64)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected unsigned integer; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalFloat(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
if n, ok := n.(*parse.NumberNode); ok && n.IsFloat {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetFloat(n.Float64)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected float; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalComplex(typ reflect.Type, n parse.Node) reflect.Value {
|
||||
if n, ok := n.(*parse.NumberNode); ok && n.IsComplex {
|
||||
value := reflect.New(typ).Elem()
|
||||
value.SetComplex(n.Complex128)
|
||||
return value
|
||||
}
|
||||
s.errorf("expected complex; found %s", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Value {
|
||||
s.at(n)
|
||||
switch n := n.(type) {
|
||||
case *parse.BoolNode:
|
||||
return reflect.ValueOf(n.True)
|
||||
case *parse.DotNode:
|
||||
return dot
|
||||
case *parse.FieldNode:
|
||||
return s.evalFieldNode(dot, n, nil, zero)
|
||||
case *parse.IdentifierNode:
|
||||
return s.evalFunction(dot, n, n, nil, zero)
|
||||
case *parse.NilNode:
|
||||
// NilNode is handled in evalArg, the only place that calls here.
|
||||
s.errorf("evalEmptyInterface: nil (can't happen)")
|
||||
case *parse.NumberNode:
|
||||
return s.idealConstant(n)
|
||||
case *parse.StringNode:
|
||||
return reflect.ValueOf(n.Text)
|
||||
case *parse.VariableNode:
|
||||
return s.evalVariableNode(dot, n, nil, zero)
|
||||
case *parse.PipeNode:
|
||||
return s.evalPipeline(dot, n)
|
||||
}
|
||||
s.errorf("can't handle assignment of %s to empty interface argument", n)
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
// indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
|
||||
// We indirect through pointers and empty interfaces (only) because
|
||||
// non-empty interfaces have methods we might need.
|
||||
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
|
||||
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
|
||||
if v.IsNil() {
|
||||
return v, true
|
||||
}
|
||||
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
|
||||
// printValue writes the textual representation of the value to the output of
|
||||
// the template.
|
||||
func (s *state) printValue(n parse.Node, v reflect.Value) {
|
||||
s.at(n)
|
||||
iface, ok := printableValue(v)
|
||||
if !ok {
|
||||
s.errorf("can't print %s of type %s", n, v.Type())
|
||||
}
|
||||
fmt.Fprint(s.wr, iface)
|
||||
}
|
||||
|
||||
// printableValue returns the, possibly indirected, interface value inside v that
|
||||
// is best for a call to formatted printer.
|
||||
func printableValue(v reflect.Value) (interface{}, bool) {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v, _ = indirect(v) // fmt.Fprint handles nil.
|
||||
}
|
||||
if !v.IsValid() {
|
||||
return "<no value>", true
|
||||
}
|
||||
|
||||
if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
|
||||
if v.CanAddr() && (reflect.PtrTo(v.Type()).Implements(errorType) || reflect.PtrTo(v.Type()).Implements(fmtStringerType)) {
|
||||
v = v.Addr()
|
||||
} else {
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return v.Interface(), true
|
||||
}
|
||||
|
||||
// Types to help sort the keys in a map for reproducible output.
|
||||
|
||||
type rvs []reflect.Value
|
||||
|
||||
func (x rvs) Len() int { return len(x) }
|
||||
func (x rvs) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
|
||||
type rvInts struct{ rvs }
|
||||
|
||||
func (x rvInts) Less(i, j int) bool { return x.rvs[i].Int() < x.rvs[j].Int() }
|
||||
|
||||
type rvUints struct{ rvs }
|
||||
|
||||
func (x rvUints) Less(i, j int) bool { return x.rvs[i].Uint() < x.rvs[j].Uint() }
|
||||
|
||||
type rvFloats struct{ rvs }
|
||||
|
||||
func (x rvFloats) Less(i, j int) bool { return x.rvs[i].Float() < x.rvs[j].Float() }
|
||||
|
||||
type rvStrings struct{ rvs }
|
||||
|
||||
func (x rvStrings) Less(i, j int) bool { return x.rvs[i].String() < x.rvs[j].String() }
|
||||
|
||||
// sortKeys sorts (if it can) the slice of reflect.Values, which is a slice of map keys.
|
||||
func sortKeys(v []reflect.Value) []reflect.Value {
|
||||
if len(v) <= 1 {
|
||||
return v
|
||||
}
|
||||
switch v[0].Kind() {
|
||||
case reflect.Float32, reflect.Float64:
|
||||
sort.Sort(rvFloats{v})
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
sort.Sort(rvInts{v})
|
||||
case reflect.String:
|
||||
sort.Sort(rvStrings{v})
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
sort.Sort(rvUints{v})
|
||||
}
|
||||
return v
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,598 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// FuncMap is the type of the map defining the mapping from names to functions.
|
||||
// Each function must have either a single return value, or two return values of
|
||||
// which the second has type error. In that case, if the second (error)
|
||||
// return value evaluates to non-nil during execution, execution terminates and
|
||||
// Execute returns that error.
|
||||
type FuncMap map[string]interface{}
|
||||
|
||||
var builtins = FuncMap{
|
||||
"and": and,
|
||||
"call": call,
|
||||
"html": HTMLEscaper,
|
||||
"index": index,
|
||||
"js": JSEscaper,
|
||||
"len": length,
|
||||
"not": not,
|
||||
"or": or,
|
||||
"print": fmt.Sprint,
|
||||
"printf": fmt.Sprintf,
|
||||
"println": fmt.Sprintln,
|
||||
"urlquery": URLQueryEscaper,
|
||||
|
||||
// Comparisons
|
||||
"eq": eq, // ==
|
||||
"ge": ge, // >=
|
||||
"gt": gt, // >
|
||||
"le": le, // <=
|
||||
"lt": lt, // <
|
||||
"ne": ne, // !=
|
||||
}
|
||||
|
||||
var builtinFuncs = createValueFuncs(builtins)
|
||||
|
||||
// createValueFuncs turns a FuncMap into a map[string]reflect.Value
|
||||
func createValueFuncs(funcMap FuncMap) map[string]reflect.Value {
|
||||
m := make(map[string]reflect.Value)
|
||||
addValueFuncs(m, funcMap)
|
||||
return m
|
||||
}
|
||||
|
||||
// addValueFuncs adds to values the functions in funcs, converting them to reflect.Values.
|
||||
func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
|
||||
for name, fn := range in {
|
||||
v := reflect.ValueOf(fn)
|
||||
if v.Kind() != reflect.Func {
|
||||
panic("value for " + name + " not a function")
|
||||
}
|
||||
if !goodFunc(v.Type()) {
|
||||
panic(fmt.Errorf("can't install method/function %q with %d results", name, v.Type().NumOut()))
|
||||
}
|
||||
out[name] = v
|
||||
}
|
||||
}
|
||||
|
||||
// addFuncs adds to values the functions in funcs. It does no checking of the input -
|
||||
// call addValueFuncs first.
|
||||
func addFuncs(out, in FuncMap) {
|
||||
for name, fn := range in {
|
||||
out[name] = fn
|
||||
}
|
||||
}
|
||||
|
||||
// goodFunc checks that the function or method has the right result signature.
|
||||
func goodFunc(typ reflect.Type) bool {
|
||||
// We allow functions with 1 result or 2 results where the second is an error.
|
||||
switch {
|
||||
case typ.NumOut() == 1:
|
||||
return true
|
||||
case typ.NumOut() == 2 && typ.Out(1) == errorType:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// findFunction looks for a function in the template, and global map.
|
||||
func findFunction(name string, tmpl *Template) (reflect.Value, bool) {
|
||||
if tmpl != nil && tmpl.common != nil {
|
||||
if fn := tmpl.execFuncs[name]; fn.IsValid() {
|
||||
return fn, true
|
||||
}
|
||||
}
|
||||
if fn := builtinFuncs[name]; fn.IsValid() {
|
||||
return fn, true
|
||||
}
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
|
||||
// Indexing.
|
||||
|
||||
// index returns the result of indexing its first argument by the following
|
||||
// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
|
||||
// indexed item must be a map, slice, or array.
|
||||
func index(item interface{}, indices ...interface{}) (interface{}, error) {
|
||||
v := reflect.ValueOf(item)
|
||||
for _, i := range indices {
|
||||
index := reflect.ValueOf(i)
|
||||
var isNil bool
|
||||
if v, isNil = indirect(v); isNil {
|
||||
return nil, fmt.Errorf("index of nil pointer")
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.String:
|
||||
var x int64
|
||||
switch index.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
x = index.Int()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
x = int64(index.Uint())
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot index slice/array with type %s", index.Type())
|
||||
}
|
||||
if x < 0 || x >= int64(v.Len()) {
|
||||
return nil, fmt.Errorf("index out of range: %d", x)
|
||||
}
|
||||
v = v.Index(int(x))
|
||||
case reflect.Map:
|
||||
if !index.IsValid() {
|
||||
index = reflect.Zero(v.Type().Key())
|
||||
}
|
||||
if !index.Type().AssignableTo(v.Type().Key()) {
|
||||
return nil, fmt.Errorf("%s is not index type for %s", index.Type(), v.Type())
|
||||
}
|
||||
if x := v.MapIndex(index); x.IsValid() {
|
||||
v = x
|
||||
} else {
|
||||
v = reflect.Zero(v.Type().Elem())
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("can't index item of type %s", v.Type())
|
||||
}
|
||||
}
|
||||
return v.Interface(), nil
|
||||
}
|
||||
|
||||
// Length
|
||||
|
||||
// length returns the length of the item, with an error if it has no defined length.
|
||||
func length(item interface{}) (int, error) {
|
||||
v, isNil := indirect(reflect.ValueOf(item))
|
||||
if isNil {
|
||||
return 0, fmt.Errorf("len of nil pointer")
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
|
||||
return v.Len(), nil
|
||||
}
|
||||
return 0, fmt.Errorf("len of type %s", v.Type())
|
||||
}
|
||||
|
||||
// Function invocation
|
||||
|
||||
// call returns the result of evaluating the first argument as a function.
|
||||
// The function must return 1 result, or 2 results, the second of which is an error.
|
||||
func call(fn interface{}, args ...interface{}) (interface{}, error) {
|
||||
v := reflect.ValueOf(fn)
|
||||
typ := v.Type()
|
||||
if typ.Kind() != reflect.Func {
|
||||
return nil, fmt.Errorf("non-function of type %s", typ)
|
||||
}
|
||||
if !goodFunc(typ) {
|
||||
return nil, fmt.Errorf("function called with %d args; should be 1 or 2", typ.NumOut())
|
||||
}
|
||||
numIn := typ.NumIn()
|
||||
var dddType reflect.Type
|
||||
if typ.IsVariadic() {
|
||||
if len(args) < numIn-1 {
|
||||
return nil, fmt.Errorf("wrong number of args: got %d want at least %d", len(args), numIn-1)
|
||||
}
|
||||
dddType = typ.In(numIn - 1).Elem()
|
||||
} else {
|
||||
if len(args) != numIn {
|
||||
return nil, fmt.Errorf("wrong number of args: got %d want %d", len(args), numIn)
|
||||
}
|
||||
}
|
||||
argv := make([]reflect.Value, len(args))
|
||||
for i, arg := range args {
|
||||
value := reflect.ValueOf(arg)
|
||||
// Compute the expected type. Clumsy because of variadics.
|
||||
var argType reflect.Type
|
||||
if !typ.IsVariadic() || i < numIn-1 {
|
||||
argType = typ.In(i)
|
||||
} else {
|
||||
argType = dddType
|
||||
}
|
||||
if !value.IsValid() && canBeNil(argType) {
|
||||
value = reflect.Zero(argType)
|
||||
}
|
||||
if !value.Type().AssignableTo(argType) {
|
||||
return nil, fmt.Errorf("arg %d has type %s; should be %s", i, value.Type(), argType)
|
||||
}
|
||||
argv[i] = value
|
||||
}
|
||||
result := v.Call(argv)
|
||||
if len(result) == 2 && !result[1].IsNil() {
|
||||
return result[0].Interface(), result[1].Interface().(error)
|
||||
}
|
||||
return result[0].Interface(), nil
|
||||
}
|
||||
|
||||
// Boolean logic.
|
||||
|
||||
func truth(a interface{}) bool {
|
||||
t, _ := isTrue(reflect.ValueOf(a))
|
||||
return t
|
||||
}
|
||||
|
||||
// and computes the Boolean AND of its arguments, returning
|
||||
// the first false argument it encounters, or the last argument.
|
||||
func and(arg0 interface{}, args ...interface{}) interface{} {
|
||||
if !truth(arg0) {
|
||||
return arg0
|
||||
}
|
||||
for i := range args {
|
||||
arg0 = args[i]
|
||||
if !truth(arg0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return arg0
|
||||
}
|
||||
|
||||
// or computes the Boolean OR of its arguments, returning
|
||||
// the first true argument it encounters, or the last argument.
|
||||
func or(arg0 interface{}, args ...interface{}) interface{} {
|
||||
if truth(arg0) {
|
||||
return arg0
|
||||
}
|
||||
for i := range args {
|
||||
arg0 = args[i]
|
||||
if truth(arg0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return arg0
|
||||
}
|
||||
|
||||
// not returns the Boolean negation of its argument.
|
||||
func not(arg interface{}) (truth bool) {
|
||||
truth, _ = isTrue(reflect.ValueOf(arg))
|
||||
return !truth
|
||||
}
|
||||
|
||||
// Comparison.
|
||||
|
||||
// TODO: Perhaps allow comparison between signed and unsigned integers.
|
||||
|
||||
var (
|
||||
errBadComparisonType = errors.New("invalid type for comparison")
|
||||
errBadComparison = errors.New("incompatible types for comparison")
|
||||
errNoComparison = errors.New("missing argument for comparison")
|
||||
)
|
||||
|
||||
type kind int
|
||||
|
||||
const (
|
||||
invalidKind kind = iota
|
||||
boolKind
|
||||
complexKind
|
||||
intKind
|
||||
floatKind
|
||||
integerKind
|
||||
stringKind
|
||||
uintKind
|
||||
)
|
||||
|
||||
func basicKind(v reflect.Value) (kind, error) {
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
return boolKind, nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return intKind, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return uintKind, nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return floatKind, nil
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return complexKind, nil
|
||||
case reflect.String:
|
||||
return stringKind, nil
|
||||
}
|
||||
return invalidKind, errBadComparisonType
|
||||
}
|
||||
|
||||
// eq evaluates the comparison a == b || a == c || ...
|
||||
func eq(arg1 interface{}, arg2 ...interface{}) (bool, error) {
|
||||
v1 := reflect.ValueOf(arg1)
|
||||
k1, err := basicKind(v1)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(arg2) == 0 {
|
||||
return false, errNoComparison
|
||||
}
|
||||
for _, arg := range arg2 {
|
||||
v2 := reflect.ValueOf(arg)
|
||||
k2, err := basicKind(v2)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
truth := false
|
||||
if k1 != k2 {
|
||||
// Special case: Can compare integer values regardless of type's sign.
|
||||
switch {
|
||||
case k1 == intKind && k2 == uintKind:
|
||||
truth = v1.Int() >= 0 && uint64(v1.Int()) == v2.Uint()
|
||||
case k1 == uintKind && k2 == intKind:
|
||||
truth = v2.Int() >= 0 && v1.Uint() == uint64(v2.Int())
|
||||
default:
|
||||
return false, errBadComparison
|
||||
}
|
||||
} else {
|
||||
switch k1 {
|
||||
case boolKind:
|
||||
truth = v1.Bool() == v2.Bool()
|
||||
case complexKind:
|
||||
truth = v1.Complex() == v2.Complex()
|
||||
case floatKind:
|
||||
truth = v1.Float() == v2.Float()
|
||||
case intKind:
|
||||
truth = v1.Int() == v2.Int()
|
||||
case stringKind:
|
||||
truth = v1.String() == v2.String()
|
||||
case uintKind:
|
||||
truth = v1.Uint() == v2.Uint()
|
||||
default:
|
||||
panic("invalid kind")
|
||||
}
|
||||
}
|
||||
if truth {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ne evaluates the comparison a != b.
|
||||
func ne(arg1, arg2 interface{}) (bool, error) {
|
||||
// != is the inverse of ==.
|
||||
equal, err := eq(arg1, arg2)
|
||||
return !equal, err
|
||||
}
|
||||
|
||||
// lt evaluates the comparison a < b.
|
||||
func lt(arg1, arg2 interface{}) (bool, error) {
|
||||
v1 := reflect.ValueOf(arg1)
|
||||
k1, err := basicKind(v1)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
v2 := reflect.ValueOf(arg2)
|
||||
k2, err := basicKind(v2)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
truth := false
|
||||
if k1 != k2 {
|
||||
// Special case: Can compare integer values regardless of type's sign.
|
||||
switch {
|
||||
case k1 == intKind && k2 == uintKind:
|
||||
truth = v1.Int() < 0 || uint64(v1.Int()) < v2.Uint()
|
||||
case k1 == uintKind && k2 == intKind:
|
||||
truth = v2.Int() >= 0 && v1.Uint() < uint64(v2.Int())
|
||||
default:
|
||||
return false, errBadComparison
|
||||
}
|
||||
} else {
|
||||
switch k1 {
|
||||
case boolKind, complexKind:
|
||||
return false, errBadComparisonType
|
||||
case floatKind:
|
||||
truth = v1.Float() < v2.Float()
|
||||
case intKind:
|
||||
truth = v1.Int() < v2.Int()
|
||||
case stringKind:
|
||||
truth = v1.String() < v2.String()
|
||||
case uintKind:
|
||||
truth = v1.Uint() < v2.Uint()
|
||||
default:
|
||||
panic("invalid kind")
|
||||
}
|
||||
}
|
||||
return truth, nil
|
||||
}
|
||||
|
||||
// le evaluates the comparison <= b.
|
||||
func le(arg1, arg2 interface{}) (bool, error) {
|
||||
// <= is < or ==.
|
||||
lessThan, err := lt(arg1, arg2)
|
||||
if lessThan || err != nil {
|
||||
return lessThan, err
|
||||
}
|
||||
return eq(arg1, arg2)
|
||||
}
|
||||
|
||||
// gt evaluates the comparison a > b.
|
||||
func gt(arg1, arg2 interface{}) (bool, error) {
|
||||
// > is the inverse of <=.
|
||||
lessOrEqual, err := le(arg1, arg2)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return !lessOrEqual, nil
|
||||
}
|
||||
|
||||
// ge evaluates the comparison a >= b.
|
||||
func ge(arg1, arg2 interface{}) (bool, error) {
|
||||
// >= is the inverse of <.
|
||||
lessThan, err := lt(arg1, arg2)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return !lessThan, nil
|
||||
}
|
||||
|
||||
// HTML escaping.
|
||||
|
||||
var (
|
||||
htmlQuot = []byte(""") // shorter than """
|
||||
htmlApos = []byte("'") // shorter than "'" and apos was not in HTML until HTML5
|
||||
htmlAmp = []byte("&")
|
||||
htmlLt = []byte("<")
|
||||
htmlGt = []byte(">")
|
||||
)
|
||||
|
||||
// HTMLEscape writes to w the escaped HTML equivalent of the plain text data b.
|
||||
func HTMLEscape(w io.Writer, b []byte) {
|
||||
last := 0
|
||||
for i, c := range b {
|
||||
var html []byte
|
||||
switch c {
|
||||
case '"':
|
||||
html = htmlQuot
|
||||
case '\'':
|
||||
html = htmlApos
|
||||
case '&':
|
||||
html = htmlAmp
|
||||
case '<':
|
||||
html = htmlLt
|
||||
case '>':
|
||||
html = htmlGt
|
||||
default:
|
||||
continue
|
||||
}
|
||||
w.Write(b[last:i])
|
||||
w.Write(html)
|
||||
last = i + 1
|
||||
}
|
||||
w.Write(b[last:])
|
||||
}
|
||||
|
||||
// HTMLEscapeString returns the escaped HTML equivalent of the plain text data s.
|
||||
func HTMLEscapeString(s string) string {
|
||||
// Avoid allocation if we can.
|
||||
if strings.IndexAny(s, `'"&<>`) < 0 {
|
||||
return s
|
||||
}
|
||||
var b bytes.Buffer
|
||||
HTMLEscape(&b, []byte(s))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// HTMLEscaper returns the escaped HTML equivalent of the textual
|
||||
// representation of its arguments.
|
||||
func HTMLEscaper(args ...interface{}) string {
|
||||
return HTMLEscapeString(evalArgs(args))
|
||||
}
|
||||
|
||||
// JavaScript escaping.
|
||||
|
||||
var (
|
||||
jsLowUni = []byte(`\u00`)
|
||||
hex = []byte("0123456789ABCDEF")
|
||||
|
||||
jsBackslash = []byte(`\\`)
|
||||
jsApos = []byte(`\'`)
|
||||
jsQuot = []byte(`\"`)
|
||||
jsLt = []byte(`\x3C`)
|
||||
jsGt = []byte(`\x3E`)
|
||||
)
|
||||
|
||||
// JSEscape writes to w the escaped JavaScript equivalent of the plain text data b.
|
||||
func JSEscape(w io.Writer, b []byte) {
|
||||
last := 0
|
||||
for i := 0; i < len(b); i++ {
|
||||
c := b[i]
|
||||
|
||||
if !jsIsSpecial(rune(c)) {
|
||||
// fast path: nothing to do
|
||||
continue
|
||||
}
|
||||
w.Write(b[last:i])
|
||||
|
||||
if c < utf8.RuneSelf {
|
||||
// Quotes, slashes and angle brackets get quoted.
|
||||
// Control characters get written as \u00XX.
|
||||
switch c {
|
||||
case '\\':
|
||||
w.Write(jsBackslash)
|
||||
case '\'':
|
||||
w.Write(jsApos)
|
||||
case '"':
|
||||
w.Write(jsQuot)
|
||||
case '<':
|
||||
w.Write(jsLt)
|
||||
case '>':
|
||||
w.Write(jsGt)
|
||||
default:
|
||||
w.Write(jsLowUni)
|
||||
t, b := c>>4, c&0x0f
|
||||
w.Write(hex[t : t+1])
|
||||
w.Write(hex[b : b+1])
|
||||
}
|
||||
} else {
|
||||
// Unicode rune.
|
||||
r, size := utf8.DecodeRune(b[i:])
|
||||
if unicode.IsPrint(r) {
|
||||
w.Write(b[i : i+size])
|
||||
} else {
|
||||
fmt.Fprintf(w, "\\u%04X", r)
|
||||
}
|
||||
i += size - 1
|
||||
}
|
||||
last = i + 1
|
||||
}
|
||||
w.Write(b[last:])
|
||||
}
|
||||
|
||||
// JSEscapeString returns the escaped JavaScript equivalent of the plain text data s.
|
||||
func JSEscapeString(s string) string {
|
||||
// Avoid allocation if we can.
|
||||
if strings.IndexFunc(s, jsIsSpecial) < 0 {
|
||||
return s
|
||||
}
|
||||
var b bytes.Buffer
|
||||
JSEscape(&b, []byte(s))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func jsIsSpecial(r rune) bool {
|
||||
switch r {
|
||||
case '\\', '\'', '"', '<', '>':
|
||||
return true
|
||||
}
|
||||
return r < ' ' || utf8.RuneSelf <= r
|
||||
}
|
||||
|
||||
// JSEscaper returns the escaped JavaScript equivalent of the textual
|
||||
// representation of its arguments.
|
||||
func JSEscaper(args ...interface{}) string {
|
||||
return JSEscapeString(evalArgs(args))
|
||||
}
|
||||
|
||||
// URLQueryEscaper returns the escaped value of the textual representation of
|
||||
// its arguments in a form suitable for embedding in a URL query.
|
||||
func URLQueryEscaper(args ...interface{}) string {
|
||||
return url.QueryEscape(evalArgs(args))
|
||||
}
|
||||
|
||||
// evalArgs formats the list of arguments into a string. It is therefore equivalent to
|
||||
// fmt.Sprint(args...)
|
||||
// except that each argument is indirected (if a pointer), as required,
|
||||
// using the same rules as the default string evaluation during template
|
||||
// execution.
|
||||
func evalArgs(args []interface{}) string {
|
||||
ok := false
|
||||
var s string
|
||||
// Fast path for simple common case.
|
||||
if len(args) == 1 {
|
||||
s, ok = args[0].(string)
|
||||
}
|
||||
if !ok {
|
||||
for i, arg := range args {
|
||||
a, ok := printableValue(reflect.ValueOf(arg))
|
||||
if ok {
|
||||
args[i] = a
|
||||
} // else left fmt do its thing
|
||||
}
|
||||
s = fmt.Sprint(args...)
|
||||
}
|
||||
return s
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Helper functions to make constructing templates easier.
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Functions and methods to parse templates.
|
||||
|
||||
// Must is a helper that wraps a call to a function returning (*Template, error)
|
||||
// and panics if the error is non-nil. It is intended for use in variable
|
||||
// initializations such as
|
||||
// var t = template.Must(template.New("name").Parse("text"))
|
||||
func Must(t *Template, err error) *Template {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// ParseFiles creates a new Template and parses the template definitions from
|
||||
// the named files. The returned template's name will have the (base) name and
|
||||
// (parsed) contents of the first file. There must be at least one file.
|
||||
// If an error occurs, parsing stops and the returned *Template is nil.
|
||||
func ParseFiles(filenames ...string) (*Template, error) {
|
||||
return parseFiles(nil, filenames...)
|
||||
}
|
||||
|
||||
// ParseFiles parses the named files and associates the resulting templates with
|
||||
// t. If an error occurs, parsing stops and the returned template is nil;
|
||||
// otherwise it is t. There must be at least one file.
|
||||
func (t *Template) ParseFiles(filenames ...string) (*Template, error) {
|
||||
return parseFiles(t, filenames...)
|
||||
}
|
||||
|
||||
// parseFiles is the helper for the method and function. If the argument
|
||||
// template is nil, it is created from the first file.
|
||||
func parseFiles(t *Template, filenames ...string) (*Template, error) {
|
||||
if len(filenames) == 0 {
|
||||
// Not really a problem, but be consistent.
|
||||
return nil, fmt.Errorf("template: no files named in call to ParseFiles")
|
||||
}
|
||||
for _, filename := range filenames {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(b)
|
||||
name := filepath.Base(filename)
|
||||
// First template becomes return value if not already defined,
|
||||
// and we use that one for subsequent New calls to associate
|
||||
// all the templates together. Also, if this file has the same name
|
||||
// as t, this file becomes the contents of t, so
|
||||
// t, err := New(name).Funcs(xxx).ParseFiles(name)
|
||||
// works. Otherwise we create a new template associated with t.
|
||||
var tmpl *Template
|
||||
if t == nil {
|
||||
t = New(name)
|
||||
}
|
||||
if name == t.Name() {
|
||||
tmpl = t
|
||||
} else {
|
||||
tmpl = t.New(name)
|
||||
}
|
||||
_, err = tmpl.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ParseGlob creates a new Template and parses the template definitions from the
|
||||
// files identified by the pattern, which must match at least one file. The
|
||||
// returned template will have the (base) name and (parsed) contents of the
|
||||
// first file matched by the pattern. ParseGlob is equivalent to calling
|
||||
// ParseFiles with the list of files matched by the pattern.
|
||||
func ParseGlob(pattern string) (*Template, error) {
|
||||
return parseGlob(nil, pattern)
|
||||
}
|
||||
|
||||
// ParseGlob parses the template definitions in the files identified by the
|
||||
// pattern and associates the resulting templates with t. The pattern is
|
||||
// processed by filepath.Glob and must match at least one file. ParseGlob is
|
||||
// equivalent to calling t.ParseFiles with the list of files matched by the
|
||||
// pattern.
|
||||
func (t *Template) ParseGlob(pattern string) (*Template, error) {
|
||||
return parseGlob(t, pattern)
|
||||
}
|
||||
|
||||
// parseGlob is the implementation of the function and method ParseGlob.
|
||||
func parseGlob(t *Template, pattern string) (*Template, error) {
|
||||
filenames, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(filenames) == 0 {
|
||||
return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern)
|
||||
}
|
||||
return parseFiles(t, filenames...)
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package template
|
||||
|
||||
// Tests for mulitple-template parsing and execution.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alecthomas/template/parse"
|
||||
)
|
||||
|
||||
const (
|
||||
noError = true
|
||||
hasError = false
|
||||
)
|
||||
|
||||
type multiParseTest struct {
|
||||
name string
|
||||
input string
|
||||
ok bool
|
||||
names []string
|
||||
results []string
|
||||
}
|
||||
|
||||
var multiParseTests = []multiParseTest{
|
||||
{"empty", "", noError,
|
||||
nil,
|
||||
nil},
|
||||
{"one", `{{define "foo"}} FOO {{end}}`, noError,
|
||||
[]string{"foo"},
|
||||
[]string{" FOO "}},
|
||||
{"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
|
||||
[]string{"foo", "bar"},
|
||||
[]string{" FOO ", " BAR "}},
|
||||
// errors
|
||||
{"missing end", `{{define "foo"}} FOO `, hasError,
|
||||
nil,
|
||||
nil},
|
||||
{"malformed name", `{{define "foo}} FOO `, hasError,
|
||||
nil,
|
||||
nil},
|
||||
}
|
||||
|
||||
func TestMultiParse(t *testing.T) {
|
||||
for _, test := range multiParseTests {
|
||||
template, err := New("root").Parse(test.input)
|
||||
switch {
|
||||
case err == nil && !test.ok:
|
||||
t.Errorf("%q: expected error; got none", test.name)
|
||||
continue
|
||||
case err != nil && test.ok:
|
||||
t.Errorf("%q: unexpected error: %v", test.name, err)
|
||||
continue
|
||||
case err != nil && !test.ok:
|
||||
// expected error, got one
|
||||
if *debug {
|
||||
fmt.Printf("%s: %s\n\t%s\n", test.name, test.input, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if template == nil {
|
||||
continue
|
||||
}
|
||||
if len(template.tmpl) != len(test.names)+1 { // +1 for root
|
||||
t.Errorf("%s: wrong number of templates; wanted %d got %d", test.name, len(test.names), len(template.tmpl))
|
||||
continue
|
||||
}
|
||||
for i, name := range test.names {
|
||||
tmpl, ok := template.tmpl[name]
|
||||
if !ok {
|
||||
t.Errorf("%s: can't find template %q", test.name, name)
|
||||
continue
|
||||
}
|
||||
result := tmpl.Root.String()
|
||||
if result != test.results[i] {
|
||||
t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, result, test.results[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var multiExecTests = []execTest{
|
||||
{"empty", "", "", nil, true},
|
||||
{"text", "some text", "some text", nil, true},
|
||||
{"invoke x", `{{template "x" .SI}}`, "TEXT", tVal, true},
|
||||
{"invoke x no args", `{{template "x"}}`, "TEXT", tVal, true},
|
||||
{"invoke dot int", `{{template "dot" .I}}`, "17", tVal, true},
|
||||
{"invoke dot []int", `{{template "dot" .SI}}`, "[3 4 5]", tVal, true},
|
||||
{"invoke dotV", `{{template "dotV" .U}}`, "v", tVal, true},
|
||||
{"invoke nested int", `{{template "nested" .I}}`, "17", tVal, true},
|
||||
{"variable declared by template", `{{template "nested" $x:=.SI}},{{index $x 1}}`, "[3 4 5],4", tVal, true},
|
||||
|
||||
// User-defined function: test argument evaluator.
|
||||
{"testFunc literal", `{{oneArg "joe"}}`, "oneArg=joe", tVal, true},
|
||||
{"testFunc .", `{{oneArg .}}`, "oneArg=joe", "joe", true},
|
||||
}
|
||||
|
||||
// These strings are also in testdata/*.
|
||||
const multiText1 = `
|
||||
{{define "x"}}TEXT{{end}}
|
||||
{{define "dotV"}}{{.V}}{{end}}
|
||||
`
|
||||
|
||||
const multiText2 = `
|
||||
{{define "dot"}}{{.}}{{end}}
|
||||
{{define "nested"}}{{template "dot" .}}{{end}}
|
||||
`
|
||||
|
||||
func TestMultiExecute(t *testing.T) {
|
||||
// Declare a couple of templates first.
|
||||
template, err := New("root").Parse(multiText1)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error for 1: %s", err)
|
||||
}
|
||||
_, err = template.Parse(multiText2)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error for 2: %s", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseFiles(t *testing.T) {
|
||||
_, err := ParseFiles("DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
template := New("root")
|
||||
_, err = template.ParseFiles("testdata/file1.tmpl", "testdata/file2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseGlob(t *testing.T) {
|
||||
_, err := ParseGlob("DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
_, err = New("error").ParseGlob("[x")
|
||||
if err == nil {
|
||||
t.Error("expected error for bad pattern; got none")
|
||||
}
|
||||
template := New("root")
|
||||
_, err = template.ParseGlob("testdata/file*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
// In these tests, actual content (not just template definitions) comes from the parsed files.
|
||||
|
||||
var templateFileExecTests = []execTest{
|
||||
{"test", `{{template "tmpl1.tmpl"}}{{template "tmpl2.tmpl"}}`, "template1\n\ny\ntemplate2\n\nx\n", 0, true},
|
||||
}
|
||||
|
||||
func TestParseFilesWithData(t *testing.T) {
|
||||
template, err := New("root").ParseFiles("testdata/tmpl1.tmpl", "testdata/tmpl2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(templateFileExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseGlobWithData(t *testing.T) {
|
||||
template, err := New("root").ParseGlob("testdata/tmpl*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(templateFileExecTests, template, t)
|
||||
}
|
||||
|
||||
const (
|
||||
cloneText1 = `{{define "a"}}{{template "b"}}{{template "c"}}{{end}}`
|
||||
cloneText2 = `{{define "b"}}b{{end}}`
|
||||
cloneText3 = `{{define "c"}}root{{end}}`
|
||||
cloneText4 = `{{define "c"}}clone{{end}}`
|
||||
)
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
// Create some templates and clone the root.
|
||||
root, err := New("root").Parse(cloneText1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = root.Parse(cloneText2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clone := Must(root.Clone())
|
||||
// Add variants to both.
|
||||
_, err = root.Parse(cloneText3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = clone.Parse(cloneText4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Verify that the clone is self-consistent.
|
||||
for k, v := range clone.tmpl {
|
||||
if k == clone.name && v.tmpl[k] != clone {
|
||||
t.Error("clone does not contain root")
|
||||
}
|
||||
if v != v.tmpl[v.name] {
|
||||
t.Errorf("clone does not contain self for %q", k)
|
||||
}
|
||||
}
|
||||
// Execute root.
|
||||
var b bytes.Buffer
|
||||
err = root.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "broot" {
|
||||
t.Errorf("expected %q got %q", "broot", b.String())
|
||||
}
|
||||
// Execute copy.
|
||||
b.Reset()
|
||||
err = clone.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "bclone" {
|
||||
t.Errorf("expected %q got %q", "bclone", b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddParseTree(t *testing.T) {
|
||||
// Create some templates.
|
||||
root, err := New("root").Parse(cloneText1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = root.Parse(cloneText2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Add a new parse tree.
|
||||
tree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
added, err := root.AddParseTree("c", tree["c"])
|
||||
// Execute.
|
||||
var b bytes.Buffer
|
||||
err = added.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "broot" {
|
||||
t.Errorf("expected %q got %q", "broot", b.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 7032
|
||||
func TestAddParseTreeToUnparsedTemplate(t *testing.T) {
|
||||
master := "{{define \"master\"}}{{end}}"
|
||||
tmpl := New("master")
|
||||
tree, err := parse.Parse("master", master, "", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parse err: %v", err)
|
||||
}
|
||||
masterTree := tree["master"]
|
||||
tmpl.AddParseTree("master", masterTree) // used to panic
|
||||
}
|
||||
|
||||
func TestRedefinition(t *testing.T) {
|
||||
var tmpl *Template
|
||||
var err error
|
||||
if tmpl, err = New("tmpl1").Parse(`{{define "test"}}foo{{end}}`); err != nil {
|
||||
t.Fatalf("parse 1: %v", err)
|
||||
}
|
||||
if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "redefinition") {
|
||||
t.Fatalf("expected redefinition error; got %v", err)
|
||||
}
|
||||
if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "redefinition") {
|
||||
t.Fatalf("expected redefinition error; got %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,556 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// item represents a token or text string returned from the scanner.
|
||||
type item struct {
|
||||
typ itemType // The type of this item.
|
||||
pos Pos // The starting position, in bytes, of this item in the input string.
|
||||
val string // The value of this item.
|
||||
}
|
||||
|
||||
func (i item) String() string {
|
||||
switch {
|
||||
case i.typ == itemEOF:
|
||||
return "EOF"
|
||||
case i.typ == itemError:
|
||||
return i.val
|
||||
case i.typ > itemKeyword:
|
||||
return fmt.Sprintf("<%s>", i.val)
|
||||
case len(i.val) > 10:
|
||||
return fmt.Sprintf("%.10q...", i.val)
|
||||
}
|
||||
return fmt.Sprintf("%q", i.val)
|
||||
}
|
||||
|
||||
// itemType identifies the type of lex items.
|
||||
type itemType int
|
||||
|
||||
const (
|
||||
itemError itemType = iota // error occurred; value is text of error
|
||||
itemBool // boolean constant
|
||||
itemChar // printable ASCII character; grab bag for comma etc.
|
||||
itemCharConstant // character constant
|
||||
itemComplex // complex constant (1+2i); imaginary is just a number
|
||||
itemColonEquals // colon-equals (':=') introducing a declaration
|
||||
itemEOF
|
||||
itemField // alphanumeric identifier starting with '.'
|
||||
itemIdentifier // alphanumeric identifier not starting with '.'
|
||||
itemLeftDelim // left action delimiter
|
||||
itemLeftParen // '(' inside action
|
||||
itemNumber // simple number, including imaginary
|
||||
itemPipe // pipe symbol
|
||||
itemRawString // raw quoted string (includes quotes)
|
||||
itemRightDelim // right action delimiter
|
||||
itemElideNewline // elide newline after right delim
|
||||
itemRightParen // ')' inside action
|
||||
itemSpace // run of spaces separating arguments
|
||||
itemString // quoted string (includes quotes)
|
||||
itemText // plain text
|
||||
itemVariable // variable starting with '$', such as '$' or '$1' or '$hello'
|
||||
// Keywords appear after all the rest.
|
||||
itemKeyword // used only to delimit the keywords
|
||||
itemDot // the cursor, spelled '.'
|
||||
itemDefine // define keyword
|
||||
itemElse // else keyword
|
||||
itemEnd // end keyword
|
||||
itemIf // if keyword
|
||||
itemNil // the untyped nil constant, easiest to treat as a keyword
|
||||
itemRange // range keyword
|
||||
itemTemplate // template keyword
|
||||
itemWith // with keyword
|
||||
)
|
||||
|
||||
var key = map[string]itemType{
|
||||
".": itemDot,
|
||||
"define": itemDefine,
|
||||
"else": itemElse,
|
||||
"end": itemEnd,
|
||||
"if": itemIf,
|
||||
"range": itemRange,
|
||||
"nil": itemNil,
|
||||
"template": itemTemplate,
|
||||
"with": itemWith,
|
||||
}
|
||||
|
||||
const eof = -1
|
||||
|
||||
// stateFn represents the state of the scanner as a function that returns the next state.
|
||||
type stateFn func(*lexer) stateFn
|
||||
|
||||
// lexer holds the state of the scanner.
|
||||
type lexer struct {
|
||||
name string // the name of the input; used only for error reports
|
||||
input string // the string being scanned
|
||||
leftDelim string // start of action
|
||||
rightDelim string // end of action
|
||||
state stateFn // the next lexing function to enter
|
||||
pos Pos // current position in the input
|
||||
start Pos // start position of this item
|
||||
width Pos // width of last rune read from input
|
||||
lastPos Pos // position of most recent item returned by nextItem
|
||||
items chan item // channel of scanned items
|
||||
parenDepth int // nesting depth of ( ) exprs
|
||||
}
|
||||
|
||||
// next returns the next rune in the input.
|
||||
func (l *lexer) next() rune {
|
||||
if int(l.pos) >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.width = Pos(w)
|
||||
l.pos += l.width
|
||||
return r
|
||||
}
|
||||
|
||||
// peek returns but does not consume the next rune in the input.
|
||||
func (l *lexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
// backup steps back one rune. Can only be called once per call of next.
|
||||
func (l *lexer) backup() {
|
||||
l.pos -= l.width
|
||||
}
|
||||
|
||||
// emit passes an item back to the client.
|
||||
func (l *lexer) emit(t itemType) {
|
||||
l.items <- item{t, l.start, l.input[l.start:l.pos]}
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
// ignore skips over the pending input before this point.
|
||||
func (l *lexer) ignore() {
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
// accept consumes the next rune if it's from the valid set.
|
||||
func (l *lexer) accept(valid string) bool {
|
||||
if strings.IndexRune(valid, l.next()) >= 0 {
|
||||
return true
|
||||
}
|
||||
l.backup()
|
||||
return false
|
||||
}
|
||||
|
||||
// acceptRun consumes a run of runes from the valid set.
|
||||
func (l *lexer) acceptRun(valid string) {
|
||||
for strings.IndexRune(valid, l.next()) >= 0 {
|
||||
}
|
||||
l.backup()
|
||||
}
|
||||
|
||||
// lineNumber reports which line we're on, based on the position of
|
||||
// the previous item returned by nextItem. Doing it this way
|
||||
// means we don't have to worry about peek double counting.
|
||||
func (l *lexer) lineNumber() int {
|
||||
return 1 + strings.Count(l.input[:l.lastPos], "\n")
|
||||
}
|
||||
|
||||
// errorf returns an error token and terminates the scan by passing
|
||||
// back a nil pointer that will be the next state, terminating l.nextItem.
|
||||
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
|
||||
l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextItem returns the next item from the input.
|
||||
func (l *lexer) nextItem() item {
|
||||
item := <-l.items
|
||||
l.lastPos = item.pos
|
||||
return item
|
||||
}
|
||||
|
||||
// lex creates a new scanner for the input string.
|
||||
func lex(name, input, left, right string) *lexer {
|
||||
if left == "" {
|
||||
left = leftDelim
|
||||
}
|
||||
if right == "" {
|
||||
right = rightDelim
|
||||
}
|
||||
l := &lexer{
|
||||
name: name,
|
||||
input: input,
|
||||
leftDelim: left,
|
||||
rightDelim: right,
|
||||
items: make(chan item),
|
||||
}
|
||||
go l.run()
|
||||
return l
|
||||
}
|
||||
|
||||
// run runs the state machine for the lexer.
|
||||
func (l *lexer) run() {
|
||||
for l.state = lexText; l.state != nil; {
|
||||
l.state = l.state(l)
|
||||
}
|
||||
}
|
||||
|
||||
// state functions
|
||||
|
||||
const (
|
||||
leftDelim = "{{"
|
||||
rightDelim = "}}"
|
||||
leftComment = "/*"
|
||||
rightComment = "*/"
|
||||
)
|
||||
|
||||
// lexText scans until an opening action delimiter, "{{".
|
||||
func lexText(l *lexer) stateFn {
|
||||
for {
|
||||
if strings.HasPrefix(l.input[l.pos:], l.leftDelim) {
|
||||
if l.pos > l.start {
|
||||
l.emit(itemText)
|
||||
}
|
||||
return lexLeftDelim
|
||||
}
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Correctly reached EOF.
|
||||
if l.pos > l.start {
|
||||
l.emit(itemText)
|
||||
}
|
||||
l.emit(itemEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
// lexLeftDelim scans the left delimiter, which is known to be present.
|
||||
func lexLeftDelim(l *lexer) stateFn {
|
||||
l.pos += Pos(len(l.leftDelim))
|
||||
if strings.HasPrefix(l.input[l.pos:], leftComment) {
|
||||
return lexComment
|
||||
}
|
||||
l.emit(itemLeftDelim)
|
||||
l.parenDepth = 0
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexComment scans a comment. The left comment marker is known to be present.
|
||||
func lexComment(l *lexer) stateFn {
|
||||
l.pos += Pos(len(leftComment))
|
||||
i := strings.Index(l.input[l.pos:], rightComment)
|
||||
if i < 0 {
|
||||
return l.errorf("unclosed comment")
|
||||
}
|
||||
l.pos += Pos(i + len(rightComment))
|
||||
if !strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
|
||||
return l.errorf("comment ends before closing delimiter")
|
||||
|
||||
}
|
||||
l.pos += Pos(len(l.rightDelim))
|
||||
l.ignore()
|
||||
return lexText
|
||||
}
|
||||
|
||||
// lexRightDelim scans the right delimiter, which is known to be present.
|
||||
func lexRightDelim(l *lexer) stateFn {
|
||||
l.pos += Pos(len(l.rightDelim))
|
||||
l.emit(itemRightDelim)
|
||||
if l.peek() == '\\' {
|
||||
l.pos++
|
||||
l.emit(itemElideNewline)
|
||||
}
|
||||
return lexText
|
||||
}
|
||||
|
||||
// lexInsideAction scans the elements inside action delimiters.
|
||||
func lexInsideAction(l *lexer) stateFn {
|
||||
// Either number, quoted string, or identifier.
|
||||
// Spaces separate arguments; runs of spaces turn into itemSpace.
|
||||
// Pipe symbols separate and are emitted.
|
||||
if strings.HasPrefix(l.input[l.pos:], l.rightDelim+"\\") || strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
|
||||
if l.parenDepth == 0 {
|
||||
return lexRightDelim
|
||||
}
|
||||
return l.errorf("unclosed left paren")
|
||||
}
|
||||
switch r := l.next(); {
|
||||
case r == eof || isEndOfLine(r):
|
||||
return l.errorf("unclosed action")
|
||||
case isSpace(r):
|
||||
return lexSpace
|
||||
case r == ':':
|
||||
if l.next() != '=' {
|
||||
return l.errorf("expected :=")
|
||||
}
|
||||
l.emit(itemColonEquals)
|
||||
case r == '|':
|
||||
l.emit(itemPipe)
|
||||
case r == '"':
|
||||
return lexQuote
|
||||
case r == '`':
|
||||
return lexRawQuote
|
||||
case r == '$':
|
||||
return lexVariable
|
||||
case r == '\'':
|
||||
return lexChar
|
||||
case r == '.':
|
||||
// special look-ahead for ".field" so we don't break l.backup().
|
||||
if l.pos < Pos(len(l.input)) {
|
||||
r := l.input[l.pos]
|
||||
if r < '0' || '9' < r {
|
||||
return lexField
|
||||
}
|
||||
}
|
||||
fallthrough // '.' can start a number.
|
||||
case r == '+' || r == '-' || ('0' <= r && r <= '9'):
|
||||
l.backup()
|
||||
return lexNumber
|
||||
case isAlphaNumeric(r):
|
||||
l.backup()
|
||||
return lexIdentifier
|
||||
case r == '(':
|
||||
l.emit(itemLeftParen)
|
||||
l.parenDepth++
|
||||
return lexInsideAction
|
||||
case r == ')':
|
||||
l.emit(itemRightParen)
|
||||
l.parenDepth--
|
||||
if l.parenDepth < 0 {
|
||||
return l.errorf("unexpected right paren %#U", r)
|
||||
}
|
||||
return lexInsideAction
|
||||
case r <= unicode.MaxASCII && unicode.IsPrint(r):
|
||||
l.emit(itemChar)
|
||||
return lexInsideAction
|
||||
default:
|
||||
return l.errorf("unrecognized character in action: %#U", r)
|
||||
}
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexSpace scans a run of space characters.
|
||||
// One space has already been seen.
|
||||
func lexSpace(l *lexer) stateFn {
|
||||
for isSpace(l.peek()) {
|
||||
l.next()
|
||||
}
|
||||
l.emit(itemSpace)
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexIdentifier scans an alphanumeric.
|
||||
func lexIdentifier(l *lexer) stateFn {
|
||||
Loop:
|
||||
for {
|
||||
switch r := l.next(); {
|
||||
case isAlphaNumeric(r):
|
||||
// absorb.
|
||||
default:
|
||||
l.backup()
|
||||
word := l.input[l.start:l.pos]
|
||||
if !l.atTerminator() {
|
||||
return l.errorf("bad character %#U", r)
|
||||
}
|
||||
switch {
|
||||
case key[word] > itemKeyword:
|
||||
l.emit(key[word])
|
||||
case word[0] == '.':
|
||||
l.emit(itemField)
|
||||
case word == "true", word == "false":
|
||||
l.emit(itemBool)
|
||||
default:
|
||||
l.emit(itemIdentifier)
|
||||
}
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexField scans a field: .Alphanumeric.
|
||||
// The . has been scanned.
|
||||
func lexField(l *lexer) stateFn {
|
||||
return lexFieldOrVariable(l, itemField)
|
||||
}
|
||||
|
||||
// lexVariable scans a Variable: $Alphanumeric.
|
||||
// The $ has been scanned.
|
||||
func lexVariable(l *lexer) stateFn {
|
||||
if l.atTerminator() { // Nothing interesting follows -> "$".
|
||||
l.emit(itemVariable)
|
||||
return lexInsideAction
|
||||
}
|
||||
return lexFieldOrVariable(l, itemVariable)
|
||||
}
|
||||
|
||||
// lexVariable scans a field or variable: [.$]Alphanumeric.
|
||||
// The . or $ has been scanned.
|
||||
func lexFieldOrVariable(l *lexer, typ itemType) stateFn {
|
||||
if l.atTerminator() { // Nothing interesting follows -> "." or "$".
|
||||
if typ == itemVariable {
|
||||
l.emit(itemVariable)
|
||||
} else {
|
||||
l.emit(itemDot)
|
||||
}
|
||||
return lexInsideAction
|
||||
}
|
||||
var r rune
|
||||
for {
|
||||
r = l.next()
|
||||
if !isAlphaNumeric(r) {
|
||||
l.backup()
|
||||
break
|
||||
}
|
||||
}
|
||||
if !l.atTerminator() {
|
||||
return l.errorf("bad character %#U", r)
|
||||
}
|
||||
l.emit(typ)
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// atTerminator reports whether the input is at valid termination character to
|
||||
// appear after an identifier. Breaks .X.Y into two pieces. Also catches cases
|
||||
// like "$x+2" not being acceptable without a space, in case we decide one
|
||||
// day to implement arithmetic.
|
||||
func (l *lexer) atTerminator() bool {
|
||||
r := l.peek()
|
||||
if isSpace(r) || isEndOfLine(r) {
|
||||
return true
|
||||
}
|
||||
switch r {
|
||||
case eof, '.', ',', '|', ':', ')', '(':
|
||||
return true
|
||||
}
|
||||
// Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will
|
||||
// succeed but should fail) but only in extremely rare cases caused by willfully
|
||||
// bad choice of delimiter.
|
||||
if rd, _ := utf8.DecodeRuneInString(l.rightDelim); rd == r {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// lexChar scans a character constant. The initial quote is already
|
||||
// scanned. Syntax checking is done by the parser.
|
||||
func lexChar(l *lexer) stateFn {
|
||||
Loop:
|
||||
for {
|
||||
switch l.next() {
|
||||
case '\\':
|
||||
if r := l.next(); r != eof && r != '\n' {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case eof, '\n':
|
||||
return l.errorf("unterminated character constant")
|
||||
case '\'':
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
l.emit(itemCharConstant)
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexNumber scans a number: decimal, octal, hex, float, or imaginary. This
|
||||
// isn't a perfect number scanner - for instance it accepts "." and "0x0.2"
|
||||
// and "089" - but when it's wrong the input is invalid and the parser (via
|
||||
// strconv) will notice.
|
||||
func lexNumber(l *lexer) stateFn {
|
||||
if !l.scanNumber() {
|
||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
||||
}
|
||||
if sign := l.peek(); sign == '+' || sign == '-' {
|
||||
// Complex: 1+2i. No spaces, must end in 'i'.
|
||||
if !l.scanNumber() || l.input[l.pos-1] != 'i' {
|
||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
||||
}
|
||||
l.emit(itemComplex)
|
||||
} else {
|
||||
l.emit(itemNumber)
|
||||
}
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
func (l *lexer) scanNumber() bool {
|
||||
// Optional leading sign.
|
||||
l.accept("+-")
|
||||
// Is it hex?
|
||||
digits := "0123456789"
|
||||
if l.accept("0") && l.accept("xX") {
|
||||
digits = "0123456789abcdefABCDEF"
|
||||
}
|
||||
l.acceptRun(digits)
|
||||
if l.accept(".") {
|
||||
l.acceptRun(digits)
|
||||
}
|
||||
if l.accept("eE") {
|
||||
l.accept("+-")
|
||||
l.acceptRun("0123456789")
|
||||
}
|
||||
// Is it imaginary?
|
||||
l.accept("i")
|
||||
// Next thing mustn't be alphanumeric.
|
||||
if isAlphaNumeric(l.peek()) {
|
||||
l.next()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// lexQuote scans a quoted string.
|
||||
func lexQuote(l *lexer) stateFn {
|
||||
Loop:
|
||||
for {
|
||||
switch l.next() {
|
||||
case '\\':
|
||||
if r := l.next(); r != eof && r != '\n' {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case eof, '\n':
|
||||
return l.errorf("unterminated quoted string")
|
||||
case '"':
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
l.emit(itemString)
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexRawQuote scans a raw quoted string.
|
||||
func lexRawQuote(l *lexer) stateFn {
|
||||
Loop:
|
||||
for {
|
||||
switch l.next() {
|
||||
case eof, '\n':
|
||||
return l.errorf("unterminated raw quoted string")
|
||||
case '`':
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
l.emit(itemRawString)
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// isSpace reports whether r is a space character.
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
}
|
||||
|
||||
// isEndOfLine reports whether r is an end-of-line character.
|
||||
func isEndOfLine(r rune) bool {
|
||||
return r == '\r' || r == '\n'
|
||||
}
|
||||
|
||||
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
|
||||
func isAlphaNumeric(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
}
|
|
@ -0,0 +1,468 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Make the types prettyprint.
|
||||
var itemName = map[itemType]string{
|
||||
itemError: "error",
|
||||
itemBool: "bool",
|
||||
itemChar: "char",
|
||||
itemCharConstant: "charconst",
|
||||
itemComplex: "complex",
|
||||
itemColonEquals: ":=",
|
||||
itemEOF: "EOF",
|
||||
itemField: "field",
|
||||
itemIdentifier: "identifier",
|
||||
itemLeftDelim: "left delim",
|
||||
itemLeftParen: "(",
|
||||
itemNumber: "number",
|
||||
itemPipe: "pipe",
|
||||
itemRawString: "raw string",
|
||||
itemRightDelim: "right delim",
|
||||
itemElideNewline: "elide newline",
|
||||
itemRightParen: ")",
|
||||
itemSpace: "space",
|
||||
itemString: "string",
|
||||
itemVariable: "variable",
|
||||
|
||||
// keywords
|
||||
itemDot: ".",
|
||||
itemDefine: "define",
|
||||
itemElse: "else",
|
||||
itemIf: "if",
|
||||
itemEnd: "end",
|
||||
itemNil: "nil",
|
||||
itemRange: "range",
|
||||
itemTemplate: "template",
|
||||
itemWith: "with",
|
||||
}
|
||||
|
||||
func (i itemType) String() string {
|
||||
s := itemName[i]
|
||||
if s == "" {
|
||||
return fmt.Sprintf("item%d", int(i))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type lexTest struct {
|
||||
name string
|
||||
input string
|
||||
items []item
|
||||
}
|
||||
|
||||
var (
|
||||
tEOF = item{itemEOF, 0, ""}
|
||||
tFor = item{itemIdentifier, 0, "for"}
|
||||
tLeft = item{itemLeftDelim, 0, "{{"}
|
||||
tLpar = item{itemLeftParen, 0, "("}
|
||||
tPipe = item{itemPipe, 0, "|"}
|
||||
tQuote = item{itemString, 0, `"abc \n\t\" "`}
|
||||
tRange = item{itemRange, 0, "range"}
|
||||
tRight = item{itemRightDelim, 0, "}}"}
|
||||
tElideNewline = item{itemElideNewline, 0, "\\"}
|
||||
tRpar = item{itemRightParen, 0, ")"}
|
||||
tSpace = item{itemSpace, 0, " "}
|
||||
raw = "`" + `abc\n\t\" ` + "`"
|
||||
tRawQuote = item{itemRawString, 0, raw}
|
||||
)
|
||||
|
||||
var lexTests = []lexTest{
|
||||
{"empty", "", []item{tEOF}},
|
||||
{"spaces", " \t\n", []item{{itemText, 0, " \t\n"}, tEOF}},
|
||||
{"text", `now is the time`, []item{{itemText, 0, "now is the time"}, tEOF}},
|
||||
{"elide newline", "{{}}\\", []item{tLeft, tRight, tElideNewline, tEOF}},
|
||||
{"text with comment", "hello-{{/* this is a comment */}}-world", []item{
|
||||
{itemText, 0, "hello-"},
|
||||
{itemText, 0, "-world"},
|
||||
tEOF,
|
||||
}},
|
||||
{"punctuation", "{{,@% }}", []item{
|
||||
tLeft,
|
||||
{itemChar, 0, ","},
|
||||
{itemChar, 0, "@"},
|
||||
{itemChar, 0, "%"},
|
||||
tSpace,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"parens", "{{((3))}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
tLpar,
|
||||
{itemNumber, 0, "3"},
|
||||
tRpar,
|
||||
tRpar,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
|
||||
{"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
|
||||
{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
|
||||
{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
|
||||
{"numbers", "{{1 02 0x14 -7.2i 1e3 +1.2e-4 4.2i 1+2i}}", []item{
|
||||
tLeft,
|
||||
{itemNumber, 0, "1"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "02"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "0x14"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "-7.2i"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "1e3"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "+1.2e-4"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "4.2i"},
|
||||
tSpace,
|
||||
{itemComplex, 0, "1+2i"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"characters", `{{'a' '\n' '\'' '\\' '\u00FF' '\xFF' '本'}}`, []item{
|
||||
tLeft,
|
||||
{itemCharConstant, 0, `'a'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\n'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\''`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\\'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\u00FF'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\xFF'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'本'`},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"bools", "{{true false}}", []item{
|
||||
tLeft,
|
||||
{itemBool, 0, "true"},
|
||||
tSpace,
|
||||
{itemBool, 0, "false"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"dot", "{{.}}", []item{
|
||||
tLeft,
|
||||
{itemDot, 0, "."},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"nil", "{{nil}}", []item{
|
||||
tLeft,
|
||||
{itemNil, 0, "nil"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"dots", "{{.x . .2 .x.y.z}}", []item{
|
||||
tLeft,
|
||||
{itemField, 0, ".x"},
|
||||
tSpace,
|
||||
{itemDot, 0, "."},
|
||||
tSpace,
|
||||
{itemNumber, 0, ".2"},
|
||||
tSpace,
|
||||
{itemField, 0, ".x"},
|
||||
{itemField, 0, ".y"},
|
||||
{itemField, 0, ".z"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"keywords", "{{range if else end with}}", []item{
|
||||
tLeft,
|
||||
{itemRange, 0, "range"},
|
||||
tSpace,
|
||||
{itemIf, 0, "if"},
|
||||
tSpace,
|
||||
{itemElse, 0, "else"},
|
||||
tSpace,
|
||||
{itemEnd, 0, "end"},
|
||||
tSpace,
|
||||
{itemWith, 0, "with"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$c"},
|
||||
tSpace,
|
||||
{itemColonEquals, 0, ":="},
|
||||
tSpace,
|
||||
{itemIdentifier, 0, "printf"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$hello"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$23"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$var"},
|
||||
{itemField, 0, ".Field"},
|
||||
tSpace,
|
||||
{itemField, 0, ".Method"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"variable invocation", "{{$x 23}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$x"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "23"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"pipeline", `intro {{echo hi 1.2 |noargs|args 1 "hi"}} outro`, []item{
|
||||
{itemText, 0, "intro "},
|
||||
tLeft,
|
||||
{itemIdentifier, 0, "echo"},
|
||||
tSpace,
|
||||
{itemIdentifier, 0, "hi"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "1.2"},
|
||||
tSpace,
|
||||
tPipe,
|
||||
{itemIdentifier, 0, "noargs"},
|
||||
tPipe,
|
||||
{itemIdentifier, 0, "args"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "1"},
|
||||
tSpace,
|
||||
{itemString, 0, `"hi"`},
|
||||
tRight,
|
||||
{itemText, 0, " outro"},
|
||||
tEOF,
|
||||
}},
|
||||
{"declaration", "{{$v := 3}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$v"},
|
||||
tSpace,
|
||||
{itemColonEquals, 0, ":="},
|
||||
tSpace,
|
||||
{itemNumber, 0, "3"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"2 declarations", "{{$v , $w := 3}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$v"},
|
||||
tSpace,
|
||||
{itemChar, 0, ","},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$w"},
|
||||
tSpace,
|
||||
{itemColonEquals, 0, ":="},
|
||||
tSpace,
|
||||
{itemNumber, 0, "3"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"field of parenthesized expression", "{{(.X).Y}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
{itemField, 0, ".X"},
|
||||
tRpar,
|
||||
{itemField, 0, ".Y"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
// errors
|
||||
{"badchar", "#{{\x01}}", []item{
|
||||
{itemText, 0, "#"},
|
||||
tLeft,
|
||||
{itemError, 0, "unrecognized character in action: U+0001"},
|
||||
}},
|
||||
{"unclosed action", "{{\n}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unclosed action"},
|
||||
}},
|
||||
{"EOF in action", "{{range", []item{
|
||||
tLeft,
|
||||
tRange,
|
||||
{itemError, 0, "unclosed action"},
|
||||
}},
|
||||
{"unclosed quote", "{{\"\n\"}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unterminated quoted string"},
|
||||
}},
|
||||
{"unclosed raw quote", "{{`xx\n`}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unterminated raw quoted string"},
|
||||
}},
|
||||
{"unclosed char constant", "{{'\n}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unterminated character constant"},
|
||||
}},
|
||||
{"bad number", "{{3k}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, `bad number syntax: "3k"`},
|
||||
}},
|
||||
{"unclosed paren", "{{(3}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
{itemNumber, 0, "3"},
|
||||
{itemError, 0, `unclosed left paren`},
|
||||
}},
|
||||
{"extra right paren", "{{3)}}", []item{
|
||||
tLeft,
|
||||
{itemNumber, 0, "3"},
|
||||
tRpar,
|
||||
{itemError, 0, `unexpected right paren U+0029 ')'`},
|
||||
}},
|
||||
|
||||
// Fixed bugs
|
||||
// Many elements in an action blew the lookahead until
|
||||
// we made lexInsideAction not loop.
|
||||
{"long pipeline deadlock", "{{|||||}}", []item{
|
||||
tLeft,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"text with bad comment", "hello-{{/*/}}-world", []item{
|
||||
{itemText, 0, "hello-"},
|
||||
{itemError, 0, `unclosed comment`},
|
||||
}},
|
||||
{"text with comment close separted from delim", "hello-{{/* */ }}-world", []item{
|
||||
{itemText, 0, "hello-"},
|
||||
{itemError, 0, `comment ends before closing delimiter`},
|
||||
}},
|
||||
// This one is an error that we can't catch because it breaks templates with
|
||||
// minimized JavaScript. Should have fixed it before Go 1.1.
|
||||
{"unmatched right delimiter", "hello-{.}}-world", []item{
|
||||
{itemText, 0, "hello-{.}}-world"},
|
||||
tEOF,
|
||||
}},
|
||||
}
|
||||
|
||||
// collect gathers the emitted items into a slice.
|
||||
func collect(t *lexTest, left, right string) (items []item) {
|
||||
l := lex(t.name, t.input, left, right)
|
||||
for {
|
||||
item := l.nextItem()
|
||||
items = append(items, item)
|
||||
if item.typ == itemEOF || item.typ == itemError {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func equal(i1, i2 []item, checkPos bool) bool {
|
||||
if len(i1) != len(i2) {
|
||||
return false
|
||||
}
|
||||
for k := range i1 {
|
||||
if i1[k].typ != i2[k].typ {
|
||||
return false
|
||||
}
|
||||
if i1[k].val != i2[k].val {
|
||||
return false
|
||||
}
|
||||
if checkPos && i1[k].pos != i2[k].pos {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestLex(t *testing.T) {
|
||||
for _, test := range lexTests {
|
||||
items := collect(&test, "", "")
|
||||
if !equal(items, test.items, false) {
|
||||
t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, items, test.items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some easy cases from above, but with delimiters $$ and @@
|
||||
var lexDelimTests = []lexTest{
|
||||
{"punctuation", "$$,@%{{}}@@", []item{
|
||||
tLeftDelim,
|
||||
{itemChar, 0, ","},
|
||||
{itemChar, 0, "@"},
|
||||
{itemChar, 0, "%"},
|
||||
{itemChar, 0, "{"},
|
||||
{itemChar, 0, "{"},
|
||||
{itemChar, 0, "}"},
|
||||
{itemChar, 0, "}"},
|
||||
tRightDelim,
|
||||
tEOF,
|
||||
}},
|
||||
{"empty action", `$$@@`, []item{tLeftDelim, tRightDelim, tEOF}},
|
||||
{"for", `$$for@@`, []item{tLeftDelim, tFor, tRightDelim, tEOF}},
|
||||
{"quote", `$$"abc \n\t\" "@@`, []item{tLeftDelim, tQuote, tRightDelim, tEOF}},
|
||||
{"raw quote", "$$" + raw + "@@", []item{tLeftDelim, tRawQuote, tRightDelim, tEOF}},
|
||||
}
|
||||
|
||||
var (
|
||||
tLeftDelim = item{itemLeftDelim, 0, "$$"}
|
||||
tRightDelim = item{itemRightDelim, 0, "@@"}
|
||||
)
|
||||
|
||||
func TestDelims(t *testing.T) {
|
||||
for _, test := range lexDelimTests {
|
||||
items := collect(&test, "$$", "@@")
|
||||
if !equal(items, test.items, false) {
|
||||
t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lexPosTests = []lexTest{
|
||||
{"empty", "", []item{tEOF}},
|
||||
{"punctuation", "{{,@%#}}", []item{
|
||||
{itemLeftDelim, 0, "{{"},
|
||||
{itemChar, 2, ","},
|
||||
{itemChar, 3, "@"},
|
||||
{itemChar, 4, "%"},
|
||||
{itemChar, 5, "#"},
|
||||
{itemRightDelim, 6, "}}"},
|
||||
{itemEOF, 8, ""},
|
||||
}},
|
||||
{"sample", "0123{{hello}}xyz", []item{
|
||||
{itemText, 0, "0123"},
|
||||
{itemLeftDelim, 4, "{{"},
|
||||
{itemIdentifier, 6, "hello"},
|
||||
{itemRightDelim, 11, "}}"},
|
||||
{itemText, 13, "xyz"},
|
||||
{itemEOF, 16, ""},
|
||||
}},
|
||||
}
|
||||
|
||||
// The other tests don't check position, to make the test cases easier to construct.
|
||||
// This one does.
|
||||
func TestPos(t *testing.T) {
|
||||
for _, test := range lexPosTests {
|
||||
items := collect(&test, "", "")
|
||||
if !equal(items, test.items, true) {
|
||||
t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
|
||||
if len(items) == len(test.items) {
|
||||
// Detailed print; avoid item.String() to expose the position value.
|
||||
for i := range items {
|
||||
if !equal(items[i:i+1], test.items[i:i+1], true) {
|
||||
i1 := items[i]
|
||||
i2 := test.items[i]
|
||||
t.Errorf("\t#%d: got {%v %d %q} expected {%v %d %q}", i, i1.typ, i1.pos, i1.val, i2.typ, i2.pos, i2.val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,834 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Parse nodes.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var textFormat = "%s" // Changed to "%q" in tests for better error messages.
|
||||
|
||||
// A Node is an element in the parse tree. The interface is trivial.
|
||||
// The interface contains an unexported method so that only
|
||||
// types local to this package can satisfy it.
|
||||
type Node interface {
|
||||
Type() NodeType
|
||||
String() string
|
||||
// Copy does a deep copy of the Node and all its components.
|
||||
// To avoid type assertions, some XxxNodes also have specialized
|
||||
// CopyXxx methods that return *XxxNode.
|
||||
Copy() Node
|
||||
Position() Pos // byte position of start of node in full original input string
|
||||
// tree returns the containing *Tree.
|
||||
// It is unexported so all implementations of Node are in this package.
|
||||
tree() *Tree
|
||||
}
|
||||
|
||||
// NodeType identifies the type of a parse tree node.
|
||||
type NodeType int
|
||||
|
||||
// Pos represents a byte position in the original input text from which
|
||||
// this template was parsed.
|
||||
type Pos int
|
||||
|
||||
func (p Pos) Position() Pos {
|
||||
return p
|
||||
}
|
||||
|
||||
// Type returns itself and provides an easy default implementation
|
||||
// for embedding in a Node. Embedded in all non-trivial Nodes.
|
||||
func (t NodeType) Type() NodeType {
|
||||
return t
|
||||
}
|
||||
|
||||
const (
|
||||
NodeText NodeType = iota // Plain text.
|
||||
NodeAction // A non-control action such as a field evaluation.
|
||||
NodeBool // A boolean constant.
|
||||
NodeChain // A sequence of field accesses.
|
||||
NodeCommand // An element of a pipeline.
|
||||
NodeDot // The cursor, dot.
|
||||
nodeElse // An else action. Not added to tree.
|
||||
nodeEnd // An end action. Not added to tree.
|
||||
NodeField // A field or method name.
|
||||
NodeIdentifier // An identifier; always a function name.
|
||||
NodeIf // An if action.
|
||||
NodeList // A list of Nodes.
|
||||
NodeNil // An untyped nil constant.
|
||||
NodeNumber // A numerical constant.
|
||||
NodePipe // A pipeline of commands.
|
||||
NodeRange // A range action.
|
||||
NodeString // A string constant.
|
||||
NodeTemplate // A template invocation action.
|
||||
NodeVariable // A $ variable.
|
||||
NodeWith // A with action.
|
||||
)
|
||||
|
||||
// Nodes.
|
||||
|
||||
// ListNode holds a sequence of nodes.
|
||||
type ListNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Nodes []Node // The element nodes in lexical order.
|
||||
}
|
||||
|
||||
func (t *Tree) newList(pos Pos) *ListNode {
|
||||
return &ListNode{tr: t, NodeType: NodeList, Pos: pos}
|
||||
}
|
||||
|
||||
func (l *ListNode) append(n Node) {
|
||||
l.Nodes = append(l.Nodes, n)
|
||||
}
|
||||
|
||||
func (l *ListNode) tree() *Tree {
|
||||
return l.tr
|
||||
}
|
||||
|
||||
func (l *ListNode) String() string {
|
||||
b := new(bytes.Buffer)
|
||||
for _, n := range l.Nodes {
|
||||
fmt.Fprint(b, n)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (l *ListNode) CopyList() *ListNode {
|
||||
if l == nil {
|
||||
return l
|
||||
}
|
||||
n := l.tr.newList(l.Pos)
|
||||
for _, elem := range l.Nodes {
|
||||
n.append(elem.Copy())
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (l *ListNode) Copy() Node {
|
||||
return l.CopyList()
|
||||
}
|
||||
|
||||
// TextNode holds plain text.
|
||||
type TextNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Text []byte // The text; may span newlines.
|
||||
}
|
||||
|
||||
func (t *Tree) newText(pos Pos, text string) *TextNode {
|
||||
return &TextNode{tr: t, NodeType: NodeText, Pos: pos, Text: []byte(text)}
|
||||
}
|
||||
|
||||
func (t *TextNode) String() string {
|
||||
return fmt.Sprintf(textFormat, t.Text)
|
||||
}
|
||||
|
||||
func (t *TextNode) tree() *Tree {
|
||||
return t.tr
|
||||
}
|
||||
|
||||
func (t *TextNode) Copy() Node {
|
||||
return &TextNode{tr: t.tr, NodeType: NodeText, Pos: t.Pos, Text: append([]byte{}, t.Text...)}
|
||||
}
|
||||
|
||||
// PipeNode holds a pipeline with optional declaration
|
||||
type PipeNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Line int // The line number in the input (deprecated; kept for compatibility)
|
||||
Decl []*VariableNode // Variable declarations in lexical order.
|
||||
Cmds []*CommandNode // The commands in lexical order.
|
||||
}
|
||||
|
||||
func (t *Tree) newPipeline(pos Pos, line int, decl []*VariableNode) *PipeNode {
|
||||
return &PipeNode{tr: t, NodeType: NodePipe, Pos: pos, Line: line, Decl: decl}
|
||||
}
|
||||
|
||||
func (p *PipeNode) append(command *CommandNode) {
|
||||
p.Cmds = append(p.Cmds, command)
|
||||
}
|
||||
|
||||
func (p *PipeNode) String() string {
|
||||
s := ""
|
||||
if len(p.Decl) > 0 {
|
||||
for i, v := range p.Decl {
|
||||
if i > 0 {
|
||||
s += ", "
|
||||
}
|
||||
s += v.String()
|
||||
}
|
||||
s += " := "
|
||||
}
|
||||
for i, c := range p.Cmds {
|
||||
if i > 0 {
|
||||
s += " | "
|
||||
}
|
||||
s += c.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *PipeNode) tree() *Tree {
|
||||
return p.tr
|
||||
}
|
||||
|
||||
func (p *PipeNode) CopyPipe() *PipeNode {
|
||||
if p == nil {
|
||||
return p
|
||||
}
|
||||
var decl []*VariableNode
|
||||
for _, d := range p.Decl {
|
||||
decl = append(decl, d.Copy().(*VariableNode))
|
||||
}
|
||||
n := p.tr.newPipeline(p.Pos, p.Line, decl)
|
||||
for _, c := range p.Cmds {
|
||||
n.append(c.Copy().(*CommandNode))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (p *PipeNode) Copy() Node {
|
||||
return p.CopyPipe()
|
||||
}
|
||||
|
||||
// ActionNode holds an action (something bounded by delimiters).
|
||||
// Control actions have their own nodes; ActionNode represents simple
|
||||
// ones such as field evaluations and parenthesized pipelines.
|
||||
type ActionNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Line int // The line number in the input (deprecated; kept for compatibility)
|
||||
Pipe *PipeNode // The pipeline in the action.
|
||||
}
|
||||
|
||||
func (t *Tree) newAction(pos Pos, line int, pipe *PipeNode) *ActionNode {
|
||||
return &ActionNode{tr: t, NodeType: NodeAction, Pos: pos, Line: line, Pipe: pipe}
|
||||
}
|
||||
|
||||
func (a *ActionNode) String() string {
|
||||
return fmt.Sprintf("{{%s}}", a.Pipe)
|
||||
|
||||
}
|
||||
|
||||
func (a *ActionNode) tree() *Tree {
|
||||
return a.tr
|
||||
}
|
||||
|
||||
func (a *ActionNode) Copy() Node {
|
||||
return a.tr.newAction(a.Pos, a.Line, a.Pipe.CopyPipe())
|
||||
|
||||
}
|
||||
|
||||
// CommandNode holds a command (a pipeline inside an evaluating action).
|
||||
type CommandNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Args []Node // Arguments in lexical order: Identifier, field, or constant.
|
||||
}
|
||||
|
||||
func (t *Tree) newCommand(pos Pos) *CommandNode {
|
||||
return &CommandNode{tr: t, NodeType: NodeCommand, Pos: pos}
|
||||
}
|
||||
|
||||
func (c *CommandNode) append(arg Node) {
|
||||
c.Args = append(c.Args, arg)
|
||||
}
|
||||
|
||||
func (c *CommandNode) String() string {
|
||||
s := ""
|
||||
for i, arg := range c.Args {
|
||||
if i > 0 {
|
||||
s += " "
|
||||
}
|
||||
if arg, ok := arg.(*PipeNode); ok {
|
||||
s += "(" + arg.String() + ")"
|
||||
continue
|
||||
}
|
||||
s += arg.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *CommandNode) tree() *Tree {
|
||||
return c.tr
|
||||
}
|
||||
|
||||
func (c *CommandNode) Copy() Node {
|
||||
if c == nil {
|
||||
return c
|
||||
}
|
||||
n := c.tr.newCommand(c.Pos)
|
||||
for _, c := range c.Args {
|
||||
n.append(c.Copy())
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// IdentifierNode holds an identifier.
|
||||
type IdentifierNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Ident string // The identifier's name.
|
||||
}
|
||||
|
||||
// NewIdentifier returns a new IdentifierNode with the given identifier name.
|
||||
func NewIdentifier(ident string) *IdentifierNode {
|
||||
return &IdentifierNode{NodeType: NodeIdentifier, Ident: ident}
|
||||
}
|
||||
|
||||
// SetPos sets the position. NewIdentifier is a public method so we can't modify its signature.
|
||||
// Chained for convenience.
|
||||
// TODO: fix one day?
|
||||
func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
|
||||
i.Pos = pos
|
||||
return i
|
||||
}
|
||||
|
||||
// SetTree sets the parent tree for the node. NewIdentifier is a public method so we can't modify its signature.
|
||||
// Chained for convenience.
|
||||
// TODO: fix one day?
|
||||
func (i *IdentifierNode) SetTree(t *Tree) *IdentifierNode {
|
||||
i.tr = t
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *IdentifierNode) String() string {
|
||||
return i.Ident
|
||||
}
|
||||
|
||||
func (i *IdentifierNode) tree() *Tree {
|
||||
return i.tr
|
||||
}
|
||||
|
||||
func (i *IdentifierNode) Copy() Node {
|
||||
return NewIdentifier(i.Ident).SetTree(i.tr).SetPos(i.Pos)
|
||||
}
|
||||
|
||||
// VariableNode holds a list of variable names, possibly with chained field
|
||||
// accesses. The dollar sign is part of the (first) name.
|
||||
type VariableNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Ident []string // Variable name and fields in lexical order.
|
||||
}
|
||||
|
||||
func (t *Tree) newVariable(pos Pos, ident string) *VariableNode {
|
||||
return &VariableNode{tr: t, NodeType: NodeVariable, Pos: pos, Ident: strings.Split(ident, ".")}
|
||||
}
|
||||
|
||||
func (v *VariableNode) String() string {
|
||||
s := ""
|
||||
for i, id := range v.Ident {
|
||||
if i > 0 {
|
||||
s += "."
|
||||
}
|
||||
s += id
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (v *VariableNode) tree() *Tree {
|
||||
return v.tr
|
||||
}
|
||||
|
||||
func (v *VariableNode) Copy() Node {
|
||||
return &VariableNode{tr: v.tr, NodeType: NodeVariable, Pos: v.Pos, Ident: append([]string{}, v.Ident...)}
|
||||
}
|
||||
|
||||
// DotNode holds the special identifier '.'.
|
||||
type DotNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
}
|
||||
|
||||
func (t *Tree) newDot(pos Pos) *DotNode {
|
||||
return &DotNode{tr: t, NodeType: NodeDot, Pos: pos}
|
||||
}
|
||||
|
||||
func (d *DotNode) Type() NodeType {
|
||||
// Override method on embedded NodeType for API compatibility.
|
||||
// TODO: Not really a problem; could change API without effect but
|
||||
// api tool complains.
|
||||
return NodeDot
|
||||
}
|
||||
|
||||
func (d *DotNode) String() string {
|
||||
return "."
|
||||
}
|
||||
|
||||
func (d *DotNode) tree() *Tree {
|
||||
return d.tr
|
||||
}
|
||||
|
||||
func (d *DotNode) Copy() Node {
|
||||
return d.tr.newDot(d.Pos)
|
||||
}
|
||||
|
||||
// NilNode holds the special identifier 'nil' representing an untyped nil constant.
|
||||
type NilNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
}
|
||||
|
||||
func (t *Tree) newNil(pos Pos) *NilNode {
|
||||
return &NilNode{tr: t, NodeType: NodeNil, Pos: pos}
|
||||
}
|
||||
|
||||
func (n *NilNode) Type() NodeType {
|
||||
// Override method on embedded NodeType for API compatibility.
|
||||
// TODO: Not really a problem; could change API without effect but
|
||||
// api tool complains.
|
||||
return NodeNil
|
||||
}
|
||||
|
||||
func (n *NilNode) String() string {
|
||||
return "nil"
|
||||
}
|
||||
|
||||
func (n *NilNode) tree() *Tree {
|
||||
return n.tr
|
||||
}
|
||||
|
||||
func (n *NilNode) Copy() Node {
|
||||
return n.tr.newNil(n.Pos)
|
||||
}
|
||||
|
||||
// FieldNode holds a field (identifier starting with '.').
|
||||
// The names may be chained ('.x.y').
|
||||
// The period is dropped from each ident.
|
||||
type FieldNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Ident []string // The identifiers in lexical order.
|
||||
}
|
||||
|
||||
func (t *Tree) newField(pos Pos, ident string) *FieldNode {
|
||||
return &FieldNode{tr: t, NodeType: NodeField, Pos: pos, Ident: strings.Split(ident[1:], ".")} // [1:] to drop leading period
|
||||
}
|
||||
|
||||
func (f *FieldNode) String() string {
|
||||
s := ""
|
||||
for _, id := range f.Ident {
|
||||
s += "." + id
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (f *FieldNode) tree() *Tree {
|
||||
return f.tr
|
||||
}
|
||||
|
||||
func (f *FieldNode) Copy() Node {
|
||||
return &FieldNode{tr: f.tr, NodeType: NodeField, Pos: f.Pos, Ident: append([]string{}, f.Ident...)}
|
||||
}
|
||||
|
||||
// ChainNode holds a term followed by a chain of field accesses (identifier starting with '.').
|
||||
// The names may be chained ('.x.y').
|
||||
// The periods are dropped from each ident.
|
||||
type ChainNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Node Node
|
||||
Field []string // The identifiers in lexical order.
|
||||
}
|
||||
|
||||
func (t *Tree) newChain(pos Pos, node Node) *ChainNode {
|
||||
return &ChainNode{tr: t, NodeType: NodeChain, Pos: pos, Node: node}
|
||||
}
|
||||
|
||||
// Add adds the named field (which should start with a period) to the end of the chain.
|
||||
func (c *ChainNode) Add(field string) {
|
||||
if len(field) == 0 || field[0] != '.' {
|
||||
panic("no dot in field")
|
||||
}
|
||||
field = field[1:] // Remove leading dot.
|
||||
if field == "" {
|
||||
panic("empty field")
|
||||
}
|
||||
c.Field = append(c.Field, field)
|
||||
}
|
||||
|
||||
func (c *ChainNode) String() string {
|
||||
s := c.Node.String()
|
||||
if _, ok := c.Node.(*PipeNode); ok {
|
||||
s = "(" + s + ")"
|
||||
}
|
||||
for _, field := range c.Field {
|
||||
s += "." + field
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *ChainNode) tree() *Tree {
|
||||
return c.tr
|
||||
}
|
||||
|
||||
func (c *ChainNode) Copy() Node {
|
||||
return &ChainNode{tr: c.tr, NodeType: NodeChain, Pos: c.Pos, Node: c.Node, Field: append([]string{}, c.Field...)}
|
||||
}
|
||||
|
||||
// BoolNode holds a boolean constant.
|
||||
type BoolNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
True bool // The value of the boolean constant.
|
||||
}
|
||||
|
||||
func (t *Tree) newBool(pos Pos, true bool) *BoolNode {
|
||||
return &BoolNode{tr: t, NodeType: NodeBool, Pos: pos, True: true}
|
||||
}
|
||||
|
||||
func (b *BoolNode) String() string {
|
||||
if b.True {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func (b *BoolNode) tree() *Tree {
|
||||
return b.tr
|
||||
}
|
||||
|
||||
func (b *BoolNode) Copy() Node {
|
||||
return b.tr.newBool(b.Pos, b.True)
|
||||
}
|
||||
|
||||
// NumberNode holds a number: signed or unsigned integer, float, or complex.
|
||||
// The value is parsed and stored under all the types that can represent the value.
|
||||
// This simulates in a small amount of code the behavior of Go's ideal constants.
|
||||
type NumberNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
IsInt bool // Number has an integral value.
|
||||
IsUint bool // Number has an unsigned integral value.
|
||||
IsFloat bool // Number has a floating-point value.
|
||||
IsComplex bool // Number is complex.
|
||||
Int64 int64 // The signed integer value.
|
||||
Uint64 uint64 // The unsigned integer value.
|
||||
Float64 float64 // The floating-point value.
|
||||
Complex128 complex128 // The complex value.
|
||||
Text string // The original textual representation from the input.
|
||||
}
|
||||
|
||||
func (t *Tree) newNumber(pos Pos, text string, typ itemType) (*NumberNode, error) {
|
||||
n := &NumberNode{tr: t, NodeType: NodeNumber, Pos: pos, Text: text}
|
||||
switch typ {
|
||||
case itemCharConstant:
|
||||
rune, _, tail, err := strconv.UnquoteChar(text[1:], text[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tail != "'" {
|
||||
return nil, fmt.Errorf("malformed character constant: %s", text)
|
||||
}
|
||||
n.Int64 = int64(rune)
|
||||
n.IsInt = true
|
||||
n.Uint64 = uint64(rune)
|
||||
n.IsUint = true
|
||||
n.Float64 = float64(rune) // odd but those are the rules.
|
||||
n.IsFloat = true
|
||||
return n, nil
|
||||
case itemComplex:
|
||||
// fmt.Sscan can parse the pair, so let it do the work.
|
||||
if _, err := fmt.Sscan(text, &n.Complex128); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n.IsComplex = true
|
||||
n.simplifyComplex()
|
||||
return n, nil
|
||||
}
|
||||
// Imaginary constants can only be complex unless they are zero.
|
||||
if len(text) > 0 && text[len(text)-1] == 'i' {
|
||||
f, err := strconv.ParseFloat(text[:len(text)-1], 64)
|
||||
if err == nil {
|
||||
n.IsComplex = true
|
||||
n.Complex128 = complex(0, f)
|
||||
n.simplifyComplex()
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
// Do integer test first so we get 0x123 etc.
|
||||
u, err := strconv.ParseUint(text, 0, 64) // will fail for -0; fixed below.
|
||||
if err == nil {
|
||||
n.IsUint = true
|
||||
n.Uint64 = u
|
||||
}
|
||||
i, err := strconv.ParseInt(text, 0, 64)
|
||||
if err == nil {
|
||||
n.IsInt = true
|
||||
n.Int64 = i
|
||||
if i == 0 {
|
||||
n.IsUint = true // in case of -0.
|
||||
n.Uint64 = u
|
||||
}
|
||||
}
|
||||
// If an integer extraction succeeded, promote the float.
|
||||
if n.IsInt {
|
||||
n.IsFloat = true
|
||||
n.Float64 = float64(n.Int64)
|
||||
} else if n.IsUint {
|
||||
n.IsFloat = true
|
||||
n.Float64 = float64(n.Uint64)
|
||||
} else {
|
||||
f, err := strconv.ParseFloat(text, 64)
|
||||
if err == nil {
|
||||
n.IsFloat = true
|
||||
n.Float64 = f
|
||||
// If a floating-point extraction succeeded, extract the int if needed.
|
||||
if !n.IsInt && float64(int64(f)) == f {
|
||||
n.IsInt = true
|
||||
n.Int64 = int64(f)
|
||||
}
|
||||
if !n.IsUint && float64(uint64(f)) == f {
|
||||
n.IsUint = true
|
||||
n.Uint64 = uint64(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !n.IsInt && !n.IsUint && !n.IsFloat {
|
||||
return nil, fmt.Errorf("illegal number syntax: %q", text)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// simplifyComplex pulls out any other types that are represented by the complex number.
|
||||
// These all require that the imaginary part be zero.
|
||||
func (n *NumberNode) simplifyComplex() {
|
||||
n.IsFloat = imag(n.Complex128) == 0
|
||||
if n.IsFloat {
|
||||
n.Float64 = real(n.Complex128)
|
||||
n.IsInt = float64(int64(n.Float64)) == n.Float64
|
||||
if n.IsInt {
|
||||
n.Int64 = int64(n.Float64)
|
||||
}
|
||||
n.IsUint = float64(uint64(n.Float64)) == n.Float64
|
||||
if n.IsUint {
|
||||
n.Uint64 = uint64(n.Float64)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NumberNode) String() string {
|
||||
return n.Text
|
||||
}
|
||||
|
||||
func (n *NumberNode) tree() *Tree {
|
||||
return n.tr
|
||||
}
|
||||
|
||||
func (n *NumberNode) Copy() Node {
|
||||
nn := new(NumberNode)
|
||||
*nn = *n // Easy, fast, correct.
|
||||
return nn
|
||||
}
|
||||
|
||||
// StringNode holds a string constant. The value has been "unquoted".
|
||||
type StringNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Quoted string // The original text of the string, with quotes.
|
||||
Text string // The string, after quote processing.
|
||||
}
|
||||
|
||||
func (t *Tree) newString(pos Pos, orig, text string) *StringNode {
|
||||
return &StringNode{tr: t, NodeType: NodeString, Pos: pos, Quoted: orig, Text: text}
|
||||
}
|
||||
|
||||
func (s *StringNode) String() string {
|
||||
return s.Quoted
|
||||
}
|
||||
|
||||
func (s *StringNode) tree() *Tree {
|
||||
return s.tr
|
||||
}
|
||||
|
||||
func (s *StringNode) Copy() Node {
|
||||
return s.tr.newString(s.Pos, s.Quoted, s.Text)
|
||||
}
|
||||
|
||||
// endNode represents an {{end}} action.
|
||||
// It does not appear in the final parse tree.
|
||||
type endNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
}
|
||||
|
||||
func (t *Tree) newEnd(pos Pos) *endNode {
|
||||
return &endNode{tr: t, NodeType: nodeEnd, Pos: pos}
|
||||
}
|
||||
|
||||
func (e *endNode) String() string {
|
||||
return "{{end}}"
|
||||
}
|
||||
|
||||
func (e *endNode) tree() *Tree {
|
||||
return e.tr
|
||||
}
|
||||
|
||||
func (e *endNode) Copy() Node {
|
||||
return e.tr.newEnd(e.Pos)
|
||||
}
|
||||
|
||||
// elseNode represents an {{else}} action. Does not appear in the final tree.
|
||||
type elseNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Line int // The line number in the input (deprecated; kept for compatibility)
|
||||
}
|
||||
|
||||
func (t *Tree) newElse(pos Pos, line int) *elseNode {
|
||||
return &elseNode{tr: t, NodeType: nodeElse, Pos: pos, Line: line}
|
||||
}
|
||||
|
||||
func (e *elseNode) Type() NodeType {
|
||||
return nodeElse
|
||||
}
|
||||
|
||||
func (e *elseNode) String() string {
|
||||
return "{{else}}"
|
||||
}
|
||||
|
||||
func (e *elseNode) tree() *Tree {
|
||||
return e.tr
|
||||
}
|
||||
|
||||
func (e *elseNode) Copy() Node {
|
||||
return e.tr.newElse(e.Pos, e.Line)
|
||||
}
|
||||
|
||||
// BranchNode is the common representation of if, range, and with.
|
||||
type BranchNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Line int // The line number in the input (deprecated; kept for compatibility)
|
||||
Pipe *PipeNode // The pipeline to be evaluated.
|
||||
List *ListNode // What to execute if the value is non-empty.
|
||||
ElseList *ListNode // What to execute if the value is empty (nil if absent).
|
||||
}
|
||||
|
||||
func (b *BranchNode) String() string {
|
||||
name := ""
|
||||
switch b.NodeType {
|
||||
case NodeIf:
|
||||
name = "if"
|
||||
case NodeRange:
|
||||
name = "range"
|
||||
case NodeWith:
|
||||
name = "with"
|
||||
default:
|
||||
panic("unknown branch type")
|
||||
}
|
||||
if b.ElseList != nil {
|
||||
return fmt.Sprintf("{{%s %s}}%s{{else}}%s{{end}}", name, b.Pipe, b.List, b.ElseList)
|
||||
}
|
||||
return fmt.Sprintf("{{%s %s}}%s{{end}}", name, b.Pipe, b.List)
|
||||
}
|
||||
|
||||
func (b *BranchNode) tree() *Tree {
|
||||
return b.tr
|
||||
}
|
||||
|
||||
func (b *BranchNode) Copy() Node {
|
||||
switch b.NodeType {
|
||||
case NodeIf:
|
||||
return b.tr.newIf(b.Pos, b.Line, b.Pipe, b.List, b.ElseList)
|
||||
case NodeRange:
|
||||
return b.tr.newRange(b.Pos, b.Line, b.Pipe, b.List, b.ElseList)
|
||||
case NodeWith:
|
||||
return b.tr.newWith(b.Pos, b.Line, b.Pipe, b.List, b.ElseList)
|
||||
default:
|
||||
panic("unknown branch type")
|
||||
}
|
||||
}
|
||||
|
||||
// IfNode represents an {{if}} action and its commands.
|
||||
type IfNode struct {
|
||||
BranchNode
|
||||
}
|
||||
|
||||
func (t *Tree) newIf(pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) *IfNode {
|
||||
return &IfNode{BranchNode{tr: t, NodeType: NodeIf, Pos: pos, Line: line, Pipe: pipe, List: list, ElseList: elseList}}
|
||||
}
|
||||
|
||||
func (i *IfNode) Copy() Node {
|
||||
return i.tr.newIf(i.Pos, i.Line, i.Pipe.CopyPipe(), i.List.CopyList(), i.ElseList.CopyList())
|
||||
}
|
||||
|
||||
// RangeNode represents a {{range}} action and its commands.
|
||||
type RangeNode struct {
|
||||
BranchNode
|
||||
}
|
||||
|
||||
func (t *Tree) newRange(pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) *RangeNode {
|
||||
return &RangeNode{BranchNode{tr: t, NodeType: NodeRange, Pos: pos, Line: line, Pipe: pipe, List: list, ElseList: elseList}}
|
||||
}
|
||||
|
||||
func (r *RangeNode) Copy() Node {
|
||||
return r.tr.newRange(r.Pos, r.Line, r.Pipe.CopyPipe(), r.List.CopyList(), r.ElseList.CopyList())
|
||||
}
|
||||
|
||||
// WithNode represents a {{with}} action and its commands.
|
||||
type WithNode struct {
|
||||
BranchNode
|
||||
}
|
||||
|
||||
func (t *Tree) newWith(pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) *WithNode {
|
||||
return &WithNode{BranchNode{tr: t, NodeType: NodeWith, Pos: pos, Line: line, Pipe: pipe, List: list, ElseList: elseList}}
|
||||
}
|
||||
|
||||
func (w *WithNode) Copy() Node {
|
||||
return w.tr.newWith(w.Pos, w.Line, w.Pipe.CopyPipe(), w.List.CopyList(), w.ElseList.CopyList())
|
||||
}
|
||||
|
||||
// TemplateNode represents a {{template}} action.
|
||||
type TemplateNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Line int // The line number in the input (deprecated; kept for compatibility)
|
||||
Name string // The name of the template (unquoted).
|
||||
Pipe *PipeNode // The command to evaluate as dot for the template.
|
||||
}
|
||||
|
||||
func (t *Tree) newTemplate(pos Pos, line int, name string, pipe *PipeNode) *TemplateNode {
|
||||
return &TemplateNode{tr: t, NodeType: NodeTemplate, Pos: pos, Line: line, Name: name, Pipe: pipe}
|
||||
}
|
||||
|
||||
func (t *TemplateNode) String() string {
|
||||
if t.Pipe == nil {
|
||||
return fmt.Sprintf("{{template %q}}", t.Name)
|
||||
}
|
||||
return fmt.Sprintf("{{template %q %s}}", t.Name, t.Pipe)
|
||||
}
|
||||
|
||||
func (t *TemplateNode) tree() *Tree {
|
||||
return t.tr
|
||||
}
|
||||
|
||||
func (t *TemplateNode) Copy() Node {
|
||||
return t.tr.newTemplate(t.Pos, t.Line, t.Name, t.Pipe.CopyPipe())
|
||||
}
|
|
@ -0,0 +1,700 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package parse builds parse trees for templates as defined by text/template
|
||||
// and html/template. Clients should use those packages to construct templates
|
||||
// rather than this one, which provides shared internal data structures not
|
||||
// intended for general use.
|
||||
package parse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Tree is the representation of a single parsed template.
|
||||
type Tree struct {
|
||||
Name string // name of the template represented by the tree.
|
||||
ParseName string // name of the top-level template during parsing, for error messages.
|
||||
Root *ListNode // top-level root of the tree.
|
||||
text string // text parsed to create the template (or its parent)
|
||||
// Parsing only; cleared after parse.
|
||||
funcs []map[string]interface{}
|
||||
lex *lexer
|
||||
token [3]item // three-token lookahead for parser.
|
||||
peekCount int
|
||||
vars []string // variables defined at the moment.
|
||||
}
|
||||
|
||||
// Copy returns a copy of the Tree. Any parsing state is discarded.
|
||||
func (t *Tree) Copy() *Tree {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return &Tree{
|
||||
Name: t.Name,
|
||||
ParseName: t.ParseName,
|
||||
Root: t.Root.CopyList(),
|
||||
text: t.text,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse returns a map from template name to parse.Tree, created by parsing the
|
||||
// templates described in the argument string. The top-level template will be
|
||||
// given the specified name. If an error is encountered, parsing stops and an
|
||||
// empty map is returned with the error.
|
||||
func Parse(name, text, leftDelim, rightDelim string, funcs ...map[string]interface{}) (treeSet map[string]*Tree, err error) {
|
||||
treeSet = make(map[string]*Tree)
|
||||
t := New(name)
|
||||
t.text = text
|
||||
_, err = t.Parse(text, leftDelim, rightDelim, treeSet, funcs...)
|
||||
return
|
||||
}
|
||||
|
||||
// next returns the next token.
|
||||
func (t *Tree) next() item {
|
||||
if t.peekCount > 0 {
|
||||
t.peekCount--
|
||||
} else {
|
||||
t.token[0] = t.lex.nextItem()
|
||||
}
|
||||
return t.token[t.peekCount]
|
||||
}
|
||||
|
||||
// backup backs the input stream up one token.
|
||||
func (t *Tree) backup() {
|
||||
t.peekCount++
|
||||
}
|
||||
|
||||
// backup2 backs the input stream up two tokens.
|
||||
// The zeroth token is already there.
|
||||
func (t *Tree) backup2(t1 item) {
|
||||
t.token[1] = t1
|
||||
t.peekCount = 2
|
||||
}
|
||||
|
||||
// backup3 backs the input stream up three tokens
|
||||
// The zeroth token is already there.
|
||||
func (t *Tree) backup3(t2, t1 item) { // Reverse order: we're pushing back.
|
||||
t.token[1] = t1
|
||||
t.token[2] = t2
|
||||
t.peekCount = 3
|
||||
}
|
||||
|
||||
// peek returns but does not consume the next token.
|
||||
func (t *Tree) peek() item {
|
||||
if t.peekCount > 0 {
|
||||
return t.token[t.peekCount-1]
|
||||
}
|
||||
t.peekCount = 1
|
||||
t.token[0] = t.lex.nextItem()
|
||||
return t.token[0]
|
||||
}
|
||||
|
||||
// nextNonSpace returns the next non-space token.
|
||||
func (t *Tree) nextNonSpace() (token item) {
|
||||
for {
|
||||
token = t.next()
|
||||
if token.typ != itemSpace {
|
||||
break
|
||||
}
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
// peekNonSpace returns but does not consume the next non-space token.
|
||||
func (t *Tree) peekNonSpace() (token item) {
|
||||
for {
|
||||
token = t.next()
|
||||
if token.typ != itemSpace {
|
||||
break
|
||||
}
|
||||
}
|
||||
t.backup()
|
||||
return token
|
||||
}
|
||||
|
||||
// Parsing.
|
||||
|
||||
// New allocates a new parse tree with the given name.
|
||||
func New(name string, funcs ...map[string]interface{}) *Tree {
|
||||
return &Tree{
|
||||
Name: name,
|
||||
funcs: funcs,
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorContext returns a textual representation of the location of the node in the input text.
|
||||
// The receiver is only used when the node does not have a pointer to the tree inside,
|
||||
// which can occur in old code.
|
||||
func (t *Tree) ErrorContext(n Node) (location, context string) {
|
||||
pos := int(n.Position())
|
||||
tree := n.tree()
|
||||
if tree == nil {
|
||||
tree = t
|
||||
}
|
||||
text := tree.text[:pos]
|
||||
byteNum := strings.LastIndex(text, "\n")
|
||||
if byteNum == -1 {
|
||||
byteNum = pos // On first line.
|
||||
} else {
|
||||
byteNum++ // After the newline.
|
||||
byteNum = pos - byteNum
|
||||
}
|
||||
lineNum := 1 + strings.Count(text, "\n")
|
||||
context = n.String()
|
||||
if len(context) > 20 {
|
||||
context = fmt.Sprintf("%.20s...", context)
|
||||
}
|
||||
return fmt.Sprintf("%s:%d:%d", tree.ParseName, lineNum, byteNum), context
|
||||
}
|
||||
|
||||
// errorf formats the error and terminates processing.
|
||||
func (t *Tree) errorf(format string, args ...interface{}) {
|
||||
t.Root = nil
|
||||
format = fmt.Sprintf("template: %s:%d: %s", t.ParseName, t.lex.lineNumber(), format)
|
||||
panic(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
// error terminates processing.
|
||||
func (t *Tree) error(err error) {
|
||||
|