전체 코드 정리
This commit is contained in:
parent
bcd906c850
commit
8ebf91bf19
|
@ -120,6 +120,5 @@ $RECYCLE.BIN/
|
||||||
*.lnk
|
*.lnk
|
||||||
|
|
||||||
build/
|
build/
|
||||||
vendor/
|
|
||||||
|
|
||||||
cpu_ctrl
|
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
|
package consumer
|
||||||
|
|
||||||
import (
|
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"
|
"time"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
"amuz.es/src/infra/goutils/handler"
|
||||||
|
"amuz.es/src/infra/cpu_ctrl/producer"
|
||||||
var (
|
"github.com/influxdata/influxdb/client/v2"
|
||||||
influxLogger = logger.NewLogger("influx")
|
zlog "amuz.es/src/infra/goutils/logger/zap"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type data struct {
|
type data struct {
|
||||||
|
@ -21,29 +18,31 @@ type data struct {
|
||||||
type influxMetric struct {
|
type influxMetric struct {
|
||||||
host string
|
host string
|
||||||
processorCount int
|
processorCount int
|
||||||
handler util.Handler
|
handler *handler.Handler
|
||||||
fanSpeedConsumer chan processor.FanspeedInfo
|
fanSpeedConsumer chan producer.FanspeedInfo
|
||||||
tempetureConsumer chan processor.TempetureInfo
|
tempetureConsumer chan producer.TempetureInfo
|
||||||
|
logger *zap.SugaredLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
type InfluxMetric interface {
|
type InfluxMetric interface {
|
||||||
FanSpeedConsumer() chan<- processor.FanspeedInfo
|
FanSpeedConsumer() chan<- producer.FanspeedInfo
|
||||||
TempetureConsumer() chan<- processor.TempetureInfo
|
TempetureConsumer() chan<- producer.TempetureInfo
|
||||||
StartLogging()
|
StartLogging()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInfluxMetric(host string, processorCount int, handler util.Handler) InfluxMetric {
|
func NewInfluxMetric(host string, processorCount int, handler *handler.Handler) InfluxMetric {
|
||||||
return &influxMetric{
|
return &influxMetric{
|
||||||
host: host,
|
host: host,
|
||||||
processorCount: processorCount,
|
processorCount: processorCount,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
fanSpeedConsumer: make(chan processor.FanspeedInfo, processorCount),
|
fanSpeedConsumer: make(chan producer.FanspeedInfo, processorCount),
|
||||||
tempetureConsumer: make(chan processor.TempetureInfo, 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) FanSpeedConsumer() chan<- producer.FanspeedInfo { return m.fanSpeedConsumer }
|
||||||
func (m *influxMetric) TempetureConsumer() chan<- processor.TempetureInfo { return m.tempetureConsumer }
|
func (m *influxMetric) TempetureConsumer() chan<- producer.TempetureInfo { return m.tempetureConsumer }
|
||||||
|
|
||||||
func (m *influxMetric) StartLogging() {
|
func (m *influxMetric) StartLogging() {
|
||||||
defer m.handler.DecreaseWait()
|
defer m.handler.DecreaseWait()
|
||||||
|
@ -53,17 +52,15 @@ func (m *influxMetric) StartLogging() {
|
||||||
m.handler.NotifyError(err.(error))
|
m.handler.NotifyError(err.(error))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
defer close(m.fanSpeedConsumer)
|
|
||||||
defer close(m.tempetureConsumer)
|
|
||||||
|
|
||||||
defer influxLogger.Info("Metric logging stopped")
|
defer m.logger.Info("Metric logging stopped")
|
||||||
influxLogger.Info("Metric logging started")
|
m.logger.Info("Metric logging started")
|
||||||
|
|
||||||
var influxDbConn client.Client
|
var influxDbConn client.Client
|
||||||
for {
|
for {
|
||||||
conn, err := client.NewUDPClient(client.UDPConfig{Addr: m.host,})
|
conn, err := client.NewUDPClient(client.UDPConfig{Addr: m.host,})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
influxLogger.Error(err)
|
m.logger.Error(err)
|
||||||
} else {
|
} else {
|
||||||
influxDbConn = conn
|
influxDbConn = conn
|
||||||
break
|
break
|
||||||
|
@ -81,15 +78,23 @@ func (m *influxMetric) StartLogging() {
|
||||||
metricData := make([]data, m.processorCount)
|
metricData := make([]data, m.processorCount)
|
||||||
sendData := 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 {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker:
|
case <-ticker:
|
||||||
copy(sendData, metricData)
|
copy(sendData, metricData)
|
||||||
go m.sendPoint(influxDbConn, sendData)
|
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():
|
case <-m.handler.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -104,7 +109,7 @@ func (m *influxMetric) sendPoint(
|
||||||
if point, err := m.getPoint(id, data, at); err == nil {
|
if point, err := m.getPoint(id, data, at); err == nil {
|
||||||
pointList = append(pointList, point)
|
pointList = append(pointList, point)
|
||||||
} else {
|
} 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",
|
Precision: "s",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
influxLogger.Warn(err)
|
m.logger.Warn(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
batchPoint.AddPoints(pointList)
|
batchPoint.AddPoints(pointList)
|
||||||
if err := influxDbConn.Write(batchPoint); err != nil {
|
if err := influxDbConn.Write(batchPoint); err != nil {
|
||||||
influxLogger.Warn(err)
|
m.logger.Warn(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,39 +3,40 @@ package consumer
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"amuz.es/src/infra/cpu_ctrl/util"
|
|
||||||
"time"
|
"time"
|
||||||
"amuz.es/src/infra/cpu_ctrl/processor"
|
zlog "amuz.es/src/infra/goutils/logger/zap"
|
||||||
"amuz.es/src/infra/cpu_ctrl/logger"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"amuz.es/src/infra/goutils/handler"
|
||||||
|
"amuz.es/src/infra/cpu_ctrl/producer"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var ()
|
||||||
fanspeedLogger = logger.NewLogger("fanspeed")
|
|
||||||
)
|
|
||||||
|
|
||||||
type fanControl struct {
|
type fanControl struct {
|
||||||
processorCount int
|
processorCount int
|
||||||
handler util.Handler
|
handler *handler.Handler
|
||||||
fanSpeedConsumer chan processor.FanspeedInfo
|
fanSpeedConsumer chan producer.FanspeedInfo
|
||||||
sampleDuration time.Duration
|
sampleDuration time.Duration
|
||||||
|
logger *zap.SugaredLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
type FanControl interface {
|
type FanControl interface {
|
||||||
Consumer() chan<- processor.FanspeedInfo
|
Consumer() chan<- producer.FanspeedInfo
|
||||||
StartControl()
|
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{
|
return &fanControl{
|
||||||
processorCount: processorCount,
|
processorCount: processorCount,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
fanSpeedConsumer: make(chan processor.FanspeedInfo, processorCount),
|
fanSpeedConsumer: make(chan producer.FanspeedInfo, processorCount),
|
||||||
sampleDuration: sampleDuration,
|
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() {
|
func (c *fanControl) StartControl() {
|
||||||
defer c.handler.DecreaseWait()
|
defer c.handler.DecreaseWait()
|
||||||
|
@ -45,8 +46,8 @@ func (c *fanControl) StartControl() {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
defer close(c.fanSpeedConsumer)
|
defer close(c.fanSpeedConsumer)
|
||||||
defer fanspeedLogger.Info("Fan control stopped")
|
defer c.logger.Info("Fan control stopped")
|
||||||
fanspeedLogger.Info("Fan control started")
|
c.logger.Info("Fan control started")
|
||||||
|
|
||||||
ticker := time.Tick(c.sampleDuration)
|
ticker := time.Tick(c.sampleDuration)
|
||||||
pastFanSpeedList, newFanSpeedList := make([]int, c.processorCount), make([]int, c.processorCount)
|
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.WriteString(fmt.Sprintf("0x%x", item))
|
||||||
buf.WriteRune(' ')
|
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 {
|
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")
|
|
||||||
}
|
|
207
main.go
207
main.go
|
@ -1,173 +1,84 @@
|
||||||
package main
|
package main // import "amuz.es/src/infra/cpu_ctrl"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"runtime"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
"context"
|
||||||
"amuz.es/src/infra/cpu_ctrl/daemon"
|
"os"
|
||||||
"amuz.es/src/infra/cpu_ctrl/logger"
|
"fmt"
|
||||||
"amuz.es/src/infra/cpu_ctrl/processor"
|
"go.uber.org/zap"
|
||||||
"amuz.es/src/infra/cpu_ctrl/util"
|
|
||||||
"amuz.es/src/infra/cpu_ctrl/consumer"
|
|
||||||
|
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
|
"runtime"
|
||||||
|
"amuz.es/src/infra/goutils/handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "cpu_ctrl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// command argument
|
||||||
var (
|
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()
|
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()
|
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()
|
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()
|
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() {
|
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 {
|
if _, err := app.Parse(os.Args[1:]); err != nil {
|
||||||
panic(err)
|
fmt.Fprintln(os.Stderr, err.(error).Error())
|
||||||
}
|
os.Exit(1)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 엔트리 포인트
|
||||||
func main() {
|
func main() {
|
||||||
|
defer finalCloser()
|
||||||
|
|
||||||
var (
|
// 로그 초기화
|
||||||
processorCount = processor.GetProcessorCount()
|
loggerCleanup := initLogger()
|
||||||
processors []processor.Processor
|
defer loggerCleanup()
|
||||||
exitSignal = make(chan os.Signal, 1)
|
|
||||||
handler = util.NewHandler()
|
// 컨텍스트 생성
|
||||||
sampleDuration = time.Second
|
// 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!"))
|
processorCleanup := initProcessor(mainHandler)
|
||||||
}
|
defer processorCleanup()
|
||||||
var (
|
|
||||||
tempetureChannel = make(chan processor.TempetureInfo,1)
|
// 뭔가 문제가 나거나 signal이 발생할 때 까지 애플리케이션이 종료로 빠지지 않게 잡아주는 부분
|
||||||
fanspeedChannel = make(chan processor.FanspeedInfo,1)
|
contextWatcher, contextCloser := initContext(mainHandler)
|
||||||
)
|
defer contextCloser()
|
||||||
processors = make([]processor.Processor, 0, processorCount)
|
|
||||||
for i := 0; i < processorCount; i++ {
|
// ok
|
||||||
if info, err := processor.NewProcessorInfo(handler, i, sampleDuration,
|
logger.Info("bootstrapped application ", time.Since(start), " ", time.Now())
|
||||||
*P, *I, *D, *SetPoint, 0x64, 0x4,
|
contextWatcher()
|
||||||
tempetureChannel, fanspeedChannel,
|
logger.Info("served duration: ", time.Since(start))
|
||||||
);
|
|
||||||
err != nil {
|
|
||||||
handler.NotifyError(err)
|
|
||||||
} else {
|
|
||||||
processors = append(processors, info)
|
|
||||||
handler.IncreaseWait()
|
|
||||||
go info.StartMonitoring()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fanController := consumer.NewFanControl(processorCount, sampleDuration, handler)
|
|
||||||
metricLogger := consumer.NewInfluxMetric("db:8089", processorCount, handler)
|
|
||||||
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 (
|
import (
|
||||||
"github.com/shirou/gopsutil/cpu"
|
"github.com/shirou/gopsutil/cpu"
|
||||||
|
@ -9,16 +9,18 @@ import (
|
||||||
"time"
|
"time"
|
||||||
"sync"
|
"sync"
|
||||||
"path"
|
"path"
|
||||||
"amuz.es/src/infra/cpu_ctrl/logger"
|
zlog "amuz.es/src/infra/goutils/logger/zap"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"strings"
|
"strings"
|
||||||
"amuz.es/src/infra/cpu_ctrl/pid"
|
"amuz.es/src/infra/cpu_ctrl/pid"
|
||||||
"amuz.es/src/infra/cpu_ctrl/util"
|
"amuz.es/src/infra/goutils/handler"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"context"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type processor struct {
|
type processor struct {
|
||||||
handler util.Handler
|
handler *handler.Handler
|
||||||
id int
|
id int
|
||||||
tempeturePath string
|
tempeturePath string
|
||||||
tempeture float64
|
tempeture float64
|
||||||
|
@ -29,6 +31,7 @@ type processor struct {
|
||||||
fanMaxSpeed int
|
fanMaxSpeed int
|
||||||
fanMinSpeed int
|
fanMinSpeed int
|
||||||
fanSpeedChanged chan<- FanspeedInfo
|
fanSpeedChanged chan<- FanspeedInfo
|
||||||
|
logger *zap.SugaredLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
type TempetureInfo struct {
|
type TempetureInfo struct {
|
||||||
|
@ -55,10 +58,6 @@ type Processor interface {
|
||||||
normalizeFanspeed(float64) (int)
|
normalizeFanspeed(float64) (int)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
log = logger.NewLogger("processor")
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetProcessorCount() (maxcpu int) {
|
func GetProcessorCount() (maxcpu int) {
|
||||||
stat, err := cpu.Info()
|
stat, err := cpu.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -77,7 +76,7 @@ func GetProcessorCount() (maxcpu int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProcessorInfo(
|
func NewProcessorInfo(
|
||||||
handler util.Handler,
|
handler *handler.Handler,
|
||||||
processorId int,
|
processorId int,
|
||||||
sampleDuration time.Duration,
|
sampleDuration time.Duration,
|
||||||
P, I, D,
|
P, I, D,
|
||||||
|
@ -109,6 +108,7 @@ func NewProcessorInfo(
|
||||||
fanSpeedChanged: fanSpeedChanged,
|
fanSpeedChanged: fanSpeedChanged,
|
||||||
fanMaxSpeed: maxNoob,
|
fanMaxSpeed: maxNoob,
|
||||||
fanMinSpeed: minNoob,
|
fanMinSpeed: minNoob,
|
||||||
|
logger: zlog.New(nil, "processor"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,6 @@ func (p *processor) FanMaxSpeed() int { return p.fanMaxSpeed }
|
||||||
func (p *processor) FanMinSpeed() int { return p.fanMinSpeed }
|
func (p *processor) FanMinSpeed() int { return p.fanMinSpeed }
|
||||||
|
|
||||||
func (p *processor) StartMonitoring() {
|
func (p *processor) StartMonitoring() {
|
||||||
defer p.handler.DecreaseWait()
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
p.handler.NotifyError(err.(error))
|
p.handler.NotifyError(err.(error))
|
||||||
|
@ -128,8 +127,8 @@ func (p *processor) StartMonitoring() {
|
||||||
|
|
||||||
tempeturePathGlob := path.Join(p.tempeturePath, "temp?_input")
|
tempeturePathGlob := path.Join(p.tempeturePath, "temp?_input")
|
||||||
ticker := time.Tick(p.sampleDuration)
|
ticker := time.Tick(p.sampleDuration)
|
||||||
defer log.Infof("Processor %d monitor stopped", p.id)
|
defer p.logger.Infof("Processor %d monitor stopped", p.id)
|
||||||
log.Infof("Processor %d monitor started with %s", p.id, p.sampleDuration)
|
p.logger.Infof("Processor %d monitor started with %s", p.id, p.sampleDuration)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
@ -155,7 +154,7 @@ func (p *processor) StartMonitoring() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.tempeture = highestTemp
|
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
|
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
|
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():
|
case <-p.handler.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -224,3 +223,43 @@ func (p *processor) normalizeFanspeed(response float64) (adjusted int) {
|
||||||
}
|
}
|
||||||
return
|
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) {
|
||||||
|
t.errorf("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// expect consumes the next token and guarantees it has the required type.
|
||||||
|
func (t *Tree) expect(expected itemType, context string) item {
|
||||||
|
token := t.nextNonSpace()
|
||||||
|
if token.typ != expected {
|
||||||
|
t.unexpected(token, context)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// expectOneOf consumes the next token and guarantees it has one of the required types.
|
||||||
|
func (t *Tree) expectOneOf(expected1, expected2 itemType, context string) item {
|
||||||
|
token := t.nextNonSpace()
|
||||||
|
if token.typ != expected1 && token.typ != expected2 {
|
||||||
|
t.unexpected(token, context)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// unexpected complains about the token and terminates processing.
|
||||||
|
func (t *Tree) unexpected(token item, context string) {
|
||||||
|
t.errorf("unexpected %s in %s", token, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recover is the handler that turns panics into returns from the top level of Parse.
|
||||||
|
func (t *Tree) recover(errp *error) {
|
||||||
|
e := recover()
|
||||||
|
if e != nil {
|
||||||
|
if _, ok := e.(runtime.Error); ok {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
if t != nil {
|
||||||
|
t.stopParse()
|
||||||
|
}
|
||||||
|
*errp = e.(error)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// startParse initializes the parser, using the lexer.
|
||||||
|
func (t *Tree) startParse(funcs []map[string]interface{}, lex *lexer) {
|
||||||
|
t.Root = nil
|
||||||
|
t.lex = lex
|
||||||
|
t.vars = []string{"$"}
|
||||||
|
t.funcs = funcs
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopParse terminates parsing.
|
||||||
|
func (t *Tree) stopParse() {
|
||||||
|
t.lex = nil
|
||||||
|
t.vars = nil
|
||||||
|
t.funcs = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses the template definition string to construct a representation of
|
||||||
|
// the template for execution. If either action delimiter string is empty, the
|
||||||
|
// default ("{{" or "}}") is used. Embedded template definitions are added to
|
||||||
|
// the treeSet map.
|
||||||
|
func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) {
|
||||||
|
defer t.recover(&err)
|
||||||
|
t.ParseName = t.Name
|
||||||
|
t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim))
|
||||||
|
t.text = text
|
||||||
|
t.parse(treeSet)
|
||||||
|
t.add(treeSet)
|
||||||
|
t.stopParse()
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// add adds tree to the treeSet.
|
||||||
|
func (t *Tree) add(treeSet map[string]*Tree) {
|
||||||
|
tree := treeSet[t.Name]
|
||||||
|
if tree == nil || IsEmptyTree(tree.Root) {
|
||||||
|
treeSet[t.Name] = t
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !IsEmptyTree(t.Root) {
|
||||||
|
t.errorf("template: multiple definition of template %q", t.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmptyTree reports whether this tree (node) is empty of everything but space.
|
||||||
|
func IsEmptyTree(n Node) bool {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case nil:
|
||||||
|
return true
|
||||||
|
case *ActionNode:
|
||||||
|
case *IfNode:
|
||||||
|
case *ListNode:
|
||||||
|
for _, node := range n.Nodes {
|
||||||
|
if !IsEmptyTree(node) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case *RangeNode:
|
||||||
|
case *TemplateNode:
|
||||||
|
case *TextNode:
|
||||||
|
return len(bytes.TrimSpace(n.Text)) == 0
|
||||||
|
case *WithNode:
|
||||||
|
default:
|
||||||
|
panic("unknown node: " + n.String())
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse is the top-level parser for a template, essentially the same
|
||||||
|
// as itemList except it also parses {{define}} actions.
|
||||||
|
// It runs to EOF.
|
||||||
|
func (t *Tree) parse(treeSet map[string]*Tree) (next Node) {
|
||||||
|
t.Root = t.newList(t.peek().pos)
|
||||||
|
for t.peek().typ != itemEOF {
|
||||||
|
if t.peek().typ == itemLeftDelim {
|
||||||
|
delim := t.next()
|
||||||
|
if t.nextNonSpace().typ == itemDefine {
|
||||||
|
newT := New("definition") // name will be updated once we know it.
|
||||||
|
newT.text = t.text
|
||||||
|
newT.ParseName = t.ParseName
|
||||||
|
newT.startParse(t.funcs, t.lex)
|
||||||
|
newT.parseDefinition(treeSet)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.backup2(delim)
|
||||||
|
}
|
||||||
|
n := t.textOrAction()
|
||||||
|
if n.Type() == nodeEnd {
|
||||||
|
t.errorf("unexpected %s", n)
|
||||||
|
}
|
||||||
|
t.Root.append(n)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDefinition parses a {{define}} ... {{end}} template definition and
|
||||||
|
// installs the definition in the treeSet map. The "define" keyword has already
|
||||||
|
// been scanned.
|
||||||
|
func (t *Tree) parseDefinition(treeSet map[string]*Tree) {
|
||||||
|
const context = "define clause"
|
||||||
|
name := t.expectOneOf(itemString, itemRawString, context)
|
||||||
|
var err error
|
||||||
|
t.Name, err = strconv.Unquote(name.val)
|
||||||
|
if err != nil {
|
||||||
|
t.error(err)
|
||||||
|
}
|
||||||
|
t.expect(itemRightDelim, context)
|
||||||
|
var end Node
|
||||||
|
t.Root, end = t.itemList()
|
||||||
|
if end.Type() != nodeEnd {
|
||||||
|
t.errorf("unexpected %s in %s", end, context)
|
||||||
|
}
|
||||||
|
t.add(treeSet)
|
||||||
|
t.stopParse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// itemList:
|
||||||
|
// textOrAction*
|
||||||
|
// Terminates at {{end}} or {{else}}, returned separately.
|
||||||
|
func (t *Tree) itemList() (list *ListNode, next Node) {
|
||||||
|
list = t.newList(t.peekNonSpace().pos)
|
||||||
|
for t.peekNonSpace().typ != itemEOF {
|
||||||
|
n := t.textOrAction()
|
||||||
|
switch n.Type() {
|
||||||
|
case nodeEnd, nodeElse:
|
||||||
|
return list, n
|
||||||
|
}
|
||||||
|
list.append(n)
|
||||||
|
}
|
||||||
|
t.errorf("unexpected EOF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// textOrAction:
|
||||||
|
// text | action
|
||||||
|
func (t *Tree) textOrAction() Node {
|
||||||
|
switch token := t.nextNonSpace(); token.typ {
|
||||||
|
case itemElideNewline:
|
||||||
|
return t.elideNewline()
|
||||||
|
case itemText:
|
||||||
|
return t.newText(token.pos, token.val)
|
||||||
|
case itemLeftDelim:
|
||||||
|
return t.action()
|
||||||
|
default:
|
||||||
|
t.unexpected(token, "input")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// elideNewline:
|
||||||
|
// Remove newlines trailing rightDelim if \\ is present.
|
||||||
|
func (t *Tree) elideNewline() Node {
|
||||||
|
token := t.peek()
|
||||||
|
if token.typ != itemText {
|
||||||
|
t.unexpected(token, "input")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t.next()
|
||||||
|
stripped := strings.TrimLeft(token.val, "\n\r")
|
||||||
|
diff := len(token.val) - len(stripped)
|
||||||
|
if diff > 0 {
|
||||||
|
// This is a bit nasty. We mutate the token in-place to remove
|
||||||
|
// preceding newlines.
|
||||||
|
token.pos += Pos(diff)
|
||||||
|
token.val = stripped
|
||||||
|
}
|
||||||
|
return t.newText(token.pos, token.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action:
|
||||||
|
// control
|
||||||
|
// command ("|" command)*
|
||||||
|
// Left delim is past. Now get actions.
|
||||||
|
// First word could be a keyword such as range.
|
||||||
|
func (t *Tree) action() (n Node) {
|
||||||
|
switch token := t.nextNonSpace(); token.typ {
|
||||||
|
case itemElse:
|
||||||
|
return t.elseControl()
|
||||||
|
case itemEnd:
|
||||||
|
return t.endControl()
|
||||||
|
case itemIf:
|
||||||
|
return t.ifControl()
|
||||||
|
case itemRange:
|
||||||
|
return t.rangeControl()
|
||||||
|
case itemTemplate:
|
||||||
|
return t.templateControl()
|
||||||
|
case itemWith:
|
||||||
|
return t.withControl()
|
||||||
|
}
|
||||||
|
t.backup()
|
||||||
|
// Do not pop variables; they persist until "end".
|
||||||
|
return t.newAction(t.peek().pos, t.lex.lineNumber(), t.pipeline("command"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pipeline:
|
||||||
|
// declarations? command ('|' command)*
|
||||||
|
func (t *Tree) pipeline(context string) (pipe *PipeNode) {
|
||||||
|
var decl []*VariableNode
|
||||||
|
pos := t.peekNonSpace().pos
|
||||||
|
// Are there declarations?
|
||||||
|
for {
|
||||||
|
if v := t.peekNonSpace(); v.typ == itemVariable {
|
||||||
|
t.next()
|
||||||
|
// Since space is a token, we need 3-token look-ahead here in the worst case:
|
||||||
|
// in "$x foo" we need to read "foo" (as opposed to ":=") to know that $x is an
|
||||||
|
// argument variable rather than a declaration. So remember the token
|
||||||
|
// adjacent to the variable so we can push it back if necessary.
|
||||||
|
tokenAfterVariable := t.peek()
|
||||||
|
if next := t.peekNonSpace(); next.typ == itemColonEquals || (next.typ == itemChar && next.val == ",") {
|
||||||
|
t.nextNonSpace()
|
||||||
|
variable := t.newVariable(v.pos, v.val)
|
||||||
|
decl = append(decl, variable)
|
||||||
|
t.vars = append(t.vars, v.val)
|
||||||
|
if next.typ == itemChar && next.val == "," {
|
||||||
|
if context == "range" && len(decl) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.errorf("too many declarations in %s", context)
|
||||||
|
}
|
||||||
|
} else if tokenAfterVariable.typ == itemSpace {
|
||||||
|
t.backup3(v, tokenAfterVariable)
|
||||||
|
} else {
|
||||||
|
t.backup2(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pipe = t.newPipeline(pos, t.lex.lineNumber(), decl)
|
||||||
|
for {
|
||||||
|
switch token := t.nextNonSpace(); token.typ {
|
||||||
|
case itemRightDelim, itemRightParen:
|
||||||
|
if len(pipe.Cmds) == 0 {
|
||||||
|
t.errorf("missing value for %s", context)
|
||||||
|
}
|
||||||
|
if token.typ == itemRightParen {
|
||||||
|
t.backup()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case itemBool, itemCharConstant, itemComplex, itemDot, itemField, itemIdentifier,
|
||||||
|
itemNumber, itemNil, itemRawString, itemString, itemVariable, itemLeftParen:
|
||||||
|
t.backup()
|
||||||
|
pipe.append(t.command())
|
||||||
|
default:
|
||||||
|
t.unexpected(token, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
|
||||||
|
defer t.popVars(len(t.vars))
|
||||||
|
line = t.lex.lineNumber()
|
||||||
|
pipe = t.pipeline(context)
|
||||||
|
var next Node
|
||||||
|
list, next = t.itemList()
|
||||||
|
switch next.Type() {
|
||||||
|
case nodeEnd: //done
|
||||||
|
case nodeElse:
|
||||||
|
if allowElseIf {
|
||||||
|
// Special case for "else if". If the "else" is followed immediately by an "if",
|
||||||
|
// the elseControl will have left the "if" token pending. Treat
|
||||||
|
// {{if a}}_{{else if b}}_{{end}}
|
||||||
|
// as
|
||||||
|
// {{if a}}_{{else}}{{if b}}_{{end}}{{end}}.
|
||||||
|
// To do this, parse the if as usual and stop at it {{end}}; the subsequent{{end}}
|
||||||
|
// is assumed. This technique works even for long if-else-if chains.
|
||||||
|
// TODO: Should we allow else-if in with and range?
|
||||||
|
if t.peek().typ == itemIf {
|
||||||
|
t.next() // Consume the "if" token.
|
||||||
|
elseList = t.newList(next.Position())
|
||||||
|
elseList.append(t.ifControl())
|
||||||
|
// Do not consume the next item - only one {{end}} required.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseList, next = t.itemList()
|
||||||
|
if next.Type() != nodeEnd {
|
||||||
|
t.errorf("expected end; found %s", next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pipe.Position(), line, pipe, list, elseList
|
||||||
|
}
|
||||||
|
|
||||||
|
// If:
|
||||||
|
// {{if pipeline}} itemList {{end}}
|
||||||
|
// {{if pipeline}} itemList {{else}} itemList {{end}}
|
||||||
|
// If keyword is past.
|
||||||
|
func (t *Tree) ifControl() Node {
|
||||||
|
return t.newIf(t.parseControl(true, "if"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range:
|
||||||
|
// {{range pipeline}} itemList {{end}}
|
||||||
|
// {{range pipeline}} itemList {{else}} itemList {{end}}
|
||||||
|
// Range keyword is past.
|
||||||
|
func (t *Tree) rangeControl() Node {
|
||||||
|
return t.newRange(t.parseControl(false, "range"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// With:
|
||||||
|
// {{with pipeline}} itemList {{end}}
|
||||||
|
// {{with pipeline}} itemList {{else}} itemList {{end}}
|
||||||
|
// If keyword is past.
|
||||||
|
func (t *Tree) withControl() Node {
|
||||||
|
return t.newWith(t.parseControl(false, "with"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// End:
|
||||||
|
// {{end}}
|
||||||
|
// End keyword is past.
|
||||||
|
func (t *Tree) endControl() Node {
|
||||||
|
return t.newEnd(t.expect(itemRightDelim, "end").pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else:
|
||||||
|
// {{else}}
|
||||||
|
// Else keyword is past.
|
||||||
|
func (t *Tree) elseControl() Node {
|
||||||
|
// Special case for "else if".
|
||||||
|
peek := t.peekNonSpace()
|
||||||
|
if peek.typ == itemIf {
|
||||||
|
// We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ".
|
||||||
|
return t.newElse(peek.pos, t.lex.lineNumber())
|
||||||
|
}
|
||||||
|
return t.newElse(t.expect(itemRightDelim, "else").pos, t.lex.lineNumber())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template:
|
||||||
|
// {{template stringValue pipeline}}
|
||||||
|
// Template keyword is past. The name must be something that can evaluate
|
||||||
|
// to a string.
|
||||||
|
func (t *Tree) templateControl() Node {
|
||||||
|
var name string
|
||||||
|
token := t.nextNonSpace()
|
||||||
|
switch token.typ {
|
||||||
|
case itemString, itemRawString:
|
||||||
|
s, err := strconv.Unquote(token.val)
|
||||||
|
if err != nil {
|
||||||
|
t.error(err)
|
||||||
|
}
|
||||||
|
name = s
|
||||||
|
default:
|
||||||
|
t.unexpected(token, "template invocation")
|
||||||
|
}
|
||||||
|
var pipe *PipeNode
|
||||||
|
if t.nextNonSpace().typ != itemRightDelim {
|
||||||
|
t.backup()
|
||||||
|
// Do not pop variables; they persist until "end".
|
||||||
|
pipe = t.pipeline("template")
|
||||||
|
}
|
||||||
|
return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe)
|
||||||
|
}
|
||||||
|
|
||||||
|
// command:
|
||||||
|
// operand (space operand)*
|
||||||
|
// space-separated arguments up to a pipeline character or right delimiter.
|
||||||
|
// we consume the pipe character but leave the right delim to terminate the action.
|
||||||
|
func (t *Tree) command() *CommandNode {
|
||||||
|
cmd := t.newCommand(t.peekNonSpace().pos)
|
||||||
|
for {
|
||||||
|
t.peekNonSpace() // skip leading spaces.
|
||||||
|
operand := t.operand()
|
||||||
|
if operand != nil {
|
||||||
|
cmd.append(operand)
|
||||||
|
}
|
||||||
|
switch token := t.next(); token.typ {
|
||||||
|
case itemSpace:
|
||||||
|
continue
|
||||||
|
case itemError:
|
||||||
|
t.errorf("%s", token.val)
|
||||||
|
case itemRightDelim, itemRightParen:
|
||||||
|
t.backup()
|
||||||
|
case itemPipe:
|
||||||
|
default:
|
||||||
|
t.errorf("unexpected %s in operand; missing space?", token)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if len(cmd.Args) == 0 {
|
||||||
|
t.errorf("empty command")
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// operand:
|
||||||
|
// term .Field*
|
||||||
|
// An operand is a space-separated component of a command,
|
||||||
|
// a term possibly followed by field accesses.
|
||||||
|
// A nil return means the next item is not an operand.
|
||||||
|
func (t *Tree) operand() Node {
|
||||||
|
node := t.term()
|
||||||
|
if node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if t.peek().typ == itemField {
|
||||||
|
chain := t.newChain(t.peek().pos, node)
|
||||||
|
for t.peek().typ == itemField {
|
||||||
|
chain.Add(t.next().val)
|
||||||
|
}
|
||||||
|
// Compatibility with original API: If the term is of type NodeField
|
||||||
|
// or NodeVariable, just put more fields on the original.
|
||||||
|
// Otherwise, keep the Chain node.
|
||||||
|
// TODO: Switch to Chains always when we can.
|
||||||
|
switch node.Type() {
|
||||||
|
case NodeField:
|
||||||
|
node = t.newField(chain.Position(), chain.String())
|
||||||
|
case NodeVariable:
|
||||||
|
node = t.newVariable(chain.Position(), chain.String())
|
||||||
|
default:
|
||||||
|
node = chain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
// term:
|
||||||
|
// literal (number, string, nil, boolean)
|
||||||
|
// function (identifier)
|
||||||
|
// .
|
||||||
|
// .Field
|
||||||
|
// $
|
||||||
|
// '(' pipeline ')'
|
||||||
|
// A term is a simple "expression".
|
||||||
|
// A nil return means the next item is not a term.
|
||||||
|
func (t *Tree) term() Node {
|
||||||
|
switch token := t.nextNonSpace(); token.typ {
|
||||||
|
case itemError:
|
||||||
|
t.errorf("%s", token.val)
|
||||||
|
case itemIdentifier:
|
||||||
|
if !t.hasFunction(token.val) {
|
||||||
|
t.errorf("function %q not defined", token.val)
|
||||||
|
}
|
||||||
|
return NewIdentifier(token.val).SetTree(t).SetPos(token.pos)
|
||||||
|
case itemDot:
|
||||||
|
return t.newDot(token.pos)
|
||||||
|
case itemNil:
|
||||||
|
return t.newNil(token.pos)
|
||||||
|
case itemVariable:
|
||||||
|
return t.useVar(token.pos, token.val)
|
||||||
|
case itemField:
|
||||||
|
return t.newField(token.pos, token.val)
|
||||||
|
case itemBool:
|
||||||
|
return t.newBool(token.pos, token.val == "true")
|
||||||
|
case itemCharConstant, itemComplex, itemNumber:
|
||||||
|
number, err := t.newNumber(token.pos, token.val, token.typ)
|
||||||
|
if err != nil {
|
||||||
|
t.error(err)
|
||||||
|
}
|
||||||
|
return number
|
||||||
|
case itemLeftParen:
|
||||||
|
pipe := t.pipeline("parenthesized pipeline")
|
||||||
|
if token := t.next(); token.typ != itemRightParen {
|
||||||
|
t.errorf("unclosed right paren: unexpected %s", token)
|
||||||
|
}
|
||||||
|
return pipe
|
||||||
|
case itemString, itemRawString:
|
||||||
|
s, err := strconv.Unquote(token.val)
|
||||||
|
if err != nil {
|
||||||
|
t.error(err)
|
||||||
|
}
|
||||||
|
return t.newString(token.pos, token.val, s)
|
||||||
|
}
|
||||||
|
t.backup()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasFunction reports if a function name exists in the Tree's maps.
|
||||||
|
func (t *Tree) hasFunction(name string) bool {
|
||||||
|
for _, funcMap := range t.funcs {
|
||||||
|
if funcMap == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if funcMap[name] != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// popVars trims the variable list to the specified length
|
||||||
|
func (t *Tree) popVars(n int) {
|
||||||
|
t.vars = t.vars[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
// useVar returns a node for a variable reference. It errors if the
|
||||||
|
// variable is not defined.
|
||||||
|
func (t *Tree) useVar(pos Pos, name string) Node {
|
||||||
|
v := t.newVariable(pos, name)
|
||||||
|
for _, varName := range t.vars {
|
||||||
|
if varName == v.Ident[0] {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.errorf("undefined variable %q", v.Ident[0])
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,426 @@
|
||||||
|
// 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 (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var debug = flag.Bool("debug", false, "show the errors produced by the main tests")
|
||||||
|
|
||||||
|
type numberTest struct {
|
||||||
|
text string
|
||||||
|
isInt bool
|
||||||
|
isUint bool
|
||||||
|
isFloat bool
|
||||||
|
isComplex bool
|
||||||
|
int64
|
||||||
|
uint64
|
||||||
|
float64
|
||||||
|
complex128
|
||||||
|
}
|
||||||
|
|
||||||
|
var numberTests = []numberTest{
|
||||||
|
// basics
|
||||||
|
{"0", true, true, true, false, 0, 0, 0, 0},
|
||||||
|
{"-0", true, true, true, false, 0, 0, 0, 0}, // check that -0 is a uint.
|
||||||
|
{"73", true, true, true, false, 73, 73, 73, 0},
|
||||||
|
{"073", true, true, true, false, 073, 073, 073, 0},
|
||||||
|
{"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||||
|
{"-73", true, false, true, false, -73, 0, -73, 0},
|
||||||
|
{"+73", true, false, true, false, 73, 0, 73, 0},
|
||||||
|
{"100", true, true, true, false, 100, 100, 100, 0},
|
||||||
|
{"1e9", true, true, true, false, 1e9, 1e9, 1e9, 0},
|
||||||
|
{"-1e9", true, false, true, false, -1e9, 0, -1e9, 0},
|
||||||
|
{"-1.2", false, false, true, false, 0, 0, -1.2, 0},
|
||||||
|
{"1e19", false, true, true, false, 0, 1e19, 1e19, 0},
|
||||||
|
{"-1e19", false, false, true, false, 0, 0, -1e19, 0},
|
||||||
|
{"4i", false, false, false, true, 0, 0, 0, 4i},
|
||||||
|
{"-1.2+4.2i", false, false, false, true, 0, 0, 0, -1.2 + 4.2i},
|
||||||
|
{"073i", false, false, false, true, 0, 0, 0, 73i}, // not octal!
|
||||||
|
// complex with 0 imaginary are float (and maybe integer)
|
||||||
|
{"0i", true, true, true, true, 0, 0, 0, 0},
|
||||||
|
{"-1.2+0i", false, false, true, true, 0, 0, -1.2, -1.2},
|
||||||
|
{"-12+0i", true, false, true, true, -12, 0, -12, -12},
|
||||||
|
{"13+0i", true, true, true, true, 13, 13, 13, 13},
|
||||||
|
// funny bases
|
||||||
|
{"0123", true, true, true, false, 0123, 0123, 0123, 0},
|
||||||
|
{"-0x0", true, true, true, false, 0, 0, 0, 0},
|
||||||
|
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
|
||||||
|
// character constants
|
||||||
|
{`'a'`, true, true, true, false, 'a', 'a', 'a', 0},
|
||||||
|
{`'\n'`, true, true, true, false, '\n', '\n', '\n', 0},
|
||||||
|
{`'\\'`, true, true, true, false, '\\', '\\', '\\', 0},
|
||||||
|
{`'\''`, true, true, true, false, '\'', '\'', '\'', 0},
|
||||||
|
{`'\xFF'`, true, true, true, false, 0xFF, 0xFF, 0xFF, 0},
|
||||||
|
{`'パ'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||||
|
{`'\u30d1'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||||
|
{`'\U000030d1'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||||
|
// some broken syntax
|
||||||
|
{text: "+-2"},
|
||||||
|
{text: "0x123."},
|
||||||
|
{text: "1e."},
|
||||||
|
{text: "0xi."},
|
||||||
|
{text: "1+2."},
|
||||||
|
{text: "'x"},
|
||||||
|
{text: "'xx'"},
|
||||||
|
// Issue 8622 - 0xe parsed as floating point. Very embarrassing.
|
||||||
|
{"0xef", true, true, true, false, 0xef, 0xef, 0xef, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumberParse(t *testing.T) {
|
||||||
|
for _, test := range numberTests {
|
||||||
|
// If fmt.Sscan thinks it's complex, it's complex. We can't trust the output
|
||||||
|
// because imaginary comes out as a number.
|
||||||
|
var c complex128
|
||||||
|
typ := itemNumber
|
||||||
|
var tree *Tree
|
||||||
|
if test.text[0] == '\'' {
|
||||||
|
typ = itemCharConstant
|
||||||
|
} else {
|
||||||
|
_, err := fmt.Sscan(test.text, &c)
|
||||||
|
if err == nil {
|
||||||
|
typ = itemComplex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n, err := tree.newNumber(0, test.text, typ)
|
||||||
|
ok := test.isInt || test.isUint || test.isFloat || test.isComplex
|
||||||
|
if ok && err != nil {
|
||||||
|
t.Errorf("unexpected error for %q: %s", test.text, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !ok && err == nil {
|
||||||
|
t.Errorf("expected error for %q", test.text)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
if *debug {
|
||||||
|
fmt.Printf("%s\n\t%s\n", test.text, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n.IsComplex != test.isComplex {
|
||||||
|
t.Errorf("complex incorrect for %q; should be %t", test.text, test.isComplex)
|
||||||
|
}
|
||||||
|
if test.isInt {
|
||||||
|
if !n.IsInt {
|
||||||
|
t.Errorf("expected integer for %q", test.text)
|
||||||
|
}
|
||||||
|
if n.Int64 != test.int64 {
|
||||||
|
t.Errorf("int64 for %q should be %d Is %d", test.text, test.int64, n.Int64)
|
||||||
|
}
|
||||||
|
} else if n.IsInt {
|
||||||
|
t.Errorf("did not expect integer for %q", test.text)
|
||||||
|
}
|
||||||
|
if test.isUint {
|
||||||
|
if !n.IsUint {
|
||||||
|
t.Errorf("expected unsigned integer for %q", test.text)
|
||||||
|
}
|
||||||
|
if n.Uint64 != test.uint64 {
|
||||||
|
t.Errorf("uint64 for %q should be %d Is %d", test.text, test.uint64, n.Uint64)
|
||||||
|
}
|
||||||
|
} else if n.IsUint {
|
||||||
|
t.Errorf("did not expect unsigned integer for %q", test.text)
|
||||||
|
}
|
||||||
|
if test.isFloat {
|
||||||
|
if !n.IsFloat {
|
||||||
|
t.Errorf("expected float for %q", test.text)
|
||||||
|
}
|
||||||
|
if n.Float64 != test.float64 {
|
||||||
|
t.Errorf("float64 for %q should be %g Is %g", test.text, test.float64, n.Float64)
|
||||||
|
}
|
||||||
|
} else if n.IsFloat {
|
||||||
|
t.Errorf("did not expect float for %q", test.text)
|
||||||
|
}
|
||||||
|
if test.isComplex {
|
||||||
|
if !n.IsComplex {
|
||||||
|
t.Errorf("expected complex for %q", test.text)
|
||||||
|
}
|
||||||
|
if n.Complex128 != test.complex128 {
|
||||||
|
t.Errorf("complex128 for %q should be %g Is %g", test.text, test.complex128, n.Complex128)
|
||||||
|
}
|
||||||
|
} else if n.IsComplex {
|
||||||
|
t.Errorf("did not expect complex for %q", test.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type parseTest struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
ok bool
|
||||||
|
result string // what the user would see in an error message.
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
noError = true
|
||||||
|
hasError = false
|
||||||
|
)
|
||||||
|
|
||||||
|
var parseTests = []parseTest{
|
||||||
|
{"empty", "", noError,
|
||||||
|
``},
|
||||||
|
{"comment", "{{/*\n\n\n*/}}", noError,
|
||||||
|
``},
|
||||||
|
{"spaces", " \t\n", noError,
|
||||||
|
`" \t\n"`},
|
||||||
|
{"text", "some text", noError,
|
||||||
|
`"some text"`},
|
||||||
|
{"emptyAction", "{{}}", hasError,
|
||||||
|
`{{}}`},
|
||||||
|
{"field", "{{.X}}", noError,
|
||||||
|
`{{.X}}`},
|
||||||
|
{"simple command", "{{printf}}", noError,
|
||||||
|
`{{printf}}`},
|
||||||
|
{"$ invocation", "{{$}}", noError,
|
||||||
|
"{{$}}"},
|
||||||
|
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
|
||||||
|
"{{with $x := 3}}{{$x 23}}{{end}}"},
|
||||||
|
{"variable with fields", "{{$.I}}", noError,
|
||||||
|
"{{$.I}}"},
|
||||||
|
{"multi-word command", "{{printf `%d` 23}}", noError,
|
||||||
|
"{{printf `%d` 23}}"},
|
||||||
|
{"pipeline", "{{.X|.Y}}", noError,
|
||||||
|
`{{.X | .Y}}`},
|
||||||
|
{"pipeline with decl", "{{$x := .X|.Y}}", noError,
|
||||||
|
`{{$x := .X | .Y}}`},
|
||||||
|
{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
|
||||||
|
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
|
||||||
|
{"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
|
||||||
|
`{{(.Y .Z).Field}}`},
|
||||||
|
{"simple if", "{{if .X}}hello{{end}}", noError,
|
||||||
|
`{{if .X}}"hello"{{end}}`},
|
||||||
|
{"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
|
||||||
|
`{{if .X}}"true"{{else}}"false"{{end}}`},
|
||||||
|
{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
|
||||||
|
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`},
|
||||||
|
{"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
|
||||||
|
`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},
|
||||||
|
{"simple range", "{{range .X}}hello{{end}}", noError,
|
||||||
|
`{{range .X}}"hello"{{end}}`},
|
||||||
|
{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
|
||||||
|
`{{range .X.Y.Z}}"hello"{{end}}`},
|
||||||
|
{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
|
||||||
|
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`},
|
||||||
|
{"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
|
||||||
|
`{{range .X}}"true"{{else}}"false"{{end}}`},
|
||||||
|
{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
|
||||||
|
`{{range .X | .M}}"true"{{else}}"false"{{end}}`},
|
||||||
|
{"range []int", "{{range .SI}}{{.}}{{end}}", noError,
|
||||||
|
`{{range .SI}}{{.}}{{end}}`},
|
||||||
|
{"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
|
||||||
|
`{{range $x := .SI}}{{.}}{{end}}`},
|
||||||
|
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
|
||||||
|
`{{range $x, $y := .SI}}{{.}}{{end}}`},
|
||||||
|
{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
|
||||||
|
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
|
||||||
|
{"template", "{{template `x`}}", noError,
|
||||||
|
`{{template "x"}}`},
|
||||||
|
{"template with arg", "{{template `x` .Y}}", noError,
|
||||||
|
`{{template "x" .Y}}`},
|
||||||
|
{"with", "{{with .X}}hello{{end}}", noError,
|
||||||
|
`{{with .X}}"hello"{{end}}`},
|
||||||
|
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
|
||||||
|
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
|
||||||
|
{"elide newline", "{{true}}\\\n ", noError,
|
||||||
|
`{{true}}" "`},
|
||||||
|
// Errors.
|
||||||
|
{"unclosed action", "hello{{range", hasError, ""},
|
||||||
|
{"unmatched end", "{{end}}", hasError, ""},
|
||||||
|
{"missing end", "hello{{range .x}}", hasError, ""},
|
||||||
|
{"missing end after else", "hello{{range .x}}{{else}}", hasError, ""},
|
||||||
|
{"undefined function", "hello{{undefined}}", hasError, ""},
|
||||||
|
{"undefined variable", "{{$x}}", hasError, ""},
|
||||||
|
{"variable undefined after end", "{{with $x := 4}}{{end}}{{$x}}", hasError, ""},
|
||||||
|
{"variable undefined in template", "{{template $v}}", hasError, ""},
|
||||||
|
{"declare with field", "{{with $x.Y := 4}}{{end}}", hasError, ""},
|
||||||
|
{"template with field ref", "{{template .X}}", hasError, ""},
|
||||||
|
{"template with var", "{{template $v}}", hasError, ""},
|
||||||
|
{"invalid punctuation", "{{printf 3, 4}}", hasError, ""},
|
||||||
|
{"multidecl outside range", "{{with $v, $u := 3}}{{end}}", hasError, ""},
|
||||||
|
{"too many decls in range", "{{range $u, $v, $w := 3}}{{end}}", hasError, ""},
|
||||||
|
{"dot applied to parentheses", "{{printf (printf .).}}", hasError, ""},
|
||||||
|
{"adjacent args", "{{printf 3`x`}}", hasError, ""},
|
||||||
|
{"adjacent args with .", "{{printf `x`.}}", hasError, ""},
|
||||||
|
{"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", hasError, ""},
|
||||||
|
{"invalid newline elision", "{{true}}\\{{true}}", hasError, ""},
|
||||||
|
// Equals (and other chars) do not assignments make (yet).
|
||||||
|
{"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"},
|
||||||
|
{"bug0b", "{{$x = 1}}{{$x}}", hasError, ""},
|
||||||
|
{"bug0c", "{{$x ! 2}}{{$x}}", hasError, ""},
|
||||||
|
{"bug0d", "{{$x % 3}}{{$x}}", hasError, ""},
|
||||||
|
// Check the parse fails for := rather than comma.
|
||||||
|
{"bug0e", "{{range $x := $y := 3}}{{end}}", hasError, ""},
|
||||||
|
// Another bug: variable read must ignore following punctuation.
|
||||||
|
{"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! is just illegal here.
|
||||||
|
{"bug1b", "{{$x:=.}}{{$x+2}}", hasError, ""}, // $x+2 should not parse as ($x) (+2).
|
||||||
|
{"bug1c", "{{$x:=.}}{{$x +2}}", noError, "{{$x := .}}{{$x +2}}"}, // It's OK with a space.
|
||||||
|
}
|
||||||
|
|
||||||
|
var builtins = map[string]interface{}{
|
||||||
|
"printf": fmt.Sprintf,
|
||||||
|
}
|
||||||
|
|
||||||
|
func testParse(doCopy bool, t *testing.T) {
|
||||||
|
textFormat = "%q"
|
||||||
|
defer func() { textFormat = "%s" }()
|
||||||
|
for _, test := range parseTests {
|
||||||
|
tmpl, err := New(test.name).Parse(test.input, "", "", make(map[string]*Tree), builtins)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
var result string
|
||||||
|
if doCopy {
|
||||||
|
result = tmpl.Root.Copy().String()
|
||||||
|
} else {
|
||||||
|
result = tmpl.Root.String()
|
||||||
|
}
|
||||||
|
if result != test.result {
|
||||||
|
t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, result, test.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
testParse(false, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as TestParse, but we copy the node first
|
||||||
|
func TestParseCopy(t *testing.T) {
|
||||||
|
testParse(true, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
type isEmptyTest struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
empty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEmptyTests = []isEmptyTest{
|
||||||
|
{"empty", ``, true},
|
||||||
|
{"nonempty", `hello`, false},
|
||||||
|
{"spaces only", " \t\n \t\n", true},
|
||||||
|
{"definition", `{{define "x"}}something{{end}}`, true},
|
||||||
|
{"definitions and space", "{{define `x`}}something{{end}}\n\n{{define `y`}}something{{end}}\n\n", true},
|
||||||
|
{"definitions and text", "{{define `x`}}something{{end}}\nx\n{{define `y`}}something{{end}}\ny\n", false},
|
||||||
|
{"definition and action", "{{define `x`}}something{{end}}{{if 3}}foo{{end}}", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsEmpty(t *testing.T) {
|
||||||
|
if !IsEmptyTree(nil) {
|
||||||
|
t.Errorf("nil tree is not empty")
|
||||||
|
}
|
||||||
|
for _, test := range isEmptyTests {
|
||||||
|
tree, err := New("root").Parse(test.input, "", "", make(map[string]*Tree), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%q: unexpected error: %v", test.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if empty := IsEmptyTree(tree.Root); empty != test.empty {
|
||||||
|
t.Errorf("%q: expected %t got %t", test.name, test.empty, empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorContextWithTreeCopy(t *testing.T) {
|
||||||
|
tree, err := New("root").Parse("{{if true}}{{end}}", "", "", make(map[string]*Tree), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected tree parse failure: %v", err)
|
||||||
|
}
|
||||||
|
treeCopy := tree.Copy()
|
||||||
|
wantLocation, wantContext := tree.ErrorContext(tree.Root.Nodes[0])
|
||||||
|
gotLocation, gotContext := treeCopy.ErrorContext(treeCopy.Root.Nodes[0])
|
||||||
|
if wantLocation != gotLocation {
|
||||||
|
t.Errorf("wrong error location want %q got %q", wantLocation, gotLocation)
|
||||||
|
}
|
||||||
|
if wantContext != gotContext {
|
||||||
|
t.Errorf("wrong error location want %q got %q", wantContext, gotContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All failures, and the result is a string that must appear in the error message.
|
||||||
|
var errorTests = []parseTest{
|
||||||
|
// Check line numbers are accurate.
|
||||||
|
{"unclosed1",
|
||||||
|
"line1\n{{",
|
||||||
|
hasError, `unclosed1:2: unexpected unclosed action in command`},
|
||||||
|
{"unclosed2",
|
||||||
|
"line1\n{{define `x`}}line2\n{{",
|
||||||
|
hasError, `unclosed2:3: unexpected unclosed action in command`},
|
||||||
|
// Specific errors.
|
||||||
|
{"function",
|
||||||
|
"{{foo}}",
|
||||||
|
hasError, `function "foo" not defined`},
|
||||||
|
{"comment",
|
||||||
|
"{{/*}}",
|
||||||
|
hasError, `unclosed comment`},
|
||||||
|
{"lparen",
|
||||||
|
"{{.X (1 2 3}}",
|
||||||
|
hasError, `unclosed left paren`},
|
||||||
|
{"rparen",
|
||||||
|
"{{.X 1 2 3)}}",
|
||||||
|
hasError, `unexpected ")"`},
|
||||||
|
{"space",
|
||||||
|
"{{`x`3}}",
|
||||||
|
hasError, `missing space?`},
|
||||||
|
{"idchar",
|
||||||
|
"{{a#}}",
|
||||||
|
hasError, `'#'`},
|
||||||
|
{"charconst",
|
||||||
|
"{{'a}}",
|
||||||
|
hasError, `unterminated character constant`},
|
||||||
|
{"stringconst",
|
||||||
|
`{{"a}}`,
|
||||||
|
hasError, `unterminated quoted string`},
|
||||||
|
{"rawstringconst",
|
||||||
|
"{{`a}}",
|
||||||
|
hasError, `unterminated raw quoted string`},
|
||||||
|
{"number",
|
||||||
|
"{{0xi}}",
|
||||||
|
hasError, `number syntax`},
|
||||||
|
{"multidefine",
|
||||||
|
"{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
|
||||||
|
hasError, `multiple definition of template`},
|
||||||
|
{"eof",
|
||||||
|
"{{range .X}}",
|
||||||
|
hasError, `unexpected EOF`},
|
||||||
|
{"variable",
|
||||||
|
// Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration.
|
||||||
|
"{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
|
||||||
|
hasError, `unexpected ":="`},
|
||||||
|
{"multidecl",
|
||||||
|
"{{$a,$b,$c := 23}}",
|
||||||
|
hasError, `too many declarations`},
|
||||||
|
{"undefvar",
|
||||||
|
"{{$a}}",
|
||||||
|
hasError, `undefined variable`},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrors(t *testing.T) {
|
||||||
|
for _, test := range errorTests {
|
||||||
|
_, err := New(test.name).Parse(test.input, "", "", make(map[string]*Tree))
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("%q: expected error", test.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), test.result) {
|
||||||
|
t.Errorf("%q: error %q does not contain %q", test.name, err, test.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/alecthomas/template/parse"
|
||||||
|
)
|
||||||
|
|
||||||
|
// common holds the information shared by related templates.
|
||||||
|
type common struct {
|
||||||
|
tmpl map[string]*Template
|
||||||
|
// We use two maps, one for parsing and one for execution.
|
||||||
|
// This separation makes the API cleaner since it doesn't
|
||||||
|
// expose reflection to the client.
|
||||||
|
parseFuncs FuncMap
|
||||||
|
execFuncs map[string]reflect.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template is the representation of a parsed template. The *parse.Tree
|
||||||
|
// field is exported only for use by html/template and should be treated
|
||||||
|
// as unexported by all other clients.
|
||||||
|
type Template struct {
|
||||||
|
name string
|
||||||
|
*parse.Tree
|
||||||
|
*common
|
||||||
|
leftDelim string
|
||||||
|
rightDelim string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New allocates a new template with the given name.
|
||||||
|
func New(name string) *Template {
|
||||||
|
return &Template{
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the template.
|
||||||
|
func (t *Template) Name() string {
|
||||||
|
return t.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// New allocates a new template associated with the given one and with the same
|
||||||
|
// delimiters. The association, which is transitive, allows one template to
|
||||||
|
// invoke another with a {{template}} action.
|
||||||
|
func (t *Template) New(name string) *Template {
|
||||||
|
t.init()
|
||||||
|
return &Template{
|
||||||
|
name: name,
|
||||||
|
common: t.common,
|
||||||
|
leftDelim: t.leftDelim,
|
||||||
|
rightDelim: t.rightDelim,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Template) init() {
|
||||||
|
if t.common == nil {
|
||||||
|
t.common = new(common)
|
||||||
|
t.tmpl = make(map[string]*Template)
|
||||||
|
t.parseFuncs = make(FuncMap)
|
||||||
|
t.execFuncs = make(map[string]reflect.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a duplicate of the template, including all associated
|
||||||
|
// templates. The actual representation is not copied, but the name space of
|
||||||
|
// associated templates is, so further calls to Parse in the copy will add
|
||||||
|
// templates to the copy but not to the original. Clone can be used to prepare
|
||||||
|
// common templates and use them with variant definitions for other templates
|
||||||
|
// by adding the variants after the clone is made.
|
||||||
|
func (t *Template) Clone() (*Template, error) {
|
||||||
|
nt := t.copy(nil)
|
||||||
|
nt.init()
|
||||||
|
nt.tmpl[t.name] = nt
|
||||||
|
for k, v := range t.tmpl {
|
||||||
|
if k == t.name { // Already installed.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// The associated templates share nt's common structure.
|
||||||
|
tmpl := v.copy(nt.common)
|
||||||
|
nt.tmpl[k] = tmpl
|
||||||
|
}
|
||||||
|
for k, v := range t.parseFuncs {
|
||||||
|
nt.parseFuncs[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range t.execFuncs {
|
||||||
|
nt.execFuncs[k] = v
|
||||||
|
}
|
||||||
|
return nt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy returns a shallow copy of t, with common set to the argument.
|
||||||
|
func (t *Template) copy(c *common) *Template {
|
||||||
|
nt := New(t.name)
|
||||||
|
nt.Tree = t.Tree
|
||||||
|
nt.common = c
|
||||||
|
nt.leftDelim = t.leftDelim
|
||||||
|
nt.rightDelim = t.rightDelim
|
||||||
|
return nt
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddParseTree creates a new template with the name and parse tree
|
||||||
|
// and associates it with t.
|
||||||
|
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
|
||||||
|
if t.common != nil && t.tmpl[name] != nil {
|
||||||
|
return nil, fmt.Errorf("template: redefinition of template %q", name)
|
||||||
|
}
|
||||||
|
nt := t.New(name)
|
||||||
|
nt.Tree = tree
|
||||||
|
t.tmpl[name] = nt
|
||||||
|
return nt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Templates returns a slice of the templates associated with t, including t
|
||||||
|
// itself.
|
||||||
|
func (t *Template) Templates() []*Template {
|
||||||
|
if t.common == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Return a slice so we don't expose the map.
|
||||||
|
m := make([]*Template, 0, len(t.tmpl))
|
||||||
|
for _, v := range t.tmpl {
|
||||||
|
m = append(m, v)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delims sets the action delimiters to the specified strings, to be used in
|
||||||
|
// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
|
||||||
|
// definitions will inherit the settings. An empty delimiter stands for the
|
||||||
|
// corresponding default: {{ or }}.
|
||||||
|
// The return value is the template, so calls can be chained.
|
||||||
|
func (t *Template) Delims(left, right string) *Template {
|
||||||
|
t.leftDelim = left
|
||||||
|
t.rightDelim = right
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funcs adds the elements of the argument map to the template's function map.
|
||||||
|
// It panics if a value in the map is not a function with appropriate return
|
||||||
|
// type. However, it is legal to overwrite elements of the map. The return
|
||||||
|
// value is the template, so calls can be chained.
|
||||||
|
func (t *Template) Funcs(funcMap FuncMap) *Template {
|
||||||
|
t.init()
|
||||||
|
addValueFuncs(t.execFuncs, funcMap)
|
||||||
|
addFuncs(t.parseFuncs, funcMap)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup returns the template with the given name that is associated with t,
|
||||||
|
// or nil if there is no such template.
|
||||||
|
func (t *Template) Lookup(name string) *Template {
|
||||||
|
if t.common == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return t.tmpl[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses a string into a template. Nested template definitions will be
|
||||||
|
// associated with the top-level template t. Parse may be called multiple times
|
||||||
|
// to parse definitions of templates to associate with t. It is an error if a
|
||||||
|
// resulting template is non-empty (contains content other than template
|
||||||
|
// definitions) and would replace a non-empty template with the same name.
|
||||||
|
// (In multiple calls to Parse with the same receiver template, only one call
|
||||||
|
// can contain text other than space, comments, and template definitions.)
|
||||||
|
func (t *Template) Parse(text string) (*Template, error) {
|
||||||
|
t.init()
|
||||||
|
trees, err := parse.Parse(t.name, text, t.leftDelim, t.rightDelim, t.parseFuncs, builtins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Add the newly parsed trees, including the one for t, into our common structure.
|
||||||
|
for name, tree := range trees {
|
||||||
|
// If the name we parsed is the name of this template, overwrite this template.
|
||||||
|
// The associate method checks it's not a redefinition.
|
||||||
|
tmpl := t
|
||||||
|
if name != t.name {
|
||||||
|
tmpl = t.New(name)
|
||||||
|
}
|
||||||
|
// Even if t == tmpl, we need to install it in the common.tmpl map.
|
||||||
|
if replace, err := t.associate(tmpl, tree); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if replace {
|
||||||
|
tmpl.Tree = tree
|
||||||
|
}
|
||||||
|
tmpl.leftDelim = t.leftDelim
|
||||||
|
tmpl.rightDelim = t.rightDelim
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// associate installs the new template into the group of templates associated
|
||||||
|
// with t. It is an error to reuse a name except to overwrite an empty
|
||||||
|
// template. The two are already known to share the common structure.
|
||||||
|
// The boolean return value reports wither to store this tree as t.Tree.
|
||||||
|
func (t *Template) associate(new *Template, tree *parse.Tree) (bool, error) {
|
||||||
|
if new.common != t.common {
|
||||||
|
panic("internal error: associate not common")
|
||||||
|
}
|
||||||
|
name := new.name
|
||||||
|
if old := t.tmpl[name]; old != nil {
|
||||||
|
oldIsEmpty := parse.IsEmptyTree(old.Root)
|
||||||
|
newIsEmpty := parse.IsEmptyTree(tree.Root)
|
||||||
|
if newIsEmpty {
|
||||||
|
// Whether old is empty or not, new is empty; no reason to replace old.
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if !oldIsEmpty {
|
||||||
|
return false, fmt.Errorf("template: redefinition of template %q", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.tmpl[name] = new
|
||||||
|
return true, nil
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
{{define "x"}}TEXT{{end}}
|
||||||
|
{{define "dotV"}}{{.V}}{{end}}
|
|
@ -0,0 +1,2 @@
|
||||||
|
{{define "dot"}}{{.}}{{end}}
|
||||||
|
{{define "nested"}}{{template "dot" .}}{{end}}
|
|
@ -0,0 +1,3 @@
|
||||||
|
template1
|
||||||
|
{{define "x"}}x{{end}}
|
||||||
|
{{template "y"}}
|
|
@ -0,0 +1,3 @@
|
||||||
|
template2
|
||||||
|
{{define "y"}}y{{end}}
|
||||||
|
{{template "x"}}
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (C) 2014 Alec Thomas
|
||||||
|
|
||||||
|
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,11 @@
|
||||||
|
# Units - Helpful unit multipliers and functions for Go
|
||||||
|
|
||||||
|
The goal of this package is to have functionality similar to the [time](http://golang.org/pkg/time/) package.
|
||||||
|
|
||||||
|
It allows for code like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
n, err := ParseBase2Bytes("1KB")
|
||||||
|
// n == 1024
|
||||||
|
n = units.Mebibyte * 512
|
||||||
|
```
|
|
@ -0,0 +1,83 @@
|
||||||
|
package units
|
||||||
|
|
||||||
|
// Base2Bytes is the old non-SI power-of-2 byte scale (1024 bytes in a kilobyte,
|
||||||
|
// etc.).
|
||||||
|
type Base2Bytes int64
|
||||||
|
|
||||||
|
// Base-2 byte units.
|
||||||
|
const (
|
||||||
|
Kibibyte Base2Bytes = 1024
|
||||||
|
KiB = Kibibyte
|
||||||
|
Mebibyte = Kibibyte * 1024
|
||||||
|
MiB = Mebibyte
|
||||||
|
Gibibyte = Mebibyte * 1024
|
||||||
|
GiB = Gibibyte
|
||||||
|
Tebibyte = Gibibyte * 1024
|
||||||
|
TiB = Tebibyte
|
||||||
|
Pebibyte = Tebibyte * 1024
|
||||||
|
PiB = Pebibyte
|
||||||
|
Exbibyte = Pebibyte * 1024
|
||||||
|
EiB = Exbibyte
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bytesUnitMap = MakeUnitMap("iB", "B", 1024)
|
||||||
|
oldBytesUnitMap = MakeUnitMap("B", "B", 1024)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseBase2Bytes supports both iB and B in base-2 multipliers. That is, KB
|
||||||
|
// and KiB are both 1024.
|
||||||
|
func ParseBase2Bytes(s string) (Base2Bytes, error) {
|
||||||
|
n, err := ParseUnit(s, bytesUnitMap)
|
||||||
|
if err != nil {
|
||||||
|
n, err = ParseUnit(s, oldBytesUnitMap)
|
||||||
|
}
|
||||||
|
return Base2Bytes(n), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Base2Bytes) String() string {
|
||||||
|
return ToString(int64(b), 1024, "iB", "B")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
metricBytesUnitMap = MakeUnitMap("B", "B", 1000)
|
||||||
|
)
|
||||||
|
|
||||||
|
// MetricBytes are SI byte units (1000 bytes in a kilobyte).
|
||||||
|
type MetricBytes SI
|
||||||
|
|
||||||
|
// SI base-10 byte units.
|
||||||
|
const (
|
||||||
|
Kilobyte MetricBytes = 1000
|
||||||
|
KB = Kilobyte
|
||||||
|
Megabyte = Kilobyte * 1000
|
||||||
|
MB = Megabyte
|
||||||
|
Gigabyte = Megabyte * 1000
|
||||||
|
GB = Gigabyte
|
||||||
|
Terabyte = Gigabyte * 1000
|
||||||
|
TB = Terabyte
|
||||||
|
Petabyte = Terabyte * 1000
|
||||||
|
PB = Petabyte
|
||||||
|
Exabyte = Petabyte * 1000
|
||||||
|
EB = Exabyte
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseMetricBytes parses base-10 metric byte units. That is, KB is 1000 bytes.
|
||||||
|
func ParseMetricBytes(s string) (MetricBytes, error) {
|
||||||
|
n, err := ParseUnit(s, metricBytesUnitMap)
|
||||||
|
return MetricBytes(n), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MetricBytes) String() string {
|
||||||
|
return ToString(int64(m), 1000, "B", "B")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseStrictBytes supports both iB and B suffixes for base 2 and metric,
|
||||||
|
// respectively. That is, KiB represents 1024 and KB represents 1000.
|
||||||
|
func ParseStrictBytes(s string) (int64, error) {
|
||||||
|
n, err := ParseUnit(s, bytesUnitMap)
|
||||||
|
if err != nil {
|
||||||
|
n, err = ParseUnit(s, metricBytesUnitMap)
|
||||||
|
}
|
||||||
|
return int64(n), err
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package units
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBase2BytesString(t *testing.T) {
|
||||||
|
assert.Equal(t, Base2Bytes(0).String(), "0B")
|
||||||
|
assert.Equal(t, Base2Bytes(1025).String(), "1KiB1B")
|
||||||
|
assert.Equal(t, Base2Bytes(1048577).String(), "1MiB1B")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBase2Bytes(t *testing.T) {
|
||||||
|
n, err := ParseBase2Bytes("0B")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, int(n))
|
||||||
|
n, err = ParseBase2Bytes("1KB")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1024, int(n))
|
||||||
|
n, err = ParseBase2Bytes("1MB1KB25B")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1049625, int(n))
|
||||||
|
n, err = ParseBase2Bytes("1.5MB")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1572864, int(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetricBytesString(t *testing.T) {
|
||||||
|
assert.Equal(t, MetricBytes(0).String(), "0B")
|
||||||
|
assert.Equal(t, MetricBytes(1001).String(), "1KB1B")
|
||||||
|
assert.Equal(t, MetricBytes(1001025).String(), "1MB1KB25B")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMetricBytes(t *testing.T) {
|
||||||
|
n, err := ParseMetricBytes("0B")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, int(n))
|
||||||
|
n, err = ParseMetricBytes("1KB1B")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1001, int(n))
|
||||||
|
n, err = ParseMetricBytes("1MB1KB25B")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1001025, int(n))
|
||||||
|
n, err = ParseMetricBytes("1.5MB")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1500000, int(n))
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Package units provides helpful unit multipliers and functions for Go.
|
||||||
|
//
|
||||||
|
// The goal of this package is to have functionality similar to the time [1] package.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// [1] http://golang.org/pkg/time/
|
||||||
|
//
|
||||||
|
// It allows for code like this:
|
||||||
|
//
|
||||||
|
// n, err := ParseBase2Bytes("1KB")
|
||||||
|
// // n == 1024
|
||||||
|
// n = units.Mebibyte * 512
|
||||||
|
package units
|
|
@ -0,0 +1,26 @@
|
||||||
|
package units
|
||||||
|
|
||||||
|
// SI units.
|
||||||
|
type SI int64
|
||||||
|
|
||||||
|
// SI unit multiples.
|
||||||
|
const (
|
||||||
|
Kilo SI = 1000
|
||||||
|
Mega = Kilo * 1000
|
||||||
|
Giga = Mega * 1000
|
||||||
|
Tera = Giga * 1000
|
||||||
|
Peta = Tera * 1000
|
||||||
|
Exa = Peta * 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeUnitMap(suffix, shortSuffix string, scale int64) map[string]float64 {
|
||||||
|
return map[string]float64{
|
||||||
|
shortSuffix: 1,
|
||||||
|
"K" + suffix: float64(scale),
|
||||||
|
"M" + suffix: float64(scale * scale),
|
||||||
|
"G" + suffix: float64(scale * scale * scale),
|
||||||
|
"T" + suffix: float64(scale * scale * scale * scale),
|
||||||
|
"P" + suffix: float64(scale * scale * scale * scale * scale),
|
||||||
|
"E" + suffix: float64(scale * scale * scale * scale * scale * scale),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
package units
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
siUnits = []string{"", "K", "M", "G", "T", "P", "E"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func ToString(n int64, scale int64, suffix, baseSuffix string) string {
|
||||||
|
mn := len(siUnits)
|
||||||
|
out := make([]string, mn)
|
||||||
|
for i, m := range siUnits {
|
||||||
|
if n%scale != 0 || i == 0 && n == 0 {
|
||||||
|
s := suffix
|
||||||
|
if i == 0 {
|
||||||
|
s = baseSuffix
|
||||||
|
}
|
||||||
|
out[mn-1-i] = fmt.Sprintf("%d%s%s", n%scale, m, s)
|
||||||
|
}
|
||||||
|
n /= scale
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(out, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Below code ripped straight from http://golang.org/src/pkg/time/format.go?s=33392:33438#L1123
|
||||||
|
var errLeadingInt = errors.New("units: bad [0-9]*") // never printed
|
||||||
|
|
||||||
|
// leadingInt consumes the leading [0-9]* from s.
|
||||||
|
func leadingInt(s string) (x int64, rem string, err error) {
|
||||||
|
i := 0
|
||||||
|
for ; i < len(s); i++ {
|
||||||
|
c := s[i]
|
||||||
|
if c < '0' || c > '9' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if x >= (1<<63-10)/10 {
|
||||||
|
// overflow
|
||||||
|
return 0, "", errLeadingInt
|
||||||
|
}
|
||||||
|
x = x*10 + int64(c) - '0'
|
||||||
|
}
|
||||||
|
return x, s[i:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseUnit(s string, unitMap map[string]float64) (int64, error) {
|
||||||
|
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
|
||||||
|
orig := s
|
||||||
|
f := float64(0)
|
||||||
|
neg := false
|
||||||
|
|
||||||
|
// Consume [-+]?
|
||||||
|
if s != "" {
|
||||||
|
c := s[0]
|
||||||
|
if c == '-' || c == '+' {
|
||||||
|
neg = c == '-'
|
||||||
|
s = s[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Special case: if all that is left is "0", this is zero.
|
||||||
|
if s == "0" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
return 0, errors.New("units: invalid " + orig)
|
||||||
|
}
|
||||||
|
for s != "" {
|
||||||
|
g := float64(0) // this element of the sequence
|
||||||
|
|
||||||
|
var x int64
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// The next character must be [0-9.]
|
||||||
|
if !(s[0] == '.' || ('0' <= s[0] && s[0] <= '9')) {
|
||||||
|
return 0, errors.New("units: invalid " + orig)
|
||||||
|
}
|
||||||
|
// Consume [0-9]*
|
||||||
|
pl := len(s)
|
||||||
|
x, s, err = leadingInt(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.New("units: invalid " + orig)
|
||||||
|
}
|
||||||
|
g = float64(x)
|
||||||
|
pre := pl != len(s) // whether we consumed anything before a period
|
||||||
|
|
||||||
|
// Consume (\.[0-9]*)?
|
||||||
|
post := false
|
||||||
|
if s != "" && s[0] == '.' {
|
||||||
|
s = s[1:]
|
||||||
|
pl := len(s)
|
||||||
|
x, s, err = leadingInt(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.New("units: invalid " + orig)
|
||||||
|
}
|
||||||
|
scale := 1.0
|
||||||
|
for n := pl - len(s); n > 0; n-- {
|
||||||
|
scale *= 10
|
||||||
|
}
|
||||||
|
g += float64(x) / scale
|
||||||
|
post = pl != len(s)
|
||||||
|
}
|
||||||
|
if !pre && !post {
|
||||||
|
// no digits (e.g. ".s" or "-.s")
|
||||||
|
return 0, errors.New("units: invalid " + orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume unit.
|
||||||
|
i := 0
|
||||||
|
for ; i < len(s); i++ {
|
||||||
|
c := s[i]
|
||||||
|
if c == '.' || ('0' <= c && c <= '9') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u := s[:i]
|
||||||
|
s = s[i:]
|
||||||
|
unit, ok := unitMap[u]
|
||||||
|
if !ok {
|
||||||
|
return 0, errors.New("units: unknown unit " + u + " in " + orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
f += g * unit
|
||||||
|
}
|
||||||
|
|
||||||
|
if neg {
|
||||||
|
f = -f
|
||||||
|
}
|
||||||
|
if f < float64(-1<<63) || f > float64(1<<63-1) {
|
||||||
|
return 0, errors.New("units: overflow parsing unit")
|
||||||
|
}
|
||||||
|
return int64(f), nil
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright 2014 Docker, Inc.
|
||||||
|
// Copyright 2015-2018 CoreOS, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
// Package daemon provides a Go implementation of the sd_notify protocol.
|
||||||
|
// It can be used to inform systemd of service start-up completion, watchdog
|
||||||
|
// events, and other status changes.
|
||||||
|
//
|
||||||
|
// https://www.freedesktop.org/software/systemd/man/sd_notify.html#Description
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SdNotifyReady tells the service manager that service startup is finished
|
||||||
|
// or the service finished loading its configuration.
|
||||||
|
SdNotifyReady = "READY=1"
|
||||||
|
|
||||||
|
// SdNotifyStopping tells the service manager that the service is beginning
|
||||||
|
// its shutdown.
|
||||||
|
SdNotifyStopping = "STOPPING=1"
|
||||||
|
|
||||||
|
// SdNotifyReloading tells the service manager that this service is
|
||||||
|
// reloading its configuration. Note that you must call SdNotifyReady when
|
||||||
|
// it completed reloading.
|
||||||
|
SdNotifyReloading = "RELOADING=1"
|
||||||
|
|
||||||
|
// SdNotifyWatchdog tells the service manager to update the watchdog
|
||||||
|
// timestamp for the service.
|
||||||
|
SdNotifyWatchdog = "WATCHDOG=1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SdNotify sends a message to the init daemon. It is common to ignore the error.
|
||||||
|
// If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET`
|
||||||
|
// will be unconditionally unset.
|
||||||
|
//
|
||||||
|
// It returns one of the following:
|
||||||
|
// (false, nil) - notification not supported (i.e. NOTIFY_SOCKET is unset)
|
||||||
|
// (false, err) - notification supported, but failure happened (e.g. error connecting to NOTIFY_SOCKET or while sending data)
|
||||||
|
// (true, nil) - notification supported, data has been sent
|
||||||
|
func SdNotify(unsetEnvironment bool, state string) (bool, error) {
|
||||||
|
socketAddr := &net.UnixAddr{
|
||||||
|
Name: os.Getenv("NOTIFY_SOCKET"),
|
||||||
|
Net: "unixgram",
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTIFY_SOCKET not set
|
||||||
|
if socketAddr.Name == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsetEnvironment {
|
||||||
|
if err := os.Unsetenv("NOTIFY_SOCKET"); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
|
||||||
|
// Error connecting to NOTIFY_SOCKET
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if _, err = conn.Write([]byte(state)); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
// Copyright 2016 CoreOS, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSdNotify
|
||||||
|
func TestSdNotify(t *testing.T) {
|
||||||
|
|
||||||
|
testDir, e := ioutil.TempDir("/tmp/", "test-")
|
||||||
|
if e != nil {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(testDir)
|
||||||
|
|
||||||
|
notifySocket := testDir + "/notify-socket.sock"
|
||||||
|
laddr := net.UnixAddr{
|
||||||
|
Name: notifySocket,
|
||||||
|
Net: "unixgram",
|
||||||
|
}
|
||||||
|
_, e = net.ListenUnixgram("unixgram", &laddr)
|
||||||
|
if e != nil {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
unsetEnv bool
|
||||||
|
envSocket string
|
||||||
|
|
||||||
|
wsent bool
|
||||||
|
werr bool
|
||||||
|
}{
|
||||||
|
// (true, nil) - notification supported, data has been sent
|
||||||
|
{false, notifySocket, true, false},
|
||||||
|
// (false, err) - notification supported, but failure happened
|
||||||
|
{true, testDir + "/missing.sock", false, true},
|
||||||
|
// (false, nil) - notification not supported
|
||||||
|
{true, "", false, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
must(os.Unsetenv("NOTIFY_SOCKET"))
|
||||||
|
if tt.envSocket != "" {
|
||||||
|
must(os.Setenv("NOTIFY_SOCKET", tt.envSocket))
|
||||||
|
}
|
||||||
|
sent, err := SdNotify(tt.unsetEnv, fmt.Sprintf("TestSdNotify test message #%d", i))
|
||||||
|
|
||||||
|
if sent != tt.wsent {
|
||||||
|
t.Errorf("#%d: expected send result %t, got %t", i, tt.wsent, sent)
|
||||||
|
}
|
||||||
|
if tt.werr && err == nil {
|
||||||
|
t.Errorf("#%d: want non-nil err, got nil", i)
|
||||||
|
} else if !tt.werr && err != nil {
|
||||||
|
t.Errorf("#%d: want nil err, got %v", i, err)
|
||||||
|
}
|
||||||
|
if tt.unsetEnv && tt.envSocket != "" && os.Getenv("NOTIFY_SOCKET") != "" {
|
||||||
|
t.Errorf("#%d: environment variable not cleaned up", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
// Copyright 2016 CoreOS, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SdWatchdogEnabled returns watchdog information for a service.
|
||||||
|
// Processes should call daemon.SdNotify(false, daemon.SdNotifyWatchdog) every
|
||||||
|
// time / 2.
|
||||||
|
// If `unsetEnvironment` is true, the environment variables `WATCHDOG_USEC` and
|
||||||
|
// `WATCHDOG_PID` will be unconditionally unset.
|
||||||
|
//
|
||||||
|
// It returns one of the following:
|
||||||
|
// (0, nil) - watchdog isn't enabled or we aren't the watched PID.
|
||||||
|
// (0, err) - an error happened (e.g. error converting time).
|
||||||
|
// (time, nil) - watchdog is enabled and we can send ping.
|
||||||
|
// time is delay before inactive service will be killed.
|
||||||
|
func SdWatchdogEnabled(unsetEnvironment bool) (time.Duration, error) {
|
||||||
|
wusec := os.Getenv("WATCHDOG_USEC")
|
||||||
|
wpid := os.Getenv("WATCHDOG_PID")
|
||||||
|
if unsetEnvironment {
|
||||||
|
wusecErr := os.Unsetenv("WATCHDOG_USEC")
|
||||||
|
wpidErr := os.Unsetenv("WATCHDOG_PID")
|
||||||
|
if wusecErr != nil {
|
||||||
|
return 0, wusecErr
|
||||||
|
}
|
||||||
|
if wpidErr != nil {
|
||||||
|
return 0, wpidErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if wusec == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
s, err := strconv.Atoi(wusec)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("error converting WATCHDOG_USEC: %s", err)
|
||||||
|
}
|
||||||
|
if s <= 0 {
|
||||||
|
return 0, fmt.Errorf("error WATCHDOG_USEC must be a positive number")
|
||||||
|
}
|
||||||
|
interval := time.Duration(s) * time.Microsecond
|
||||||
|
|
||||||
|
if wpid == "" {
|
||||||
|
return interval, nil
|
||||||
|
}
|
||||||
|
p, err := strconv.Atoi(wpid)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("error converting WATCHDOG_PID: %s", err)
|
||||||
|
}
|
||||||
|
if os.Getpid() != p {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return interval, nil
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
// Copyright 2016 CoreOS, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func must(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSdWatchdogEnabled(t *testing.T) {
|
||||||
|
mypid := strconv.Itoa(os.Getpid())
|
||||||
|
tests := []struct {
|
||||||
|
usec string // empty => unset
|
||||||
|
pid string // empty => unset
|
||||||
|
unsetEnv bool // arbitrarily set across testcases
|
||||||
|
|
||||||
|
werr bool
|
||||||
|
wdelay time.Duration
|
||||||
|
}{
|
||||||
|
// Success cases
|
||||||
|
{"100", mypid, true, false, 100 * time.Microsecond},
|
||||||
|
{"50", mypid, true, false, 50 * time.Microsecond},
|
||||||
|
{"1", mypid, false, false, 1 * time.Microsecond},
|
||||||
|
{"1", "", true, false, 1 * time.Microsecond},
|
||||||
|
|
||||||
|
// No-op cases
|
||||||
|
{"", mypid, true, false, 0}, // WATCHDOG_USEC not set
|
||||||
|
{"1", "0", false, false, 0}, // WATCHDOG_PID doesn't match
|
||||||
|
{"", "", true, false, 0}, // Both not set
|
||||||
|
|
||||||
|
// Failure cases
|
||||||
|
{"-1", mypid, true, true, 0}, // Negative USEC
|
||||||
|
{"string", "1", false, true, 0}, // Non-integer USEC value
|
||||||
|
{"1", "string", true, true, 0}, // Non-integer PID value
|
||||||
|
{"stringa", "stringb", false, true, 0}, // E v e r y t h i n g
|
||||||
|
{"-10239", "-eleventythree", true, true, 0}, // i s w r o n g
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
if tt.usec != "" {
|
||||||
|
must(os.Setenv("WATCHDOG_USEC", tt.usec))
|
||||||
|
} else {
|
||||||
|
must(os.Unsetenv("WATCHDOG_USEC"))
|
||||||
|
}
|
||||||
|
if tt.pid != "" {
|
||||||
|
must(os.Setenv("WATCHDOG_PID", tt.pid))
|
||||||
|
} else {
|
||||||
|
must(os.Unsetenv("WATCHDOG_PID"))
|
||||||
|
}
|
||||||
|
|
||||||
|
delay, err := SdWatchdogEnabled(tt.unsetEnv)
|
||||||
|
|
||||||
|
if tt.werr && err == nil {
|
||||||
|
t.Errorf("#%d: want non-nil err, got nil", i)
|
||||||
|
} else if !tt.werr && err != nil {
|
||||||
|
t.Errorf("#%d: want nil err, got %v", i, err)
|
||||||
|
}
|
||||||
|
if tt.wdelay != delay {
|
||||||
|
t.Errorf("#%d: want delay=%d, got %d", i, tt.wdelay, delay)
|
||||||
|
}
|
||||||
|
if tt.unsetEnv && (os.Getenv("WATCHDOG_PID") != "" || os.Getenv("WATCHDOG_USEC") != "") {
|
||||||
|
t.Errorf("#%d: environment variables not cleaned up", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||||
|
// when the code is not running on Google App Engine, compiled by GopherJS, and
|
||||||
|
// "-tags safe" is not added to the go build command line. The "disableunsafe"
|
||||||
|
// tag is deprecated and thus should not be used.
|
||||||
|
// +build !js,!appengine,!safe,!disableunsafe
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UnsafeDisabled is a build-time constant which specifies whether or
|
||||||
|
// not access to the unsafe package is available.
|
||||||
|
UnsafeDisabled = false
|
||||||
|
|
||||||
|
// ptrSize is the size of a pointer on the current arch.
|
||||||
|
ptrSize = unsafe.Sizeof((*byte)(nil))
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// offsetPtr, offsetScalar, and offsetFlag are the offsets for the
|
||||||
|
// internal reflect.Value fields. These values are valid before golang
|
||||||
|
// commit ecccf07e7f9d which changed the format. The are also valid
|
||||||
|
// after commit 82f48826c6c7 which changed the format again to mirror
|
||||||
|
// the original format. Code in the init function updates these offsets
|
||||||
|
// as necessary.
|
||||||
|
offsetPtr = uintptr(ptrSize)
|
||||||
|
offsetScalar = uintptr(0)
|
||||||
|
offsetFlag = uintptr(ptrSize * 2)
|
||||||
|
|
||||||
|
// flagKindWidth and flagKindShift indicate various bits that the
|
||||||
|
// reflect package uses internally to track kind information.
|
||||||
|
//
|
||||||
|
// flagRO indicates whether or not the value field of a reflect.Value is
|
||||||
|
// read-only.
|
||||||
|
//
|
||||||
|
// flagIndir indicates whether the value field of a reflect.Value is
|
||||||
|
// the actual data or a pointer to the data.
|
||||||
|
//
|
||||||
|
// These values are valid before golang commit 90a7c3c86944 which
|
||||||
|
// changed their positions. Code in the init function updates these
|
||||||
|
// flags as necessary.
|
||||||
|
flagKindWidth = uintptr(5)
|
||||||
|
flagKindShift = uintptr(flagKindWidth - 1)
|
||||||
|
flagRO = uintptr(1 << 0)
|
||||||
|
flagIndir = uintptr(1 << 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Older versions of reflect.Value stored small integers directly in the
|
||||||
|
// ptr field (which is named val in the older versions). Versions
|
||||||
|
// between commits ecccf07e7f9d and 82f48826c6c7 added a new field named
|
||||||
|
// scalar for this purpose which unfortunately came before the flag
|
||||||
|
// field, so the offset of the flag field is different for those
|
||||||
|
// versions.
|
||||||
|
//
|
||||||
|
// This code constructs a new reflect.Value from a known small integer
|
||||||
|
// and checks if the size of the reflect.Value struct indicates it has
|
||||||
|
// the scalar field. When it does, the offsets are updated accordingly.
|
||||||
|
vv := reflect.ValueOf(0xf00)
|
||||||
|
if unsafe.Sizeof(vv) == (ptrSize * 4) {
|
||||||
|
offsetScalar = ptrSize * 2
|
||||||
|
offsetFlag = ptrSize * 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit 90a7c3c86944 changed the flag positions such that the low
|
||||||
|
// order bits are the kind. This code extracts the kind from the flags
|
||||||
|
// field and ensures it's the correct type. When it's not, the flag
|
||||||
|
// order has been changed to the newer format, so the flags are updated
|
||||||
|
// accordingly.
|
||||||
|
upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag)
|
||||||
|
upfv := *(*uintptr)(upf)
|
||||||
|
flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift)
|
||||||
|
if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) {
|
||||||
|
flagKindShift = 0
|
||||||
|
flagRO = 1 << 5
|
||||||
|
flagIndir = 1 << 6
|
||||||
|
|
||||||
|
// Commit adf9b30e5594 modified the flags to separate the
|
||||||
|
// flagRO flag into two bits which specifies whether or not the
|
||||||
|
// field is embedded. This causes flagIndir to move over a bit
|
||||||
|
// and means that flagRO is the combination of either of the
|
||||||
|
// original flagRO bit and the new bit.
|
||||||
|
//
|
||||||
|
// This code detects the change by extracting what used to be
|
||||||
|
// the indirect bit to ensure it's set. When it's not, the flag
|
||||||
|
// order has been changed to the newer format, so the flags are
|
||||||
|
// updated accordingly.
|
||||||
|
if upfv&flagIndir == 0 {
|
||||||
|
flagRO = 3 << 5
|
||||||
|
flagIndir = 1 << 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
|
||||||
|
// the typical safety restrictions preventing access to unaddressable and
|
||||||
|
// unexported data. It works by digging the raw pointer to the underlying
|
||||||
|
// value out of the protected value and generating a new unprotected (unsafe)
|
||||||
|
// reflect.Value to it.
|
||||||
|
//
|
||||||
|
// This allows us to check for implementations of the Stringer and error
|
||||||
|
// interfaces to be used for pretty printing ordinarily unaddressable and
|
||||||
|
// inaccessible values such as unexported struct fields.
|
||||||
|
func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
|
||||||
|
indirects := 1
|
||||||
|
vt := v.Type()
|
||||||
|
upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr)
|
||||||
|
rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag))
|
||||||
|
if rvf&flagIndir != 0 {
|
||||||
|
vt = reflect.PtrTo(v.Type())
|
||||||
|
indirects++
|
||||||
|
} else if offsetScalar != 0 {
|
||||||
|
// The value is in the scalar field when it's not one of the
|
||||||
|
// reference types.
|
||||||
|
switch vt.Kind() {
|
||||||
|
case reflect.Uintptr:
|
||||||
|
case reflect.Chan:
|
||||||
|
case reflect.Func:
|
||||||
|
case reflect.Map:
|
||||||
|
case reflect.Ptr:
|
||||||
|
case reflect.UnsafePointer:
|
||||||
|
default:
|
||||||
|
upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) +
|
||||||
|
offsetScalar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pv := reflect.NewAt(vt, upv)
|
||||||
|
rv = pv
|
||||||
|
for i := 0; i < indirects; i++ {
|
||||||
|
rv = rv.Elem()
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||||
|
// when the code is running on Google App Engine, compiled by GopherJS, or
|
||||||
|
// "-tags safe" is added to the go build command line. The "disableunsafe"
|
||||||
|
// tag is deprecated and thus should not be used.
|
||||||
|
// +build js appengine safe disableunsafe
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UnsafeDisabled is a build-time constant which specifies whether or
|
||||||
|
// not access to the unsafe package is available.
|
||||||
|
UnsafeDisabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// unsafeReflectValue typically converts the passed reflect.Value into a one
|
||||||
|
// that bypasses the typical safety restrictions preventing access to
|
||||||
|
// unaddressable and unexported data. However, doing this relies on access to
|
||||||
|
// the unsafe package. This is a stub version which simply returns the passed
|
||||||
|
// reflect.Value when the unsafe package is not available.
|
||||||
|
func unsafeReflectValue(v reflect.Value) reflect.Value {
|
||||||
|
return v
|
||||||
|
}
|
|
@ -0,0 +1,341 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Some constants in the form of bytes to avoid string overhead. This mirrors
|
||||||
|
// the technique used in the fmt package.
|
||||||
|
var (
|
||||||
|
panicBytes = []byte("(PANIC=")
|
||||||
|
plusBytes = []byte("+")
|
||||||
|
iBytes = []byte("i")
|
||||||
|
trueBytes = []byte("true")
|
||||||
|
falseBytes = []byte("false")
|
||||||
|
interfaceBytes = []byte("(interface {})")
|
||||||
|
commaNewlineBytes = []byte(",\n")
|
||||||
|
newlineBytes = []byte("\n")
|
||||||
|
openBraceBytes = []byte("{")
|
||||||
|
openBraceNewlineBytes = []byte("{\n")
|
||||||
|
closeBraceBytes = []byte("}")
|
||||||
|
asteriskBytes = []byte("*")
|
||||||
|
colonBytes = []byte(":")
|
||||||
|
colonSpaceBytes = []byte(": ")
|
||||||
|
openParenBytes = []byte("(")
|
||||||
|
closeParenBytes = []byte(")")
|
||||||
|
spaceBytes = []byte(" ")
|
||||||
|
pointerChainBytes = []byte("->")
|
||||||
|
nilAngleBytes = []byte("<nil>")
|
||||||
|
maxNewlineBytes = []byte("<max depth reached>\n")
|
||||||
|
maxShortBytes = []byte("<max>")
|
||||||
|
circularBytes = []byte("<already shown>")
|
||||||
|
circularShortBytes = []byte("<shown>")
|
||||||
|
invalidAngleBytes = []byte("<invalid>")
|
||||||
|
openBracketBytes = []byte("[")
|
||||||
|
closeBracketBytes = []byte("]")
|
||||||
|
percentBytes = []byte("%")
|
||||||
|
precisionBytes = []byte(".")
|
||||||
|
openAngleBytes = []byte("<")
|
||||||
|
closeAngleBytes = []byte(">")
|
||||||
|
openMapBytes = []byte("map[")
|
||||||
|
closeMapBytes = []byte("]")
|
||||||
|
lenEqualsBytes = []byte("len=")
|
||||||
|
capEqualsBytes = []byte("cap=")
|
||||||
|
)
|
||||||
|
|
||||||
|
// hexDigits is used to map a decimal value to a hex digit.
|
||||||
|
var hexDigits = "0123456789abcdef"
|
||||||
|
|
||||||
|
// catchPanic handles any panics that might occur during the handleMethods
|
||||||
|
// calls.
|
||||||
|
func catchPanic(w io.Writer, v reflect.Value) {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
w.Write(panicBytes)
|
||||||
|
fmt.Fprintf(w, "%v", err)
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMethods attempts to call the Error and String methods on the underlying
|
||||||
|
// type the passed reflect.Value represents and outputes the result to Writer w.
|
||||||
|
//
|
||||||
|
// It handles panics in any called methods by catching and displaying the error
|
||||||
|
// as the formatted value.
|
||||||
|
func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
|
||||||
|
// We need an interface to check if the type implements the error or
|
||||||
|
// Stringer interface. However, the reflect package won't give us an
|
||||||
|
// interface on certain things like unexported struct fields in order
|
||||||
|
// to enforce visibility rules. We use unsafe, when it's available,
|
||||||
|
// to bypass these restrictions since this package does not mutate the
|
||||||
|
// values.
|
||||||
|
if !v.CanInterface() {
|
||||||
|
if UnsafeDisabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
v = unsafeReflectValue(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose whether or not to do error and Stringer interface lookups against
|
||||||
|
// the base type or a pointer to the base type depending on settings.
|
||||||
|
// Technically calling one of these methods with a pointer receiver can
|
||||||
|
// mutate the value, however, types which choose to satisify an error or
|
||||||
|
// Stringer interface with a pointer receiver should not be mutating their
|
||||||
|
// state inside these interface methods.
|
||||||
|
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
|
||||||
|
v = unsafeReflectValue(v)
|
||||||
|
}
|
||||||
|
if v.CanAddr() {
|
||||||
|
v = v.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it an error or Stringer?
|
||||||
|
switch iface := v.Interface().(type) {
|
||||||
|
case error:
|
||||||
|
defer catchPanic(w, v)
|
||||||
|
if cs.ContinueOnMethod {
|
||||||
|
w.Write(openParenBytes)
|
||||||
|
w.Write([]byte(iface.Error()))
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
w.Write(spaceBytes)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(iface.Error()))
|
||||||
|
return true
|
||||||
|
|
||||||
|
case fmt.Stringer:
|
||||||
|
defer catchPanic(w, v)
|
||||||
|
if cs.ContinueOnMethod {
|
||||||
|
w.Write(openParenBytes)
|
||||||
|
w.Write([]byte(iface.String()))
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
w.Write(spaceBytes)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
w.Write([]byte(iface.String()))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// printBool outputs a boolean value as true or false to Writer w.
|
||||||
|
func printBool(w io.Writer, val bool) {
|
||||||
|
if val {
|
||||||
|
w.Write(trueBytes)
|
||||||
|
} else {
|
||||||
|
w.Write(falseBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printInt outputs a signed integer value to Writer w.
|
||||||
|
func printInt(w io.Writer, val int64, base int) {
|
||||||
|
w.Write([]byte(strconv.FormatInt(val, base)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// printUint outputs an unsigned integer value to Writer w.
|
||||||
|
func printUint(w io.Writer, val uint64, base int) {
|
||||||
|
w.Write([]byte(strconv.FormatUint(val, base)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// printFloat outputs a floating point value using the specified precision,
|
||||||
|
// which is expected to be 32 or 64bit, to Writer w.
|
||||||
|
func printFloat(w io.Writer, val float64, precision int) {
|
||||||
|
w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// printComplex outputs a complex value using the specified float precision
|
||||||
|
// for the real and imaginary parts to Writer w.
|
||||||
|
func printComplex(w io.Writer, c complex128, floatPrecision int) {
|
||||||
|
r := real(c)
|
||||||
|
w.Write(openParenBytes)
|
||||||
|
w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
|
||||||
|
i := imag(c)
|
||||||
|
if i >= 0 {
|
||||||
|
w.Write(plusBytes)
|
||||||
|
}
|
||||||
|
w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
|
||||||
|
w.Write(iBytes)
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x'
|
||||||
|
// prefix to Writer w.
|
||||||
|
func printHexPtr(w io.Writer, p uintptr) {
|
||||||
|
// Null pointer.
|
||||||
|
num := uint64(p)
|
||||||
|
if num == 0 {
|
||||||
|
w.Write(nilAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
|
||||||
|
buf := make([]byte, 18)
|
||||||
|
|
||||||
|
// It's simpler to construct the hex string right to left.
|
||||||
|
base := uint64(16)
|
||||||
|
i := len(buf) - 1
|
||||||
|
for num >= base {
|
||||||
|
buf[i] = hexDigits[num%base]
|
||||||
|
num /= base
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
buf[i] = hexDigits[num]
|
||||||
|
|
||||||
|
// Add '0x' prefix.
|
||||||
|
i--
|
||||||
|
buf[i] = 'x'
|
||||||
|
i--
|
||||||
|
buf[i] = '0'
|
||||||
|
|
||||||
|
// Strip unused leading bytes.
|
||||||
|
buf = buf[i:]
|
||||||
|
w.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// valuesSorter implements sort.Interface to allow a slice of reflect.Value
|
||||||
|
// elements to be sorted.
|
||||||
|
type valuesSorter struct {
|
||||||
|
values []reflect.Value
|
||||||
|
strings []string // either nil or same len and values
|
||||||
|
cs *ConfigState
|
||||||
|
}
|
||||||
|
|
||||||
|
// newValuesSorter initializes a valuesSorter instance, which holds a set of
|
||||||
|
// surrogate keys on which the data should be sorted. It uses flags in
|
||||||
|
// ConfigState to decide if and how to populate those surrogate keys.
|
||||||
|
func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
|
||||||
|
vs := &valuesSorter{values: values, cs: cs}
|
||||||
|
if canSortSimply(vs.values[0].Kind()) {
|
||||||
|
return vs
|
||||||
|
}
|
||||||
|
if !cs.DisableMethods {
|
||||||
|
vs.strings = make([]string, len(values))
|
||||||
|
for i := range vs.values {
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
if !handleMethods(cs, &b, vs.values[i]) {
|
||||||
|
vs.strings = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
vs.strings[i] = b.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if vs.strings == nil && cs.SpewKeys {
|
||||||
|
vs.strings = make([]string, len(values))
|
||||||
|
for i := range vs.values {
|
||||||
|
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vs
|
||||||
|
}
|
||||||
|
|
||||||
|
// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
|
||||||
|
// directly, or whether it should be considered for sorting by surrogate keys
|
||||||
|
// (if the ConfigState allows it).
|
||||||
|
func canSortSimply(kind reflect.Kind) bool {
|
||||||
|
// This switch parallels valueSortLess, except for the default case.
|
||||||
|
switch kind {
|
||||||
|
case reflect.Bool:
|
||||||
|
return true
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
return true
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
return true
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return true
|
||||||
|
case reflect.String:
|
||||||
|
return true
|
||||||
|
case reflect.Uintptr:
|
||||||
|
return true
|
||||||
|
case reflect.Array:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of values in the slice. It is part of the
|
||||||
|
// sort.Interface implementation.
|
||||||
|
func (s *valuesSorter) Len() int {
|
||||||
|
return len(s.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps the values at the passed indices. It is part of the
|
||||||
|
// sort.Interface implementation.
|
||||||
|
func (s *valuesSorter) Swap(i, j int) {
|
||||||
|
s.values[i], s.values[j] = s.values[j], s.values[i]
|
||||||
|
if s.strings != nil {
|
||||||
|
s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// valueSortLess returns whether the first value should sort before the second
|
||||||
|
// value. It is used by valueSorter.Less as part of the sort.Interface
|
||||||
|
// implementation.
|
||||||
|
func valueSortLess(a, b reflect.Value) bool {
|
||||||
|
switch a.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return !a.Bool() && b.Bool()
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
return a.Int() < b.Int()
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
return a.Uint() < b.Uint()
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return a.Float() < b.Float()
|
||||||
|
case reflect.String:
|
||||||
|
return a.String() < b.String()
|
||||||
|
case reflect.Uintptr:
|
||||||
|
return a.Uint() < b.Uint()
|
||||||
|
case reflect.Array:
|
||||||
|
// Compare the contents of both arrays.
|
||||||
|
l := a.Len()
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
av := a.Index(i)
|
||||||
|
bv := b.Index(i)
|
||||||
|
if av.Interface() == bv.Interface() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return valueSortLess(av, bv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.String() < b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less returns whether the value at index i should sort before the
|
||||||
|
// value at index j. It is part of the sort.Interface implementation.
|
||||||
|
func (s *valuesSorter) Less(i, j int) bool {
|
||||||
|
if s.strings == nil {
|
||||||
|
return valueSortLess(s.values[i], s.values[j])
|
||||||
|
}
|
||||||
|
return s.strings[i] < s.strings[j]
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortValues is a sort function that handles both native types and any type that
|
||||||
|
// can be converted to error or Stringer. Other inputs are sorted according to
|
||||||
|
// their Value.String() value to ensure display stability.
|
||||||
|
func sortValues(values []reflect.Value, cs *ConfigState) {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Sort(newValuesSorter(values, cs))
|
||||||
|
}
|
|
@ -0,0 +1,298 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
)
|
||||||
|
|
||||||
|
// custom type to test Stinger interface on non-pointer receiver.
|
||||||
|
type stringer string
|
||||||
|
|
||||||
|
// String implements the Stringer interface for testing invocation of custom
|
||||||
|
// stringers on types with non-pointer receivers.
|
||||||
|
func (s stringer) String() string {
|
||||||
|
return "stringer " + string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// custom type to test Stinger interface on pointer receiver.
|
||||||
|
type pstringer string
|
||||||
|
|
||||||
|
// String implements the Stringer interface for testing invocation of custom
|
||||||
|
// stringers on types with only pointer receivers.
|
||||||
|
func (s *pstringer) String() string {
|
||||||
|
return "stringer " + string(*s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// xref1 and xref2 are cross referencing structs for testing circular reference
|
||||||
|
// detection.
|
||||||
|
type xref1 struct {
|
||||||
|
ps2 *xref2
|
||||||
|
}
|
||||||
|
type xref2 struct {
|
||||||
|
ps1 *xref1
|
||||||
|
}
|
||||||
|
|
||||||
|
// indirCir1, indirCir2, and indirCir3 are used to generate an indirect circular
|
||||||
|
// reference for testing detection.
|
||||||
|
type indirCir1 struct {
|
||||||
|
ps2 *indirCir2
|
||||||
|
}
|
||||||
|
type indirCir2 struct {
|
||||||
|
ps3 *indirCir3
|
||||||
|
}
|
||||||
|
type indirCir3 struct {
|
||||||
|
ps1 *indirCir1
|
||||||
|
}
|
||||||
|
|
||||||
|
// embed is used to test embedded structures.
|
||||||
|
type embed struct {
|
||||||
|
a string
|
||||||
|
}
|
||||||
|
|
||||||
|
// embedwrap is used to test embedded structures.
|
||||||
|
type embedwrap struct {
|
||||||
|
*embed
|
||||||
|
e *embed
|
||||||
|
}
|
||||||
|
|
||||||
|
// panicer is used to intentionally cause a panic for testing spew properly
|
||||||
|
// handles them
|
||||||
|
type panicer int
|
||||||
|
|
||||||
|
func (p panicer) String() string {
|
||||||
|
panic("test panic")
|
||||||
|
}
|
||||||
|
|
||||||
|
// customError is used to test custom error interface invocation.
|
||||||
|
type customError int
|
||||||
|
|
||||||
|
func (e customError) Error() string {
|
||||||
|
return fmt.Sprintf("error: %d", int(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringizeWants converts a slice of wanted test output into a format suitable
|
||||||
|
// for a test error message.
|
||||||
|
func stringizeWants(wants []string) string {
|
||||||
|
s := ""
|
||||||
|
for i, want := range wants {
|
||||||
|
if i > 0 {
|
||||||
|
s += fmt.Sprintf("want%d: %s", i+1, want)
|
||||||
|
} else {
|
||||||
|
s += "want: " + want
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// testFailed returns whether or not a test failed by checking if the result
|
||||||
|
// of the test is in the slice of wanted strings.
|
||||||
|
func testFailed(result string, wants []string) bool {
|
||||||
|
for _, want := range wants {
|
||||||
|
if result == want {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortableStruct struct {
|
||||||
|
x int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss sortableStruct) String() string {
|
||||||
|
return fmt.Sprintf("ss.%d", ss.x)
|
||||||
|
}
|
||||||
|
|
||||||
|
type unsortableStruct struct {
|
||||||
|
x int
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortTestCase struct {
|
||||||
|
input []reflect.Value
|
||||||
|
expected []reflect.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func helpTestSortValues(tests []sortTestCase, cs *spew.ConfigState, t *testing.T) {
|
||||||
|
getInterfaces := func(values []reflect.Value) []interface{} {
|
||||||
|
interfaces := []interface{}{}
|
||||||
|
for _, v := range values {
|
||||||
|
interfaces = append(interfaces, v.Interface())
|
||||||
|
}
|
||||||
|
return interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
spew.SortValues(test.input, cs)
|
||||||
|
// reflect.DeepEqual cannot really make sense of reflect.Value,
|
||||||
|
// probably because of all the pointer tricks. For instance,
|
||||||
|
// v(2.0) != v(2.0) on a 32-bits system. Turn them into interface{}
|
||||||
|
// instead.
|
||||||
|
input := getInterfaces(test.input)
|
||||||
|
expected := getInterfaces(test.expected)
|
||||||
|
if !reflect.DeepEqual(input, expected) {
|
||||||
|
t.Errorf("Sort mismatch:\n %v != %v", input, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSortValues ensures the sort functionality for relect.Value based sorting
|
||||||
|
// works as intended.
|
||||||
|
func TestSortValues(t *testing.T) {
|
||||||
|
v := reflect.ValueOf
|
||||||
|
|
||||||
|
a := v("a")
|
||||||
|
b := v("b")
|
||||||
|
c := v("c")
|
||||||
|
embedA := v(embed{"a"})
|
||||||
|
embedB := v(embed{"b"})
|
||||||
|
embedC := v(embed{"c"})
|
||||||
|
tests := []sortTestCase{
|
||||||
|
// No values.
|
||||||
|
{
|
||||||
|
[]reflect.Value{},
|
||||||
|
[]reflect.Value{},
|
||||||
|
},
|
||||||
|
// Bools.
|
||||||
|
{
|
||||||
|
[]reflect.Value{v(false), v(true), v(false)},
|
||||||
|
[]reflect.Value{v(false), v(false), v(true)},
|
||||||
|
},
|
||||||
|
// Ints.
|
||||||
|
{
|
||||||
|
[]reflect.Value{v(2), v(1), v(3)},
|
||||||
|
[]reflect.Value{v(1), v(2), v(3)},
|
||||||
|
},
|
||||||
|
// Uints.
|
||||||
|
{
|
||||||
|
[]reflect.Value{v(uint8(2)), v(uint8(1)), v(uint8(3))},
|
||||||
|
[]reflect.Value{v(uint8(1)), v(uint8(2)), v(uint8(3))},
|
||||||
|
},
|
||||||
|
// Floats.
|
||||||
|
{
|
||||||
|
[]reflect.Value{v(2.0), v(1.0), v(3.0)},
|
||||||
|
[]reflect.Value{v(1.0), v(2.0), v(3.0)},
|
||||||
|
},
|
||||||
|
// Strings.
|
||||||
|
{
|
||||||
|
[]reflect.Value{b, a, c},
|
||||||
|
[]reflect.Value{a, b, c},
|
||||||
|
},
|
||||||
|
// Array
|
||||||
|
{
|
||||||
|
[]reflect.Value{v([3]int{3, 2, 1}), v([3]int{1, 3, 2}), v([3]int{1, 2, 3})},
|
||||||
|
[]reflect.Value{v([3]int{1, 2, 3}), v([3]int{1, 3, 2}), v([3]int{3, 2, 1})},
|
||||||
|
},
|
||||||
|
// Uintptrs.
|
||||||
|
{
|
||||||
|
[]reflect.Value{v(uintptr(2)), v(uintptr(1)), v(uintptr(3))},
|
||||||
|
[]reflect.Value{v(uintptr(1)), v(uintptr(2)), v(uintptr(3))},
|
||||||
|
},
|
||||||
|
// SortableStructs.
|
||||||
|
{
|
||||||
|
// Note: not sorted - DisableMethods is set.
|
||||||
|
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||||
|
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||||
|
},
|
||||||
|
// UnsortableStructs.
|
||||||
|
{
|
||||||
|
// Note: not sorted - SpewKeys is false.
|
||||||
|
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||||
|
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||||
|
},
|
||||||
|
// Invalid.
|
||||||
|
{
|
||||||
|
[]reflect.Value{embedB, embedA, embedC},
|
||||||
|
[]reflect.Value{embedB, embedA, embedC},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cs := spew.ConfigState{DisableMethods: true, SpewKeys: false}
|
||||||
|
helpTestSortValues(tests, &cs, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSortValuesWithMethods ensures the sort functionality for relect.Value
|
||||||
|
// based sorting works as intended when using string methods.
|
||||||
|
func TestSortValuesWithMethods(t *testing.T) {
|
||||||
|
v := reflect.ValueOf
|
||||||
|
|
||||||
|
a := v("a")
|
||||||
|
b := v("b")
|
||||||
|
c := v("c")
|
||||||
|
tests := []sortTestCase{
|
||||||
|
// Ints.
|
||||||
|
{
|
||||||
|
[]reflect.Value{v(2), v(1), v(3)},
|
||||||
|
[]reflect.Value{v(1), v(2), v(3)},
|
||||||
|
},
|
||||||
|
// Strings.
|
||||||
|
{
|
||||||
|
[]reflect.Value{b, a, c},
|
||||||
|
[]reflect.Value{a, b, c},
|
||||||
|
},
|
||||||
|
// SortableStructs.
|
||||||
|
{
|
||||||
|
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||||
|
[]reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})},
|
||||||
|
},
|
||||||
|
// UnsortableStructs.
|
||||||
|
{
|
||||||
|
// Note: not sorted - SpewKeys is false.
|
||||||
|
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||||
|
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cs := spew.ConfigState{DisableMethods: false, SpewKeys: false}
|
||||||
|
helpTestSortValues(tests, &cs, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSortValuesWithSpew ensures the sort functionality for relect.Value
|
||||||
|
// based sorting works as intended when using spew to stringify keys.
|
||||||
|
func TestSortValuesWithSpew(t *testing.T) {
|
||||||
|
v := reflect.ValueOf
|
||||||
|
|
||||||
|
a := v("a")
|
||||||
|
b := v("b")
|
||||||
|
c := v("c")
|
||||||
|
tests := []sortTestCase{
|
||||||
|
// Ints.
|
||||||
|
{
|
||||||
|
[]reflect.Value{v(2), v(1), v(3)},
|
||||||
|
[]reflect.Value{v(1), v(2), v(3)},
|
||||||
|
},
|
||||||
|
// Strings.
|
||||||
|
{
|
||||||
|
[]reflect.Value{b, a, c},
|
||||||
|
[]reflect.Value{a, b, c},
|
||||||
|
},
|
||||||
|
// SortableStructs.
|
||||||
|
{
|
||||||
|
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
|
||||||
|
[]reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})},
|
||||||
|
},
|
||||||
|
// UnsortableStructs.
|
||||||
|
{
|
||||||
|
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
|
||||||
|
[]reflect.Value{v(unsortableStruct{1}), v(unsortableStruct{2}), v(unsortableStruct{3})},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cs := spew.ConfigState{DisableMethods: true, SpewKeys: true}
|
||||||
|
helpTestSortValues(tests, &cs, t)
|
||||||
|
}
|
|
@ -0,0 +1,306 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigState houses the configuration options used by spew to format and
|
||||||
|
// display values. There is a global instance, Config, that is used to control
|
||||||
|
// all top-level Formatter and Dump functionality. Each ConfigState instance
|
||||||
|
// provides methods equivalent to the top-level functions.
|
||||||
|
//
|
||||||
|
// The zero value for ConfigState provides no indentation. You would typically
|
||||||
|
// want to set it to a space or a tab.
|
||||||
|
//
|
||||||
|
// Alternatively, you can use NewDefaultConfig to get a ConfigState instance
|
||||||
|
// with default settings. See the documentation of NewDefaultConfig for default
|
||||||
|
// values.
|
||||||
|
type ConfigState struct {
|
||||||
|
// Indent specifies the string to use for each indentation level. The
|
||||||
|
// global config instance that all top-level functions use set this to a
|
||||||
|
// single space by default. If you would like more indentation, you might
|
||||||
|
// set this to a tab with "\t" or perhaps two spaces with " ".
|
||||||
|
Indent string
|
||||||
|
|
||||||
|
// MaxDepth controls the maximum number of levels to descend into nested
|
||||||
|
// data structures. The default, 0, means there is no limit.
|
||||||
|
//
|
||||||
|
// NOTE: Circular data structures are properly detected, so it is not
|
||||||
|
// necessary to set this value unless you specifically want to limit deeply
|
||||||
|
// nested data structures.
|
||||||
|
MaxDepth int
|
||||||
|
|
||||||
|
// DisableMethods specifies whether or not error and Stringer interfaces are
|
||||||
|
// invoked for types that implement them.
|
||||||
|
DisableMethods bool
|
||||||
|
|
||||||
|
// DisablePointerMethods specifies whether or not to check for and invoke
|
||||||
|
// error and Stringer interfaces on types which only accept a pointer
|
||||||
|
// receiver when the current type is not a pointer.
|
||||||
|
//
|
||||||
|
// NOTE: This might be an unsafe action since calling one of these methods
|
||||||
|
// with a pointer receiver could technically mutate the value, however,
|
||||||
|
// in practice, types which choose to satisify an error or Stringer
|
||||||
|
// interface with a pointer receiver should not be mutating their state
|
||||||
|
// inside these interface methods. As a result, this option relies on
|
||||||
|
// access to the unsafe package, so it will not have any effect when
|
||||||
|
// running in environments without access to the unsafe package such as
|
||||||
|
// Google App Engine or with the "safe" build tag specified.
|
||||||
|
DisablePointerMethods bool
|
||||||
|
|
||||||
|
// DisablePointerAddresses specifies whether to disable the printing of
|
||||||
|
// pointer addresses. This is useful when diffing data structures in tests.
|
||||||
|
DisablePointerAddresses bool
|
||||||
|
|
||||||
|
// DisableCapacities specifies whether to disable the printing of capacities
|
||||||
|
// for arrays, slices, maps and channels. This is useful when diffing
|
||||||
|
// data structures in tests.
|
||||||
|
DisableCapacities bool
|
||||||
|
|
||||||
|
// ContinueOnMethod specifies whether or not recursion should continue once
|
||||||
|
// a custom error or Stringer interface is invoked. The default, false,
|
||||||
|
// means it will print the results of invoking the custom error or Stringer
|
||||||
|
// interface and return immediately instead of continuing to recurse into
|
||||||
|
// the internals of the data type.
|
||||||
|
//
|
||||||
|
// NOTE: This flag does not have any effect if method invocation is disabled
|
||||||
|
// via the DisableMethods or DisablePointerMethods options.
|
||||||
|
ContinueOnMethod bool
|
||||||
|
|
||||||
|
// SortKeys specifies map keys should be sorted before being printed. Use
|
||||||
|
// this to have a more deterministic, diffable output. Note that only
|
||||||
|
// native types (bool, int, uint, floats, uintptr and string) and types
|
||||||
|
// that support the error or Stringer interfaces (if methods are
|
||||||
|
// enabled) are supported, with other types sorted according to the
|
||||||
|
// reflect.Value.String() output which guarantees display stability.
|
||||||
|
SortKeys bool
|
||||||
|
|
||||||
|
// SpewKeys specifies that, as a last resort attempt, map keys should
|
||||||
|
// be spewed to strings and sorted by those strings. This is only
|
||||||
|
// considered if SortKeys is true.
|
||||||
|
SpewKeys bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is the active configuration of the top-level functions.
|
||||||
|
// The configuration can be changed by modifying the contents of spew.Config.
|
||||||
|
var Config = ConfigState{Indent: " "}
|
||||||
|
|
||||||
|
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the formatted string as a value that satisfies error. See NewFormatter
|
||||||
|
// for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) {
|
||||||
|
return fmt.Errorf(format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprint(w, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintf(w, format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintln(w, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print is a wrapper for fmt.Print that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Print(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Print(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Print(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Printf(format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println is a wrapper for fmt.Println that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Println(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Println(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Println(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Sprint(a ...interface{}) string {
|
||||||
|
return fmt.Sprint(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Sprintf(format string, a ...interface{}) string {
|
||||||
|
return fmt.Sprintf(format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
|
||||||
|
// were passed with a Formatter interface returned by c.NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Sprintln(a ...interface{}) string {
|
||||||
|
return fmt.Sprintln(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
|
||||||
|
interface. As a result, it integrates cleanly with standard fmt package
|
||||||
|
printing functions. The formatter is useful for inline printing of smaller data
|
||||||
|
types similar to the standard %v format specifier.
|
||||||
|
|
||||||
|
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||||
|
addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb
|
||||||
|
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||||
|
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||||
|
the width and precision arguments (however they will still work on the format
|
||||||
|
specifiers not handled by the custom formatter).
|
||||||
|
|
||||||
|
Typically this function shouldn't be called directly. It is much easier to make
|
||||||
|
use of the custom formatter by calling one of the convenience functions such as
|
||||||
|
c.Printf, c.Println, or c.Printf.
|
||||||
|
*/
|
||||||
|
func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter {
|
||||||
|
return newFormatter(c, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fdump formats and displays the passed arguments to io.Writer w. It formats
|
||||||
|
// exactly the same as Dump.
|
||||||
|
func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) {
|
||||||
|
fdump(c, w, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dump displays the passed parameters to standard out with newlines, customizable
|
||||||
|
indentation, and additional debug information such as complete types and all
|
||||||
|
pointer addresses used to indirect to the final value. It provides the
|
||||||
|
following features over the built-in printing facilities provided by the fmt
|
||||||
|
package:
|
||||||
|
|
||||||
|
* Pointers are dereferenced and followed
|
||||||
|
* Circular data structures are detected and handled properly
|
||||||
|
* Custom Stringer/error interfaces are optionally invoked, including
|
||||||
|
on unexported types
|
||||||
|
* Custom types which only implement the Stringer/error interfaces via
|
||||||
|
a pointer receiver are optionally invoked when passing non-pointer
|
||||||
|
variables
|
||||||
|
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||||
|
includes offsets, byte values in hex, and ASCII output
|
||||||
|
|
||||||
|
The configuration options are controlled by modifying the public members
|
||||||
|
of c. See ConfigState for options documentation.
|
||||||
|
|
||||||
|
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
|
||||||
|
get the formatted result as a string.
|
||||||
|
*/
|
||||||
|
func (c *ConfigState) Dump(a ...interface{}) {
|
||||||
|
fdump(c, os.Stdout, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sdump returns a string with the passed arguments formatted exactly the same
|
||||||
|
// as Dump.
|
||||||
|
func (c *ConfigState) Sdump(a ...interface{}) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fdump(c, &buf, a...)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertArgs accepts a slice of arguments and returns a slice of the same
|
||||||
|
// length with each argument converted to a spew Formatter interface using
|
||||||
|
// the ConfigState associated with s.
|
||||||
|
func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) {
|
||||||
|
formatters = make([]interface{}, len(args))
|
||||||
|
for index, arg := range args {
|
||||||
|
formatters[index] = newFormatter(c, arg)
|
||||||
|
}
|
||||||
|
return formatters
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultConfig returns a ConfigState with the following default settings.
|
||||||
|
//
|
||||||
|
// Indent: " "
|
||||||
|
// MaxDepth: 0
|
||||||
|
// DisableMethods: false
|
||||||
|
// DisablePointerMethods: false
|
||||||
|
// ContinueOnMethod: false
|
||||||
|
// SortKeys: false
|
||||||
|
func NewDefaultConfig() *ConfigState {
|
||||||
|
return &ConfigState{Indent: " "}
|
||||||
|
}
|
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package spew implements a deep pretty printer for Go data structures to aid in
|
||||||
|
debugging.
|
||||||
|
|
||||||
|
A quick overview of the additional features spew provides over the built-in
|
||||||
|
printing facilities for Go data types are as follows:
|
||||||
|
|
||||||
|
* Pointers are dereferenced and followed
|
||||||
|
* Circular data structures are detected and handled properly
|
||||||
|
* Custom Stringer/error interfaces are optionally invoked, including
|
||||||
|
on unexported types
|
||||||
|
* Custom types which only implement the Stringer/error interfaces via
|
||||||
|
a pointer receiver are optionally invoked when passing non-pointer
|
||||||
|
variables
|
||||||
|
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||||
|
includes offsets, byte values in hex, and ASCII output (only when using
|
||||||
|
Dump style)
|
||||||
|
|
||||||
|
There are two different approaches spew allows for dumping Go data structures:
|
||||||
|
|
||||||
|
* Dump style which prints with newlines, customizable indentation,
|
||||||
|
and additional debug information such as types and all pointer addresses
|
||||||
|
used to indirect to the final value
|
||||||
|
* A custom Formatter interface that integrates cleanly with the standard fmt
|
||||||
|
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
|
||||||
|
similar to the default %v while providing the additional functionality
|
||||||
|
outlined above and passing unsupported format verbs such as %x and %q
|
||||||
|
along to fmt
|
||||||
|
|
||||||
|
Quick Start
|
||||||
|
|
||||||
|
This section demonstrates how to quickly get started with spew. See the
|
||||||
|
sections below for further details on formatting and configuration options.
|
||||||
|
|
||||||
|
To dump a variable with full newlines, indentation, type, and pointer
|
||||||
|
information use Dump, Fdump, or Sdump:
|
||||||
|
spew.Dump(myVar1, myVar2, ...)
|
||||||
|
spew.Fdump(someWriter, myVar1, myVar2, ...)
|
||||||
|
str := spew.Sdump(myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
Alternatively, if you would prefer to use format strings with a compacted inline
|
||||||
|
printing style, use the convenience wrappers Printf, Fprintf, etc with
|
||||||
|
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
|
||||||
|
%#+v (adds types and pointer addresses):
|
||||||
|
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
|
||||||
|
Configuration Options
|
||||||
|
|
||||||
|
Configuration of spew is handled by fields in the ConfigState type. For
|
||||||
|
convenience, all of the top-level functions use a global state available
|
||||||
|
via the spew.Config global.
|
||||||
|
|
||||||
|
It is also possible to create a ConfigState instance that provides methods
|
||||||
|
equivalent to the top-level functions. This allows concurrent configuration
|
||||||
|
options. See the ConfigState documentation for more details.
|
||||||
|
|
||||||
|
The following configuration options are available:
|
||||||
|
* Indent
|
||||||
|
String to use for each indentation level for Dump functions.
|
||||||
|
It is a single space by default. A popular alternative is "\t".
|
||||||
|
|
||||||
|
* MaxDepth
|
||||||
|
Maximum number of levels to descend into nested data structures.
|
||||||
|
There is no limit by default.
|
||||||
|
|
||||||
|
* DisableMethods
|
||||||
|
Disables invocation of error and Stringer interface methods.
|
||||||
|
Method invocation is enabled by default.
|
||||||
|
|
||||||
|
* DisablePointerMethods
|
||||||
|
Disables invocation of error and Stringer interface methods on types
|
||||||
|
which only accept pointer receivers from non-pointer variables.
|
||||||
|
Pointer method invocation is enabled by default.
|
||||||
|
|
||||||
|
* DisablePointerAddresses
|
||||||
|
DisablePointerAddresses specifies whether to disable the printing of
|
||||||
|
pointer addresses. This is useful when diffing data structures in tests.
|
||||||
|
|
||||||
|
* DisableCapacities
|
||||||
|
DisableCapacities specifies whether to disable the printing of
|
||||||
|
capacities for arrays, slices, maps and channels. This is useful when
|
||||||
|
diffing data structures in tests.
|
||||||
|
|
||||||
|
* ContinueOnMethod
|
||||||
|
Enables recursion into types after invoking error and Stringer interface
|
||||||
|
methods. Recursion after method invocation is disabled by default.
|
||||||
|
|
||||||
|
* SortKeys
|
||||||
|
Specifies map keys should be sorted before being printed. Use
|
||||||
|
this to have a more deterministic, diffable output. Note that
|
||||||
|
only native types (bool, int, uint, floats, uintptr and string)
|
||||||
|
and types which implement error or Stringer interfaces are
|
||||||
|
supported with other types sorted according to the
|
||||||
|
reflect.Value.String() output which guarantees display
|
||||||
|
stability. Natural map order is used by default.
|
||||||
|
|
||||||
|
* SpewKeys
|
||||||
|
Specifies that, as a last resort attempt, map keys should be
|
||||||
|
spewed to strings and sorted by those strings. This is only
|
||||||
|
considered if SortKeys is true.
|
||||||
|
|
||||||
|
Dump Usage
|
||||||
|
|
||||||
|
Simply call spew.Dump with a list of variables you want to dump:
|
||||||
|
|
||||||
|
spew.Dump(myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
You may also call spew.Fdump if you would prefer to output to an arbitrary
|
||||||
|
io.Writer. For example, to dump to standard error:
|
||||||
|
|
||||||
|
spew.Fdump(os.Stderr, myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
A third option is to call spew.Sdump to get the formatted output as a string:
|
||||||
|
|
||||||
|
str := spew.Sdump(myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
Sample Dump Output
|
||||||
|
|
||||||
|
See the Dump example for details on the setup of the types and variables being
|
||||||
|
shown here.
|
||||||
|
|
||||||
|
(main.Foo) {
|
||||||
|
unexportedField: (*main.Bar)(0xf84002e210)({
|
||||||
|
flag: (main.Flag) flagTwo,
|
||||||
|
data: (uintptr) <nil>
|
||||||
|
}),
|
||||||
|
ExportedField: (map[interface {}]interface {}) (len=1) {
|
||||||
|
(string) (len=3) "one": (bool) true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
|
||||||
|
command as shown.
|
||||||
|
([]uint8) (len=32 cap=32) {
|
||||||
|
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
|
||||||
|
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
|
||||||
|
00000020 31 32 |12|
|
||||||
|
}
|
||||||
|
|
||||||
|
Custom Formatter
|
||||||
|
|
||||||
|
Spew provides a custom formatter that implements the fmt.Formatter interface
|
||||||
|
so that it integrates cleanly with standard fmt package printing functions. The
|
||||||
|
formatter is useful for inline printing of smaller data types similar to the
|
||||||
|
standard %v format specifier.
|
||||||
|
|
||||||
|
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||||
|
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
|
||||||
|
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||||
|
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||||
|
the width and precision arguments (however they will still work on the format
|
||||||
|
specifiers not handled by the custom formatter).
|
||||||
|
|
||||||
|
Custom Formatter Usage
|
||||||
|
|
||||||
|
The simplest way to make use of the spew custom formatter is to call one of the
|
||||||
|
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
|
||||||
|
functions have syntax you are most likely already familiar with:
|
||||||
|
|
||||||
|
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
spew.Println(myVar, myVar2)
|
||||||
|
spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
|
||||||
|
See the Index for the full list convenience functions.
|
||||||
|
|
||||||
|
Sample Formatter Output
|
||||||
|
|
||||||
|
Double pointer to a uint8:
|
||||||
|
%v: <**>5
|
||||||
|
%+v: <**>(0xf8400420d0->0xf8400420c8)5
|
||||||
|
%#v: (**uint8)5
|
||||||
|
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
|
||||||
|
|
||||||
|
Pointer to circular struct with a uint8 field and a pointer to itself:
|
||||||
|
%v: <*>{1 <*><shown>}
|
||||||
|
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
|
||||||
|
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
|
||||||
|
%#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>}
|
||||||
|
|
||||||
|
See the Printf example for details on the setup of variables being shown
|
||||||
|
here.
|
||||||
|
|
||||||
|
Errors
|
||||||
|
|
||||||
|
Since it is possible for custom Stringer/error interfaces to panic, spew
|
||||||
|
detects them and handles them internally by printing the panic information
|
||||||
|
inline with the output. Since spew is intended to provide deep pretty printing
|
||||||
|
capabilities on structures, it intentionally does not return any errors.
|
||||||
|
*/
|
||||||
|
package spew
|
|
@ -0,0 +1,509 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// uint8Type is a reflect.Type representing a uint8. It is used to
|
||||||
|
// convert cgo types to uint8 slices for hexdumping.
|
||||||
|
uint8Type = reflect.TypeOf(uint8(0))
|
||||||
|
|
||||||
|
// cCharRE is a regular expression that matches a cgo char.
|
||||||
|
// It is used to detect character arrays to hexdump them.
|
||||||
|
cCharRE = regexp.MustCompile("^.*\\._Ctype_char$")
|
||||||
|
|
||||||
|
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
|
||||||
|
// char. It is used to detect unsigned character arrays to hexdump
|
||||||
|
// them.
|
||||||
|
cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$")
|
||||||
|
|
||||||
|
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
|
||||||
|
// It is used to detect uint8_t arrays to hexdump them.
|
||||||
|
cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$")
|
||||||
|
)
|
||||||
|
|
||||||
|
// dumpState contains information about the state of a dump operation.
|
||||||
|
type dumpState struct {
|
||||||
|
w io.Writer
|
||||||
|
depth int
|
||||||
|
pointers map[uintptr]int
|
||||||
|
ignoreNextType bool
|
||||||
|
ignoreNextIndent bool
|
||||||
|
cs *ConfigState
|
||||||
|
}
|
||||||
|
|
||||||
|
// indent performs indentation according to the depth level and cs.Indent
|
||||||
|
// option.
|
||||||
|
func (d *dumpState) indent() {
|
||||||
|
if d.ignoreNextIndent {
|
||||||
|
d.ignoreNextIndent = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth))
|
||||||
|
}
|
||||||
|
|
||||||
|
// unpackValue returns values inside of non-nil interfaces when possible.
|
||||||
|
// This is useful for data types like structs, arrays, slices, and maps which
|
||||||
|
// can contain varying types packed inside an interface.
|
||||||
|
func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
|
||||||
|
if v.Kind() == reflect.Interface && !v.IsNil() {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumpPtr handles formatting of pointers by indirecting them as necessary.
|
||||||
|
func (d *dumpState) dumpPtr(v reflect.Value) {
|
||||||
|
// Remove pointers at or below the current depth from map used to detect
|
||||||
|
// circular refs.
|
||||||
|
for k, depth := range d.pointers {
|
||||||
|
if depth >= d.depth {
|
||||||
|
delete(d.pointers, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep list of all dereferenced pointers to show later.
|
||||||
|
pointerChain := make([]uintptr, 0)
|
||||||
|
|
||||||
|
// Figure out how many levels of indirection there are by dereferencing
|
||||||
|
// pointers and unpacking interfaces down the chain while detecting circular
|
||||||
|
// references.
|
||||||
|
nilFound := false
|
||||||
|
cycleFound := false
|
||||||
|
indirects := 0
|
||||||
|
ve := v
|
||||||
|
for ve.Kind() == reflect.Ptr {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
indirects++
|
||||||
|
addr := ve.Pointer()
|
||||||
|
pointerChain = append(pointerChain, addr)
|
||||||
|
if pd, ok := d.pointers[addr]; ok && pd < d.depth {
|
||||||
|
cycleFound = true
|
||||||
|
indirects--
|
||||||
|
break
|
||||||
|
}
|
||||||
|
d.pointers[addr] = d.depth
|
||||||
|
|
||||||
|
ve = ve.Elem()
|
||||||
|
if ve.Kind() == reflect.Interface {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ve = ve.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display type information.
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
d.w.Write(bytes.Repeat(asteriskBytes, indirects))
|
||||||
|
d.w.Write([]byte(ve.Type().String()))
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
|
||||||
|
// Display pointer information.
|
||||||
|
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
for i, addr := range pointerChain {
|
||||||
|
if i > 0 {
|
||||||
|
d.w.Write(pointerChainBytes)
|
||||||
|
}
|
||||||
|
printHexPtr(d.w, addr)
|
||||||
|
}
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display dereferenced value.
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
switch {
|
||||||
|
case nilFound == true:
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
|
||||||
|
case cycleFound == true:
|
||||||
|
d.w.Write(circularBytes)
|
||||||
|
|
||||||
|
default:
|
||||||
|
d.ignoreNextType = true
|
||||||
|
d.dump(ve)
|
||||||
|
}
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumpSlice handles formatting of arrays and slices. Byte (uint8 under
|
||||||
|
// reflection) arrays and slices are dumped in hexdump -C fashion.
|
||||||
|
func (d *dumpState) dumpSlice(v reflect.Value) {
|
||||||
|
// Determine whether this type should be hex dumped or not. Also,
|
||||||
|
// for types which should be hexdumped, try to use the underlying data
|
||||||
|
// first, then fall back to trying to convert them to a uint8 slice.
|
||||||
|
var buf []uint8
|
||||||
|
doConvert := false
|
||||||
|
doHexDump := false
|
||||||
|
numEntries := v.Len()
|
||||||
|
if numEntries > 0 {
|
||||||
|
vt := v.Index(0).Type()
|
||||||
|
vts := vt.String()
|
||||||
|
switch {
|
||||||
|
// C types that need to be converted.
|
||||||
|
case cCharRE.MatchString(vts):
|
||||||
|
fallthrough
|
||||||
|
case cUnsignedCharRE.MatchString(vts):
|
||||||
|
fallthrough
|
||||||
|
case cUint8tCharRE.MatchString(vts):
|
||||||
|
doConvert = true
|
||||||
|
|
||||||
|
// Try to use existing uint8 slices and fall back to converting
|
||||||
|
// and copying if that fails.
|
||||||
|
case vt.Kind() == reflect.Uint8:
|
||||||
|
// We need an addressable interface to convert the type
|
||||||
|
// to a byte slice. However, the reflect package won't
|
||||||
|
// give us an interface on certain things like
|
||||||
|
// unexported struct fields in order to enforce
|
||||||
|
// visibility rules. We use unsafe, when available, to
|
||||||
|
// bypass these restrictions since this package does not
|
||||||
|
// mutate the values.
|
||||||
|
vs := v
|
||||||
|
if !vs.CanInterface() || !vs.CanAddr() {
|
||||||
|
vs = unsafeReflectValue(vs)
|
||||||
|
}
|
||||||
|
if !UnsafeDisabled {
|
||||||
|
vs = vs.Slice(0, numEntries)
|
||||||
|
|
||||||
|
// Use the existing uint8 slice if it can be
|
||||||
|
// type asserted.
|
||||||
|
iface := vs.Interface()
|
||||||
|
if slice, ok := iface.([]uint8); ok {
|
||||||
|
buf = slice
|
||||||
|
doHexDump = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The underlying data needs to be converted if it can't
|
||||||
|
// be type asserted to a uint8 slice.
|
||||||
|
doConvert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy and convert the underlying type if needed.
|
||||||
|
if doConvert && vt.ConvertibleTo(uint8Type) {
|
||||||
|
// Convert and copy each element into a uint8 byte
|
||||||
|
// slice.
|
||||||
|
buf = make([]uint8, numEntries)
|
||||||
|
for i := 0; i < numEntries; i++ {
|
||||||
|
vv := v.Index(i)
|
||||||
|
buf[i] = uint8(vv.Convert(uint8Type).Uint())
|
||||||
|
}
|
||||||
|
doHexDump = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hexdump the entire slice as needed.
|
||||||
|
if doHexDump {
|
||||||
|
indent := strings.Repeat(d.cs.Indent, d.depth)
|
||||||
|
str := indent + hex.Dump(buf)
|
||||||
|
str = strings.Replace(str, "\n", "\n"+indent, -1)
|
||||||
|
str = strings.TrimRight(str, d.cs.Indent)
|
||||||
|
d.w.Write([]byte(str))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively call dump for each item.
|
||||||
|
for i := 0; i < numEntries; i++ {
|
||||||
|
d.dump(d.unpackValue(v.Index(i)))
|
||||||
|
if i < (numEntries - 1) {
|
||||||
|
d.w.Write(commaNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dump is the main workhorse for dumping a value. It uses the passed reflect
|
||||||
|
// value to figure out what kind of object we are dealing with and formats it
|
||||||
|
// appropriately. It is a recursive function, however circular data structures
|
||||||
|
// are detected and handled properly.
|
||||||
|
func (d *dumpState) dump(v reflect.Value) {
|
||||||
|
// Handle invalid reflect values immediately.
|
||||||
|
kind := v.Kind()
|
||||||
|
if kind == reflect.Invalid {
|
||||||
|
d.w.Write(invalidAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pointers specially.
|
||||||
|
if kind == reflect.Ptr {
|
||||||
|
d.indent()
|
||||||
|
d.dumpPtr(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print type information unless already handled elsewhere.
|
||||||
|
if !d.ignoreNextType {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
d.w.Write([]byte(v.Type().String()))
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
d.w.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
d.ignoreNextType = false
|
||||||
|
|
||||||
|
// Display length and capacity if the built-in len and cap functions
|
||||||
|
// work with the value's kind and the len/cap itself is non-zero.
|
||||||
|
valueLen, valueCap := 0, 0
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Array, reflect.Slice, reflect.Chan:
|
||||||
|
valueLen, valueCap = v.Len(), v.Cap()
|
||||||
|
case reflect.Map, reflect.String:
|
||||||
|
valueLen = v.Len()
|
||||||
|
}
|
||||||
|
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
if valueLen != 0 {
|
||||||
|
d.w.Write(lenEqualsBytes)
|
||||||
|
printInt(d.w, int64(valueLen), 10)
|
||||||
|
}
|
||||||
|
if !d.cs.DisableCapacities && valueCap != 0 {
|
||||||
|
if valueLen != 0 {
|
||||||
|
d.w.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
d.w.Write(capEqualsBytes)
|
||||||
|
printInt(d.w, int64(valueCap), 10)
|
||||||
|
}
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
d.w.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Stringer/error interfaces if they exist and the handle methods flag
|
||||||
|
// is enabled
|
||||||
|
if !d.cs.DisableMethods {
|
||||||
|
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
|
||||||
|
if handled := handleMethods(d.cs, d.w, v); handled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case reflect.Invalid:
|
||||||
|
// Do nothing. We should never get here since invalid has already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Bool:
|
||||||
|
printBool(d.w, v.Bool())
|
||||||
|
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
printInt(d.w, v.Int(), 10)
|
||||||
|
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
printUint(d.w, v.Uint(), 10)
|
||||||
|
|
||||||
|
case reflect.Float32:
|
||||||
|
printFloat(d.w, v.Float(), 32)
|
||||||
|
|
||||||
|
case reflect.Float64:
|
||||||
|
printFloat(d.w, v.Float(), 64)
|
||||||
|
|
||||||
|
case reflect.Complex64:
|
||||||
|
printComplex(d.w, v.Complex(), 32)
|
||||||
|
|
||||||
|
case reflect.Complex128:
|
||||||
|
printComplex(d.w, v.Complex(), 64)
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
if v.IsNil() {
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case reflect.Array:
|
||||||
|
d.w.Write(openBraceNewlineBytes)
|
||||||
|
d.depth++
|
||||||
|
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(maxNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.dumpSlice(v)
|
||||||
|
}
|
||||||
|
d.depth--
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
d.w.Write([]byte(strconv.Quote(v.String())))
|
||||||
|
|
||||||
|
case reflect.Interface:
|
||||||
|
// The only time we should get here is for nil interfaces due to
|
||||||
|
// unpackValue calls.
|
||||||
|
if v.IsNil() {
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Ptr:
|
||||||
|
// Do nothing. We should never get here since pointers have already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
// nil maps should be indicated as different than empty maps
|
||||||
|
if v.IsNil() {
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
d.w.Write(openBraceNewlineBytes)
|
||||||
|
d.depth++
|
||||||
|
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(maxNewlineBytes)
|
||||||
|
} else {
|
||||||
|
numEntries := v.Len()
|
||||||
|
keys := v.MapKeys()
|
||||||
|
if d.cs.SortKeys {
|
||||||
|
sortValues(keys, d.cs)
|
||||||
|
}
|
||||||
|
for i, key := range keys {
|
||||||
|
d.dump(d.unpackValue(key))
|
||||||
|
d.w.Write(colonSpaceBytes)
|
||||||
|
d.ignoreNextIndent = true
|
||||||
|
d.dump(d.unpackValue(v.MapIndex(key)))
|
||||||
|
if i < (numEntries - 1) {
|
||||||
|
d.w.Write(commaNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.depth--
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
d.w.Write(openBraceNewlineBytes)
|
||||||
|
d.depth++
|
||||||
|
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(maxNewlineBytes)
|
||||||
|
} else {
|
||||||
|
vt := v.Type()
|
||||||
|
numFields := v.NumField()
|
||||||
|
for i := 0; i < numFields; i++ {
|
||||||
|
d.indent()
|
||||||
|
vtf := vt.Field(i)
|
||||||
|
d.w.Write([]byte(vtf.Name))
|
||||||
|
d.w.Write(colonSpaceBytes)
|
||||||
|
d.ignoreNextIndent = true
|
||||||
|
d.dump(d.unpackValue(v.Field(i)))
|
||||||
|
if i < (numFields - 1) {
|
||||||
|
d.w.Write(commaNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.depth--
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.Uintptr:
|
||||||
|
printHexPtr(d.w, uintptr(v.Uint()))
|
||||||
|
|
||||||
|
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||||||
|
printHexPtr(d.w, v.Pointer())
|
||||||
|
|
||||||
|
// There were not any other types at the time this code was written, but
|
||||||
|
// fall back to letting the default fmt package handle it in case any new
|
||||||
|
// types are added.
|
||||||
|
default:
|
||||||
|
if v.CanInterface() {
|
||||||
|
fmt.Fprintf(d.w, "%v", v.Interface())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(d.w, "%v", v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fdump is a helper function to consolidate the logic from the various public
|
||||||
|
// methods which take varying writers and config states.
|
||||||
|
func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
|
||||||
|
for _, arg := range a {
|
||||||
|
if arg == nil {
|
||||||
|
w.Write(interfaceBytes)
|
||||||
|
w.Write(spaceBytes)
|
||||||
|
w.Write(nilAngleBytes)
|
||||||
|
w.Write(newlineBytes)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
d := dumpState{w: w, cs: cs}
|
||||||
|
d.pointers = make(map[uintptr]int)
|
||||||
|
d.dump(reflect.ValueOf(arg))
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fdump formats and displays the passed arguments to io.Writer w. It formats
|
||||||
|
// exactly the same as Dump.
|
||||||
|
func Fdump(w io.Writer, a ...interface{}) {
|
||||||
|
fdump(&Config, w, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sdump returns a string with the passed arguments formatted exactly the same
|
||||||
|
// as Dump.
|
||||||
|
func Sdump(a ...interface{}) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fdump(&Config, &buf, a...)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dump displays the passed parameters to standard out with newlines, customizable
|
||||||
|
indentation, and additional debug information such as complete types and all
|
||||||
|
pointer addresses used to indirect to the final value. It provides the
|
||||||
|
following features over the built-in printing facilities provided by the fmt
|
||||||
|
package:
|
||||||
|
|
||||||
|
* Pointers are dereferenced and followed
|
||||||
|
* Circular data structures are detected and handled properly
|
||||||
|
* Custom Stringer/error interfaces are optionally invoked, including
|
||||||
|
on unexported types
|
||||||
|
* Custom types which only implement the Stringer/error interfaces via
|
||||||
|
a pointer receiver are optionally invoked when passing non-pointer
|
||||||
|
variables
|
||||||
|
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||||
|
includes offsets, byte values in hex, and ASCII output
|
||||||
|
|
||||||
|
The configuration options are controlled by an exported package global,
|
||||||
|
spew.Config. See ConfigState for options documentation.
|
||||||
|
|
||||||
|
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
|
||||||
|
get the formatted result as a string.
|
||||||
|
*/
|
||||||
|
func Dump(a ...interface{}) {
|
||||||
|
fdump(&Config, os.Stdout, a...)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||||
|
// when both cgo is supported and "-tags testcgo" is added to the go test
|
||||||
|
// command line. This means the cgo tests are only added (and hence run) when
|
||||||
|
// specifially requested. This configuration is used because spew itself
|
||||||
|
// does not require cgo to run even though it does handle certain cgo types
|
||||||
|
// specially. Rather than forcing all clients to require cgo and an external
|
||||||
|
// C compiler just to run the tests, this scheme makes them optional.
|
||||||
|
// +build cgo,testcgo
|
||||||
|
|
||||||
|
package spew_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew/testdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addCgoDumpTests() {
|
||||||
|
// C char pointer.
|
||||||
|
v := testdata.GetCgoCharPointer()
|
||||||
|
nv := testdata.GetCgoNullCharPointer()
|
||||||
|
pv := &v
|
||||||
|
vcAddr := fmt.Sprintf("%p", v)
|
||||||
|
vAddr := fmt.Sprintf("%p", pv)
|
||||||
|
pvAddr := fmt.Sprintf("%p", &pv)
|
||||||
|
vt := "*testdata._Ctype_char"
|
||||||
|
vs := "116"
|
||||||
|
addDumpTest(v, "("+vt+")("+vcAddr+")("+vs+")\n")
|
||||||
|
addDumpTest(pv, "(*"+vt+")("+vAddr+"->"+vcAddr+")("+vs+")\n")
|
||||||
|
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+"->"+vcAddr+")("+vs+")\n")
|
||||||
|
addDumpTest(nv, "("+vt+")(<nil>)\n")
|
||||||
|
|
||||||
|
// C char array.
|
||||||
|
v2, v2l, v2c := testdata.GetCgoCharArray()
|
||||||
|
v2Len := fmt.Sprintf("%d", v2l)
|
||||||
|
v2Cap := fmt.Sprintf("%d", v2c)
|
||||||
|
v2t := "[6]testdata._Ctype_char"
|
||||||
|
v2s := "(len=" + v2Len + " cap=" + v2Cap + ") " +
|
||||||
|
"{\n 00000000 74 65 73 74 32 00 " +
|
||||||
|
" |test2.|\n}"
|
||||||
|
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
|
||||||
|
|
||||||
|
// C unsigned char array.
|
||||||
|
v3, v3l, v3c := testdata.GetCgoUnsignedCharArray()
|
||||||
|
v3Len := fmt.Sprintf("%d", v3l)
|
||||||
|
v3Cap := fmt.Sprintf("%d", v3c)
|
||||||
|
v3t := "[6]testdata._Ctype_unsignedchar"
|
||||||
|
v3t2 := "[6]testdata._Ctype_uchar"
|
||||||
|
v3s := "(len=" + v3Len + " cap=" + v3Cap + ") " +
|
||||||
|
"{\n 00000000 74 65 73 74 33 00 " +
|
||||||
|
" |test3.|\n}"
|
||||||
|
addDumpTest(v3, "("+v3t+") "+v3s+"\n", "("+v3t2+") "+v3s+"\n")
|
||||||
|
|
||||||
|
// C signed char array.
|
||||||
|
v4, v4l, v4c := testdata.GetCgoSignedCharArray()
|
||||||
|
v4Len := fmt.Sprintf("%d", v4l)
|
||||||
|
v4Cap := fmt.Sprintf("%d", v4c)
|
||||||
|
v4t := "[6]testdata._Ctype_schar"
|
||||||
|
v4t2 := "testdata._Ctype_schar"
|
||||||
|
v4s := "(len=" + v4Len + " cap=" + v4Cap + ") " +
|
||||||
|
"{\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 101,\n (" + v4t2 +
|
||||||
|
") 115,\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 52,\n (" + v4t2 +
|
||||||
|
") 0\n}"
|
||||||
|
addDumpTest(v4, "("+v4t+") "+v4s+"\n")
|
||||||
|
|
||||||
|
// C uint8_t array.
|
||||||
|
v5, v5l, v5c := testdata.GetCgoUint8tArray()
|
||||||
|
v5Len := fmt.Sprintf("%d", v5l)
|
||||||
|
v5Cap := fmt.Sprintf("%d", v5c)
|
||||||
|
v5t := "[6]testdata._Ctype_uint8_t"
|
||||||
|
v5s := "(len=" + v5Len + " cap=" + v5Cap + ") " +
|
||||||
|
"{\n 00000000 74 65 73 74 35 00 " +
|
||||||
|
" |test5.|\n}"
|
||||||
|
addDumpTest(v5, "("+v5t+") "+v5s+"\n")
|
||||||
|
|
||||||
|
// C typedefed unsigned char array.
|
||||||
|
v6, v6l, v6c := testdata.GetCgoTypdefedUnsignedCharArray()
|
||||||
|
v6Len := fmt.Sprintf("%d", v6l)
|
||||||
|
v6Cap := fmt.Sprintf("%d", v6c)
|
||||||
|
v6t := "[6]testdata._Ctype_custom_uchar_t"
|
||||||
|
v6s := "(len=" + v6Len + " cap=" + v6Cap + ") " +
|
||||||
|
"{\n 00000000 74 65 73 74 36 00 " +
|
||||||
|
" |test6.|\n}"
|
||||||
|
addDumpTest(v6, "("+v6t+") "+v6s+"\n")
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||||
|
// when either cgo is not supported or "-tags testcgo" is not added to the go
|
||||||
|
// test command line. This file intentionally does not setup any cgo tests in
|
||||||
|
// this scenario.
|
||||||
|
// +build !cgo !testcgo
|
||||||
|
|
||||||
|
package spew_test
|
||||||
|
|
||||||
|
func addCgoDumpTests() {
|
||||||
|
// Don't add any tests for cgo since this file is only compiled when
|
||||||
|
// there should not be any cgo tests.
|
||||||
|
}
|
|
@ -0,0 +1,226 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Flag int
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagOne Flag = iota
|
||||||
|
flagTwo
|
||||||
|
)
|
||||||
|
|
||||||
|
var flagStrings = map[Flag]string{
|
||||||
|
flagOne: "flagOne",
|
||||||
|
flagTwo: "flagTwo",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Flag) String() string {
|
||||||
|
if s, ok := flagStrings[f]; ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Unknown flag (%d)", int(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bar struct {
|
||||||
|
data uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type Foo struct {
|
||||||
|
unexportedField Bar
|
||||||
|
ExportedField map[interface{}]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This example demonstrates how to use Dump to dump variables to stdout.
|
||||||
|
func ExampleDump() {
|
||||||
|
// The following package level declarations are assumed for this example:
|
||||||
|
/*
|
||||||
|
type Flag int
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagOne Flag = iota
|
||||||
|
flagTwo
|
||||||
|
)
|
||||||
|
|
||||||
|
var flagStrings = map[Flag]string{
|
||||||
|
flagOne: "flagOne",
|
||||||
|
flagTwo: "flagTwo",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Flag) String() string {
|
||||||
|
if s, ok := flagStrings[f]; ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Unknown flag (%d)", int(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bar struct {
|
||||||
|
data uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type Foo struct {
|
||||||
|
unexportedField Bar
|
||||||
|
ExportedField map[interface{}]interface{}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Setup some sample data structures for the example.
|
||||||
|
bar := Bar{uintptr(0)}
|
||||||
|
s1 := Foo{bar, map[interface{}]interface{}{"one": true}}
|
||||||
|
f := Flag(5)
|
||||||
|
b := []byte{
|
||||||
|
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
|
||||||
|
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
|
||||||
|
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
|
||||||
|
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
|
||||||
|
0x31, 0x32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump!
|
||||||
|
spew.Dump(s1, f, b)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// (spew_test.Foo) {
|
||||||
|
// unexportedField: (spew_test.Bar) {
|
||||||
|
// data: (uintptr) <nil>
|
||||||
|
// },
|
||||||
|
// ExportedField: (map[interface {}]interface {}) (len=1) {
|
||||||
|
// (string) (len=3) "one": (bool) true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// (spew_test.Flag) Unknown flag (5)
|
||||||
|
// ([]uint8) (len=34 cap=34) {
|
||||||
|
// 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
|
||||||
|
// 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
|
||||||
|
// 00000020 31 32 |12|
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
// This example demonstrates how to use Printf to display a variable with a
|
||||||
|
// format string and inline formatting.
|
||||||
|
func ExamplePrintf() {
|
||||||
|
// Create a double pointer to a uint 8.
|
||||||
|
ui8 := uint8(5)
|
||||||
|
pui8 := &ui8
|
||||||
|
ppui8 := &pui8
|
||||||
|
|
||||||
|
// Create a circular data type.
|
||||||
|
type circular struct {
|
||||||
|
ui8 uint8
|
||||||
|
c *circular
|
||||||
|
}
|
||||||
|
c := circular{ui8: 1}
|
||||||
|
c.c = &c
|
||||||
|
|
||||||
|
// Print!
|
||||||
|
spew.Printf("ppui8: %v\n", ppui8)
|
||||||
|
spew.Printf("circular: %v\n", c)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// ppui8: <**>5
|
||||||
|
// circular: {1 <*>{1 <*><shown>}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This example demonstrates how to use a ConfigState.
|
||||||
|
func ExampleConfigState() {
|
||||||
|
// Modify the indent level of the ConfigState only. The global
|
||||||
|
// configuration is not modified.
|
||||||
|
scs := spew.ConfigState{Indent: "\t"}
|
||||||
|
|
||||||
|
// Output using the ConfigState instance.
|
||||||
|
v := map[string]int{"one": 1}
|
||||||
|
scs.Printf("v: %v\n", v)
|
||||||
|
scs.Dump(v)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// v: map[one:1]
|
||||||
|
// (map[string]int) (len=1) {
|
||||||
|
// (string) (len=3) "one": (int) 1
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// This example demonstrates how to use ConfigState.Dump to dump variables to
|
||||||
|
// stdout
|
||||||
|
func ExampleConfigState_Dump() {
|
||||||
|
// See the top-level Dump example for details on the types used in this
|
||||||
|
// example.
|
||||||
|
|
||||||
|
// Create two ConfigState instances with different indentation.
|
||||||
|
scs := spew.ConfigState{Indent: "\t"}
|
||||||
|
scs2 := spew.ConfigState{Indent: " "}
|
||||||
|
|
||||||
|
// Setup some sample data structures for the example.
|
||||||
|
bar := Bar{uintptr(0)}
|
||||||
|
s1 := Foo{bar, map[interface{}]interface{}{"one": true}}
|
||||||
|
|
||||||
|
// Dump using the ConfigState instances.
|
||||||
|
scs.Dump(s1)
|
||||||
|
scs2.Dump(s1)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// (spew_test.Foo) {
|
||||||
|
// unexportedField: (spew_test.Bar) {
|
||||||
|
// data: (uintptr) <nil>
|
||||||
|
// },
|
||||||
|
// ExportedField: (map[interface {}]interface {}) (len=1) {
|
||||||
|
// (string) (len=3) "one": (bool) true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// (spew_test.Foo) {
|
||||||
|
// unexportedField: (spew_test.Bar) {
|
||||||
|
// data: (uintptr) <nil>
|
||||||
|
// },
|
||||||
|
// ExportedField: (map[interface {}]interface {}) (len=1) {
|
||||||
|
// (string) (len=3) "one": (bool) true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
// This example demonstrates how to use ConfigState.Printf to display a variable
|
||||||
|
// with a format string and inline formatting.
|
||||||
|
func ExampleConfigState_Printf() {
|
||||||
|
// See the top-level Dump example for details on the types used in this
|
||||||
|
// example.
|
||||||
|
|
||||||
|
// Create two ConfigState instances and modify the method handling of the
|
||||||
|
// first ConfigState only.
|
||||||
|
scs := spew.NewDefaultConfig()
|
||||||
|
scs2 := spew.NewDefaultConfig()
|
||||||
|
scs.DisableMethods = true
|
||||||
|
|
||||||
|
// Alternatively
|
||||||
|
// scs := spew.ConfigState{Indent: " ", DisableMethods: true}
|
||||||
|
// scs2 := spew.ConfigState{Indent: " "}
|
||||||
|
|
||||||
|
// This is of type Flag which implements a Stringer and has raw value 1.
|
||||||
|
f := flagTwo
|
||||||
|
|
||||||
|
// Dump using the ConfigState instances.
|
||||||
|
scs.Printf("f: %v\n", f)
|
||||||
|
scs2.Printf("f: %v\n", f)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// f: 1
|
||||||
|
// f: flagTwo
|
||||||
|
}
|
|
@ -0,0 +1,419 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// supportedFlags is a list of all the character flags supported by fmt package.
|
||||||
|
const supportedFlags = "0-+# "
|
||||||
|
|
||||||
|
// formatState implements the fmt.Formatter interface and contains information
|
||||||
|
// about the state of a formatting operation. The NewFormatter function can
|
||||||
|
// be used to get a new Formatter which can be used directly as arguments
|
||||||
|
// in standard fmt package printing calls.
|
||||||
|
type formatState struct {
|
||||||
|
value interface{}
|
||||||
|
fs fmt.State
|
||||||
|
depth int
|
||||||
|
pointers map[uintptr]int
|
||||||
|
ignoreNextType bool
|
||||||
|
cs *ConfigState
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDefaultFormat recreates the original format string without precision
|
||||||
|
// and width information to pass in to fmt.Sprintf in the case of an
|
||||||
|
// unrecognized type. Unless new types are added to the language, this
|
||||||
|
// function won't ever be called.
|
||||||
|
func (f *formatState) buildDefaultFormat() (format string) {
|
||||||
|
buf := bytes.NewBuffer(percentBytes)
|
||||||
|
|
||||||
|
for _, flag := range supportedFlags {
|
||||||
|
if f.fs.Flag(int(flag)) {
|
||||||
|
buf.WriteRune(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteRune('v')
|
||||||
|
|
||||||
|
format = buf.String()
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructOrigFormat recreates the original format string including precision
|
||||||
|
// and width information to pass along to the standard fmt package. This allows
|
||||||
|
// automatic deferral of all format strings this package doesn't support.
|
||||||
|
func (f *formatState) constructOrigFormat(verb rune) (format string) {
|
||||||
|
buf := bytes.NewBuffer(percentBytes)
|
||||||
|
|
||||||
|
for _, flag := range supportedFlags {
|
||||||
|
if f.fs.Flag(int(flag)) {
|
||||||
|
buf.WriteRune(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if width, ok := f.fs.Width(); ok {
|
||||||
|
buf.WriteString(strconv.Itoa(width))
|
||||||
|
}
|
||||||
|
|
||||||
|
if precision, ok := f.fs.Precision(); ok {
|
||||||
|
buf.Write(precisionBytes)
|
||||||
|
buf.WriteString(strconv.Itoa(precision))
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteRune(verb)
|
||||||
|
|
||||||
|
format = buf.String()
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
|
||||||
|
// unpackValue returns values inside of non-nil interfaces when possible and
|
||||||
|
// ensures that types for values which have been unpacked from an interface
|
||||||
|
// are displayed when the show types flag is also set.
|
||||||
|
// This is useful for data types like structs, arrays, slices, and maps which
|
||||||
|
// can contain varying types packed inside an interface.
|
||||||
|
func (f *formatState) unpackValue(v reflect.Value) reflect.Value {
|
||||||
|
if v.Kind() == reflect.Interface {
|
||||||
|
f.ignoreNextType = false
|
||||||
|
if !v.IsNil() {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatPtr handles formatting of pointers by indirecting them as necessary.
|
||||||
|
func (f *formatState) formatPtr(v reflect.Value) {
|
||||||
|
// Display nil if top level pointer is nil.
|
||||||
|
showTypes := f.fs.Flag('#')
|
||||||
|
if v.IsNil() && (!showTypes || f.ignoreNextType) {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove pointers at or below the current depth from map used to detect
|
||||||
|
// circular refs.
|
||||||
|
for k, depth := range f.pointers {
|
||||||
|
if depth >= f.depth {
|
||||||
|
delete(f.pointers, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep list of all dereferenced pointers to possibly show later.
|
||||||
|
pointerChain := make([]uintptr, 0)
|
||||||
|
|
||||||
|
// Figure out how many levels of indirection there are by derferencing
|
||||||
|
// pointers and unpacking interfaces down the chain while detecting circular
|
||||||
|
// references.
|
||||||
|
nilFound := false
|
||||||
|
cycleFound := false
|
||||||
|
indirects := 0
|
||||||
|
ve := v
|
||||||
|
for ve.Kind() == reflect.Ptr {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
indirects++
|
||||||
|
addr := ve.Pointer()
|
||||||
|
pointerChain = append(pointerChain, addr)
|
||||||
|
if pd, ok := f.pointers[addr]; ok && pd < f.depth {
|
||||||
|
cycleFound = true
|
||||||
|
indirects--
|
||||||
|
break
|
||||||
|
}
|
||||||
|
f.pointers[addr] = f.depth
|
||||||
|
|
||||||
|
ve = ve.Elem()
|
||||||
|
if ve.Kind() == reflect.Interface {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ve = ve.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display type or indirection level depending on flags.
|
||||||
|
if showTypes && !f.ignoreNextType {
|
||||||
|
f.fs.Write(openParenBytes)
|
||||||
|
f.fs.Write(bytes.Repeat(asteriskBytes, indirects))
|
||||||
|
f.fs.Write([]byte(ve.Type().String()))
|
||||||
|
f.fs.Write(closeParenBytes)
|
||||||
|
} else {
|
||||||
|
if nilFound || cycleFound {
|
||||||
|
indirects += strings.Count(ve.Type().String(), "*")
|
||||||
|
}
|
||||||
|
f.fs.Write(openAngleBytes)
|
||||||
|
f.fs.Write([]byte(strings.Repeat("*", indirects)))
|
||||||
|
f.fs.Write(closeAngleBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display pointer information depending on flags.
|
||||||
|
if f.fs.Flag('+') && (len(pointerChain) > 0) {
|
||||||
|
f.fs.Write(openParenBytes)
|
||||||
|
for i, addr := range pointerChain {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(pointerChainBytes)
|
||||||
|
}
|
||||||
|
printHexPtr(f.fs, addr)
|
||||||
|
}
|
||||||
|
f.fs.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display dereferenced value.
|
||||||
|
switch {
|
||||||
|
case nilFound == true:
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
|
||||||
|
case cycleFound == true:
|
||||||
|
f.fs.Write(circularShortBytes)
|
||||||
|
|
||||||
|
default:
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(ve)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// format is the main workhorse for providing the Formatter interface. It
|
||||||
|
// uses the passed reflect value to figure out what kind of object we are
|
||||||
|
// dealing with and formats it appropriately. It is a recursive function,
|
||||||
|
// however circular data structures are detected and handled properly.
|
||||||
|
func (f *formatState) format(v reflect.Value) {
|
||||||
|
// Handle invalid reflect values immediately.
|
||||||
|
kind := v.Kind()
|
||||||
|
if kind == reflect.Invalid {
|
||||||
|
f.fs.Write(invalidAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pointers specially.
|
||||||
|
if kind == reflect.Ptr {
|
||||||
|
f.formatPtr(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print type information unless already handled elsewhere.
|
||||||
|
if !f.ignoreNextType && f.fs.Flag('#') {
|
||||||
|
f.fs.Write(openParenBytes)
|
||||||
|
f.fs.Write([]byte(v.Type().String()))
|
||||||
|
f.fs.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
f.ignoreNextType = false
|
||||||
|
|
||||||
|
// Call Stringer/error interfaces if they exist and the handle methods
|
||||||
|
// flag is enabled.
|
||||||
|
if !f.cs.DisableMethods {
|
||||||
|
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
|
||||||
|
if handled := handleMethods(f.cs, f.fs, v); handled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case reflect.Invalid:
|
||||||
|
// Do nothing. We should never get here since invalid has already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Bool:
|
||||||
|
printBool(f.fs, v.Bool())
|
||||||
|
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
printInt(f.fs, v.Int(), 10)
|
||||||
|
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
printUint(f.fs, v.Uint(), 10)
|
||||||
|
|
||||||
|
case reflect.Float32:
|
||||||
|
printFloat(f.fs, v.Float(), 32)
|
||||||
|
|
||||||
|
case reflect.Float64:
|
||||||
|
printFloat(f.fs, v.Float(), 64)
|
||||||
|
|
||||||
|
case reflect.Complex64:
|
||||||
|
printComplex(f.fs, v.Complex(), 32)
|
||||||
|
|
||||||
|
case reflect.Complex128:
|
||||||
|
printComplex(f.fs, v.Complex(), 64)
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
if v.IsNil() {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case reflect.Array:
|
||||||
|
f.fs.Write(openBracketBytes)
|
||||||
|
f.depth++
|
||||||
|
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||||
|
f.fs.Write(maxShortBytes)
|
||||||
|
} else {
|
||||||
|
numEntries := v.Len()
|
||||||
|
for i := 0; i < numEntries; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(f.unpackValue(v.Index(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.depth--
|
||||||
|
f.fs.Write(closeBracketBytes)
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
f.fs.Write([]byte(v.String()))
|
||||||
|
|
||||||
|
case reflect.Interface:
|
||||||
|
// The only time we should get here is for nil interfaces due to
|
||||||
|
// unpackValue calls.
|
||||||
|
if v.IsNil() {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Ptr:
|
||||||
|
// Do nothing. We should never get here since pointers have already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
// nil maps should be indicated as different than empty maps
|
||||||
|
if v.IsNil() {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
f.fs.Write(openMapBytes)
|
||||||
|
f.depth++
|
||||||
|
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||||
|
f.fs.Write(maxShortBytes)
|
||||||
|
} else {
|
||||||
|
keys := v.MapKeys()
|
||||||
|
if f.cs.SortKeys {
|
||||||
|
sortValues(keys, f.cs)
|
||||||
|
}
|
||||||
|
for i, key := range keys {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(f.unpackValue(key))
|
||||||
|
f.fs.Write(colonBytes)
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(f.unpackValue(v.MapIndex(key)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.depth--
|
||||||
|
f.fs.Write(closeMapBytes)
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
numFields := v.NumField()
|
||||||
|
f.fs.Write(openBraceBytes)
|
||||||
|
f.depth++
|
||||||
|
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||||
|
f.fs.Write(maxShortBytes)
|
||||||
|
} else {
|
||||||
|
vt := v.Type()
|
||||||
|
for i := 0; i < numFields; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
vtf := vt.Field(i)
|
||||||
|
if f.fs.Flag('+') || f.fs.Flag('#') {
|
||||||
|
f.fs.Write([]byte(vtf.Name))
|
||||||
|
f.fs.Write(colonBytes)
|
||||||
|
}
|
||||||
|
f.format(f.unpackValue(v.Field(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.depth--
|
||||||
|
f.fs.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.Uintptr:
|
||||||
|
printHexPtr(f.fs, uintptr(v.Uint()))
|
||||||
|
|
||||||
|
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||||||
|
printHexPtr(f.fs, v.Pointer())
|
||||||
|
|
||||||
|
// There were not any other types at the time this code was written, but
|
||||||
|
// fall back to letting the default fmt package handle it if any get added.
|
||||||
|
default:
|
||||||
|
format := f.buildDefaultFormat()
|
||||||
|
if v.CanInterface() {
|
||||||
|
fmt.Fprintf(f.fs, format, v.Interface())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(f.fs, format, v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format satisfies the fmt.Formatter interface. See NewFormatter for usage
|
||||||
|
// details.
|
||||||
|
func (f *formatState) Format(fs fmt.State, verb rune) {
|
||||||
|
f.fs = fs
|
||||||
|
|
||||||
|
// Use standard formatting for verbs that are not v.
|
||||||
|
if verb != 'v' {
|
||||||
|
format := f.constructOrigFormat(verb)
|
||||||
|
fmt.Fprintf(fs, format, f.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.value == nil {
|
||||||
|
if fs.Flag('#') {
|
||||||
|
fs.Write(interfaceBytes)
|
||||||
|
}
|
||||||
|
fs.Write(nilAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.format(reflect.ValueOf(f.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFormatter is a helper function to consolidate the logic from the various
|
||||||
|
// public methods which take varying config states.
|
||||||
|
func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter {
|
||||||
|
fs := &formatState{value: v, cs: cs}
|
||||||
|
fs.pointers = make(map[uintptr]int)
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
|
||||||
|
interface. As a result, it integrates cleanly with standard fmt package
|
||||||
|
printing functions. The formatter is useful for inline printing of smaller data
|
||||||
|
types similar to the standard %v format specifier.
|
||||||
|
|
||||||
|
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||||
|
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
|
||||||
|
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||||
|
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||||
|
the width and precision arguments (however they will still work on the format
|
||||||
|
specifiers not handled by the custom formatter).
|
||||||
|
|
||||||
|
Typically this function shouldn't be called directly. It is much easier to make
|
||||||
|
use of the custom formatter by calling one of the convenience functions such as
|
||||||
|
Printf, Println, or Fprintf.
|
||||||
|
*/
|
||||||
|
func NewFormatter(v interface{}) fmt.Formatter {
|
||||||
|
return newFormatter(&Config, v)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
This test file is part of the spew package rather than than the spew_test
|
||||||
|
package because it needs access to internals to properly test certain cases
|
||||||
|
which are not possible via the public interface since they should never happen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dummyFmtState implements a fake fmt.State to use for testing invalid
|
||||||
|
// reflect.Value handling. This is necessary because the fmt package catches
|
||||||
|
// invalid values before invoking the formatter on them.
|
||||||
|
type dummyFmtState struct {
|
||||||
|
bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfs *dummyFmtState) Flag(f int) bool {
|
||||||
|
if f == int('+') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfs *dummyFmtState) Precision() (int, bool) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfs *dummyFmtState) Width() (int, bool) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInvalidReflectValue ensures the dump and formatter code handles an
|
||||||
|
// invalid reflect value properly. This needs access to internal state since it
|
||||||
|
// should never happen in real code and therefore can't be tested via the public
|
||||||
|
// API.
|
||||||
|
func TestInvalidReflectValue(t *testing.T) {
|
||||||
|
i := 1
|
||||||
|
|
||||||
|
// Dump invalid reflect value.
|
||||||
|
v := new(reflect.Value)
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
d := dumpState{w: buf, cs: &Config}
|
||||||
|
d.dump(*v)
|
||||||
|
s := buf.String()
|
||||||
|
want := "<invalid>"
|
||||||
|
if s != want {
|
||||||
|
t.Errorf("InvalidReflectValue #%d\n got: %s want: %s", i, s, want)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
// Formatter invalid reflect value.
|
||||||
|
buf2 := new(dummyFmtState)
|
||||||
|
f := formatState{value: *v, cs: &Config, fs: buf2}
|
||||||
|
f.format(*v)
|
||||||
|
s = buf2.String()
|
||||||
|
want = "<invalid>"
|
||||||
|
if s != want {
|
||||||
|
t.Errorf("InvalidReflectValue #%d got: %s want: %s", i, s, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortValues makes the internal sortValues function available to the test
|
||||||
|
// package.
|
||||||
|
func SortValues(values []reflect.Value, cs *ConfigState) {
|
||||||
|
sortValues(values, cs)
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||||
|
// when the code is not running on Google App Engine, compiled by GopherJS, and
|
||||||
|
// "-tags safe" is not added to the go build command line. The "disableunsafe"
|
||||||
|
// tag is deprecated and thus should not be used.
|
||||||
|
// +build !js,!appengine,!safe,!disableunsafe
|
||||||
|
|
||||||
|
/*
|
||||||
|
This test file is part of the spew package rather than than the spew_test
|
||||||
|
package because it needs access to internals to properly test certain cases
|
||||||
|
which are not possible via the public interface since they should never happen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// changeKind uses unsafe to intentionally change the kind of a reflect.Value to
|
||||||
|
// the maximum kind value which does not exist. This is needed to test the
|
||||||
|
// fallback code which punts to the standard fmt library for new types that
|
||||||
|
// might get added to the language.
|
||||||
|
func changeKind(v *reflect.Value, readOnly bool) {
|
||||||
|
rvf := (*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + offsetFlag))
|
||||||
|
*rvf = *rvf | ((1<<flagKindWidth - 1) << flagKindShift)
|
||||||
|
if readOnly {
|
||||||
|
*rvf |= flagRO
|
||||||
|
} else {
|
||||||
|
*rvf &= ^uintptr(flagRO)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddedReflectValue tests functionaly of the dump and formatter code which
|
||||||
|
// falls back to the standard fmt library for new types that might get added to
|
||||||
|
// the language.
|
||||||
|
func TestAddedReflectValue(t *testing.T) {
|
||||||
|
i := 1
|
||||||
|
|
||||||
|
// Dump using a reflect.Value that is exported.
|
||||||
|
v := reflect.ValueOf(int8(5))
|
||||||
|
changeKind(&v, false)
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
d := dumpState{w: buf, cs: &Config}
|
||||||
|
d.dump(v)
|
||||||
|
s := buf.String()
|
||||||
|
want := "(int8) 5"
|
||||||
|
if s != want {
|
||||||
|
t.Errorf("TestAddedReflectValue #%d\n got: %s want: %s", i, s, want)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
// Dump using a reflect.Value that is not exported.
|
||||||
|
changeKind(&v, true)
|
||||||
|
buf.Reset()
|
||||||
|
d.dump(v)
|
||||||
|
s = buf.String()
|
||||||
|
want = "(int8) <int8 Value>"
|
||||||
|
if s != want {
|
||||||
|
t.Errorf("TestAddedReflectValue #%d\n got: %s want: %s", i, s, want)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
// Formatter using a reflect.Value that is exported.
|
||||||
|
changeKind(&v, false)
|
||||||
|
buf2 := new(dummyFmtState)
|
||||||
|
f := formatState{value: v, cs: &Config, fs: buf2}
|
||||||
|
f.format(v)
|
||||||
|
s = buf2.String()
|
||||||
|
want = "5"
|
||||||
|
if s != want {
|
||||||
|
t.Errorf("TestAddedReflectValue #%d got: %s want: %s", i, s, want)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
// Formatter using a reflect.Value that is not exported.
|
||||||
|
changeKind(&v, true)
|
||||||
|
buf2.Reset()
|
||||||
|
f = formatState{value: v, cs: &Config, fs: buf2}
|
||||||
|
f.format(v)
|
||||||
|
s = buf2.String()
|
||||||
|
want = "<int8 Value>"
|
||||||
|
if s != want {
|
||||||
|
t.Errorf("TestAddedReflectValue #%d got: %s want: %s", i, s, want)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the formatted string as a value that satisfies error. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Errorf(format string, a ...interface{}) (err error) {
|
||||||
|
return fmt.Errorf(format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprint(w, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintf(w, format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintln(w, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print is a wrapper for fmt.Print that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Print(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Print(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Printf(format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Printf(format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println is a wrapper for fmt.Println that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Println(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Println(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Sprint(a ...interface{}) string {
|
||||||
|
return fmt.Sprint(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Sprintf(format string, a ...interface{}) string {
|
||||||
|
return fmt.Sprintf(format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
|
||||||
|
// were passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Sprintln(a ...interface{}) string {
|
||||||
|
return fmt.Sprintln(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertArgs accepts a slice of arguments and returns a slice of the same
|
||||||
|
// length with each argument converted to a default spew Formatter interface.
|
||||||
|
func convertArgs(args []interface{}) (formatters []interface{}) {
|
||||||
|
formatters = make([]interface{}, len(args))
|
||||||
|
for index, arg := range args {
|
||||||
|
formatters[index] = NewFormatter(arg)
|
||||||
|
}
|
||||||
|
return formatters
|
||||||
|
}
|
|
@ -0,0 +1,320 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
)
|
||||||
|
|
||||||
|
// spewFunc is used to identify which public function of the spew package or
|
||||||
|
// ConfigState a test applies to.
|
||||||
|
type spewFunc int
|
||||||
|
|
||||||
|
const (
|
||||||
|
fCSFdump spewFunc = iota
|
||||||
|
fCSFprint
|
||||||
|
fCSFprintf
|
||||||
|
fCSFprintln
|
||||||
|
fCSPrint
|
||||||
|
fCSPrintln
|
||||||
|
fCSSdump
|
||||||
|
fCSSprint
|
||||||
|
fCSSprintf
|
||||||
|
fCSSprintln
|
||||||
|
fCSErrorf
|
||||||
|
fCSNewFormatter
|
||||||
|
fErrorf
|
||||||
|
fFprint
|
||||||
|
fFprintln
|
||||||
|
fPrint
|
||||||
|
fPrintln
|
||||||
|
fSdump
|
||||||
|
fSprint
|
||||||
|
fSprintf
|
||||||
|
fSprintln
|
||||||
|
)
|
||||||
|
|
||||||
|
// Map of spewFunc values to names for pretty printing.
|
||||||
|
var spewFuncStrings = map[spewFunc]string{
|
||||||
|
fCSFdump: "ConfigState.Fdump",
|
||||||
|
fCSFprint: "ConfigState.Fprint",
|
||||||
|
fCSFprintf: "ConfigState.Fprintf",
|
||||||
|
fCSFprintln: "ConfigState.Fprintln",
|
||||||
|
fCSSdump: "ConfigState.Sdump",
|
||||||
|
fCSPrint: "ConfigState.Print",
|
||||||
|
fCSPrintln: "ConfigState.Println",
|
||||||
|
fCSSprint: "ConfigState.Sprint",
|
||||||
|
fCSSprintf: "ConfigState.Sprintf",
|
||||||
|
fCSSprintln: "ConfigState.Sprintln",
|
||||||
|
fCSErrorf: "ConfigState.Errorf",
|
||||||
|
fCSNewFormatter: "ConfigState.NewFormatter",
|
||||||
|
fErrorf: "spew.Errorf",
|
||||||
|
fFprint: "spew.Fprint",
|
||||||
|
fFprintln: "spew.Fprintln",
|
||||||
|
fPrint: "spew.Print",
|
||||||
|
fPrintln: "spew.Println",
|
||||||
|
fSdump: "spew.Sdump",
|
||||||
|
fSprint: "spew.Sprint",
|
||||||
|
fSprintf: "spew.Sprintf",
|
||||||
|
fSprintln: "spew.Sprintln",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f spewFunc) String() string {
|
||||||
|
if s, ok := spewFuncStrings[f]; ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Unknown spewFunc (%d)", int(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
// spewTest is used to describe a test to be performed against the public
|
||||||
|
// functions of the spew package or ConfigState.
|
||||||
|
type spewTest struct {
|
||||||
|
cs *spew.ConfigState
|
||||||
|
f spewFunc
|
||||||
|
format string
|
||||||
|
in interface{}
|
||||||
|
want string
|
||||||
|
}
|
||||||
|
|
||||||
|
// spewTests houses the tests to be performed against the public functions of
|
||||||
|
// the spew package and ConfigState.
|
||||||
|
//
|
||||||
|
// These tests are only intended to ensure the public functions are exercised
|
||||||
|
// and are intentionally not exhaustive of types. The exhaustive type
|
||||||
|
// tests are handled in the dump and format tests.
|
||||||
|
var spewTests []spewTest
|
||||||
|
|
||||||
|
// redirStdout is a helper function to return the standard output from f as a
|
||||||
|
// byte slice.
|
||||||
|
func redirStdout(f func()) ([]byte, error) {
|
||||||
|
tempFile, err := ioutil.TempFile("", "ss-test")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fileName := tempFile.Name()
|
||||||
|
defer os.Remove(fileName) // Ignore error
|
||||||
|
|
||||||
|
origStdout := os.Stdout
|
||||||
|
os.Stdout = tempFile
|
||||||
|
f()
|
||||||
|
os.Stdout = origStdout
|
||||||
|
tempFile.Close()
|
||||||
|
|
||||||
|
return ioutil.ReadFile(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSpewTests() {
|
||||||
|
// Config states with various settings.
|
||||||
|
scsDefault := spew.NewDefaultConfig()
|
||||||
|
scsNoMethods := &spew.ConfigState{Indent: " ", DisableMethods: true}
|
||||||
|
scsNoPmethods := &spew.ConfigState{Indent: " ", DisablePointerMethods: true}
|
||||||
|
scsMaxDepth := &spew.ConfigState{Indent: " ", MaxDepth: 1}
|
||||||
|
scsContinue := &spew.ConfigState{Indent: " ", ContinueOnMethod: true}
|
||||||
|
scsNoPtrAddr := &spew.ConfigState{DisablePointerAddresses: true}
|
||||||
|
scsNoCap := &spew.ConfigState{DisableCapacities: true}
|
||||||
|
|
||||||
|
// Variables for tests on types which implement Stringer interface with and
|
||||||
|
// without a pointer receiver.
|
||||||
|
ts := stringer("test")
|
||||||
|
tps := pstringer("test")
|
||||||
|
|
||||||
|
type ptrTester struct {
|
||||||
|
s *struct{}
|
||||||
|
}
|
||||||
|
tptr := &ptrTester{s: &struct{}{}}
|
||||||
|
|
||||||
|
// depthTester is used to test max depth handling for structs, array, slices
|
||||||
|
// and maps.
|
||||||
|
type depthTester struct {
|
||||||
|
ic indirCir1
|
||||||
|
arr [1]string
|
||||||
|
slice []string
|
||||||
|
m map[string]int
|
||||||
|
}
|
||||||
|
dt := depthTester{indirCir1{nil}, [1]string{"arr"}, []string{"slice"},
|
||||||
|
map[string]int{"one": 1}}
|
||||||
|
|
||||||
|
// Variable for tests on types which implement error interface.
|
||||||
|
te := customError(10)
|
||||||
|
|
||||||
|
spewTests = []spewTest{
|
||||||
|
{scsDefault, fCSFdump, "", int8(127), "(int8) 127\n"},
|
||||||
|
{scsDefault, fCSFprint, "", int16(32767), "32767"},
|
||||||
|
{scsDefault, fCSFprintf, "%v", int32(2147483647), "2147483647"},
|
||||||
|
{scsDefault, fCSFprintln, "", int(2147483647), "2147483647\n"},
|
||||||
|
{scsDefault, fCSPrint, "", int64(9223372036854775807), "9223372036854775807"},
|
||||||
|
{scsDefault, fCSPrintln, "", uint8(255), "255\n"},
|
||||||
|
{scsDefault, fCSSdump, "", uint8(64), "(uint8) 64\n"},
|
||||||
|
{scsDefault, fCSSprint, "", complex(1, 2), "(1+2i)"},
|
||||||
|
{scsDefault, fCSSprintf, "%v", complex(float32(3), 4), "(3+4i)"},
|
||||||
|
{scsDefault, fCSSprintln, "", complex(float64(5), 6), "(5+6i)\n"},
|
||||||
|
{scsDefault, fCSErrorf, "%#v", uint16(65535), "(uint16)65535"},
|
||||||
|
{scsDefault, fCSNewFormatter, "%v", uint32(4294967295), "4294967295"},
|
||||||
|
{scsDefault, fErrorf, "%v", uint64(18446744073709551615), "18446744073709551615"},
|
||||||
|
{scsDefault, fFprint, "", float32(3.14), "3.14"},
|
||||||
|
{scsDefault, fFprintln, "", float64(6.28), "6.28\n"},
|
||||||
|
{scsDefault, fPrint, "", true, "true"},
|
||||||
|
{scsDefault, fPrintln, "", false, "false\n"},
|
||||||
|
{scsDefault, fSdump, "", complex(-10, -20), "(complex128) (-10-20i)\n"},
|
||||||
|
{scsDefault, fSprint, "", complex(-1, -2), "(-1-2i)"},
|
||||||
|
{scsDefault, fSprintf, "%v", complex(float32(-3), -4), "(-3-4i)"},
|
||||||
|
{scsDefault, fSprintln, "", complex(float64(-5), -6), "(-5-6i)\n"},
|
||||||
|
{scsNoMethods, fCSFprint, "", ts, "test"},
|
||||||
|
{scsNoMethods, fCSFprint, "", &ts, "<*>test"},
|
||||||
|
{scsNoMethods, fCSFprint, "", tps, "test"},
|
||||||
|
{scsNoMethods, fCSFprint, "", &tps, "<*>test"},
|
||||||
|
{scsNoPmethods, fCSFprint, "", ts, "stringer test"},
|
||||||
|
{scsNoPmethods, fCSFprint, "", &ts, "<*>stringer test"},
|
||||||
|
{scsNoPmethods, fCSFprint, "", tps, "test"},
|
||||||
|
{scsNoPmethods, fCSFprint, "", &tps, "<*>stringer test"},
|
||||||
|
{scsMaxDepth, fCSFprint, "", dt, "{{<max>} [<max>] [<max>] map[<max>]}"},
|
||||||
|
{scsMaxDepth, fCSFdump, "", dt, "(spew_test.depthTester) {\n" +
|
||||||
|
" ic: (spew_test.indirCir1) {\n <max depth reached>\n },\n" +
|
||||||
|
" arr: ([1]string) (len=1 cap=1) {\n <max depth reached>\n },\n" +
|
||||||
|
" slice: ([]string) (len=1 cap=1) {\n <max depth reached>\n },\n" +
|
||||||
|
" m: (map[string]int) (len=1) {\n <max depth reached>\n }\n}\n"},
|
||||||
|
{scsContinue, fCSFprint, "", ts, "(stringer test) test"},
|
||||||
|
{scsContinue, fCSFdump, "", ts, "(spew_test.stringer) " +
|
||||||
|
"(len=4) (stringer test) \"test\"\n"},
|
||||||
|
{scsContinue, fCSFprint, "", te, "(error: 10) 10"},
|
||||||
|
{scsContinue, fCSFdump, "", te, "(spew_test.customError) " +
|
||||||
|
"(error: 10) 10\n"},
|
||||||
|
{scsNoPtrAddr, fCSFprint, "", tptr, "<*>{<*>{}}"},
|
||||||
|
{scsNoPtrAddr, fCSSdump, "", tptr, "(*spew_test.ptrTester)({\ns: (*struct {})({\n})\n})\n"},
|
||||||
|
{scsNoCap, fCSSdump, "", make([]string, 0, 10), "([]string) {\n}\n"},
|
||||||
|
{scsNoCap, fCSSdump, "", make([]string, 1, 10), "([]string) (len=1) {\n(string) \"\"\n}\n"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSpew executes all of the tests described by spewTests.
|
||||||
|
func TestSpew(t *testing.T) {
|
||||||
|
initSpewTests()
|
||||||
|
|
||||||
|
t.Logf("Running %d tests", len(spewTests))
|
||||||
|
for i, test := range spewTests {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
switch test.f {
|
||||||
|
case fCSFdump:
|
||||||
|
test.cs.Fdump(buf, test.in)
|
||||||
|
|
||||||
|
case fCSFprint:
|
||||||
|
test.cs.Fprint(buf, test.in)
|
||||||
|
|
||||||
|
case fCSFprintf:
|
||||||
|
test.cs.Fprintf(buf, test.format, test.in)
|
||||||
|
|
||||||
|
case fCSFprintln:
|
||||||
|
test.cs.Fprintln(buf, test.in)
|
||||||
|
|
||||||
|
case fCSPrint:
|
||||||
|
b, err := redirStdout(func() { test.cs.Print(test.in) })
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v #%d %v", test.f, i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf.Write(b)
|
||||||
|
|
||||||
|
case fCSPrintln:
|
||||||
|
b, err := redirStdout(func() { test.cs.Println(test.in) })
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v #%d %v", test.f, i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf.Write(b)
|
||||||
|
|
||||||
|
case fCSSdump:
|
||||||
|
str := test.cs.Sdump(test.in)
|
||||||
|
buf.WriteString(str)
|
||||||
|
|
||||||
|
case fCSSprint:
|
||||||
|
str := test.cs.Sprint(test.in)
|
||||||
|
buf.WriteString(str)
|
||||||
|
|
||||||
|
case fCSSprintf:
|
||||||
|
str := test.cs.Sprintf(test.format, test.in)
|
||||||
|
buf.WriteString(str)
|
||||||
|
|
||||||
|
case fCSSprintln:
|
||||||
|
str := test.cs.Sprintln(test.in)
|
||||||
|
buf.WriteString(str)
|
||||||
|
|
||||||
|
case fCSErrorf:
|
||||||
|
err := test.cs.Errorf(test.format, test.in)
|
||||||
|
buf.WriteString(err.Error())
|
||||||
|
|
||||||
|
case fCSNewFormatter:
|
||||||
|
fmt.Fprintf(buf, test.format, test.cs.NewFormatter(test.in))
|
||||||
|
|
||||||
|
case fErrorf:
|
||||||
|
err := spew.Errorf(test.format, test.in)
|
||||||
|
buf.WriteString(err.Error())
|
||||||
|
|
||||||
|
case fFprint:
|
||||||
|
spew.Fprint(buf, test.in)
|
||||||
|
|
||||||
|
case fFprintln:
|
||||||
|
spew.Fprintln(buf, test.in)
|
||||||
|
|
||||||
|
case fPrint:
|
||||||
|
b, err := redirStdout(func() { spew.Print(test.in) })
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v #%d %v", test.f, i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf.Write(b)
|
||||||
|
|
||||||
|
case fPrintln:
|
||||||
|
b, err := redirStdout(func() { spew.Println(test.in) })
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v #%d %v", test.f, i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf.Write(b)
|
||||||
|
|
||||||
|
case fSdump:
|
||||||
|
str := spew.Sdump(test.in)
|
||||||
|
buf.WriteString(str)
|
||||||
|
|
||||||
|
case fSprint:
|
||||||
|
str := spew.Sprint(test.in)
|
||||||
|
buf.WriteString(str)
|
||||||
|
|
||||||
|
case fSprintf:
|
||||||
|
str := spew.Sprintf(test.format, test.in)
|
||||||
|
buf.WriteString(str)
|
||||||
|
|
||||||
|
case fSprintln:
|
||||||
|
str := spew.Sprintln(test.in)
|
||||||
|
buf.WriteString(str)
|
||||||
|
|
||||||
|
default:
|
||||||
|
t.Errorf("%v #%d unrecognized function", test.f, i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := buf.String()
|
||||||
|
if test.want != s {
|
||||||
|
t.Errorf("ConfigState #%d\n got: %s want: %s", i, s, test.want)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright (c) 2013 Dave Collins <dave@davec.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||||
|
// when both cgo is supported and "-tags testcgo" is added to the go test
|
||||||
|
// command line. This code should really only be in the dumpcgo_test.go file,
|
||||||
|
// but unfortunately Go will not allow cgo in test files, so this is a
|
||||||
|
// workaround to allow cgo types to be tested. This configuration is used
|
||||||
|
// because spew itself does not require cgo to run even though it does handle
|
||||||
|
// certain cgo types specially. Rather than forcing all clients to require cgo
|
||||||
|
// and an external C compiler just to run the tests, this scheme makes them
|
||||||
|
// optional.
|
||||||
|
// +build cgo,testcgo
|
||||||
|
|
||||||
|
package testdata
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <stdint.h>
|
||||||
|
typedef unsigned char custom_uchar_t;
|
||||||
|
|
||||||
|
char *ncp = 0;
|
||||||
|
char *cp = "test";
|
||||||
|
char ca[6] = {'t', 'e', 's', 't', '2', '\0'};
|
||||||
|
unsigned char uca[6] = {'t', 'e', 's', 't', '3', '\0'};
|
||||||
|
signed char sca[6] = {'t', 'e', 's', 't', '4', '\0'};
|
||||||
|
uint8_t ui8ta[6] = {'t', 'e', 's', 't', '5', '\0'};
|
||||||
|
custom_uchar_t tuca[6] = {'t', 'e', 's', 't', '6', '\0'};
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
// GetCgoNullCharPointer returns a null char pointer via cgo. This is only
|
||||||
|
// used for tests.
|
||||||
|
func GetCgoNullCharPointer() interface{} {
|
||||||
|
return C.ncp
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCgoCharPointer returns a char pointer via cgo. This is only used for
|
||||||
|
// tests.
|
||||||
|
func GetCgoCharPointer() interface{} {
|
||||||
|
return C.cp
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCgoCharArray returns a char array via cgo and the array's len and cap.
|
||||||
|
// This is only used for tests.
|
||||||
|
func GetCgoCharArray() (interface{}, int, int) {
|
||||||
|
return C.ca, len(C.ca), cap(C.ca)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCgoUnsignedCharArray returns an unsigned char array via cgo and the
|
||||||
|
// array's len and cap. This is only used for tests.
|
||||||
|
func GetCgoUnsignedCharArray() (interface{}, int, int) {
|
||||||
|
return C.uca, len(C.uca), cap(C.uca)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCgoSignedCharArray returns a signed char array via cgo and the array's len
|
||||||
|
// and cap. This is only used for tests.
|
||||||
|
func GetCgoSignedCharArray() (interface{}, int, int) {
|
||||||
|
return C.sca, len(C.sca), cap(C.sca)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCgoUint8tArray returns a uint8_t array via cgo and the array's len and
|
||||||
|
// cap. This is only used for tests.
|
||||||
|
func GetCgoUint8tArray() (interface{}, int, int) {
|
||||||
|
return C.ui8ta, len(C.ui8ta), cap(C.ui8ta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCgoTypdefedUnsignedCharArray returns a typedefed unsigned char array via
|
||||||
|
// cgo and the array's len and cap. This is only used for tests.
|
||||||
|
func GetCgoTypdefedUnsignedCharArray() (interface{}, int, int) {
|
||||||
|
return C.tuca, len(C.tuca), cap(C.tuca)
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
// Package strftime provides the POSIX strftime(3) function.
|
||||||
|
package strftime
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Strftime implements the POSIX strftime(3) function. The underlying
|
||||||
|
implementation uses the native C library function on Linux and Darwin, and a
|
||||||
|
pure Go replacement otherwise. There are some functional differences between
|
||||||
|
the implementations; see also the documentation of StrftimePure.
|
||||||
|
|
||||||
|
The following documentation is adapted from the linux man-pages project.
|
||||||
|
|
||||||
|
The Strftime function formats the time `t` according to the format
|
||||||
|
specification `format` and places the result in the string s.
|
||||||
|
|
||||||
|
The format specification is a string and may contain special character
|
||||||
|
sequences called conversion specifications, each of which is introduced by a
|
||||||
|
'%' character and terminated by some other character known as a conversion
|
||||||
|
specifier character. All other character sequences are ordinary character
|
||||||
|
sequences.
|
||||||
|
|
||||||
|
The characters of ordinary character sequences are copied verbatim from format
|
||||||
|
to s. However, the characters of conversion specifications are replaced as
|
||||||
|
follows:
|
||||||
|
|
||||||
|
%a The abbreviated weekday name according to the current locale.
|
||||||
|
%A The full weekday name according to the current locale.
|
||||||
|
%b The abbreviated month name according to the current locale.
|
||||||
|
%B The full month name according to the current locale.
|
||||||
|
%c The preferred date and time representation for the current locale.
|
||||||
|
%C The century number (year/100) as a 2-digit integer. (SU)
|
||||||
|
%d The day of the month as a decimal number (range 01 to 31).
|
||||||
|
%D Equivalent to %m/%d/%y. (Yecch—for Americans only. Americans should note that in other countries %d/%m/%y is rather common. This means that in international context this format is ambiguous and should not be used.) (SU)
|
||||||
|
%e Like %d, the day of the month as a decimal number, but a leading zero is replaced by a space. (SU)
|
||||||
|
%E Modifier: use alternative format, see below. (SU)
|
||||||
|
%F Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99)
|
||||||
|
%G The ISO 8601 week-based year (see NOTES) with century as a decimal number. The 4-digit year corresponding to the ISO week number (see %V). This has the same format and value as %Y, except that if the ISO week number belongs to the previous or next year, that year is used instead. (TZ)
|
||||||
|
%g Like %G, but without century, that is, with a 2-digit year (00-99). (TZ)
|
||||||
|
%h Equivalent to %b. (SU)
|
||||||
|
%H The hour as a decimal number using a 24-hour clock (range 00 to 23).
|
||||||
|
%I The hour as a decimal number using a 12-hour clock (range 01 to 12).
|
||||||
|
%j The day of the year as a decimal number (range 001 to 366).
|
||||||
|
%k The hour (24-hour clock) as a decimal number (range 0 to 23); single digits are preceded by a blank. (See also %H.) (TZ)
|
||||||
|
%l The hour (12-hour clock) as a decimal number (range 1 to 12); single digits are preceded by a blank. (See also %I.) (TZ)
|
||||||
|
%m The month as a decimal number (range 01 to 12).
|
||||||
|
%M The minute as a decimal number (range 00 to 59).
|
||||||
|
%n A newline character. (SU)
|
||||||
|
%O Modifier: use alternative format, see below. (SU)
|
||||||
|
%p Either "AM" or "PM" according to the given time value, or the corresponding strings for the current locale. Noon is treated as "PM" and midnight as "AM".
|
||||||
|
%P Like %p but in lowercase: "am" or "pm" or a corresponding string for the current locale. (GNU)
|
||||||
|
%r The time in a.m. or p.m. notation. In the POSIX locale this is equivalent to %I:%M:%S %p. (SU)
|
||||||
|
%R The time in 24-hour notation (%H:%M). (SU) For a version including the seconds, see %T below.
|
||||||
|
%s The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). (TZ)
|
||||||
|
%S The second as a decimal number (range 00 to 60). (The range is up to 60 to allow for occasional leap seconds.)
|
||||||
|
%t A tab character. (SU)
|
||||||
|
%T The time in 24-hour notation (%H:%M:%S). (SU)
|
||||||
|
%u The day of the week as a decimal, range 1 to 7, Monday being 1. See also %w. (SU)
|
||||||
|
%U The week number of the current year as a decimal number, range 00 to 53, starting with the first Sunday as the first day of week 01. See also %V and %W.
|
||||||
|
%V The ISO 8601 week number (see NOTES) of the current year as a decimal number, range 01 to 53, where week 1 is the first week that has at least 4 days in the new year. See also %U and %W. (SU)
|
||||||
|
%w The day of the week as a decimal, range 0 to 6, Sunday being 0. See also %u.
|
||||||
|
%W The week number of the current year as a decimal number, range 00 to 53, starting with the first Monday as the first day of week 01.
|
||||||
|
%x The preferred date representation for the current locale without the time.
|
||||||
|
%X The preferred time representation for the current locale without the date.
|
||||||
|
%y The year as a decimal number without a century (range 00 to 99).
|
||||||
|
%Y The year as a decimal number including the century.
|
||||||
|
%z The +hhmm or -hhmm numeric timezone (that is, the hour and minute offset from UTC). (SU)
|
||||||
|
%Z The timezone or name or abbreviation.
|
||||||
|
%+ The date and time in date(1) format. (TZ) (Not supported in glibc2.)
|
||||||
|
%% A literal '%' character.
|
||||||
|
|
||||||
|
Some conversion specifications can be modified by preceding the conversion
|
||||||
|
specifier character by the E or O modifier to indicate that an alternative
|
||||||
|
format should be used. If the alternative format or specification does not
|
||||||
|
exist for the current locale, the behavior will be as if the unmodified
|
||||||
|
conversion specification were used. (SU) The Single UNIX Specification mentions
|
||||||
|
%Ec, %EC, %Ex, %EX, %Ey, %EY, %Od, %Oe, %OH, %OI, %Om, %OM, %OS, %Ou, %OU, %OV,
|
||||||
|
%Ow, %OW, %Oy, where the effect of the O modifier is to use alternative numeric
|
||||||
|
symbols (say, roman numerals), and that of the E modifier is to use a
|
||||||
|
locale-dependent alternative representation.
|
||||||
|
|
||||||
|
*/
|
||||||
|
func Strftime(format string, t time.Time) (s string) {
|
||||||
|
return strftime(format, t)
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package strftime
|
||||||
|
|
||||||
|
/*
|
||||||
|
#define _POSIX_SOURCE
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const initialBufSize = 256
|
||||||
|
|
||||||
|
func strftime(format string, t time.Time) (s string) {
|
||||||
|
if format == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt := C.CString(format)
|
||||||
|
defer C.free(unsafe.Pointer(fmt))
|
||||||
|
|
||||||
|
// pass timezone to strftime(3) through TZ environment var.
|
||||||
|
|
||||||
|
// XXX: this is not threadsafe; someone may set TZ to a different value
|
||||||
|
// between when we get and reset it. we could check that it's unchanged
|
||||||
|
// right before setting it back, but that would leave a race between
|
||||||
|
// testing and setting, and also a worse scenario where another thread sets
|
||||||
|
// TZ to the same value of `zone` as this one (which can't be detected),
|
||||||
|
// only to have us unhelpfully reset it to a now-stale value.
|
||||||
|
//
|
||||||
|
// since a runtime environment where different threads are stomping on TZ
|
||||||
|
// is inherently unsafe, don't waste time trying.
|
||||||
|
|
||||||
|
zone, _ := t.Zone()
|
||||||
|
oldZone := os.Getenv("TZ")
|
||||||
|
if oldZone != zone {
|
||||||
|
defer os.Setenv("TZ", oldZone)
|
||||||
|
os.Setenv("TZ", zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
timep := C.time_t(t.Unix())
|
||||||
|
|
||||||
|
var tm C.struct_tm
|
||||||
|
C.localtime_r(&timep, &tm)
|
||||||
|
|
||||||
|
for size := initialBufSize; ; size *= 2 {
|
||||||
|
buf := (*C.char)(C.malloc(C.size_t(size))) // can panic
|
||||||
|
defer C.free(unsafe.Pointer(buf))
|
||||||
|
n := C.strftime(buf, C.size_t(size), fmt, &tm)
|
||||||
|
if n == 0 {
|
||||||
|
// strftime(3), unhelpfully: "Note that the return value 0 does not
|
||||||
|
// necessarily indicate an error; for example, in many locales %p
|
||||||
|
// yields an empty string." This leaves no definite way to
|
||||||
|
// distinguish between the cases where the value doesn't fit and
|
||||||
|
// where it does because the string is empty. In the latter case,
|
||||||
|
// allocating increasingly larger buffers will never change the
|
||||||
|
// result, so we need some heuristic for bailing out.
|
||||||
|
//
|
||||||
|
// Since a single 2-byte conversion sequence should not produce an
|
||||||
|
// output longer than about 24 bytes, we conservatively allow the
|
||||||
|
// buffer size to grow up to 20 times larger than the format string
|
||||||
|
// before giving up.
|
||||||
|
if size > 20*len(format) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if int(n) < size {
|
||||||
|
s = C.GoStringN(buf, C.int(n))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
116
vendor/github.com/fastly/go-utils/strftime/strftime_linux_test.go
generated
vendored
Normal file
116
vendor/github.com/fastly/go-utils/strftime/strftime_linux_test.go
generated
vendored
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package strftime_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fastly/go-utils/strftime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// these test the pure Go implementation (linux otherwise won't use/test it)
|
||||||
|
func TestStrftimePureAgainstReference(t *testing.T) {
|
||||||
|
testStrftimeAgainstReference(t, strftime.StrftimePure, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStrftimePureAgainstPerl(t *testing.T) {
|
||||||
|
testStrftimeAgainstPerl(t, strftime.StrftimePure, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares Strftime's output to strftime as exposed by Perl.
|
||||||
|
func TestStrftimeAgainstPerl(t *testing.T) {
|
||||||
|
testStrftimeAgainstPerl(t, strftime.Strftime, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// number of consecutive seconds to test, starting from refTime
|
||||||
|
const (
|
||||||
|
duration int64 = 2 * 365 * 24 * 60 * 60
|
||||||
|
stride int64 = 7 * 24 * 60 * 60
|
||||||
|
)
|
||||||
|
|
||||||
|
// testStrftimePerl checks a range of times and uses the system's strftime(3)
|
||||||
|
// as a reference for the correct answer. To access the system strftime, it
|
||||||
|
// uses the core POSIX module of the first perl(1) found in the path. The test
|
||||||
|
// is only run if the CHECK_AGAINST_PERL environment variable is non-empty.
|
||||||
|
func testStrftimeAgainstPerl(t *testing.T, impl strftimeImpl, strict bool) {
|
||||||
|
if os.Getenv("CHECK_AGAINST_PERL") == "" {
|
||||||
|
t.Skip()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
perlPath, err := exec.LookPath("perl")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Couldn't locate perl, skipping: %s", err)
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
perlOpts := []string{"-MPOSIX=strftime", "-E"}
|
||||||
|
|
||||||
|
origTimezone := os.Getenv("TZ")
|
||||||
|
var perlTests []test
|
||||||
|
for _, test := range tests {
|
||||||
|
if test.perlShouldWork {
|
||||||
|
perlTests = append(perlTests, test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stmts := make([]string, len(perlTests))
|
||||||
|
|
||||||
|
for n := int64(0); n < duration; n += stride {
|
||||||
|
when := refTime + n
|
||||||
|
var tm = time.Unix(when, 0).In(tz)
|
||||||
|
/*
|
||||||
|
if n%100 == 0 {
|
||||||
|
log.Printf("Testing %d/%d (%.3f%%): %s", n+1, duration, 100*float64(n)/float64(duration), tm)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// batch up all the tests for a single timestamp into one call out to perl
|
||||||
|
for i, test := range perlTests {
|
||||||
|
stmts[i] = fmt.Sprintf(`say strftime "%s", @t`, test.format)
|
||||||
|
}
|
||||||
|
|
||||||
|
prog := fmt.Sprintf("@t=localtime %d; %s", when, strings.Join(stmts, "; "))
|
||||||
|
args := append(perlOpts, prog)
|
||||||
|
//t.Logf("Executing perl: %s %v", perlPath, args)
|
||||||
|
os.Setenv("TZ", timezone)
|
||||||
|
output, err := exec.Command(perlPath, args...).CombinedOutput()
|
||||||
|
os.Setenv("TZ", origTimezone)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("perl failed: %s [%q]", err, output)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
results := strings.Split(string(output), "\n")
|
||||||
|
results = results[:len(results)-1] // strip trailing \n
|
||||||
|
if len(results) != len(perlTests) {
|
||||||
|
t.Fatalf("t=%d: got %d results from perl, expected %d: %q", when, len(results), len(perlTests), output)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range perlTests {
|
||||||
|
got := strftime.Strftime(test.format, tm)
|
||||||
|
expected := results[i]
|
||||||
|
if got == string(expected) {
|
||||||
|
//t.Logf("ok %d/%d: `%s` => `%s`", i, when, test.format, got)
|
||||||
|
} else if test.localeDependent && !strict {
|
||||||
|
//t.Logf("meh %d/%d: `%s` => got `%s`, perl produced `%s`", i, when, test.format, got, expected)
|
||||||
|
} else {
|
||||||
|
t.Errorf("FAIL %d/%d: `%s` => got `%s`, perl produced `%s`", i, when, test.format, got, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if testing.Short() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStrftime(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
strftime.Strftime(benchFmt, benchTime)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package strftime
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func strftime(format string, t time.Time) string {
|
||||||
|
return StrftimePure(format, t)
|
||||||
|
}
|
|
@ -0,0 +1,277 @@
|
||||||
|
package strftime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StrftimePure is a locale-unaware implementation of strftime(3). It does not
|
||||||
|
// correctly account for locale-specific conversion specifications, so formats
|
||||||
|
// like `%c` may behave differently from the underlying platform. Additionally,
|
||||||
|
// the `%E` and `%O` modifiers are passed through as raw strings.
|
||||||
|
//
|
||||||
|
// The implementation of locale-specific conversions attempts to mirror the
|
||||||
|
// strftime(3) implementation in glibc 2.15 under LC_TIME=C.
|
||||||
|
func StrftimePure(format string, t time.Time) string {
|
||||||
|
buf := make([]byte, 0, 512)
|
||||||
|
for i := 0; i < len(format); {
|
||||||
|
c := format[i]
|
||||||
|
if c != '%' {
|
||||||
|
buf = append(buf, c)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
if i == len(format) {
|
||||||
|
buf = append(buf, '%')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b := format[i]
|
||||||
|
|
||||||
|
switch b {
|
||||||
|
default:
|
||||||
|
buf = append(buf, '%', b)
|
||||||
|
case 'a':
|
||||||
|
// The abbreviated weekday name according to the current locale.
|
||||||
|
buf = append(buf, t.Format("Mon")...)
|
||||||
|
case 'A':
|
||||||
|
// The full weekday name according to the current locale.
|
||||||
|
buf = append(buf, t.Format("Monday")...)
|
||||||
|
case 'b':
|
||||||
|
// The abbreviated month name according to the current locale.
|
||||||
|
buf = append(buf, t.Format("Jan")...)
|
||||||
|
case 'B':
|
||||||
|
// The full month name according to the current locale.
|
||||||
|
buf = append(buf, t.Month().String()...)
|
||||||
|
case 'C':
|
||||||
|
// The century number (year/100) as a 2-digit integer. (SU)
|
||||||
|
buf = zero2d(buf, int(t.Year())/100)
|
||||||
|
case 'c':
|
||||||
|
// The preferred date and time representation for the current locale.
|
||||||
|
buf = append(buf, t.Format("Mon Jan 2 15:04:05 2006")...)
|
||||||
|
case 'd':
|
||||||
|
// The day of the month as a decimal number (range 01 to 31).
|
||||||
|
buf = zero2d(buf, t.Day())
|
||||||
|
case 'D':
|
||||||
|
// Equivalent to %m/%d/%y. (Yecch—for Americans only. Americans should note that in other countries %d/%m/%y is rather com‐ mon. This means that in international context this format is ambiguous and should not be used.) (SU)
|
||||||
|
buf = zero2d(buf, int(t.Month()))
|
||||||
|
buf = append(buf, '/')
|
||||||
|
buf = zero2d(buf, t.Day())
|
||||||
|
buf = append(buf, '/')
|
||||||
|
buf = zero2d(buf, t.Year()%100)
|
||||||
|
case 'E':
|
||||||
|
// Modifier: use alternative format, see below. (SU)
|
||||||
|
if i+1 < len(format) {
|
||||||
|
i++
|
||||||
|
buf = append(buf, '%', 'E', format[i])
|
||||||
|
|
||||||
|
} else {
|
||||||
|
buf = append(buf, "%E"...)
|
||||||
|
}
|
||||||
|
case 'e':
|
||||||
|
// Like %d, the day of the month as a decimal number, but a leading zero is replaced by a space. (SU)
|
||||||
|
buf = twoD(buf, t.Day())
|
||||||
|
case 'F':
|
||||||
|
// Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99)
|
||||||
|
buf = zero4d(buf, t.Year())
|
||||||
|
buf = append(buf, '-')
|
||||||
|
buf = zero2d(buf, int(t.Month()))
|
||||||
|
buf = append(buf, '-')
|
||||||
|
buf = zero2d(buf, t.Day())
|
||||||
|
case 'G':
|
||||||
|
// The ISO 8601 week-based year (see NOTES) with century as a decimal number. The 4-digit year corresponding to the ISO week number (see %V). This has the same format and value as %Y, except that if the ISO week number belongs to the previous or next year, that year is used instead. (TZ)
|
||||||
|
year, _ := t.ISOWeek()
|
||||||
|
buf = zero4d(buf, year)
|
||||||
|
case 'g':
|
||||||
|
// Like %G, but without century, that is, with a 2-digit year (00-99). (TZ)
|
||||||
|
year, _ := t.ISOWeek()
|
||||||
|
buf = zero2d(buf, year%100)
|
||||||
|
case 'h':
|
||||||
|
// Equivalent to %b. (SU)
|
||||||
|
buf = append(buf, t.Format("Jan")...)
|
||||||
|
case 'H':
|
||||||
|
// The hour as a decimal number using a 24-hour clock (range 00 to 23).
|
||||||
|
buf = zero2d(buf, t.Hour())
|
||||||
|
case 'I':
|
||||||
|
// The hour as a decimal number using a 12-hour clock (range 01 to 12).
|
||||||
|
buf = zero2d(buf, t.Hour()%12)
|
||||||
|
case 'j':
|
||||||
|
// The day of the year as a decimal number (range 001 to 366).
|
||||||
|
buf = zero3d(buf, t.YearDay())
|
||||||
|
case 'k':
|
||||||
|
// The hour (24-hour clock) as a decimal number (range 0 to 23); single digits are preceded by a blank. (See also %H.) (TZ)
|
||||||
|
buf = twoD(buf, t.Hour())
|
||||||
|
case 'l':
|
||||||
|
// The hour (12-hour clock) as a decimal number (range 1 to 12); single digits are preceded by a blank. (See also %I.) (TZ)
|
||||||
|
buf = twoD(buf, t.Hour()%12)
|
||||||
|
case 'm':
|
||||||
|
// The month as a decimal number (range 01 to 12).
|
||||||
|
buf = zero2d(buf, int(t.Month()))
|
||||||
|
case 'M':
|
||||||
|
// The minute as a decimal number (range 00 to 59).
|
||||||
|
buf = zero2d(buf, t.Minute())
|
||||||
|
case 'n':
|
||||||
|
// A newline character. (SU)
|
||||||
|
buf = append(buf, '\n')
|
||||||
|
case 'O':
|
||||||
|
// Modifier: use alternative format, see below. (SU)
|
||||||
|
if i+1 < len(format) {
|
||||||
|
i++
|
||||||
|
buf = append(buf, '%', 'O', format[i])
|
||||||
|
} else {
|
||||||
|
buf = append(buf, "%O"...)
|
||||||
|
}
|
||||||
|
case 'p':
|
||||||
|
// Either "AM" or "PM" according to the given time value, or the corresponding strings for the current locale. Noon is treated as "PM" and midnight as "AM".
|
||||||
|
buf = appendAMPM(buf, t.Hour())
|
||||||
|
case 'P':
|
||||||
|
// Like %p but in lowercase: "am" or "pm" or a corresponding string for the current locale. (GNU)
|
||||||
|
buf = appendampm(buf, t.Hour())
|
||||||
|
case 'r':
|
||||||
|
// The time in a.m. or p.m. notation. In the POSIX locale this is equivalent to %I:%M:%S %p. (SU)
|
||||||
|
h := t.Hour()
|
||||||
|
buf = zero2d(buf, h%12)
|
||||||
|
buf = append(buf, ':')
|
||||||
|
buf = zero2d(buf, t.Minute())
|
||||||
|
buf = append(buf, ':')
|
||||||
|
buf = zero2d(buf, t.Second())
|
||||||
|
buf = append(buf, ' ')
|
||||||
|
buf = appendAMPM(buf, h)
|
||||||
|
case 'R':
|
||||||
|
// The time in 24-hour notation (%H:%M). (SU) For a version including the seconds, see %T below.
|
||||||
|
buf = zero2d(buf, t.Hour())
|
||||||
|
buf = append(buf, ':')
|
||||||
|
buf = zero2d(buf, t.Minute())
|
||||||
|
case 's':
|
||||||
|
// The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). (TZ)
|
||||||
|
buf = strconv.AppendInt(buf, t.Unix(), 10)
|
||||||
|
case 'S':
|
||||||
|
// The second as a decimal number (range 00 to 60). (The range is up to 60 to allow for occasional leap seconds.)
|
||||||
|
buf = zero2d(buf, t.Second())
|
||||||
|
case 't':
|
||||||
|
// A tab character. (SU)
|
||||||
|
buf = append(buf, '\t')
|
||||||
|
case 'T':
|
||||||
|
// The time in 24-hour notation (%H:%M:%S). (SU)
|
||||||
|
buf = zero2d(buf, t.Hour())
|
||||||
|
buf = append(buf, ':')
|
||||||
|
buf = zero2d(buf, t.Minute())
|
||||||
|
buf = append(buf, ':')
|
||||||
|
buf = zero2d(buf, t.Second())
|
||||||
|
case 'u':
|
||||||
|
// The day of the week as a decimal, range 1 to 7, Monday being 1. See also %w. (SU)
|
||||||
|
day := byte(t.Weekday())
|
||||||
|
if day == 0 {
|
||||||
|
day = 7
|
||||||
|
}
|
||||||
|
buf = append(buf, '0'+day)
|
||||||
|
case 'U':
|
||||||
|
// The week number of the current year as a decimal number, range 00 to 53, starting with the first Sunday as the first day of week 01. See also %V and %W.
|
||||||
|
buf = zero2d(buf, (t.YearDay()-int(t.Weekday())+7)/7)
|
||||||
|
case 'V':
|
||||||
|
// The ISO 8601 week number (see NOTES) of the current year as a decimal number, range 01 to 53, where week 1 is the first week that has at least 4 days in the new year. See also %U and %W. (SU)
|
||||||
|
_, week := t.ISOWeek()
|
||||||
|
buf = zero2d(buf, week)
|
||||||
|
case 'w':
|
||||||
|
// The day of the week as a decimal, range 0 to 6, Sunday being 0. See also %u.
|
||||||
|
buf = strconv.AppendInt(buf, int64(t.Weekday()), 10)
|
||||||
|
case 'W':
|
||||||
|
// The week number of the current year as a decimal number, range 00 to 53, starting with the first Monday as the first day of week 01.
|
||||||
|
buf = zero2d(buf, (t.YearDay()-(int(t.Weekday())-1+7)%7+7)/7)
|
||||||
|
case 'x':
|
||||||
|
// The preferred date representation for the current locale without the time.
|
||||||
|
buf = zero2d(buf, int(t.Month()))
|
||||||
|
buf = append(buf, '/')
|
||||||
|
buf = zero2d(buf, t.Day())
|
||||||
|
buf = append(buf, '/')
|
||||||
|
buf = zero2d(buf, t.Year()%100)
|
||||||
|
case 'X':
|
||||||
|
// The preferred time representation for the current locale without the date.
|
||||||
|
buf = zero2d(buf, t.Hour())
|
||||||
|
buf = append(buf, ':')
|
||||||
|
buf = zero2d(buf, t.Minute())
|
||||||
|
buf = append(buf, ':')
|
||||||
|
buf = zero2d(buf, t.Second())
|
||||||
|
case 'y':
|
||||||
|
// The year as a decimal number without a century (range 00 to 99).
|
||||||
|
buf = zero2d(buf, t.Year()%100)
|
||||||
|
case 'Y':
|
||||||
|
// The year as a decimal number including the century.
|
||||||
|
buf = zero4d(buf, t.Year())
|
||||||
|
case 'z':
|
||||||
|
// The +hhmm or -hhmm numeric timezone (that is, the hour and minute offset from UTC). (SU)
|
||||||
|
buf = append(buf, t.Format("-0700")...)
|
||||||
|
case 'Z':
|
||||||
|
// The timezone or name or abbreviation.
|
||||||
|
buf = append(buf, t.Format("MST")...)
|
||||||
|
case '+':
|
||||||
|
// The date and time in date(1) format. (TZ) (Not supported in glibc2.)
|
||||||
|
buf = append(buf, t.Format("Mon Jan _2 15:04:05 MST 2006")...)
|
||||||
|
case '%':
|
||||||
|
// A literal '%' character.
|
||||||
|
buf = append(buf, '%')
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to append %2d ints
|
||||||
|
func twoD(p []byte, n int) []byte {
|
||||||
|
if n < 10 {
|
||||||
|
return append(p, ' ', '0'+byte(n))
|
||||||
|
}
|
||||||
|
return strconv.AppendInt(p, int64(n), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to append %02d ints
|
||||||
|
func zero2d(p []byte, n int) []byte {
|
||||||
|
if n < 10 {
|
||||||
|
return append(p, '0', '0'+byte(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.AppendInt(p, int64(n), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to append %03d ints
|
||||||
|
func zero3d(p []byte, n int) []byte {
|
||||||
|
switch {
|
||||||
|
case n < 10:
|
||||||
|
p = append(p, "00"...)
|
||||||
|
case n < 100:
|
||||||
|
p = append(p, '0')
|
||||||
|
}
|
||||||
|
return strconv.AppendInt(p, int64(n), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to append %04d ints
|
||||||
|
func zero4d(p []byte, n int) []byte {
|
||||||
|
switch {
|
||||||
|
case n < 10:
|
||||||
|
p = append(p, "000"...)
|
||||||
|
case n < 100:
|
||||||
|
p = append(p, "00"...)
|
||||||
|
case n < 1000:
|
||||||
|
p = append(p, '0')
|
||||||
|
}
|
||||||
|
return strconv.AppendInt(p, int64(n), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendampm(p []byte, h int) []byte {
|
||||||
|
var m string
|
||||||
|
if h < 12 {
|
||||||
|
m = "am"
|
||||||
|
} else {
|
||||||
|
m = "pm"
|
||||||
|
}
|
||||||
|
return append(p, m...)
|
||||||
|
}
|
||||||
|
func appendAMPM(p []byte, h int) []byte {
|
||||||
|
var m string
|
||||||
|
if h < 12 {
|
||||||
|
m = "AM"
|
||||||
|
} else {
|
||||||
|
m = "PM"
|
||||||
|
}
|
||||||
|
return append(p, m...)
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package strftime_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fastly/go-utils/strftime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const benchFmt = "%a|%A|%b|%B|%c|%C|%d|%D|%e|%E|%EF|%F|%G|%g|%h|%H|%I|%j|%k|%l|%m|%M|%O|%OF|%p|%P|%r|%R|%s|%S|%t|%T|%u|%U|%V|%w|%W|%x|%X|%y|%Y|%z|%Z|%%|%^|%"
|
||||||
|
|
||||||
|
var benchTime = time.Date(2014, time.July, 2, 11, 57, 42, 234098432, time.UTC)
|
||||||
|
|
||||||
|
func BenchmarkStrftimePure(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
strftime.StrftimePure(benchFmt, benchTime)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package strftime_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fastly/go-utils/strftime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
refTime int64 = 1136239445 // see package time
|
||||||
|
timezone = "MST"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tz *time.Location
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
tz, err = time.LoadLocation(timezone)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("couldn't load timezone %q: %s", timezone, err))
|
||||||
|
}
|
||||||
|
// try to get consistent answers by using a relatively well-defined locale
|
||||||
|
os.Setenv("LC_TIME", "C")
|
||||||
|
}
|
||||||
|
|
||||||
|
type test struct {
|
||||||
|
format, expected string
|
||||||
|
localeDependent, perlShouldWork bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var tests = []test{
|
||||||
|
{"", ``, false, true},
|
||||||
|
|
||||||
|
// invalid formats
|
||||||
|
{"%", `%`, false, true},
|
||||||
|
{"%^", `%^`, false, true},
|
||||||
|
{"%^ ", `%^ `, false, true},
|
||||||
|
{"%^ x", `%^ x`, false, true},
|
||||||
|
{"x%^ x", `x%^ x`, false, true},
|
||||||
|
|
||||||
|
// supported locale-invariant formats
|
||||||
|
{"%a", `Mon`, false, true},
|
||||||
|
{"%A", `Monday`, false, true},
|
||||||
|
{"%b", `Jan`, false, true},
|
||||||
|
{"%B", `January`, false, true},
|
||||||
|
{"%C", `20`, false, true},
|
||||||
|
{"%d", `02`, false, true},
|
||||||
|
{"%D", `01/02/06`, false, true},
|
||||||
|
{"%e", ` 2`, false, true},
|
||||||
|
{"%F", `2006-01-02`, false, true},
|
||||||
|
{"%G", `2006`, false, true},
|
||||||
|
{"%g", `06`, false, true},
|
||||||
|
{"%h", `Jan`, false, true},
|
||||||
|
{"%H", `15`, false, true},
|
||||||
|
{"%I", `03`, false, true},
|
||||||
|
{"%j", `002`, false, true},
|
||||||
|
{"%k", `15`, false, true},
|
||||||
|
{"%l", ` 3`, false, true},
|
||||||
|
{"%m", `01`, false, true},
|
||||||
|
{"%M", `04`, false, true},
|
||||||
|
{"%n", "\n", false, false},
|
||||||
|
{"%p", `PM`, false, true},
|
||||||
|
{"%r", `03:04:05 PM`, false, true},
|
||||||
|
{"%R", `15:04`, false, true},
|
||||||
|
{"%s", `1136239445`, false, true},
|
||||||
|
{"%S", `05`, false, true},
|
||||||
|
{"%t", "\t", false, true},
|
||||||
|
{"%T", `15:04:05`, false, true},
|
||||||
|
{"%u", `1`, false, true},
|
||||||
|
{"%U", `01`, false, true},
|
||||||
|
{"%V", `01`, false, true},
|
||||||
|
{"%w", `1`, false, true},
|
||||||
|
{"%W", `01`, false, true},
|
||||||
|
{"%x", `01/02/06`, true, true},
|
||||||
|
{"%X", `15:04:05`, true, true},
|
||||||
|
{"%y", `06`, false, true},
|
||||||
|
{"%Y", `2006`, false, true},
|
||||||
|
{"%z", `-0700`, false, true},
|
||||||
|
{"%Z", `MST`, false, true},
|
||||||
|
{"%%", `%`, false, true},
|
||||||
|
|
||||||
|
// supported locale-varying formats
|
||||||
|
{"%c", `Mon Jan 2 15:04:05 2006`, true, true},
|
||||||
|
{"%E", `%E`, true, true},
|
||||||
|
{"%EF", `%EF`, true, true},
|
||||||
|
{"%O", `%O`, true, true},
|
||||||
|
{"%OF", `%OF`, true, true},
|
||||||
|
{"%P", `pm`, true, true},
|
||||||
|
{"%+", `Mon Jan 2 15:04:05 MST 2006`, true, false},
|
||||||
|
{
|
||||||
|
"%a|%A|%b|%B|%c|%C|%d|%D|%e|%E|%EF|%F|%G|%g|%h|%H|%I|%j|%k|%l|%m|%M|%O|%OF|%p|%P|%r|%R|%s|%S|%t|%T|%u|%U|%V|%w|%W|%x|%X|%y|%Y|%z|%Z|%%",
|
||||||
|
`Mon|Monday|Jan|January|Mon Jan 2 15:04:05 2006|20|02|01/02/06| 2|%E|%EF|2006-01-02|2006|06|Jan|15|03|002|15| 3|01|04|%O|%OF|PM|pm|03:04:05 PM|15:04|1136239445|05| |15:04:05|1|01|01|1|01|01/02/06|15:04:05|06|2006|-0700|MST|%`,
|
||||||
|
true, true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type strftimeImpl func(string, time.Time) string
|
||||||
|
|
||||||
|
func TestStrftimeAgainstReference(t *testing.T) {
|
||||||
|
testStrftimeAgainstReference(t, strftime.Strftime, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStrftimeAgainstReference(t *testing.T, impl strftimeImpl, strict bool) {
|
||||||
|
var tm = time.Unix(refTime, 0).In(tz)
|
||||||
|
for i, test := range tests {
|
||||||
|
got := impl(test.format, tm)
|
||||||
|
if got == test.expected {
|
||||||
|
t.Logf("ok %d: `%s` => `%s`", i, test.format, got)
|
||||||
|
} else if test.localeDependent && !strict {
|
||||||
|
// locale-reliant conversions are generally unpredictable, so
|
||||||
|
// failures are expected
|
||||||
|
t.Logf("meh %d: `%s` => got `%s`, expected `%s`", i, test.format, got, test.expected)
|
||||||
|
} else {
|
||||||
|
t.Errorf("FAIL %d: `%s` => got `%s`, expected `%s`", i, test.format, got, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.1
|
||||||
|
- 1.2
|
||||||
|
- 1.3
|
||||||
|
- 1.4
|
||||||
|
- tip
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Version 1.x.x
|
||||||
|
|
||||||
|
* **Add more test cases and reference new test COM server project.** (Placeholder for future additions)
|
||||||
|
|
||||||
|
# Version 1.2.0-alphaX
|
||||||
|
|
||||||
|
**Minimum supported version is now Go 1.4. Go 1.1 support is deprecated, but should still build.**
|
||||||
|
|
||||||
|
* Added CI configuration for Travis-CI and AppVeyor.
|
||||||
|
* Added test InterfaceID and ClassID for the COM Test Server project.
|
||||||
|
* Added more inline documentation (#83).
|
||||||
|
* Added IEnumVARIANT implementation (#88).
|
||||||
|
* Added IEnumVARIANT test cases (#99, #100, #101).
|
||||||
|
* Added support for retrieving `time.Time` from VARIANT (#92).
|
||||||
|
* Added test case for IUnknown (#64).
|
||||||
|
* Added test case for IDispatch (#64).
|
||||||
|
* Added test cases for scalar variants (#64, #76).
|
||||||
|
|
||||||
|
# Version 1.1.1
|
||||||
|
|
||||||
|
* Fixes for Linux build.
|
||||||
|
* Fixes for Windows build.
|
||||||
|
|
||||||
|
# Version 1.1.0
|
||||||
|
|
||||||
|
The change to provide building on all platforms is a new feature. The increase in minor version reflects that and allows those who wish to stay on 1.0.x to continue to do so. Support for 1.0.x will be limited to bug fixes.
|
||||||
|
|
||||||
|
* Move GUID out of variables.go into its own file to make new documentation available.
|
||||||
|
* Move OleError out of ole.go into its own file to make new documentation available.
|
||||||
|
* Add documentation to utility functions.
|
||||||
|
* Add documentation to variant receiver functions.
|
||||||
|
* Add documentation to ole structures.
|
||||||
|
* Make variant available to other systems outside of Windows.
|
||||||
|
* Make OLE structures available to other systems outside of Windows.
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
|
||||||
|
* Library should now be built on all platforms supported by Go. Library will NOOP on any platform that is not Windows.
|
||||||
|
* More functions are now documented and available on godoc.org.
|
||||||
|
|
||||||
|
# Version 1.0.1
|
||||||
|
|
||||||
|
1. Fix package references from repository location change.
|
||||||
|
|
||||||
|
# Version 1.0.0
|
||||||
|
|
||||||
|
This version is stable enough for use. The COM API is still incomplete, but provides enough functionality for accessing COM servers using IDispatch interface.
|
||||||
|
|
||||||
|
There is no changelog for this version. Check commits for history.
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright © 2013-2017 Yasuhiro Matsumoto, <mattn.jp@gmail.com>
|
||||||
|
|
||||||
|
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,46 @@
|
||||||
|
#Go OLE
|
||||||
|
|
||||||
|
[![Build status](https://ci.appveyor.com/api/projects/status/qr0u2sf7q43us9fj?svg=true)](https://ci.appveyor.com/project/jacobsantos/go-ole-jgs28)
|
||||||
|
[![Build Status](https://travis-ci.org/go-ole/go-ole.svg?branch=master)](https://travis-ci.org/go-ole/go-ole)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/go-ole/go-ole?status.svg)](https://godoc.org/github.com/go-ole/go-ole)
|
||||||
|
|
||||||
|
Go bindings for Windows COM using shared libraries instead of cgo.
|
||||||
|
|
||||||
|
By Yasuhiro Matsumoto.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
To experiment with go-ole, you can just compile and run the example program:
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/go-ole/go-ole
|
||||||
|
cd /path/to/go-ole/
|
||||||
|
go test
|
||||||
|
|
||||||
|
cd /path/to/go-ole/example/excel
|
||||||
|
go run excel.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Continuous Integration
|
||||||
|
|
||||||
|
Continuous integration configuration has been added for both Travis-CI and AppVeyor. You will have to add these to your own account for your fork in order for it to run.
|
||||||
|
|
||||||
|
**Travis-CI**
|
||||||
|
|
||||||
|
Travis-CI was added to check builds on Linux to ensure that `go get` works when cross building. Currently, Travis-CI is not used to test cross-building, but this may be changed in the future. It is also not currently possible to test the library on Linux, since COM API is specific to Windows and it is not currently possible to run a COM server on Linux or even connect to a remote COM server.
|
||||||
|
|
||||||
|
**AppVeyor**
|
||||||
|
|
||||||
|
AppVeyor is used to build on Windows using the (in-development) test COM server. It is currently only used to test the build and ensure that the code works on Windows. It will be used to register a COM server and then run the test cases based on the test COM server.
|
||||||
|
|
||||||
|
The tests currently do run and do pass and this should be maintained with commits.
|
||||||
|
|
||||||
|
##Versioning
|
||||||
|
|
||||||
|
Go OLE uses [semantic versioning](http://semver.org) for version numbers, which is similar to the version contract of the Go language. Which means that the major version will always maintain backwards compatibility with minor versions. Minor versions will only add new additions and changes. Fixes will always be in patch.
|
||||||
|
|
||||||
|
This contract should allow you to upgrade to new minor and patch versions without breakage or modifications to your existing code. Leave a ticket, if there is breakage, so that it could be fixed.
|
||||||
|
|
||||||
|
##LICENSE
|
||||||
|
|
||||||
|
Under the MIT License: http://mattn.mit-license.org/2013
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Notes:
|
||||||
|
# - Minimal appveyor.yml file is an empty file. All sections are optional.
|
||||||
|
# - Indent each level of configuration with 2 spaces. Do not use tabs!
|
||||||
|
# - All section names are case-sensitive.
|
||||||
|
# - Section names should be unique on each level.
|
||||||
|
|
||||||
|
version: "1.3.0.{build}-alpha-{branch}"
|
||||||
|
|
||||||
|
os: Windows Server 2012 R2
|
||||||
|
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- v1.2
|
||||||
|
- v1.1
|
||||||
|
- v1.0
|
||||||
|
|
||||||
|
skip_tags: true
|
||||||
|
|
||||||
|
clone_folder: c:\gopath\src\github.com\go-ole\go-ole
|
||||||
|
|
||||||
|
environment:
|
||||||
|
GOPATH: c:\gopath
|
||||||
|
matrix:
|
||||||
|
- GOARCH: amd64
|
||||||
|
GOVERSION: 1.5
|
||||||
|
GOROOT: c:\go
|
||||||
|
DOWNLOADPLATFORM: "x64"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- choco install mingw
|
||||||
|
- SET PATH=c:\tools\mingw64\bin;%PATH%
|
||||||
|
# - Download COM Server
|
||||||
|
- ps: Start-FileDownload "https://github.com/go-ole/test-com-server/releases/download/v1.0.2/test-com-server-${env:DOWNLOADPLATFORM}.zip"
|
||||||
|
- 7z e test-com-server-%DOWNLOADPLATFORM%.zip -oc:\gopath\src\github.com\go-ole\go-ole > NUL
|
||||||
|
- c:\gopath\src\github.com\go-ole\go-ole\build\register-assembly.bat
|
||||||
|
# - set
|
||||||
|
- go version
|
||||||
|
- go env
|
||||||
|
- go get -u golang.org/x/tools/cmd/cover
|
||||||
|
- go get -u golang.org/x/tools/cmd/godoc
|
||||||
|
- go get -u golang.org/x/tools/cmd/stringer
|
||||||
|
|
||||||
|
build_script:
|
||||||
|
- cd c:\gopath\src\github.com\go-ole\go-ole
|
||||||
|
- go get -v -t ./...
|
||||||
|
- go build
|
||||||
|
- go test -v -cover ./...
|
||||||
|
|
||||||
|
# disable automatic tests
|
||||||
|
test: off
|
||||||
|
|
||||||
|
# disable deployment
|
||||||
|
deploy: off
|
|
@ -0,0 +1,329 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package ole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
procCoInitialize, _ = modole32.FindProc("CoInitialize")
|
||||||
|
procCoInitializeEx, _ = modole32.FindProc("CoInitializeEx")
|
||||||
|
procCoUninitialize, _ = modole32.FindProc("CoUninitialize")
|
||||||
|
procCoCreateInstance, _ = modole32.FindProc("CoCreateInstance")
|
||||||
|
procCoTaskMemFree, _ = modole32.FindProc("CoTaskMemFree")
|
||||||
|
procCLSIDFromProgID, _ = modole32.FindProc("CLSIDFromProgID")
|
||||||
|
procCLSIDFromString, _ = modole32.FindProc("CLSIDFromString")
|
||||||
|
procStringFromCLSID, _ = modole32.FindProc("StringFromCLSID")
|
||||||
|
procStringFromIID, _ = modole32.FindProc("StringFromIID")
|
||||||
|
procIIDFromString, _ = modole32.FindProc("IIDFromString")
|
||||||
|
procGetUserDefaultLCID, _ = modkernel32.FindProc("GetUserDefaultLCID")
|
||||||
|
procCopyMemory, _ = modkernel32.FindProc("RtlMoveMemory")
|
||||||
|
procVariantInit, _ = modoleaut32.FindProc("VariantInit")
|
||||||
|
procVariantClear, _ = modoleaut32.FindProc("VariantClear")
|
||||||
|
procVariantTimeToSystemTime, _ = modoleaut32.FindProc("VariantTimeToSystemTime")
|
||||||
|
procSysAllocString, _ = modoleaut32.FindProc("SysAllocString")
|
||||||
|
procSysAllocStringLen, _ = modoleaut32.FindProc("SysAllocStringLen")
|
||||||
|
procSysFreeString, _ = modoleaut32.FindProc("SysFreeString")
|
||||||
|
procSysStringLen, _ = modoleaut32.FindProc("SysStringLen")
|
||||||
|
procCreateDispTypeInfo, _ = modoleaut32.FindProc("CreateDispTypeInfo")
|
||||||
|
procCreateStdDispatch, _ = modoleaut32.FindProc("CreateStdDispatch")
|
||||||
|
procGetActiveObject, _ = modoleaut32.FindProc("GetActiveObject")
|
||||||
|
|
||||||
|
procGetMessageW, _ = moduser32.FindProc("GetMessageW")
|
||||||
|
procDispatchMessageW, _ = moduser32.FindProc("DispatchMessageW")
|
||||||
|
)
|
||||||
|
|
||||||
|
// coInitialize initializes COM library on current thread.
|
||||||
|
//
|
||||||
|
// MSDN documentation suggests that this function should not be called. Call
|
||||||
|
// CoInitializeEx() instead. The reason has to do with threading and this
|
||||||
|
// function is only for single-threaded apartments.
|
||||||
|
//
|
||||||
|
// That said, most users of the library have gotten away with just this
|
||||||
|
// function. If you are experiencing threading issues, then use
|
||||||
|
// CoInitializeEx().
|
||||||
|
func coInitialize() (err error) {
|
||||||
|
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms678543(v=vs.85).aspx
|
||||||
|
// Suggests that no value should be passed to CoInitialized.
|
||||||
|
// Could just be Call() since the parameter is optional. <-- Needs testing to be sure.
|
||||||
|
hr, _, _ := procCoInitialize.Call(uintptr(0))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// coInitializeEx initializes COM library with concurrency model.
|
||||||
|
func coInitializeEx(coinit uint32) (err error) {
|
||||||
|
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms695279(v=vs.85).aspx
|
||||||
|
// Suggests that the first parameter is not only optional but should always be NULL.
|
||||||
|
hr, _, _ := procCoInitializeEx.Call(uintptr(0), uintptr(coinit))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoInitialize initializes COM library on current thread.
|
||||||
|
//
|
||||||
|
// MSDN documentation suggests that this function should not be called. Call
|
||||||
|
// CoInitializeEx() instead. The reason has to do with threading and this
|
||||||
|
// function is only for single-threaded apartments.
|
||||||
|
//
|
||||||
|
// That said, most users of the library have gotten away with just this
|
||||||
|
// function. If you are experiencing threading issues, then use
|
||||||
|
// CoInitializeEx().
|
||||||
|
func CoInitialize(p uintptr) (err error) {
|
||||||
|
// p is ignored and won't be used.
|
||||||
|
// Avoid any variable not used errors.
|
||||||
|
p = uintptr(0)
|
||||||
|
return coInitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoInitializeEx initializes COM library with concurrency model.
|
||||||
|
func CoInitializeEx(p uintptr, coinit uint32) (err error) {
|
||||||
|
// Avoid any variable not used errors.
|
||||||
|
p = uintptr(0)
|
||||||
|
return coInitializeEx(coinit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoUninitialize uninitializes COM Library.
|
||||||
|
func CoUninitialize() {
|
||||||
|
procCoUninitialize.Call()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoTaskMemFree frees memory pointer.
|
||||||
|
func CoTaskMemFree(memptr uintptr) {
|
||||||
|
procCoTaskMemFree.Call(memptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLSIDFromProgID retrieves Class Identifier with the given Program Identifier.
|
||||||
|
//
|
||||||
|
// The Programmatic Identifier must be registered, because it will be looked up
|
||||||
|
// in the Windows Registry. The registry entry has the following keys: CLSID,
|
||||||
|
// Insertable, Protocol and Shell
|
||||||
|
// (https://msdn.microsoft.com/en-us/library/dd542719(v=vs.85).aspx).
|
||||||
|
//
|
||||||
|
// programID identifies the class id with less precision and is not guaranteed
|
||||||
|
// to be unique. These are usually found in the registry under
|
||||||
|
// HKEY_LOCAL_MACHINE\SOFTWARE\Classes, usually with the format of
|
||||||
|
// "Program.Component.Version" with version being optional.
|
||||||
|
//
|
||||||
|
// CLSIDFromProgID in Windows API.
|
||||||
|
func CLSIDFromProgID(progId string) (clsid *GUID, err error) {
|
||||||
|
var guid GUID
|
||||||
|
lpszProgID := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(progId)))
|
||||||
|
hr, _, _ := procCLSIDFromProgID.Call(lpszProgID, uintptr(unsafe.Pointer(&guid)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
clsid = &guid
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLSIDFromString retrieves Class ID from string representation.
|
||||||
|
//
|
||||||
|
// This is technically the string version of the GUID and will convert the
|
||||||
|
// string to object.
|
||||||
|
//
|
||||||
|
// CLSIDFromString in Windows API.
|
||||||
|
func CLSIDFromString(str string) (clsid *GUID, err error) {
|
||||||
|
var guid GUID
|
||||||
|
lpsz := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(str)))
|
||||||
|
hr, _, _ := procCLSIDFromString.Call(lpsz, uintptr(unsafe.Pointer(&guid)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
clsid = &guid
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringFromCLSID returns GUID formated string from GUID object.
|
||||||
|
func StringFromCLSID(clsid *GUID) (str string, err error) {
|
||||||
|
var p *uint16
|
||||||
|
hr, _, _ := procStringFromCLSID.Call(uintptr(unsafe.Pointer(clsid)), uintptr(unsafe.Pointer(&p)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
str = LpOleStrToString(p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IIDFromString returns GUID from program ID.
|
||||||
|
func IIDFromString(progId string) (clsid *GUID, err error) {
|
||||||
|
var guid GUID
|
||||||
|
lpsz := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(progId)))
|
||||||
|
hr, _, _ := procIIDFromString.Call(lpsz, uintptr(unsafe.Pointer(&guid)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
clsid = &guid
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringFromIID returns GUID formatted string from GUID object.
|
||||||
|
func StringFromIID(iid *GUID) (str string, err error) {
|
||||||
|
var p *uint16
|
||||||
|
hr, _, _ := procStringFromIID.Call(uintptr(unsafe.Pointer(iid)), uintptr(unsafe.Pointer(&p)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
str = LpOleStrToString(p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInstance of single uninitialized object with GUID.
|
||||||
|
func CreateInstance(clsid *GUID, iid *GUID) (unk *IUnknown, err error) {
|
||||||
|
if iid == nil {
|
||||||
|
iid = IID_IUnknown
|
||||||
|
}
|
||||||
|
hr, _, _ := procCoCreateInstance.Call(
|
||||||
|
uintptr(unsafe.Pointer(clsid)),
|
||||||
|
0,
|
||||||
|
CLSCTX_SERVER,
|
||||||
|
uintptr(unsafe.Pointer(iid)),
|
||||||
|
uintptr(unsafe.Pointer(&unk)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveObject retrieves pointer to active object.
|
||||||
|
func GetActiveObject(clsid *GUID, iid *GUID) (unk *IUnknown, err error) {
|
||||||
|
if iid == nil {
|
||||||
|
iid = IID_IUnknown
|
||||||
|
}
|
||||||
|
hr, _, _ := procGetActiveObject.Call(
|
||||||
|
uintptr(unsafe.Pointer(clsid)),
|
||||||
|
uintptr(unsafe.Pointer(iid)),
|
||||||
|
uintptr(unsafe.Pointer(&unk)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// VariantInit initializes variant.
|
||||||
|
func VariantInit(v *VARIANT) (err error) {
|
||||||
|
hr, _, _ := procVariantInit.Call(uintptr(unsafe.Pointer(v)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// VariantClear clears value in Variant settings to VT_EMPTY.
|
||||||
|
func VariantClear(v *VARIANT) (err error) {
|
||||||
|
hr, _, _ := procVariantClear.Call(uintptr(unsafe.Pointer(v)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SysAllocString allocates memory for string and copies string into memory.
|
||||||
|
func SysAllocString(v string) (ss *int16) {
|
||||||
|
pss, _, _ := procSysAllocString.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(v))))
|
||||||
|
ss = (*int16)(unsafe.Pointer(pss))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SysAllocStringLen copies up to length of given string returning pointer.
|
||||||
|
func SysAllocStringLen(v string) (ss *int16) {
|
||||||
|
utf16 := utf16.Encode([]rune(v + "\x00"))
|
||||||
|
ptr := &utf16[0]
|
||||||
|
|
||||||
|
pss, _, _ := procSysAllocStringLen.Call(uintptr(unsafe.Pointer(ptr)), uintptr(len(utf16)-1))
|
||||||
|
ss = (*int16)(unsafe.Pointer(pss))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SysFreeString frees string system memory. This must be called with SysAllocString.
|
||||||
|
func SysFreeString(v *int16) (err error) {
|
||||||
|
hr, _, _ := procSysFreeString.Call(uintptr(unsafe.Pointer(v)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SysStringLen is the length of the system allocated string.
|
||||||
|
func SysStringLen(v *int16) uint32 {
|
||||||
|
l, _, _ := procSysStringLen.Call(uintptr(unsafe.Pointer(v)))
|
||||||
|
return uint32(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateStdDispatch provides default IDispatch implementation for IUnknown.
|
||||||
|
//
|
||||||
|
// This handles default IDispatch implementation for objects. It haves a few
|
||||||
|
// limitations with only supporting one language. It will also only return
|
||||||
|
// default exception codes.
|
||||||
|
func CreateStdDispatch(unk *IUnknown, v uintptr, ptinfo *IUnknown) (disp *IDispatch, err error) {
|
||||||
|
hr, _, _ := procCreateStdDispatch.Call(
|
||||||
|
uintptr(unsafe.Pointer(unk)),
|
||||||
|
v,
|
||||||
|
uintptr(unsafe.Pointer(ptinfo)),
|
||||||
|
uintptr(unsafe.Pointer(&disp)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDispTypeInfo provides default ITypeInfo implementation for IDispatch.
|
||||||
|
//
|
||||||
|
// This will not handle the full implementation of the interface.
|
||||||
|
func CreateDispTypeInfo(idata *INTERFACEDATA) (pptinfo *IUnknown, err error) {
|
||||||
|
hr, _, _ := procCreateDispTypeInfo.Call(
|
||||||
|
uintptr(unsafe.Pointer(idata)),
|
||||||
|
uintptr(GetUserDefaultLCID()),
|
||||||
|
uintptr(unsafe.Pointer(&pptinfo)))
|
||||||
|
if hr != 0 {
|
||||||
|
err = NewError(hr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyMemory moves location of a block of memory.
|
||||||
|
func copyMemory(dest unsafe.Pointer, src unsafe.Pointer, length uint32) {
|
||||||
|
procCopyMemory.Call(uintptr(dest), uintptr(src), uintptr(length))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserDefaultLCID retrieves current user default locale.
|
||||||
|
func GetUserDefaultLCID() (lcid uint32) {
|
||||||
|
ret, _, _ := procGetUserDefaultLCID.Call()
|
||||||
|
lcid = uint32(ret)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessage in message queue from runtime.
|
||||||
|
//
|
||||||
|
// This function appears to block. PeekMessage does not block.
|
||||||
|
func GetMessage(msg *Msg, hwnd uint32, MsgFilterMin uint32, MsgFilterMax uint32) (ret int32, err error) {
|
||||||
|
r0, _, err := procGetMessageW.Call(uintptr(unsafe.Pointer(msg)), uintptr(hwnd), uintptr(MsgFilterMin), uintptr(MsgFilterMax))
|
||||||
|
ret = int32(r0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DispatchMessage to window procedure.
|
||||||
|
func DispatchMessage(msg *Msg) (ret int32) {
|
||||||
|
r0, _, _ := procDispatchMessageW.Call(uintptr(unsafe.Pointer(msg)))
|
||||||
|
ret = int32(r0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVariantDate converts COM Variant Time value to Go time.Time.
|
||||||
|
func GetVariantDate(value float64) (time.Time, error) {
|
||||||
|
var st syscall.Systemtime
|
||||||
|
r, _, _ := procVariantTimeToSystemTime.Call(uintptr(value), uintptr(unsafe.Pointer(&st)))
|
||||||
|
if r != 0 {
|
||||||
|
return time.Date(int(st.Year), time.Month(st.Month), int(st.Day), int(st.Hour), int(st.Minute), int(st.Second), int(st.Milliseconds/1000), time.UTC), nil
|
||||||
|
}
|
||||||
|
return time.Now(), errors.New("Could not convert to time, passing current time.")
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package ole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// coInitialize initializes COM library on current thread.
|
||||||
|
//
|
||||||
|
// MSDN documentation suggests that this function should not be called. Call
|
||||||
|
// CoInitializeEx() instead. The reason has to do with threading and this
|
||||||
|
// function is only for single-threaded apartments.
|
||||||
|
//
|
||||||
|
// That said, most users of the library have gotten away with just this
|
||||||
|
// function. If you are experiencing threading issues, then use
|
||||||
|
// CoInitializeEx().
|
||||||
|
func coInitialize() error {
|
||||||
|
return NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// coInitializeEx initializes COM library with concurrency model.
|
||||||
|
func coInitializeEx(coinit uint32) error {
|
||||||
|
return NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoInitialize initializes COM library on current thread.
|
||||||
|
//
|
||||||
|
// MSDN documentation suggests that this function should not be called. Call
|
||||||
|
// CoInitializeEx() instead. The reason has to do with threading and this
|
||||||
|
// function is only for single-threaded apartments.
|
||||||
|
//
|
||||||
|
// That said, most users of the library have gotten away with just this
|
||||||
|
// function. If you are experiencing threading issues, then use
|
||||||
|
// CoInitializeEx().
|
||||||
|
func CoInitialize(p uintptr) error {
|
||||||
|
return NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoInitializeEx initializes COM library with concurrency model.
|
||||||
|
func CoInitializeEx(p uintptr, coinit uint32) error {
|
||||||
|
return NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoUninitialize uninitializes COM Library.
|
||||||
|
func CoUninitialize() {}
|
||||||
|
|
||||||
|
// CoTaskMemFree frees memory pointer.
|
||||||
|
func CoTaskMemFree(memptr uintptr) {}
|
||||||
|
|
||||||
|
// CLSIDFromProgID retrieves Class Identifier with the given Program Identifier.
|
||||||
|
//
|
||||||
|
// The Programmatic Identifier must be registered, because it will be looked up
|
||||||
|
// in the Windows Registry. The registry entry has the following keys: CLSID,
|
||||||
|
// Insertable, Protocol and Shell
|
||||||
|
// (https://msdn.microsoft.com/en-us/library/dd542719(v=vs.85).aspx).
|
||||||
|
//
|
||||||
|
// programID identifies the class id with less precision and is not guaranteed
|
||||||
|
// to be unique. These are usually found in the registry under
|
||||||
|
// HKEY_LOCAL_MACHINE\SOFTWARE\Classes, usually with the format of
|
||||||
|
// "Program.Component.Version" with version being optional.
|
||||||
|
//
|
||||||
|
// CLSIDFromProgID in Windows API.
|
||||||
|
func CLSIDFromProgID(progId string) (*GUID, error) {
|
||||||
|
return nil, NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLSIDFromString retrieves Class ID from string representation.
|
||||||
|
//
|
||||||
|
// This is technically the string version of the GUID and will convert the
|
||||||
|
// string to object.
|
||||||
|
//
|
||||||
|
// CLSIDFromString in Windows API.
|
||||||
|
func CLSIDFromString(str string) (*GUID, error) {
|
||||||
|
return nil, NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringFromCLSID returns GUID formated string from GUID object.
|
||||||
|
func StringFromCLSID(clsid *GUID) (string, error) {
|
||||||
|
return "", NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IIDFromString returns GUID from program ID.
|
||||||
|
func IIDFromString(progId string) (*GUID, error) {
|
||||||
|
return nil, NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringFromIID returns GUID formatted string from GUID object.
|
||||||
|
func StringFromIID(iid *GUID) (string, error) {
|
||||||
|
return "", NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInstance of single uninitialized object with GUID.
|
||||||
|
func CreateInstance(clsid *GUID, iid *GUID) (*IUnknown, error) {
|
||||||
|
return nil, NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveObject retrieves pointer to active object.
|
||||||
|
func GetActiveObject(clsid *GUID, iid *GUID) (*IUnknown, error) {
|
||||||
|
return nil, NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VariantInit initializes variant.
|
||||||
|
func VariantInit(v *VARIANT) error {
|
||||||
|
return NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VariantClear clears value in Variant settings to VT_EMPTY.
|
||||||
|
func VariantClear(v *VARIANT) error {
|
||||||
|
return NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SysAllocString allocates memory for string and copies string into memory.
|
||||||
|
func SysAllocString(v string) *int16 {
|
||||||
|
u := int16(0)
|
||||||
|
return &u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SysAllocStringLen copies up to length of given string returning pointer.
|
||||||
|
func SysAllocStringLen(v string) *int16 {
|
||||||
|
u := int16(0)
|
||||||
|
return &u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SysFreeString frees string system memory. This must be called with SysAllocString.
|
||||||
|
func SysFreeString(v *int16) error {
|
||||||
|
return NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SysStringLen is the length of the system allocated string.
|
||||||
|
func SysStringLen(v *int16) uint32 {
|
||||||
|
return uint32(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateStdDispatch provides default IDispatch implementation for IUnknown.
|
||||||
|
//
|
||||||
|
// This handles default IDispatch implementation for objects. It haves a few
|
||||||
|
// limitations with only supporting one language. It will also only return
|
||||||
|
// default exception codes.
|
||||||
|
func CreateStdDispatch(unk *IUnknown, v uintptr, ptinfo *IUnknown) (*IDispatch, error) {
|
||||||
|
return nil, NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDispTypeInfo provides default ITypeInfo implementation for IDispatch.
|
||||||
|
//
|
||||||
|
// This will not handle the full implementation of the interface.
|
||||||
|
func CreateDispTypeInfo(idata *INTERFACEDATA) (*IUnknown, error) {
|
||||||
|
return nil, NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyMemory moves location of a block of memory.
|
||||||
|
func copyMemory(dest unsafe.Pointer, src unsafe.Pointer, length uint32) {}
|
||||||
|
|
||||||
|
// GetUserDefaultLCID retrieves current user default locale.
|
||||||
|
func GetUserDefaultLCID() uint32 {
|
||||||
|
return uint32(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessage in message queue from runtime.
|
||||||
|
//
|
||||||
|
// This function appears to block. PeekMessage does not block.
|
||||||
|
func GetMessage(msg *Msg, hwnd uint32, MsgFilterMin uint32, MsgFilterMax uint32) (int32, error) {
|
||||||
|
return int32(0), NewError(E_NOTIMPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DispatchMessage to window procedure.
|
||||||
|
func DispatchMessage(msg *Msg) int32 {
|
||||||
|
return int32(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetVariantDate(value float64) (time.Time, error) {
|
||||||
|
return time.Now(), NewError(E_NOTIMPL)
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package ole
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// TestComSetupAndShutDown tests that API fails on Linux.
|
||||||
|
func TestComSetupAndShutDown(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := coInitialize()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should be error, because only Windows is supported.")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestComPublicSetupAndShutDown tests that API fails on Linux.
|
||||||
|
func TestComPublicSetupAndShutDown(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := CoInitialize(0)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should be error, because only Windows is supported.")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestComPublicSetupAndShutDown_WithValue tests that API fails on Linux.
|
||||||
|
func TestComPublicSetupAndShutDown_WithValue(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := CoInitialize(5)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should be error, because only Windows is supported.")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestComExSetupAndShutDown tests that API fails on Linux.
|
||||||
|
func TestComExSetupAndShutDown(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := coInitializeEx(COINIT_MULTITHREADED)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should be error, because only Windows is supported.")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestComPublicExSetupAndShutDown tests that API fails on Linux.
|
||||||
|
func TestComPublicExSetupAndShutDown(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := CoInitializeEx(0, COINIT_MULTITHREADED)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should be error, because only Windows is supported.")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestComPublicExSetupAndShutDown_WithValue tests that API fails on Linux.
|
||||||
|
func TestComPublicExSetupAndShutDown_WithValue(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := CoInitializeEx(5, COINIT_MULTITHREADED)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should be error, because only Windows is supported.")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClsidFromProgID_WindowsMediaNSSManager tests that API fails on Linux.
|
||||||
|
func TestClsidFromProgID_WindowsMediaNSSManager(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
coInitialize()
|
||||||
|
defer CoUninitialize()
|
||||||
|
_, err := CLSIDFromProgID("WMPNSSCI.NSSManager")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should be error, because only Windows is supported.")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClsidFromString_WindowsMediaNSSManager tests that API fails on Linux.
|
||||||
|
func TestClsidFromString_WindowsMediaNSSManager(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
coInitialize()
|
||||||
|
defer CoUninitialize()
|
||||||
|
_, err := CLSIDFromString("{92498132-4D1A-4297-9B78-9E2E4BA99C07}")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should be error, because only Windows is supported.")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateInstance_WindowsMediaNSSManager tests that API fails on Linux.
|
||||||
|
func TestCreateInstance_WindowsMediaNSSManager(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
coInitialize()
|
||||||
|
defer CoUninitialize()
|
||||||
|
_, err := CLSIDFromProgID("WMPNSSCI.NSSManager")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should be error, because only Windows is supported.")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestError tests that API fails on Linux.
|
||||||
|
func TestError(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
coInitialize()
|
||||||
|
defer CoUninitialize()
|
||||||
|
_, err := CLSIDFromProgID("INTERFACE-NOT-FOUND")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should be error, because only Windows is supported.")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch vt := err.(type) {
|
||||||
|
case *OleError:
|
||||||
|
default:
|
||||||
|
t.Fatalf("should be *ole.OleError %t", vt)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package ole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestComSetupAndShutDown(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := coInitialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComPublicSetupAndShutDown(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := CoInitialize(0)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComPublicSetupAndShutDown_WithValue(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := CoInitialize(5)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComExSetupAndShutDown(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := coInitializeEx(COINIT_MULTITHREADED)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComPublicExSetupAndShutDown(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := CoInitializeEx(0, COINIT_MULTITHREADED)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComPublicExSetupAndShutDown_WithValue(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := CoInitializeEx(5, COINIT_MULTITHREADED)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClsidFromProgID_WindowsMediaNSSManager(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
expected := &GUID{0x92498132, 0x4D1A, 0x4297, [8]byte{0x9B, 0x78, 0x9E, 0x2E, 0x4B, 0xA9, 0x9C, 0x07}}
|
||||||
|
|
||||||
|
coInitialize()
|
||||||
|
defer CoUninitialize()
|
||||||
|
actual, err := CLSIDFromProgID("WMPNSSCI.NSSManager")
|
||||||
|
if err == nil {
|
||||||
|
if !IsEqualGUID(expected, actual) {
|
||||||
|
t.Log(err)
|
||||||
|
t.Log(fmt.Sprintf("Actual GUID: %+v\n", actual))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClsidFromString_WindowsMediaNSSManager(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
expected := &GUID{0x92498132, 0x4D1A, 0x4297, [8]byte{0x9B, 0x78, 0x9E, 0x2E, 0x4B, 0xA9, 0x9C, 0x07}}
|
||||||
|
|
||||||
|
coInitialize()
|
||||||
|
defer CoUninitialize()
|
||||||
|
actual, err := CLSIDFromString("{92498132-4D1A-4297-9B78-9E2E4BA99C07}")
|
||||||
|
|
||||||
|
if !IsEqualGUID(expected, actual) {
|
||||||
|
t.Log(err)
|
||||||
|
t.Log(fmt.Sprintf("Actual GUID: %+v\n", actual))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateInstance_WindowsMediaNSSManager(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
expected := &GUID{0x92498132, 0x4D1A, 0x4297, [8]byte{0x9B, 0x78, 0x9E, 0x2E, 0x4B, 0xA9, 0x9C, 0x07}}
|
||||||
|
|
||||||
|
coInitialize()
|
||||||
|
defer CoUninitialize()
|
||||||
|
actual, err := CLSIDFromProgID("WMPNSSCI.NSSManager")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
if !IsEqualGUID(expected, actual) {
|
||||||
|
t.Log(err)
|
||||||
|
t.Log(fmt.Sprintf("Actual GUID: %+v\n", actual))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
unknown, err := CreateInstance(actual, IID_IUnknown)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
unknown.Release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestError(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Log(r)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
coInitialize()
|
||||||
|
defer CoUninitialize()
|
||||||
|
_, err := CLSIDFromProgID("INTERFACE-NOT-FOUND")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("should be fail", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch vt := err.(type) {
|
||||||
|
case *OleError:
|
||||||
|
default:
|
||||||
|
t.Fatalf("should be *ole.OleError %t", vt)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
package ole
|
||||||
|
|
||||||
|
// Connection contains IUnknown for fluent interface interaction.
|
||||||
|
//
|
||||||
|
// Deprecated. Use oleutil package instead.
|
||||||
|
type Connection struct {
|
||||||
|
Object *IUnknown // Access COM
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize COM.
|
||||||
|
func (*Connection) Initialize() (err error) {
|
||||||
|
return coInitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uninitialize COM.
|
||||||
|
func (*Connection) Uninitialize() {
|
||||||
|
CoUninitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create IUnknown object based first on ProgId and then from String.
|
||||||
|
func (c *Connection) Create(progId string) (err error) {
|
||||||
|
var clsid *GUID
|
||||||
|
clsid, err = CLSIDFromProgID(progId)
|
||||||
|
if err != nil {
|
||||||
|
clsid, err = CLSIDFromString(progId)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unknown, err := CreateInstance(clsid, IID_IUnknown)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Object = unknown
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release IUnknown object.
|
||||||
|
func (c *Connection) Release() {
|
||||||
|
c.Object.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load COM object from list of programIDs or strings.
|
||||||
|
func (c *Connection) Load(names ...string) (errors []error) {
|
||||||
|
var tempErrors []error = make([]error, len(names))
|
||||||
|
var numErrors int = 0
|
||||||
|
for _, name := range names {
|
||||||
|
err := c.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
tempErrors = append(tempErrors, err)
|
||||||
|
numErrors += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(errors, tempErrors[0:numErrors])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch returns Dispatch object.
|
||||||
|
func (c *Connection) Dispatch() (object *Dispatch, err error) {
|
||||||
|
dispatch, err := c.Object.QueryInterface(IID_IDispatch)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
object = &Dispatch{dispatch}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch stores IDispatch object.
|
||||||
|
type Dispatch struct {
|
||||||
|
Object *IDispatch // Dispatch object.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call method on IDispatch with parameters.
|
||||||
|
func (d *Dispatch) Call(method string, params ...interface{}) (result *VARIANT, err error) {
|
||||||
|
id, err := d.GetId(method)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = d.Invoke(id, DISPATCH_METHOD, params)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustCall method on IDispatch with parameters.
|
||||||
|
func (d *Dispatch) MustCall(method string, params ...interface{}) (result *VARIANT) {
|
||||||
|
id, err := d.GetId(method)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = d.Invoke(id, DISPATCH_METHOD, params)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get property on IDispatch with parameters.
|
||||||
|
func (d *Dispatch) Get(name string, params ...interface{}) (result *VARIANT, err error) {
|
||||||
|
id, err := d.GetId(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err = d.Invoke(id, DISPATCH_PROPERTYGET, params)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGet property on IDispatch with parameters.
|
||||||
|
func (d *Dispatch) MustGet(name string, params ...interface{}) (result *VARIANT) {
|
||||||
|
id, err := d.GetId(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = d.Invoke(id, DISPATCH_PROPERTYGET, params)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set property on IDispatch with parameters.
|
||||||
|
func (d *Dispatch) Set(name string, params ...interface{}) (result *VARIANT, err error) {
|
||||||
|
id, err := d.GetId(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err = d.Invoke(id, DISPATCH_PROPERTYPUT, params)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustSet property on IDispatch with parameters.
|
||||||
|
func (d *Dispatch) MustSet(name string, params ...interface{}) (result *VARIANT) {
|
||||||
|
id, err := d.GetId(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = d.Invoke(id, DISPATCH_PROPERTYPUT, params)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetId retrieves ID of name on IDispatch.
|
||||||
|
func (d *Dispatch) GetId(name string) (id int32, err error) {
|
||||||
|
var dispid []int32
|
||||||
|
dispid, err = d.Object.GetIDsOfName([]string{name})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id = dispid[0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIds retrieves all IDs of names on IDispatch.
|
||||||
|
func (d *Dispatch) GetIds(names ...string) (dispid []int32, err error) {
|
||||||
|
dispid, err = d.Object.GetIDsOfName(names)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke IDispatch on DisplayID of dispatch type with parameters.
|
||||||
|
//
|
||||||
|
// There have been problems where if send cascading params..., it would error
|
||||||
|
// out because the parameters would be empty.
|
||||||
|
func (d *Dispatch) Invoke(id int32, dispatch int16, params []interface{}) (result *VARIANT, err error) {
|
||||||
|
if len(params) < 1 {
|
||||||
|
result, err = d.Object.Invoke(id, dispatch)
|
||||||
|
} else {
|
||||||
|
result, err = d.Object.Invoke(id, dispatch, params...)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release IDispatch object.
|
||||||
|
func (d *Dispatch) Release() {
|
||||||
|
d.Object.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect initializes COM and attempts to load IUnknown based on given names.
|
||||||
|
func Connect(names ...string) (connection *Connection) {
|
||||||
|
connection.Initialize()
|
||||||
|
connection.Load(names...)
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package ole
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func Example_quickbooks() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
connection := &Connection{nil}
|
||||||
|
|
||||||
|
err = connection.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer connection.Uninitialize()
|
||||||
|
|
||||||
|
err = connection.Create("QBXMLRP2.RequestProcessor.1")
|
||||||
|
if err != nil {
|
||||||
|
if err.(*OleError).Code() == CO_E_CLASSSTRING {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer connection.Release()
|
||||||
|
|
||||||
|
dispatch, err := connection.Dispatch()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dispatch.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_quickbooksConnectHelperCallDispatch() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
connection := &Connection{nil}
|
||||||
|
|
||||||
|
err = connection.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer connection.Uninitialize()
|
||||||
|
|
||||||
|
err = connection.Create("QBXMLRP2.RequestProcessor.1")
|
||||||
|
if err != nil {
|
||||||
|
if err.(*OleError).Code() == CO_E_CLASSSTRING {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer connection.Release()
|
||||||
|
|
||||||
|
dispatch, err := connection.Dispatch()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dispatch.Release()
|
||||||
|
|
||||||
|
var result *VARIANT
|
||||||
|
|
||||||
|
_, err = dispatch.Call("OpenConnection2", "", "Test Application 1", 1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = dispatch.Call("BeginSession", "", 2)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket := result.ToString()
|
||||||
|
|
||||||
|
_, err = dispatch.Call("EndSession", ticket)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dispatch.Call("CloseConnection")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_quickbooksConnectHelperDispatchProperty() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
connection := &Connection{nil}
|
||||||
|
|
||||||
|
err = connection.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer connection.Uninitialize()
|
||||||
|
|
||||||
|
err = connection.Create("QBXMLRP2.RequestProcessor.1")
|
||||||
|
if err != nil {
|
||||||
|
if err.(*OleError).Code() == CO_E_CLASSSTRING {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer connection.Release()
|
||||||
|
|
||||||
|
dispatch, err := connection.Dispatch()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dispatch.Release()
|
||||||
|
|
||||||
|
var result *VARIANT
|
||||||
|
|
||||||
|
_, err = dispatch.Call("OpenConnection2", "", "Test Application 1", 1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = dispatch.Call("BeginSession", "", 2)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket := result.ToString()
|
||||||
|
|
||||||
|
result, err = dispatch.Get("QBXMLVersionsForSession", ticket)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conversion := result.ToArray()
|
||||||
|
|
||||||
|
totalElements, _ := conversion.TotalElements(0)
|
||||||
|
if totalElements != 13 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
versions := conversion.ToStringArray()
|
||||||
|
expectedVersionString := "1.0, 1.1, 2.0, 2.1, 3.0, 4.0, 4.1, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0"
|
||||||
|
versionString := strings.Join(versions, ", ")
|
||||||
|
|
||||||
|
if len(versions) != 13 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedVersionString != versionString {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conversion.Release()
|
||||||
|
|
||||||
|
_, err = dispatch.Call("EndSession", ticket)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dispatch.Call("CloseConnection")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package ole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example_quickbooks() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
connection := &Connection{nil}
|
||||||
|
|
||||||
|
err = connection.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer connection.Uninitialize()
|
||||||
|
|
||||||
|
err = connection.Create("QBXMLRP2.RequestProcessor.1")
|
||||||
|
if err != nil {
|
||||||
|
if err.(*OleError).Code() == CO_E_CLASSSTRING {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer connection.Release()
|
||||||
|
|
||||||
|
dispatch, err := connection.Dispatch()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dispatch.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectHelperCallDispatch_QuickBooks(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
connection := &Connection{nil}
|
||||||
|
|
||||||
|
err = connection.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
defer connection.Uninitialize()
|
||||||
|
|
||||||
|
err = connection.Create("QBXMLRP2.RequestProcessor.1")
|
||||||
|
if err != nil {
|
||||||
|
if err.(*OleError).Code() == CO_E_CLASSSTRING {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Log(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
defer connection.Release()
|
||||||
|
|
||||||
|
dispatch, err := connection.Dispatch()
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
defer dispatch.Release()
|
||||||
|
|
||||||
|
var result *VARIANT
|
||||||
|
|
||||||
|
_, err = dispatch.Call("OpenConnection2", "", "Test Application 1", 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = dispatch.Call("BeginSession", "", 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket := result.ToString()
|
||||||
|
|
||||||
|
_, err = dispatch.Call("EndSession", ticket)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dispatch.Call("CloseConnection")
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectHelperDispatchProperty_QuickBooks(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
connection := &Connection{nil}
|
||||||
|
|
||||||
|
err = connection.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
defer connection.Uninitialize()
|
||||||
|
|
||||||
|
err = connection.Create("QBXMLRP2.RequestProcessor.1")
|
||||||
|
if err != nil {
|
||||||
|
if err.(*OleError).Code() == CO_E_CLASSSTRING {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Log(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
defer connection.Release()
|
||||||
|
|
||||||
|
dispatch, err := connection.Dispatch()
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
defer dispatch.Release()
|
||||||
|
|
||||||
|
var result *VARIANT
|
||||||
|
|
||||||
|
_, err = dispatch.Call("OpenConnection2", "", "Test Application 1", 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = dispatch.Call("BeginSession", "", 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket := result.ToString()
|
||||||
|
|
||||||
|
result, err = dispatch.Get("QBXMLVersionsForSession", ticket)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
conversion := result.ToArray()
|
||||||
|
|
||||||
|
totalElements, _ := conversion.TotalElements(0)
|
||||||
|
if totalElements != 13 {
|
||||||
|
t.Log(fmt.Sprintf("%d total elements does not equal 13\n", totalElements))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
versions := conversion.ToStringArray()
|
||||||
|
expectedVersionString := "1.0, 1.1, 2.0, 2.1, 3.0, 4.0, 4.1, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0"
|
||||||
|
versionString := strings.Join(versions, ", ")
|
||||||
|
|
||||||
|
if len(versions) != 13 {
|
||||||
|
t.Log(fmt.Sprintf("%s\n", versionString))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedVersionString != versionString {
|
||||||
|
t.Log(fmt.Sprintf("Expected: %s\nActual: %s", expectedVersionString, versionString))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
conversion.Release()
|
||||||
|
|
||||||
|
_, err = dispatch.Call("EndSession", ticket)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dispatch.Call("CloseConnection")
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
package ole
|
||||||
|
|
||||||
|
const (
|
||||||
|
CLSCTX_INPROC_SERVER = 1
|
||||||
|
CLSCTX_INPROC_HANDLER = 2
|
||||||
|
CLSCTX_LOCAL_SERVER = 4
|
||||||
|
CLSCTX_INPROC_SERVER16 = 8
|
||||||
|
CLSCTX_REMOTE_SERVER = 16
|
||||||
|
CLSCTX_ALL = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER
|
||||||
|
CLSCTX_INPROC = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER
|
||||||
|
CLSCTX_SERVER = CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
COINIT_APARTMENTTHREADED = 0x2
|
||||||
|
COINIT_MULTITHREADED = 0x0
|
||||||
|
COINIT_DISABLE_OLE1DDE = 0x4
|
||||||
|
COINIT_SPEED_OVER_MEMORY = 0x8
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DISPATCH_METHOD = 1
|
||||||
|
DISPATCH_PROPERTYGET = 2
|
||||||
|
DISPATCH_PROPERTYPUT = 4
|
||||||
|
DISPATCH_PROPERTYPUTREF = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
S_OK = 0x00000000
|
||||||
|
E_UNEXPECTED = 0x8000FFFF
|
||||||
|
E_NOTIMPL = 0x80004001
|
||||||
|
E_OUTOFMEMORY = 0x8007000E
|
||||||
|
E_INVALIDARG = 0x80070057
|
||||||
|
E_NOINTERFACE = 0x80004002
|
||||||
|
E_POINTER = 0x80004003
|
||||||
|
E_HANDLE = 0x80070006
|
||||||
|
E_ABORT = 0x80004004
|
||||||
|
E_FAIL = 0x80004005
|
||||||
|
E_ACCESSDENIED = 0x80070005
|
||||||
|
E_PENDING = 0x8000000A
|
||||||
|
|
||||||
|
CO_E_CLASSSTRING = 0x800401F3
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CC_FASTCALL = iota
|
||||||
|
CC_CDECL
|
||||||
|
CC_MSCPASCAL
|
||||||
|
CC_PASCAL = CC_MSCPASCAL
|
||||||
|
CC_MACPASCAL
|
||||||
|
CC_STDCALL
|
||||||
|
CC_FPFASTCALL
|
||||||
|
CC_SYSCALL
|
||||||
|
CC_MPWCDECL
|
||||||
|
CC_MPWPASCAL
|
||||||
|
CC_MAX = CC_MPWPASCAL
|
||||||
|
)
|
||||||
|
|
||||||
|
type VT uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
VT_EMPTY VT = 0x0
|
||||||
|
VT_NULL VT = 0x1
|
||||||
|
VT_I2 VT = 0x2
|
||||||
|
VT_I4 VT = 0x3
|
||||||
|
VT_R4 VT = 0x4
|
||||||
|
VT_R8 VT = 0x5
|
||||||
|
VT_CY VT = 0x6
|
||||||
|
VT_DATE VT = 0x7
|
||||||
|
VT_BSTR VT = 0x8
|
||||||
|
VT_DISPATCH VT = 0x9
|
||||||
|
VT_ERROR VT = 0xa
|
||||||
|
VT_BOOL VT = 0xb
|
||||||
|
VT_VARIANT VT = 0xc
|
||||||
|
VT_UNKNOWN VT = 0xd
|
||||||
|
VT_DECIMAL VT = 0xe
|
||||||
|
VT_I1 VT = 0x10
|
||||||
|
VT_UI1 VT = 0x11
|
||||||
|
VT_UI2 VT = 0x12
|
||||||
|
VT_UI4 VT = 0x13
|
||||||
|
VT_I8 VT = 0x14
|
||||||
|
VT_UI8 VT = 0x15
|
||||||
|
VT_INT VT = 0x16
|
||||||
|
VT_UINT VT = 0x17
|
||||||
|
VT_VOID VT = 0x18
|
||||||
|
VT_HRESULT VT = 0x19
|
||||||
|
VT_PTR VT = 0x1a
|
||||||
|
VT_SAFEARRAY VT = 0x1b
|
||||||
|
VT_CARRAY VT = 0x1c
|
||||||
|
VT_USERDEFINED VT = 0x1d
|
||||||
|
VT_LPSTR VT = 0x1e
|
||||||
|
VT_LPWSTR VT = 0x1f
|
||||||
|
VT_RECORD VT = 0x24
|
||||||
|
VT_INT_PTR VT = 0x25
|
||||||
|
VT_UINT_PTR VT = 0x26
|
||||||
|
VT_FILETIME VT = 0x40
|
||||||
|
VT_BLOB VT = 0x41
|
||||||
|
VT_STREAM VT = 0x42
|
||||||
|
VT_STORAGE VT = 0x43
|
||||||
|
VT_STREAMED_OBJECT VT = 0x44
|
||||||
|
VT_STORED_OBJECT VT = 0x45
|
||||||
|
VT_BLOB_OBJECT VT = 0x46
|
||||||
|
VT_CF VT = 0x47
|
||||||
|
VT_CLSID VT = 0x48
|
||||||
|
VT_BSTR_BLOB VT = 0xfff
|
||||||
|
VT_VECTOR VT = 0x1000
|
||||||
|
VT_ARRAY VT = 0x2000
|
||||||
|
VT_BYREF VT = 0x4000
|
||||||
|
VT_RESERVED VT = 0x8000
|
||||||
|
VT_ILLEGAL VT = 0xffff
|
||||||
|
VT_ILLEGALMASKED VT = 0xfff
|
||||||
|
VT_TYPEMASK VT = 0xfff
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DISPID_UNKNOWN = -1
|
||||||
|
DISPID_VALUE = 0
|
||||||
|
DISPID_PROPERTYPUT = -3
|
||||||
|
DISPID_NEWENUM = -4
|
||||||
|
DISPID_EVALUATE = -5
|
||||||
|
DISPID_CONSTRUCTOR = -6
|
||||||
|
DISPID_DESTRUCTOR = -7
|
||||||
|
DISPID_COLLECT = -8
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TKIND_ENUM = 1
|
||||||
|
TKIND_RECORD = 2
|
||||||
|
TKIND_MODULE = 3
|
||||||
|
TKIND_INTERFACE = 4
|
||||||
|
TKIND_DISPATCH = 5
|
||||||
|
TKIND_COCLASS = 6
|
||||||
|
TKIND_ALIAS = 7
|
||||||
|
TKIND_UNION = 8
|
||||||
|
TKIND_MAX = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
// Safe Array Feature Flags
|
||||||
|
|
||||||
|
const (
|
||||||
|
FADF_AUTO = 0x0001
|
||||||
|
FADF_STATIC = 0x0002
|
||||||
|
FADF_EMBEDDED = 0x0004
|
||||||
|
FADF_FIXEDSIZE = 0x0010
|
||||||
|
FADF_RECORD = 0x0020
|
||||||
|
FADF_HAVEIID = 0x0040
|
||||||
|
FADF_HAVEVARTYPE = 0x0080
|
||||||
|
FADF_BSTR = 0x0100
|
||||||
|
FADF_UNKNOWN = 0x0200
|
||||||
|
FADF_DISPATCH = 0x0400
|
||||||
|
FADF_VARIANT = 0x0800
|
||||||
|
FADF_RESERVED = 0xF008
|
||||||
|
)
|
|
@ -0,0 +1,51 @@
|
||||||
|
package ole
|
||||||
|
|
||||||
|
// OleError stores COM errors.
|
||||||
|
type OleError struct {
|
||||||
|
hr uintptr
|
||||||
|
description string
|
||||||
|
subError error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewError creates new error with HResult.
|
||||||
|
func NewError(hr uintptr) *OleError {
|
||||||
|
return &OleError{hr: hr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrorWithDescription creates new COM error with HResult and description.
|
||||||
|
func NewErrorWithDescription(hr uintptr, description string) *OleError {
|
||||||
|
return &OleError{hr: hr, description: description}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrorWithSubError creates new COM error with parent error.
|
||||||
|
func NewErrorWithSubError(hr uintptr, description string, err error) *OleError {
|
||||||
|
return &OleError{hr: hr, description: description, subError: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code is the HResult.
|
||||||
|
func (v *OleError) Code() uintptr {
|
||||||
|
return uintptr(v.hr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String description, either manually set or format message with error code.
|
||||||
|
func (v *OleError) String() string {
|
||||||
|
if v.description != "" {
|
||||||
|
return errstr(int(v.hr)) + " (" + v.description + ")"
|
||||||
|
}
|
||||||
|
return errstr(int(v.hr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements error interface.
|
||||||
|
func (v *OleError) Error() string {
|
||||||
|
return v.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description retrieves error summary, if there is one.
|
||||||
|
func (v *OleError) Description() string {
|
||||||
|
return v.description
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubError returns parent error, if there is one.
|
||||||
|
func (v *OleError) SubError() error {
|
||||||
|
return v.subError
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package ole
|
||||||
|
|
||||||
|
// errstr converts error code to string.
|
||||||
|
func errstr(errno int) string {
|
||||||
|
return ""
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue