├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── args.go ├── builtin.go ├── marks.go ├── merge.go └── vars.go ├── go.mod ├── go.sum ├── main.go ├── pkg ├── data │ ├── data.go │ └── data_test.go ├── dotenv │ └── parser.go ├── internal │ └── bytebufferpool │ │ ├── bytebuffer.go │ │ ├── bytebuffer_example_test.go │ │ ├── bytebuffer_test.go │ │ ├── bytebuffer_timing_test.go │ │ ├── doc.go │ │ ├── pool.go │ │ └── pool_test.go ├── table │ ├── table.go │ └── table_test.go ├── template │ ├── example_test.go │ ├── template.go │ ├── template_test.go │ ├── template_timing_test.go │ └── unsafe.go └── vcs │ ├── support.go │ ├── vcs.go │ └── vcs_test.go └── testdata ├── Dockerfile.tpl ├── Dockerfile.vars ├── sample.tbd └── sample.vars /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test and coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 2 12 | - uses: actions/setup-go@v2 13 | with: 14 | go-version: '1.16' 15 | - name: Run coverage 16 | run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic 17 | - name: Upload coverage to Codecov 18 | run: bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Intellij 2 | .idea/**/workspace.xml 3 | .idea/**/tasks.xml 4 | .idea/**/encodings.xml 5 | .idea/**/compiler.xml 6 | .idea/**/misc.xml 7 | .idea/**/modules.xml 8 | .idea/**/vcs.xml 9 | 10 | ## VSCode 11 | .vscode/ 12 | 13 | ## File-based project format: 14 | *.iws 15 | *.iml 16 | .idea/ 17 | 18 | # Binaries for programs and plugins 19 | *.exe 20 | *.exe~ 21 | *.dll 22 | *.so 23 | *.dylib 24 | *.dat 25 | *.DS_Store 26 | 27 | # Test binary, built with `go test -c` 28 | *.test 29 | 30 | # Output of the go coverage tool, specifically when used with LiteIDE 31 | *.out 32 | 33 | # Goreleaser builds 34 | **/dist/** 35 | 36 | # This is my wip ideas folder 37 | experiments/** 38 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | # Run locally with: goreleaser --rm-dist --snapshot --skip-publish 4 | project_name: tbd 5 | before: 6 | hooks: 7 | - go mod tidy 8 | - go mod download 9 | builds: 10 | - binary: '{{ .ProjectName }}' 11 | main: ./main.go 12 | env: 13 | - CGO_ENABLED=0 14 | ldflags: 15 | - -s -w -X main.commit={{.ShortCommit}} 16 | - -a -extldflags "-static" 17 | goos: 18 | - windows 19 | - linux 20 | - darwin 21 | goarch: 22 | - arm 23 | - arm64 24 | - amd64 25 | goarm: 26 | - 7 27 | ignore: 28 | - goos: darwin 29 | goarch: arm 30 | - goos: darwin 31 | goarch: arm64 32 | - goos: windows 33 | goarch: arm 34 | - goos: windows 35 | goarch: arm64 36 | archives: 37 | - replacements: 38 | darwin: macOS 39 | windows: win 40 | amd64: 64-bit 41 | checksum: 42 | name_template: 'checksums.txt' 43 | snapshot: 44 | name_template: "{{ .ProjectName }}_{{ .Tag }}" 45 | nfpms: 46 | - 47 | package_name: tbd 48 | vendor: Luca Sepe 49 | homepage: https://lucasepe.it/ 50 | maintainer: Luca Sepe 51 | description: A really simple way to create text templates with placeholders. 52 | license: MIT 53 | replacements: 54 | amd64: 64-bit 55 | formats: 56 | - deb 57 | - rpm 58 | changelog: 59 | sort: asc 60 | filters: 61 | exclude: 62 | - '^docs:' 63 | - '^test:' 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright(c) 2021 Luca Sepe 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY := $(shell basename "$(PWD)") 2 | SOURCES := ./ 3 | GIT_COMMIT := $(shell git rev-list -1 HEAD) 4 | 5 | 6 | .PHONY: help 7 | all: help 8 | help: Makefile 9 | @echo 10 | @echo " Choose a command run in "$(PROJECTNAME)":" 11 | @echo 12 | @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' 13 | @echo 14 | 15 | .DEFAULT_GOAL := help 16 | 17 | ## build: Build the command line tool 18 | build: clean 19 | CGO_ENABLED=0 go build \ 20 | -ldflags '-w -extldflags "-static" -X main.gitCommit=$(GIT_COMMIT)' \ 21 | -o ${BINARY} ${SOURCES} 22 | 23 | ## release: Build release artifacts 24 | release: 25 | goreleaser --rm-dist --snapshot --skip-publish 26 | 27 | ## pack: Shrink the binary size 28 | pack: build 29 | upx -9 ${BINARY} 30 | 31 | ## test: Starts unit test 32 | test: 33 | go test -v ./... -coverprofile coverage.out 34 | 35 | ## clean: Clean the binary 36 | clean: 37 | rm -f $(BINARY) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `tbd` 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/lucasepe/tbd?style=flat-square)](https://goreportcard.com/report/github.com/lucasepe/tbd)     4 | [![Release](https://img.shields.io/github/release/lucasepe/tbd.svg?style=flat-square)](https://github.com/lucasepe/tbd/releases/latest)     5 | [![codecov](https://codecov.io/gh/lucasepe/tbd/branch/main/graph/badge.svg?style=flat-square)](https://codecov.io/gh/lucasepe/tbd) 6 | 7 | _"to be defined"_ 8 | 9 | ## A really simple way to create text templates with placeholders. 10 | 11 | This tool is deliberately simple and trivial, no advanced features. 12 | 13 | > If you need advanced templates rendering which supports complex syntax and a huge list of datasources (JSON, YAML, AWS EC2 metadata, BoltDB, Hashicorp > Consul and Hashicorp Vault secrets), I recommend you use one of these: 14 | > 15 | > - [gotemplate](https://github.com/hairyhenderson/gomplate) 16 | > - [pongo2](https://github.com/flosch/pongo2) 17 | > - [quicktemplate](https://github.com/valyala/quicktemplate) 18 | 19 | ## Built-in Variables 20 | 21 | When executed inside a Git repository, `tbd` automatically exports some variables related to the Git repository which may be useful in the build phase. 22 | 23 | These variables are: `ARCH`, `OS`, `REPO_COMMIT`, `REPO_HOST`, `REPO_NAME`, `REPO_ROOT`, `REPO_TAG`, `REPO_TAG_CLEAN`, `REPO_URL`, `TIMESTAMP`. 24 | 25 | Try it! With `tbd` in your `PATH`, go in a Git folder and type: 26 | 27 | ```sh 28 | $ tbd vars 29 | +----------------+------------------------------------------+ 30 | | ARCH | amd64 | 31 | | OS | linux | 32 | | REPO_COMMIT | a3193274112d3a6f5c2a0277e2ca07ec238d622f | 33 | | REPO_HOST | github.com | 34 | | REPO_NAME | tbd | 35 | | REPO_ROOT | lucasepe | 36 | | REPO_TAG | v0.1.1 | 37 | | REPO_TAG_CLEAN | 0.1.1 | 38 | | REPO_URL | https://github.com/lucasepe/tbd | 39 | | TIMESTAMP | 2021-07-26T14:22:36Z | 40 | +----------------+------------------------------------------+ 41 | ``` 42 | 43 | > Obviously in your case the values ​​will be different. 44 | 45 | ## How does a template looks like ? 46 | 47 | A template is a text document in which you can insert placeholders for the text you want to make dynamic. 48 | 49 | - a placeholder is delimited by `{{` and `}}` - (i.e. `{{ FULL_NAME }}`) 50 | - all text outside placeholders is copied to the output unchanged 51 | 52 | Example: 53 | 54 | ```yaml 55 | apiVersion: v1 56 | kind: Pod 57 | metadata: 58 | name: {{ metadata.name }} 59 | labels: 60 | app: {{ metadata.labels.app }} 61 | spec: 62 | containers: 63 | - name: {{ container.1.name }} 64 | image: {{ container.1.image }} 65 | ports: 66 | - containerPort: {{ container.1.port }} 67 | - name: {{ container.2.name }} 68 | image: {{ container.2.image }} 69 | ports: 70 | - containerPort: {{ container.2.port }} 71 | ``` 72 | 73 | Another example: 74 | 75 | ```txt 76 | {{ greeting }} 77 | 78 | I will be out of the office from {{ start.date }} until {{ return.date }}. 79 | If you need immediate assistance while I’m away, please email {{ contact.email }}. 80 | 81 | Best, 82 | {{ name }} 83 | ``` 84 | 85 | ## How can I define placeholders values? 86 | 87 | Create a text file in which you enter the values for the placeholders. 88 | 89 | - define a placeholder value using `KEY = value` (or `KEY: value`) 90 | - empty lines are skipped 91 | - lines beginning with `#` are treated as comments 92 | 93 | Example: 94 | 95 | ```sh 96 | # metadata values 97 | metadata.name = rss-site 98 | metadata.labels.app = web 99 | 100 | # containers values 101 | container.1.name = front-end 102 | container.1.image = nginx 103 | container.1.port = 80 104 | 105 | container.2.name = rss-reader 106 | container.2.image: nickchase/rss-php-nginx:v1 107 | container.2.port: 88 108 | ``` 109 | 110 | Another example... 111 | 112 | ```sh 113 | greeting: Greetings 114 | start.date: August, 9 115 | return.date: August 23 116 | contact.email: pinco.pallo@gmail.com 117 | name: Pinco Pallo 118 | ``` 119 | 120 | ## How fill in the template? 121 | 122 | > Use the `merge` command 123 | 124 | ```sh 125 | $ tbd merge /path/to/your/template /path/to/your/envfile 126 | ``` 127 | 128 | Example: 129 | 130 | ```sh 131 | $ tbd merge testdata/sample.tbd testdata/sample.vars 132 | ``` 133 | 134 | 👉 you can also specify an HTTP url to fetch your template and/or placeholders values. 135 | 136 | Example: 137 | 138 | ```sh 139 | $ tbd merge https://raw.githubusercontent.com/lucasepe/tbd/main/testdata/sample.tbd \ 140 | https://raw.githubusercontent.com/lucasepe/tbd/main/testdata/sample.vars 141 | ``` 142 | 143 | and the output is... 144 | 145 | ```txt 146 | Greetings 147 | 148 | I will be out of the office from August, 9 until August 23. 149 | If you need immediate assistance while I’m away, please email pinco.pallo@gmail.com. 150 | 151 | Best, 152 | Pinco Pallo 153 | ``` 154 | 155 | ## How to list all template placeholders? 156 | 157 | > Use the `marks` command. 158 | 159 | ```sh 160 | $ tbd marks /path/to/your/template 161 | ``` 162 | 163 | Example: 164 | 165 | ```sh 166 | $ tbd marks testdata/sample.tbd 167 | greeting 168 | start.date 169 | return.date 170 | contact.email 171 | name 172 | ``` 173 | 174 | ## How to list all variables? 175 | 176 | > Use the `vars` command. 177 | 178 | ```sh 179 | $ tbd vars /path/to/your/envfile 180 | ``` 181 | 182 | Example: 183 | 184 | ```sh 185 | $ tbd vars testdata/sample.vars 186 | +----------------+------------------------------------------+ 187 | | Label | Value | 188 | +----------------+------------------------------------------+ 189 | | ARCH | amd64 | 190 | | OS | linux | 191 | | REPO_COMMIT | a3193274112d3a6f5c2a0277e2ca07ec238d622f | 192 | | REPO_HOST | github.com | 193 | | REPO_NAME | tbd | 194 | | REPO_ROOT | lucasepe | 195 | | REPO_TAG | v0.1.1 | 196 | | REPO_TAG_CLEAN | 0.1.1 | 197 | | REPO_URL | https://github.com/lucasepe/tbd | 198 | | TIMESTAMP | 2021-07-26T14:17:49Z | 199 | | contact.email | pinco.pallo@gmail.com | 200 | | greeting | Greetings | 201 | | name | Pinco Pallo | 202 | | return.date | August 23 | 203 | | start.date | August, 9 | 204 | +----------------+------------------------------------------+ 205 | ``` 206 | 207 | > As you can see, since I ran the command in a Git repository, there are also relative variables. 208 | 209 | # How to install? 210 | 211 | If you have [golang](https://golang.org/dl/) installed: 212 | 213 | ```sh 214 | $ go install github.com/lucasepe/tbd@latest 215 | ``` 216 | 217 | This will create the executable under your `$GOPATH/bin` directory. 218 | 219 | ## Ready-To-Use Releases 220 | 221 | If you don't want to compile the sourcecode yourself, [here you can find the tool already compiled](https://github.com/lucasepe/tbd/releases/latest) for: 222 | 223 | - MacOS 224 | - Linux 225 | - Windows 226 | 227 |
228 | 229 | #### Credits 230 | 231 | Thanks to [@valyala](https://github.com/valyala/) for the [fasttemplate](https://github.com/valyala/fasttemplate) library - which I have modified by adding and removing some functions for the `tbd` purpose. 232 | -------------------------------------------------------------------------------- /cmd/args.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/alexflint/go-arg" 8 | ) 9 | 10 | const ( 11 | description = "A really simple way to create text templates with placeholders." 12 | banner = `╔╦╗ ╔╗ ╔╦╗ 13 | ║ ╠╩╗ ║║ 14 | ╩ o ╚═╝ e ═╩╝ efined` 15 | ) 16 | 17 | type App struct { 18 | Merge *MergeCmd `arg:"subcommand:merge" help:"combines a template with one or more env files"` 19 | Marks *MarksCmd `arg:"subcommand:marks" help:"shows all placeholders defined in the specified template"` 20 | Vars *VarsCmd `arg:"subcommand:vars" help:"shows all built-in (and eventually user defined) variables"` 21 | } 22 | 23 | func (App) Description() string { 24 | return fmt.Sprintf("%s\n%s\n", banner, description) 25 | } 26 | 27 | func Run() error { 28 | var app App 29 | 30 | p := arg.MustParse(&app) 31 | 32 | switch { 33 | case app.Vars != nil: 34 | return app.Vars.Run() 35 | case app.Marks != nil: 36 | return app.Marks.Run() 37 | case app.Merge != nil: 38 | return app.Merge.Run() 39 | default: 40 | p.WriteHelp(os.Stdout) 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /cmd/builtin.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/lucasepe/tbd/pkg/data" 10 | "github.com/lucasepe/tbd/pkg/dotenv" 11 | "github.com/lucasepe/tbd/pkg/vcs" 12 | ) 13 | 14 | const ( 15 | TimeStamp = "TIMESTAMP" 16 | OS = "OS" 17 | ARCH = "ARCH" 18 | ) 19 | 20 | func builtinVars() (map[string]string, error) { 21 | meta := map[string]string{} 22 | meta[TimeStamp] = time.Now().Local().UTC().Format(time.RFC3339) 23 | meta[OS] = runtime.GOOS 24 | meta[ARCH] = runtime.GOARCH 25 | 26 | if cwd, err := os.Getwd(); err == nil { 27 | vcs.GitRepoMetadata(cwd, meta) 28 | } 29 | 30 | return meta, nil 31 | } 32 | 33 | func userVars(vars map[string]string, envfile ...string) error { 34 | if len(envfile) <= 0 { 35 | return nil 36 | } 37 | 38 | for _, el := range envfile { 39 | const maxFileSize int64 = 512 * 1000 40 | buf, err := data.Fetch(el, maxFileSize) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | if err := dotenv.ParseInto(bytes.NewBuffer(buf), vars); err != nil { 46 | return err 47 | } 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /cmd/marks.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lucasepe/tbd/pkg/data" 7 | "github.com/lucasepe/tbd/pkg/template" 8 | ) 9 | 10 | type MarksCmd struct { 11 | Template string `arg:"positional,required" placeholder:"TEMPLATE"` 12 | } 13 | 14 | func (c *MarksCmd) Run() error { 15 | const maxFileSize int64 = 512 * 1000 16 | tpl, err := data.Fetch(c.Template, maxFileSize) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | list, _ := template.Marks(string(tpl), "{{", "}}") 22 | for _, x := range list { 23 | fmt.Println(x) 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /cmd/merge.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/lucasepe/tbd/pkg/data" 7 | "github.com/lucasepe/tbd/pkg/template" 8 | ) 9 | 10 | type MergeCmd struct { 11 | Template string `arg:"positional,required" placeholder:"TEMPLATE"` 12 | EnvFiles []string `arg:"positional" placeholder:"ENV_FILE"` 13 | } 14 | 15 | func (c *MergeCmd) Run() error { 16 | meta, err := builtinVars() 17 | if err != nil { 18 | return err 19 | } 20 | 21 | if err := userVars(meta, c.EnvFiles...); err != nil { 22 | return err 23 | } 24 | 25 | const maxFileSize int64 = 512 * 1000 26 | tpl, err := data.Fetch(c.Template, maxFileSize) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | env := make(map[string]interface{}) 32 | for k, v := range meta { 33 | env[k] = v 34 | } 35 | 36 | _, err = template.ExecuteStd(string(tpl), "{{", "}}", os.Stdout, env) 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /cmd/vars.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/lucasepe/tbd/pkg/table" 8 | ) 9 | 10 | type VarsCmd struct { 11 | EnvFiles []string `arg:"positional" placeholder:"ENV_FILE"` 12 | } 13 | 14 | func (c *VarsCmd) Run() error { 15 | meta, err := builtinVars() 16 | if err != nil { 17 | return err 18 | } 19 | 20 | if err := userVars(meta, c.EnvFiles...); err != nil { 21 | return err 22 | } 23 | 24 | keys := make([]string, 0, len(meta)) 25 | for k := range meta { 26 | keys = append(keys, k) 27 | } 28 | sort.Strings(keys) 29 | 30 | tbl := &table.TextTable{} 31 | tbl.SetHeader("Label", "Value") 32 | 33 | for _, k := range keys { 34 | tbl.AddRow(k, meta[k]) 35 | } 36 | 37 | fmt.Println(tbl.Draw()) 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lucasepe/tbd 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/alexflint/go-arg v1.4.2 7 | github.com/go-git/go-git/v5 v5.4.2 8 | github.com/google/go-cmp v0.5.5 9 | github.com/mattn/go-runewidth v0.0.13 10 | github.com/pkg/errors v0.9.1 11 | github.com/stretchr/testify v1.7.0 12 | github.com/whilp/git-urls v1.0.0 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= 2 | github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= 3 | github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= 4 | github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= 5 | github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= 6 | github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= 7 | github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= 8 | github.com/alexflint/go-arg v1.4.2 h1:lDWZAXxpAnZUq4qwb86p/3rIJJ2Li81EoMbTMujhVa0= 9 | github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= 10 | github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70= 11 | github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= 12 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 13 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 14 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 15 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 16 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 21 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 22 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 23 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 24 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 25 | github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= 26 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 27 | github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 28 | github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= 29 | github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 30 | github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= 31 | github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= 32 | github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= 33 | github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= 34 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 35 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 36 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 37 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 38 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 39 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 40 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 41 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 42 | github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= 43 | github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 44 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 45 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 46 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 47 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 48 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 49 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 50 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 51 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 52 | github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 53 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 54 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 55 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 56 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 57 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 58 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 59 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 60 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 61 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 62 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 64 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 65 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 66 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 67 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 68 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 69 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 70 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 72 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 73 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 74 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 75 | github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU= 76 | github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= 77 | github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= 78 | github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= 79 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 80 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 81 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= 82 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 83 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 84 | golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs= 85 | golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= 86 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 87 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 88 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 h1:RX8C8PRZc2hTIod4ds8ij+/4RQX3AqhYj3uOHmyaz4E= 94 | golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 96 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 97 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 98 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 99 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 100 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 101 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 102 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 103 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 104 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 105 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 106 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 107 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 108 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 109 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 110 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 111 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 112 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 113 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 114 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 115 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/lucasepe/tbd/cmd" 8 | ) 9 | 10 | func main() { 11 | if err := cmd.Run(); err != nil { 12 | fmt.Fprintln(os.Stderr, err) 13 | os.Exit(-1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkg/data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // Fetch gets the bytes at the specified URI. 12 | // The URI can be remote (http) or local. 13 | // if 'limit' is greater then zero, fetch stops 14 | // with EOF after 'limit' bytes. 15 | func Fetch(uri string, limit int64) ([]byte, error) { 16 | if strings.HasPrefix(uri, "http") { 17 | return FetchFromURI(uri, limit) 18 | } 19 | 20 | return FetchFromFile(uri, limit) 21 | } 22 | 23 | // FetchFromURI fetch data (with limit) from an HTTP URL. 24 | // if 'limit' is greater then zero, fetch stops 25 | // with EOF after 'limit' bytes. 26 | func FetchFromURI(uri string, limit int64) ([]byte, error) { 27 | res, err := http.Get(uri) 28 | if err != nil { 29 | return nil, err 30 | } 31 | defer res.Body.Close() 32 | 33 | if limit > 0 { 34 | return ioutil.ReadAll(io.LimitReader(res.Body, limit)) 35 | } 36 | 37 | return ioutil.ReadAll(res.Body) 38 | } 39 | 40 | // FetchFromFile fetch data (with limit) from an file. 41 | // if 'limit' is greater then zero, fetch stops 42 | // with EOF after 'limit' bytes. 43 | func FetchFromFile(filename string, limit int64) ([]byte, error) { 44 | fp, err := os.Open(filename) 45 | if err != nil { 46 | return nil, err 47 | } 48 | defer fp.Close() 49 | 50 | if limit > 0 { 51 | return ioutil.ReadAll(io.LimitReader(fp, limit)) 52 | } 53 | 54 | return ioutil.ReadAll(fp) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/data/data_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestFetchFromURI(t *testing.T) { 12 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | fmt.Fprint(w, "Hello from scrawl!") 14 | })) 15 | defer ts.Close() 16 | 17 | data, err := FetchFromURI(ts.URL, 1024*10) 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | 22 | want := "Hello from scrawl!" 23 | if got := string(data); got != want { 24 | t.Errorf("got [%v] want [%v]", got, want) 25 | } 26 | } 27 | 28 | func TestFetchFromFile(t *testing.T) { 29 | data, err := FetchFromFile("../../testdata/sample.tbd", 10) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | 34 | want := `{{ greetin` 35 | if got := flatten(string(data)); got != want { 36 | t.Errorf("got [%v] want [%v]", got, want) 37 | } 38 | } 39 | 40 | // remove tabs and newlines and spaces 41 | func flatten(s string) string { 42 | return strings.Replace((strings.Replace(s, "\n", "", -1)), "\t", "", -1) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/dotenv/parser.go: -------------------------------------------------------------------------------- 1 | package dotenv 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | func ParseInto(r io.Reader, envMap map[string]string) (err error) { 12 | var lines []string 13 | scanner := bufio.NewScanner(r) 14 | for scanner.Scan() { 15 | lines = append(lines, scanner.Text()) 16 | } 17 | 18 | if err = scanner.Err(); err != nil { 19 | return 20 | } 21 | 22 | for _, fullLine := range lines { 23 | if !isIgnoredLine(fullLine) { 24 | var key, value string 25 | key, value, err = parseLine(fullLine, envMap) 26 | 27 | if err != nil { 28 | return 29 | } 30 | envMap[key] = value 31 | } 32 | } 33 | return 34 | } 35 | 36 | // Parse reads an env file from io.Reader, returning a map of keys and values. 37 | func Parse(r io.Reader) (envMap map[string]string, err error) { 38 | envMap = make(map[string]string) 39 | err = ParseInto(r, envMap) 40 | return 41 | } 42 | 43 | var exportRegex = regexp.MustCompile(`^\s*(?:export\s+)?(.*?)\s*$`) 44 | 45 | func parseLine(line string, envMap map[string]string) (key string, value string, err error) { 46 | if len(line) == 0 { 47 | err = errors.New("zero length string") 48 | return 49 | } 50 | 51 | // ditch the comments (but keep quoted hashes) 52 | if strings.Contains(line, "#") { 53 | segmentsBetweenHashes := strings.Split(line, "#") 54 | quotesAreOpen := false 55 | var segmentsToKeep []string 56 | for _, segment := range segmentsBetweenHashes { 57 | if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 { 58 | if quotesAreOpen { 59 | quotesAreOpen = false 60 | segmentsToKeep = append(segmentsToKeep, segment) 61 | } else { 62 | quotesAreOpen = true 63 | } 64 | } 65 | 66 | if len(segmentsToKeep) == 0 || quotesAreOpen { 67 | segmentsToKeep = append(segmentsToKeep, segment) 68 | } 69 | } 70 | 71 | line = strings.Join(segmentsToKeep, "#") 72 | } 73 | 74 | firstEquals := strings.Index(line, "=") 75 | firstColon := strings.Index(line, ":") 76 | splitString := strings.SplitN(line, "=", 2) 77 | if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) { 78 | //this is a yaml-style line 79 | splitString = strings.SplitN(line, ":", 2) 80 | } 81 | 82 | if len(splitString) != 2 { 83 | err = errors.New("Can't separate key from value") 84 | return 85 | } 86 | 87 | // Parse the key 88 | key = splitString[0] 89 | if strings.HasPrefix(key, "export") { 90 | key = strings.TrimPrefix(key, "export") 91 | } 92 | key = strings.TrimSpace(key) 93 | 94 | key = exportRegex.ReplaceAllString(splitString[0], "$1") 95 | 96 | // Parse the value 97 | value = parseValue(splitString[1], envMap) 98 | return 99 | } 100 | 101 | var ( 102 | singleQuotesRegex = regexp.MustCompile(`\A'(.*)'\z`) 103 | doubleQuotesRegex = regexp.MustCompile(`\A"(.*)"\z`) 104 | escapeRegex = regexp.MustCompile(`\\.`) 105 | unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) 106 | ) 107 | 108 | func parseValue(value string, envMap map[string]string) string { 109 | 110 | // trim 111 | value = strings.Trim(value, " ") 112 | 113 | // check if we've got quoted values or possible escapes 114 | if len(value) > 1 { 115 | singleQuotes := singleQuotesRegex.FindStringSubmatch(value) 116 | 117 | doubleQuotes := doubleQuotesRegex.FindStringSubmatch(value) 118 | 119 | if singleQuotes != nil || doubleQuotes != nil { 120 | // pull the quotes off the edges 121 | value = value[1 : len(value)-1] 122 | } 123 | 124 | if doubleQuotes != nil { 125 | // expand newlines 126 | value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string { 127 | c := strings.TrimPrefix(match, `\`) 128 | switch c { 129 | case "n": 130 | return "\n" 131 | case "r": 132 | return "\r" 133 | default: 134 | return match 135 | } 136 | }) 137 | // unescape characters 138 | value = unescapeCharsRegex.ReplaceAllString(value, "$1") 139 | } 140 | 141 | if singleQuotes == nil { 142 | value = expandVariables(value, envMap) 143 | } 144 | } 145 | 146 | return value 147 | } 148 | 149 | var expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) 150 | 151 | func expandVariables(v string, m map[string]string) string { 152 | return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { 153 | submatch := expandVarRegex.FindStringSubmatch(s) 154 | if submatch == nil { 155 | return s 156 | } 157 | 158 | if submatch[1] == "\\" || submatch[2] == "(" { 159 | return submatch[0][1:] 160 | } else if submatch[4] != "" { 161 | v := m[submatch[4]] 162 | return v 163 | } 164 | return s 165 | }) 166 | } 167 | 168 | func isIgnoredLine(line string) bool { 169 | trimmedLine := strings.TrimSpace(line) 170 | return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#") 171 | } 172 | -------------------------------------------------------------------------------- /pkg/internal/bytebufferpool/bytebuffer.go: -------------------------------------------------------------------------------- 1 | package bytebufferpool 2 | 3 | import "io" 4 | 5 | // ByteBuffer provides byte buffer, which can be used for minimizing 6 | // memory allocations. 7 | // 8 | // ByteBuffer may be used with functions appending data to the given []byte 9 | // slice. See example code for details. 10 | // 11 | // Use Get for obtaining an empty byte buffer. 12 | type ByteBuffer struct { 13 | 14 | // B is a byte buffer to use in append-like workloads. 15 | // See example code for details. 16 | B []byte 17 | } 18 | 19 | // Len returns the size of the byte buffer. 20 | func (b *ByteBuffer) Len() int { 21 | return len(b.B) 22 | } 23 | 24 | // ReadFrom implements io.ReaderFrom. 25 | // 26 | // The function appends all the data read from r to b. 27 | func (b *ByteBuffer) ReadFrom(r io.Reader) (int64, error) { 28 | p := b.B 29 | nStart := int64(len(p)) 30 | nMax := int64(cap(p)) 31 | n := nStart 32 | if nMax == 0 { 33 | nMax = 64 34 | p = make([]byte, nMax) 35 | } else { 36 | p = p[:nMax] 37 | } 38 | for { 39 | if n == nMax { 40 | nMax *= 2 41 | bNew := make([]byte, nMax) 42 | copy(bNew, p) 43 | p = bNew 44 | } 45 | nn, err := r.Read(p[n:]) 46 | n += int64(nn) 47 | if err != nil { 48 | b.B = p[:n] 49 | n -= nStart 50 | if err == io.EOF { 51 | return n, nil 52 | } 53 | return n, err 54 | } 55 | } 56 | } 57 | 58 | // WriteTo implements io.WriterTo. 59 | func (b *ByteBuffer) WriteTo(w io.Writer) (int64, error) { 60 | n, err := w.Write(b.B) 61 | return int64(n), err 62 | } 63 | 64 | // Bytes returns b.B, i.e. all the bytes accumulated in the buffer. 65 | // 66 | // The purpose of this function is bytes.Buffer compatibility. 67 | func (b *ByteBuffer) Bytes() []byte { 68 | return b.B 69 | } 70 | 71 | // Write implements io.Writer - it appends p to ByteBuffer.B 72 | func (b *ByteBuffer) Write(p []byte) (int, error) { 73 | b.B = append(b.B, p...) 74 | return len(p), nil 75 | } 76 | 77 | // WriteByte appends the byte c to the buffer. 78 | // 79 | // The purpose of this function is bytes.Buffer compatibility. 80 | // 81 | // The function always returns nil. 82 | func (b *ByteBuffer) WriteByte(c byte) error { 83 | b.B = append(b.B, c) 84 | return nil 85 | } 86 | 87 | // WriteString appends s to ByteBuffer.B. 88 | func (b *ByteBuffer) WriteString(s string) (int, error) { 89 | b.B = append(b.B, s...) 90 | return len(s), nil 91 | } 92 | 93 | // Set sets ByteBuffer.B to p. 94 | func (b *ByteBuffer) Set(p []byte) { 95 | b.B = append(b.B[:0], p...) 96 | } 97 | 98 | // SetString sets ByteBuffer.B to s. 99 | func (b *ByteBuffer) SetString(s string) { 100 | b.B = append(b.B[:0], s...) 101 | } 102 | 103 | // String returns string representation of ByteBuffer.B. 104 | func (b *ByteBuffer) String() string { 105 | return string(b.B) 106 | } 107 | 108 | // Reset makes ByteBuffer.B empty. 109 | func (b *ByteBuffer) Reset() { 110 | b.B = b.B[:0] 111 | } 112 | -------------------------------------------------------------------------------- /pkg/internal/bytebufferpool/bytebuffer_example_test.go: -------------------------------------------------------------------------------- 1 | package bytebufferpool_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lucasepe/tbd/pkg/internal/bytebufferpool" 7 | ) 8 | 9 | func ExampleByteBuffer() { 10 | bb := bytebufferpool.Get() 11 | 12 | bb.WriteString("first line\n") 13 | bb.Write([]byte("second line\n")) 14 | bb.B = append(bb.B, "third line\n"...) 15 | 16 | fmt.Printf("bytebuffer contents=%q", bb.B) 17 | 18 | // It is safe to release byte buffer now, since it is 19 | // no longer used. 20 | bytebufferpool.Put(bb) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/internal/bytebufferpool/bytebuffer_test.go: -------------------------------------------------------------------------------- 1 | package bytebufferpool 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestByteBufferReadFrom(t *testing.T) { 12 | prefix := "foobar" 13 | expectedS := "asadfsdafsadfasdfisdsdfa" 14 | prefixLen := int64(len(prefix)) 15 | expectedN := int64(len(expectedS)) 16 | 17 | var bb ByteBuffer 18 | bb.WriteString(prefix) 19 | 20 | rf := (io.ReaderFrom)(&bb) 21 | for i := 0; i < 20; i++ { 22 | r := bytes.NewBufferString(expectedS) 23 | n, err := rf.ReadFrom(r) 24 | if n != expectedN { 25 | t.Fatalf("unexpected n=%d. Expecting %d. iteration %d", n, expectedN, i) 26 | } 27 | if err != nil { 28 | t.Fatalf("unexpected error: %s", err) 29 | } 30 | bbLen := int64(bb.Len()) 31 | expectedLen := prefixLen + int64(i+1)*expectedN 32 | if bbLen != expectedLen { 33 | t.Fatalf("unexpected byteBuffer length: %d. Expecting %d", bbLen, expectedLen) 34 | } 35 | for j := 0; j < i; j++ { 36 | start := prefixLen + int64(j)*expectedN 37 | b := bb.B[start : start+expectedN] 38 | if string(b) != expectedS { 39 | t.Fatalf("unexpected byteBuffer contents: %q. Expecting %q", b, expectedS) 40 | } 41 | } 42 | } 43 | } 44 | 45 | func TestByteBufferWriteTo(t *testing.T) { 46 | expectedS := "foobarbaz" 47 | var bb ByteBuffer 48 | bb.WriteString(expectedS[:3]) 49 | bb.WriteString(expectedS[3:]) 50 | 51 | wt := (io.WriterTo)(&bb) 52 | var w bytes.Buffer 53 | for i := 0; i < 10; i++ { 54 | n, err := wt.WriteTo(&w) 55 | if n != int64(len(expectedS)) { 56 | t.Fatalf("unexpected n returned from WriteTo: %d. Expecting %d", n, len(expectedS)) 57 | } 58 | if err != nil { 59 | t.Fatalf("unexpected error: %s", err) 60 | } 61 | s := string(w.Bytes()) 62 | if s != expectedS { 63 | t.Fatalf("unexpected string written %q. Expecting %q", s, expectedS) 64 | } 65 | w.Reset() 66 | } 67 | } 68 | 69 | func TestByteBufferGetPutSerial(t *testing.T) { 70 | testByteBufferGetPut(t) 71 | } 72 | 73 | func TestByteBufferGetPutConcurrent(t *testing.T) { 74 | concurrency := 10 75 | ch := make(chan struct{}, concurrency) 76 | for i := 0; i < concurrency; i++ { 77 | go func() { 78 | testByteBufferGetPut(t) 79 | ch <- struct{}{} 80 | }() 81 | } 82 | 83 | for i := 0; i < concurrency; i++ { 84 | select { 85 | case <-ch: 86 | case <-time.After(time.Second): 87 | t.Fatalf("timeout!") 88 | } 89 | } 90 | } 91 | 92 | func testByteBufferGetPut(t *testing.T) { 93 | for i := 0; i < 10; i++ { 94 | expectedS := fmt.Sprintf("num %d", i) 95 | b := Get() 96 | b.B = append(b.B, "num "...) 97 | b.B = append(b.B, fmt.Sprintf("%d", i)...) 98 | if string(b.B) != expectedS { 99 | t.Fatalf("unexpected result: %q. Expecting %q", b.B, expectedS) 100 | } 101 | Put(b) 102 | } 103 | } 104 | 105 | func testByteBufferGetString(t *testing.T) { 106 | for i := 0; i < 10; i++ { 107 | expectedS := fmt.Sprintf("num %d", i) 108 | b := Get() 109 | b.SetString(expectedS) 110 | if b.String() != expectedS { 111 | t.Fatalf("unexpected result: %q. Expecting %q", b.B, expectedS) 112 | } 113 | Put(b) 114 | } 115 | } 116 | 117 | func TestByteBufferGetStringSerial(t *testing.T) { 118 | testByteBufferGetString(t) 119 | } 120 | 121 | func TestByteBufferGetStringConcurrent(t *testing.T) { 122 | concurrency := 10 123 | ch := make(chan struct{}, concurrency) 124 | for i := 0; i < concurrency; i++ { 125 | go func() { 126 | testByteBufferGetString(t) 127 | ch <- struct{}{} 128 | }() 129 | } 130 | 131 | for i := 0; i < concurrency; i++ { 132 | select { 133 | case <-ch: 134 | case <-time.After(time.Second): 135 | t.Fatalf("timeout!") 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /pkg/internal/bytebufferpool/bytebuffer_timing_test.go: -------------------------------------------------------------------------------- 1 | package bytebufferpool 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkByteBufferWrite(b *testing.B) { 9 | s := []byte("foobarbaz") 10 | b.RunParallel(func(pb *testing.PB) { 11 | var buf ByteBuffer 12 | for pb.Next() { 13 | for i := 0; i < 100; i++ { 14 | buf.Write(s) 15 | } 16 | buf.Reset() 17 | } 18 | }) 19 | } 20 | 21 | func BenchmarkBytesBufferWrite(b *testing.B) { 22 | s := []byte("foobarbaz") 23 | b.RunParallel(func(pb *testing.PB) { 24 | var buf bytes.Buffer 25 | for pb.Next() { 26 | for i := 0; i < 100; i++ { 27 | buf.Write(s) 28 | } 29 | buf.Reset() 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/internal/bytebufferpool/doc.go: -------------------------------------------------------------------------------- 1 | // Package bytebufferpool implements a pool of byte buffers 2 | // with anti-fragmentation protection. 3 | // 4 | // The pool may waste limited amount of memory due to fragmentation. 5 | // This amount equals to the maximum total size of the byte buffers 6 | // in concurrent use. 7 | package bytebufferpool 8 | -------------------------------------------------------------------------------- /pkg/internal/bytebufferpool/pool.go: -------------------------------------------------------------------------------- 1 | package bytebufferpool 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | const ( 10 | minBitSize = 6 // 2**6=64 is a CPU cache line size 11 | steps = 20 12 | 13 | minSize = 1 << minBitSize 14 | maxSize = 1 << (minBitSize + steps - 1) 15 | 16 | calibrateCallsThreshold = 42000 17 | maxPercentile = 0.95 18 | ) 19 | 20 | // Pool represents byte buffer pool. 21 | // 22 | // Distinct pools may be used for distinct types of byte buffers. 23 | // Properly determined byte buffer types with their own pools may help reducing 24 | // memory waste. 25 | type Pool struct { 26 | calls [steps]uint64 27 | calibrating uint64 28 | 29 | defaultSize uint64 30 | maxSize uint64 31 | 32 | pool sync.Pool 33 | } 34 | 35 | var defaultPool Pool 36 | 37 | // Get returns an empty byte buffer from the pool. 38 | // 39 | // Got byte buffer may be returned to the pool via Put call. 40 | // This reduces the number of memory allocations required for byte buffer 41 | // management. 42 | func Get() *ByteBuffer { return defaultPool.Get() } 43 | 44 | // Get returns new byte buffer with zero length. 45 | // 46 | // The byte buffer may be returned to the pool via Put after the use 47 | // in order to minimize GC overhead. 48 | func (p *Pool) Get() *ByteBuffer { 49 | v := p.pool.Get() 50 | if v != nil { 51 | return v.(*ByteBuffer) 52 | } 53 | return &ByteBuffer{ 54 | B: make([]byte, 0, atomic.LoadUint64(&p.defaultSize)), 55 | } 56 | } 57 | 58 | // Put returns byte buffer to the pool. 59 | // 60 | // ByteBuffer.B mustn't be touched after returning it to the pool. 61 | // Otherwise data races will occur. 62 | func Put(b *ByteBuffer) { defaultPool.Put(b) } 63 | 64 | // Put releases byte buffer obtained via Get to the pool. 65 | // 66 | // The buffer mustn't be accessed after returning to the pool. 67 | func (p *Pool) Put(b *ByteBuffer) { 68 | idx := index(len(b.B)) 69 | 70 | if atomic.AddUint64(&p.calls[idx], 1) > calibrateCallsThreshold { 71 | p.calibrate() 72 | } 73 | 74 | maxSize := int(atomic.LoadUint64(&p.maxSize)) 75 | if maxSize == 0 || cap(b.B) <= maxSize { 76 | b.Reset() 77 | p.pool.Put(b) 78 | } 79 | } 80 | 81 | func (p *Pool) calibrate() { 82 | if !atomic.CompareAndSwapUint64(&p.calibrating, 0, 1) { 83 | return 84 | } 85 | 86 | a := make(callSizes, 0, steps) 87 | var callsSum uint64 88 | for i := uint64(0); i < steps; i++ { 89 | calls := atomic.SwapUint64(&p.calls[i], 0) 90 | callsSum += calls 91 | a = append(a, callSize{ 92 | calls: calls, 93 | size: minSize << i, 94 | }) 95 | } 96 | sort.Sort(a) 97 | 98 | defaultSize := a[0].size 99 | maxSize := defaultSize 100 | 101 | maxSum := uint64(float64(callsSum) * maxPercentile) 102 | callsSum = 0 103 | for i := 0; i < steps; i++ { 104 | if callsSum > maxSum { 105 | break 106 | } 107 | callsSum += a[i].calls 108 | size := a[i].size 109 | if size > maxSize { 110 | maxSize = size 111 | } 112 | } 113 | 114 | atomic.StoreUint64(&p.defaultSize, defaultSize) 115 | atomic.StoreUint64(&p.maxSize, maxSize) 116 | 117 | atomic.StoreUint64(&p.calibrating, 0) 118 | } 119 | 120 | type callSize struct { 121 | calls uint64 122 | size uint64 123 | } 124 | 125 | type callSizes []callSize 126 | 127 | func (ci callSizes) Len() int { 128 | return len(ci) 129 | } 130 | 131 | func (ci callSizes) Less(i, j int) bool { 132 | return ci[i].calls > ci[j].calls 133 | } 134 | 135 | func (ci callSizes) Swap(i, j int) { 136 | ci[i], ci[j] = ci[j], ci[i] 137 | } 138 | 139 | func index(n int) int { 140 | n-- 141 | n >>= minBitSize 142 | idx := 0 143 | for n > 0 { 144 | n >>= 1 145 | idx++ 146 | } 147 | if idx >= steps { 148 | idx = steps - 1 149 | } 150 | return idx 151 | } 152 | -------------------------------------------------------------------------------- /pkg/internal/bytebufferpool/pool_test.go: -------------------------------------------------------------------------------- 1 | package bytebufferpool 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestIndex(t *testing.T) { 10 | testIndex(t, 0, 0) 11 | testIndex(t, 1, 0) 12 | 13 | testIndex(t, minSize-1, 0) 14 | testIndex(t, minSize, 0) 15 | testIndex(t, minSize+1, 1) 16 | 17 | testIndex(t, 2*minSize-1, 1) 18 | testIndex(t, 2*minSize, 1) 19 | testIndex(t, 2*minSize+1, 2) 20 | 21 | testIndex(t, maxSize-1, steps-1) 22 | testIndex(t, maxSize, steps-1) 23 | testIndex(t, maxSize+1, steps-1) 24 | } 25 | 26 | func testIndex(t *testing.T, n, expectedIdx int) { 27 | idx := index(n) 28 | if idx != expectedIdx { 29 | t.Fatalf("unexpected idx for n=%d: %d. Expecting %d", n, idx, expectedIdx) 30 | } 31 | } 32 | 33 | func TestPoolCalibrate(t *testing.T) { 34 | for i := 0; i < steps*calibrateCallsThreshold; i++ { 35 | n := 1004 36 | if i%15 == 0 { 37 | n = rand.Intn(15234) 38 | } 39 | testGetPut(t, n) 40 | } 41 | } 42 | 43 | func TestPoolVariousSizesSerial(t *testing.T) { 44 | testPoolVariousSizes(t) 45 | } 46 | 47 | func TestPoolVariousSizesConcurrent(t *testing.T) { 48 | concurrency := 5 49 | ch := make(chan struct{}) 50 | for i := 0; i < concurrency; i++ { 51 | go func() { 52 | testPoolVariousSizes(t) 53 | ch <- struct{}{} 54 | }() 55 | } 56 | for i := 0; i < concurrency; i++ { 57 | select { 58 | case <-ch: 59 | case <-time.After(3 * time.Second): 60 | t.Fatalf("timeout") 61 | } 62 | } 63 | } 64 | 65 | func testPoolVariousSizes(t *testing.T) { 66 | for i := 0; i < steps+1; i++ { 67 | n := (1 << uint32(i)) 68 | 69 | testGetPut(t, n) 70 | testGetPut(t, n+1) 71 | testGetPut(t, n-1) 72 | 73 | for j := 0; j < 10; j++ { 74 | testGetPut(t, j+n) 75 | } 76 | } 77 | } 78 | 79 | func testGetPut(t *testing.T, n int) { 80 | bb := Get() 81 | if len(bb.B) > 0 { 82 | t.Fatalf("non-empty byte buffer returned from acquire") 83 | } 84 | bb.B = allocNBytes(bb.B, n) 85 | Put(bb) 86 | } 87 | 88 | func allocNBytes(dst []byte, n int) []byte { 89 | diff := n - cap(dst) 90 | if diff <= 0 { 91 | return dst[:n] 92 | } 93 | return append(dst, make([]byte, diff)...) 94 | } 95 | -------------------------------------------------------------------------------- /pkg/table/table.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/mattn/go-runewidth" 10 | ) 11 | 12 | type cellAlignment int 13 | 14 | const ( 15 | ALIGN_LEFT cellAlignment = iota 16 | ALIGN_RIGHT 17 | ) 18 | 19 | type rowType int 20 | 21 | const ( 22 | ROW_LINE rowType = iota 23 | ROW_CELLS 24 | ) 25 | 26 | type cellUnit struct { 27 | content string 28 | alignment cellAlignment 29 | } 30 | 31 | type tableRow struct { 32 | cellUnits []*cellUnit 33 | kind rowType 34 | } 35 | 36 | type tableLine struct{} 37 | 38 | type TextTable struct { 39 | header []*tableRow 40 | rows []*tableRow 41 | width int 42 | maxWidths []int 43 | } 44 | 45 | func (t *TextTable) updateColumnWidth(rows []*tableRow) { 46 | for _, row := range rows { 47 | for i, unit := range row.cellUnits { 48 | width := stringWidth(unit.content) 49 | if t.maxWidths[i] < width { 50 | t.maxWidths[i] = width 51 | } 52 | } 53 | } 54 | } 55 | 56 | /* 57 | 58 | SetHeader adds header row from strings given 59 | 60 | */ 61 | func (t *TextTable) SetHeader(headers ...string) error { 62 | if len(headers) == 0 { 63 | return errors.New("no headers") 64 | } 65 | 66 | columnSize := len(headers) 67 | 68 | t.width = columnSize 69 | t.maxWidths = make([]int, columnSize) 70 | 71 | rows := stringsToTableRow(headers) 72 | t.updateColumnWidth(rows) 73 | 74 | t.header = rows 75 | 76 | return nil 77 | } 78 | 79 | /* 80 | 81 | AddRow adds column from strings given 82 | 83 | */ 84 | func (t *TextTable) AddRow(strs ...string) error { 85 | if len(strs) == 0 { 86 | return errors.New("no rows") 87 | } 88 | 89 | if len(strs) > t.width { 90 | return errors.New("row width should be less than header width") 91 | } 92 | 93 | padded := make([]string, t.width) 94 | copy(padded, strs) 95 | rows := stringsToTableRow(padded) 96 | t.rows = append(t.rows, rows...) 97 | 98 | t.updateColumnWidth(rows) 99 | 100 | return nil 101 | } 102 | 103 | /* 104 | 105 | AddRowLine adds row border 106 | 107 | */ 108 | func (t *TextTable) AddRowLine() error { 109 | rowLine := &tableRow{kind: ROW_LINE} 110 | t.rows = append(t.rows, rowLine) 111 | 112 | return nil 113 | } 114 | 115 | func (t *TextTable) borderString() string { 116 | borderString := "+" 117 | margin := 2 118 | 119 | for _, width := range t.maxWidths { 120 | for i := 0; i < width+margin; i++ { 121 | borderString += "-" 122 | } 123 | borderString += "+" 124 | } 125 | 126 | return borderString 127 | } 128 | 129 | func stringsToTableRow(strs []string) []*tableRow { 130 | maxHeight := calcMaxHeight(strs) 131 | strLines := make([][]string, maxHeight) 132 | 133 | for i := 0; i < maxHeight; i++ { 134 | strLines[i] = make([]string, len(strs)) 135 | } 136 | 137 | alignments := make([]cellAlignment, len(strs)) 138 | for i := range strs { 139 | alignments[i] = ALIGN_LEFT // decideAlignment(str) 140 | } 141 | 142 | for i, str := range strs { 143 | divideds := strings.Split(str, "\n") 144 | for j, line := range divideds { 145 | strLines[j][i] = line 146 | } 147 | } 148 | 149 | rows := make([]*tableRow, maxHeight) 150 | for j := 0; j < maxHeight; j++ { 151 | row := new(tableRow) 152 | row.kind = ROW_CELLS 153 | for i := 0; i < len(strs); i++ { 154 | content := strLines[j][i] 155 | unit := &cellUnit{content: content} 156 | unit.alignment = alignments[i] 157 | row.cellUnits = append(row.cellUnits, unit) 158 | } 159 | 160 | rows[j] = row 161 | } 162 | 163 | return rows 164 | } 165 | 166 | var hexRegexp = regexp.MustCompile("^0x") 167 | 168 | func decideAlignment(str string) cellAlignment { 169 | // decimal/octal number 170 | _, err := strconv.ParseInt(str, 10, 64) 171 | if err == nil { 172 | return ALIGN_RIGHT 173 | } 174 | 175 | // hex number 176 | _, err = strconv.ParseInt(str, 16, 64) 177 | if err == nil { 178 | return ALIGN_RIGHT 179 | } 180 | 181 | if hexRegexp.MatchString(str) { 182 | tmp := str[2:] 183 | _, err := strconv.ParseInt(tmp, 16, 64) 184 | if err == nil { 185 | return ALIGN_RIGHT 186 | } 187 | } 188 | 189 | _, err = strconv.ParseFloat(str, 64) 190 | if err == nil { 191 | return ALIGN_RIGHT 192 | } 193 | 194 | return ALIGN_LEFT 195 | } 196 | 197 | func calcMaxHeight(strs []string) int { 198 | max := -1 199 | 200 | for _, str := range strs { 201 | lines := strings.Split(str, "\n") 202 | height := len(lines) 203 | if height > max { 204 | max = height 205 | } 206 | } 207 | 208 | return max 209 | } 210 | 211 | func stringWidth(str string) int { 212 | return runewidth.StringWidth(str) 213 | } 214 | 215 | /* 216 | 217 | Draw constructs text table from receiver and returns it as string 218 | 219 | */ 220 | func (t *TextTable) Draw() string { 221 | drawedRows := make([]string, len(t.header)+len(t.rows)+3) 222 | index := 0 223 | 224 | border := t.borderString() 225 | 226 | // top line 227 | drawedRows[index] = border 228 | index++ 229 | 230 | for _, row := range t.header { 231 | drawedRows[index] = t.generateRowString(row) 232 | index++ 233 | } 234 | 235 | drawedRows[index] = border 236 | index++ 237 | 238 | for _, row := range t.rows { 239 | var rowStr string 240 | if row.kind == ROW_CELLS { 241 | rowStr = t.generateRowString(row) 242 | } else { 243 | rowStr = border 244 | } 245 | drawedRows[index] = rowStr 246 | index++ 247 | } 248 | 249 | // bottom line 250 | if len(t.rows) != 0 { 251 | drawedRows[index] = border 252 | index++ 253 | } 254 | 255 | return strings.Join(drawedRows[:index], "\n") 256 | } 257 | 258 | func formatCellUnit(unit *cellUnit, maxWidth int) string { 259 | str := unit.content 260 | width := stringWidth(unit.content) 261 | 262 | padding := strings.Repeat(" ", maxWidth-width) 263 | 264 | var ret string 265 | if unit.alignment == ALIGN_RIGHT { 266 | ret = padding + str 267 | } else { 268 | ret = str + padding 269 | } 270 | 271 | return " " + ret + " " 272 | } 273 | 274 | func (t *TextTable) generateRowString(row *tableRow) string { 275 | separator := "|" 276 | 277 | str := separator 278 | for i, unit := range row.cellUnits { 279 | str += formatCellUnit(unit, t.maxWidths[i]) 280 | str += separator 281 | } 282 | 283 | return str 284 | } 285 | -------------------------------------------------------------------------------- /pkg/table/table_test.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // 8 | // Public Method 9 | // 10 | 11 | func TestDraw(t *testing.T) { 12 | tbl := &TextTable{} 13 | 14 | expected := `+------+----------+ 15 | | 名前 | ふりがな | 16 | +------+----------+ 17 | | foo | ふう | 18 | | hoge | ほげ | 19 | +------+----------+` 20 | 21 | tbl.SetHeader("名前", "ふりがな") 22 | 23 | tbl.AddRow("foo", "ふう") 24 | tbl.AddRow("hoge", "ほげ") 25 | 26 | got := tbl.Draw() 27 | if got != expected { 28 | t.Errorf("[got]\n%s\n\n[expected]\n%s\n", got, expected) 29 | } 30 | } 31 | 32 | func TestSetHeader(t *testing.T) { 33 | tbl := &TextTable{} 34 | 35 | err := tbl.SetHeader() 36 | if err == nil { 37 | t.Errorf("SetHeader should take one argument at least") 38 | } 39 | } 40 | 41 | func TestAddRow(t *testing.T) { 42 | tbl := &TextTable{} 43 | tbl.SetHeader("name", "age") 44 | 45 | err := tbl.AddRow("bob", "30", "182") 46 | if err == nil { 47 | t.Errorf("row length should be smaller than equal header length") 48 | } 49 | 50 | err = tbl.AddRow() 51 | if err == nil { 52 | t.Errorf("AddRow should take one argument at least") 53 | } 54 | } 55 | 56 | // 57 | // Private Function/Methods 58 | // 59 | 60 | func Test_calcMaxHeight(t *testing.T) { 61 | input := []string{ 62 | "hello", "apple\nmelon\norange", "1\n2", 63 | } 64 | 65 | got := calcMaxHeight(input) 66 | if got != 3 { 67 | t.Errorf("calcMaxHeight(%s) != 3(got=%d)", input, got) 68 | } 69 | } 70 | 71 | func Test_decideAlignment(t *testing.T) { 72 | got := decideAlignment("102948") 73 | if got != ALIGN_RIGHT { 74 | t.Errorf("decimal string of integer alighment is 'right'") 75 | } 76 | 77 | got = decideAlignment("01234") 78 | if got != ALIGN_RIGHT { 79 | t.Errorf("octal string of integer alighment is 'right'") 80 | } 81 | 82 | got = decideAlignment("ff") 83 | if got != ALIGN_RIGHT { 84 | t.Errorf("hex string without '0x' of integer alighment is 'right'") 85 | } 86 | 87 | got = decideAlignment("0xaabbccdd") 88 | if got != ALIGN_RIGHT { 89 | t.Errorf("hex string of integer alighment is 'right'") 90 | } 91 | 92 | got = decideAlignment("1.245") 93 | if got != ALIGN_RIGHT { 94 | t.Errorf("string of float alighment is 'right'") 95 | } 96 | 97 | got = decideAlignment("foo") 98 | if got != ALIGN_LEFT { 99 | t.Errorf("string alighment is 'left'") 100 | } 101 | } 102 | 103 | func Test_stringsToTableRow(t *testing.T) { 104 | input := []string{ 105 | "apple", "orange\nmelon\ngrape\nnuts", "peach\nbanana", 106 | } 107 | 108 | tableRows := stringsToTableRow(input) 109 | if len(tableRows) != 4 { 110 | t.Errorf("returned table height=%d(Expected 4)", len(tableRows)) 111 | } 112 | 113 | for i, row := range tableRows { 114 | if len(row.cellUnits) != len(input) { 115 | t.Errorf("width of tableRows[%d]=%d(Expected %d)", 116 | i, len(row.cellUnits), len(input)) 117 | } 118 | } 119 | } 120 | 121 | func Test_borderString(t *testing.T) { 122 | tbl := new(TextTable) 123 | tbl.maxWidths = []int{4, 5, 3, 2} 124 | 125 | expected := "+------+-------+-----+----+" 126 | 127 | border := tbl.borderString() 128 | if border != expected { 129 | t.Errorf("got %s(Expected %s)", border, expected) 130 | } 131 | 132 | tbl.maxWidths = []int{0} 133 | expected = "+--+" 134 | border = tbl.borderString() 135 | if border != expected { 136 | t.Errorf("got %s(Expected %s)", border, expected) 137 | } 138 | } 139 | 140 | func Test_formatCellUnit(t *testing.T) { 141 | cell := cellUnit{content: "apple", alignment: ALIGN_RIGHT} 142 | 143 | expected := " apple " 144 | got := formatCellUnit(&cell, 5) 145 | if got != expected { 146 | t.Errorf("got '%s'(Expected '%s')", got, expected) 147 | } 148 | 149 | expected = " apple " 150 | got = formatCellUnit(&cell, 10) 151 | if got != expected { 152 | t.Errorf("got '%s'(Expected '%s')", got, expected) 153 | } 154 | 155 | cellLeft := cellUnit{content: "orange", alignment: ALIGN_LEFT} 156 | expected = " orange " 157 | got = formatCellUnit(&cellLeft, 6) 158 | if got != expected { 159 | t.Errorf("got '%s'(Expected '%s')", got, expected) 160 | } 161 | 162 | expected = " orange " 163 | got = formatCellUnit(&cellLeft, 10) 164 | if got != expected { 165 | t.Errorf("got '%s'(Expected '%s')", got, expected) 166 | } 167 | } 168 | 169 | func Test_generateRowString(t *testing.T) { 170 | tbl := TextTable{} 171 | tbl.maxWidths = []int{8, 5} 172 | cells := []*cellUnit{ 173 | {content: "apple", alignment: ALIGN_RIGHT}, 174 | {content: "melon", alignment: ALIGN_RIGHT}, 175 | } 176 | 177 | row := tableRow{cellUnits: cells, kind: ROW_CELLS} 178 | got := tbl.generateRowString(&row) 179 | 180 | expected := "| apple | melon |" 181 | if got != expected { 182 | t.Errorf("got '%s'(Expected '%s')", got, expected) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /pkg/template/example_test.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/url" 8 | ) 9 | 10 | func ExampleExecuteString() { 11 | // Substitution map. 12 | // Since "baz" tag is missing in the map, it will be substituted 13 | // by an empty string. 14 | m := map[string]interface{}{ 15 | "host": "google.com", // string - convenient 16 | "bar": []byte("foobar"), // byte slice - the fastest 17 | 18 | // TagFunc - flexible value. TagFunc is called only if the given 19 | // tag exists in the template. 20 | "query": TagFunc(func(w io.Writer, tag string) (int, error) { 21 | return w.Write([]byte(url.QueryEscape(tag + "=world"))) 22 | }), 23 | } 24 | 25 | template := "http://{{host}}/?foo={{bar}}{{bar}}&q={{query}}&baz={{baz}}" 26 | 27 | s, err := ExecuteString(template, "{{", "}}", m) 28 | if err != nil { 29 | log.Fatalf("unexpected error when executing template: %s", err) 30 | } 31 | 32 | fmt.Printf("%s", s) 33 | 34 | // Output: 35 | // http://google.com/?foo=foobarfoobar&q=query%3Dworld&baz= 36 | } 37 | 38 | func ExampleTagFunc() { 39 | bazSlice := [][]byte{[]byte("123"), []byte("456"), []byte("789")} 40 | m := map[string]interface{}{ 41 | // Always wrap the function into TagFunc. 42 | // 43 | // "baz" tag function writes bazSlice contents into w. 44 | "baz": TagFunc(func(w io.Writer, tag string) (int, error) { 45 | var nn int 46 | for _, x := range bazSlice { 47 | n, err := w.Write(x) 48 | if err != nil { 49 | return nn, err 50 | } 51 | nn += n 52 | } 53 | return nn, nil 54 | }), 55 | } 56 | 57 | template := "foo[baz]bar" 58 | 59 | s, err := ExecuteString(template, "[", "]", m) 60 | if err != nil { 61 | log.Fatalf("unexpected error when executing template: %s", err) 62 | } 63 | fmt.Printf("%s", s) 64 | 65 | // Output: 66 | // foo123456789bar 67 | } 68 | 69 | func ExampleExecuteFuncString() { 70 | template := "Hello, [user]! You won [prize]!!! [foobar]" 71 | 72 | s, err := ExecuteFuncString(template, "[", "]", func(w io.Writer, tag string) (int, error) { 73 | switch tag { 74 | case "user": 75 | return w.Write([]byte("John")) 76 | case "prize": 77 | return w.Write([]byte("$100500")) 78 | default: 79 | return w.Write([]byte(fmt.Sprintf("[unknown tag %q]", tag))) 80 | } 81 | }) 82 | if err != nil { 83 | log.Fatalf("unexpected error when executing template: %s", err) 84 | } 85 | 86 | fmt.Printf("%s", s) 87 | 88 | // Output: 89 | // Hello, John! You won $100500!!! [unknown tag "foobar"] 90 | } 91 | -------------------------------------------------------------------------------- /pkg/template/template.go: -------------------------------------------------------------------------------- 1 | // Package fasttemplate implements simple and fast template library. 2 | // 3 | // Fasttemplate is faster than text/template, strings.Replace 4 | // and strings.Replacer. 5 | // 6 | // Fasttemplate ideally fits for fast and simple placeholders' substitutions. 7 | package template 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "io" 13 | "strings" 14 | 15 | "github.com/lucasepe/tbd/pkg/internal/bytebufferpool" 16 | ) 17 | 18 | // ExecuteFunc calls f on each template tag (placeholder) occurrence. 19 | // 20 | // Returns the number of bytes written to w. 21 | // 22 | // This function is optimized for constantly changing templates. 23 | // Use Template.ExecuteFunc for frozen templates. 24 | func ExecuteFunc(template, startTag, endTag string, w io.Writer, f TagFunc) (int64, error) { 25 | s := unsafeString2Bytes(template) 26 | a := unsafeString2Bytes(startTag) 27 | b := unsafeString2Bytes(endTag) 28 | 29 | var nn int64 30 | var ni int 31 | var err error 32 | for { 33 | n := bytes.Index(s, a) 34 | if n < 0 { 35 | break 36 | } 37 | ni, err = w.Write(s[:n]) 38 | nn += int64(ni) 39 | if err != nil { 40 | return nn, err 41 | } 42 | 43 | s = s[n+len(a):] 44 | n = bytes.Index(s, b) 45 | if n < 0 { 46 | // cannot find end tag - just write it to the output. 47 | ni, _ = w.Write(a) 48 | nn += int64(ni) 49 | break 50 | } 51 | 52 | tag := strings.TrimSpace(unsafeBytes2String(s[:n])) 53 | ni, err = f(w, tag) 54 | nn += int64(ni) 55 | if err != nil { 56 | return nn, err 57 | } 58 | s = s[n+len(b):] 59 | } 60 | ni, err = w.Write(s) 61 | nn += int64(ni) 62 | 63 | return nn, err 64 | } 65 | 66 | // Marks returns the list of all placeholders found in the specified template. 67 | func Marks(template, startTag, endTag string) ([]string, error) { 68 | list := []string{} 69 | _, err := ExecuteFunc(template, startTag, endTag, io.Discard, 70 | func(w io.Writer, tag string) (int, error) { 71 | return fetchTagFunc(tag, &list) 72 | }) 73 | return list, err 74 | } 75 | 76 | // Execute substitutes template tags (placeholders) with the corresponding 77 | // values from the map m and writes the result to the given writer w. 78 | // 79 | // Substitution map m may contain values with the following types: 80 | // * []byte - the fastest value type 81 | // * string - convenient value type 82 | // * TagFunc - flexible value type 83 | // 84 | // Returns the number of bytes written to w. 85 | // 86 | // This function is optimized for constantly changing templates. 87 | // Use Template.Execute for frozen templates. 88 | func Execute(template, startTag, endTag string, w io.Writer, m map[string]interface{}) (int64, error) { 89 | return ExecuteFunc(template, startTag, endTag, w, 90 | func(w io.Writer, tag string) (int, error) { 91 | return stdTagFunc(w, tag, m) 92 | }) 93 | } 94 | 95 | // ExecuteStd works the same way as Execute, but keeps the unknown placeholders. 96 | // This can be used as a drop-in replacement for strings.Replacer 97 | // 98 | // Substitution map m may contain values with the following types: 99 | // * []byte - the fastest value type 100 | // * string - convenient value type 101 | // * TagFunc - flexible value type 102 | // 103 | // Returns the number of bytes written to w. 104 | // 105 | // This function is optimized for constantly changing templates. 106 | // Use Template.ExecuteStd for frozen templates. 107 | func ExecuteStd(template, startTag, endTag string, w io.Writer, m map[string]interface{}) (int64, error) { 108 | return ExecuteFunc(template, startTag, endTag, w, 109 | func(w io.Writer, tag string) (int, error) { 110 | return keepUnknownTagFunc(w, startTag, endTag, tag, m) 111 | }) 112 | } 113 | 114 | // ExecuteFuncString calls f on each template tag (placeholder) occurrence 115 | // and substitutes it with the data written to TagFunc's w. 116 | // 117 | // Returns the resulting string that will be empty on error. 118 | func ExecuteFuncString(template, startTag, endTag string, f TagFunc) (string, error) { 119 | tagsCount := bytes.Count(unsafeString2Bytes(template), unsafeString2Bytes(startTag)) 120 | if tagsCount == 0 { 121 | return template, nil 122 | } 123 | 124 | var byteBufferPool bytebufferpool.Pool 125 | 126 | bb := byteBufferPool.Get() 127 | if _, err := ExecuteFunc(template, startTag, endTag, bb, f); err != nil { 128 | bb.Reset() 129 | byteBufferPool.Put(bb) 130 | return "", err 131 | } 132 | s := string(bb.B) 133 | bb.Reset() 134 | byteBufferPool.Put(bb) 135 | return s, nil 136 | } 137 | 138 | // ExecuteString substitutes template tags (placeholders) with the corresponding 139 | // values from the map m and returns the result. 140 | // 141 | // Substitution map m may contain values with the following types: 142 | // * []byte - the fastest value type 143 | // * string - convenient value type 144 | // * TagFunc - flexible value type 145 | // 146 | // This function is optimized for constantly changing templates. 147 | // Use Template.ExecuteString for frozen templates. 148 | func ExecuteString(template, startTag, endTag string, m map[string]interface{}) (string, error) { 149 | return ExecuteFuncString(template, startTag, endTag, 150 | func(w io.Writer, tag string) (int, error) { 151 | return stdTagFunc(w, tag, m) 152 | }) 153 | } 154 | 155 | // ExecuteStringStd works the same way as ExecuteString, but keeps the unknown placeholders. 156 | // This can be used as a drop-in replacement for strings.Replacer 157 | // 158 | // Substitution map m may contain values with the following types: 159 | // * []byte - the fastest value type 160 | // * string - convenient value type 161 | // * TagFunc - flexible value type 162 | // 163 | // This function is optimized for constantly changing templates. 164 | // Use Template.ExecuteStringStd for frozen templates. 165 | func ExecuteStringStd(template, startTag, endTag string, m map[string]interface{}) (string, error) { 166 | return ExecuteFuncString(template, startTag, endTag, 167 | func(w io.Writer, tag string) (int, error) { 168 | return keepUnknownTagFunc(w, startTag, endTag, tag, m) 169 | }) 170 | } 171 | 172 | // TagFunc can be used as a substitution value in the map passed to Execute*. 173 | // Execute* functions pass tag (placeholder) name in 'tag' argument. 174 | // 175 | // TagFunc must be safe to call from concurrently running goroutines. 176 | // 177 | // TagFunc must write contents to w and return the number of bytes written. 178 | type TagFunc func(w io.Writer, tag string) (int, error) 179 | 180 | func stdTagFunc(w io.Writer, tag string, m map[string]interface{}) (int, error) { 181 | v := m[tag] 182 | if v == nil { 183 | return 0, nil 184 | } 185 | switch value := v.(type) { 186 | case []byte: 187 | return w.Write(value) 188 | case string: 189 | return w.Write([]byte(value)) 190 | case TagFunc: 191 | return value(w, tag) 192 | default: 193 | return -1, fmt.Errorf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v) 194 | } 195 | } 196 | 197 | func keepUnknownTagFunc(w io.Writer, startTag, endTag, tag string, m map[string]interface{}) (int, error) { 198 | v, ok := m[tag] 199 | if !ok { 200 | if _, err := w.Write(unsafeString2Bytes(startTag)); err != nil { 201 | return 0, err 202 | } 203 | if _, err := w.Write(unsafeString2Bytes(tag)); err != nil { 204 | return 0, err 205 | } 206 | if _, err := w.Write(unsafeString2Bytes(endTag)); err != nil { 207 | return 0, err 208 | } 209 | return len(startTag) + len(tag) + len(endTag), nil 210 | } 211 | if v == nil { 212 | return 0, nil 213 | } 214 | switch value := v.(type) { 215 | case []byte: 216 | return w.Write(value) 217 | case string: 218 | return w.Write([]byte(value)) 219 | case TagFunc: 220 | return value(w, tag) 221 | default: 222 | return -1, fmt.Errorf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v) 223 | } 224 | } 225 | 226 | // fetchTagFunc accumulates all tags in the specified array. 227 | func fetchTagFunc(tag string, arr *[]string) (int, error) { 228 | *arr = append(*arr, tag) 229 | return 0, nil 230 | } 231 | -------------------------------------------------------------------------------- /pkg/template/template_test.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | ) 11 | 12 | func TestMarks(t *testing.T) { 13 | got, err := Marks("{{ one }} - {{two}} and {{ three }}", "{{", "}}") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | want := []string{"one", "two", "three"} 19 | if !cmp.Equal(got, want) { 20 | t.Fatalf("got [%v] wants [%v]", got, want) 21 | } 22 | } 23 | 24 | func TestExecuteFunc(t *testing.T) { 25 | testExecuteFunc(t, "", "") 26 | testExecuteFunc(t, "a", "a") 27 | testExecuteFunc(t, "abc", "abc") 28 | testExecuteFunc(t, "{foo}", "xxxx") 29 | testExecuteFunc(t, "a{foo}", "axxxx") 30 | testExecuteFunc(t, "{foo}a", "xxxxa") 31 | testExecuteFunc(t, "a{foo}bc", "axxxxbc") 32 | testExecuteFunc(t, "{foo}{foo}", "xxxxxxxx") 33 | testExecuteFunc(t, "{foo}bar{foo}", "xxxxbarxxxx") 34 | 35 | // unclosed tag 36 | testExecuteFunc(t, "{unclosed", "{unclosed") 37 | testExecuteFunc(t, "{{unclosed", "{{unclosed") 38 | testExecuteFunc(t, "{un{closed", "{un{closed") 39 | 40 | // test unknown tag 41 | testExecuteFunc(t, "{unknown}", "zz") 42 | testExecuteFunc(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxqzzzzbarxxxx") 43 | } 44 | 45 | func testExecuteFunc(t *testing.T, template, expectedOutput string) { 46 | var bb bytes.Buffer 47 | ExecuteFunc(template, "{", "}", &bb, func(w io.Writer, tag string) (int, error) { 48 | if tag == "foo" { 49 | return w.Write([]byte("xxxx")) 50 | } 51 | return w.Write([]byte("zz")) 52 | }) 53 | 54 | output := string(bb.Bytes()) 55 | if output != expectedOutput { 56 | t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput) 57 | } 58 | } 59 | 60 | func TestExecute(t *testing.T) { 61 | testExecute(t, "", "") 62 | testExecute(t, "a", "a") 63 | testExecute(t, "abc", "abc") 64 | testExecute(t, "{foo}", "xxxx") 65 | testExecute(t, "a{foo}", "axxxx") 66 | testExecute(t, "{foo}a", "xxxxa") 67 | testExecute(t, "a{foo}bc", "axxxxbc") 68 | testExecute(t, "{foo}{foo}", "xxxxxxxx") 69 | testExecute(t, "{foo}bar{foo}", "xxxxbarxxxx") 70 | 71 | // unclosed tag 72 | testExecute(t, "{unclosed", "{unclosed") 73 | testExecute(t, "{{unclosed", "{{unclosed") 74 | testExecute(t, "{un{closed", "{un{closed") 75 | 76 | // test unknown tag 77 | testExecute(t, "{unknown}", "") 78 | testExecute(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxqbarxxxx") 79 | } 80 | 81 | func testExecute(t *testing.T, template, expectedOutput string) { 82 | var bb bytes.Buffer 83 | Execute(template, "{", "}", &bb, map[string]interface{}{"foo": "xxxx"}) 84 | output := string(bb.Bytes()) 85 | if output != expectedOutput { 86 | t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput) 87 | } 88 | } 89 | 90 | func TestExecuteStd(t *testing.T) { 91 | testExecuteStd(t, "", "") 92 | testExecuteStd(t, "a", "a") 93 | testExecuteStd(t, "abc", "abc") 94 | testExecuteStd(t, "{foo}", "xxxx") 95 | testExecuteStd(t, "a{foo}", "axxxx") 96 | testExecuteStd(t, "{foo}a", "xxxxa") 97 | testExecuteStd(t, "a{foo}bc", "axxxxbc") 98 | testExecuteStd(t, "{foo}{foo}", "xxxxxxxx") 99 | testExecuteStd(t, "{foo}bar{foo}", "xxxxbarxxxx") 100 | 101 | // unclosed tag 102 | testExecuteStd(t, "{unclosed", "{unclosed") 103 | testExecuteStd(t, "{{unclosed", "{{unclosed") 104 | testExecuteStd(t, "{un{closed", "{un{closed") 105 | 106 | // test unknown tag 107 | testExecuteStd(t, "{unknown}", "{unknown}") 108 | testExecuteStd(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxq{unexpected}{missing}barxxxx") 109 | } 110 | 111 | func testExecuteStd(t *testing.T, template, expectedOutput string) { 112 | var bb bytes.Buffer 113 | ExecuteStd(template, "{", "}", &bb, map[string]interface{}{"foo": "xxxx"}) 114 | output := string(bb.Bytes()) 115 | if output != expectedOutput { 116 | t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput) 117 | } 118 | } 119 | 120 | func TestExecuteString(t *testing.T) { 121 | testExecuteString(t, "", "") 122 | testExecuteString(t, "a", "a") 123 | testExecuteString(t, "abc", "abc") 124 | testExecuteString(t, "{foo}", "xxxx") 125 | testExecuteString(t, "a{foo}", "axxxx") 126 | testExecuteString(t, "{foo}a", "xxxxa") 127 | testExecuteString(t, "a{foo}bc", "axxxxbc") 128 | testExecuteString(t, "{foo}{foo}", "xxxxxxxx") 129 | testExecuteString(t, "{foo}bar{foo}", "xxxxbarxxxx") 130 | 131 | // unclosed tag 132 | testExecuteString(t, "{unclosed", "{unclosed") 133 | testExecuteString(t, "{{unclosed", "{{unclosed") 134 | testExecuteString(t, "{un{closed", "{un{closed") 135 | 136 | // test unknown tag 137 | testExecuteString(t, "{unknown}", "") 138 | testExecuteString(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxqbarxxxx") 139 | } 140 | 141 | func testExecuteString(t *testing.T, template, expectedOutput string) { 142 | output, err := ExecuteString(template, "{", "}", map[string]interface{}{"foo": "xxxx"}) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | if output != expectedOutput { 148 | t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput) 149 | } 150 | } 151 | 152 | func TestExecuteStringStd(t *testing.T) { 153 | testExecuteStringStd(t, "", "") 154 | testExecuteStringStd(t, "a", "a") 155 | testExecuteStringStd(t, "abc", "abc") 156 | testExecuteStringStd(t, "{foo}", "xxxx") 157 | testExecuteStringStd(t, "a{foo}", "axxxx") 158 | testExecuteStringStd(t, "{foo}a", "xxxxa") 159 | testExecuteStringStd(t, "a{foo}bc", "axxxxbc") 160 | testExecuteStringStd(t, "{foo}{foo}", "xxxxxxxx") 161 | testExecuteStringStd(t, "{foo}bar{foo}", "xxxxbarxxxx") 162 | 163 | // unclosed tag 164 | testExecuteStringStd(t, "{unclosed", "{unclosed") 165 | testExecuteStringStd(t, "{{unclosed", "{{unclosed") 166 | testExecuteStringStd(t, "{un{closed", "{un{closed") 167 | 168 | // test unknown tag 169 | testExecuteStringStd(t, "{unknown}", "{unknown}") 170 | testExecuteStringStd(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxq{unexpected}{missing}barxxxx") 171 | } 172 | 173 | func testExecuteStringStd(t *testing.T, template, expectedOutput string) { 174 | output, err := ExecuteStringStd(template, "{", "}", map[string]interface{}{"foo": "xxxx"}) 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | 179 | if output != expectedOutput { 180 | t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput) 181 | } 182 | } 183 | 184 | func expectPanic(t *testing.T, f func()) { 185 | defer func() { 186 | if r := recover(); r == nil { 187 | t.Fatalf("missing panic") 188 | } 189 | }() 190 | f() 191 | } 192 | 193 | func TestExecuteFuncStringWithErr(t *testing.T) { 194 | var expectErr = errors.New("test111") 195 | result, err := ExecuteFuncString(`{a} is {b}'s best friend`, "{", "}", func(w io.Writer, tag string) (int, error) { 196 | if tag == "a" { 197 | return w.Write([]byte("Alice")) 198 | } 199 | return 0, expectErr 200 | }) 201 | if err != expectErr { 202 | t.Fatalf("error must be the same as the error returned from f, expect: %s, actual: %s", expectErr, err) 203 | } 204 | if result != "" { 205 | t.Fatalf("result should be an empty string if error occurred") 206 | } 207 | result, err = ExecuteFuncString(`{a} is {b}'s best friend`, "{", "}", func(w io.Writer, tag string) (int, error) { 208 | if tag == "a" { 209 | return w.Write([]byte("Alice")) 210 | } 211 | return w.Write([]byte("Bob")) 212 | }) 213 | if err != nil { 214 | t.Fatalf("should success but found err: %s", err) 215 | } 216 | if result != "Alice is Bob's best friend" { 217 | t.Fatalf("expect: %s, but: %s", "Alice is Bob's best friend", result) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /pkg/template/template_timing_test.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "testing" 9 | "text/template" 10 | ) 11 | 12 | var ( 13 | source = "http://{{uid}}.foo.bar.com/?cb={{cb}}{{width}}&width={{width}}&height={{height}}&timeout={{timeout}}&uid={{uid}}&subid={{subid}}&ref={{ref}}&empty={{empty}}" 14 | result = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http://google.com/aaa/bbb/ccc&empty=" 15 | resultEscaped = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http%3A%2F%2Fgoogle.com%2Faaa%2Fbbb%2Fccc&empty=" 16 | resultStd = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http://google.com/aaa/bbb/ccc&empty={{empty}}" 17 | resultTextTemplate = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http://google.com/aaa/bbb/ccc&empty=" 18 | 19 | resultBytes = []byte(result) 20 | resultEscapedBytes = []byte(resultEscaped) 21 | resultStdBytes = []byte(resultStd) 22 | resultTextTemplateBytes = []byte(resultTextTemplate) 23 | 24 | m = map[string]interface{}{ 25 | "cb": []byte("1234"), 26 | "width": []byte("1232"), 27 | "height": []byte("123"), 28 | "timeout": []byte("123123"), 29 | "uid": []byte("aaasdf"), 30 | "subid": []byte("asdfds"), 31 | "ref": []byte("http://google.com/aaa/bbb/ccc"), 32 | } 33 | ) 34 | 35 | func map2slice(m map[string]interface{}) []string { 36 | var a []string 37 | for k, v := range m { 38 | a = append(a, "{{"+k+"}}", string(v.([]byte))) 39 | } 40 | return a 41 | } 42 | 43 | func BenchmarkFmtFprintf(b *testing.B) { 44 | b.RunParallel(func(pb *testing.PB) { 45 | var w bytes.Buffer 46 | for pb.Next() { 47 | fmt.Fprintf(&w, 48 | "http://%[5]s.foo.bar.com/?cb=%[1]s%[2]s&width=%[2]s&height=%[3]s&timeout=%[4]s&uid=%[5]s&subid=%[6]s&ref=%[7]s&empty=", 49 | m["cb"], m["width"], m["height"], m["timeout"], m["uid"], m["subid"], m["ref"]) 50 | x := w.Bytes() 51 | if !bytes.Equal(x, resultBytes) { 52 | b.Fatalf("Unexpected result\n%q\nExpected\n%q\n", x, result) 53 | } 54 | w.Reset() 55 | } 56 | }) 57 | } 58 | 59 | func BenchmarkStringsReplace(b *testing.B) { 60 | mSlice := map2slice(m) 61 | 62 | b.ResetTimer() 63 | b.RunParallel(func(pb *testing.PB) { 64 | for pb.Next() { 65 | x := source 66 | for i := 0; i < len(mSlice); i += 2 { 67 | x = strings.Replace(x, mSlice[i], mSlice[i+1], -1) 68 | } 69 | if x != resultStd { 70 | b.Fatalf("Unexpected result\n%q\nExpected\n%q\n", x, resultStd) 71 | } 72 | } 73 | }) 74 | } 75 | 76 | func BenchmarkStringsReplacer(b *testing.B) { 77 | mSlice := map2slice(m) 78 | 79 | b.ResetTimer() 80 | b.RunParallel(func(pb *testing.PB) { 81 | for pb.Next() { 82 | r := strings.NewReplacer(mSlice...) 83 | x := r.Replace(source) 84 | if x != resultStd { 85 | b.Fatalf("Unexpected result\n%q\nExpected\n%q\n", x, resultStd) 86 | } 87 | } 88 | }) 89 | } 90 | 91 | func BenchmarkTextTemplate(b *testing.B) { 92 | s := strings.Replace(source, "{{", "{{.", -1) 93 | t, err := template.New("test").Parse(s) 94 | if err != nil { 95 | b.Fatalf("Error when parsing template: %s", err) 96 | } 97 | 98 | mm := make(map[string]string) 99 | for k, v := range m { 100 | mm[k] = string(v.([]byte)) 101 | } 102 | 103 | b.ResetTimer() 104 | b.RunParallel(func(pb *testing.PB) { 105 | var w bytes.Buffer 106 | for pb.Next() { 107 | if err := t.Execute(&w, mm); err != nil { 108 | b.Fatalf("error when executing template: %s", err) 109 | } 110 | x := w.Bytes() 111 | if !bytes.Equal(x, resultTextTemplateBytes) { 112 | b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultTextTemplateBytes) 113 | } 114 | w.Reset() 115 | } 116 | }) 117 | } 118 | 119 | func BenchmarkExecuteFunc(b *testing.B) { 120 | b.RunParallel(func(pb *testing.PB) { 121 | var bb bytes.Buffer 122 | for pb.Next() { 123 | ExecuteFunc(source, "{{", "}}", &bb, testTagFunc) 124 | bb.Reset() 125 | } 126 | }) 127 | } 128 | 129 | func testTagFunc(w io.Writer, tag string) (int, error) { 130 | if t, ok := m[tag]; ok { 131 | return w.Write(t.([]byte)) 132 | } 133 | return 0, nil 134 | } 135 | -------------------------------------------------------------------------------- /pkg/template/unsafe.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "reflect" 5 | "unsafe" 6 | ) 7 | 8 | func unsafeBytes2String(b []byte) string { 9 | return *(*string)(unsafe.Pointer(&b)) 10 | } 11 | 12 | func unsafeString2Bytes(s string) (b []byte) { 13 | sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) 14 | bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) 15 | bh.Data = sh.Data 16 | bh.Cap = sh.Len 17 | bh.Len = sh.Len 18 | return b 19 | } 20 | -------------------------------------------------------------------------------- /pkg/vcs/support.go: -------------------------------------------------------------------------------- 1 | package vcs 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | RepoCommit = "REPO_COMMIT" 10 | RepoTag = "REPO_TAG" 11 | RepoTagClean = "REPO_TAG_CLEAN" 12 | RepoURL = "REPO_URL" 13 | RepoHost = "REPO_HOST" 14 | RepoName = "REPO_NAME" 15 | RepoRoot = "REPO_ROOT" 16 | ) 17 | 18 | func GitRepoMetadata(path string, meta map[string]string) error { 19 | repo, err := OpenGitRepo(path) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | commit, err := CurrentCommitFromGitRepo(repo) 25 | if err != nil { 26 | return err 27 | } 28 | meta[RepoCommit] = commit 29 | 30 | tag, err := LatestTagFromGitRepo(repo) 31 | if err != nil { 32 | return err 33 | } 34 | idx := strings.LastIndex(tag, "/") 35 | if idx != -1 { 36 | tag = tag[idx+1:] 37 | } 38 | meta[RepoTag] = tag 39 | 40 | if strings.HasPrefix(tag, "v") { 41 | meta[RepoTagClean] = tag[1:] 42 | } 43 | 44 | repoURL, err := GitRepoURL(repo) 45 | if err != nil { 46 | return err 47 | } 48 | meta[RepoURL] = repoURL 49 | 50 | if u, err := url.Parse(repoURL); err == nil { 51 | idx := strings.Index(u.Path[1:], "/") 52 | if idx != -1 { 53 | meta[RepoRoot] = u.Path[1 : idx+1] 54 | } 55 | 56 | meta[RepoHost] = u.Host 57 | } 58 | 59 | meta[RepoName] = repoURL[strings.LastIndex(repoURL, "/")+1:] 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/vcs/vcs.go: -------------------------------------------------------------------------------- 1 | package vcs 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/go-git/go-git/v5" 10 | "github.com/go-git/go-git/v5/plumbing" 11 | "github.com/go-git/go-git/v5/plumbing/object" 12 | "github.com/pkg/errors" 13 | 14 | gitUrls "github.com/whilp/git-urls" 15 | ) 16 | 17 | func InitGitRepo(path string, isBare bool) (*git.Repository, error) { 18 | repo, err := git.PlainInit(path, true) 19 | if err != nil { 20 | if err == git.ErrRepositoryAlreadyExists { 21 | return git.PlainOpen(path) 22 | } 23 | return nil, err 24 | } 25 | return repo, nil 26 | } 27 | 28 | func OpenGitRepo(path string) (*git.Repository, error) { 29 | path, err := DetectGitPath(path) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | repo, err := git.PlainOpen(path) 35 | if err != nil { 36 | return nil, errors.Wrap(err, fmt.Sprintf("error opening repository '%s'", path)) 37 | } 38 | 39 | return repo, nil 40 | } 41 | 42 | func CurrentBranchFromGitRepo(repository *git.Repository) (string, error) { 43 | branchRefs, err := repository.Branches() 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | headRef, err := repository.Head() 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | var currentBranchName string 54 | err = branchRefs.ForEach(func(branchRef *plumbing.Reference) error { 55 | if branchRef.Hash() == headRef.Hash() { 56 | currentBranchName = branchRef.Name().String() 57 | 58 | return nil 59 | } 60 | 61 | return nil 62 | }) 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | return currentBranchName, nil 68 | } 69 | 70 | func CurrentCommitFromGitRepo(repository *git.Repository) (string, error) { 71 | headRef, err := repository.Head() 72 | if err != nil { 73 | return "", err 74 | } 75 | headSha := headRef.Hash().String() 76 | 77 | return headSha, nil 78 | } 79 | 80 | func LatestTagFromGitRepo(repository *git.Repository) (string, error) { 81 | tagRefs, err := repository.Tags() 82 | if err != nil { 83 | return "", err 84 | } 85 | 86 | var latestTagCommit *object.Commit 87 | var latestTagName string 88 | err = tagRefs.ForEach(func(tagRef *plumbing.Reference) error { 89 | revision := plumbing.Revision(tagRef.Name().String()) 90 | tagCommitHash, err := repository.ResolveRevision(revision) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | commit, err := repository.CommitObject(*tagCommitHash) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | if latestTagCommit == nil { 101 | latestTagCommit = commit 102 | latestTagName = tagRef.Name().String() 103 | } 104 | 105 | if commit.Committer.When.After(latestTagCommit.Committer.When) { 106 | latestTagCommit = commit 107 | latestTagName = tagRef.Name().String() 108 | } 109 | 110 | return nil 111 | }) 112 | if err != nil { 113 | return "", err 114 | } 115 | 116 | return latestTagName, nil 117 | } 118 | 119 | func GitRepoURL(repo *git.Repository) (string, error) { 120 | cfg, err := repo.Config() 121 | if err != nil { 122 | return "", err 123 | } 124 | 125 | var res string 126 | 127 | for k, v := range cfg.Remotes { 128 | if k == "origin" && len(v.URLs) > 0 { 129 | u, err := gitUrls.Parse(v.URLs[0]) 130 | if err != nil { 131 | return "", err 132 | } 133 | 134 | res = u.String() 135 | 136 | if strings.HasPrefix(u.Scheme, "ssh") { 137 | var sb strings.Builder 138 | sb.WriteString("https://") 139 | sb.WriteString(u.Host) 140 | sb.WriteString("/") 141 | sb.WriteString(u.Path) 142 | 143 | res = strings.TrimSuffix(sb.String(), ".git") 144 | } 145 | 146 | break 147 | } 148 | } 149 | 150 | return res, nil 151 | } 152 | 153 | func DetectGitPath(path string) (string, error) { 154 | // normalize the path 155 | path, err := filepath.Abs(path) 156 | if err != nil { 157 | return "", err 158 | } 159 | 160 | for { 161 | fi, err := os.Stat(filepath.Join(path, ".git")) 162 | if err == nil { 163 | if !fi.IsDir() { 164 | return "", fmt.Errorf(".git exist but '%s' is not a directory", path) 165 | } 166 | return filepath.Join(path, ".git"), nil 167 | } 168 | if !os.IsNotExist(err) { 169 | // unknown error 170 | return "", err 171 | } 172 | 173 | // detect bare repo 174 | ok, err := IsGitDir(path) 175 | if err != nil { 176 | return "", err 177 | } 178 | if ok { 179 | return path, nil 180 | } 181 | 182 | if parent := filepath.Dir(path); parent == path { 183 | return "", fmt.Errorf(".git not found in '%s'", path) 184 | } else { 185 | path = parent 186 | } 187 | } 188 | } 189 | 190 | func IsGitDir(path string) (bool, error) { 191 | markers := []string{"HEAD", "objects", "refs"} 192 | 193 | for _, marker := range markers { 194 | _, err := os.Stat(filepath.Join(path, marker)) 195 | if err == nil { 196 | continue 197 | } 198 | if !os.IsNotExist(err) { 199 | // unknown error 200 | return false, err 201 | } else { 202 | return false, nil 203 | } 204 | } 205 | 206 | return true, nil 207 | } 208 | -------------------------------------------------------------------------------- /pkg/vcs/vcs_test.go: -------------------------------------------------------------------------------- 1 | package vcs 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestNewGitRepo(t *testing.T) { 15 | // Plain 16 | plainRoot, err := ioutil.TempDir("", "") 17 | require.NoError(t, err) 18 | defer os.RemoveAll(plainRoot) 19 | 20 | _, err = InitGitRepo(plainRoot, false) 21 | require.NoError(t, err) 22 | plainGitDir := filepath.Join(plainRoot, ".git") 23 | 24 | tests := []struct { 25 | inPath string 26 | outPath string 27 | err bool 28 | }{ 29 | // errors 30 | {"/", "", true}, 31 | // parent dir of a repo 32 | {filepath.Dir(plainRoot), "", true}, 33 | 34 | // Plain repo 35 | {plainRoot, plainGitDir, false}, 36 | {plainGitDir, plainGitDir, false}, 37 | {path.Join(plainGitDir, "objects"), plainGitDir, false}, 38 | } 39 | 40 | for i, tc := range tests { 41 | dir, err := DetectGitPath(tc.inPath) 42 | if tc.err { 43 | require.Error(t, err, i) 44 | } 45 | 46 | _, err = OpenGitRepo(tc.inPath) 47 | if tc.err { 48 | require.Error(t, err, i) 49 | } else { 50 | require.NoError(t, err, i) 51 | assert.Equal(t, filepath.ToSlash(tc.outPath), filepath.Join(filepath.ToSlash(dir), ".git"), i) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /testdata/Dockerfile.tpl: -------------------------------------------------------------------------------- 1 | # Build environment 2 | # ----------------- 3 | FROM golang:1.16-alpine as build 4 | LABEL stage=builder 5 | 6 | WORKDIR /src 7 | 8 | COPY . . 9 | 10 | RUN apk add --no-cache git ca-certificates && \ 11 | go env -w GO111MODULE=on && \ 12 | git config --global url."https://{{GITHUB_TOKEN}}:x-oauth-basic@{{REPO_HOST}}/{{REPO_ROOT}}/".insteadOf "https://{{REPO_HOST}}/{{REPO_ROOT}}/" && \ 13 | go env -w GOPRIVATE={{REPO_HOST}}/{{REPO_ROOT}} && \ 14 | go mod tidy && \ 15 | CGO_ENABLED=0 go build -ldflags '-w -s' -o /bin/app 16 | 17 | # Deployment environment 18 | # ---------------------- 19 | FROM scratch 20 | 21 | COPY --from=build /bin/app /bin/app 22 | 23 | # Metadata 24 | LABEL org.label-schema.build-date={{TIMESTAMP}} \ 25 | org.label-schema.name={{REPO_NAME}} \ 26 | org.label-schema.description="{{REPO_DESCRIPTION}}" \ 27 | org.label-schema.vcs-url={{REPO_URL}} \ 28 | org.label-schema.vcs-ref={{REPO_COMMIT}} \ 29 | org.label-schema.vendor={{REPO_ROOT}} \ 30 | org.label-schema.version={{REPO_TAG}} \ 31 | org.label-schema.docker.schema-version="1.0" 32 | 33 | ARG WEBHOOK_PORT 34 | EXPOSE ${WEBHOOK_PORT} 35 | 36 | CMD ["/bin/app"] -------------------------------------------------------------------------------- /testdata/Dockerfile.vars: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN=XXXXXXXXXXXXXXXX 2 | -------------------------------------------------------------------------------- /testdata/sample.tbd: -------------------------------------------------------------------------------- 1 | {{ greeting }} 2 | 3 | I will be out of the office from {{ start.date }} until {{ return.date }}. 4 | If you need immediate assistance while I’m away, please email {{ contact.email }}. 5 | 6 | Best, 7 | {{ name }} -------------------------------------------------------------------------------- /testdata/sample.vars: -------------------------------------------------------------------------------- 1 | greeting: Greetings 2 | start.date: August, 9 3 | return.date: August 23 4 | contact.email: pinco.pallo@gmail.com 5 | name: Pinco Pallo --------------------------------------------------------------------------------