├── .github └── workflows │ └── gommon.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bytes ├── README.md ├── bytes.go └── bytes_test.go ├── color ├── README.md ├── color.go └── color_test.go ├── email ├── email.go └── email_test.go ├── go.mod ├── go.sum ├── gommon.go ├── log ├── README.md ├── color.go ├── log.go ├── log_test.go └── white.go └── random ├── random.go └── random_test.go /.github/workflows/gommon.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - '**.go' 9 | - 'go.*' 10 | - '_fixture/**' 11 | - '.github/**' 12 | - 'codecov.yml' 13 | pull_request: 14 | branches: 15 | - master 16 | paths: 17 | - '**.go' 18 | - 'go.*' 19 | - '_fixture/**' 20 | - '.github/**' 21 | - 'codecov.yml' 22 | workflow_dispatch: 23 | 24 | env: 25 | # run static analysis only with the latest Go version 26 | LATEST_GO_VERSION: "1.21" 27 | 28 | jobs: 29 | test: 30 | strategy: 31 | matrix: 32 | os: [ubuntu-latest, macos-latest, windows-latest] 33 | # Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy 34 | # Echo tests with last four major releases 35 | go: ["1.18","1.19","1.20","1.21"] 36 | name: ${{ matrix.os }} @ Go ${{ matrix.go }} 37 | runs-on: ${{ matrix.os }} 38 | steps: 39 | - name: Set up Go ${{ matrix.go }} 40 | uses: actions/setup-go@v4 41 | with: 42 | go-version: ${{ matrix.go }} 43 | 44 | - name: Checkout Code 45 | uses: actions/checkout@v4 46 | with: 47 | ref: ${{ github.ref }} 48 | 49 | - name: Run Tests 50 | run: | 51 | go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./... 52 | 53 | - name: Upload coverage to Codecov 54 | if: success() && matrix.go == env.LATEST_GO_VERSION && matrix.os == 'ubuntu-latest' 55 | uses: codecov/codecov-action@v3 56 | with: 57 | fail_ci_if_error: false 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage.txt 3 | _test 4 | vendor 5 | .idea 6 | *.iml 7 | *.out 8 | .vscode 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 labstack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG := "github.com/labstack/gommon" 2 | PKG_LIST := $(shell go list ${PKG}/...) 3 | 4 | .DEFAULT_GOAL := check 5 | check: lint vet race ## Check project 6 | 7 | init: 8 | @go install golang.org/x/lint/golint@latest 9 | 10 | lint: ## Lint the files 11 | @golint -set_exit_status ${PKG_LIST} 12 | 13 | vet: ## Vet the files 14 | @go vet ${PKG_LIST} 15 | 16 | test: ## Run tests 17 | @go test -short ${PKG_LIST} 18 | 19 | race: ## Run tests with data race detector 20 | @go test -race ${PKG_LIST} 21 | 22 | benchmark: ## Run benchmarks 23 | @go test -run="-" -bench=".*" ${PKG_LIST} 24 | 25 | help: ## Display this help screen 26 | @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 27 | 28 | goversion ?= "1.20" 29 | test_version: ## Run tests inside Docker with given version (defaults to 1.20). Example: make test_version goversion=1.20 30 | @docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make init check" 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gommon [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/gommon) [![Coverage Status](http://img.shields.io/coveralls/labstack/gommon.svg?style=flat-square)](https://coveralls.io/r/labstack/gommon) 2 | 3 | Common packages for Go 4 | - [Bytes](https://github.com/labstack/gommon/tree/master/bytes) - Format/parse bytes. 5 | - [Color](https://github.com/labstack/gommon/tree/master/color) - Style terminal text. 6 | - [Log](https://github.com/labstack/gommon/tree/master/log) - Simple logging. 7 | -------------------------------------------------------------------------------- /bytes/README.md: -------------------------------------------------------------------------------- 1 | # Bytes 2 | 3 | - Format bytes integer to human readable bytes string. 4 | - Parse human readable bytes string to bytes integer. 5 | 6 | ## Installation 7 | 8 | ```go 9 | go get github.com/labstack/gommon/bytes 10 | ``` 11 | 12 | ## [Usage](https://github.com/labstack/gommon/blob/master/bytes/bytes_test.go) 13 | 14 | ### Format 15 | 16 | ```go 17 | println(bytes.Format(13231323)) 18 | ``` 19 | 20 | `12.62MB` 21 | 22 | ### Parse 23 | 24 | ```go 25 | b, _ = Parse("2M") 26 | println(b) 27 | ``` 28 | 29 | `2097152` 30 | -------------------------------------------------------------------------------- /bytes/bytes.go: -------------------------------------------------------------------------------- 1 | package bytes 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type ( 11 | // Bytes struct 12 | Bytes struct{} 13 | ) 14 | 15 | // binary units (IEC 60027) 16 | const ( 17 | _ = 1.0 << (10 * iota) // ignore first value by assigning to blank identifier 18 | KiB 19 | MiB 20 | GiB 21 | TiB 22 | PiB 23 | EiB 24 | ) 25 | 26 | // decimal units (SI international system of units) 27 | const ( 28 | KB = 1000 29 | MB = KB * 1000 30 | GB = MB * 1000 31 | TB = GB * 1000 32 | PB = TB * 1000 33 | EB = PB * 1000 34 | ) 35 | 36 | var ( 37 | patternBinary = regexp.MustCompile(`(?i)^(-?\d+(?:\.\d+)?)\s?([KMGTPE]iB?)$`) 38 | patternDecimal = regexp.MustCompile(`(?i)^(-?\d+(?:\.\d+)?)\s?([KMGTPE]B?|B?)$`) 39 | global = New() 40 | ) 41 | 42 | // New creates a Bytes instance. 43 | func New() *Bytes { 44 | return &Bytes{} 45 | } 46 | 47 | // Format formats bytes integer to human readable string according to IEC 60027. 48 | // For example, 31323 bytes will return 30.59KB. 49 | func (b *Bytes) Format(value int64) string { 50 | return b.FormatBinary(value) 51 | } 52 | 53 | // FormatBinary formats bytes integer to human readable string according to IEC 60027. 54 | // For example, 31323 bytes will return 30.59KB. 55 | func (*Bytes) FormatBinary(value int64) string { 56 | multiple := "" 57 | val := float64(value) 58 | 59 | switch { 60 | case value >= EiB: 61 | val /= EiB 62 | multiple = "EiB" 63 | case value >= PiB: 64 | val /= PiB 65 | multiple = "PiB" 66 | case value >= TiB: 67 | val /= TiB 68 | multiple = "TiB" 69 | case value >= GiB: 70 | val /= GiB 71 | multiple = "GiB" 72 | case value >= MiB: 73 | val /= MiB 74 | multiple = "MiB" 75 | case value >= KiB: 76 | val /= KiB 77 | multiple = "KiB" 78 | case value == 0: 79 | return "0" 80 | default: 81 | return strconv.FormatInt(value, 10) + "B" 82 | } 83 | 84 | return fmt.Sprintf("%.2f%s", val, multiple) 85 | } 86 | 87 | // FormatDecimal formats bytes integer to human readable string according to SI international system of units. 88 | // For example, 31323 bytes will return 31.32KB. 89 | func (*Bytes) FormatDecimal(value int64) string { 90 | multiple := "" 91 | val := float64(value) 92 | 93 | switch { 94 | case value >= EB: 95 | val /= EB 96 | multiple = "EB" 97 | case value >= PB: 98 | val /= PB 99 | multiple = "PB" 100 | case value >= TB: 101 | val /= TB 102 | multiple = "TB" 103 | case value >= GB: 104 | val /= GB 105 | multiple = "GB" 106 | case value >= MB: 107 | val /= MB 108 | multiple = "MB" 109 | case value >= KB: 110 | val /= KB 111 | multiple = "KB" 112 | case value == 0: 113 | return "0" 114 | default: 115 | return strconv.FormatInt(value, 10) + "B" 116 | } 117 | 118 | return fmt.Sprintf("%.2f%s", val, multiple) 119 | } 120 | 121 | // Parse parses human readable bytes string to bytes integer. 122 | // For example, 6GiB (6Gi is also valid) will return 6442450944, and 123 | // 6GB (6G is also valid) will return 6000000000. 124 | func (b *Bytes) Parse(value string) (int64, error) { 125 | 126 | i, err := b.ParseBinary(value) 127 | if err == nil { 128 | return i, err 129 | } 130 | 131 | return b.ParseDecimal(value) 132 | } 133 | 134 | // ParseBinary parses human readable bytes string to bytes integer. 135 | // For example, 6GiB (6Gi is also valid) will return 6442450944. 136 | func (*Bytes) ParseBinary(value string) (i int64, err error) { 137 | parts := patternBinary.FindStringSubmatch(value) 138 | if len(parts) < 3 { 139 | return 0, fmt.Errorf("error parsing value=%s", value) 140 | } 141 | bytesString := parts[1] 142 | multiple := strings.ToUpper(parts[2]) 143 | bytes, err := strconv.ParseFloat(bytesString, 64) 144 | if err != nil { 145 | return 146 | } 147 | 148 | switch multiple { 149 | case "KI", "KIB": 150 | return int64(bytes * KiB), nil 151 | case "MI", "MIB": 152 | return int64(bytes * MiB), nil 153 | case "GI", "GIB": 154 | return int64(bytes * GiB), nil 155 | case "TI", "TIB": 156 | return int64(bytes * TiB), nil 157 | case "PI", "PIB": 158 | return int64(bytes * PiB), nil 159 | case "EI", "EIB": 160 | return int64(bytes * EiB), nil 161 | default: 162 | return int64(bytes), nil 163 | } 164 | } 165 | 166 | // ParseDecimal parses human readable bytes string to bytes integer. 167 | // For example, 6GB (6G is also valid) will return 6000000000. 168 | func (*Bytes) ParseDecimal(value string) (i int64, err error) { 169 | parts := patternDecimal.FindStringSubmatch(value) 170 | if len(parts) < 3 { 171 | return 0, fmt.Errorf("error parsing value=%s", value) 172 | } 173 | bytesString := parts[1] 174 | multiple := strings.ToUpper(parts[2]) 175 | bytes, err := strconv.ParseFloat(bytesString, 64) 176 | if err != nil { 177 | return 178 | } 179 | 180 | switch multiple { 181 | case "K", "KB": 182 | return int64(bytes * KB), nil 183 | case "M", "MB": 184 | return int64(bytes * MB), nil 185 | case "G", "GB": 186 | return int64(bytes * GB), nil 187 | case "T", "TB": 188 | return int64(bytes * TB), nil 189 | case "P", "PB": 190 | return int64(bytes * PB), nil 191 | case "E", "EB": 192 | return int64(bytes * EB), nil 193 | default: 194 | return int64(bytes), nil 195 | } 196 | } 197 | 198 | // Format wraps global Bytes's Format function. 199 | func Format(value int64) string { 200 | return global.Format(value) 201 | } 202 | 203 | // FormatBinary wraps global Bytes's FormatBinary function. 204 | func FormatBinary(value int64) string { 205 | return global.FormatBinary(value) 206 | } 207 | 208 | // FormatDecimal wraps global Bytes's FormatDecimal function. 209 | func FormatDecimal(value int64) string { 210 | return global.FormatDecimal(value) 211 | } 212 | 213 | // Parse wraps global Bytes's Parse function. 214 | func Parse(value string) (int64, error) { 215 | return global.Parse(value) 216 | } 217 | -------------------------------------------------------------------------------- /bytes/bytes_test.go: -------------------------------------------------------------------------------- 1 | package bytes 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestBytesFormat(t *testing.T) { 11 | // B 12 | b := Format(0) 13 | assert.Equal(t, "0", b) 14 | // B 15 | b = Format(515) 16 | assert.Equal(t, "515B", b) 17 | 18 | // KiB 19 | b = Format(31323) 20 | assert.Equal(t, "30.59KiB", b) 21 | 22 | // MiB 23 | b = Format(13231323) 24 | assert.Equal(t, "12.62MiB", b) 25 | 26 | // GiB 27 | b = Format(7323232398) 28 | assert.Equal(t, "6.82GiB", b) 29 | 30 | // TiB 31 | b = Format(7323232398434) 32 | assert.Equal(t, "6.66TiB", b) 33 | 34 | // PiB 35 | b = Format(9923232398434432) 36 | assert.Equal(t, "8.81PiB", b) 37 | 38 | // EiB 39 | b = Format(math.MaxInt64) 40 | assert.Equal(t, "8.00EiB", b) 41 | 42 | // KB 43 | b = FormatDecimal(31323) 44 | assert.Equal(t, "31.32KB", b) 45 | 46 | // MB 47 | b = FormatDecimal(13231323) 48 | assert.Equal(t, "13.23MB", b) 49 | 50 | // GB 51 | b = FormatDecimal(7323232398) 52 | assert.Equal(t, "7.32GB", b) 53 | 54 | // TB 55 | b = FormatDecimal(7323232398434) 56 | assert.Equal(t, "7.32TB", b) 57 | 58 | // PB 59 | b = FormatDecimal(9923232398434432) 60 | assert.Equal(t, "9.92PB", b) 61 | 62 | // EB 63 | b = FormatDecimal(math.MaxInt64) 64 | assert.Equal(t, "9.22EB", b) 65 | } 66 | 67 | func TestBytesParseErrors(t *testing.T) { 68 | _, err := Parse("B999") 69 | if assert.Error(t, err) { 70 | assert.EqualError(t, err, "error parsing value=B999") 71 | } 72 | } 73 | 74 | func TestFloats(t *testing.T) { 75 | // From string: 76 | str := "12.25KiB" 77 | value, err := Parse(str) 78 | assert.NoError(t, err) 79 | assert.Equal(t, int64(12544), value) 80 | 81 | str2 := Format(value) 82 | assert.Equal(t, str, str2) 83 | 84 | // To string: 85 | val := int64(13233029) 86 | str = Format(val) 87 | assert.Equal(t, "12.62MiB", str) 88 | 89 | val2, err := Parse(str) 90 | assert.NoError(t, err) 91 | assert.Equal(t, val, val2) 92 | 93 | // From string decimal: 94 | strDec := "12.25KB" 95 | valueDec, err := Parse(strDec) 96 | assert.NoError(t, err) 97 | assert.Equal(t, int64(12250), valueDec) 98 | 99 | strDec2 := FormatDecimal(valueDec) 100 | assert.Equal(t, strDec, strDec2) 101 | 102 | // To string decimal: 103 | valDec := int64(13230000) 104 | strDec = FormatDecimal(valDec) 105 | assert.Equal(t, "13.23MB", strDec) 106 | 107 | valDec2, err := Parse(strDec) 108 | assert.NoError(t, err) 109 | assert.Equal(t, valDec, valDec2) 110 | } 111 | 112 | func TestBytesParse(t *testing.T) { 113 | // B 114 | b, err := Parse("999") 115 | if assert.NoError(t, err) { 116 | assert.Equal(t, int64(999), b) 117 | } 118 | b, err = Parse("-100") 119 | if assert.NoError(t, err) { 120 | assert.Equal(t, int64(-100), b) 121 | } 122 | b, err = Parse("100.1") 123 | if assert.NoError(t, err) { 124 | assert.Equal(t, int64(100), b) 125 | } 126 | b, err = Parse("515B") 127 | if assert.NoError(t, err) { 128 | assert.Equal(t, int64(515), b) 129 | } 130 | 131 | // B with space 132 | b, err = Parse("515 B") 133 | if assert.NoError(t, err) { 134 | assert.Equal(t, int64(515), b) 135 | } 136 | 137 | // KiB 138 | b, err = Parse("12.25KiB") 139 | if assert.NoError(t, err) { 140 | assert.Equal(t, int64(12544), b) 141 | } 142 | b, err = Parse("12KiB") 143 | if assert.NoError(t, err) { 144 | assert.Equal(t, int64(12288), b) 145 | } 146 | b, err = Parse("12Ki") 147 | if assert.NoError(t, err) { 148 | assert.Equal(t, int64(12288), b) 149 | } 150 | 151 | // kib, lowercase multiple test 152 | b, err = Parse("12.25kib") 153 | if assert.NoError(t, err) { 154 | assert.Equal(t, int64(12544), b) 155 | } 156 | b, err = Parse("12kib") 157 | if assert.NoError(t, err) { 158 | assert.Equal(t, int64(12288), b) 159 | } 160 | b, err = Parse("12ki") 161 | if assert.NoError(t, err) { 162 | assert.Equal(t, int64(12288), b) 163 | } 164 | 165 | // KiB with space 166 | b, err = Parse("12.25 KiB") 167 | if assert.NoError(t, err) { 168 | assert.Equal(t, int64(12544), b) 169 | } 170 | b, err = Parse("12 KiB") 171 | if assert.NoError(t, err) { 172 | assert.Equal(t, int64(12288), b) 173 | } 174 | b, err = Parse("12 Ki") 175 | if assert.NoError(t, err) { 176 | assert.Equal(t, int64(12288), b) 177 | } 178 | 179 | // MiB 180 | b, err = Parse("2MiB") 181 | if assert.NoError(t, err) { 182 | assert.Equal(t, int64(2097152), b) 183 | } 184 | b, err = Parse("2Mi") 185 | if assert.NoError(t, err) { 186 | assert.Equal(t, int64(2097152), b) 187 | } 188 | 189 | // GiB with space 190 | b, err = Parse("6 GiB") 191 | if assert.NoError(t, err) { 192 | assert.Equal(t, int64(6442450944), b) 193 | } 194 | b, err = Parse("6 Gi") 195 | if assert.NoError(t, err) { 196 | assert.Equal(t, int64(6442450944), b) 197 | } 198 | 199 | // GiB 200 | b, err = Parse("6GiB") 201 | if assert.NoError(t, err) { 202 | assert.Equal(t, int64(6442450944), b) 203 | } 204 | b, err = Parse("6Gi") 205 | if assert.NoError(t, err) { 206 | assert.Equal(t, int64(6442450944), b) 207 | } 208 | 209 | // TiB 210 | b, err = Parse("5TiB") 211 | if assert.NoError(t, err) { 212 | assert.Equal(t, int64(5497558138880), b) 213 | } 214 | b, err = Parse("5Ti") 215 | if assert.NoError(t, err) { 216 | assert.Equal(t, int64(5497558138880), b) 217 | } 218 | 219 | // TiB with space 220 | b, err = Parse("5 TiB") 221 | if assert.NoError(t, err) { 222 | assert.Equal(t, int64(5497558138880), b) 223 | } 224 | b, err = Parse("5 Ti") 225 | if assert.NoError(t, err) { 226 | assert.Equal(t, int64(5497558138880), b) 227 | } 228 | 229 | // PiB 230 | b, err = Parse("9PiB") 231 | if assert.NoError(t, err) { 232 | assert.Equal(t, int64(10133099161583616), b) 233 | } 234 | b, err = Parse("9Pi") 235 | if assert.NoError(t, err) { 236 | assert.Equal(t, int64(10133099161583616), b) 237 | } 238 | 239 | // PiB with space 240 | b, err = Parse("9 PiB") 241 | if assert.NoError(t, err) { 242 | assert.Equal(t, int64(10133099161583616), b) 243 | } 244 | b, err = Parse("9 Pi") 245 | if assert.NoError(t, err) { 246 | assert.Equal(t, int64(10133099161583616), b) 247 | } 248 | 249 | // EiB 250 | b, err = Parse("8EiB") 251 | if assert.NoError(t, err) { 252 | assert.True(t, math.MaxInt64 == b-1) 253 | } 254 | b, err = Parse("8Ei") 255 | if assert.NoError(t, err) { 256 | assert.True(t, math.MaxInt64 == b-1) 257 | } 258 | 259 | // EiB with spaces 260 | b, err = Parse("8 EiB") 261 | if assert.NoError(t, err) { 262 | assert.True(t, math.MaxInt64 == b-1) 263 | } 264 | b, err = Parse("8 Ei") 265 | if assert.NoError(t, err) { 266 | assert.True(t, math.MaxInt64 == b-1) 267 | } 268 | 269 | // KB 270 | b, err = Parse("12.25KB") 271 | if assert.NoError(t, err) { 272 | assert.Equal(t, int64(12250), b) 273 | } 274 | b, err = Parse("12KB") 275 | if assert.NoError(t, err) { 276 | assert.Equal(t, int64(12000), b) 277 | } 278 | b, err = Parse("12K") 279 | if assert.NoError(t, err) { 280 | assert.Equal(t, int64(12000), b) 281 | } 282 | 283 | // kb, lowercase multiple test 284 | b, err = Parse("12.25kb") 285 | if assert.NoError(t, err) { 286 | assert.Equal(t, int64(12250), b) 287 | } 288 | b, err = Parse("12kb") 289 | if assert.NoError(t, err) { 290 | assert.Equal(t, int64(12000), b) 291 | } 292 | b, err = Parse("12k") 293 | if assert.NoError(t, err) { 294 | assert.Equal(t, int64(12000), b) 295 | } 296 | 297 | // KB with space 298 | b, err = Parse("12.25 KB") 299 | if assert.NoError(t, err) { 300 | assert.Equal(t, int64(12250), b) 301 | } 302 | b, err = Parse("12 KB") 303 | if assert.NoError(t, err) { 304 | assert.Equal(t, int64(12000), b) 305 | } 306 | b, err = Parse("12 K") 307 | if assert.NoError(t, err) { 308 | assert.Equal(t, int64(12000), b) 309 | } 310 | 311 | // MB 312 | b, err = Parse("2MB") 313 | if assert.NoError(t, err) { 314 | assert.Equal(t, int64(2000000), b) 315 | } 316 | b, err = Parse("2M") 317 | if assert.NoError(t, err) { 318 | assert.Equal(t, int64(2000000), b) 319 | } 320 | 321 | // GB with space 322 | b, err = Parse("6 GB") 323 | if assert.NoError(t, err) { 324 | assert.Equal(t, int64(6000000000), b) 325 | } 326 | b, err = Parse("6 G") 327 | if assert.NoError(t, err) { 328 | assert.Equal(t, int64(6000000000), b) 329 | } 330 | 331 | // GB 332 | b, err = Parse("6GB") 333 | if assert.NoError(t, err) { 334 | assert.Equal(t, int64(6000000000), b) 335 | } 336 | b, err = Parse("6G") 337 | if assert.NoError(t, err) { 338 | assert.Equal(t, int64(6000000000), b) 339 | } 340 | 341 | // TB 342 | b, err = Parse("5TB") 343 | if assert.NoError(t, err) { 344 | assert.Equal(t, int64(5000000000000), b) 345 | } 346 | b, err = Parse("5T") 347 | if assert.NoError(t, err) { 348 | assert.Equal(t, int64(5000000000000), b) 349 | } 350 | 351 | // TB with space 352 | b, err = Parse("5 TB") 353 | if assert.NoError(t, err) { 354 | assert.Equal(t, int64(5000000000000), b) 355 | } 356 | b, err = Parse("5 T") 357 | if assert.NoError(t, err) { 358 | assert.Equal(t, int64(5000000000000), b) 359 | } 360 | 361 | // PB 362 | b, err = Parse("9PB") 363 | if assert.NoError(t, err) { 364 | assert.Equal(t, int64(9000000000000000), b) 365 | } 366 | b, err = Parse("9P") 367 | if assert.NoError(t, err) { 368 | assert.Equal(t, int64(9000000000000000), b) 369 | } 370 | 371 | // PB with space 372 | b, err = Parse("9 PB") 373 | if assert.NoError(t, err) { 374 | assert.Equal(t, int64(9000000000000000), b) 375 | } 376 | b, err = Parse("9 P") 377 | if assert.NoError(t, err) { 378 | assert.Equal(t, int64(9000000000000000), b) 379 | } 380 | 381 | // EB 382 | b, err = Parse("8EB") 383 | if assert.NoError(t, err) { 384 | assert.Equal(t, int64(8000000000000000000), b) 385 | } 386 | b, err = Parse("8E") 387 | if assert.NoError(t, err) { 388 | assert.Equal(t, int64(8000000000000000000), b) 389 | } 390 | 391 | // EB with spaces 392 | b, err = Parse("8 EB") 393 | if assert.NoError(t, err) { 394 | assert.Equal(t, int64(8000000000000000000), b) 395 | } 396 | b, err = Parse("8 E") 397 | if assert.NoError(t, err) { 398 | assert.Equal(t, int64(8000000000000000000), b) 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /color/README.md: -------------------------------------------------------------------------------- 1 | # Color 2 | 3 | Style terminal text. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | go get github.com/labstack/gommon/color 9 | ``` 10 | 11 | ## Windows? 12 | 13 | Try [cmder](http://bliker.github.io/cmder) or https://github.com/mattn/go-colorable 14 | 15 | ## [Usage](https://github.com/labstack/gommon/blob/master/color/color_test.go) 16 | 17 | ```sh 18 | import github.com/labstack/gommon/color 19 | ``` 20 | 21 | ### Colored text 22 | 23 | ```go 24 | color.Println(color.Black("black")) 25 | color.Println(color.Red("red")) 26 | color.Println(color.Green("green")) 27 | color.Println(color.Yellow("yellow")) 28 | color.Println(color.Blue("blue")) 29 | color.Println(color.Magenta("magenta")) 30 | color.Println(color.Cyan("cyan")) 31 | color.Println(color.White("white")) 32 | color.Println(color.Grey("grey")) 33 | ``` 34 | ![Colored Text](http://i.imgur.com/8RtY1QR.png) 35 | 36 | ### Colored background 37 | 38 | ```go 39 | color.Println(color.BlackBg("black background", color.Wht)) 40 | color.Println(color.RedBg("red background")) 41 | color.Println(color.GreenBg("green background")) 42 | color.Println(color.YellowBg("yellow background")) 43 | color.Println(color.BlueBg("blue background")) 44 | color.Println(color.MagentaBg("magenta background")) 45 | color.Println(color.CyanBg("cyan background")) 46 | color.Println(color.WhiteBg("white background")) 47 | ``` 48 | ![Colored Background](http://i.imgur.com/SrrS6lw.png) 49 | 50 | ### Emphasis 51 | 52 | ```go 53 | color.Println(color.Bold("bold")) 54 | color.Println(color.Dim("dim")) 55 | color.Println(color.Italic("italic")) 56 | color.Println(color.Underline("underline")) 57 | color.Println(color.Inverse("inverse")) 58 | color.Println(color.Hidden("hidden")) 59 | color.Println(color.Strikeout("strikeout")) 60 | ``` 61 | ![Emphasis](http://i.imgur.com/3RSJBbc.png) 62 | 63 | ### Mix and match 64 | 65 | ```go 66 | color.Println(color.Green("bold green with white background", color.B, color.WhtBg)) 67 | color.Println(color.Red("underline red", color.U)) 68 | color.Println(color.Yellow("dim yellow", color.D)) 69 | color.Println(color.Cyan("inverse cyan", color.In)) 70 | color.Println(color.Blue("bold underline dim blue", color.B, color.U, color.D)) 71 | ``` 72 | ![Mix and match](http://i.imgur.com/jWGq9Ca.png) 73 | 74 | ### Enable/Disable the package 75 | 76 | ```go 77 | color.Disable() 78 | color.Enable() 79 | ``` 80 | 81 | ### New instance 82 | 83 | ```go 84 | c := New() 85 | c.Green("green") 86 | ``` 87 | -------------------------------------------------------------------------------- /color/color.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/mattn/go-colorable" 10 | "github.com/mattn/go-isatty" 11 | ) 12 | 13 | type ( 14 | inner func(interface{}, []string, *Color) string 15 | ) 16 | 17 | // Color styles 18 | const ( 19 | // Blk Black text style 20 | Blk = "30" 21 | // Rd red text style 22 | Rd = "31" 23 | // Grn green text style 24 | Grn = "32" 25 | // Yel yellow text style 26 | Yel = "33" 27 | // Blu blue text style 28 | Blu = "34" 29 | // Mgn magenta text style 30 | Mgn = "35" 31 | // Cyn cyan text style 32 | Cyn = "36" 33 | // Wht white text style 34 | Wht = "37" 35 | // Gry grey text style 36 | Gry = "90" 37 | 38 | // BlkBg black background style 39 | BlkBg = "40" 40 | // RdBg red background style 41 | RdBg = "41" 42 | // GrnBg green background style 43 | GrnBg = "42" 44 | // YelBg yellow background style 45 | YelBg = "43" 46 | // BluBg blue background style 47 | BluBg = "44" 48 | // MgnBg magenta background style 49 | MgnBg = "45" 50 | // CynBg cyan background style 51 | CynBg = "46" 52 | // WhtBg white background style 53 | WhtBg = "47" 54 | 55 | // R reset emphasis style 56 | R = "0" 57 | // B bold emphasis style 58 | B = "1" 59 | // D dim emphasis style 60 | D = "2" 61 | // I italic emphasis style 62 | I = "3" 63 | // U underline emphasis style 64 | U = "4" 65 | // In inverse emphasis style 66 | In = "7" 67 | // H hidden emphasis style 68 | H = "8" 69 | // S strikeout emphasis style 70 | S = "9" 71 | ) 72 | 73 | var ( 74 | black = outer(Blk) 75 | red = outer(Rd) 76 | green = outer(Grn) 77 | yellow = outer(Yel) 78 | blue = outer(Blu) 79 | magenta = outer(Mgn) 80 | cyan = outer(Cyn) 81 | white = outer(Wht) 82 | grey = outer(Gry) 83 | 84 | blackBg = outer(BlkBg) 85 | redBg = outer(RdBg) 86 | greenBg = outer(GrnBg) 87 | yellowBg = outer(YelBg) 88 | blueBg = outer(BluBg) 89 | magentaBg = outer(MgnBg) 90 | cyanBg = outer(CynBg) 91 | whiteBg = outer(WhtBg) 92 | 93 | reset = outer(R) 94 | bold = outer(B) 95 | dim = outer(D) 96 | italic = outer(I) 97 | underline = outer(U) 98 | inverse = outer(In) 99 | hidden = outer(H) 100 | strikeout = outer(S) 101 | 102 | global = New() 103 | ) 104 | 105 | func outer(n string) inner { 106 | return func(msg interface{}, styles []string, c *Color) string { 107 | // TODO: Drop fmt to boost performance? 108 | if c.disabled { 109 | return fmt.Sprintf("%v", msg) 110 | } 111 | 112 | b := new(bytes.Buffer) 113 | b.WriteString("\x1b[") 114 | b.WriteString(n) 115 | for _, s := range styles { 116 | b.WriteString(";") 117 | b.WriteString(s) 118 | } 119 | b.WriteString("m") 120 | return fmt.Sprintf("%s%v\x1b[0m", b.String(), msg) 121 | } 122 | } 123 | 124 | type ( 125 | Color struct { 126 | output io.Writer 127 | disabled bool 128 | } 129 | ) 130 | 131 | // New creates a Color instance. 132 | func New() (c *Color) { 133 | c = new(Color) 134 | c.SetOutput(colorable.NewColorableStdout()) 135 | return 136 | } 137 | 138 | // Output returns the output. 139 | func (c *Color) Output() io.Writer { 140 | return c.output 141 | } 142 | 143 | // SetOutput sets the output. 144 | func (c *Color) SetOutput(w io.Writer) { 145 | c.output = w 146 | if w, ok := w.(*os.File); !ok || !isatty.IsTerminal(w.Fd()) { 147 | c.disabled = true 148 | } 149 | } 150 | 151 | // Disable disables the colors and styles. 152 | func (c *Color) Disable() { 153 | c.disabled = true 154 | } 155 | 156 | // Enable enables the colors and styles. 157 | func (c *Color) Enable() { 158 | c.disabled = false 159 | } 160 | 161 | // Print is analogous to `fmt.Print` with termial detection. 162 | func (c *Color) Print(args ...interface{}) { 163 | fmt.Fprint(c.output, args...) 164 | } 165 | 166 | // Println is analogous to `fmt.Println` with termial detection. 167 | func (c *Color) Println(args ...interface{}) { 168 | fmt.Fprintln(c.output, args...) 169 | } 170 | 171 | // Printf is analogous to `fmt.Printf` with termial detection. 172 | func (c *Color) Printf(format string, args ...interface{}) { 173 | fmt.Fprintf(c.output, format, args...) 174 | } 175 | 176 | func (c *Color) Black(msg interface{}, styles ...string) string { 177 | return black(msg, styles, c) 178 | } 179 | 180 | func (c *Color) Red(msg interface{}, styles ...string) string { 181 | return red(msg, styles, c) 182 | } 183 | 184 | func (c *Color) Green(msg interface{}, styles ...string) string { 185 | return green(msg, styles, c) 186 | } 187 | 188 | func (c *Color) Yellow(msg interface{}, styles ...string) string { 189 | return yellow(msg, styles, c) 190 | } 191 | 192 | func (c *Color) Blue(msg interface{}, styles ...string) string { 193 | return blue(msg, styles, c) 194 | } 195 | 196 | func (c *Color) Magenta(msg interface{}, styles ...string) string { 197 | return magenta(msg, styles, c) 198 | } 199 | 200 | func (c *Color) Cyan(msg interface{}, styles ...string) string { 201 | return cyan(msg, styles, c) 202 | } 203 | 204 | func (c *Color) White(msg interface{}, styles ...string) string { 205 | return white(msg, styles, c) 206 | } 207 | 208 | func (c *Color) Grey(msg interface{}, styles ...string) string { 209 | return grey(msg, styles, c) 210 | } 211 | 212 | func (c *Color) BlackBg(msg interface{}, styles ...string) string { 213 | return blackBg(msg, styles, c) 214 | } 215 | 216 | func (c *Color) RedBg(msg interface{}, styles ...string) string { 217 | return redBg(msg, styles, c) 218 | } 219 | 220 | func (c *Color) GreenBg(msg interface{}, styles ...string) string { 221 | return greenBg(msg, styles, c) 222 | } 223 | 224 | func (c *Color) YellowBg(msg interface{}, styles ...string) string { 225 | return yellowBg(msg, styles, c) 226 | } 227 | 228 | func (c *Color) BlueBg(msg interface{}, styles ...string) string { 229 | return blueBg(msg, styles, c) 230 | } 231 | 232 | func (c *Color) MagentaBg(msg interface{}, styles ...string) string { 233 | return magentaBg(msg, styles, c) 234 | } 235 | 236 | func (c *Color) CyanBg(msg interface{}, styles ...string) string { 237 | return cyanBg(msg, styles, c) 238 | } 239 | 240 | func (c *Color) WhiteBg(msg interface{}, styles ...string) string { 241 | return whiteBg(msg, styles, c) 242 | } 243 | 244 | func (c *Color) Reset(msg interface{}, styles ...string) string { 245 | return reset(msg, styles, c) 246 | } 247 | 248 | func (c *Color) Bold(msg interface{}, styles ...string) string { 249 | return bold(msg, styles, c) 250 | } 251 | 252 | func (c *Color) Dim(msg interface{}, styles ...string) string { 253 | return dim(msg, styles, c) 254 | } 255 | 256 | func (c *Color) Italic(msg interface{}, styles ...string) string { 257 | return italic(msg, styles, c) 258 | } 259 | 260 | func (c *Color) Underline(msg interface{}, styles ...string) string { 261 | return underline(msg, styles, c) 262 | } 263 | 264 | func (c *Color) Inverse(msg interface{}, styles ...string) string { 265 | return inverse(msg, styles, c) 266 | } 267 | 268 | func (c *Color) Hidden(msg interface{}, styles ...string) string { 269 | return hidden(msg, styles, c) 270 | } 271 | 272 | func (c *Color) Strikeout(msg interface{}, styles ...string) string { 273 | return strikeout(msg, styles, c) 274 | } 275 | 276 | // Output returns the output. 277 | func Output() io.Writer { 278 | return global.output 279 | } 280 | 281 | // SetOutput sets the output. 282 | func SetOutput(w io.Writer) { 283 | global.SetOutput(w) 284 | } 285 | 286 | func Disable() { 287 | global.Disable() 288 | } 289 | 290 | func Enable() { 291 | global.Enable() 292 | } 293 | 294 | // Print is analogous to `fmt.Print` with termial detection. 295 | func Print(args ...interface{}) { 296 | global.Print(args...) 297 | } 298 | 299 | // Println is analogous to `fmt.Println` with termial detection. 300 | func Println(args ...interface{}) { 301 | global.Println(args...) 302 | } 303 | 304 | // Printf is analogous to `fmt.Printf` with termial detection. 305 | func Printf(format string, args ...interface{}) { 306 | global.Printf(format, args...) 307 | } 308 | 309 | func Black(msg interface{}, styles ...string) string { 310 | return global.Black(msg, styles...) 311 | } 312 | 313 | func Red(msg interface{}, styles ...string) string { 314 | return global.Red(msg, styles...) 315 | } 316 | 317 | func Green(msg interface{}, styles ...string) string { 318 | return global.Green(msg, styles...) 319 | } 320 | 321 | func Yellow(msg interface{}, styles ...string) string { 322 | return global.Yellow(msg, styles...) 323 | } 324 | 325 | func Blue(msg interface{}, styles ...string) string { 326 | return global.Blue(msg, styles...) 327 | } 328 | 329 | func Magenta(msg interface{}, styles ...string) string { 330 | return global.Magenta(msg, styles...) 331 | } 332 | 333 | func Cyan(msg interface{}, styles ...string) string { 334 | return global.Cyan(msg, styles...) 335 | } 336 | 337 | func White(msg interface{}, styles ...string) string { 338 | return global.White(msg, styles...) 339 | } 340 | 341 | func Grey(msg interface{}, styles ...string) string { 342 | return global.Grey(msg, styles...) 343 | } 344 | 345 | func BlackBg(msg interface{}, styles ...string) string { 346 | return global.BlackBg(msg, styles...) 347 | } 348 | 349 | func RedBg(msg interface{}, styles ...string) string { 350 | return global.RedBg(msg, styles...) 351 | } 352 | 353 | func GreenBg(msg interface{}, styles ...string) string { 354 | return global.GreenBg(msg, styles...) 355 | } 356 | 357 | func YellowBg(msg interface{}, styles ...string) string { 358 | return global.YellowBg(msg, styles...) 359 | } 360 | 361 | func BlueBg(msg interface{}, styles ...string) string { 362 | return global.BlueBg(msg, styles...) 363 | } 364 | 365 | func MagentaBg(msg interface{}, styles ...string) string { 366 | return global.MagentaBg(msg, styles...) 367 | } 368 | 369 | func CyanBg(msg interface{}, styles ...string) string { 370 | return global.CyanBg(msg, styles...) 371 | } 372 | 373 | func WhiteBg(msg interface{}, styles ...string) string { 374 | return global.WhiteBg(msg, styles...) 375 | } 376 | 377 | func Reset(msg interface{}, styles ...string) string { 378 | return global.Reset(msg, styles...) 379 | } 380 | 381 | func Bold(msg interface{}, styles ...string) string { 382 | return global.Bold(msg, styles...) 383 | } 384 | 385 | func Dim(msg interface{}, styles ...string) string { 386 | return global.Dim(msg, styles...) 387 | } 388 | 389 | func Italic(msg interface{}, styles ...string) string { 390 | return global.Italic(msg, styles...) 391 | } 392 | 393 | func Underline(msg interface{}, styles ...string) string { 394 | return global.Underline(msg, styles...) 395 | } 396 | 397 | func Inverse(msg interface{}, styles ...string) string { 398 | return global.Inverse(msg, styles...) 399 | } 400 | 401 | func Hidden(msg interface{}, styles ...string) string { 402 | return global.Hidden(msg, styles...) 403 | } 404 | 405 | func Strikeout(msg interface{}, styles ...string) string { 406 | return global.Strikeout(msg, styles...) 407 | } 408 | -------------------------------------------------------------------------------- /color/color_test.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestText(t *testing.T) { 10 | Println("*** colored text ***") 11 | Println(Black("black")) 12 | Println(Red("red")) 13 | Println(Green("green")) 14 | Println(Yellow("yellow")) 15 | Println(Blue("blue")) 16 | Println(Magenta("magenta")) 17 | Println(Cyan("cyan")) 18 | Println(White("white")) 19 | Println(Grey("grey")) 20 | } 21 | 22 | func TestBackground(t *testing.T) { 23 | Println("*** colored background ***") 24 | Println(BlackBg("black background", Wht)) 25 | Println(RedBg("red background")) 26 | Println(GreenBg("green background")) 27 | Println(YellowBg("yellow background")) 28 | Println(BlueBg("blue background")) 29 | Println(MagentaBg("magenta background")) 30 | Println(CyanBg("cyan background")) 31 | Println(WhiteBg("white background")) 32 | } 33 | 34 | func TestEmphasis(t *testing.T) { 35 | Println("*** emphasis ***") 36 | Println(Reset("reset")) 37 | Println(Bold("bold")) 38 | Println(Dim("dim")) 39 | Println(Italic("italic")) 40 | Println(Underline("underline")) 41 | Println(Inverse("inverse")) 42 | Println(Hidden("hidden")) 43 | Println(Strikeout("strikeout")) 44 | } 45 | 46 | func TestMixMatch(t *testing.T) { 47 | Println("*** mix and match ***") 48 | Println(Green("bold green with white background", B, WhtBg)) 49 | Println(Red("underline red", U)) 50 | Println(Yellow("dim yellow", D)) 51 | Println(Cyan("inverse cyan", In)) 52 | Println(Blue("bold underline dim blue", B, U, D)) 53 | } 54 | 55 | func TestEnableDisable(t *testing.T) { 56 | Disable() 57 | assert.Equal(t, "red", Red("red")) 58 | Enable() 59 | assert.NotEqual(t, "green", Green("green")) 60 | } 61 | -------------------------------------------------------------------------------- /email/email.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "html/template" 7 | "net" 8 | "net/mail" 9 | "net/smtp" 10 | "time" 11 | 12 | "github.com/labstack/gommon/random" 13 | ) 14 | 15 | type ( 16 | Email struct { 17 | Auth smtp.Auth 18 | Header map[string]string 19 | Template *template.Template 20 | smtpAddress string 21 | } 22 | 23 | Message struct { 24 | ID string `json:"id"` 25 | From string `json:"from"` 26 | To string `json:"to"` 27 | CC string `json:"cc"` 28 | Subject string `json:"subject"` 29 | BodyText string `json:"body_text"` 30 | BodyHTML string `json:"body_html"` 31 | Inlines []*File `json:"inlines"` 32 | Attachments []*File `json:"attachments"` 33 | buffer *bytes.Buffer 34 | boundary string 35 | } 36 | 37 | File struct { 38 | Name string 39 | Type string 40 | Content string 41 | } 42 | ) 43 | 44 | func New(smtpAddress string) *Email { 45 | return &Email{ 46 | smtpAddress: smtpAddress, 47 | Header: map[string]string{}, 48 | } 49 | } 50 | 51 | func (m *Message) writeHeader(key, value string) { 52 | m.buffer.WriteString(key) 53 | m.buffer.WriteString(": ") 54 | m.buffer.WriteString(value) 55 | m.buffer.WriteString("\r\n") 56 | } 57 | 58 | func (m *Message) writeBoundary() { 59 | m.buffer.WriteString("--") 60 | m.buffer.WriteString(m.boundary) 61 | m.buffer.WriteString("\r\n") 62 | } 63 | 64 | func (m *Message) writeText(content string, contentType string) { 65 | m.writeBoundary() 66 | m.writeHeader("Content-Type", contentType+"; charset=UTF-8") 67 | m.buffer.WriteString("\r\n") 68 | m.buffer.WriteString(content) 69 | m.buffer.WriteString("\r\n") 70 | m.buffer.WriteString("\r\n") 71 | } 72 | 73 | func (m *Message) writeFile(f *File, disposition string) { 74 | m.writeBoundary() 75 | m.writeHeader("Content-Type", f.Type+`; name="`+f.Name+`"`) 76 | m.writeHeader("Content-Disposition", disposition+`; filename="`+f.Name+`"`) 77 | m.writeHeader("Content-Transfer-Encoding", "base64") 78 | m.buffer.WriteString("\r\n") 79 | m.buffer.WriteString(f.Content) 80 | m.buffer.WriteString("\r\n") 81 | m.buffer.WriteString("\r\n") 82 | } 83 | 84 | func (e *Email) Send(m *Message) (err error) { 85 | // Message header 86 | m.buffer = bytes.NewBuffer(make([]byte, 256)) 87 | m.buffer.Reset() 88 | m.boundary = random.String(16) 89 | m.writeHeader("MIME-Version", "1.0") 90 | m.writeHeader("Message-ID", m.ID) 91 | m.writeHeader("Date", time.Now().Format(time.RFC1123Z)) 92 | m.writeHeader("From", m.From) 93 | m.writeHeader("To", m.To) 94 | if m.CC != "" { 95 | m.writeHeader("CC", m.CC) 96 | } 97 | if m.Subject != "" { 98 | m.writeHeader("Subject", m.Subject) 99 | } 100 | // Extra 101 | for k, v := range e.Header { 102 | m.writeHeader(k, v) 103 | } 104 | m.writeHeader("Content-Type", "multipart/mixed; boundary="+m.boundary) 105 | m.buffer.WriteString("\r\n") 106 | 107 | // Message body 108 | if m.BodyText != "" { 109 | m.writeText(m.BodyText, "text/plain") 110 | } else if m.BodyHTML != "" { 111 | m.writeText(m.BodyHTML, "text/html") 112 | } else { 113 | m.writeBoundary() 114 | } 115 | 116 | // Inlines/attachments 117 | for _, f := range m.Inlines { 118 | m.writeFile(f, "inline") 119 | } 120 | for _, f := range m.Attachments { 121 | m.writeFile(f, "attachment") 122 | } 123 | m.buffer.WriteString("--") 124 | m.buffer.WriteString(m.boundary) 125 | m.buffer.WriteString("--") 126 | 127 | // Dial 128 | c, err := smtp.Dial(e.smtpAddress) 129 | if err != nil { 130 | return 131 | } 132 | defer c.Quit() 133 | 134 | // Check if TLS is required 135 | if ok, _ := c.Extension("STARTTLS"); ok { 136 | host, _, _ := net.SplitHostPort(e.smtpAddress) 137 | config := &tls.Config{ServerName: host} 138 | if err = c.StartTLS(config); err != nil { 139 | return err 140 | } 141 | } 142 | 143 | // Authenticate 144 | if e.Auth != nil { 145 | if err = c.Auth(e.Auth); err != nil { 146 | return 147 | } 148 | } 149 | 150 | // Send message 151 | from, err := mail.ParseAddress(m.From) 152 | if err != nil { 153 | return 154 | } 155 | if err = c.Mail(from.Address); err != nil { 156 | return 157 | } 158 | to, err := mail.ParseAddressList(m.To) 159 | if err != nil { 160 | return 161 | } 162 | for _, a := range to { 163 | if err = c.Rcpt(a.Address); err != nil { 164 | return 165 | } 166 | } 167 | wc, err := c.Data() 168 | if err != nil { 169 | return 170 | } 171 | defer wc.Close() 172 | _, err = m.buffer.WriteTo(wc) 173 | return 174 | } 175 | -------------------------------------------------------------------------------- /email/email_test.go: -------------------------------------------------------------------------------- 1 | package email 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/labstack/gommon 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/mattn/go-colorable v0.1.13 7 | github.com/mattn/go-isatty v0.0.20 8 | github.com/stretchr/testify v1.8.4 9 | github.com/valyala/fasttemplate v1.2.2 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | github.com/valyala/bytebufferpool v1.0.0 // indirect 16 | golang.org/x/sys v0.15.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 4 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 5 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 6 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 7 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 11 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 12 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 13 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 14 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 15 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 16 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 17 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 18 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 19 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 23 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /gommon.go: -------------------------------------------------------------------------------- 1 | package gommon 2 | -------------------------------------------------------------------------------- /log/README.md: -------------------------------------------------------------------------------- 1 | ## WORK IN PROGRESS 2 | 3 | ### Usage 4 | 5 | [log_test.go](log_test.go) 6 | -------------------------------------------------------------------------------- /log/color.go: -------------------------------------------------------------------------------- 1 | // +build !appengine 2 | 3 | package log 4 | 5 | import ( 6 | "io" 7 | 8 | "github.com/mattn/go-colorable" 9 | ) 10 | 11 | func output() io.Writer { 12 | return colorable.NewColorableStdout() 13 | } 14 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path" 10 | "runtime" 11 | "strconv" 12 | "sync" 13 | "sync/atomic" 14 | "time" 15 | 16 | "github.com/mattn/go-isatty" 17 | "github.com/valyala/fasttemplate" 18 | 19 | "github.com/labstack/gommon/color" 20 | ) 21 | 22 | type ( 23 | Logger struct { 24 | prefix string 25 | level uint32 26 | skip int 27 | output io.Writer 28 | template *fasttemplate.Template 29 | levels []string 30 | color *color.Color 31 | bufferPool sync.Pool 32 | mutex sync.Mutex 33 | } 34 | 35 | Lvl uint8 36 | 37 | JSON map[string]interface{} 38 | ) 39 | 40 | const ( 41 | DEBUG Lvl = iota + 1 42 | INFO 43 | WARN 44 | ERROR 45 | OFF 46 | panicLevel 47 | fatalLevel 48 | ) 49 | 50 | var ( 51 | global = New("-") 52 | defaultHeader = `{"time":"${time_rfc3339_nano}","level":"${level}","prefix":"${prefix}",` + 53 | `"file":"${short_file}","line":"${line}"}` 54 | ) 55 | 56 | func init() { 57 | global.skip = 3 58 | } 59 | 60 | func New(prefix string) (l *Logger) { 61 | l = &Logger{ 62 | level: uint32(INFO), 63 | skip: 2, 64 | prefix: prefix, 65 | template: l.newTemplate(defaultHeader), 66 | color: color.New(), 67 | bufferPool: sync.Pool{ 68 | New: func() interface{} { 69 | return bytes.NewBuffer(make([]byte, 256)) 70 | }, 71 | }, 72 | } 73 | l.initLevels() 74 | l.SetOutput(output()) 75 | return 76 | } 77 | 78 | func (l *Logger) initLevels() { 79 | l.levels = []string{ 80 | "-", 81 | l.color.Blue("DEBUG"), 82 | l.color.Green("INFO"), 83 | l.color.Yellow("WARN"), 84 | l.color.Red("ERROR"), 85 | "", 86 | l.color.Yellow("PANIC", color.U), 87 | l.color.Red("FATAL", color.U), 88 | } 89 | } 90 | 91 | func (l *Logger) newTemplate(format string) *fasttemplate.Template { 92 | return fasttemplate.New(format, "${", "}") 93 | } 94 | 95 | func (l *Logger) DisableColor() { 96 | l.color.Disable() 97 | l.initLevels() 98 | } 99 | 100 | func (l *Logger) EnableColor() { 101 | l.color.Enable() 102 | l.initLevels() 103 | } 104 | 105 | func (l *Logger) Prefix() string { 106 | return l.prefix 107 | } 108 | 109 | func (l *Logger) SetPrefix(p string) { 110 | l.prefix = p 111 | } 112 | 113 | func (l *Logger) Level() Lvl { 114 | return Lvl(atomic.LoadUint32(&l.level)) 115 | } 116 | 117 | func (l *Logger) SetLevel(level Lvl) { 118 | atomic.StoreUint32(&l.level, uint32(level)) 119 | } 120 | 121 | func (l *Logger) Output() io.Writer { 122 | return l.output 123 | } 124 | 125 | func (l *Logger) SetOutput(w io.Writer) { 126 | l.output = w 127 | if w, ok := w.(*os.File); !ok || !isatty.IsTerminal(w.Fd()) { 128 | l.DisableColor() 129 | } 130 | } 131 | 132 | func (l *Logger) Color() *color.Color { 133 | return l.color 134 | } 135 | 136 | func (l *Logger) SetHeader(h string) { 137 | l.template = l.newTemplate(h) 138 | } 139 | 140 | func (l *Logger) Print(i ...interface{}) { 141 | l.log(0, "", i...) 142 | // fmt.Fprintln(l.output, i...) 143 | } 144 | 145 | func (l *Logger) Printf(format string, args ...interface{}) { 146 | l.log(0, format, args...) 147 | } 148 | 149 | func (l *Logger) Printj(j JSON) { 150 | l.log(0, "json", j) 151 | } 152 | 153 | func (l *Logger) Debug(i ...interface{}) { 154 | l.log(DEBUG, "", i...) 155 | } 156 | 157 | func (l *Logger) Debugf(format string, args ...interface{}) { 158 | l.log(DEBUG, format, args...) 159 | } 160 | 161 | func (l *Logger) Debugj(j JSON) { 162 | l.log(DEBUG, "json", j) 163 | } 164 | 165 | func (l *Logger) Info(i ...interface{}) { 166 | l.log(INFO, "", i...) 167 | } 168 | 169 | func (l *Logger) Infof(format string, args ...interface{}) { 170 | l.log(INFO, format, args...) 171 | } 172 | 173 | func (l *Logger) Infoj(j JSON) { 174 | l.log(INFO, "json", j) 175 | } 176 | 177 | func (l *Logger) Warn(i ...interface{}) { 178 | l.log(WARN, "", i...) 179 | } 180 | 181 | func (l *Logger) Warnf(format string, args ...interface{}) { 182 | l.log(WARN, format, args...) 183 | } 184 | 185 | func (l *Logger) Warnj(j JSON) { 186 | l.log(WARN, "json", j) 187 | } 188 | 189 | func (l *Logger) Error(i ...interface{}) { 190 | l.log(ERROR, "", i...) 191 | } 192 | 193 | func (l *Logger) Errorf(format string, args ...interface{}) { 194 | l.log(ERROR, format, args...) 195 | } 196 | 197 | func (l *Logger) Errorj(j JSON) { 198 | l.log(ERROR, "json", j) 199 | } 200 | 201 | func (l *Logger) Fatal(i ...interface{}) { 202 | l.log(fatalLevel, "", i...) 203 | os.Exit(1) 204 | } 205 | 206 | func (l *Logger) Fatalf(format string, args ...interface{}) { 207 | l.log(fatalLevel, format, args...) 208 | os.Exit(1) 209 | } 210 | 211 | func (l *Logger) Fatalj(j JSON) { 212 | l.log(fatalLevel, "json", j) 213 | os.Exit(1) 214 | } 215 | 216 | func (l *Logger) Panic(i ...interface{}) { 217 | l.log(panicLevel, "", i...) 218 | panic(fmt.Sprint(i...)) 219 | } 220 | 221 | func (l *Logger) Panicf(format string, args ...interface{}) { 222 | l.log(panicLevel, format, args...) 223 | panic(fmt.Sprintf(format, args...)) 224 | } 225 | 226 | func (l *Logger) Panicj(j JSON) { 227 | l.log(panicLevel, "json", j) 228 | panic(j) 229 | } 230 | 231 | func DisableColor() { 232 | global.DisableColor() 233 | } 234 | 235 | func EnableColor() { 236 | global.EnableColor() 237 | } 238 | 239 | func Prefix() string { 240 | return global.Prefix() 241 | } 242 | 243 | func SetPrefix(p string) { 244 | global.SetPrefix(p) 245 | } 246 | 247 | func Level() Lvl { 248 | return global.Level() 249 | } 250 | 251 | func SetLevel(level Lvl) { 252 | global.SetLevel(level) 253 | } 254 | 255 | func Output() io.Writer { 256 | return global.Output() 257 | } 258 | 259 | func SetOutput(w io.Writer) { 260 | global.SetOutput(w) 261 | } 262 | 263 | func SetHeader(h string) { 264 | global.SetHeader(h) 265 | } 266 | 267 | func Print(i ...interface{}) { 268 | global.Print(i...) 269 | } 270 | 271 | func Printf(format string, args ...interface{}) { 272 | global.Printf(format, args...) 273 | } 274 | 275 | func Printj(j JSON) { 276 | global.Printj(j) 277 | } 278 | 279 | func Debug(i ...interface{}) { 280 | global.Debug(i...) 281 | } 282 | 283 | func Debugf(format string, args ...interface{}) { 284 | global.Debugf(format, args...) 285 | } 286 | 287 | func Debugj(j JSON) { 288 | global.Debugj(j) 289 | } 290 | 291 | func Info(i ...interface{}) { 292 | global.Info(i...) 293 | } 294 | 295 | func Infof(format string, args ...interface{}) { 296 | global.Infof(format, args...) 297 | } 298 | 299 | func Infoj(j JSON) { 300 | global.Infoj(j) 301 | } 302 | 303 | func Warn(i ...interface{}) { 304 | global.Warn(i...) 305 | } 306 | 307 | func Warnf(format string, args ...interface{}) { 308 | global.Warnf(format, args...) 309 | } 310 | 311 | func Warnj(j JSON) { 312 | global.Warnj(j) 313 | } 314 | 315 | func Error(i ...interface{}) { 316 | global.Error(i...) 317 | } 318 | 319 | func Errorf(format string, args ...interface{}) { 320 | global.Errorf(format, args...) 321 | } 322 | 323 | func Errorj(j JSON) { 324 | global.Errorj(j) 325 | } 326 | 327 | func Fatal(i ...interface{}) { 328 | global.Fatal(i...) 329 | } 330 | 331 | func Fatalf(format string, args ...interface{}) { 332 | global.Fatalf(format, args...) 333 | } 334 | 335 | func Fatalj(j JSON) { 336 | global.Fatalj(j) 337 | } 338 | 339 | func Panic(i ...interface{}) { 340 | global.Panic(i...) 341 | } 342 | 343 | func Panicf(format string, args ...interface{}) { 344 | global.Panicf(format, args...) 345 | } 346 | 347 | func Panicj(j JSON) { 348 | global.Panicj(j) 349 | } 350 | 351 | func (l *Logger) log(level Lvl, format string, args ...interface{}) { 352 | if level >= l.Level() || level == 0 { 353 | buf := l.bufferPool.Get().(*bytes.Buffer) 354 | buf.Reset() 355 | defer l.bufferPool.Put(buf) 356 | _, file, line, _ := runtime.Caller(l.skip) 357 | message := "" 358 | 359 | if format == "" { 360 | message = fmt.Sprint(args...) 361 | } else if format == "json" { 362 | b, err := json.Marshal(args[0]) 363 | if err != nil { 364 | panic(err) 365 | } 366 | message = string(b) 367 | } else { 368 | message = fmt.Sprintf(format, args...) 369 | } 370 | 371 | _, err := l.template.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) { 372 | switch tag { 373 | case "time_rfc3339": 374 | return w.Write([]byte(time.Now().Format(time.RFC3339))) 375 | case "time_rfc3339_nano": 376 | return w.Write([]byte(time.Now().Format(time.RFC3339Nano))) 377 | case "level": 378 | return w.Write([]byte(l.levels[level])) 379 | case "prefix": 380 | return w.Write([]byte(l.prefix)) 381 | case "long_file": 382 | return w.Write([]byte(file)) 383 | case "short_file": 384 | return w.Write([]byte(path.Base(file))) 385 | case "line": 386 | return w.Write([]byte(strconv.Itoa(line))) 387 | } 388 | return 0, nil 389 | }) 390 | 391 | if err == nil { 392 | s := buf.String() 393 | i := buf.Len() - 1 394 | if i >= 0 && s[i] == '}' { 395 | // JSON header 396 | buf.Truncate(i) 397 | buf.WriteByte(',') 398 | if format == "json" { 399 | buf.WriteString(message[1:]) 400 | } else { 401 | buf.WriteString(`"message":`) 402 | buf.WriteString(strconv.Quote(message)) 403 | buf.WriteString(`}`) 404 | } 405 | } else { 406 | // Text header 407 | if len(s) > 0 { 408 | buf.WriteByte(' ') 409 | } 410 | buf.WriteString(message) 411 | } 412 | buf.WriteByte('\n') 413 | l.mutex.Lock() 414 | defer l.mutex.Unlock() 415 | l.output.Write(buf.Bytes()) 416 | } 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /log/log_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func test(l *Logger, t *testing.T) { 14 | b := new(bytes.Buffer) 15 | l.SetOutput(b) 16 | l.DisableColor() 17 | l.SetLevel(WARN) 18 | 19 | l.Print("print") 20 | l.Printf("print%s", "f") 21 | l.Debug("debug") 22 | l.Debugf("debug%s", "f") 23 | l.Info("info") 24 | l.Infof("info%s", "f") 25 | l.Warn("warn") 26 | l.Warnf("warn%s", "f") 27 | l.Error("error") 28 | l.Errorf("error%s", "f") 29 | 30 | assert.Contains(t, b.String(), "print") 31 | assert.Contains(t, b.String(), "printf") 32 | assert.NotContains(t, b.String(), "debug") 33 | assert.NotContains(t, b.String(), "debugf") 34 | assert.NotContains(t, b.String(), "info") 35 | assert.NotContains(t, b.String(), "infof") 36 | assert.Contains(t, b.String(), `"level":"WARN","prefix":"`+l.prefix+`"`) 37 | assert.Contains(t, b.String(), `"message":"warn"`) 38 | assert.Contains(t, b.String(), `"level":"ERROR","prefix":"`+l.prefix+`"`) 39 | assert.Contains(t, b.String(), `"message":"errorf"`) 40 | } 41 | 42 | func TestLog(t *testing.T) { 43 | l := New("test") 44 | test(l, t) 45 | } 46 | 47 | func TestGlobal(t *testing.T) { 48 | test(global, t) 49 | } 50 | 51 | func TestLogConcurrent(t *testing.T) { 52 | var wg sync.WaitGroup 53 | for i := 0; i < 2; i++ { 54 | wg.Add(1) 55 | go func() { 56 | TestLog(t) 57 | wg.Done() 58 | }() 59 | } 60 | wg.Wait() 61 | } 62 | 63 | func TestFatal(t *testing.T) { 64 | l := New("test") 65 | switch os.Getenv("TEST_LOGGER_FATAL") { 66 | case "fatal": 67 | l.Fatal("fatal") 68 | return 69 | case "fatalf": 70 | l.Fatalf("fatal-%s", "f") 71 | return 72 | } 73 | 74 | loggerFatalTest(t, "fatal", "fatal") 75 | loggerFatalTest(t, "fatalf", "fatal-f") 76 | } 77 | 78 | func loggerFatalTest(t *testing.T, env string, contains string) { 79 | buf := new(bytes.Buffer) 80 | cmd := exec.Command(os.Args[0], "-test.run=TestFatal") 81 | cmd.Env = append(os.Environ(), "TEST_LOGGER_FATAL="+env) 82 | cmd.Stdout = buf 83 | cmd.Stderr = buf 84 | err := cmd.Run() 85 | if e, ok := err.(*exec.ExitError); ok && !e.Success() { 86 | assert.Contains(t, buf.String(), contains) 87 | return 88 | } 89 | t.Fatalf("process ran with err %v, want exit status 1", err) 90 | } 91 | 92 | func TestNoFormat(t *testing.T) { 93 | } 94 | 95 | func TestFormat(t *testing.T) { 96 | l := New("test") 97 | l.SetHeader("${level} | ${prefix}") 98 | b := new(bytes.Buffer) 99 | l.SetOutput(b) 100 | l.Info("info") 101 | assert.Equal(t, "INFO | test info\n", b.String()) 102 | } 103 | 104 | func TestJSON(t *testing.T) { 105 | l := New("test") 106 | b := new(bytes.Buffer) 107 | l.SetOutput(b) 108 | l.SetLevel(DEBUG) 109 | l.Debugj(JSON{"name": "value"}) 110 | assert.Contains(t, b.String(), `"name":"value"`) 111 | } 112 | 113 | func TestStringWithQuotes(t *testing.T) { 114 | l := New("test") 115 | b := new(bytes.Buffer) 116 | l.SetOutput(b) 117 | l.SetLevel(DEBUG) 118 | l.Debugf("Content-Type: %q", "") 119 | assert.Contains(t, b.String(), `"message":"Content-Type: \"\""`) 120 | } 121 | 122 | func TestEmptyHeader(t *testing.T) { 123 | l := New("test") 124 | b := new(bytes.Buffer) 125 | l.SetOutput(b) 126 | l.SetHeader("") 127 | l.SetLevel(DEBUG) 128 | l.Debugf("captain's log") 129 | assert.Contains(t, b.String(), `captain's log`) 130 | } 131 | 132 | func BenchmarkLog(b *testing.B) { 133 | l := New("test") 134 | l.SetOutput(new(bytes.Buffer)) 135 | for i := 0; i < b.N; i++ { 136 | l.Infof("Info=%s, Debug=%s", "info", "debug") 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /log/white.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | package log 4 | 5 | import ( 6 | "io" 7 | "os" 8 | ) 9 | 10 | func output() io.Writer { 11 | return os.Stdout 12 | } 13 | -------------------------------------------------------------------------------- /random/random.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "bufio" 5 | "crypto/rand" 6 | "io" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | type ( 12 | Random struct { 13 | readerPool sync.Pool 14 | } 15 | ) 16 | 17 | // Charsets 18 | const ( 19 | Uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 20 | Lowercase = "abcdefghijklmnopqrstuvwxyz" 21 | Alphabetic = Uppercase + Lowercase 22 | Numeric = "0123456789" 23 | Alphanumeric = Alphabetic + Numeric 24 | Symbols = "`" + `~!@#$%^&*()-_+={}[]|\;:"<>,./?` 25 | Hex = Numeric + "abcdef" 26 | ) 27 | 28 | var ( 29 | global = New() 30 | ) 31 | 32 | func New() *Random { 33 | // https://tip.golang.org/doc/go1.19#:~:text=Read%20no%20longer%20buffers%20random%20data%20obtained%20from%20the%20operating%20system%20between%20calls 34 | p := sync.Pool{New: func() interface{} { 35 | return bufio.NewReader(rand.Reader) 36 | }} 37 | return &Random{readerPool: p} 38 | } 39 | 40 | func (r *Random) String(length uint8, charsets ...string) string { 41 | charset := strings.Join(charsets, "") 42 | if charset == "" { 43 | charset = Alphanumeric 44 | } 45 | 46 | charsetLen := len(charset) 47 | if charsetLen > 255 { 48 | charsetLen = 255 49 | } 50 | maxByte := 255 - (256 % charsetLen) 51 | 52 | reader := r.readerPool.Get().(*bufio.Reader) 53 | defer r.readerPool.Put(reader) 54 | 55 | b := make([]byte, length) 56 | rs := make([]byte, length+(length/4)) // perf: avoid read from rand.Reader many times 57 | var i uint8 = 0 58 | 59 | // security note: 60 | // we can't just simply do b[i]=charset[rb%byte(charsetLen)], 61 | // for example, when charsetLen is 52, and rb is [0, 255], 256 = 52 * 4 + 48. 62 | // this will make the first 48 characters more possibly to be generated then others. 63 | // so we have to skip bytes when rb > maxByte 64 | 65 | for { 66 | _, err := io.ReadFull(reader, rs) 67 | if err != nil { 68 | panic("unexpected error happened when reading from bufio.NewReader(crypto/rand.Reader)") 69 | } 70 | for _, rb := range rs { 71 | if rb > byte(maxByte) { 72 | // Skip this number to avoid bias. 73 | continue 74 | } 75 | b[i] = charset[rb%byte(charsetLen)] 76 | i++ 77 | if i == length { 78 | return string(b) 79 | } 80 | } 81 | } 82 | } 83 | 84 | func String(length uint8, charsets ...string) string { 85 | return global.String(length, charsets...) 86 | } 87 | -------------------------------------------------------------------------------- /random/random_test.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test(t *testing.T) { 12 | assert.Len(t, String(32), 32) 13 | r := New() 14 | assert.Regexp(t, regexp.MustCompile("[0-9]+$"), r.String(8, Numeric)) 15 | } 16 | 17 | func TestRandomString(t *testing.T) { 18 | var testCases = []struct { 19 | name string 20 | whenLength uint8 21 | expect string 22 | }{ 23 | { 24 | name: "ok, 16", 25 | whenLength: 16, 26 | }, 27 | { 28 | name: "ok, 32", 29 | whenLength: 32, 30 | }, 31 | } 32 | 33 | for _, tc := range testCases { 34 | t.Run(tc.name, func(t *testing.T) { 35 | uid := String(tc.whenLength, Alphabetic) 36 | assert.Len(t, uid, int(tc.whenLength)) 37 | }) 38 | } 39 | } 40 | 41 | func TestRandomStringBias(t *testing.T) { 42 | t.Parallel() 43 | const slen = 33 44 | const loop = 100000 45 | 46 | counts := make(map[rune]int) 47 | var count int64 48 | 49 | for i := 0; i < loop; i++ { 50 | s := String(slen, Alphabetic) 51 | require.Equal(t, slen, len(s)) 52 | for _, b := range s { 53 | counts[b]++ 54 | count++ 55 | } 56 | } 57 | 58 | require.Equal(t, len(Alphabetic), len(counts)) 59 | 60 | avg := float64(count) / float64(len(counts)) 61 | for k, n := range counts { 62 | diff := float64(n) / avg 63 | if diff < 0.95 || diff > 1.05 { 64 | t.Errorf("Bias on '%c': expected average %f, got %d", k, avg, n) 65 | } 66 | } 67 | } 68 | --------------------------------------------------------------------------------