├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── test.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── README.md ├── crypto.go ├── crypto_test.go ├── date.go ├── date_test.go ├── defaults.go ├── defaults_test.go ├── dict.go ├── dict_test.go ├── doc.go ├── docs ├── _config.yml ├── conversion.md ├── crypto.md ├── date.md ├── defaults.md ├── dicts.md ├── encoding.md ├── flow_control.md ├── index.md ├── integer_slice.md ├── lists.md ├── math.md ├── mathf.md ├── network.md ├── os.md ├── paths.md ├── reflection.md ├── semver.md ├── string_slice.md ├── strings.md ├── url.md └── uuid.md ├── example_test.go ├── flow_control_test.go ├── functions.go ├── functions_linux_test.go ├── functions_test.go ├── functions_windows_test.go ├── go.mod ├── go.sum ├── issue_188_test.go ├── list.go ├── list_test.go ├── network.go ├── network_test.go ├── numeric.go ├── numeric_test.go ├── reflect.go ├── reflect_test.go ├── regex.go ├── regex_test.go ├── semver.go ├── semver_test.go ├── strings.go ├── strings_test.go ├── url.go └── url_test.go /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Tests 3 | permissions: 4 | contents: read 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | go-version: [1.21.x, 1.22.x, 1.23.x] 10 | platform: [ubuntu-latest, macos-latest, windows-latest] 11 | runs-on: ${{ matrix.platform }} 12 | steps: 13 | - name: Install Go 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: ${{ matrix.go-version }} 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | - name: Test 20 | env: 21 | GO111MODULE: on 22 | run: go test -cover . 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | /.glide 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Release 3.3.0 (2024-08-29) 4 | 5 | ### Added 6 | 7 | - #400: added sha512sum function (thanks @itzik-elayev) 8 | 9 | ### Changed 10 | 11 | - #407: Removed duplicate documentation (functions were documentated in 2 places) 12 | - #290: Corrected copy/paster oops in math documentation (thanks @zzhu41) 13 | - #369: Corrected template reference in docs (thanks @chey) 14 | - #375: Added link to URL documenation (thanks @carlpett) 15 | - #406: Updated the mergo dependency which had a breaking change (which was accounted for) 16 | - #376: Fixed documentation error (thanks @jheyduk) 17 | - #404: Updated dependency tree 18 | - #391: Fixed misspelling (thanks @chrishalbert) 19 | - #405: Updated Go versions used in testing 20 | 21 | ## Release 3.2.3 (2022-11-29) 22 | 23 | ### Changed 24 | 25 | - Updated docs (thanks @book987 @aJetHorn @neelayu @pellizzetti @apricote @SaigyoujiYuyuko233 @AlekSi) 26 | - #348: Updated huandu/xstrings which fixed a snake case bug (thanks @yxxhero) 27 | - #353: Updated masterminds/semver which included bug fixes 28 | - #354: Updated golang.org/x/crypto which included bug fixes 29 | 30 | ## Release 3.2.2 (2021-02-04) 31 | 32 | This is a re-release of 3.2.1 to satisfy something with the Go module system. 33 | 34 | ## Release 3.2.1 (2021-02-04) 35 | 36 | ### Changed 37 | 38 | - Upgraded `Masterminds/goutils` to `v1.1.1`. see the [Security Advisory](https://github.com/Masterminds/goutils/security/advisories/GHSA-xg2h-wx96-xgxr) 39 | 40 | ## Release 3.2.0 (2020-12-14) 41 | 42 | ### Added 43 | 44 | - #211: Added randInt function (thanks @kochurovro) 45 | - #223: Added fromJson and mustFromJson functions (thanks @mholt) 46 | - #242: Added a bcrypt function (thanks @robbiet480) 47 | - #253: Added randBytes function (thanks @MikaelSmith) 48 | - #254: Added dig function for dicts (thanks @nyarly) 49 | - #257: Added regexQuoteMeta for quoting regex metadata (thanks @rheaton) 50 | - #261: Added filepath functions osBase, osDir, osExt, osClean, osIsAbs (thanks @zugl) 51 | - #268: Added and and all functions for testing conditions (thanks @phuslu) 52 | - #181: Added float64 arithmetic addf, add1f, subf, divf, mulf, maxf, and minf 53 | (thanks @andrewmostello) 54 | - #265: Added chunk function to split array into smaller arrays (thanks @karelbilek) 55 | - #270: Extend certificate functions to handle non-RSA keys + add support for 56 | ed25519 keys (thanks @misberner) 57 | 58 | ### Changed 59 | 60 | - Removed testing and support for Go 1.12. ed25519 support requires Go 1.13 or newer 61 | - Using semver 3.1.1 and mergo 0.3.11 62 | 63 | ### Fixed 64 | 65 | - #249: Fix htmlDateInZone example (thanks @spawnia) 66 | 67 | NOTE: The dependency github.com/imdario/mergo reverted the breaking change in 68 | 0.3.9 via 0.3.10 release. 69 | 70 | ## Release 3.1.0 (2020-04-16) 71 | 72 | NOTE: The dependency github.com/imdario/mergo made a behavior change in 0.3.9 73 | that impacts sprig functionality. Do not use sprig with a version newer than 0.3.8. 74 | 75 | ### Added 76 | 77 | - #225: Added support for generating htpasswd hash (thanks @rustycl0ck) 78 | - #224: Added duration filter (thanks @frebib) 79 | - #205: Added `seq` function (thanks @thadc23) 80 | 81 | ### Changed 82 | 83 | - #203: Unlambda functions with correct signature (thanks @muesli) 84 | - #236: Updated the license formatting for GitHub display purposes 85 | - #238: Updated package dependency versions. Note, mergo not updated to 0.3.9 86 | as it causes a breaking change for sprig. That issue is tracked at 87 | https://github.com/imdario/mergo/issues/139 88 | 89 | ### Fixed 90 | 91 | - #229: Fix `seq` example in docs (thanks @kalmant) 92 | 93 | ## Release 3.0.2 (2019-12-13) 94 | 95 | ### Fixed 96 | 97 | - #220: Updating to semver v3.0.3 to fix issue with <= ranges 98 | - #218: fix typo elyptical->elliptic in ecdsa key description (thanks @laverya) 99 | 100 | ## Release 3.0.1 (2019-12-08) 101 | 102 | ### Fixed 103 | 104 | - #212: Updated semver fixing broken constraint checking with ^0.0 105 | 106 | ## Release 3.0.0 (2019-10-02) 107 | 108 | ### Added 109 | 110 | - #187: Added durationRound function (thanks @yjp20) 111 | - #189: Added numerous template functions that return errors rather than panic (thanks @nrvnrvn) 112 | - #193: Added toRawJson support (thanks @Dean-Coakley) 113 | - #197: Added get support to dicts (thanks @Dean-Coakley) 114 | 115 | ### Changed 116 | 117 | - #186: Moving dependency management to Go modules 118 | - #186: Updated semver to v3. This has changes in the way ^ is handled 119 | - #194: Updated documentation on merging and how it copies. Added example using deepCopy 120 | - #196: trunc now supports negative values (thanks @Dean-Coakley) 121 | 122 | ## Release 2.22.0 (2019-10-02) 123 | 124 | ### Added 125 | 126 | - #173: Added getHostByName function to resolve dns names to ips (thanks @fcgravalos) 127 | - #195: Added deepCopy function for use with dicts 128 | 129 | ### Changed 130 | 131 | - Updated merge and mergeOverwrite documentation to explain copying and how to 132 | use deepCopy with it 133 | 134 | ## Release 2.21.0 (2019-09-18) 135 | 136 | ### Added 137 | 138 | - #122: Added encryptAES/decryptAES functions (thanks @n0madic) 139 | - #128: Added toDecimal support (thanks @Dean-Coakley) 140 | - #169: Added list contcat (thanks @astorath) 141 | - #174: Added deepEqual function (thanks @bonifaido) 142 | - #170: Added url parse and join functions (thanks @astorath) 143 | 144 | ### Changed 145 | 146 | - #171: Updated glide config for Google UUID to v1 and to add ranges to semver and testify 147 | 148 | ### Fixed 149 | 150 | - #172: Fix semver wildcard example (thanks @piepmatz) 151 | - #175: Fix dateInZone doc example (thanks @s3than) 152 | 153 | ## Release 2.20.0 (2019-06-18) 154 | 155 | ### Added 156 | 157 | - #164: Adding function to get unix epoch for a time (@mattfarina) 158 | - #166: Adding tests for date_in_zone (@mattfarina) 159 | 160 | ### Changed 161 | 162 | - #144: Fix function comments based on best practices from Effective Go (@CodeLingoTeam) 163 | - #150: Handles pointer type for time.Time in "htmlDate" (@mapreal19) 164 | - #161, #157, #160, #153, #158, #156, #155, #159, #152 documentation updates (@badeadan) 165 | 166 | ### Fixed 167 | 168 | ## Release 2.19.0 (2019-03-02) 169 | 170 | IMPORTANT: This release reverts a change from 2.18.0 171 | 172 | In the previous release (2.18), we prematurely merged a partial change to the crypto functions that led to creating two sets of crypto functions (I blame @technosophos -- since that's me). This release rolls back that change, and does what was originally intended: It alters the existing crypto functions to use secure random. 173 | 174 | We debated whether this classifies as a change worthy of major revision, but given the proximity to the last release, we have decided that treating 2.18 as a faulty release is the correct course of action. We apologize for any inconvenience. 175 | 176 | ### Changed 177 | 178 | - Fix substr panic 35fb796 (Alexey igrychev) 179 | - Remove extra period 1eb7729 (Matthew Lorimor) 180 | - Make random string functions use crypto by default 6ceff26 (Matthew Lorimor) 181 | - README edits/fixes/suggestions 08fe136 (Lauri Apple) 182 | 183 | 184 | ## Release 2.18.0 (2019-02-12) 185 | 186 | ### Added 187 | 188 | - Added mergeOverwrite function 189 | - cryptographic functions that use secure random (see fe1de12) 190 | 191 | ### Changed 192 | 193 | - Improve documentation of regexMatch function, resolves #139 90b89ce (Jan Tagscherer) 194 | - Handle has for nil list 9c10885 (Daniel Cohen) 195 | - Document behaviour of mergeOverwrite fe0dbe9 (Lukas Rieder) 196 | - doc: adds missing documentation. 4b871e6 (Fernandez Ludovic) 197 | - Replace outdated goutils imports 01893d2 (Matthew Lorimor) 198 | - Surface crypto secure random strings from goutils fe1de12 (Matthew Lorimor) 199 | - Handle untyped nil values as paramters to string functions 2b2ec8f (Morten Torkildsen) 200 | 201 | ### Fixed 202 | 203 | - Fix dict merge issue and provide mergeOverwrite .dst .src1 to overwrite from src -> dst 4c59c12 (Lukas Rieder) 204 | - Fix substr var names and comments d581f80 (Dean Coakley) 205 | - Fix substr documentation 2737203 (Dean Coakley) 206 | 207 | ## Release 2.17.1 (2019-01-03) 208 | 209 | ### Fixed 210 | 211 | The 2.17.0 release did not have a version pinned for xstrings, which caused compilation failures when xstrings < 1.2 was used. This adds the correct version string to glide.yaml. 212 | 213 | ## Release 2.17.0 (2019-01-03) 214 | 215 | ### Added 216 | 217 | - adds alder32sum function and test 6908fc2 (marshallford) 218 | - Added kebabcase function ca331a1 (Ilyes512) 219 | 220 | ### Changed 221 | 222 | - Update goutils to 1.1.0 4e1125d (Matt Butcher) 223 | 224 | ### Fixed 225 | 226 | - Fix 'has' documentation e3f2a85 (dean-coakley) 227 | - docs(dict): fix typo in pick example dc424f9 (Dustin Specker) 228 | - fixes spelling errors... not sure how that happened 4cf188a (marshallford) 229 | 230 | ## Release 2.16.0 (2018-08-13) 231 | 232 | ### Added 233 | 234 | - add splitn function fccb0b0 (Helgi Þorbjörnsson) 235 | - Add slice func df28ca7 (gongdo) 236 | - Generate serial number a3bdffd (Cody Coons) 237 | - Extract values of dict with values function df39312 (Lawrence Jones) 238 | 239 | ### Changed 240 | 241 | - Modify panic message for list.slice ae38335 (gongdo) 242 | - Minor improvement in code quality - Removed an unreachable piece of code at defaults.go#L26:6 - Resolve formatting issues. 5834241 (Abhishek Kashyap) 243 | - Remove duplicated documentation 1d97af1 (Matthew Fisher) 244 | - Test on go 1.11 49df809 (Helgi Þormar Þorbjörnsson) 245 | 246 | ### Fixed 247 | 248 | - Fix file permissions c5f40b5 (gongdo) 249 | - Fix example for buildCustomCert 7779e0d (Tin Lam) 250 | 251 | ## Release 2.15.0 (2018-04-02) 252 | 253 | ### Added 254 | 255 | - #68 and #69: Add json helpers to docs (thanks @arunvelsriram) 256 | - #66: Add ternary function (thanks @binoculars) 257 | - #67: Allow keys function to take multiple dicts (thanks @binoculars) 258 | - #89: Added sha1sum to crypto function (thanks @benkeil) 259 | - #81: Allow customizing Root CA that used by genSignedCert (thanks @chenzhiwei) 260 | - #92: Add travis testing for go 1.10 261 | - #93: Adding appveyor config for windows testing 262 | 263 | ### Changed 264 | 265 | - #90: Updating to more recent dependencies 266 | - #73: replace satori/go.uuid with google/uuid (thanks @petterw) 267 | 268 | ### Fixed 269 | 270 | - #76: Fixed documentation typos (thanks @Thiht) 271 | - Fixed rounding issue on the `ago` function. Note, the removes support for Go 1.8 and older 272 | 273 | ## Release 2.14.1 (2017-12-01) 274 | 275 | ### Fixed 276 | 277 | - #60: Fix typo in function name documentation (thanks @neil-ca-moore) 278 | - #61: Removing line with {{ due to blocking github pages genertion 279 | - #64: Update the list functions to handle int, string, and other slices for compatibility 280 | 281 | ## Release 2.14.0 (2017-10-06) 282 | 283 | This new version of Sprig adds a set of functions for generating and working with SSL certificates. 284 | 285 | - `genCA` generates an SSL Certificate Authority 286 | - `genSelfSignedCert` generates an SSL self-signed certificate 287 | - `genSignedCert` generates an SSL certificate and key based on a given CA 288 | 289 | ## Release 2.13.0 (2017-09-18) 290 | 291 | This release adds new functions, including: 292 | 293 | - `regexMatch`, `regexFindAll`, `regexFind`, `regexReplaceAll`, `regexReplaceAllLiteral`, and `regexSplit` to work with regular expressions 294 | - `floor`, `ceil`, and `round` math functions 295 | - `toDate` converts a string to a date 296 | - `nindent` is just like `indent` but also prepends a new line 297 | - `ago` returns the time from `time.Now` 298 | 299 | ### Added 300 | 301 | - #40: Added basic regex functionality (thanks @alanquillin) 302 | - #41: Added ceil floor and round functions (thanks @alanquillin) 303 | - #48: Added toDate function (thanks @andreynering) 304 | - #50: Added nindent function (thanks @binoculars) 305 | - #46: Added ago function (thanks @slayer) 306 | 307 | ### Changed 308 | 309 | - #51: Updated godocs to include new string functions (thanks @curtisallen) 310 | - #49: Added ability to merge multiple dicts (thanks @binoculars) 311 | 312 | ## Release 2.12.0 (2017-05-17) 313 | 314 | - `snakecase`, `camelcase`, and `shuffle` are three new string functions 315 | - `fail` allows you to bail out of a template render when conditions are not met 316 | 317 | ## Release 2.11.0 (2017-05-02) 318 | 319 | - Added `toJson` and `toPrettyJson` 320 | - Added `merge` 321 | - Refactored documentation 322 | 323 | ## Release 2.10.0 (2017-03-15) 324 | 325 | - Added `semver` and `semverCompare` for Semantic Versions 326 | - `list` replaces `tuple` 327 | - Fixed issue with `join` 328 | - Added `first`, `last`, `initial`, `rest`, `prepend`, `append`, `toString`, `toStrings`, `sortAlpha`, `reverse`, `coalesce`, `pluck`, `pick`, `compact`, `keys`, `omit`, `uniq`, `has`, `without` 329 | 330 | ## Release 2.9.0 (2017-02-23) 331 | 332 | - Added `splitList` to split a list 333 | - Added crypto functions of `genPrivateKey` and `derivePassword` 334 | 335 | ## Release 2.8.0 (2016-12-21) 336 | 337 | - Added access to several path functions (`base`, `dir`, `clean`, `ext`, and `abs`) 338 | - Added functions for _mutating_ dictionaries (`set`, `unset`, `hasKey`) 339 | 340 | ## Release 2.7.0 (2016-12-01) 341 | 342 | - Added `sha256sum` to generate a hash of an input 343 | - Added functions to convert a numeric or string to `int`, `int64`, `float64` 344 | 345 | ## Release 2.6.0 (2016-10-03) 346 | 347 | - Added a `uuidv4` template function for generating UUIDs inside of a template. 348 | 349 | ## Release 2.5.0 (2016-08-19) 350 | 351 | - New `trimSuffix`, `trimPrefix`, `hasSuffix`, and `hasPrefix` functions 352 | - New aliases have been added for a few functions that didn't follow the naming conventions (`trimAll` and `abbrevBoth`) 353 | - `trimall` and `abbrevboth` (notice the case) are deprecated and will be removed in 3.0.0 354 | 355 | ## Release 2.4.0 (2016-08-16) 356 | 357 | - Adds two functions: `until` and `untilStep` 358 | 359 | ## Release 2.3.0 (2016-06-21) 360 | 361 | - cat: Concatenate strings with whitespace separators. 362 | - replace: Replace parts of a string: `replace " " "-" "Me First"` renders "Me-First" 363 | - plural: Format plurals: `len "foo" | plural "one foo" "many foos"` renders "many foos" 364 | - indent: Indent blocks of text in a way that is sensitive to "\n" characters. 365 | 366 | ## Release 2.2.0 (2016-04-21) 367 | 368 | - Added a `genPrivateKey` function (Thanks @bacongobbler) 369 | 370 | ## Release 2.1.0 (2016-03-30) 371 | 372 | - `default` now prints the default value when it does not receive a value down the pipeline. It is much safer now to do `{{.Foo | default "bar"}}`. 373 | - Added accessors for "hermetic" functions. These return only functions that, when given the same input, produce the same output. 374 | 375 | ## Release 2.0.0 (2016-03-29) 376 | 377 | Because we switched from `int` to `int64` as the return value for all integer math functions, the library's major version number has been incremented. 378 | 379 | - `min` complements `max` (formerly `biggest`) 380 | - `empty` indicates that a value is the empty value for its type 381 | - `tuple` creates a tuple inside of a template: `{{$t := tuple "a", "b" "c"}}` 382 | - `dict` creates a dictionary inside of a template `{{$d := dict "key1" "val1" "key2" "val2"}}` 383 | - Date formatters have been added for HTML dates (as used in `date` input fields) 384 | - Integer math functions can convert from a number of types, including `string` (via `strconv.ParseInt`). 385 | 386 | ## Release 1.2.0 (2016-02-01) 387 | 388 | - Added quote and squote 389 | - Added b32enc and b32dec 390 | - add now takes varargs 391 | - biggest now takes varargs 392 | 393 | ## Release 1.1.0 (2015-12-29) 394 | 395 | - Added #4: Added contains function. strings.Contains, but with the arguments 396 | switched to simplify common pipelines. (thanks krancour) 397 | - Added Travis-CI testing support 398 | 399 | ## Release 1.0.0 (2015-12-23) 400 | 401 | - Initial release 402 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013-2020 Masterminds 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | @echo "==> Running tests" 4 | GO111MODULE=on go test -v 5 | 6 | .PHONY: test-cover 7 | test-cover: 8 | @echo "==> Running Tests with coverage" 9 | GO111MODULE=on go test -cover . 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sprig: Template functions for Go templates 2 | 3 | [![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/Masterminds/sprig/v3) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/Masterminds/sprig)](https://goreportcard.com/report/github.com/Masterminds/sprig) 5 | [![Stability: Sustained](https://masterminds.github.io/stability/sustained.svg)](https://masterminds.github.io/stability/sustained.html) 6 | [![](https://github.com/Masterminds/sprig/workflows/Tests/badge.svg)](https://github.com/Masterminds/sprig/actions) 7 | 8 | The Go language comes with a [built-in template 9 | language](http://golang.org/pkg/text/template/), but not 10 | very many template functions. Sprig is a library that provides more than 100 commonly 11 | used template functions. 12 | 13 | It is inspired by the template functions found in 14 | [Twig](http://twig.sensiolabs.org/documentation) and in various 15 | JavaScript libraries, such as [underscore.js](http://underscorejs.org/). 16 | 17 | ## IMPORTANT NOTES 18 | 19 | Sprig leverages [mergo](https://github.com/imdario/mergo) to handle merges. In 20 | its v0.3.9 release, there was a behavior change that impacts merging template 21 | functions in sprig. It is currently recommended to use v0.3.10 or later of that package. 22 | Using v0.3.9 will cause sprig tests to fail. 23 | 24 | ## Package Versions 25 | 26 | There are two active major versions of the `sprig` package. 27 | 28 | * v3 is currently stable release series on the `master` branch. The Go API should 29 | remain compatible with v2, the current stable version. Behavior change behind 30 | some functions is the reason for the new major version. 31 | * v2 is the previous stable release series. It has been more than three years since 32 | the initial release of v2. You can read the documentation and see the code 33 | on the [release-2](https://github.com/Masterminds/sprig/tree/release-2) branch. 34 | Bug fixes to this major version will continue for some time. 35 | 36 | ## Usage 37 | 38 | **Template developers**: Please use Sprig's [function documentation](http://masterminds.github.io/sprig/) for 39 | detailed instructions and code snippets for the >100 template functions available. 40 | 41 | **Go developers**: If you'd like to include Sprig as a library in your program, 42 | our API documentation is available [at GoDoc.org](http://godoc.org/github.com/Masterminds/sprig). 43 | 44 | For standard usage, read on. 45 | 46 | ### Load the Sprig library 47 | 48 | To load the Sprig `FuncMap`: 49 | 50 | ```go 51 | 52 | import ( 53 | "github.com/Masterminds/sprig/v3" 54 | "html/template" 55 | ) 56 | 57 | // This example illustrates that the FuncMap *must* be set before the 58 | // templates themselves are loaded. 59 | tpl := template.Must( 60 | template.New("base").Funcs(sprig.FuncMap()).ParseGlob("*.html") 61 | ) 62 | 63 | 64 | ``` 65 | 66 | ### Calling the functions inside of templates 67 | 68 | By convention, all functions are lowercase. This seems to follow the Go 69 | idiom for template functions (as opposed to template methods, which are 70 | TitleCase). For example, this: 71 | 72 | ``` 73 | {{ "hello!" | upper | repeat 5 }} 74 | ``` 75 | 76 | produces this: 77 | 78 | ``` 79 | HELLO!HELLO!HELLO!HELLO!HELLO! 80 | ``` 81 | 82 | ## Principles Driving Our Function Selection 83 | 84 | We followed these principles to decide which functions to add and how to implement them: 85 | 86 | - Use template functions to build layout. The following 87 | types of operations are within the domain of template functions: 88 | - Formatting 89 | - Layout 90 | - Simple type conversions 91 | - Utilities that assist in handling common formatting and layout needs (e.g. arithmetic) 92 | - Template functions should not return errors unless there is no way to print 93 | a sensible value. For example, converting a string to an integer should not 94 | produce an error if conversion fails. Instead, it should display a default 95 | value. 96 | - Simple math is necessary for grid layouts, pagers, and so on. Complex math 97 | (anything other than arithmetic) should be done outside of templates. 98 | - Template functions only deal with the data passed into them. They never retrieve 99 | data from a source. 100 | - Finally, do not override core Go template functions. 101 | -------------------------------------------------------------------------------- /crypto_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/base64" 6 | "encoding/pem" 7 | "fmt" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | bcrypt_lib "golang.org/x/crypto/bcrypt" 13 | ) 14 | 15 | const ( 16 | beginCertificate = "-----BEGIN CERTIFICATE-----" 17 | endCertificate = "-----END CERTIFICATE-----" 18 | ) 19 | 20 | var ( 21 | // fastCertKeyAlgos is the list of private key algorithms that are supported for certificate use, and 22 | // are fast to generate. 23 | fastCertKeyAlgos = []string{ 24 | "ecdsa", 25 | "ed25519", 26 | } 27 | ) 28 | 29 | func TestSha512Sum(t *testing.T) { 30 | tpl := `{{"abc" | sha512sum}}` 31 | if err := runt(tpl, "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"); err != nil { 32 | t.Error(err) 33 | } 34 | } 35 | 36 | func TestSha256Sum(t *testing.T) { 37 | tpl := `{{"abc" | sha256sum}}` 38 | if err := runt(tpl, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); err != nil { 39 | t.Error(err) 40 | } 41 | } 42 | 43 | func TestSha1Sum(t *testing.T) { 44 | tpl := `{{"abc" | sha1sum}}` 45 | if err := runt(tpl, "a9993e364706816aba3e25717850c26c9cd0d89d"); err != nil { 46 | t.Error(err) 47 | } 48 | } 49 | 50 | func TestAdler32Sum(t *testing.T) { 51 | tpl := `{{"abc" | adler32sum}}` 52 | if err := runt(tpl, "38600999"); err != nil { 53 | t.Error(err) 54 | } 55 | } 56 | 57 | func TestBcrypt(t *testing.T) { 58 | out, err := runRaw(`{{"abc" | bcrypt}}`, nil) 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | if bcrypt_lib.CompareHashAndPassword([]byte(out), []byte("abc")) != nil { 63 | t.Error("Generated hash is not the equivalent for password:", "abc") 64 | } 65 | } 66 | 67 | type HtpasswdCred struct { 68 | Username string 69 | Password string 70 | HashAlgorithm HashAlgorithm 71 | Valid bool 72 | } 73 | 74 | func TestHtpasswd(t *testing.T) { 75 | expectations := []HtpasswdCred{ 76 | {Username: "myUser", Password: "myPassword", HashAlgorithm: HashBCrypt, Valid: true}, 77 | {Username: "special'o79Cv_*qFe,) year: 126 | return strconv.FormatUint(u/year, 10) + "y" 127 | case u > month: 128 | return strconv.FormatUint(u/month, 10) + "mo" 129 | case u > day: 130 | return strconv.FormatUint(u/day, 10) + "d" 131 | case u > hour: 132 | return strconv.FormatUint(u/hour, 10) + "h" 133 | case u > minute: 134 | return strconv.FormatUint(u/minute, 10) + "m" 135 | case u > second: 136 | return strconv.FormatUint(u/second, 10) + "s" 137 | } 138 | return "0s" 139 | } 140 | 141 | func toDate(fmt, str string) time.Time { 142 | t, _ := time.ParseInLocation(fmt, str, time.Local) 143 | return t 144 | } 145 | 146 | func mustToDate(fmt, str string) (time.Time, error) { 147 | return time.ParseInLocation(fmt, str, time.Local) 148 | } 149 | 150 | func unixEpoch(date time.Time) string { 151 | return strconv.FormatInt(date.Unix(), 10) 152 | } 153 | -------------------------------------------------------------------------------- /date_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestHtmlDate(t *testing.T) { 9 | t.Skip() 10 | tpl := `{{ htmlDate 0}}` 11 | if err := runt(tpl, "1970-01-01"); err != nil { 12 | t.Error(err) 13 | } 14 | } 15 | 16 | func TestAgo(t *testing.T) { 17 | tpl := "{{ ago .Time }}" 18 | if err := runtv(tpl, "2m5s", map[string]interface{}{"Time": time.Now().Add(-125 * time.Second)}); err != nil { 19 | t.Error(err) 20 | } 21 | 22 | if err := runtv(tpl, "2h34m17s", map[string]interface{}{"Time": time.Now().Add(-(2*3600 + 34*60 + 17) * time.Second)}); err != nil { 23 | t.Error(err) 24 | } 25 | 26 | if err := runtv(tpl, "-5s", map[string]interface{}{"Time": time.Now().Add(5 * time.Second)}); err != nil { 27 | t.Error(err) 28 | } 29 | } 30 | 31 | func TestToDate(t *testing.T) { 32 | tpl := `{{toDate "2006-01-02" "2017-12-31" | date "02/01/2006"}}` 33 | if err := runt(tpl, "31/12/2017"); err != nil { 34 | t.Error(err) 35 | } 36 | } 37 | 38 | func TestUnixEpoch(t *testing.T) { 39 | tm, err := time.Parse("02 Jan 06 15:04:05 MST", "13 Jun 19 20:39:39 GMT") 40 | if err != nil { 41 | t.Error(err) 42 | } 43 | tpl := `{{unixEpoch .Time}}` 44 | 45 | if err = runtv(tpl, "1560458379", map[string]interface{}{"Time": tm}); err != nil { 46 | t.Error(err) 47 | } 48 | } 49 | 50 | func TestDateInZone(t *testing.T) { 51 | tm, err := time.Parse("02 Jan 06 15:04:05 MST", "13 Jun 19 20:39:39 GMT") 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | tpl := `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "UTC" }}` 56 | 57 | // Test time.Time input 58 | if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": tm}); err != nil { 59 | t.Error(err) 60 | } 61 | 62 | // Test pointer to time.Time input 63 | if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": &tm}); err != nil { 64 | t.Error(err) 65 | } 66 | 67 | // Test no time input. This should be close enough to time.Now() we can test 68 | loc, _ := time.LoadLocation("UTC") 69 | if err = runtv(tpl, time.Now().In(loc).Format("02 Jan 06 15:04 -0700"), map[string]interface{}{"Time": ""}); err != nil { 70 | t.Error(err) 71 | } 72 | 73 | // Test unix timestamp as int64 74 | if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int64(1560458379)}); err != nil { 75 | t.Error(err) 76 | } 77 | 78 | // Test unix timestamp as int32 79 | if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int32(1560458379)}); err != nil { 80 | t.Error(err) 81 | } 82 | 83 | // Test unix timestamp as int 84 | if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int(1560458379)}); err != nil { 85 | t.Error(err) 86 | } 87 | 88 | // Test case of invalid timezone 89 | tpl = `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "foobar" }}` 90 | if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": tm}); err != nil { 91 | t.Error(err) 92 | } 93 | } 94 | 95 | func TestDuration(t *testing.T) { 96 | tpl := "{{ duration .Secs }}" 97 | if err := runtv(tpl, "1m1s", map[string]interface{}{"Secs": "61"}); err != nil { 98 | t.Error(err) 99 | } 100 | if err := runtv(tpl, "1h0m0s", map[string]interface{}{"Secs": "3600"}); err != nil { 101 | t.Error(err) 102 | } 103 | // 1d2h3m4s but go is opinionated 104 | if err := runtv(tpl, "26h3m4s", map[string]interface{}{"Secs": "93784"}); err != nil { 105 | t.Error(err) 106 | } 107 | } 108 | 109 | func TestDurationRound(t *testing.T) { 110 | tpl := "{{ durationRound .Time }}" 111 | if err := runtv(tpl, "2h", map[string]interface{}{"Time": "2h5s"}); err != nil { 112 | t.Error(err) 113 | } 114 | if err := runtv(tpl, "1d", map[string]interface{}{"Time": "24h5s"}); err != nil { 115 | t.Error(err) 116 | } 117 | if err := runtv(tpl, "3mo", map[string]interface{}{"Time": "2400h5s"}); err != nil { 118 | t.Error(err) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /defaults.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "math/rand" 7 | "reflect" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func init() { 13 | rand.Seed(time.Now().UnixNano()) 14 | } 15 | 16 | // dfault checks whether `given` is set, and returns default if not set. 17 | // 18 | // This returns `d` if `given` appears not to be set, and `given` otherwise. 19 | // 20 | // For numeric types 0 is unset. 21 | // For strings, maps, arrays, and slices, len() = 0 is considered unset. 22 | // For bool, false is unset. 23 | // Structs are never considered unset. 24 | // 25 | // For everything else, including pointers, a nil value is unset. 26 | func dfault(d interface{}, given ...interface{}) interface{} { 27 | 28 | if empty(given) || empty(given[0]) { 29 | return d 30 | } 31 | return given[0] 32 | } 33 | 34 | // empty returns true if the given value has the zero value for its type. 35 | func empty(given interface{}) bool { 36 | g := reflect.ValueOf(given) 37 | if !g.IsValid() { 38 | return true 39 | } 40 | 41 | // Basically adapted from text/template.isTrue 42 | switch g.Kind() { 43 | default: 44 | return g.IsNil() 45 | case reflect.Array, reflect.Slice, reflect.Map, reflect.String: 46 | return g.Len() == 0 47 | case reflect.Bool: 48 | return !g.Bool() 49 | case reflect.Complex64, reflect.Complex128: 50 | return g.Complex() == 0 51 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 52 | return g.Int() == 0 53 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 54 | return g.Uint() == 0 55 | case reflect.Float32, reflect.Float64: 56 | return g.Float() == 0 57 | case reflect.Struct: 58 | return false 59 | } 60 | } 61 | 62 | // coalesce returns the first non-empty value. 63 | func coalesce(v ...interface{}) interface{} { 64 | for _, val := range v { 65 | if !empty(val) { 66 | return val 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | // all returns true if empty(x) is false for all values x in the list. 73 | // If the list is empty, return true. 74 | func all(v ...interface{}) bool { 75 | for _, val := range v { 76 | if empty(val) { 77 | return false 78 | } 79 | } 80 | return true 81 | } 82 | 83 | // any returns true if empty(x) is false for any x in the list. 84 | // If the list is empty, return false. 85 | func any(v ...interface{}) bool { 86 | for _, val := range v { 87 | if !empty(val) { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | 94 | // fromJson decodes JSON into a structured value, ignoring errors. 95 | func fromJson(v string) interface{} { 96 | output, _ := mustFromJson(v) 97 | return output 98 | } 99 | 100 | // mustFromJson decodes JSON into a structured value, returning errors. 101 | func mustFromJson(v string) (interface{}, error) { 102 | var output interface{} 103 | err := json.Unmarshal([]byte(v), &output) 104 | return output, err 105 | } 106 | 107 | // toJson encodes an item into a JSON string 108 | func toJson(v interface{}) string { 109 | output, _ := json.Marshal(v) 110 | return string(output) 111 | } 112 | 113 | func mustToJson(v interface{}) (string, error) { 114 | output, err := json.Marshal(v) 115 | if err != nil { 116 | return "", err 117 | } 118 | return string(output), nil 119 | } 120 | 121 | // toPrettyJson encodes an item into a pretty (indented) JSON string 122 | func toPrettyJson(v interface{}) string { 123 | output, _ := json.MarshalIndent(v, "", " ") 124 | return string(output) 125 | } 126 | 127 | func mustToPrettyJson(v interface{}) (string, error) { 128 | output, err := json.MarshalIndent(v, "", " ") 129 | if err != nil { 130 | return "", err 131 | } 132 | return string(output), nil 133 | } 134 | 135 | // toRawJson encodes an item into a JSON string with no escaping of HTML characters. 136 | func toRawJson(v interface{}) string { 137 | output, err := mustToRawJson(v) 138 | if err != nil { 139 | panic(err) 140 | } 141 | return string(output) 142 | } 143 | 144 | // mustToRawJson encodes an item into a JSON string with no escaping of HTML characters. 145 | func mustToRawJson(v interface{}) (string, error) { 146 | buf := new(bytes.Buffer) 147 | enc := json.NewEncoder(buf) 148 | enc.SetEscapeHTML(false) 149 | err := enc.Encode(&v) 150 | if err != nil { 151 | return "", err 152 | } 153 | return strings.TrimSuffix(buf.String(), "\n"), nil 154 | } 155 | 156 | // ternary returns the first value if the last value is true, otherwise returns the second value. 157 | func ternary(vt interface{}, vf interface{}, v bool) interface{} { 158 | if v { 159 | return vt 160 | } 161 | 162 | return vf 163 | } 164 | -------------------------------------------------------------------------------- /defaults_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDefault(t *testing.T) { 10 | tpl := `{{"" | default "foo"}}` 11 | if err := runt(tpl, "foo"); err != nil { 12 | t.Error(err) 13 | } 14 | tpl = `{{default "foo" 234}}` 15 | if err := runt(tpl, "234"); err != nil { 16 | t.Error(err) 17 | } 18 | tpl = `{{default "foo" 2.34}}` 19 | if err := runt(tpl, "2.34"); err != nil { 20 | t.Error(err) 21 | } 22 | 23 | tpl = `{{ .Nothing | default "123" }}` 24 | if err := runt(tpl, "123"); err != nil { 25 | t.Error(err) 26 | } 27 | tpl = `{{ default "123" }}` 28 | if err := runt(tpl, "123"); err != nil { 29 | t.Error(err) 30 | } 31 | } 32 | 33 | func TestEmpty(t *testing.T) { 34 | tpl := `{{if empty 1}}1{{else}}0{{end}}` 35 | if err := runt(tpl, "0"); err != nil { 36 | t.Error(err) 37 | } 38 | 39 | tpl = `{{if empty 0}}1{{else}}0{{end}}` 40 | if err := runt(tpl, "1"); err != nil { 41 | t.Error(err) 42 | } 43 | tpl = `{{if empty ""}}1{{else}}0{{end}}` 44 | if err := runt(tpl, "1"); err != nil { 45 | t.Error(err) 46 | } 47 | tpl = `{{if empty 0.0}}1{{else}}0{{end}}` 48 | if err := runt(tpl, "1"); err != nil { 49 | t.Error(err) 50 | } 51 | tpl = `{{if empty false}}1{{else}}0{{end}}` 52 | if err := runt(tpl, "1"); err != nil { 53 | t.Error(err) 54 | } 55 | 56 | dict := map[string]interface{}{"top": map[string]interface{}{}} 57 | tpl = `{{if empty .top.NoSuchThing}}1{{else}}0{{end}}` 58 | if err := runtv(tpl, "1", dict); err != nil { 59 | t.Error(err) 60 | } 61 | tpl = `{{if empty .bottom.NoSuchThing}}1{{else}}0{{end}}` 62 | if err := runtv(tpl, "1", dict); err != nil { 63 | t.Error(err) 64 | } 65 | } 66 | 67 | func TestCoalesce(t *testing.T) { 68 | tests := map[string]string{ 69 | `{{ coalesce 1 }}`: "1", 70 | `{{ coalesce "" 0 nil 2 }}`: "2", 71 | `{{ $two := 2 }}{{ coalesce "" 0 nil $two }}`: "2", 72 | `{{ $two := 2 }}{{ coalesce "" $two 0 0 0 }}`: "2", 73 | `{{ $two := 2 }}{{ coalesce "" $two 3 4 5 }}`: "2", 74 | `{{ coalesce }}`: "", 75 | } 76 | for tpl, expect := range tests { 77 | assert.NoError(t, runt(tpl, expect)) 78 | } 79 | 80 | dict := map[string]interface{}{"top": map[string]interface{}{}} 81 | tpl := `{{ coalesce .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` 82 | if err := runtv(tpl, "airplane", dict); err != nil { 83 | t.Error(err) 84 | } 85 | } 86 | 87 | func TestAll(t *testing.T) { 88 | tests := map[string]string{ 89 | `{{ all 1 }}`: "true", 90 | `{{ all "" 0 nil 2 }}`: "false", 91 | `{{ $two := 2 }}{{ all "" 0 nil $two }}`: "false", 92 | `{{ $two := 2 }}{{ all "" $two 0 0 0 }}`: "false", 93 | `{{ $two := 2 }}{{ all "" $two 3 4 5 }}`: "false", 94 | `{{ all }}`: "true", 95 | } 96 | for tpl, expect := range tests { 97 | assert.NoError(t, runt(tpl, expect)) 98 | } 99 | 100 | dict := map[string]interface{}{"top": map[string]interface{}{}} 101 | tpl := `{{ all .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` 102 | if err := runtv(tpl, "false", dict); err != nil { 103 | t.Error(err) 104 | } 105 | } 106 | 107 | func TestAny(t *testing.T) { 108 | tests := map[string]string{ 109 | `{{ any 1 }}`: "true", 110 | `{{ any "" 0 nil 2 }}`: "true", 111 | `{{ $two := 2 }}{{ any "" 0 nil $two }}`: "true", 112 | `{{ $two := 2 }}{{ any "" $two 3 4 5 }}`: "true", 113 | `{{ $zero := 0 }}{{ any "" $zero 0 0 0 }}`: "false", 114 | `{{ any }}`: "false", 115 | } 116 | for tpl, expect := range tests { 117 | assert.NoError(t, runt(tpl, expect)) 118 | } 119 | 120 | dict := map[string]interface{}{"top": map[string]interface{}{}} 121 | tpl := `{{ any .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` 122 | if err := runtv(tpl, "true", dict); err != nil { 123 | t.Error(err) 124 | } 125 | } 126 | 127 | func TestFromJson(t *testing.T) { 128 | dict := map[string]interface{}{"Input": `{"foo": 55}`} 129 | 130 | tpl := `{{.Input | fromJson}}` 131 | expected := `map[foo:55]` 132 | if err := runtv(tpl, expected, dict); err != nil { 133 | t.Error(err) 134 | } 135 | 136 | tpl = `{{(.Input | fromJson).foo}}` 137 | expected = `55` 138 | if err := runtv(tpl, expected, dict); err != nil { 139 | t.Error(err) 140 | } 141 | } 142 | 143 | func TestToJson(t *testing.T) { 144 | dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42}} 145 | 146 | tpl := `{{.Top | toJson}}` 147 | expected := `{"bool":true,"number":42,"string":"test"}` 148 | if err := runtv(tpl, expected, dict); err != nil { 149 | t.Error(err) 150 | } 151 | } 152 | 153 | func TestToPrettyJson(t *testing.T) { 154 | dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42}} 155 | tpl := `{{.Top | toPrettyJson}}` 156 | expected := `{ 157 | "bool": true, 158 | "number": 42, 159 | "string": "test" 160 | }` 161 | if err := runtv(tpl, expected, dict); err != nil { 162 | t.Error(err) 163 | } 164 | } 165 | 166 | func TestToRawJson(t *testing.T) { 167 | dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42, "html": ""}} 168 | tpl := `{{.Top | toRawJson}}` 169 | expected := `{"bool":true,"html":"","number":42,"string":"test"}` 170 | 171 | if err := runtv(tpl, expected, dict); err != nil { 172 | t.Error(err) 173 | } 174 | } 175 | 176 | func TestTernary(t *testing.T) { 177 | tpl := `{{true | ternary "foo" "bar"}}` 178 | if err := runt(tpl, "foo"); err != nil { 179 | t.Error(err) 180 | } 181 | 182 | tpl = `{{ternary "foo" "bar" true}}` 183 | if err := runt(tpl, "foo"); err != nil { 184 | t.Error(err) 185 | } 186 | 187 | tpl = `{{false | ternary "foo" "bar"}}` 188 | if err := runt(tpl, "bar"); err != nil { 189 | t.Error(err) 190 | } 191 | 192 | tpl = `{{ternary "foo" "bar" false}}` 193 | if err := runt(tpl, "bar"); err != nil { 194 | t.Error(err) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /dict.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "dario.cat/mergo" 5 | "github.com/mitchellh/copystructure" 6 | ) 7 | 8 | func get(d map[string]interface{}, key string) interface{} { 9 | if val, ok := d[key]; ok { 10 | return val 11 | } 12 | return "" 13 | } 14 | 15 | func set(d map[string]interface{}, key string, value interface{}) map[string]interface{} { 16 | d[key] = value 17 | return d 18 | } 19 | 20 | func unset(d map[string]interface{}, key string) map[string]interface{} { 21 | delete(d, key) 22 | return d 23 | } 24 | 25 | func hasKey(d map[string]interface{}, key string) bool { 26 | _, ok := d[key] 27 | return ok 28 | } 29 | 30 | func pluck(key string, d ...map[string]interface{}) []interface{} { 31 | res := []interface{}{} 32 | for _, dict := range d { 33 | if val, ok := dict[key]; ok { 34 | res = append(res, val) 35 | } 36 | } 37 | return res 38 | } 39 | 40 | func keys(dicts ...map[string]interface{}) []string { 41 | k := []string{} 42 | for _, dict := range dicts { 43 | for key := range dict { 44 | k = append(k, key) 45 | } 46 | } 47 | return k 48 | } 49 | 50 | func pick(dict map[string]interface{}, keys ...string) map[string]interface{} { 51 | res := map[string]interface{}{} 52 | for _, k := range keys { 53 | if v, ok := dict[k]; ok { 54 | res[k] = v 55 | } 56 | } 57 | return res 58 | } 59 | 60 | func omit(dict map[string]interface{}, keys ...string) map[string]interface{} { 61 | res := map[string]interface{}{} 62 | 63 | omit := make(map[string]bool, len(keys)) 64 | for _, k := range keys { 65 | omit[k] = true 66 | } 67 | 68 | for k, v := range dict { 69 | if _, ok := omit[k]; !ok { 70 | res[k] = v 71 | } 72 | } 73 | return res 74 | } 75 | 76 | func dict(v ...interface{}) map[string]interface{} { 77 | dict := map[string]interface{}{} 78 | lenv := len(v) 79 | for i := 0; i < lenv; i += 2 { 80 | key := strval(v[i]) 81 | if i+1 >= lenv { 82 | dict[key] = "" 83 | continue 84 | } 85 | dict[key] = v[i+1] 86 | } 87 | return dict 88 | } 89 | 90 | func merge(dst map[string]interface{}, srcs ...map[string]interface{}) interface{} { 91 | for _, src := range srcs { 92 | if err := mergo.Merge(&dst, src); err != nil { 93 | // Swallow errors inside of a template. 94 | return "" 95 | } 96 | } 97 | return dst 98 | } 99 | 100 | func mustMerge(dst map[string]interface{}, srcs ...map[string]interface{}) (interface{}, error) { 101 | for _, src := range srcs { 102 | if err := mergo.Merge(&dst, src); err != nil { 103 | return nil, err 104 | } 105 | } 106 | return dst, nil 107 | } 108 | 109 | func mergeOverwrite(dst map[string]interface{}, srcs ...map[string]interface{}) interface{} { 110 | for _, src := range srcs { 111 | if err := mergo.MergeWithOverwrite(&dst, src); err != nil { 112 | // Swallow errors inside of a template. 113 | return "" 114 | } 115 | } 116 | return dst 117 | } 118 | 119 | func mustMergeOverwrite(dst map[string]interface{}, srcs ...map[string]interface{}) (interface{}, error) { 120 | for _, src := range srcs { 121 | if err := mergo.MergeWithOverwrite(&dst, src); err != nil { 122 | return nil, err 123 | } 124 | } 125 | return dst, nil 126 | } 127 | 128 | func values(dict map[string]interface{}) []interface{} { 129 | values := []interface{}{} 130 | for _, value := range dict { 131 | values = append(values, value) 132 | } 133 | 134 | return values 135 | } 136 | 137 | func deepCopy(i interface{}) interface{} { 138 | c, err := mustDeepCopy(i) 139 | if err != nil { 140 | panic("deepCopy error: " + err.Error()) 141 | } 142 | 143 | return c 144 | } 145 | 146 | func mustDeepCopy(i interface{}) (interface{}, error) { 147 | return copystructure.Copy(i) 148 | } 149 | 150 | func dig(ps ...interface{}) (interface{}, error) { 151 | if len(ps) < 3 { 152 | panic("dig needs at least three arguments") 153 | } 154 | dict := ps[len(ps)-1].(map[string]interface{}) 155 | def := ps[len(ps)-2] 156 | ks := make([]string, len(ps)-2) 157 | for i := 0; i < len(ks); i++ { 158 | ks[i] = ps[i].(string) 159 | } 160 | 161 | return digFromDict(dict, def, ks) 162 | } 163 | 164 | func digFromDict(dict map[string]interface{}, d interface{}, ks []string) (interface{}, error) { 165 | k, ns := ks[0], ks[1:len(ks)] 166 | step, has := dict[k] 167 | if !has { 168 | return d, nil 169 | } 170 | if len(ns) == 0 { 171 | return step, nil 172 | } 173 | return digFromDict(step.(map[string]interface{}), d, ns) 174 | } 175 | -------------------------------------------------------------------------------- /dict_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDict(t *testing.T) { 11 | tpl := `{{$d := dict 1 2 "three" "four" 5}}{{range $k, $v := $d}}{{$k}}{{$v}}{{end}}` 12 | out, err := runRaw(tpl, nil) 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | if len(out) != 12 { 17 | t.Errorf("Expected length 12, got %d", len(out)) 18 | } 19 | // dict does not guarantee ordering because it is backed by a map. 20 | if !strings.Contains(out, "12") { 21 | t.Error("Expected grouping 12") 22 | } 23 | if !strings.Contains(out, "threefour") { 24 | t.Error("Expected grouping threefour") 25 | } 26 | if !strings.Contains(out, "5") { 27 | t.Error("Expected 5") 28 | } 29 | tpl = `{{$t := dict "I" "shot" "the" "albatross"}}{{$t.the}} {{$t.I}}` 30 | if err := runt(tpl, "albatross shot"); err != nil { 31 | t.Error(err) 32 | } 33 | } 34 | 35 | func TestUnset(t *testing.T) { 36 | tpl := `{{- $d := dict "one" 1 "two" 222222 -}} 37 | {{- $_ := unset $d "two" -}} 38 | {{- range $k, $v := $d}}{{$k}}{{$v}}{{- end -}} 39 | ` 40 | 41 | expect := "one1" 42 | if err := runt(tpl, expect); err != nil { 43 | t.Error(err) 44 | } 45 | } 46 | func TestHasKey(t *testing.T) { 47 | tpl := `{{- $d := dict "one" 1 "two" 222222 -}} 48 | {{- if hasKey $d "one" -}}1{{- end -}} 49 | ` 50 | 51 | expect := "1" 52 | if err := runt(tpl, expect); err != nil { 53 | t.Error(err) 54 | } 55 | } 56 | 57 | func TestPluck(t *testing.T) { 58 | tpl := ` 59 | {{- $d := dict "one" 1 "two" 222222 -}} 60 | {{- $d2 := dict "one" 1 "two" 33333 -}} 61 | {{- $d3 := dict "one" 1 -}} 62 | {{- $d4 := dict "one" 1 "two" 4444 -}} 63 | {{- pluck "two" $d $d2 $d3 $d4 -}} 64 | ` 65 | 66 | expect := "[222222 33333 4444]" 67 | if err := runt(tpl, expect); err != nil { 68 | t.Error(err) 69 | } 70 | } 71 | 72 | func TestKeys(t *testing.T) { 73 | tests := map[string]string{ 74 | `{{ dict "foo" 1 "bar" 2 | keys | sortAlpha }}`: "[bar foo]", 75 | `{{ dict | keys }}`: "[]", 76 | `{{ keys (dict "foo" 1) (dict "bar" 2) (dict "bar" 3) | uniq | sortAlpha }}`: "[bar foo]", 77 | } 78 | for tpl, expect := range tests { 79 | if err := runt(tpl, expect); err != nil { 80 | t.Error(err) 81 | } 82 | } 83 | } 84 | 85 | func TestPick(t *testing.T) { 86 | tests := map[string]string{ 87 | `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "two" | len -}}`: "1", 88 | `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "two" -}}`: "map[two:222222]", 89 | `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "one" "two" | len -}}`: "2", 90 | `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "one" "two" "three" | len -}}`: "2", 91 | `{{- $d := dict }}{{ pick $d "two" | len -}}`: "0", 92 | } 93 | for tpl, expect := range tests { 94 | if err := runt(tpl, expect); err != nil { 95 | t.Error(err) 96 | } 97 | } 98 | } 99 | func TestOmit(t *testing.T) { 100 | tests := map[string]string{ 101 | `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" | len -}}`: "1", 102 | `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" -}}`: "map[two:222222]", 103 | `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" "two" | len -}}`: "0", 104 | `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "two" "three" | len -}}`: "1", 105 | `{{- $d := dict }}{{ omit $d "two" | len -}}`: "0", 106 | } 107 | for tpl, expect := range tests { 108 | if err := runt(tpl, expect); err != nil { 109 | t.Error(err) 110 | } 111 | } 112 | } 113 | 114 | func TestGet(t *testing.T) { 115 | tests := map[string]string{ 116 | `{{- $d := dict "one" 1 }}{{ get $d "one" -}}`: "1", 117 | `{{- $d := dict "one" 1 "two" "2" }}{{ get $d "two" -}}`: "2", 118 | `{{- $d := dict }}{{ get $d "two" -}}`: "", 119 | } 120 | for tpl, expect := range tests { 121 | if err := runt(tpl, expect); err != nil { 122 | t.Error(err) 123 | } 124 | } 125 | } 126 | 127 | func TestSet(t *testing.T) { 128 | tpl := `{{- $d := dict "one" 1 "two" 222222 -}} 129 | {{- $_ := set $d "two" 2 -}} 130 | {{- $_ := set $d "three" 3 -}} 131 | {{- if hasKey $d "one" -}}{{$d.one}}{{- end -}} 132 | {{- if hasKey $d "two" -}}{{$d.two}}{{- end -}} 133 | {{- if hasKey $d "three" -}}{{$d.three}}{{- end -}} 134 | ` 135 | 136 | expect := "123" 137 | if err := runt(tpl, expect); err != nil { 138 | t.Error(err) 139 | } 140 | } 141 | 142 | func TestMerge(t *testing.T) { 143 | dict := map[string]interface{}{ 144 | "src2": map[string]interface{}{ 145 | "h": 10, 146 | "i": "i", 147 | "j": "j", 148 | }, 149 | "src1": map[string]interface{}{ 150 | "a": 1, 151 | "b": 2, 152 | "d": map[string]interface{}{ 153 | "e": "four", 154 | }, 155 | "g": []int{6, 7}, 156 | "i": "aye", 157 | "j": "jay", 158 | "k": map[string]interface{}{ 159 | "l": false, 160 | }, 161 | }, 162 | "dst": map[string]interface{}{ 163 | "a": "one", 164 | "c": 3, 165 | "d": map[string]interface{}{ 166 | "f": 5, 167 | }, 168 | "g": []int{8, 9}, 169 | "i": "eye", 170 | "k": map[string]interface{}{ 171 | "l": true, 172 | }, 173 | }, 174 | } 175 | tpl := `{{merge .dst .src1 .src2}}` 176 | _, err := runRaw(tpl, dict) 177 | if err != nil { 178 | t.Error(err) 179 | } 180 | expected := map[string]interface{}{ 181 | "a": "one", // key overridden 182 | "b": 2, // merged from src1 183 | "c": 3, // merged from dst 184 | "d": map[string]interface{}{ // deep merge 185 | "e": "four", 186 | "f": 5, 187 | }, 188 | "g": []int{8, 9}, // overridden - arrays are not merged 189 | "h": 10, // merged from src2 190 | "i": "eye", // overridden twice 191 | "j": "jay", // overridden and merged 192 | "k": map[string]interface{}{ 193 | "l": true, // overridden 194 | }, 195 | } 196 | assert.Equal(t, expected, dict["dst"]) 197 | } 198 | 199 | func TestMergeOverwrite(t *testing.T) { 200 | dict := map[string]interface{}{ 201 | "src2": map[string]interface{}{ 202 | "h": 10, 203 | "i": "i", 204 | "j": "j", 205 | }, 206 | "src1": map[string]interface{}{ 207 | "a": 1, 208 | "b": 2, 209 | "d": map[string]interface{}{ 210 | "e": "four", 211 | }, 212 | "g": []int{6, 7}, 213 | "i": "aye", 214 | "j": "jay", 215 | "k": map[string]interface{}{ 216 | "l": false, 217 | }, 218 | }, 219 | "dst": map[string]interface{}{ 220 | "a": "one", 221 | "c": 3, 222 | "d": map[string]interface{}{ 223 | "f": 5, 224 | }, 225 | "g": []int{8, 9}, 226 | "i": "eye", 227 | "k": map[string]interface{}{ 228 | "l": true, 229 | }, 230 | }, 231 | } 232 | tpl := `{{mergeOverwrite .dst .src1 .src2}}` 233 | _, err := runRaw(tpl, dict) 234 | if err != nil { 235 | t.Error(err) 236 | } 237 | expected := map[string]interface{}{ 238 | "a": 1, // key overwritten from src1 239 | "b": 2, // merged from src1 240 | "c": 3, // merged from dst 241 | "d": map[string]interface{}{ // deep merge 242 | "e": "four", 243 | "f": 5, 244 | }, 245 | "g": []int{6, 7}, // overwritten src1 wins 246 | "h": 10, // merged from src2 247 | "i": "i", // overwritten twice src2 wins 248 | "j": "j", // overwritten twice src2 wins 249 | "k": map[string]interface{}{ // deep merge 250 | "l": false, // overwritten src1 wins 251 | }, 252 | } 253 | assert.Equal(t, expected, dict["dst"]) 254 | } 255 | 256 | func TestValues(t *testing.T) { 257 | tests := map[string]string{ 258 | `{{- $d := dict "a" 1 "b" 2 }}{{ values $d | sortAlpha | join "," }}`: "1,2", 259 | `{{- $d := dict "a" "first" "b" 2 }}{{ values $d | sortAlpha | join "," }}`: "2,first", 260 | } 261 | 262 | for tpl, expect := range tests { 263 | if err := runt(tpl, expect); err != nil { 264 | t.Error(err) 265 | } 266 | } 267 | } 268 | 269 | func TestDeepCopy(t *testing.T) { 270 | tests := map[string]string{ 271 | `{{- $d := dict "a" 1 "b" 2 | deepCopy }}{{ values $d | sortAlpha | join "," }}`: "1,2", 272 | `{{- $d := dict "a" 1 "b" 2 | deepCopy }}{{ keys $d | sortAlpha | join "," }}`: "a,b", 273 | `{{- $one := dict "foo" (dict "bar" "baz") "qux" true -}}{{ deepCopy $one }}`: "map[foo:map[bar:baz] qux:true]", 274 | } 275 | 276 | for tpl, expect := range tests { 277 | if err := runt(tpl, expect); err != nil { 278 | t.Error(err) 279 | } 280 | } 281 | } 282 | 283 | func TestMustDeepCopy(t *testing.T) { 284 | tests := map[string]string{ 285 | `{{- $d := dict "a" 1 "b" 2 | mustDeepCopy }}{{ values $d | sortAlpha | join "," }}`: "1,2", 286 | `{{- $d := dict "a" 1 "b" 2 | mustDeepCopy }}{{ keys $d | sortAlpha | join "," }}`: "a,b", 287 | `{{- $one := dict "foo" (dict "bar" "baz") "qux" true -}}{{ mustDeepCopy $one }}`: "map[foo:map[bar:baz] qux:true]", 288 | } 289 | 290 | for tpl, expect := range tests { 291 | if err := runt(tpl, expect); err != nil { 292 | t.Error(err) 293 | } 294 | } 295 | } 296 | 297 | func TestDig(t *testing.T) { 298 | tests := map[string]string{ 299 | `{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "c" "" $d }}`: "1", 300 | `{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "z" "2" $d }}`: "2", 301 | `{{ dict "a" 1 | dig "a" "" }}`: "1", 302 | `{{ dict "a" 1 | dig "z" "2" }}`: "2", 303 | } 304 | 305 | for tpl, expect := range tests { 306 | if err := runt(tpl, expect); err != nil { 307 | t.Error(err) 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package sprig provides template functions for Go. 3 | 4 | This package contains a number of utility functions for working with data 5 | inside of Go `html/template` and `text/template` files. 6 | 7 | To add these functions, use the `template.Funcs()` method: 8 | 9 | t := template.New("foo").Funcs(sprig.FuncMap()) 10 | 11 | Note that you should add the function map before you parse any template files. 12 | 13 | In several cases, Sprig reverses the order of arguments from the way they 14 | appear in the standard library. This is to make it easier to pipe 15 | arguments into functions. 16 | 17 | See http://masterminds.github.io/sprig/ for more detailed documentation on each of the available functions. 18 | */ 19 | package sprig 20 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/conversion.md: -------------------------------------------------------------------------------- 1 | # Type Conversion Functions 2 | 3 | The following type conversion functions are provided by Sprig: 4 | 5 | - `atoi`: Convert a string to an integer. 6 | - `float64`: Convert to a `float64`. 7 | - `int`: Convert to an `int` at the system's width. 8 | - `int64`: Convert to an `int64`. 9 | - `toDecimal`: Convert a unix octal to a `int64`. 10 | - `toString`: Convert to a string. 11 | - `toStrings`: Convert a list, slice, or array to a list of strings. 12 | 13 | Only `atoi` requires that the input be a specific type. The others will attempt 14 | to convert from any type to the destination type. For example, `int64` can convert 15 | floats to ints, and it can also convert strings to ints. 16 | 17 | ## toStrings 18 | 19 | Given a list-like collection, produce a slice of strings. 20 | 21 | ``` 22 | list 1 2 3 | toStrings 23 | ``` 24 | 25 | The above converts `1` to `"1"`, `2` to `"2"`, and so on, and then returns 26 | them as a list. 27 | 28 | ## toDecimal 29 | 30 | Given a unix octal permission, produce a decimal. 31 | 32 | ``` 33 | "0777" | toDecimal 34 | ``` 35 | 36 | The above converts `0777` to `511` and returns the value as an int64. 37 | -------------------------------------------------------------------------------- /docs/crypto.md: -------------------------------------------------------------------------------- 1 | # Cryptographic and Security Functions 2 | 3 | Sprig provides a couple of advanced cryptographic functions. 4 | 5 | ## sha1sum 6 | 7 | The `sha1sum` function receives a string, and computes it's SHA1 digest. 8 | 9 | ``` 10 | sha1sum "Hello world!" 11 | ``` 12 | 13 | ## sha256sum 14 | 15 | The `sha256sum` function receives a string, and computes it's SHA256 digest. 16 | 17 | ``` 18 | sha256sum "Hello world!" 19 | ``` 20 | 21 | The above will compute the SHA 256 sum in an "ASCII armored" format that is 22 | safe to print. 23 | 24 | ## sha512sum 25 | 26 | The `sha512sum` function receives a string, and computes it's SHA512 digest. 27 | 28 | ``` 29 | sha512sum "Hello world!" 30 | ``` 31 | 32 | The above will compute the SHA 512 sum in an "ASCII armored" format that is 33 | safe to print. 34 | 35 | ## adler32sum 36 | 37 | The `adler32sum` function receives a string, and computes its Adler-32 checksum. 38 | 39 | ``` 40 | adler32sum "Hello world!" 41 | ``` 42 | ## bcrypt 43 | 44 | The `bcrypt` function receives a string, and generates its `bcrypt` hash. 45 | 46 | ``` 47 | bcrypt "myPassword" 48 | ``` 49 | 50 | ## htpasswd 51 | 52 | The `htpasswd` function takes a `username`, a `password`, and a `hashAlgorithm` and generates a `bcrypt` (recommended) or a base64 encoded and prefixed `sha` hash of the password. `hashAlgorithm` is optional and defaults to `bcrypt`. The result can be used for basic authentication on an [Apache HTTP Server](https://httpd.apache.org/docs/2.4/misc/password_encryptions.html#basic). 53 | 54 | ``` 55 | htpasswd "myUser" "myPassword" ["bcrypt"|"sha"] 56 | ``` 57 | 58 | Note that it is insecure to store the password directly in the template. 59 | 60 | ## randBytes 61 | 62 | The `randBytes` function accepts a count `N` and generates a cryptographically 63 | secure (uses ```crypto/rand```) random sequence of `N` bytes. The sequence is 64 | returned as a base64 encoded string. 65 | 66 | ``` 67 | randBytes 24 68 | ``` 69 | 70 | ## derivePassword 71 | 72 | The `derivePassword` function can be used to derive a specific password based on 73 | some shared "master password" constraints. The algorithm for this is 74 | [well specified](https://spectre.app/spectre-algorithm.pdf). 75 | 76 | ``` 77 | derivePassword 1 "long" "password" "user" "example.com" 78 | ``` 79 | 80 | Note that it is considered insecure to store the parts directly in the template. 81 | 82 | ## genPrivateKey 83 | 84 | The `genPrivateKey` function generates a new private key encoded into a PEM 85 | block. 86 | 87 | It takes one of the values for its first param: 88 | 89 | - `ecdsa`: Generate an elliptic curve DSA key (P256) 90 | - `dsa`: Generate a DSA key (L2048N256) 91 | - `rsa`: Generate an RSA 4096 key 92 | - `ed25519`: Generate an Ed25519 key 93 | 94 | ## buildCustomCert 95 | 96 | The `buildCustomCert` function allows customizing the certificate. 97 | 98 | It takes the following string parameters: 99 | 100 | - A base64 encoded PEM format certificate 101 | - A base64 encoded PEM format private key 102 | 103 | It returns a certificate object with the following attributes: 104 | 105 | - `Cert`: A PEM-encoded certificate 106 | - `Key`: A PEM-encoded private key 107 | 108 | Example: 109 | 110 | ``` 111 | $ca := buildCustomCert "base64-encoded-ca-crt" "base64-encoded-ca-key" 112 | ``` 113 | 114 | Note that the returned object can be passed to the `genSignedCert` function 115 | to sign a certificate using this CA. 116 | 117 | ## genCA 118 | 119 | The `genCA` function generates a new, self-signed x509 certificate authority using a 120 | 2048-bit RSA private key. 121 | 122 | It takes the following parameters: 123 | 124 | - Subject's common name (cn) 125 | - Cert validity duration in days 126 | 127 | It returns an object with the following attributes: 128 | 129 | - `Cert`: A PEM-encoded certificate 130 | - `Key`: A PEM-encoded private key 131 | 132 | Example: 133 | 134 | ``` 135 | $ca := genCA "foo-ca" 365 136 | ``` 137 | 138 | Note that the returned object can be passed to the `genSignedCert` function 139 | to sign a certificate using this CA. 140 | 141 | ## genCAWithKey 142 | 143 | The `genCAWithKey` function generates a new, self-signed x509 certificate authority using a 144 | given private key. 145 | 146 | It takes the following parameters: 147 | 148 | - Subject's common name (cn) 149 | - Cert validity duration in days 150 | - Private key (PEM-encoded); DSA keys are not supported 151 | 152 | It returns an object with the following attributes: 153 | 154 | - `Cert`: A PEM-encoded certificate 155 | - `Key`: A PEM-encoded private key 156 | 157 | Example: 158 | 159 | ``` 160 | $ca := genCAWithKey "foo-ca" 365 (genPrivateKey "rsa") 161 | ``` 162 | 163 | Note that the returned object can be passed to the `genSignedCert` function 164 | to sign a certificate using this CA. 165 | 166 | ## genSelfSignedCert 167 | 168 | The `genSelfSignedCert` function generates a new, self-signed x509 certificate using a 169 | 2048-bit RSA private key. 170 | 171 | It takes the following parameters: 172 | 173 | - Subject's common name (cn) 174 | - Optional list of IPs; may be nil 175 | - Optional list of alternate DNS names; may be nil 176 | - Cert validity duration in days 177 | 178 | It returns an object with the following attributes: 179 | 180 | - `Cert`: A PEM-encoded certificate 181 | - `Key`: A PEM-encoded private key 182 | 183 | Example: 184 | 185 | ``` 186 | $cert := genSelfSignedCert "foo.com" (list "10.0.0.1" "10.0.0.2") (list "bar.com" "bat.com") 365 187 | ``` 188 | 189 | ## genSelfSignedCertWithKey 190 | 191 | The `genSelfSignedCertWithKey` function generates a new, self-signed x509 certificate using a 192 | given private key. 193 | 194 | It takes the following parameters: 195 | 196 | - Subject's common name (cn) 197 | - Optional list of IPs; may be nil 198 | - Optional list of alternate DNS names; may be nil 199 | - Cert validity duration in days 200 | - Private key (PEM-encoded); DSA keys are not supported 201 | 202 | It returns an object with the following attributes: 203 | 204 | - `Cert`: A PEM-encoded certificate 205 | - `Key`: A PEM-encoded private key 206 | 207 | Example: 208 | 209 | ``` 210 | $cert := genSelfSignedCertWithKey "foo.com" (list "10.0.0.1" "10.0.0.2") (list "bar.com" "bat.com") 365 (genPrivateKey "ecdsa") 211 | ``` 212 | 213 | ## genSignedCert 214 | 215 | The `genSignedCert` function generates a new, x509 certificate signed by the 216 | specified CA, using a 2048-bit RSA private key. 217 | 218 | It takes the following parameters: 219 | 220 | - Subject's common name (cn) 221 | - Optional list of IPs; may be nil 222 | - Optional list of alternate DNS names; may be nil 223 | - Cert validity duration in days 224 | - CA (see `genCA`) 225 | 226 | Example: 227 | 228 | ``` 229 | $ca := genCA "foo-ca" 365 230 | $cert := genSignedCert "foo.com" (list "10.0.0.1" "10.0.0.2") (list "bar.com" "bat.com") 365 $ca 231 | ``` 232 | 233 | ## genSignedCertWithKey 234 | 235 | The `genSignedCertWithKey` function generates a new, x509 certificate signed by the 236 | specified CA, using a given private key. 237 | 238 | It takes the following parameters: 239 | 240 | - Subject's common name (cn) 241 | - Optional list of IPs; may be nil 242 | - Optional list of alternate DNS names; may be nil 243 | - Cert validity duration in days 244 | - CA (see `genCA`) 245 | - Private key (PEM-encoded); DSA keys are not supported 246 | 247 | Example: 248 | 249 | ``` 250 | $ca := genCA "foo-ca" 365 251 | $cert := genSignedCert "foo.com" (list "10.0.0.1" "10.0.0.2") (list "bar.com" "bat.com") 365 $ca (genPrivateKey "ed25519") 252 | ``` 253 | 254 | ## encryptAES 255 | 256 | The `encryptAES` function encrypts text with AES-256 CBC and returns a base64 encoded string. 257 | 258 | ``` 259 | encryptAES "secretkey" "plaintext" 260 | ``` 261 | 262 | ## decryptAES 263 | 264 | The `decryptAES` function receives a base64 string encoded by the AES-256 CBC 265 | algorithm and returns the decoded text. 266 | 267 | ``` 268 | "30tEfhuJSVRhpG97XCuWgz2okj7L8vQ1s6V9zVUPeDQ=" | decryptAES "secretkey" 269 | ``` 270 | -------------------------------------------------------------------------------- /docs/date.md: -------------------------------------------------------------------------------- 1 | # Date Functions 2 | 3 | ## now 4 | 5 | The current date/time. Use this in conjunction with other date functions. 6 | 7 | ## ago 8 | 9 | The `ago` function returns duration from time.Now in seconds resolution. 10 | 11 | ``` 12 | ago .CreatedAt" 13 | ``` 14 | 15 | returns in `time.Duration` String() format 16 | 17 | ``` 18 | 2h34m7s 19 | ``` 20 | 21 | ## date 22 | 23 | The `date` function formats a date. 24 | 25 | Format the date to YEAR-MONTH-DAY: 26 | 27 | ``` 28 | now | date "2006-01-02" 29 | ``` 30 | 31 | Date formatting in Go is a [little bit different](https://pauladamsmith.com/blog/2011/05/go_time.html). 32 | 33 | In short, take this as the base date: 34 | 35 | ``` 36 | Mon Jan 2 15:04:05 MST 2006 37 | ``` 38 | 39 | Write it in the format you want. Above, `2006-01-02` is the same date, but 40 | in the format we want. 41 | 42 | ## dateInZone 43 | 44 | Same as `date`, but with a timezone. 45 | 46 | ``` 47 | dateInZone "2006-01-02" (now) "UTC" 48 | ``` 49 | 50 | ## duration 51 | 52 | Formats a given amount of seconds as a `time.Duration`. 53 | 54 | This returns 1m35s 55 | 56 | ``` 57 | duration "95" 58 | ``` 59 | 60 | ## durationRound 61 | 62 | Rounds a given duration to the most significant unit. Strings and `time.Duration` 63 | gets parsed as a duration, while a `time.Time` is calculated as the duration since. 64 | 65 | This return 2h 66 | 67 | ``` 68 | durationRound "2h10m5s" 69 | ``` 70 | 71 | This returns 3mo 72 | 73 | ``` 74 | durationRound "2400h10m5s" 75 | ``` 76 | 77 | ## unixEpoch 78 | 79 | Returns the seconds since the unix epoch for a `time.Time`. 80 | 81 | ``` 82 | now | unixEpoch 83 | ``` 84 | 85 | ## dateModify, mustDateModify 86 | 87 | The `dateModify` takes a modification and a date and returns the timestamp. 88 | 89 | Subtract an hour and thirty minutes from the current time: 90 | 91 | ``` 92 | now | date_modify "-1.5h" 93 | ``` 94 | 95 | If the modification format is wrong `dateModify` will return the date unmodified. `mustDateModify` will return an error otherwise. 96 | 97 | ## htmlDate 98 | 99 | The `htmlDate` function formats a date for inserting into an HTML date picker 100 | input field. 101 | 102 | ``` 103 | now | htmlDate 104 | ``` 105 | 106 | ## htmlDateInZone 107 | 108 | Same as htmlDate, but with a timezone. 109 | 110 | ``` 111 | htmlDateInZone (now) "UTC" 112 | ``` 113 | 114 | ## toDate, mustToDate 115 | 116 | `toDate` converts a string to a date. The first argument is the date layout and 117 | the second the date string. If the string can't be convert it returns the zero 118 | value. 119 | `mustToDate` will return an error in case the string cannot be converted. 120 | 121 | This is useful when you want to convert a string date to another format 122 | (using pipe). The example below converts "2017-12-31" to "31/12/2017". 123 | 124 | ``` 125 | toDate "2006-01-02" "2017-12-31" | date "02/01/2006" 126 | ``` 127 | -------------------------------------------------------------------------------- /docs/defaults.md: -------------------------------------------------------------------------------- 1 | # Default Functions 2 | 3 | Sprig provides tools for setting default values for templates. 4 | 5 | ## default 6 | 7 | To set a simple default value, use `default`: 8 | 9 | ``` 10 | default "foo" .Bar 11 | ``` 12 | 13 | In the above, if `.Bar` evaluates to a non-empty value, it will be used. But if 14 | it is empty, `foo` will be returned instead. 15 | 16 | The definition of "empty" depends on type: 17 | 18 | - Numeric: 0 19 | - String: "" 20 | - Lists: `[]` 21 | - Dicts: `{}` 22 | - Boolean: `false` 23 | - And always `nil` (aka null) 24 | 25 | For structs, there is no definition of empty, so a struct will never return the 26 | default. 27 | 28 | ## empty 29 | 30 | The `empty` function returns `true` if the given value is considered empty, and 31 | `false` otherwise. The empty values are listed in the `default` section. 32 | 33 | ``` 34 | empty .Foo 35 | ``` 36 | 37 | Note that in Go template conditionals, emptiness is calculated for you. Thus, 38 | you rarely need `if empty .Foo`. Instead, just use `if .Foo`. 39 | 40 | ## coalesce 41 | 42 | The `coalesce` function takes a list of values and returns the first non-empty 43 | one. 44 | 45 | ``` 46 | coalesce 0 1 2 47 | ``` 48 | 49 | The above returns `1`. 50 | 51 | This function is useful for scanning through multiple variables or values: 52 | 53 | ``` 54 | coalesce .name .parent.name "Matt" 55 | ``` 56 | 57 | The above will first check to see if `.name` is empty. If it is not, it will return 58 | that value. If it _is_ empty, `coalesce` will evaluate `.parent.name` for emptiness. 59 | Finally, if both `.name` and `.parent.name` are empty, it will return `Matt`. 60 | 61 | ## all 62 | 63 | The `all` function takes a list of values and returns true if all values are non-empty. 64 | 65 | ``` 66 | all 0 1 2 67 | ``` 68 | 69 | The above returns `false`. 70 | 71 | This function is useful for evaluating multiple conditions of variables or values: 72 | 73 | ``` 74 | all (eq .Request.TLS.Version 0x0304) (.Request.ProtoAtLeast 2 0) (eq .Request.Method "POST") 75 | ``` 76 | 77 | The above will check http.Request is POST with tls 1.3 and http/2. 78 | 79 | ## any 80 | 81 | The `any` function takes a list of values and returns true if any value is non-empty. 82 | 83 | ``` 84 | any 0 1 2 85 | ``` 86 | 87 | The above returns `true`. 88 | 89 | This function is useful for evaluating multiple conditions of variables or values: 90 | 91 | ``` 92 | any (eq .Request.Method "GET") (eq .Request.Method "POST") (eq .Request.Method "OPTIONS") 93 | ``` 94 | 95 | The above will check http.Request method is one of GET/POST/OPTIONS. 96 | 97 | ## fromJson, mustFromJson 98 | 99 | `fromJson` decodes a JSON document into a structure. If the input cannot be decoded as JSON the function will return an empty string. 100 | `mustFromJson` will return an error in case the JSON is invalid. 101 | 102 | ``` 103 | fromJson "{\"foo\": 55}" 104 | ``` 105 | 106 | ## toJson, mustToJson 107 | 108 | The `toJson` function encodes an item into a JSON string. If the item cannot be converted to JSON the function will return an empty string. 109 | `mustToJson` will return an error in case the item cannot be encoded in JSON. 110 | 111 | ``` 112 | toJson .Item 113 | ``` 114 | 115 | The above returns JSON string representation of `.Item`. 116 | 117 | ## toPrettyJson, mustToPrettyJson 118 | 119 | The `toPrettyJson` function encodes an item into a pretty (indented) JSON string. 120 | 121 | ``` 122 | toPrettyJson .Item 123 | ``` 124 | 125 | The above returns indented JSON string representation of `.Item`. 126 | 127 | ## toRawJson, mustToRawJson 128 | 129 | The `toRawJson` function encodes an item into JSON string with HTML characters unescaped. 130 | 131 | ``` 132 | toRawJson .Item 133 | ``` 134 | 135 | The above returns unescaped JSON string representation of `.Item`. 136 | 137 | ## ternary 138 | 139 | The `ternary` function takes two values, and a test value. If the test value is 140 | true, the first value will be returned. If the test value is empty, the second 141 | value will be returned. This is similar to the c ternary operator. 142 | 143 | ### true test value 144 | 145 | ``` 146 | ternary "foo" "bar" true 147 | ``` 148 | 149 | or 150 | 151 | ``` 152 | true | ternary "foo" "bar" 153 | ``` 154 | 155 | The above returns `"foo"`. 156 | 157 | ### false test value 158 | 159 | ``` 160 | ternary "foo" "bar" false 161 | ``` 162 | 163 | or 164 | 165 | ``` 166 | false | ternary "foo" "bar" 167 | ``` 168 | 169 | The above returns `"bar"`. 170 | -------------------------------------------------------------------------------- /docs/dicts.md: -------------------------------------------------------------------------------- 1 | # Dictionaries and Dict Functions 2 | 3 | Sprig provides a key/value storage type called a `dict` (short for "dictionary", 4 | as in Python). A `dict` is an _unorder_ type. 5 | 6 | The key to a dictionary **must be a string**. However, the value can be any 7 | type, even another `dict` or `list`. 8 | 9 | Unlike `list`s, `dict`s are not immutable. The `set` and `unset` functions will 10 | modify the contents of a dictionary. 11 | 12 | ## dict 13 | 14 | Creating dictionaries is done by calling the `dict` function and passing it a 15 | list of pairs. 16 | 17 | The following creates a dictionary with three items: 18 | 19 | ``` 20 | $myDict := dict "name1" "value1" "name2" "value2" "name3" "value 3" 21 | ``` 22 | 23 | ## get 24 | 25 | Given a map and a key, get the value from the map. 26 | 27 | ``` 28 | get $myDict "name1" 29 | ``` 30 | 31 | The above returns `"value1"` 32 | 33 | Note that if the key is not found, this operation will simply return `""`. No error 34 | will be generated. 35 | 36 | ## set 37 | 38 | Use `set` to add a new key/value pair to a dictionary. 39 | 40 | ``` 41 | $_ := set $myDict "name4" "value4" 42 | ``` 43 | 44 | Note that `set` _returns the dictionary_ (a requirement of Go template functions), 45 | so you may need to trap the value as done above with the `$_` assignment. 46 | 47 | ## unset 48 | 49 | Given a map and a key, delete the key from the map. 50 | 51 | ``` 52 | $_ := unset $myDict "name4" 53 | ``` 54 | 55 | As with `set`, this returns the dictionary. 56 | 57 | Note that if the key is not found, this operation will simply return. No error 58 | will be generated. 59 | 60 | ## hasKey 61 | 62 | The `hasKey` function returns `true` if the given dict contains the given key. 63 | 64 | ``` 65 | hasKey $myDict "name1" 66 | ``` 67 | 68 | If the key is not found, this returns `false`. 69 | 70 | ## pluck 71 | 72 | The `pluck` function makes it possible to give one key and multiple maps, and 73 | get a list of all of the matches: 74 | 75 | ``` 76 | pluck "name1" $myDict $myOtherDict 77 | ``` 78 | 79 | The above will return a `list` containing every found value (`[value1 otherValue1]`). 80 | 81 | If the give key is _not found_ in a map, that map will not have an item in the 82 | list (and the length of the returned list will be less than the number of dicts 83 | in the call to `pluck`. 84 | 85 | If the key is _found_ but the value is an empty value, that value will be 86 | inserted. 87 | 88 | A common idiom in Sprig templates is to uses `pluck... | first` to get the first 89 | matching key out of a collection of dictionaries. 90 | 91 | ## dig 92 | 93 | The `dig` function traverses a nested set of dicts, selecting keys from a list 94 | of values. It returns a default value if any of the keys are not found at the 95 | associated dict. 96 | 97 | ``` 98 | dig "user" "role" "humanName" "guest" $dict 99 | ``` 100 | 101 | Given a dict structured like 102 | ``` 103 | { 104 | user: { 105 | role: { 106 | humanName: "curator" 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | the above would return `"curator"`. If the dict lacked even a `user` field, 113 | the result would be `"guest"`. 114 | 115 | Dig can be very useful in cases where you'd like to avoid guard clauses, 116 | especially since Go's template package's `and` doesn't shortcut. For instance 117 | `and a.maybeNil a.maybeNil.iNeedThis` will always evaluate 118 | `a.maybeNil.iNeedThis`, and panic if `a` lacks a `maybeNil` field.) 119 | 120 | `dig` accepts its dict argument last in order to support pipelining. For instance: 121 | ``` 122 | merge a b c | dig "one" "two" "three" "" 123 | ``` 124 | 125 | ## merge, mustMerge 126 | 127 | Merge two or more dictionaries into one, giving precedence to the dest dictionary: 128 | 129 | ``` 130 | $newdict := merge $dest $source1 $source2 131 | ``` 132 | 133 | This is a deep merge operation but not a deep copy operation. Nested objects that 134 | are merged are the same instance on both dicts. If you want a deep copy along 135 | with the merge than use the `deepCopy` function along with merging. For example, 136 | 137 | ``` 138 | deepCopy $source | merge $dest 139 | ``` 140 | 141 | `mustMerge` will return an error in case of unsuccessful merge. 142 | 143 | ## mergeOverwrite, mustMergeOverwrite 144 | 145 | Merge two or more dictionaries into one, giving precedence from **right to left**, effectively 146 | overwriting values in the dest dictionary: 147 | 148 | Given: 149 | 150 | ``` 151 | dst: 152 | default: default 153 | overwrite: me 154 | key: true 155 | 156 | src: 157 | overwrite: overwritten 158 | key: false 159 | ``` 160 | 161 | will result in: 162 | 163 | ``` 164 | newdict: 165 | default: default 166 | overwrite: overwritten 167 | key: false 168 | ``` 169 | 170 | ``` 171 | $newdict := mergeOverwrite $dest $source1 $source2 172 | ``` 173 | 174 | This is a deep merge operation but not a deep copy operation. Nested objects that 175 | are merged are the same instance on both dicts. If you want a deep copy along 176 | with the merge than use the `deepCopy` function along with merging. For example, 177 | 178 | ``` 179 | deepCopy $source | mergeOverwrite $dest 180 | ``` 181 | 182 | `mustMergeOverwrite` will return an error in case of unsuccessful merge. 183 | 184 | ## keys 185 | 186 | The `keys` function will return a `list` of all of the keys in one or more `dict` 187 | types. Since a dictionary is _unordered_, the keys will not be in a predictable order. 188 | They can be sorted with `sortAlpha`. 189 | 190 | ``` 191 | keys $myDict | sortAlpha 192 | ``` 193 | 194 | When supplying multiple dictionaries, the keys will be concatenated. Use the `uniq` 195 | function along with `sortAlpha` to get a unqiue, sorted list of keys. 196 | 197 | ``` 198 | keys $myDict $myOtherDict | uniq | sortAlpha 199 | ``` 200 | 201 | ## pick 202 | 203 | The `pick` function selects just the given keys out of a dictionary, creating a 204 | new `dict`. 205 | 206 | ``` 207 | $new := pick $myDict "name1" "name2" 208 | ``` 209 | 210 | The above returns `{name1: value1, name2: value2}` 211 | 212 | ## omit 213 | 214 | The `omit` function is similar to `pick`, except it returns a new `dict` with all 215 | the keys that _do not_ match the given keys. 216 | 217 | ``` 218 | $new := omit $myDict "name1" "name3" 219 | ``` 220 | 221 | The above returns `{name2: value2}` 222 | 223 | ## values 224 | 225 | The `values` function is similar to `keys`, except it returns a new `list` with 226 | all the values of the source `dict` (only one dictionary is supported). 227 | 228 | ``` 229 | $vals := values $myDict 230 | ``` 231 | 232 | The above returns `list["value1", "value2", "value 3"]`. Note that the `values` 233 | function gives no guarantees about the result ordering- if you care about this, 234 | then use `sortAlpha`. 235 | 236 | ## deepCopy, mustDeepCopy 237 | 238 | The `deepCopy` and `mustDeepCopy` functions takes a value and makes a deep copy 239 | of the value. This includes dicts and other structures. `deepCopy` panics 240 | when there is a problem while `mustDeepCopy` returns an error to the template 241 | system when there is an error. 242 | 243 | ``` 244 | dict "a" 1 "b" 2 | deepCopy 245 | ``` 246 | 247 | ## A Note on Dict Internals 248 | 249 | A `dict` is implemented in Go as a `map[string]interface{}`. Go developers can 250 | pass `map[string]interface{}` values into the context to make them available 251 | to templates as `dict`s. 252 | -------------------------------------------------------------------------------- /docs/encoding.md: -------------------------------------------------------------------------------- 1 | # Encoding Functions 2 | 3 | Sprig has the following encoding and decoding functions: 4 | 5 | - `b64enc`/`b64dec`: Encode or decode with Base64 6 | - `b32enc`/`b32dec`: Encode or decode with Base32 7 | -------------------------------------------------------------------------------- /docs/flow_control.md: -------------------------------------------------------------------------------- 1 | # Flow Control Functions 2 | 3 | ## fail 4 | 5 | Unconditionally returns an empty `string` and an `error` with the specified 6 | text. This is useful in scenarios where other conditionals have determined that 7 | template rendering should fail. 8 | 9 | ``` 10 | fail "Please accept the end user license agreement" 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Sprig Function Documentation 2 | 3 | The Sprig library provides over 70 template functions for Go's template language. 4 | 5 | - [String Functions](strings.md): `trim`, `wrap`, `randAlpha`, `plural`, etc. 6 | - [String List Functions](string_slice.md): `splitList`, `sortAlpha`, etc. 7 | - [Integer Math Functions](math.md): `add`, `max`, `mul`, etc. 8 | - [Integer Slice Functions](integer_slice.md): `until`, `untilStep` 9 | - [Float Math Functions](mathf.md): `addf`, `maxf`, `mulf`, etc. 10 | - [Date Functions](date.md): `now`, `date`, etc. 11 | - [Defaults Functions](defaults.md): `default`, `empty`, `coalesce`, `fromJson`, `toJson`, `toPrettyJson`, `toRawJson`, `ternary` 12 | - [Encoding Functions](encoding.md): `b64enc`, `b64dec`, etc. 13 | - [Lists and List Functions](lists.md): `list`, `first`, `uniq`, etc. 14 | - [Dictionaries and Dict Functions](dicts.md): `get`, `set`, `dict`, `hasKey`, `pluck`, `dig`, `deepCopy`, etc. 15 | - [Type Conversion Functions](conversion.md): `atoi`, `int64`, `toString`, etc. 16 | - [Path and Filepath Functions](paths.md): `base`, `dir`, `ext`, `clean`, `isAbs`, `osBase`, `osDir`, `osExt`, `osClean`, `osIsAbs` 17 | - [Flow Control Functions](flow_control.md): `fail` 18 | - Advanced Functions 19 | - [UUID Functions](uuid.md): `uuidv4` 20 | - [OS Functions](os.md): `env`, `expandenv` 21 | - [Version Comparison Functions](semver.md): `semver`, `semverCompare` 22 | - [Reflection](reflection.md): `typeOf`, `kindIs`, `typeIsLike`, etc. 23 | - [Cryptographic and Security Functions](crypto.md): `derivePassword`, `sha256sum`, `genPrivateKey`, etc. 24 | - [Network](network.md): `getHostByName` 25 | - [URL](url.md): `urlParse`, `urlJoin` 26 | -------------------------------------------------------------------------------- /docs/integer_slice.md: -------------------------------------------------------------------------------- 1 | # Integer Slice Functions 2 | 3 | ## until 4 | 5 | The `until` function builds a range of integers. 6 | 7 | ``` 8 | until 5 9 | ``` 10 | 11 | The above generates the list `[0, 1, 2, 3, 4]`. 12 | 13 | This is useful for looping with `range $i, $e := until 5`. 14 | 15 | ## untilStep 16 | 17 | Like `until`, `untilStep` generates a list of counting integers. But it allows 18 | you to define a start, stop, and step: 19 | 20 | ``` 21 | untilStep 3 6 2 22 | ``` 23 | 24 | The above will produce `[3 5]` by starting with 3, and adding 2 until it is equal 25 | or greater than 6. This is similar to Python's `range` function. 26 | 27 | ## seq 28 | 29 | Works like the bash `seq` command. 30 | * 1 parameter (end) - will generate all counting integers between 1 and `end` inclusive. 31 | * 2 parameters (start, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by 1. 32 | * 3 parameters (start, step, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by `step`. 33 | 34 | ``` 35 | seq 5 => 1 2 3 4 5 36 | seq -3 => 1 0 -1 -2 -3 37 | seq 0 2 => 0 1 2 38 | seq 2 -2 => 2 1 0 -1 -2 39 | seq 0 2 10 => 0 2 4 6 8 10 40 | seq 0 -2 -5 => 0 -2 -4 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/lists.md: -------------------------------------------------------------------------------- 1 | # Lists and List Functions 2 | 3 | Sprig provides a simple `list` type that can contain arbitrary sequential lists 4 | of data. This is similar to arrays or slices, but lists are designed to be used 5 | as immutable data types. 6 | 7 | Create a list of integers: 8 | 9 | ``` 10 | $myList := list 1 2 3 4 5 11 | ``` 12 | 13 | The above creates a list of `[1 2 3 4 5]`. 14 | 15 | ## first, mustFirst 16 | 17 | To get the head item on a list, use `first`. 18 | 19 | `first $myList` returns `1` 20 | 21 | `first` panics if there is a problem while `mustFirst` returns an error to the 22 | template engine if there is a problem. 23 | 24 | ## rest, mustRest 25 | 26 | To get the tail of the list (everything but the first item), use `rest`. 27 | 28 | `rest $myList` returns `[2 3 4 5]` 29 | 30 | `rest` panics if there is a problem while `mustRest` returns an error to the 31 | template engine if there is a problem. 32 | 33 | ## last, mustLast 34 | 35 | To get the last item on a list, use `last`: 36 | 37 | `last $myList` returns `5`. This is roughly analogous to reversing a list and 38 | then calling `first`. 39 | 40 | ## initial, mustInitial 41 | 42 | This compliments `last` by returning all _but_ the last element. 43 | `initial $myList` returns `[1 2 3 4]`. 44 | 45 | `initial` panics if there is a problem while `mustInitial` returns an error to the 46 | template engine if there is a problem. 47 | 48 | ## append, mustAppend 49 | 50 | Append a new item to an existing list, creating a new list. 51 | 52 | ``` 53 | $new = append $myList 6 54 | ``` 55 | 56 | The above would set `$new` to `[1 2 3 4 5 6]`. `$myList` would remain unaltered. 57 | 58 | `append` panics if there is a problem while `mustAppend` returns an error to the 59 | template engine if there is a problem. 60 | 61 | ## prepend, mustPrepend 62 | 63 | Push an element onto the front of a list, creating a new list. 64 | 65 | ``` 66 | prepend $myList 0 67 | ``` 68 | 69 | The above would produce `[0 1 2 3 4 5]`. `$myList` would remain unaltered. 70 | 71 | `prepend` panics if there is a problem while `mustPrepend` returns an error to the 72 | template engine if there is a problem. 73 | 74 | ## concat 75 | 76 | Concatenate arbitrary number of lists into one. 77 | 78 | ``` 79 | concat $myList ( list 6 7 ) ( list 8 ) 80 | ``` 81 | 82 | The above would produce `[1 2 3 4 5 6 7 8]`. `$myList` would remain unaltered. 83 | 84 | ## reverse, mustReverse 85 | 86 | Produce a new list with the reversed elements of the given list. 87 | 88 | ``` 89 | reverse $myList 90 | ``` 91 | 92 | The above would generate the list `[5 4 3 2 1]`. 93 | 94 | `reverse` panics if there is a problem while `mustReverse` returns an error to the 95 | template engine if there is a problem. 96 | 97 | ## uniq, mustUniq 98 | 99 | Generate a list with all of the duplicates removed. 100 | 101 | ``` 102 | list 1 1 1 2 | uniq 103 | ``` 104 | 105 | The above would produce `[1 2]` 106 | 107 | `uniq` panics if there is a problem while `mustUniq` returns an error to the 108 | template engine if there is a problem. 109 | 110 | ## without, mustWithout 111 | 112 | The `without` function filters items out of a list. 113 | 114 | ``` 115 | without $myList 3 116 | ``` 117 | 118 | The above would produce `[1 2 4 5]` 119 | 120 | Without can take more than one filter: 121 | 122 | ``` 123 | without $myList 1 3 5 124 | ``` 125 | 126 | That would produce `[2 4]` 127 | 128 | `without` panics if there is a problem while `mustWithout` returns an error to the 129 | template engine if there is a problem. 130 | 131 | ## has, mustHas 132 | 133 | Test to see if a list has a particular element. 134 | 135 | ``` 136 | has 4 $myList 137 | ``` 138 | 139 | The above would return `true`, while `has "hello" $myList` would return false. 140 | 141 | `has` panics if there is a problem while `mustHas` returns an error to the 142 | template engine if there is a problem. 143 | 144 | ## compact, mustCompact 145 | 146 | Accepts a list and removes entries with empty values. 147 | 148 | ``` 149 | $list := list 1 "a" "foo" "" 150 | $copy := compact $list 151 | ``` 152 | 153 | `compact` will return a new list with the empty (i.e., "") item removed. 154 | 155 | `compact` panics if there is a problem and `mustCompact` returns an error to the 156 | template engine if there is a problem. 157 | 158 | ## slice, mustSlice 159 | 160 | To get partial elements of a list, use `slice list [n] [m]`. It is 161 | equivalent of `list[n:m]`. 162 | 163 | - `slice $myList` returns `[1 2 3 4 5]`. It is same as `myList[:]`. 164 | - `slice $myList 3` returns `[4 5]`. It is same as `myList[3:]`. 165 | - `slice $myList 1 3` returns `[2 3]`. It is same as `myList[1:3]`. 166 | - `slice $myList 0 3` returns `[1 2 3]`. It is same as `myList[:3]`. 167 | 168 | `slice` panics if there is a problem while `mustSlice` returns an error to the 169 | template engine if there is a problem. 170 | 171 | ## chunk 172 | 173 | To split a list into chunks of given size, use `chunk size list`. This is useful for pagination. 174 | 175 | ``` 176 | chunk 3 (list 1 2 3 4 5 6 7 8) 177 | ``` 178 | 179 | This produces list of lists `[ [ 1 2 3 ] [ 4 5 6 ] [ 7 8 ] ]`. 180 | 181 | ## A Note on List Internals 182 | 183 | A list is implemented in Go as a `[]interface{}`. For Go developers embedding 184 | Sprig, you may pass `[]interface{}` items into your template context and be 185 | able to use all of the `list` functions on those items. 186 | -------------------------------------------------------------------------------- /docs/math.md: -------------------------------------------------------------------------------- 1 | # Integer Math Functions 2 | 3 | The following math functions operate on `int64` values. 4 | 5 | ## add 6 | 7 | Sum numbers with `add`. Accepts two or more inputs. 8 | 9 | ``` 10 | add 1 2 3 11 | ``` 12 | 13 | ## add1 14 | 15 | To increment by 1, use `add1` 16 | 17 | ## sub 18 | 19 | To subtract, use `sub` 20 | 21 | ## div 22 | 23 | Perform integer division with `div` 24 | 25 | ## mod 26 | 27 | Modulo with `mod` 28 | 29 | ## mul 30 | 31 | Multiply with `mul`. Accepts two or more inputs. 32 | 33 | ``` 34 | mul 1 2 3 35 | ``` 36 | 37 | ## max 38 | 39 | Return the largest of a series of integers: 40 | 41 | This will return `3`: 42 | 43 | ``` 44 | max 1 2 3 45 | ``` 46 | 47 | ## min 48 | 49 | Return the smallest of a series of integers. 50 | 51 | `min 1 2 3` will return `1` 52 | 53 | ## floor 54 | 55 | Returns the greatest float value less than or equal to input value 56 | 57 | `floor 123.9999` will return `123.0` 58 | 59 | ## ceil 60 | 61 | Returns the greatest float value greater than or equal to input value 62 | 63 | `ceil 123.001` will return `124.0` 64 | 65 | ## round 66 | 67 | Returns a float value with the remainder rounded to the given number to digits after the decimal point. 68 | 69 | `round 123.555555 3` will return `123.556` 70 | 71 | ## randInt 72 | Returns a random integer value from min (inclusive) to max (exclusive). 73 | 74 | ``` 75 | randInt 12 30 76 | ``` 77 | 78 | The above will produce a random number in the range [12,30]. 79 | -------------------------------------------------------------------------------- /docs/mathf.md: -------------------------------------------------------------------------------- 1 | # Float Math Functions 2 | 3 | All math functions operate on `float64` values. 4 | 5 | ## addf 6 | 7 | Sum numbers with `addf` 8 | 9 | This will return `5.5`: 10 | 11 | ``` 12 | addf 1.5 2 2 13 | ``` 14 | 15 | ## add1f 16 | 17 | To increment by 1, use `add1f` 18 | 19 | ## subf 20 | 21 | To subtract, use `subf` 22 | 23 | This is equivalent to `7.5 - 2 - 3` and will return `2.5`: 24 | 25 | ``` 26 | subf 7.5 2 3 27 | ``` 28 | 29 | ## divf 30 | 31 | Perform integer division with `divf` 32 | 33 | This is equivalent to `10 / 2 / 4` and will return `1.25`: 34 | 35 | ``` 36 | divf 10 2 4 37 | ``` 38 | 39 | ## mulf 40 | 41 | Multiply with `mulf` 42 | 43 | This will return `6`: 44 | 45 | ``` 46 | mulf 1.5 2 2 47 | ``` 48 | 49 | ## maxf 50 | 51 | Return the largest of a series of floats: 52 | 53 | This will return `3`: 54 | 55 | ``` 56 | maxf 1 2.5 3 57 | ``` 58 | 59 | ## minf 60 | 61 | Return the smallest of a series of floats. 62 | 63 | This will return `1.5`: 64 | 65 | ``` 66 | minf 1.5 2 3 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/network.md: -------------------------------------------------------------------------------- 1 | # Network Functions 2 | 3 | Sprig network manipulation functions. 4 | 5 | ## getHostByName 6 | 7 | The `getHostByName` receives a domain name and returns the ip address. 8 | 9 | ``` 10 | getHostByName "www.google.com" would return the corresponding ip address of www.google.com 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/os.md: -------------------------------------------------------------------------------- 1 | # OS Functions 2 | 3 | _WARNING:_ These functions can lead to information leakage if not used 4 | appropriately. 5 | 6 | _WARNING:_ Some notable implementations of Sprig (such as 7 | [Kubernetes Helm](http://helm.sh)) _do not provide these functions for security 8 | reasons_. 9 | 10 | ## env 11 | 12 | The `env` function reads an environment variable: 13 | 14 | ``` 15 | env "HOME" 16 | ``` 17 | 18 | ## expandenv 19 | 20 | To substitute environment variables in a string, use `expandenv`: 21 | 22 | ``` 23 | expandenv "Your path is set to $PATH" 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/paths.md: -------------------------------------------------------------------------------- 1 | # Path and Filepath Functions 2 | 3 | While Sprig does not grant access to the filesystem, it does provide functions 4 | for working with strings that follow file path conventions. 5 | 6 | ## Paths 7 | 8 | Paths separated by the slash character (`/`), processed by the `path` package. 9 | 10 | Examples: 11 | 12 | - The [Linux](https://en.wikipedia.org/wiki/Linux) and 13 | [MacOS](https://en.wikipedia.org/wiki/MacOS) 14 | [filesystems](https://en.wikipedia.org/wiki/File_system): 15 | `/home/user/file`, `/etc/config`; 16 | - The path component of 17 | [URIs](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier): 18 | `https://example.com/some/content/`, `ftp://example.com/file/`. 19 | 20 | ### base 21 | 22 | Return the last element of a path. 23 | 24 | ``` 25 | base "foo/bar/baz" 26 | ``` 27 | 28 | The above prints "baz". 29 | 30 | ### dir 31 | 32 | Return the directory, stripping the last part of the path. So `dir "foo/bar/baz"` 33 | returns `foo/bar`. 34 | 35 | ### clean 36 | 37 | Clean up a path. 38 | 39 | ``` 40 | clean "foo/bar/../baz" 41 | ``` 42 | 43 | The above resolves the `..` and returns `foo/baz`. 44 | 45 | ### ext 46 | 47 | Return the file extension. 48 | 49 | ``` 50 | ext "foo.bar" 51 | ``` 52 | 53 | The above returns `.bar`. 54 | 55 | ### isAbs 56 | 57 | To check whether a path is absolute, use `isAbs`. 58 | 59 | ## Filepaths 60 | 61 | Paths separated by the `os.PathSeparator` variable, processed by the `path/filepath` package. 62 | 63 | These are the recommended functions to use when parsing paths of local filesystems, usually when dealing with local files, directories, etc. 64 | 65 | Examples: 66 | 67 | - Running on Linux or MacOS the filesystem path is separated by the slash character (`/`): 68 | `/home/user/file`, `/etc/config`; 69 | - Running on [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows) 70 | the filesystem path is separated by the backslash character (`\`): 71 | `C:\Users\Username\`, `C:\Program Files\Application\`; 72 | 73 | ### osBase 74 | 75 | Return the last element of a filepath. 76 | 77 | ``` 78 | osBase "/foo/bar/baz" 79 | osBase "C:\\foo\\bar\\baz" 80 | ``` 81 | 82 | The above prints "baz" on Linux and Windows, respectively. 83 | 84 | ### osDir 85 | 86 | Return the directory, stripping the last part of the path. So `osDir "/foo/bar/baz"` 87 | returns `/foo/bar` on Linux, and `osDir "C:\\foo\\bar\\baz"` 88 | returns `C:\\foo\\bar` on Windows. 89 | 90 | ### osClean 91 | 92 | Clean up a path. 93 | 94 | ``` 95 | osClean "/foo/bar/../baz" 96 | osClean "C:\\foo\\bar\\..\\baz" 97 | ``` 98 | 99 | The above resolves the `..` and returns `foo/baz` on Linux and `C:\\foo\\baz` on Windows. 100 | 101 | ### osExt 102 | 103 | Return the file extension. 104 | 105 | ``` 106 | osExt "/foo.bar" 107 | osExt "C:\\foo.bar" 108 | ``` 109 | 110 | The above returns `.bar` on Linux and Windows, respectively. 111 | 112 | ### osIsAbs 113 | 114 | To check whether a file path is absolute, use `osIsAbs`. 115 | -------------------------------------------------------------------------------- /docs/reflection.md: -------------------------------------------------------------------------------- 1 | # Reflection Functions 2 | 3 | Sprig provides rudimentary reflection tools. These help advanced template 4 | developers understand the underlying Go type information for a particular value. 5 | 6 | Go has several primitive _kinds_, like `string`, `slice`, `int64`, and `bool`. 7 | 8 | Go has an open _type_ system that allows developers to create their own types. 9 | 10 | Sprig provides a set of functions for each. 11 | 12 | ## Kind Functions 13 | 14 | There are two Kind functions: `kindOf` returns the kind of an object. 15 | 16 | ``` 17 | kindOf "hello" 18 | ``` 19 | 20 | The above would return `string`. For simple tests (like in `if` blocks), the 21 | `kindIs` function will let you verify that a value is a particular kind: 22 | 23 | ``` 24 | kindIs "int" 123 25 | ``` 26 | 27 | The above will return `true` 28 | 29 | ## Type Functions 30 | 31 | Types are slightly harder to work with, so there are three different functions: 32 | 33 | - `typeOf` returns the underlying type of a value: `typeOf $foo` 34 | - `typeIs` is like `kindIs`, but for types: `typeIs "*io.Buffer" $myVal` 35 | - `typeIsLike` works as `typeIs`, except that it also dereferences pointers. 36 | 37 | **Note:** None of these can test whether or not something implements a given 38 | interface, since doing so would require compiling the interface in ahead of time. 39 | 40 | ## deepEqual 41 | 42 | `deepEqual` returns true if two values are ["deeply equal"](https://golang.org/pkg/reflect/#DeepEqual) 43 | 44 | Works for non-primitive types as well (compared to the built-in `eq`). 45 | 46 | ``` 47 | deepEqual (list 1 2 3) (list 1 2 3) 48 | ``` 49 | 50 | The above will return `true` 51 | -------------------------------------------------------------------------------- /docs/semver.md: -------------------------------------------------------------------------------- 1 | # Semantic Version Functions 2 | 3 | Some version schemes are easily parseable and comparable. Sprig provides functions 4 | for working with [SemVer 2](http://semver.org) versions. 5 | 6 | ## semver 7 | 8 | The `semver` function parses a string into a Semantic Version: 9 | 10 | ``` 11 | $version := semver "1.2.3-alpha.1+123" 12 | ``` 13 | 14 | _If the parser fails, it will cause template execution to halt with an error._ 15 | 16 | At this point, `$version` is a pointer to a `Version` object with the following 17 | properties: 18 | 19 | - `$version.Major`: The major number (`1` above) 20 | - `$version.Minor`: The minor number (`2` above) 21 | - `$version.Patch`: The patch number (`3` above) 22 | - `$version.Prerelease`: The prerelease (`alpha.1` above) 23 | - `$version.Metadata`: The build metadata (`123` above) 24 | - `$version.Original`: The original version as a string 25 | 26 | Additionally, you can compare a `Version` to another `version` using the `Compare` 27 | function: 28 | 29 | ``` 30 | semver "1.4.3" | (semver "1.2.3").Compare 31 | ``` 32 | 33 | The above will return `-1`. 34 | 35 | The return values are: 36 | 37 | - `-1` if the given semver is greater than the semver whose `Compare` method was called 38 | - `1` if the version who's `Compare` function was called is greater. 39 | - `0` if they are the same version 40 | 41 | (Note that in SemVer, the `Metadata` field is not compared during version 42 | comparison operations.) 43 | 44 | ## semverCompare 45 | 46 | A more robust comparison function is provided as `semverCompare`. It returns `true` if 47 | the constraint matches, or `false` if it does not match. This version supports version ranges: 48 | 49 | - `semverCompare "1.2.3" "1.2.3"` checks for an exact match 50 | - `semverCompare "^1.2.0" "1.2.3"` checks that the major and minor versions match, and that the patch 51 | number of the second version is _greater than or equal to_ the first parameter. 52 | 53 | The SemVer functions use the [Masterminds semver library](https://github.com/Masterminds/semver), 54 | from the creators of Sprig. 55 | 56 | ## Basic Comparisons 57 | 58 | There are two elements to the comparisons. First, a comparison string is a list 59 | of space or comma separated AND comparisons. These are then separated by || (OR) 60 | comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a 61 | comparison that's greater than or equal to 1.2 and less than 3.0.0 or is 62 | greater than or equal to 4.2.3. 63 | 64 | The basic comparisons are: 65 | 66 | - `=`: equal (aliased to no operator) 67 | - `!=`: not equal 68 | - `>`: greater than 69 | - `<`: less than 70 | - `>=`: greater than or equal to 71 | - `<=`: less than or equal to 72 | 73 | _Note, according to the Semantic Version specification pre-releases may not be 74 | API compliant with their release counterpart. It says,_ 75 | 76 | ## Working With Prerelease Versions 77 | 78 | Pre-releases, for those not familiar with them, are used for software releases 79 | prior to stable or generally available releases. Examples of prereleases include 80 | development, alpha, beta, and release candidate releases. A prerelease may be 81 | a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the 82 | order of precedence, prereleases come before their associated releases. In this 83 | example `1.2.3-beta.1 < 1.2.3`. 84 | 85 | According to the Semantic Version specification prereleases may not be 86 | API compliant with their release counterpart. It says, 87 | 88 | > A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. 89 | 90 | SemVer comparisons using constraints without a prerelease comparator will skip 91 | prerelease versions. For example, `>=1.2.3` will skip prereleases when looking 92 | at a list of releases while `>=1.2.3-0` will evaluate and find prereleases. 93 | 94 | The reason for the `0` as a pre-release version in the example comparison is 95 | because pre-releases can only contain ASCII alphanumerics and hyphens (along with 96 | `.` separators), per the spec. Sorting happens in ASCII sort order, again per the 97 | spec. The lowest character is a `0` in ASCII sort order 98 | (see an [ASCII Table](http://www.asciitable.com/)) 99 | 100 | Understanding ASCII sort ordering is important because A-Z comes before a-z. That 101 | means `>=1.2.3-BETA` will return `1.2.3-alpha`. What you might expect from case 102 | sensitivity doesn't apply here. This is due to ASCII sort ordering which is what 103 | the spec specifies. 104 | 105 | ## Hyphen Range Comparisons 106 | 107 | There are multiple methods to handle ranges and the first is hyphens ranges. 108 | These look like: 109 | 110 | - `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5` 111 | - `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` 112 | 113 | ## Wildcards In Comparisons 114 | 115 | The `x`, `X`, and `*` characters can be used as a wildcard character. This works 116 | for all comparison operators. When used on the `=` operator it falls 117 | back to the patch level comparison (see tilde below). For example, 118 | 119 | - `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` 120 | - `>= 1.2.x` is equivalent to `>= 1.2.0` 121 | - `<= 2.x` is equivalent to `< 3` 122 | - `*` is equivalent to `>= 0.0.0` 123 | 124 | ## Tilde Range Comparisons (Patch) 125 | 126 | The tilde (`~`) comparison operator is for patch level ranges when a minor 127 | version is specified and major level changes when the minor number is missing. 128 | For example, 129 | 130 | - `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0` 131 | - `~1` is equivalent to `>= 1, < 2` 132 | - `~2.3` is equivalent to `>= 2.3, < 2.4` 133 | - `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` 134 | - `~1.x` is equivalent to `>= 1, < 2` 135 | 136 | ## Caret Range Comparisons (Major) 137 | 138 | The caret (`^`) comparison operator is for major level changes once a stable 139 | (1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts 140 | as the API stability level. This is useful when comparisons of API versions as a 141 | major change is API breaking. For example, 142 | 143 | - `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` 144 | - `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` 145 | - `^2.3` is equivalent to `>= 2.3, < 3` 146 | - `^2.x` is equivalent to `>= 2.0.0, < 3` 147 | - `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` 148 | - `^0.2` is equivalent to `>=0.2.0 <0.3.0` 149 | - `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` 150 | - `^0.0` is equivalent to `>=0.0.0 <0.1.0` 151 | - `^0` is equivalent to `>=0.0.0 <1.0.0` 152 | -------------------------------------------------------------------------------- /docs/string_slice.md: -------------------------------------------------------------------------------- 1 | # String Slice Functions 2 | 3 | These function operate on or generate slices of strings. In Go, a slice is a 4 | growable array. In Sprig, it's a special case of a `list`. 5 | 6 | ## join 7 | 8 | Join a list of strings into a single string, with the given separator. 9 | 10 | ``` 11 | list "hello" "world" | join "_" 12 | ``` 13 | 14 | The above will produce `hello_world` 15 | 16 | `join` will try to convert non-strings to a string value: 17 | 18 | ``` 19 | list 1 2 3 | join "+" 20 | ``` 21 | 22 | The above will produce `1+2+3` 23 | 24 | ## splitList and split 25 | 26 | Split a string into a list of strings: 27 | 28 | ``` 29 | splitList "$" "foo$bar$baz" 30 | ``` 31 | 32 | The above will return `[foo bar baz]` 33 | 34 | The older `split` function splits a string into a `dict`. It is designed to make 35 | it easy to use template dot notation for accessing members: 36 | 37 | ``` 38 | $a := split "$" "foo$bar$baz" 39 | ``` 40 | 41 | The above produces a map with index keys. `{_0: foo, _1: bar, _2: baz}` 42 | 43 | ``` 44 | $a._0 45 | ``` 46 | 47 | The above produces `foo` 48 | 49 | ## splitn 50 | 51 | `splitn` function splits a string into a `dict`. It is designed to make 52 | it easy to use template dot notation for accessing members: 53 | 54 | ``` 55 | $a := splitn "$" 2 "foo$bar$baz" 56 | ``` 57 | 58 | The above produces a map with index keys. `{_0: foo, _1: bar$baz}` 59 | 60 | ``` 61 | $a._0 62 | ``` 63 | 64 | The above produces `foo` 65 | 66 | ## sortAlpha 67 | 68 | The `sortAlpha` function sorts a list of strings into alphabetical (lexicographical) 69 | order. 70 | 71 | It does _not_ sort in place, but returns a sorted copy of the list, in keeping 72 | with the immutability of lists. 73 | -------------------------------------------------------------------------------- /docs/strings.md: -------------------------------------------------------------------------------- 1 | # String Functions 2 | 3 | Sprig has a number of string manipulation functions. 4 | 5 | ## trim 6 | 7 | The `trim` function removes space from either side of a string: 8 | 9 | ``` 10 | trim " hello " 11 | ``` 12 | 13 | The above produces `hello` 14 | 15 | ## trimAll 16 | 17 | Remove given characters from the front or back of a string: 18 | 19 | ``` 20 | trimAll "$" "$5.00" 21 | ``` 22 | 23 | The above returns `5.00` (as a string). 24 | 25 | ## trimSuffix 26 | 27 | Trim just the suffix from a string: 28 | 29 | ``` 30 | trimSuffix "-" "hello-" 31 | ``` 32 | 33 | The above returns `hello` 34 | 35 | ## trimPrefix 36 | 37 | Trim just the prefix from a string: 38 | 39 | ``` 40 | trimPrefix "-" "-hello" 41 | ``` 42 | 43 | The above returns `hello` 44 | 45 | ## upper 46 | 47 | Convert the entire string to uppercase: 48 | 49 | ``` 50 | upper "hello" 51 | ``` 52 | 53 | The above returns `HELLO` 54 | 55 | ## lower 56 | 57 | Convert the entire string to lowercase: 58 | 59 | ``` 60 | lower "HELLO" 61 | ``` 62 | 63 | The above returns `hello` 64 | 65 | ## title 66 | 67 | Convert to title case: 68 | 69 | ``` 70 | title "hello world" 71 | ``` 72 | 73 | The above returns `Hello World` 74 | 75 | ## untitle 76 | 77 | Remove title casing. `untitle "Hello World"` produces `hello world`. 78 | 79 | ## repeat 80 | 81 | Repeat a string multiple times: 82 | 83 | ``` 84 | repeat 3 "hello" 85 | ``` 86 | 87 | The above returns `hellohellohello` 88 | 89 | ## substr 90 | 91 | Get a substring from a string. It takes three parameters: 92 | 93 | - start (int) 94 | - end (int) 95 | - string (string) 96 | 97 | ``` 98 | substr 0 5 "hello world" 99 | ``` 100 | 101 | The above returns `hello` 102 | 103 | ## nospace 104 | 105 | Remove all whitespace from a string. 106 | 107 | ``` 108 | nospace "hello w o r l d" 109 | ``` 110 | 111 | The above returns `helloworld` 112 | 113 | ## trunc 114 | 115 | Truncate a string (and add no suffix) 116 | 117 | ``` 118 | trunc 5 "hello world" 119 | ``` 120 | 121 | The above produces `hello`. 122 | 123 | ``` 124 | trunc -5 "hello world" 125 | ``` 126 | 127 | The above produces `world`. 128 | 129 | ## abbrev 130 | 131 | Truncate a string with ellipses (`...`) 132 | 133 | Parameters: 134 | 135 | - max length 136 | - the string 137 | 138 | ``` 139 | abbrev 5 "hello world" 140 | ``` 141 | 142 | The above returns `he...`, since it counts the width of the ellipses against the 143 | maximum length. 144 | 145 | ## abbrevboth 146 | 147 | Abbreviate both sides: 148 | 149 | ``` 150 | abbrevboth 5 10 "1234 5678 9123" 151 | ``` 152 | 153 | the above produces `...5678...` 154 | 155 | It takes: 156 | 157 | - left offset 158 | - max length 159 | - the string 160 | 161 | ## initials 162 | 163 | Given multiple words, take the first letter of each word and combine. 164 | 165 | ``` 166 | initials "First Try" 167 | ``` 168 | 169 | The above returns `FT` 170 | 171 | ## randAlphaNum, randAlpha, randNumeric, and randAscii 172 | 173 | These four functions generate cryptographically secure (uses ```crypto/rand```) 174 | random strings, but with different base character sets: 175 | 176 | - `randAlphaNum` uses `0-9a-zA-Z` 177 | - `randAlpha` uses `a-zA-Z` 178 | - `randNumeric` uses `0-9` 179 | - `randAscii` uses all printable ASCII characters 180 | 181 | Each of them takes one parameter: the integer length of the string. 182 | 183 | ``` 184 | randNumeric 3 185 | ``` 186 | 187 | The above will produce a random string with three digits. 188 | 189 | ## wrap 190 | 191 | Wrap text at a given column count: 192 | 193 | ``` 194 | wrap 80 $someText 195 | ``` 196 | 197 | The above will wrap the string in `$someText` at 80 columns. 198 | 199 | ## wrapWith 200 | 201 | `wrapWith` works as `wrap`, but lets you specify the string to wrap with. 202 | (`wrap` uses `\n`) 203 | 204 | ``` 205 | wrapWith 5 "\t" "Hello World" 206 | ``` 207 | 208 | The above produces `hello world` (where the whitespace is an ASCII tab 209 | character) 210 | 211 | ## contains 212 | 213 | Test to see if one string is contained inside of another: 214 | 215 | ``` 216 | contains "cat" "catch" 217 | ``` 218 | 219 | The above returns `true` because `catch` contains `cat`. 220 | 221 | ## hasPrefix and hasSuffix 222 | 223 | The `hasPrefix` and `hasSuffix` functions test whether a string has a given 224 | prefix or suffix: 225 | 226 | ``` 227 | hasPrefix "cat" "catch" 228 | ``` 229 | 230 | The above returns `true` because `catch` has the prefix `cat`. 231 | 232 | ## quote and squote 233 | 234 | These functions wrap a string in double quotes (`quote`) or single quotes 235 | (`squote`). 236 | 237 | ## cat 238 | 239 | The `cat` function concatenates multiple strings together into one, separating 240 | them with spaces: 241 | 242 | ``` 243 | cat "hello" "beautiful" "world" 244 | ``` 245 | 246 | The above produces `hello beautiful world` 247 | 248 | ## indent 249 | 250 | The `indent` function indents every line in a given string to the specified 251 | indent width. This is useful when aligning multi-line strings: 252 | 253 | ``` 254 | indent 4 $lots_of_text 255 | ``` 256 | 257 | The above will indent every line of text by 4 space characters. 258 | 259 | ## nindent 260 | 261 | The `nindent` function is the same as the indent function, but prepends a new 262 | line to the beginning of the string. 263 | 264 | ``` 265 | nindent 4 $lots_of_text 266 | ``` 267 | 268 | The above will indent every line of text by 4 space characters and add a new 269 | line to the beginning. 270 | 271 | ## replace 272 | 273 | Perform simple string replacement. 274 | 275 | It takes three arguments: 276 | 277 | - string to replace 278 | - string to replace with 279 | - source string 280 | 281 | ``` 282 | "I Am Henry VIII" | replace " " "-" 283 | ``` 284 | 285 | The above will produce `I-Am-Henry-VIII` 286 | 287 | ## plural 288 | 289 | Pluralize a string. 290 | 291 | ``` 292 | len $fish | plural "one anchovy" "many anchovies" 293 | ``` 294 | 295 | In the above, if the length of the string is 1, the first argument will be 296 | printed (`one anchovy`). Otherwise, the second argument will be printed 297 | (`many anchovies`). 298 | 299 | The arguments are: 300 | 301 | - singular string 302 | - plural string 303 | - length integer 304 | 305 | NOTE: Sprig does not currently support languages with more complex pluralization 306 | rules. And `0` is considered a plural because the English language treats it 307 | as such (`zero anchovies`). The Sprig developers are working on a solution for 308 | better internationalization. 309 | 310 | ## snakecase 311 | 312 | Convert string from camelCase to snake_case. 313 | 314 | ``` 315 | snakecase "FirstName" 316 | ``` 317 | 318 | This above will produce `first_name`. 319 | 320 | ## camelcase 321 | 322 | Convert string from snake_case to CamelCase 323 | 324 | ``` 325 | camelcase "http_server" 326 | ``` 327 | 328 | This above will produce `HttpServer`. 329 | 330 | ## kebabcase 331 | 332 | Convert string from camelCase to kebab-case. 333 | 334 | ``` 335 | kebabcase "FirstName" 336 | ``` 337 | 338 | This above will produce `first-name`. 339 | 340 | ## swapcase 341 | 342 | Swap the case of a string using a word based algorithm. 343 | 344 | Conversion algorithm: 345 | 346 | - Upper case character converts to Lower case 347 | - Title case character converts to Lower case 348 | - Lower case character after Whitespace or at start converts to Title case 349 | - Other Lower case character converts to Upper case 350 | - Whitespace is defined by unicode.IsSpace(char) 351 | 352 | ``` 353 | swapcase "This Is A.Test" 354 | ``` 355 | 356 | This above will produce `tHIS iS a.tEST`. 357 | 358 | ## shuffle 359 | 360 | Shuffle a string. 361 | 362 | ``` 363 | shuffle "hello" 364 | ``` 365 | 366 | The above will randomize the letters in `hello`, perhaps producing `oelhl`. 367 | 368 | ## regexMatch, mustRegexMatch 369 | 370 | Returns true if the input string contains any match of the regular expression. 371 | 372 | ``` 373 | regexMatch "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" "test@acme.com" 374 | ``` 375 | 376 | The above produces `true` 377 | 378 | `regexMatch` panics if there is a problem and `mustRegexMatch` returns an error to the 379 | template engine if there is a problem. 380 | 381 | ## regexFindAll, mustRegexFindAll 382 | 383 | Returns a slice of all matches of the regular expression in the input string. 384 | The last parameter n determines the number of substrings to return, where -1 means return all matches 385 | 386 | ``` 387 | regexFindAll "[2,4,6,8]" "123456789" -1 388 | ``` 389 | 390 | The above produces `[2 4 6 8]` 391 | 392 | `regexFindAll` panics if there is a problem and `mustRegexFindAll` returns an error to the 393 | template engine if there is a problem. 394 | 395 | ## regexFind, mustRegexFind 396 | 397 | Return the first (left most) match of the regular expression in the input string 398 | 399 | ``` 400 | regexFind "[a-zA-Z][1-9]" "abcd1234" 401 | ``` 402 | 403 | The above produces `d1` 404 | 405 | `regexFind` panics if there is a problem and `mustRegexFind` returns an error to the 406 | template engine if there is a problem. 407 | 408 | ## regexReplaceAll, mustRegexReplaceAll 409 | 410 | Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. 411 | Inside string replacement, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch 412 | 413 | ``` 414 | regexReplaceAll "a(x*)b" "-ab-axxb-" "${1}W" 415 | ``` 416 | 417 | The above produces `-W-xxW-` 418 | 419 | `regexReplaceAll` panics if there is a problem and `mustRegexReplaceAll` returns an error to the 420 | template engine if there is a problem. 421 | 422 | ## regexReplaceAllLiteral, mustRegexReplaceAllLiteral 423 | 424 | Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement 425 | The replacement string is substituted directly, without using Expand 426 | 427 | ``` 428 | regexReplaceAllLiteral "a(x*)b" "-ab-axxb-" "${1}" 429 | ``` 430 | 431 | The above produces `-${1}-${1}-` 432 | 433 | `regexReplaceAllLiteral` panics if there is a problem and `mustRegexReplaceAllLiteral` returns an error to the 434 | template engine if there is a problem. 435 | 436 | ## regexSplit, mustRegexSplit 437 | 438 | Slices the input string into substrings separated by the expression and returns a slice of the substrings between those expression matches. The last parameter `n` determines the number of substrings to return, where `-1` means return all matches 439 | 440 | ``` 441 | regexSplit "z+" "pizza" -1 442 | ``` 443 | 444 | The above produces `[pi a]` 445 | 446 | `regexSplit` panics if there is a problem and `mustRegexSplit` returns an error to the 447 | template engine if there is a problem. 448 | 449 | ## regexQuoteMeta 450 | 451 | Returns a string that escapes all regular expression metacharacters inside the argument text; 452 | the returned string is a regular expression matching the literal text. 453 | 454 | ``` 455 | regexQuoteMeta "1.2.3" 456 | ``` 457 | 458 | The above produces `1\.2\.3` 459 | 460 | ## See Also... 461 | 462 | The [Conversion Functions](conversion.html) contain functions for converting 463 | strings. The [String Slice Functions](string_slice.html) contains functions 464 | for working with an array of strings. 465 | -------------------------------------------------------------------------------- /docs/url.md: -------------------------------------------------------------------------------- 1 | # URL Functions 2 | 3 | ## urlParse 4 | Parses string for URL and produces dict with URL parts 5 | 6 | ``` 7 | urlParse "http://admin:secret@server.com:8080/api?list=false#anchor" 8 | ``` 9 | 10 | The above returns a dict, containing URL object: 11 | ```yaml 12 | scheme: 'http' 13 | host: 'server.com:8080' 14 | path: '/api' 15 | query: 'list=false' 16 | opaque: nil 17 | fragment: 'anchor' 18 | userinfo: 'admin:secret' 19 | ``` 20 | 21 | For more info, check https://golang.org/pkg/net/url/#URL 22 | 23 | ## urlJoin 24 | Joins map (produced by `urlParse`) to produce URL string 25 | 26 | ``` 27 | urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "http") 28 | ``` 29 | 30 | The above returns the following string: 31 | ``` 32 | proto://host:80/path?query#fragment 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/uuid.md: -------------------------------------------------------------------------------- 1 | # UUID Functions 2 | 3 | Sprig can generate UUID v4 universally unique IDs. 4 | 5 | ``` 6 | uuidv4 7 | ``` 8 | 9 | The above returns a new UUID of the v4 (randomly generated) type. 10 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/template" 7 | ) 8 | 9 | func Example() { 10 | // Set up variables and template. 11 | vars := map[string]interface{}{"Name": " John Jacob Jingleheimer Schmidt "} 12 | tpl := `Hello {{.Name | trim | lower}}` 13 | 14 | // Get the Sprig function map. 15 | fmap := TxtFuncMap() 16 | t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) 17 | 18 | err := t.Execute(os.Stdout, vars) 19 | if err != nil { 20 | fmt.Printf("Error during template execution: %s", err) 21 | return 22 | } 23 | // Output: 24 | // Hello john jacob jingleheimer schmidt 25 | } 26 | -------------------------------------------------------------------------------- /flow_control_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFail(t *testing.T) { 11 | const msg = "This is an error!" 12 | tpl := fmt.Sprintf(`{{fail "%s"}}`, msg) 13 | _, err := runRaw(tpl, nil) 14 | assert.Error(t, err) 15 | assert.Contains(t, err.Error(), msg) 16 | } 17 | -------------------------------------------------------------------------------- /functions.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "errors" 5 | "html/template" 6 | "math/rand" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | ttemplate "text/template" 14 | "time" 15 | 16 | util "github.com/Masterminds/goutils" 17 | "github.com/huandu/xstrings" 18 | "github.com/shopspring/decimal" 19 | ) 20 | 21 | // FuncMap produces the function map. 22 | // 23 | // Use this to pass the functions into the template engine: 24 | // 25 | // tpl := template.New("foo").Funcs(sprig.FuncMap())) 26 | func FuncMap() template.FuncMap { 27 | return HtmlFuncMap() 28 | } 29 | 30 | // HermeticTxtFuncMap returns a 'text/template'.FuncMap with only repeatable functions. 31 | func HermeticTxtFuncMap() ttemplate.FuncMap { 32 | r := TxtFuncMap() 33 | for _, name := range nonhermeticFunctions { 34 | delete(r, name) 35 | } 36 | return r 37 | } 38 | 39 | // HermeticHtmlFuncMap returns an 'html/template'.Funcmap with only repeatable functions. 40 | func HermeticHtmlFuncMap() template.FuncMap { 41 | r := HtmlFuncMap() 42 | for _, name := range nonhermeticFunctions { 43 | delete(r, name) 44 | } 45 | return r 46 | } 47 | 48 | // TxtFuncMap returns a 'text/template'.FuncMap 49 | func TxtFuncMap() ttemplate.FuncMap { 50 | return ttemplate.FuncMap(GenericFuncMap()) 51 | } 52 | 53 | // HtmlFuncMap returns an 'html/template'.Funcmap 54 | func HtmlFuncMap() template.FuncMap { 55 | return template.FuncMap(GenericFuncMap()) 56 | } 57 | 58 | // GenericFuncMap returns a copy of the basic function map as a map[string]interface{}. 59 | func GenericFuncMap() map[string]interface{} { 60 | gfm := make(map[string]interface{}, len(genericMap)) 61 | for k, v := range genericMap { 62 | gfm[k] = v 63 | } 64 | return gfm 65 | } 66 | 67 | // These functions are not guaranteed to evaluate to the same result for given input, because they 68 | // refer to the environment or global state. 69 | var nonhermeticFunctions = []string{ 70 | // Date functions 71 | "date", 72 | "date_in_zone", 73 | "date_modify", 74 | "now", 75 | "htmlDate", 76 | "htmlDateInZone", 77 | "dateInZone", 78 | "dateModify", 79 | 80 | // Strings 81 | "randAlphaNum", 82 | "randAlpha", 83 | "randAscii", 84 | "randNumeric", 85 | "randBytes", 86 | "uuidv4", 87 | 88 | // OS 89 | "env", 90 | "expandenv", 91 | 92 | // Network 93 | "getHostByName", 94 | } 95 | 96 | var genericMap = map[string]interface{}{ 97 | "hello": func() string { return "Hello!" }, 98 | 99 | // Date functions 100 | "ago": dateAgo, 101 | "date": date, 102 | "date_in_zone": dateInZone, 103 | "date_modify": dateModify, 104 | "dateInZone": dateInZone, 105 | "dateModify": dateModify, 106 | "duration": duration, 107 | "durationRound": durationRound, 108 | "htmlDate": htmlDate, 109 | "htmlDateInZone": htmlDateInZone, 110 | "must_date_modify": mustDateModify, 111 | "mustDateModify": mustDateModify, 112 | "mustToDate": mustToDate, 113 | "now": time.Now, 114 | "toDate": toDate, 115 | "unixEpoch": unixEpoch, 116 | 117 | // Strings 118 | "abbrev": abbrev, 119 | "abbrevboth": abbrevboth, 120 | "trunc": trunc, 121 | "trim": strings.TrimSpace, 122 | "upper": strings.ToUpper, 123 | "lower": strings.ToLower, 124 | "title": strings.Title, 125 | "untitle": untitle, 126 | "substr": substring, 127 | // Switch order so that "foo" | repeat 5 128 | "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, 129 | // Deprecated: Use trimAll. 130 | "trimall": func(a, b string) string { return strings.Trim(b, a) }, 131 | // Switch order so that "$foo" | trimall "$" 132 | "trimAll": func(a, b string) string { return strings.Trim(b, a) }, 133 | "trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) }, 134 | "trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) }, 135 | "nospace": util.DeleteWhiteSpace, 136 | "initials": initials, 137 | "randAlphaNum": randAlphaNumeric, 138 | "randAlpha": randAlpha, 139 | "randAscii": randAscii, 140 | "randNumeric": randNumeric, 141 | "swapcase": util.SwapCase, 142 | "shuffle": xstrings.Shuffle, 143 | "snakecase": xstrings.ToSnakeCase, 144 | // camelcase used to call xstrings.ToCamelCase, but that function had a breaking change in version 145 | // 1.5 that moved it from upper camel case to lower camel case. This is a breaking change for sprig. 146 | // A new xstrings.ToPascalCase function was added that provided upper camel case. 147 | "camelcase": xstrings.ToPascalCase, 148 | "kebabcase": xstrings.ToKebabCase, 149 | "wrap": func(l int, s string) string { return util.Wrap(s, l) }, 150 | "wrapWith": func(l int, sep, str string) string { return util.WrapCustom(str, l, sep, true) }, 151 | // Switch order so that "foobar" | contains "foo" 152 | "contains": func(substr string, str string) bool { return strings.Contains(str, substr) }, 153 | "hasPrefix": func(substr string, str string) bool { return strings.HasPrefix(str, substr) }, 154 | "hasSuffix": func(substr string, str string) bool { return strings.HasSuffix(str, substr) }, 155 | "quote": quote, 156 | "squote": squote, 157 | "cat": cat, 158 | "indent": indent, 159 | "nindent": nindent, 160 | "replace": replace, 161 | "plural": plural, 162 | "sha1sum": sha1sum, 163 | "sha256sum": sha256sum, 164 | "sha512sum": sha512sum, 165 | "adler32sum": adler32sum, 166 | "toString": strval, 167 | 168 | // Wrap Atoi to stop errors. 169 | "atoi": func(a string) int { i, _ := strconv.Atoi(a); return i }, 170 | "int64": toInt64, 171 | "int": toInt, 172 | "float64": toFloat64, 173 | "seq": seq, 174 | "toDecimal": toDecimal, 175 | 176 | //"gt": func(a, b int) bool {return a > b}, 177 | //"gte": func(a, b int) bool {return a >= b}, 178 | //"lt": func(a, b int) bool {return a < b}, 179 | //"lte": func(a, b int) bool {return a <= b}, 180 | 181 | // split "/" foo/bar returns map[int]string{0: foo, 1: bar} 182 | "split": split, 183 | "splitList": func(sep, orig string) []string { return strings.Split(orig, sep) }, 184 | // splitn "/" foo/bar/fuu returns map[int]string{0: foo, 1: bar/fuu} 185 | "splitn": splitn, 186 | "toStrings": strslice, 187 | 188 | "until": until, 189 | "untilStep": untilStep, 190 | 191 | // VERY basic arithmetic. 192 | "add1": func(i interface{}) int64 { return toInt64(i) + 1 }, 193 | "add": func(i ...interface{}) int64 { 194 | var a int64 = 0 195 | for _, b := range i { 196 | a += toInt64(b) 197 | } 198 | return a 199 | }, 200 | "sub": func(a, b interface{}) int64 { return toInt64(a) - toInt64(b) }, 201 | "div": func(a, b interface{}) int64 { return toInt64(a) / toInt64(b) }, 202 | "mod": func(a, b interface{}) int64 { return toInt64(a) % toInt64(b) }, 203 | "mul": func(a interface{}, v ...interface{}) int64 { 204 | val := toInt64(a) 205 | for _, b := range v { 206 | val = val * toInt64(b) 207 | } 208 | return val 209 | }, 210 | "randInt": func(min, max int) int { return rand.Intn(max-min) + min }, 211 | "add1f": func(i interface{}) float64 { 212 | return execDecimalOp(i, []interface{}{1}, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Add(d2) }) 213 | }, 214 | "addf": func(i ...interface{}) float64 { 215 | a := interface{}(float64(0)) 216 | return execDecimalOp(a, i, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Add(d2) }) 217 | }, 218 | "subf": func(a interface{}, v ...interface{}) float64 { 219 | return execDecimalOp(a, v, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Sub(d2) }) 220 | }, 221 | "divf": func(a interface{}, v ...interface{}) float64 { 222 | return execDecimalOp(a, v, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Div(d2) }) 223 | }, 224 | "mulf": func(a interface{}, v ...interface{}) float64 { 225 | return execDecimalOp(a, v, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Mul(d2) }) 226 | }, 227 | "biggest": max, 228 | "max": max, 229 | "min": min, 230 | "maxf": maxf, 231 | "minf": minf, 232 | "ceil": ceil, 233 | "floor": floor, 234 | "round": round, 235 | 236 | // string slices. Note that we reverse the order b/c that's better 237 | // for template processing. 238 | "join": join, 239 | "sortAlpha": sortAlpha, 240 | 241 | // Defaults 242 | "default": dfault, 243 | "empty": empty, 244 | "coalesce": coalesce, 245 | "all": all, 246 | "any": any, 247 | "compact": compact, 248 | "mustCompact": mustCompact, 249 | "fromJson": fromJson, 250 | "toJson": toJson, 251 | "toPrettyJson": toPrettyJson, 252 | "toRawJson": toRawJson, 253 | "mustFromJson": mustFromJson, 254 | "mustToJson": mustToJson, 255 | "mustToPrettyJson": mustToPrettyJson, 256 | "mustToRawJson": mustToRawJson, 257 | "ternary": ternary, 258 | "deepCopy": deepCopy, 259 | "mustDeepCopy": mustDeepCopy, 260 | 261 | // Reflection 262 | "typeOf": typeOf, 263 | "typeIs": typeIs, 264 | "typeIsLike": typeIsLike, 265 | "kindOf": kindOf, 266 | "kindIs": kindIs, 267 | "deepEqual": reflect.DeepEqual, 268 | 269 | // OS: 270 | "env": os.Getenv, 271 | "expandenv": os.ExpandEnv, 272 | 273 | // Network: 274 | "getHostByName": getHostByName, 275 | 276 | // Paths: 277 | "base": path.Base, 278 | "dir": path.Dir, 279 | "clean": path.Clean, 280 | "ext": path.Ext, 281 | "isAbs": path.IsAbs, 282 | 283 | // Filepaths: 284 | "osBase": filepath.Base, 285 | "osClean": filepath.Clean, 286 | "osDir": filepath.Dir, 287 | "osExt": filepath.Ext, 288 | "osIsAbs": filepath.IsAbs, 289 | 290 | // Encoding: 291 | "b64enc": base64encode, 292 | "b64dec": base64decode, 293 | "b32enc": base32encode, 294 | "b32dec": base32decode, 295 | 296 | // Data Structures: 297 | "tuple": list, // FIXME: with the addition of append/prepend these are no longer immutable. 298 | "list": list, 299 | "dict": dict, 300 | "get": get, 301 | "set": set, 302 | "unset": unset, 303 | "hasKey": hasKey, 304 | "pluck": pluck, 305 | "keys": keys, 306 | "pick": pick, 307 | "omit": omit, 308 | "merge": merge, 309 | "mergeOverwrite": mergeOverwrite, 310 | "mustMerge": mustMerge, 311 | "mustMergeOverwrite": mustMergeOverwrite, 312 | "values": values, 313 | 314 | "append": push, "push": push, 315 | "mustAppend": mustPush, "mustPush": mustPush, 316 | "prepend": prepend, 317 | "mustPrepend": mustPrepend, 318 | "first": first, 319 | "mustFirst": mustFirst, 320 | "rest": rest, 321 | "mustRest": mustRest, 322 | "last": last, 323 | "mustLast": mustLast, 324 | "initial": initial, 325 | "mustInitial": mustInitial, 326 | "reverse": reverse, 327 | "mustReverse": mustReverse, 328 | "uniq": uniq, 329 | "mustUniq": mustUniq, 330 | "without": without, 331 | "mustWithout": mustWithout, 332 | "has": has, 333 | "mustHas": mustHas, 334 | "slice": slice, 335 | "mustSlice": mustSlice, 336 | "concat": concat, 337 | "dig": dig, 338 | "chunk": chunk, 339 | "mustChunk": mustChunk, 340 | 341 | // Crypto: 342 | "bcrypt": bcrypt, 343 | "htpasswd": htpasswd, 344 | "genPrivateKey": generatePrivateKey, 345 | "derivePassword": derivePassword, 346 | "buildCustomCert": buildCustomCertificate, 347 | "genCA": generateCertificateAuthority, 348 | "genCAWithKey": generateCertificateAuthorityWithPEMKey, 349 | "genSelfSignedCert": generateSelfSignedCertificate, 350 | "genSelfSignedCertWithKey": generateSelfSignedCertificateWithPEMKey, 351 | "genSignedCert": generateSignedCertificate, 352 | "genSignedCertWithKey": generateSignedCertificateWithPEMKey, 353 | "encryptAES": encryptAES, 354 | "decryptAES": decryptAES, 355 | "randBytes": randBytes, 356 | 357 | // UUIDs: 358 | "uuidv4": uuidv4, 359 | 360 | // SemVer: 361 | "semver": semver, 362 | "semverCompare": semverCompare, 363 | 364 | // Flow Control: 365 | "fail": func(msg string) (string, error) { return "", errors.New(msg) }, 366 | 367 | // Regex 368 | "regexMatch": regexMatch, 369 | "mustRegexMatch": mustRegexMatch, 370 | "regexFindAll": regexFindAll, 371 | "mustRegexFindAll": mustRegexFindAll, 372 | "regexFind": regexFind, 373 | "mustRegexFind": mustRegexFind, 374 | "regexReplaceAll": regexReplaceAll, 375 | "mustRegexReplaceAll": mustRegexReplaceAll, 376 | "regexReplaceAllLiteral": regexReplaceAllLiteral, 377 | "mustRegexReplaceAllLiteral": mustRegexReplaceAllLiteral, 378 | "regexSplit": regexSplit, 379 | "mustRegexSplit": mustRegexSplit, 380 | "regexQuoteMeta": regexQuoteMeta, 381 | 382 | // URLs: 383 | "urlParse": urlParse, 384 | "urlJoin": urlJoin, 385 | } 386 | -------------------------------------------------------------------------------- /functions_linux_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestOsBase(t *testing.T) { 10 | assert.NoError(t, runt(`{{ osBase "foo/bar" }}`, "bar")) 11 | } 12 | 13 | func TestOsDir(t *testing.T) { 14 | assert.NoError(t, runt(`{{ osDir "foo/bar/baz" }}`, "foo/bar")) 15 | } 16 | 17 | func TestOsIsAbs(t *testing.T) { 18 | assert.NoError(t, runt(`{{ osIsAbs "/foo" }}`, "true")) 19 | assert.NoError(t, runt(`{{ osIsAbs "foo" }}`, "false")) 20 | } 21 | 22 | func TestOsClean(t *testing.T) { 23 | assert.NoError(t, runt(`{{ osClean "/foo/../foo/../bar" }}`, "/bar")) 24 | } 25 | 26 | func TestOsExt(t *testing.T) { 27 | assert.NoError(t, runt(`{{ osExt "/foo/bar/baz.txt" }}`, ".txt")) 28 | } 29 | -------------------------------------------------------------------------------- /functions_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "testing" 9 | "text/template" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestEnv(t *testing.T) { 16 | os.Setenv("FOO", "bar") 17 | tpl := `{{env "FOO"}}` 18 | if err := runt(tpl, "bar"); err != nil { 19 | t.Error(err) 20 | } 21 | } 22 | 23 | func TestExpandEnv(t *testing.T) { 24 | os.Setenv("FOO", "bar") 25 | tpl := `{{expandenv "Hello $FOO"}}` 26 | if err := runt(tpl, "Hello bar"); err != nil { 27 | t.Error(err) 28 | } 29 | } 30 | 31 | func TestBase(t *testing.T) { 32 | assert.NoError(t, runt(`{{ base "foo/bar" }}`, "bar")) 33 | } 34 | 35 | func TestDir(t *testing.T) { 36 | assert.NoError(t, runt(`{{ dir "foo/bar/baz" }}`, "foo/bar")) 37 | } 38 | 39 | func TestIsAbs(t *testing.T) { 40 | assert.NoError(t, runt(`{{ isAbs "/foo" }}`, "true")) 41 | assert.NoError(t, runt(`{{ isAbs "foo" }}`, "false")) 42 | } 43 | 44 | func TestClean(t *testing.T) { 45 | assert.NoError(t, runt(`{{ clean "/foo/../foo/../bar" }}`, "/bar")) 46 | } 47 | 48 | func TestExt(t *testing.T) { 49 | assert.NoError(t, runt(`{{ ext "/foo/bar/baz.txt" }}`, ".txt")) 50 | } 51 | 52 | func TestSnakeCase(t *testing.T) { 53 | assert.NoError(t, runt(`{{ snakecase "FirstName" }}`, "first_name")) 54 | assert.NoError(t, runt(`{{ snakecase "HTTPServer" }}`, "http_server")) 55 | assert.NoError(t, runt(`{{ snakecase "NoHTTPS" }}`, "no_https")) 56 | assert.NoError(t, runt(`{{ snakecase "GO_PATH" }}`, "go_path")) 57 | assert.NoError(t, runt(`{{ snakecase "GO PATH" }}`, "go_path")) 58 | assert.NoError(t, runt(`{{ snakecase "GO-PATH" }}`, "go_path")) 59 | } 60 | 61 | func TestCamelCase(t *testing.T) { 62 | assert.NoError(t, runt(`{{ camelcase "http_server" }}`, "HttpServer")) 63 | assert.NoError(t, runt(`{{ camelcase "_camel_case" }}`, "_CamelCase")) 64 | assert.NoError(t, runt(`{{ camelcase "no_https" }}`, "NoHttps")) 65 | assert.NoError(t, runt(`{{ camelcase "_complex__case_" }}`, "_Complex_Case_")) 66 | assert.NoError(t, runt(`{{ camelcase "all" }}`, "All")) 67 | } 68 | 69 | func TestKebabCase(t *testing.T) { 70 | assert.NoError(t, runt(`{{ kebabcase "FirstName" }}`, "first-name")) 71 | assert.NoError(t, runt(`{{ kebabcase "HTTPServer" }}`, "http-server")) 72 | assert.NoError(t, runt(`{{ kebabcase "NoHTTPS" }}`, "no-https")) 73 | assert.NoError(t, runt(`{{ kebabcase "GO_PATH" }}`, "go-path")) 74 | assert.NoError(t, runt(`{{ kebabcase "GO PATH" }}`, "go-path")) 75 | assert.NoError(t, runt(`{{ kebabcase "GO-PATH" }}`, "go-path")) 76 | } 77 | 78 | func TestShuffle(t *testing.T) { 79 | defer rand.Seed(time.Now().UnixNano()) 80 | rand.Seed(1) 81 | // Because we're using a random number generator, we need these to go in 82 | // a predictable sequence: 83 | assert.NoError(t, runt(`{{ shuffle "Hello World" }}`, "rldo HWlloe")) 84 | } 85 | 86 | func TestRegex(t *testing.T) { 87 | assert.NoError(t, runt(`{{ regexQuoteMeta "1.2.3" }}`, "1\\.2\\.3")) 88 | assert.NoError(t, runt(`{{ regexQuoteMeta "pretzel" }}`, "pretzel")) 89 | } 90 | 91 | // runt runs a template and checks that the output exactly matches the expected string. 92 | func runt(tpl, expect string) error { 93 | return runtv(tpl, expect, map[string]string{}) 94 | } 95 | 96 | // runtv takes a template, and expected return, and values for substitution. 97 | // 98 | // It runs the template and verifies that the output is an exact match. 99 | func runtv(tpl, expect string, vars interface{}) error { 100 | fmap := TxtFuncMap() 101 | t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) 102 | var b bytes.Buffer 103 | err := t.Execute(&b, vars) 104 | if err != nil { 105 | return err 106 | } 107 | if expect != b.String() { 108 | return fmt.Errorf("Expected '%s', got '%s'", expect, b.String()) 109 | } 110 | return nil 111 | } 112 | 113 | // runRaw runs a template with the given variables and returns the result. 114 | func runRaw(tpl string, vars interface{}) (string, error) { 115 | fmap := TxtFuncMap() 116 | t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) 117 | var b bytes.Buffer 118 | err := t.Execute(&b, vars) 119 | if err != nil { 120 | return "", err 121 | } 122 | return b.String(), nil 123 | } 124 | -------------------------------------------------------------------------------- /functions_windows_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestOsBase(t *testing.T) { 10 | assert.NoError(t, runt(`{{ osBase "C:\\foo\\bar" }}`, "bar")) 11 | } 12 | 13 | func TestOsDir(t *testing.T) { 14 | assert.NoError(t, runt(`{{ osDir "C:\\foo\\bar\\baz" }}`, "C:\\foo\\bar")) 15 | } 16 | 17 | func TestOsIsAbs(t *testing.T) { 18 | assert.NoError(t, runt(`{{ osIsAbs "C:\\foo" }}`, "true")) 19 | assert.NoError(t, runt(`{{ osIsAbs "foo" }}`, "false")) 20 | } 21 | 22 | func TestOsClean(t *testing.T) { 23 | assert.NoError(t, runt(`{{ osClean "C:\\foo\\..\\foo\\..\\bar" }}`, "C:\\bar")) 24 | } 25 | 26 | func TestOsExt(t *testing.T) { 27 | assert.NoError(t, runt(`{{ osExt "C:\\foo\\bar\\baz.txt" }}`, ".txt")) 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Masterminds/sprig/v3 2 | 3 | go 1.21 4 | 5 | require ( 6 | dario.cat/mergo v1.0.1 7 | github.com/Masterminds/goutils v1.1.1 8 | github.com/Masterminds/semver/v3 v3.3.0 9 | github.com/google/uuid v1.6.0 10 | github.com/huandu/xstrings v1.5.0 11 | github.com/mitchellh/copystructure v1.2.0 12 | github.com/shopspring/decimal v1.4.0 13 | github.com/spf13/cast v1.7.0 14 | github.com/stretchr/testify v1.5.1 15 | golang.org/x/crypto v0.26.0 16 | ) 17 | 18 | require ( 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/google/go-cmp v0.6.0 // indirect 21 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 22 | github.com/pmezard/go-difflib v1.0.0 // indirect 23 | gopkg.in/yaml.v2 v2.3.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 4 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 5 | github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= 6 | github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 11 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 12 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 13 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 14 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 15 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 16 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 17 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 18 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 19 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 20 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 21 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 22 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 23 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 24 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 25 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 29 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 30 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 31 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 32 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 33 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 34 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 35 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 36 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 37 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 38 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 42 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 43 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 44 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 45 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 46 | -------------------------------------------------------------------------------- /issue_188_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIssue188(t *testing.T) { 8 | tests := map[string]string{ 9 | 10 | // This first test shows two merges and the merge is NOT A DEEP COPY MERGE. 11 | // The first merge puts $one on to $target. When the second merge of $two 12 | // on to $target the nested dict brought over from $one is changed on 13 | // $one as well as $target. 14 | `{{- $target := dict -}} 15 | {{- $one := dict "foo" (dict "bar" "baz") "qux" true -}} 16 | {{- $two := dict "foo" (dict "bar" "baz2") "qux" false -}} 17 | {{- mergeOverwrite $target $one | toString | trunc 0 }}{{ $__ := mergeOverwrite $target $two }}{{ $one }}`: "map[foo:map[bar:baz2] qux:true]", 18 | 19 | // This test uses deepCopy on $one to create a deep copy and then merge 20 | // that. In this case the merge of $two on to $target does not affect 21 | // $one because a deep copy was used for that merge. 22 | `{{- $target := dict -}} 23 | {{- $one := dict "foo" (dict "bar" "baz") "qux" true -}} 24 | {{- $two := dict "foo" (dict "bar" "baz2") "qux" false -}} 25 | {{- deepCopy $one | mergeOverwrite $target | toString | trunc 0 }}{{ $__ := mergeOverwrite $target $two }}{{ $one }}`: "map[foo:map[bar:baz] qux:true]", 26 | } 27 | 28 | for tpl, expect := range tests { 29 | if err := runt(tpl, expect); err != nil { 30 | t.Error(err) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "reflect" 7 | "sort" 8 | ) 9 | 10 | // Reflection is used in these functions so that slices and arrays of strings, 11 | // ints, and other types not implementing []interface{} can be worked with. 12 | // For example, this is useful if you need to work on the output of regexs. 13 | 14 | func list(v ...interface{}) []interface{} { 15 | return v 16 | } 17 | 18 | func push(list interface{}, v interface{}) []interface{} { 19 | l, err := mustPush(list, v) 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | return l 25 | } 26 | 27 | func mustPush(list interface{}, v interface{}) ([]interface{}, error) { 28 | tp := reflect.TypeOf(list).Kind() 29 | switch tp { 30 | case reflect.Slice, reflect.Array: 31 | l2 := reflect.ValueOf(list) 32 | 33 | l := l2.Len() 34 | nl := make([]interface{}, l) 35 | for i := 0; i < l; i++ { 36 | nl[i] = l2.Index(i).Interface() 37 | } 38 | 39 | return append(nl, v), nil 40 | 41 | default: 42 | return nil, fmt.Errorf("Cannot push on type %s", tp) 43 | } 44 | } 45 | 46 | func prepend(list interface{}, v interface{}) []interface{} { 47 | l, err := mustPrepend(list, v) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | return l 53 | } 54 | 55 | func mustPrepend(list interface{}, v interface{}) ([]interface{}, error) { 56 | //return append([]interface{}{v}, list...) 57 | 58 | tp := reflect.TypeOf(list).Kind() 59 | switch tp { 60 | case reflect.Slice, reflect.Array: 61 | l2 := reflect.ValueOf(list) 62 | 63 | l := l2.Len() 64 | nl := make([]interface{}, l) 65 | for i := 0; i < l; i++ { 66 | nl[i] = l2.Index(i).Interface() 67 | } 68 | 69 | return append([]interface{}{v}, nl...), nil 70 | 71 | default: 72 | return nil, fmt.Errorf("Cannot prepend on type %s", tp) 73 | } 74 | } 75 | 76 | func chunk(size int, list interface{}) [][]interface{} { 77 | l, err := mustChunk(size, list) 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | return l 83 | } 84 | 85 | func mustChunk(size int, list interface{}) ([][]interface{}, error) { 86 | tp := reflect.TypeOf(list).Kind() 87 | switch tp { 88 | case reflect.Slice, reflect.Array: 89 | l2 := reflect.ValueOf(list) 90 | 91 | l := l2.Len() 92 | 93 | cs := int(math.Floor(float64(l-1)/float64(size)) + 1) 94 | nl := make([][]interface{}, cs) 95 | 96 | for i := 0; i < cs; i++ { 97 | clen := size 98 | if i == cs-1 { 99 | clen = int(math.Floor(math.Mod(float64(l), float64(size)))) 100 | if clen == 0 { 101 | clen = size 102 | } 103 | } 104 | 105 | nl[i] = make([]interface{}, clen) 106 | 107 | for j := 0; j < clen; j++ { 108 | ix := i*size + j 109 | nl[i][j] = l2.Index(ix).Interface() 110 | } 111 | } 112 | 113 | return nl, nil 114 | 115 | default: 116 | return nil, fmt.Errorf("Cannot chunk type %s", tp) 117 | } 118 | } 119 | 120 | func last(list interface{}) interface{} { 121 | l, err := mustLast(list) 122 | if err != nil { 123 | panic(err) 124 | } 125 | 126 | return l 127 | } 128 | 129 | func mustLast(list interface{}) (interface{}, error) { 130 | tp := reflect.TypeOf(list).Kind() 131 | switch tp { 132 | case reflect.Slice, reflect.Array: 133 | l2 := reflect.ValueOf(list) 134 | 135 | l := l2.Len() 136 | if l == 0 { 137 | return nil, nil 138 | } 139 | 140 | return l2.Index(l - 1).Interface(), nil 141 | default: 142 | return nil, fmt.Errorf("Cannot find last on type %s", tp) 143 | } 144 | } 145 | 146 | func first(list interface{}) interface{} { 147 | l, err := mustFirst(list) 148 | if err != nil { 149 | panic(err) 150 | } 151 | 152 | return l 153 | } 154 | 155 | func mustFirst(list interface{}) (interface{}, error) { 156 | tp := reflect.TypeOf(list).Kind() 157 | switch tp { 158 | case reflect.Slice, reflect.Array: 159 | l2 := reflect.ValueOf(list) 160 | 161 | l := l2.Len() 162 | if l == 0 { 163 | return nil, nil 164 | } 165 | 166 | return l2.Index(0).Interface(), nil 167 | default: 168 | return nil, fmt.Errorf("Cannot find first on type %s", tp) 169 | } 170 | } 171 | 172 | func rest(list interface{}) []interface{} { 173 | l, err := mustRest(list) 174 | if err != nil { 175 | panic(err) 176 | } 177 | 178 | return l 179 | } 180 | 181 | func mustRest(list interface{}) ([]interface{}, error) { 182 | tp := reflect.TypeOf(list).Kind() 183 | switch tp { 184 | case reflect.Slice, reflect.Array: 185 | l2 := reflect.ValueOf(list) 186 | 187 | l := l2.Len() 188 | if l == 0 { 189 | return nil, nil 190 | } 191 | 192 | nl := make([]interface{}, l-1) 193 | for i := 1; i < l; i++ { 194 | nl[i-1] = l2.Index(i).Interface() 195 | } 196 | 197 | return nl, nil 198 | default: 199 | return nil, fmt.Errorf("Cannot find rest on type %s", tp) 200 | } 201 | } 202 | 203 | func initial(list interface{}) []interface{} { 204 | l, err := mustInitial(list) 205 | if err != nil { 206 | panic(err) 207 | } 208 | 209 | return l 210 | } 211 | 212 | func mustInitial(list interface{}) ([]interface{}, error) { 213 | tp := reflect.TypeOf(list).Kind() 214 | switch tp { 215 | case reflect.Slice, reflect.Array: 216 | l2 := reflect.ValueOf(list) 217 | 218 | l := l2.Len() 219 | if l == 0 { 220 | return nil, nil 221 | } 222 | 223 | nl := make([]interface{}, l-1) 224 | for i := 0; i < l-1; i++ { 225 | nl[i] = l2.Index(i).Interface() 226 | } 227 | 228 | return nl, nil 229 | default: 230 | return nil, fmt.Errorf("Cannot find initial on type %s", tp) 231 | } 232 | } 233 | 234 | func sortAlpha(list interface{}) []string { 235 | k := reflect.Indirect(reflect.ValueOf(list)).Kind() 236 | switch k { 237 | case reflect.Slice, reflect.Array: 238 | a := strslice(list) 239 | s := sort.StringSlice(a) 240 | s.Sort() 241 | return s 242 | } 243 | return []string{strval(list)} 244 | } 245 | 246 | func reverse(v interface{}) []interface{} { 247 | l, err := mustReverse(v) 248 | if err != nil { 249 | panic(err) 250 | } 251 | 252 | return l 253 | } 254 | 255 | func mustReverse(v interface{}) ([]interface{}, error) { 256 | tp := reflect.TypeOf(v).Kind() 257 | switch tp { 258 | case reflect.Slice, reflect.Array: 259 | l2 := reflect.ValueOf(v) 260 | 261 | l := l2.Len() 262 | // We do not sort in place because the incoming array should not be altered. 263 | nl := make([]interface{}, l) 264 | for i := 0; i < l; i++ { 265 | nl[l-i-1] = l2.Index(i).Interface() 266 | } 267 | 268 | return nl, nil 269 | default: 270 | return nil, fmt.Errorf("Cannot find reverse on type %s", tp) 271 | } 272 | } 273 | 274 | func compact(list interface{}) []interface{} { 275 | l, err := mustCompact(list) 276 | if err != nil { 277 | panic(err) 278 | } 279 | 280 | return l 281 | } 282 | 283 | func mustCompact(list interface{}) ([]interface{}, error) { 284 | tp := reflect.TypeOf(list).Kind() 285 | switch tp { 286 | case reflect.Slice, reflect.Array: 287 | l2 := reflect.ValueOf(list) 288 | 289 | l := l2.Len() 290 | nl := []interface{}{} 291 | var item interface{} 292 | for i := 0; i < l; i++ { 293 | item = l2.Index(i).Interface() 294 | if !empty(item) { 295 | nl = append(nl, item) 296 | } 297 | } 298 | 299 | return nl, nil 300 | default: 301 | return nil, fmt.Errorf("Cannot compact on type %s", tp) 302 | } 303 | } 304 | 305 | func uniq(list interface{}) []interface{} { 306 | l, err := mustUniq(list) 307 | if err != nil { 308 | panic(err) 309 | } 310 | 311 | return l 312 | } 313 | 314 | func mustUniq(list interface{}) ([]interface{}, error) { 315 | tp := reflect.TypeOf(list).Kind() 316 | switch tp { 317 | case reflect.Slice, reflect.Array: 318 | l2 := reflect.ValueOf(list) 319 | 320 | l := l2.Len() 321 | dest := []interface{}{} 322 | var item interface{} 323 | for i := 0; i < l; i++ { 324 | item = l2.Index(i).Interface() 325 | if !inList(dest, item) { 326 | dest = append(dest, item) 327 | } 328 | } 329 | 330 | return dest, nil 331 | default: 332 | return nil, fmt.Errorf("Cannot find uniq on type %s", tp) 333 | } 334 | } 335 | 336 | func inList(haystack []interface{}, needle interface{}) bool { 337 | for _, h := range haystack { 338 | if reflect.DeepEqual(needle, h) { 339 | return true 340 | } 341 | } 342 | return false 343 | } 344 | 345 | func without(list interface{}, omit ...interface{}) []interface{} { 346 | l, err := mustWithout(list, omit...) 347 | if err != nil { 348 | panic(err) 349 | } 350 | 351 | return l 352 | } 353 | 354 | func mustWithout(list interface{}, omit ...interface{}) ([]interface{}, error) { 355 | tp := reflect.TypeOf(list).Kind() 356 | switch tp { 357 | case reflect.Slice, reflect.Array: 358 | l2 := reflect.ValueOf(list) 359 | 360 | l := l2.Len() 361 | res := []interface{}{} 362 | var item interface{} 363 | for i := 0; i < l; i++ { 364 | item = l2.Index(i).Interface() 365 | if !inList(omit, item) { 366 | res = append(res, item) 367 | } 368 | } 369 | 370 | return res, nil 371 | default: 372 | return nil, fmt.Errorf("Cannot find without on type %s", tp) 373 | } 374 | } 375 | 376 | func has(needle interface{}, haystack interface{}) bool { 377 | l, err := mustHas(needle, haystack) 378 | if err != nil { 379 | panic(err) 380 | } 381 | 382 | return l 383 | } 384 | 385 | func mustHas(needle interface{}, haystack interface{}) (bool, error) { 386 | if haystack == nil { 387 | return false, nil 388 | } 389 | tp := reflect.TypeOf(haystack).Kind() 390 | switch tp { 391 | case reflect.Slice, reflect.Array: 392 | l2 := reflect.ValueOf(haystack) 393 | var item interface{} 394 | l := l2.Len() 395 | for i := 0; i < l; i++ { 396 | item = l2.Index(i).Interface() 397 | if reflect.DeepEqual(needle, item) { 398 | return true, nil 399 | } 400 | } 401 | 402 | return false, nil 403 | default: 404 | return false, fmt.Errorf("Cannot find has on type %s", tp) 405 | } 406 | } 407 | 408 | // $list := [1, 2, 3, 4, 5] 409 | // slice $list -> list[0:5] = list[:] 410 | // slice $list 0 3 -> list[0:3] = list[:3] 411 | // slice $list 3 5 -> list[3:5] 412 | // slice $list 3 -> list[3:5] = list[3:] 413 | func slice(list interface{}, indices ...interface{}) interface{} { 414 | l, err := mustSlice(list, indices...) 415 | if err != nil { 416 | panic(err) 417 | } 418 | 419 | return l 420 | } 421 | 422 | func mustSlice(list interface{}, indices ...interface{}) (interface{}, error) { 423 | tp := reflect.TypeOf(list).Kind() 424 | switch tp { 425 | case reflect.Slice, reflect.Array: 426 | l2 := reflect.ValueOf(list) 427 | 428 | l := l2.Len() 429 | if l == 0 { 430 | return nil, nil 431 | } 432 | 433 | var start, end int 434 | if len(indices) > 0 { 435 | start = toInt(indices[0]) 436 | } 437 | if len(indices) < 2 { 438 | end = l 439 | } else { 440 | end = toInt(indices[1]) 441 | } 442 | 443 | return l2.Slice(start, end).Interface(), nil 444 | default: 445 | return nil, fmt.Errorf("list should be type of slice or array but %s", tp) 446 | } 447 | } 448 | 449 | func concat(lists ...interface{}) interface{} { 450 | var res []interface{} 451 | for _, list := range lists { 452 | tp := reflect.TypeOf(list).Kind() 453 | switch tp { 454 | case reflect.Slice, reflect.Array: 455 | l2 := reflect.ValueOf(list) 456 | for i := 0; i < l2.Len(); i++ { 457 | res = append(res, l2.Index(i).Interface()) 458 | } 459 | default: 460 | panic(fmt.Sprintf("Cannot concat type %s as list", tp)) 461 | } 462 | } 463 | return res 464 | } 465 | -------------------------------------------------------------------------------- /list_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTuple(t *testing.T) { 10 | tpl := `{{$t := tuple 1 "a" "foo"}}{{index $t 2}}{{index $t 0 }}{{index $t 1}}` 11 | if err := runt(tpl, "foo1a"); err != nil { 12 | t.Error(err) 13 | } 14 | } 15 | 16 | func TestList(t *testing.T) { 17 | tpl := `{{$t := list 1 "a" "foo"}}{{index $t 2}}{{index $t 0 }}{{index $t 1}}` 18 | if err := runt(tpl, "foo1a"); err != nil { 19 | t.Error(err) 20 | } 21 | } 22 | 23 | func TestPush(t *testing.T) { 24 | // Named `append` in the function map 25 | tests := map[string]string{ 26 | `{{ $t := tuple 1 2 3 }}{{ append $t 4 | len }}`: "4", 27 | `{{ $t := tuple 1 2 3 4 }}{{ append $t 5 | join "-" }}`: "1-2-3-4-5", 28 | `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ append $t "qux" | join "-" }}`: "foo-bar-baz-qux", 29 | } 30 | for tpl, expect := range tests { 31 | assert.NoError(t, runt(tpl, expect)) 32 | } 33 | } 34 | 35 | func TestMustPush(t *testing.T) { 36 | // Named `append` in the function map 37 | tests := map[string]string{ 38 | `{{ $t := tuple 1 2 3 }}{{ mustAppend $t 4 | len }}`: "4", 39 | `{{ $t := tuple 1 2 3 4 }}{{ mustAppend $t 5 | join "-" }}`: "1-2-3-4-5", 40 | `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ mustPush $t "qux" | join "-" }}`: "foo-bar-baz-qux", 41 | } 42 | for tpl, expect := range tests { 43 | assert.NoError(t, runt(tpl, expect)) 44 | } 45 | } 46 | 47 | func TestChunk(t *testing.T) { 48 | tests := map[string]string{ 49 | `{{ tuple 1 2 3 4 5 6 7 | chunk 3 | len }}`: "3", 50 | `{{ tuple | chunk 3 | len }}`: "0", 51 | `{{ range ( tuple 1 2 3 4 5 6 7 8 9 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8-9|", 52 | `{{ range ( tuple 1 2 3 4 5 6 7 8 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8|", 53 | `{{ range ( tuple 1 2 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2|", 54 | } 55 | for tpl, expect := range tests { 56 | assert.NoError(t, runt(tpl, expect)) 57 | } 58 | } 59 | 60 | func TestMustChunk(t *testing.T) { 61 | tests := map[string]string{ 62 | `{{ tuple 1 2 3 4 5 6 7 | mustChunk 3 | len }}`: "3", 63 | `{{ tuple | mustChunk 3 | len }}`: "0", 64 | `{{ range ( tuple 1 2 3 4 5 6 7 8 9 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8-9|", 65 | `{{ range ( tuple 1 2 3 4 5 6 7 8 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8|", 66 | `{{ range ( tuple 1 2 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2|", 67 | } 68 | for tpl, expect := range tests { 69 | assert.NoError(t, runt(tpl, expect)) 70 | } 71 | } 72 | 73 | func TestPrepend(t *testing.T) { 74 | tests := map[string]string{ 75 | `{{ $t := tuple 1 2 3 }}{{ prepend $t 0 | len }}`: "4", 76 | `{{ $t := tuple 1 2 3 4 }}{{ prepend $t 0 | join "-" }}`: "0-1-2-3-4", 77 | `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ prepend $t "qux" | join "-" }}`: "qux-foo-bar-baz", 78 | } 79 | for tpl, expect := range tests { 80 | assert.NoError(t, runt(tpl, expect)) 81 | } 82 | } 83 | 84 | func TestMustPrepend(t *testing.T) { 85 | tests := map[string]string{ 86 | `{{ $t := tuple 1 2 3 }}{{ mustPrepend $t 0 | len }}`: "4", 87 | `{{ $t := tuple 1 2 3 4 }}{{ mustPrepend $t 0 | join "-" }}`: "0-1-2-3-4", 88 | `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ mustPrepend $t "qux" | join "-" }}`: "qux-foo-bar-baz", 89 | } 90 | for tpl, expect := range tests { 91 | assert.NoError(t, runt(tpl, expect)) 92 | } 93 | } 94 | 95 | func TestFirst(t *testing.T) { 96 | tests := map[string]string{ 97 | `{{ list 1 2 3 | first }}`: "1", 98 | `{{ list | first }}`: "", 99 | `{{ regexSplit "/src/" "foo/src/bar" -1 | first }}`: "foo", 100 | } 101 | for tpl, expect := range tests { 102 | assert.NoError(t, runt(tpl, expect)) 103 | } 104 | } 105 | 106 | func TestMustFirst(t *testing.T) { 107 | tests := map[string]string{ 108 | `{{ list 1 2 3 | mustFirst }}`: "1", 109 | `{{ list | mustFirst }}`: "", 110 | `{{ regexSplit "/src/" "foo/src/bar" -1 | mustFirst }}`: "foo", 111 | } 112 | for tpl, expect := range tests { 113 | assert.NoError(t, runt(tpl, expect)) 114 | } 115 | } 116 | 117 | func TestLast(t *testing.T) { 118 | tests := map[string]string{ 119 | `{{ list 1 2 3 | last }}`: "3", 120 | `{{ list | last }}`: "", 121 | `{{ regexSplit "/src/" "foo/src/bar" -1 | last }}`: "bar", 122 | } 123 | for tpl, expect := range tests { 124 | assert.NoError(t, runt(tpl, expect)) 125 | } 126 | } 127 | 128 | func TestMustLast(t *testing.T) { 129 | tests := map[string]string{ 130 | `{{ list 1 2 3 | mustLast }}`: "3", 131 | `{{ list | mustLast }}`: "", 132 | `{{ regexSplit "/src/" "foo/src/bar" -1 | mustLast }}`: "bar", 133 | } 134 | for tpl, expect := range tests { 135 | assert.NoError(t, runt(tpl, expect)) 136 | } 137 | } 138 | 139 | func TestInitial(t *testing.T) { 140 | tests := map[string]string{ 141 | `{{ list 1 2 3 | initial | len }}`: "2", 142 | `{{ list 1 2 3 | initial | last }}`: "2", 143 | `{{ list 1 2 3 | initial | first }}`: "1", 144 | `{{ list | initial }}`: "[]", 145 | `{{ regexSplit "/" "foo/bar/baz" -1 | initial }}`: "[foo bar]", 146 | } 147 | for tpl, expect := range tests { 148 | assert.NoError(t, runt(tpl, expect)) 149 | } 150 | } 151 | 152 | func TestMustInitial(t *testing.T) { 153 | tests := map[string]string{ 154 | `{{ list 1 2 3 | mustInitial | len }}`: "2", 155 | `{{ list 1 2 3 | mustInitial | last }}`: "2", 156 | `{{ list 1 2 3 | mustInitial | first }}`: "1", 157 | `{{ list | mustInitial }}`: "[]", 158 | `{{ regexSplit "/" "foo/bar/baz" -1 | mustInitial }}`: "[foo bar]", 159 | } 160 | for tpl, expect := range tests { 161 | assert.NoError(t, runt(tpl, expect)) 162 | } 163 | } 164 | 165 | func TestRest(t *testing.T) { 166 | tests := map[string]string{ 167 | `{{ list 1 2 3 | rest | len }}`: "2", 168 | `{{ list 1 2 3 | rest | last }}`: "3", 169 | `{{ list 1 2 3 | rest | first }}`: "2", 170 | `{{ list | rest }}`: "[]", 171 | `{{ regexSplit "/" "foo/bar/baz" -1 | rest }}`: "[bar baz]", 172 | } 173 | for tpl, expect := range tests { 174 | assert.NoError(t, runt(tpl, expect)) 175 | } 176 | } 177 | 178 | func TestMustRest(t *testing.T) { 179 | tests := map[string]string{ 180 | `{{ list 1 2 3 | mustRest | len }}`: "2", 181 | `{{ list 1 2 3 | mustRest | last }}`: "3", 182 | `{{ list 1 2 3 | mustRest | first }}`: "2", 183 | `{{ list | mustRest }}`: "[]", 184 | `{{ regexSplit "/" "foo/bar/baz" -1 | mustRest }}`: "[bar baz]", 185 | } 186 | for tpl, expect := range tests { 187 | assert.NoError(t, runt(tpl, expect)) 188 | } 189 | } 190 | 191 | func TestReverse(t *testing.T) { 192 | tests := map[string]string{ 193 | `{{ list 1 2 3 | reverse | first }}`: "3", 194 | `{{ list 1 2 3 | reverse | rest | first }}`: "2", 195 | `{{ list 1 2 3 | reverse | last }}`: "1", 196 | `{{ list 1 2 3 4 | reverse }}`: "[4 3 2 1]", 197 | `{{ list 1 | reverse }}`: "[1]", 198 | `{{ list | reverse }}`: "[]", 199 | `{{ regexSplit "/" "foo/bar/baz" -1 | reverse }}`: "[baz bar foo]", 200 | } 201 | for tpl, expect := range tests { 202 | assert.NoError(t, runt(tpl, expect)) 203 | } 204 | } 205 | 206 | func TestMustReverse(t *testing.T) { 207 | tests := map[string]string{ 208 | `{{ list 1 2 3 | mustReverse | first }}`: "3", 209 | `{{ list 1 2 3 | mustReverse | rest | first }}`: "2", 210 | `{{ list 1 2 3 | mustReverse | last }}`: "1", 211 | `{{ list 1 2 3 4 | mustReverse }}`: "[4 3 2 1]", 212 | `{{ list 1 | mustReverse }}`: "[1]", 213 | `{{ list | mustReverse }}`: "[]", 214 | `{{ regexSplit "/" "foo/bar/baz" -1 | mustReverse }}`: "[baz bar foo]", 215 | } 216 | for tpl, expect := range tests { 217 | assert.NoError(t, runt(tpl, expect)) 218 | } 219 | } 220 | 221 | func TestCompact(t *testing.T) { 222 | tests := map[string]string{ 223 | `{{ list 1 0 "" "hello" | compact }}`: `[1 hello]`, 224 | `{{ list "" "" | compact }}`: `[]`, 225 | `{{ list | compact }}`: `[]`, 226 | `{{ regexSplit "/" "foo//bar" -1 | compact }}`: "[foo bar]", 227 | } 228 | for tpl, expect := range tests { 229 | assert.NoError(t, runt(tpl, expect)) 230 | } 231 | } 232 | 233 | func TestMustCompact(t *testing.T) { 234 | tests := map[string]string{ 235 | `{{ list 1 0 "" "hello" | mustCompact }}`: `[1 hello]`, 236 | `{{ list "" "" | mustCompact }}`: `[]`, 237 | `{{ list | mustCompact }}`: `[]`, 238 | `{{ regexSplit "/" "foo//bar" -1 | mustCompact }}`: "[foo bar]", 239 | } 240 | for tpl, expect := range tests { 241 | assert.NoError(t, runt(tpl, expect)) 242 | } 243 | } 244 | 245 | func TestUniq(t *testing.T) { 246 | tests := map[string]string{ 247 | `{{ list 1 2 3 4 | uniq }}`: `[1 2 3 4]`, 248 | `{{ list "a" "b" "c" "d" | uniq }}`: `[a b c d]`, 249 | `{{ list 1 1 1 1 2 2 2 2 | uniq }}`: `[1 2]`, 250 | `{{ list "foo" 1 1 1 1 "foo" "foo" | uniq }}`: `[foo 1]`, 251 | `{{ list | uniq }}`: `[]`, 252 | `{{ regexSplit "/" "foo/foo/bar" -1 | uniq }}`: "[foo bar]", 253 | } 254 | for tpl, expect := range tests { 255 | assert.NoError(t, runt(tpl, expect)) 256 | } 257 | } 258 | 259 | func TestMustUniq(t *testing.T) { 260 | tests := map[string]string{ 261 | `{{ list 1 2 3 4 | mustUniq }}`: `[1 2 3 4]`, 262 | `{{ list "a" "b" "c" "d" | mustUniq }}`: `[a b c d]`, 263 | `{{ list 1 1 1 1 2 2 2 2 | mustUniq }}`: `[1 2]`, 264 | `{{ list "foo" 1 1 1 1 "foo" "foo" | mustUniq }}`: `[foo 1]`, 265 | `{{ list | mustUniq }}`: `[]`, 266 | `{{ regexSplit "/" "foo/foo/bar" -1 | mustUniq }}`: "[foo bar]", 267 | } 268 | for tpl, expect := range tests { 269 | assert.NoError(t, runt(tpl, expect)) 270 | } 271 | } 272 | 273 | func TestWithout(t *testing.T) { 274 | tests := map[string]string{ 275 | `{{ without (list 1 2 3 4) 1 }}`: `[2 3 4]`, 276 | `{{ without (list "a" "b" "c" "d") "a" }}`: `[b c d]`, 277 | `{{ without (list 1 1 1 1 2) 1 }}`: `[2]`, 278 | `{{ without (list) 1 }}`: `[]`, 279 | `{{ without (list 1 2 3) }}`: `[1 2 3]`, 280 | `{{ without list }}`: `[]`, 281 | `{{ without (regexSplit "/" "foo/bar/baz" -1 ) "foo" }}`: "[bar baz]", 282 | } 283 | for tpl, expect := range tests { 284 | assert.NoError(t, runt(tpl, expect)) 285 | } 286 | } 287 | 288 | func TestMustWithout(t *testing.T) { 289 | tests := map[string]string{ 290 | `{{ mustWithout (list 1 2 3 4) 1 }}`: `[2 3 4]`, 291 | `{{ mustWithout (list "a" "b" "c" "d") "a" }}`: `[b c d]`, 292 | `{{ mustWithout (list 1 1 1 1 2) 1 }}`: `[2]`, 293 | `{{ mustWithout (list) 1 }}`: `[]`, 294 | `{{ mustWithout (list 1 2 3) }}`: `[1 2 3]`, 295 | `{{ mustWithout list }}`: `[]`, 296 | `{{ mustWithout (regexSplit "/" "foo/bar/baz" -1 ) "foo" }}`: "[bar baz]", 297 | } 298 | for tpl, expect := range tests { 299 | assert.NoError(t, runt(tpl, expect)) 300 | } 301 | } 302 | 303 | func TestHas(t *testing.T) { 304 | tests := map[string]string{ 305 | `{{ list 1 2 3 | has 1 }}`: `true`, 306 | `{{ list 1 2 3 | has 4 }}`: `false`, 307 | `{{ regexSplit "/" "foo/bar/baz" -1 | has "bar" }}`: `true`, 308 | `{{ has "bar" nil }}`: `false`, 309 | } 310 | for tpl, expect := range tests { 311 | assert.NoError(t, runt(tpl, expect)) 312 | } 313 | } 314 | 315 | func TestMustHas(t *testing.T) { 316 | tests := map[string]string{ 317 | `{{ list 1 2 3 | mustHas 1 }}`: `true`, 318 | `{{ list 1 2 3 | mustHas 4 }}`: `false`, 319 | `{{ regexSplit "/" "foo/bar/baz" -1 | mustHas "bar" }}`: `true`, 320 | `{{ mustHas "bar" nil }}`: `false`, 321 | } 322 | for tpl, expect := range tests { 323 | assert.NoError(t, runt(tpl, expect)) 324 | } 325 | } 326 | 327 | func TestSlice(t *testing.T) { 328 | tests := map[string]string{ 329 | `{{ slice (list 1 2 3) }}`: "[1 2 3]", 330 | `{{ slice (list 1 2 3) 0 1 }}`: "[1]", 331 | `{{ slice (list 1 2 3) 1 3 }}`: "[2 3]", 332 | `{{ slice (list 1 2 3) 1 }}`: "[2 3]", 333 | `{{ slice (regexSplit "/" "foo/bar/baz" -1) 1 2 }}`: "[bar]", 334 | } 335 | for tpl, expect := range tests { 336 | assert.NoError(t, runt(tpl, expect)) 337 | } 338 | } 339 | 340 | func TestMustSlice(t *testing.T) { 341 | tests := map[string]string{ 342 | `{{ mustSlice (list 1 2 3) }}`: "[1 2 3]", 343 | `{{ mustSlice (list 1 2 3) 0 1 }}`: "[1]", 344 | `{{ mustSlice (list 1 2 3) 1 3 }}`: "[2 3]", 345 | `{{ mustSlice (list 1 2 3) 1 }}`: "[2 3]", 346 | `{{ mustSlice (regexSplit "/" "foo/bar/baz" -1) 1 2 }}`: "[bar]", 347 | } 348 | for tpl, expect := range tests { 349 | assert.NoError(t, runt(tpl, expect)) 350 | } 351 | } 352 | 353 | func TestConcat(t *testing.T) { 354 | tests := map[string]string{ 355 | `{{ concat (list 1 2 3) }}`: "[1 2 3]", 356 | `{{ concat (list 1 2 3) (list 4 5) }}`: "[1 2 3 4 5]", 357 | `{{ concat (list 1 2 3) (list 4 5) (list) }}`: "[1 2 3 4 5]", 358 | `{{ concat (list 1 2 3) (list 4 5) (list nil) }}`: "[1 2 3 4 5 ]", 359 | `{{ concat (list 1 2 3) (list 4 5) (list ( list "foo" ) ) }}`: "[1 2 3 4 5 [foo]]", 360 | } 361 | for tpl, expect := range tests { 362 | assert.NoError(t, runt(tpl, expect)) 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /network.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "math/rand" 5 | "net" 6 | ) 7 | 8 | func getHostByName(name string) string { 9 | addrs, _ := net.LookupHost(name) 10 | //TODO: add error handing when release v3 comes out 11 | return addrs[rand.Intn(len(addrs))] 12 | } 13 | -------------------------------------------------------------------------------- /network_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetHostByName(t *testing.T) { 11 | tpl := `{{"www.google.com" | getHostByName}}` 12 | 13 | resolvedIP, _ := runRaw(tpl, nil) 14 | 15 | ip := net.ParseIP(resolvedIP) 16 | assert.NotNil(t, ip) 17 | assert.NotEmpty(t, ip) 18 | } 19 | -------------------------------------------------------------------------------- /numeric.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/spf13/cast" 10 | "github.com/shopspring/decimal" 11 | ) 12 | 13 | // toFloat64 converts 64-bit floats 14 | func toFloat64(v interface{}) float64 { 15 | return cast.ToFloat64(v) 16 | } 17 | 18 | func toInt(v interface{}) int { 19 | return cast.ToInt(v) 20 | } 21 | 22 | // toInt64 converts integer types to 64-bit integers 23 | func toInt64(v interface{}) int64 { 24 | return cast.ToInt64(v) 25 | } 26 | 27 | func max(a interface{}, i ...interface{}) int64 { 28 | aa := toInt64(a) 29 | for _, b := range i { 30 | bb := toInt64(b) 31 | if bb > aa { 32 | aa = bb 33 | } 34 | } 35 | return aa 36 | } 37 | 38 | func maxf(a interface{}, i ...interface{}) float64 { 39 | aa := toFloat64(a) 40 | for _, b := range i { 41 | bb := toFloat64(b) 42 | aa = math.Max(aa, bb) 43 | } 44 | return aa 45 | } 46 | 47 | func min(a interface{}, i ...interface{}) int64 { 48 | aa := toInt64(a) 49 | for _, b := range i { 50 | bb := toInt64(b) 51 | if bb < aa { 52 | aa = bb 53 | } 54 | } 55 | return aa 56 | } 57 | 58 | func minf(a interface{}, i ...interface{}) float64 { 59 | aa := toFloat64(a) 60 | for _, b := range i { 61 | bb := toFloat64(b) 62 | aa = math.Min(aa, bb) 63 | } 64 | return aa 65 | } 66 | 67 | func until(count int) []int { 68 | step := 1 69 | if count < 0 { 70 | step = -1 71 | } 72 | return untilStep(0, count, step) 73 | } 74 | 75 | func untilStep(start, stop, step int) []int { 76 | v := []int{} 77 | 78 | if stop < start { 79 | if step >= 0 { 80 | return v 81 | } 82 | for i := start; i > stop; i += step { 83 | v = append(v, i) 84 | } 85 | return v 86 | } 87 | 88 | if step <= 0 { 89 | return v 90 | } 91 | for i := start; i < stop; i += step { 92 | v = append(v, i) 93 | } 94 | return v 95 | } 96 | 97 | func floor(a interface{}) float64 { 98 | aa := toFloat64(a) 99 | return math.Floor(aa) 100 | } 101 | 102 | func ceil(a interface{}) float64 { 103 | aa := toFloat64(a) 104 | return math.Ceil(aa) 105 | } 106 | 107 | func round(a interface{}, p int, rOpt ...float64) float64 { 108 | roundOn := .5 109 | if len(rOpt) > 0 { 110 | roundOn = rOpt[0] 111 | } 112 | val := toFloat64(a) 113 | places := toFloat64(p) 114 | 115 | var round float64 116 | pow := math.Pow(10, places) 117 | digit := pow * val 118 | _, div := math.Modf(digit) 119 | if div >= roundOn { 120 | round = math.Ceil(digit) 121 | } else { 122 | round = math.Floor(digit) 123 | } 124 | return round / pow 125 | } 126 | 127 | // converts unix octal to decimal 128 | func toDecimal(v interface{}) int64 { 129 | result, err := strconv.ParseInt(fmt.Sprint(v), 8, 64) 130 | if err != nil { 131 | return 0 132 | } 133 | return result 134 | } 135 | 136 | func seq(params ...int) string { 137 | increment := 1 138 | switch len(params) { 139 | case 0: 140 | return "" 141 | case 1: 142 | start := 1 143 | end := params[0] 144 | if end < start { 145 | increment = -1 146 | } 147 | return intArrayToString(untilStep(start, end+increment, increment), " ") 148 | case 3: 149 | start := params[0] 150 | end := params[2] 151 | step := params[1] 152 | if end < start { 153 | increment = -1 154 | if step > 0 { 155 | return "" 156 | } 157 | } 158 | return intArrayToString(untilStep(start, end+increment, step), " ") 159 | case 2: 160 | start := params[0] 161 | end := params[1] 162 | step := 1 163 | if end < start { 164 | step = -1 165 | } 166 | return intArrayToString(untilStep(start, end+step, step), " ") 167 | default: 168 | return "" 169 | } 170 | } 171 | 172 | func intArrayToString(slice []int, delimeter string) string { 173 | return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimeter), "[]") 174 | } 175 | 176 | // performs a float and subsequent decimal.Decimal conversion on inputs, 177 | // and iterates through a and b executing the mathmetical operation f 178 | func execDecimalOp(a interface{}, b []interface{}, f func(d1, d2 decimal.Decimal) decimal.Decimal) float64 { 179 | prt := decimal.NewFromFloat(toFloat64(a)) 180 | for _, x := range b { 181 | dx := decimal.NewFromFloat(toFloat64(x)) 182 | prt = f(prt, dx) 183 | } 184 | rslt, _ := prt.Float64() 185 | return rslt 186 | } 187 | -------------------------------------------------------------------------------- /numeric_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "strconv" 7 | "testing" 8 | ) 9 | 10 | func TestUntil(t *testing.T) { 11 | tests := map[string]string{ 12 | `{{range $i, $e := until 5}}{{$i}}{{$e}}{{end}}`: "0011223344", 13 | `{{range $i, $e := until -5}}{{$i}}{{$e}} {{end}}`: "00 1-1 2-2 3-3 4-4 ", 14 | } 15 | for tpl, expect := range tests { 16 | if err := runt(tpl, expect); err != nil { 17 | t.Error(err) 18 | } 19 | } 20 | } 21 | func TestUntilStep(t *testing.T) { 22 | tests := map[string]string{ 23 | `{{range $i, $e := untilStep 0 5 1}}{{$i}}{{$e}}{{end}}`: "0011223344", 24 | `{{range $i, $e := untilStep 3 6 1}}{{$i}}{{$e}}{{end}}`: "031425", 25 | `{{range $i, $e := untilStep 0 -10 -2}}{{$i}}{{$e}} {{end}}`: "00 1-2 2-4 3-6 4-8 ", 26 | `{{range $i, $e := untilStep 3 0 1}}{{$i}}{{$e}}{{end}}`: "", 27 | `{{range $i, $e := untilStep 3 99 0}}{{$i}}{{$e}}{{end}}`: "", 28 | `{{range $i, $e := untilStep 3 99 -1}}{{$i}}{{$e}}{{end}}`: "", 29 | `{{range $i, $e := untilStep 3 0 0}}{{$i}}{{$e}}{{end}}`: "", 30 | } 31 | for tpl, expect := range tests { 32 | if err := runt(tpl, expect); err != nil { 33 | t.Error(err) 34 | } 35 | } 36 | 37 | } 38 | func TestBiggest(t *testing.T) { 39 | tpl := `{{ biggest 1 2 3 345 5 6 7}}` 40 | if err := runt(tpl, `345`); err != nil { 41 | t.Error(err) 42 | } 43 | 44 | tpl = `{{ max 345}}` 45 | if err := runt(tpl, `345`); err != nil { 46 | t.Error(err) 47 | } 48 | } 49 | func TestMaxf(t *testing.T) { 50 | tpl := `{{ maxf 1 2 3 345.7 5 6 7}}` 51 | if err := runt(tpl, `345.7`); err != nil { 52 | t.Error(err) 53 | } 54 | 55 | tpl = `{{ max 345 }}` 56 | if err := runt(tpl, `345`); err != nil { 57 | t.Error(err) 58 | } 59 | } 60 | func TestMin(t *testing.T) { 61 | tpl := `{{ min 1 2 3 345 5 6 7}}` 62 | if err := runt(tpl, `1`); err != nil { 63 | t.Error(err) 64 | } 65 | 66 | tpl = `{{ min 345}}` 67 | if err := runt(tpl, `345`); err != nil { 68 | t.Error(err) 69 | } 70 | } 71 | 72 | func TestMinf(t *testing.T) { 73 | tpl := `{{ minf 1.4 2 3 345.6 5 6 7}}` 74 | if err := runt(tpl, `1.4`); err != nil { 75 | t.Error(err) 76 | } 77 | 78 | tpl = `{{ minf 345 }}` 79 | if err := runt(tpl, `345`); err != nil { 80 | t.Error(err) 81 | } 82 | } 83 | 84 | func TestToFloat64(t *testing.T) { 85 | target := float64(102) 86 | if target != toFloat64(int8(102)) { 87 | t.Errorf("Expected 102") 88 | } 89 | if target != toFloat64(int(102)) { 90 | t.Errorf("Expected 102") 91 | } 92 | if target != toFloat64(int32(102)) { 93 | t.Errorf("Expected 102") 94 | } 95 | if target != toFloat64(int16(102)) { 96 | t.Errorf("Expected 102") 97 | } 98 | if target != toFloat64(int64(102)) { 99 | t.Errorf("Expected 102") 100 | } 101 | if target != toFloat64("102") { 102 | t.Errorf("Expected 102") 103 | } 104 | if 0 != toFloat64("frankie") { 105 | t.Errorf("Expected 0") 106 | } 107 | if target != toFloat64(uint16(102)) { 108 | t.Errorf("Expected 102") 109 | } 110 | if target != toFloat64(uint64(102)) { 111 | t.Errorf("Expected 102") 112 | } 113 | if 102.1234 != toFloat64(float64(102.1234)) { 114 | t.Errorf("Expected 102.1234") 115 | } 116 | if 1 != toFloat64(true) { 117 | t.Errorf("Expected 102") 118 | } 119 | } 120 | func TestToInt64(t *testing.T) { 121 | target := int64(102) 122 | if target != toInt64(int8(102)) { 123 | t.Errorf("Expected 102") 124 | } 125 | if target != toInt64(int(102)) { 126 | t.Errorf("Expected 102") 127 | } 128 | if target != toInt64(int32(102)) { 129 | t.Errorf("Expected 102") 130 | } 131 | if target != toInt64(int16(102)) { 132 | t.Errorf("Expected 102") 133 | } 134 | if target != toInt64(int64(102)) { 135 | t.Errorf("Expected 102") 136 | } 137 | if target != toInt64("102") { 138 | t.Errorf("Expected 102") 139 | } 140 | if 0 != toInt64("frankie") { 141 | t.Errorf("Expected 0") 142 | } 143 | if target != toInt64(uint16(102)) { 144 | t.Errorf("Expected 102") 145 | } 146 | if target != toInt64(uint64(102)) { 147 | t.Errorf("Expected 102") 148 | } 149 | if target != toInt64(float64(102.1234)) { 150 | t.Errorf("Expected 102") 151 | } 152 | if 1 != toInt64(true) { 153 | t.Errorf("Expected 102") 154 | } 155 | } 156 | 157 | func TestToInt(t *testing.T) { 158 | target := int(102) 159 | if target != toInt(int8(102)) { 160 | t.Errorf("Expected 102") 161 | } 162 | if target != toInt(int(102)) { 163 | t.Errorf("Expected 102") 164 | } 165 | if target != toInt(int32(102)) { 166 | t.Errorf("Expected 102") 167 | } 168 | if target != toInt(int16(102)) { 169 | t.Errorf("Expected 102") 170 | } 171 | if target != toInt(int64(102)) { 172 | t.Errorf("Expected 102") 173 | } 174 | if target != toInt("102") { 175 | t.Errorf("Expected 102") 176 | } 177 | if 0 != toInt("frankie") { 178 | t.Errorf("Expected 0") 179 | } 180 | if target != toInt(uint16(102)) { 181 | t.Errorf("Expected 102") 182 | } 183 | if target != toInt(uint64(102)) { 184 | t.Errorf("Expected 102") 185 | } 186 | if target != toInt(float64(102.1234)) { 187 | t.Errorf("Expected 102") 188 | } 189 | if 1 != toInt(true) { 190 | t.Errorf("Expected 102") 191 | } 192 | } 193 | 194 | func TestToDecimal(t *testing.T) { 195 | tests := map[interface{}]int64{ 196 | "777": 511, 197 | 777: 511, 198 | 770: 504, 199 | 755: 493, 200 | } 201 | 202 | for input, expectedResult := range tests { 203 | result := toDecimal(input) 204 | if result != expectedResult { 205 | t.Errorf("Expected %v but got %v", expectedResult, result) 206 | } 207 | } 208 | } 209 | 210 | func TestAdd1(t *testing.T) { 211 | tpl := `{{ 3 | add1 }}` 212 | if err := runt(tpl, `4`); err != nil { 213 | t.Error(err) 214 | } 215 | } 216 | 217 | func TestAdd1f(t *testing.T) { 218 | tpl := `{{ 3.4 | add1f }}` 219 | if err := runt(tpl, `4.4`); err != nil { 220 | t.Error(err) 221 | } 222 | } 223 | 224 | func TestAdd(t *testing.T) { 225 | tpl := `{{ 3 | add 1 2}}` 226 | if err := runt(tpl, `6`); err != nil { 227 | t.Error(err) 228 | } 229 | } 230 | 231 | func TestAddf(t *testing.T) { 232 | tpl := `{{ 3 | addf 1.5 2.2}}` 233 | if err := runt(tpl, `6.7`); err != nil { 234 | t.Error(err) 235 | } 236 | } 237 | 238 | func TestDiv(t *testing.T) { 239 | tpl := `{{ 4 | div 5 }}` 240 | if err := runt(tpl, `1`); err != nil { 241 | t.Error(err) 242 | } 243 | } 244 | 245 | func TestDivf(t *testing.T) { 246 | tpl := `{{ 2 | divf 5 4 }}` 247 | if err := runt(tpl, `0.625`); err != nil { 248 | t.Error(err) 249 | } 250 | } 251 | 252 | func TestMul(t *testing.T) { 253 | tpl := `{{ 1 | mul "2" 3 "4"}}` 254 | if err := runt(tpl, `24`); err != nil { 255 | t.Error(err) 256 | } 257 | } 258 | 259 | func TestMulf(t *testing.T) { 260 | tpl := `{{ 1.2 | mulf "2.4" 10 "4"}}` 261 | if err := runt(tpl, `115.2`); err != nil { 262 | t.Error(err) 263 | } 264 | } 265 | 266 | func TestSub(t *testing.T) { 267 | tpl := `{{ 3 | sub 14 }}` 268 | if err := runt(tpl, `11`); err != nil { 269 | t.Error(err) 270 | } 271 | } 272 | 273 | func TestSubf(t *testing.T) { 274 | tpl := `{{ 3 | subf 4.5 1 }}` 275 | if err := runt(tpl, `0.5`); err != nil { 276 | t.Error(err) 277 | } 278 | } 279 | 280 | func TestCeil(t *testing.T) { 281 | assert.Equal(t, 123.0, ceil(123)) 282 | assert.Equal(t, 123.0, ceil("123")) 283 | assert.Equal(t, 124.0, ceil(123.01)) 284 | assert.Equal(t, 124.0, ceil("123.01")) 285 | } 286 | 287 | func TestFloor(t *testing.T) { 288 | assert.Equal(t, 123.0, floor(123)) 289 | assert.Equal(t, 123.0, floor("123")) 290 | assert.Equal(t, 123.0, floor(123.9999)) 291 | assert.Equal(t, 123.0, floor("123.9999")) 292 | } 293 | 294 | func TestRound(t *testing.T) { 295 | assert.Equal(t, 123.556, round(123.5555, 3)) 296 | assert.Equal(t, 123.556, round("123.55555", 3)) 297 | assert.Equal(t, 124.0, round(123.500001, 0)) 298 | assert.Equal(t, 123.0, round(123.49999999, 0)) 299 | assert.Equal(t, 123.23, round(123.2329999, 2, .3)) 300 | assert.Equal(t, 123.24, round(123.233, 2, .3)) 301 | } 302 | 303 | func TestRandomInt(t *testing.T) { 304 | var tests = []struct { 305 | min int 306 | max int 307 | }{ 308 | {10, 11}, 309 | {10, 13}, 310 | {0, 1}, 311 | {5, 50}, 312 | } 313 | for _, v := range tests { 314 | x, _ := runRaw(fmt.Sprintf(`{{ randInt %d %d }}`, v.min, v.max), nil) 315 | r, err := strconv.Atoi(x) 316 | assert.NoError(t, err) 317 | assert.True(t, func(min, max, r int) bool { 318 | return r >= v.min && r < v.max 319 | }(v.min, v.max, r)) 320 | } 321 | } 322 | 323 | func TestSeq(t *testing.T) { 324 | tests := map[string]string{ 325 | `{{seq 0 1 3}}`: "0 1 2 3", 326 | `{{seq 0 3 10}}`: "0 3 6 9", 327 | `{{seq 3 3 2}}`: "", 328 | `{{seq 3 -3 2}}`: "3", 329 | `{{seq}}`: "", 330 | `{{seq 0 4}}`: "0 1 2 3 4", 331 | `{{seq 5}}`: "1 2 3 4 5", 332 | `{{seq -5}}`: "1 0 -1 -2 -3 -4 -5", 333 | `{{seq 0}}`: "1 0", 334 | `{{seq 0 1 2 3}}`: "", 335 | `{{seq 0 -4}}`: "0 -1 -2 -3 -4", 336 | } 337 | for tpl, expect := range tests { 338 | if err := runt(tpl, expect); err != nil { 339 | t.Error(err) 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /reflect.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // typeIs returns true if the src is the type named in target. 9 | func typeIs(target string, src interface{}) bool { 10 | return target == typeOf(src) 11 | } 12 | 13 | func typeIsLike(target string, src interface{}) bool { 14 | t := typeOf(src) 15 | return target == t || "*"+target == t 16 | } 17 | 18 | func typeOf(src interface{}) string { 19 | return fmt.Sprintf("%T", src) 20 | } 21 | 22 | func kindIs(target string, src interface{}) bool { 23 | return target == kindOf(src) 24 | } 25 | 26 | func kindOf(src interface{}) string { 27 | return reflect.ValueOf(src).Kind().String() 28 | } 29 | -------------------------------------------------------------------------------- /reflect_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type fixtureTO struct { 8 | Name, Value string 9 | } 10 | 11 | func TestTypeOf(t *testing.T) { 12 | f := &fixtureTO{"hello", "world"} 13 | tpl := `{{typeOf .}}` 14 | if err := runtv(tpl, "*sprig.fixtureTO", f); err != nil { 15 | t.Error(err) 16 | } 17 | } 18 | 19 | func TestKindOf(t *testing.T) { 20 | tpl := `{{kindOf .}}` 21 | 22 | f := fixtureTO{"hello", "world"} 23 | if err := runtv(tpl, "struct", f); err != nil { 24 | t.Error(err) 25 | } 26 | 27 | f2 := []string{"hello"} 28 | if err := runtv(tpl, "slice", f2); err != nil { 29 | t.Error(err) 30 | } 31 | 32 | var f3 *fixtureTO 33 | if err := runtv(tpl, "ptr", f3); err != nil { 34 | t.Error(err) 35 | } 36 | } 37 | 38 | func TestTypeIs(t *testing.T) { 39 | f := &fixtureTO{"hello", "world"} 40 | tpl := `{{if typeIs "*sprig.fixtureTO" .}}t{{else}}f{{end}}` 41 | if err := runtv(tpl, "t", f); err != nil { 42 | t.Error(err) 43 | } 44 | 45 | f2 := "hello" 46 | if err := runtv(tpl, "f", f2); err != nil { 47 | t.Error(err) 48 | } 49 | } 50 | func TestTypeIsLike(t *testing.T) { 51 | f := "foo" 52 | tpl := `{{if typeIsLike "string" .}}t{{else}}f{{end}}` 53 | if err := runtv(tpl, "t", f); err != nil { 54 | t.Error(err) 55 | } 56 | 57 | // Now make a pointer. Should still match. 58 | f2 := &f 59 | if err := runtv(tpl, "t", f2); err != nil { 60 | t.Error(err) 61 | } 62 | } 63 | func TestKindIs(t *testing.T) { 64 | f := &fixtureTO{"hello", "world"} 65 | tpl := `{{if kindIs "ptr" .}}t{{else}}f{{end}}` 66 | if err := runtv(tpl, "t", f); err != nil { 67 | t.Error(err) 68 | } 69 | f2 := "hello" 70 | if err := runtv(tpl, "f", f2); err != nil { 71 | t.Error(err) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /regex.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | func regexMatch(regex string, s string) bool { 8 | match, _ := regexp.MatchString(regex, s) 9 | return match 10 | } 11 | 12 | func mustRegexMatch(regex string, s string) (bool, error) { 13 | return regexp.MatchString(regex, s) 14 | } 15 | 16 | func regexFindAll(regex string, s string, n int) []string { 17 | r := regexp.MustCompile(regex) 18 | return r.FindAllString(s, n) 19 | } 20 | 21 | func mustRegexFindAll(regex string, s string, n int) ([]string, error) { 22 | r, err := regexp.Compile(regex) 23 | if err != nil { 24 | return []string{}, err 25 | } 26 | return r.FindAllString(s, n), nil 27 | } 28 | 29 | func regexFind(regex string, s string) string { 30 | r := regexp.MustCompile(regex) 31 | return r.FindString(s) 32 | } 33 | 34 | func mustRegexFind(regex string, s string) (string, error) { 35 | r, err := regexp.Compile(regex) 36 | if err != nil { 37 | return "", err 38 | } 39 | return r.FindString(s), nil 40 | } 41 | 42 | func regexReplaceAll(regex string, s string, repl string) string { 43 | r := regexp.MustCompile(regex) 44 | return r.ReplaceAllString(s, repl) 45 | } 46 | 47 | func mustRegexReplaceAll(regex string, s string, repl string) (string, error) { 48 | r, err := regexp.Compile(regex) 49 | if err != nil { 50 | return "", err 51 | } 52 | return r.ReplaceAllString(s, repl), nil 53 | } 54 | 55 | func regexReplaceAllLiteral(regex string, s string, repl string) string { 56 | r := regexp.MustCompile(regex) 57 | return r.ReplaceAllLiteralString(s, repl) 58 | } 59 | 60 | func mustRegexReplaceAllLiteral(regex string, s string, repl string) (string, error) { 61 | r, err := regexp.Compile(regex) 62 | if err != nil { 63 | return "", err 64 | } 65 | return r.ReplaceAllLiteralString(s, repl), nil 66 | } 67 | 68 | func regexSplit(regex string, s string, n int) []string { 69 | r := regexp.MustCompile(regex) 70 | return r.Split(s, n) 71 | } 72 | 73 | func mustRegexSplit(regex string, s string, n int) ([]string, error) { 74 | r, err := regexp.Compile(regex) 75 | if err != nil { 76 | return []string{}, err 77 | } 78 | return r.Split(s, n), nil 79 | } 80 | 81 | func regexQuoteMeta(s string) string { 82 | return regexp.QuoteMeta(s) 83 | } 84 | -------------------------------------------------------------------------------- /regex_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRegexMatch(t *testing.T) { 10 | regex := "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" 11 | 12 | assert.True(t, regexMatch(regex, "test@acme.com")) 13 | assert.True(t, regexMatch(regex, "Test@Acme.Com")) 14 | assert.False(t, regexMatch(regex, "test")) 15 | assert.False(t, regexMatch(regex, "test.com")) 16 | assert.False(t, regexMatch(regex, "test@acme")) 17 | } 18 | 19 | func TestMustRegexMatch(t *testing.T) { 20 | regex := "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" 21 | 22 | o, err := mustRegexMatch(regex, "test@acme.com") 23 | assert.True(t, o) 24 | assert.Nil(t, err) 25 | 26 | o, err = mustRegexMatch(regex, "Test@Acme.Com") 27 | assert.True(t, o) 28 | assert.Nil(t, err) 29 | 30 | o, err = mustRegexMatch(regex, "test") 31 | assert.False(t, o) 32 | assert.Nil(t, err) 33 | 34 | o, err = mustRegexMatch(regex, "test.com") 35 | assert.False(t, o) 36 | assert.Nil(t, err) 37 | 38 | o, err = mustRegexMatch(regex, "test@acme") 39 | assert.False(t, o) 40 | assert.Nil(t, err) 41 | } 42 | 43 | func TestRegexFindAll(t *testing.T) { 44 | regex := "a{2}" 45 | assert.Equal(t, 1, len(regexFindAll(regex, "aa", -1))) 46 | assert.Equal(t, 1, len(regexFindAll(regex, "aaaaaaaa", 1))) 47 | assert.Equal(t, 2, len(regexFindAll(regex, "aaaa", -1))) 48 | assert.Equal(t, 0, len(regexFindAll(regex, "none", -1))) 49 | } 50 | 51 | func TestMustRegexFindAll(t *testing.T) { 52 | type args struct { 53 | regex, s string 54 | n int 55 | } 56 | cases := []struct { 57 | expected int 58 | args args 59 | }{ 60 | {1, args{"a{2}", "aa", -1}}, 61 | {1, args{"a{2}", "aaaaaaaa", 1}}, 62 | {2, args{"a{2}", "aaaa", -1}}, 63 | {0, args{"a{2}", "none", -1}}, 64 | } 65 | 66 | for _, c := range cases { 67 | res, err := mustRegexFindAll(c.args.regex, c.args.s, c.args.n) 68 | if err != nil { 69 | t.Errorf("regexFindAll test case %v failed with err %s", c, err) 70 | } 71 | assert.Equal(t, c.expected, len(res), "case %#v", c.args) 72 | } 73 | } 74 | 75 | func TestRegexFindl(t *testing.T) { 76 | regex := "fo.?" 77 | assert.Equal(t, "foo", regexFind(regex, "foorbar")) 78 | assert.Equal(t, "foo", regexFind(regex, "foo foe fome")) 79 | assert.Equal(t, "", regexFind(regex, "none")) 80 | } 81 | 82 | func TestMustRegexFindl(t *testing.T) { 83 | type args struct{ regex, s string } 84 | cases := []struct { 85 | expected string 86 | args args 87 | }{ 88 | {"foo", args{"fo.?", "foorbar"}}, 89 | {"foo", args{"fo.?", "foo foe fome"}}, 90 | {"", args{"fo.?", "none"}}, 91 | } 92 | 93 | for _, c := range cases { 94 | res, err := mustRegexFind(c.args.regex, c.args.s) 95 | if err != nil { 96 | t.Errorf("regexFind test case %v failed with err %s", c, err) 97 | } 98 | assert.Equal(t, c.expected, res, "case %#v", c.args) 99 | } 100 | } 101 | 102 | func TestRegexReplaceAll(t *testing.T) { 103 | regex := "a(x*)b" 104 | assert.Equal(t, "-T-T-", regexReplaceAll(regex, "-ab-axxb-", "T")) 105 | assert.Equal(t, "--xx-", regexReplaceAll(regex, "-ab-axxb-", "$1")) 106 | assert.Equal(t, "---", regexReplaceAll(regex, "-ab-axxb-", "$1W")) 107 | assert.Equal(t, "-W-xxW-", regexReplaceAll(regex, "-ab-axxb-", "${1}W")) 108 | } 109 | 110 | func TestMustRegexReplaceAll(t *testing.T) { 111 | type args struct{ regex, s, repl string } 112 | cases := []struct { 113 | expected string 114 | args args 115 | }{ 116 | {"-T-T-", args{"a(x*)b", "-ab-axxb-", "T"}}, 117 | {"--xx-", args{"a(x*)b", "-ab-axxb-", "$1"}}, 118 | {"---", args{"a(x*)b", "-ab-axxb-", "$1W"}}, 119 | {"-W-xxW-", args{"a(x*)b", "-ab-axxb-", "${1}W"}}, 120 | } 121 | 122 | for _, c := range cases { 123 | res, err := mustRegexReplaceAll(c.args.regex, c.args.s, c.args.repl) 124 | if err != nil { 125 | t.Errorf("regexReplaceAll test case %v failed with err %s", c, err) 126 | } 127 | assert.Equal(t, c.expected, res, "case %#v", c.args) 128 | } 129 | } 130 | 131 | func TestRegexReplaceAllLiteral(t *testing.T) { 132 | regex := "a(x*)b" 133 | assert.Equal(t, "-T-T-", regexReplaceAllLiteral(regex, "-ab-axxb-", "T")) 134 | assert.Equal(t, "-$1-$1-", regexReplaceAllLiteral(regex, "-ab-axxb-", "$1")) 135 | assert.Equal(t, "-${1}-${1}-", regexReplaceAllLiteral(regex, "-ab-axxb-", "${1}")) 136 | } 137 | 138 | func TestMustRegexReplaceAllLiteral(t *testing.T) { 139 | type args struct{ regex, s, repl string } 140 | cases := []struct { 141 | expected string 142 | args args 143 | }{ 144 | {"-T-T-", args{"a(x*)b", "-ab-axxb-", "T"}}, 145 | {"-$1-$1-", args{"a(x*)b", "-ab-axxb-", "$1"}}, 146 | {"-${1}-${1}-", args{"a(x*)b", "-ab-axxb-", "${1}"}}, 147 | } 148 | 149 | for _, c := range cases { 150 | res, err := mustRegexReplaceAllLiteral(c.args.regex, c.args.s, c.args.repl) 151 | if err != nil { 152 | t.Errorf("regexReplaceAllLiteral test case %v failed with err %s", c, err) 153 | } 154 | assert.Equal(t, c.expected, res, "case %#v", c.args) 155 | } 156 | } 157 | 158 | func TestRegexSplit(t *testing.T) { 159 | regex := "a" 160 | assert.Equal(t, 4, len(regexSplit(regex, "banana", -1))) 161 | assert.Equal(t, 0, len(regexSplit(regex, "banana", 0))) 162 | assert.Equal(t, 1, len(regexSplit(regex, "banana", 1))) 163 | assert.Equal(t, 2, len(regexSplit(regex, "banana", 2))) 164 | 165 | regex = "z+" 166 | assert.Equal(t, 2, len(regexSplit(regex, "pizza", -1))) 167 | assert.Equal(t, 0, len(regexSplit(regex, "pizza", 0))) 168 | assert.Equal(t, 1, len(regexSplit(regex, "pizza", 1))) 169 | assert.Equal(t, 2, len(regexSplit(regex, "pizza", 2))) 170 | } 171 | 172 | func TestMustRegexSplit(t *testing.T) { 173 | type args struct { 174 | regex, s string 175 | n int 176 | } 177 | cases := []struct { 178 | expected int 179 | args args 180 | }{ 181 | {4, args{"a", "banana", -1}}, 182 | {0, args{"a", "banana", 0}}, 183 | {1, args{"a", "banana", 1}}, 184 | {2, args{"a", "banana", 2}}, 185 | {2, args{"z+", "pizza", -1}}, 186 | {0, args{"z+", "pizza", 0}}, 187 | {1, args{"z+", "pizza", 1}}, 188 | {2, args{"z+", "pizza", 2}}, 189 | } 190 | 191 | for _, c := range cases { 192 | res, err := mustRegexSplit(c.args.regex, c.args.s, c.args.n) 193 | if err != nil { 194 | t.Errorf("regexSplit test case %v failed with err %s", c, err) 195 | } 196 | assert.Equal(t, c.expected, len(res), "case %#v", c.args) 197 | } 198 | } 199 | 200 | func TestRegexQuoteMeta(t *testing.T) { 201 | assert.Equal(t, "1\\.2\\.3", regexQuoteMeta("1.2.3")) 202 | assert.Equal(t, "pretzel", regexQuoteMeta("pretzel")) 203 | } 204 | -------------------------------------------------------------------------------- /semver.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | sv2 "github.com/Masterminds/semver/v3" 5 | ) 6 | 7 | func semverCompare(constraint, version string) (bool, error) { 8 | c, err := sv2.NewConstraint(constraint) 9 | if err != nil { 10 | return false, err 11 | } 12 | 13 | v, err := sv2.NewVersion(version) 14 | if err != nil { 15 | return false, err 16 | } 17 | 18 | return c.Check(v), nil 19 | } 20 | 21 | func semver(version string) (*sv2.Version, error) { 22 | return sv2.NewVersion(version) 23 | } 24 | -------------------------------------------------------------------------------- /semver_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSemverCompare(t *testing.T) { 10 | tests := map[string]string{ 11 | `{{ semverCompare "1.2.3" "1.2.3" }}`: `true`, 12 | `{{ semverCompare "^1.2.0" "1.2.3" }}`: `true`, 13 | `{{ semverCompare "^1.2.0" "2.2.3" }}`: `false`, 14 | } 15 | for tpl, expect := range tests { 16 | assert.NoError(t, runt(tpl, expect)) 17 | } 18 | } 19 | 20 | func TestSemver(t *testing.T) { 21 | tests := map[string]string{ 22 | `{{ $s := semver "1.2.3-beta.1+c0ff33" }}{{ $s.Prerelease }}`: "beta.1", 23 | `{{ $s := semver "1.2.3-beta.1+c0ff33" }}{{ $s.Major}}`: "1", 24 | `{{ semver "1.2.3" | (semver "1.2.3").Compare }}`: `0`, 25 | `{{ semver "1.2.3" | (semver "1.3.3").Compare }}`: `1`, 26 | `{{ semver "1.4.3" | (semver "1.2.3").Compare }}`: `-1`, 27 | } 28 | for tpl, expect := range tests { 29 | assert.NoError(t, runt(tpl, expect)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /strings.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "encoding/base32" 5 | "encoding/base64" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | 11 | util "github.com/Masterminds/goutils" 12 | ) 13 | 14 | func base64encode(v string) string { 15 | return base64.StdEncoding.EncodeToString([]byte(v)) 16 | } 17 | 18 | func base64decode(v string) string { 19 | data, err := base64.StdEncoding.DecodeString(v) 20 | if err != nil { 21 | return err.Error() 22 | } 23 | return string(data) 24 | } 25 | 26 | func base32encode(v string) string { 27 | return base32.StdEncoding.EncodeToString([]byte(v)) 28 | } 29 | 30 | func base32decode(v string) string { 31 | data, err := base32.StdEncoding.DecodeString(v) 32 | if err != nil { 33 | return err.Error() 34 | } 35 | return string(data) 36 | } 37 | 38 | func abbrev(width int, s string) string { 39 | if width < 4 { 40 | return s 41 | } 42 | r, _ := util.Abbreviate(s, width) 43 | return r 44 | } 45 | 46 | func abbrevboth(left, right int, s string) string { 47 | if right < 4 || left > 0 && right < 7 { 48 | return s 49 | } 50 | r, _ := util.AbbreviateFull(s, left, right) 51 | return r 52 | } 53 | func initials(s string) string { 54 | // Wrap this just to eliminate the var args, which templates don't do well. 55 | return util.Initials(s) 56 | } 57 | 58 | func randAlphaNumeric(count int) string { 59 | // It is not possible, it appears, to actually generate an error here. 60 | r, _ := util.CryptoRandomAlphaNumeric(count) 61 | return r 62 | } 63 | 64 | func randAlpha(count int) string { 65 | r, _ := util.CryptoRandomAlphabetic(count) 66 | return r 67 | } 68 | 69 | func randAscii(count int) string { 70 | r, _ := util.CryptoRandomAscii(count) 71 | return r 72 | } 73 | 74 | func randNumeric(count int) string { 75 | r, _ := util.CryptoRandomNumeric(count) 76 | return r 77 | } 78 | 79 | func untitle(str string) string { 80 | return util.Uncapitalize(str) 81 | } 82 | 83 | func quote(str ...interface{}) string { 84 | out := make([]string, 0, len(str)) 85 | for _, s := range str { 86 | if s != nil { 87 | out = append(out, fmt.Sprintf("%q", strval(s))) 88 | } 89 | } 90 | return strings.Join(out, " ") 91 | } 92 | 93 | func squote(str ...interface{}) string { 94 | out := make([]string, 0, len(str)) 95 | for _, s := range str { 96 | if s != nil { 97 | out = append(out, fmt.Sprintf("'%v'", s)) 98 | } 99 | } 100 | return strings.Join(out, " ") 101 | } 102 | 103 | func cat(v ...interface{}) string { 104 | v = removeNilElements(v) 105 | r := strings.TrimSpace(strings.Repeat("%v ", len(v))) 106 | return fmt.Sprintf(r, v...) 107 | } 108 | 109 | func indent(spaces int, v string) string { 110 | pad := strings.Repeat(" ", spaces) 111 | return pad + strings.Replace(v, "\n", "\n"+pad, -1) 112 | } 113 | 114 | func nindent(spaces int, v string) string { 115 | return "\n" + indent(spaces, v) 116 | } 117 | 118 | func replace(old, new, src string) string { 119 | return strings.Replace(src, old, new, -1) 120 | } 121 | 122 | func plural(one, many string, count int) string { 123 | if count == 1 { 124 | return one 125 | } 126 | return many 127 | } 128 | 129 | func strslice(v interface{}) []string { 130 | switch v := v.(type) { 131 | case []string: 132 | return v 133 | case []interface{}: 134 | b := make([]string, 0, len(v)) 135 | for _, s := range v { 136 | if s != nil { 137 | b = append(b, strval(s)) 138 | } 139 | } 140 | return b 141 | default: 142 | val := reflect.ValueOf(v) 143 | switch val.Kind() { 144 | case reflect.Array, reflect.Slice: 145 | l := val.Len() 146 | b := make([]string, 0, l) 147 | for i := 0; i < l; i++ { 148 | value := val.Index(i).Interface() 149 | if value != nil { 150 | b = append(b, strval(value)) 151 | } 152 | } 153 | return b 154 | default: 155 | if v == nil { 156 | return []string{} 157 | } 158 | 159 | return []string{strval(v)} 160 | } 161 | } 162 | } 163 | 164 | func removeNilElements(v []interface{}) []interface{} { 165 | newSlice := make([]interface{}, 0, len(v)) 166 | for _, i := range v { 167 | if i != nil { 168 | newSlice = append(newSlice, i) 169 | } 170 | } 171 | return newSlice 172 | } 173 | 174 | func strval(v interface{}) string { 175 | switch v := v.(type) { 176 | case string: 177 | return v 178 | case []byte: 179 | return string(v) 180 | case error: 181 | return v.Error() 182 | case fmt.Stringer: 183 | return v.String() 184 | default: 185 | return fmt.Sprintf("%v", v) 186 | } 187 | } 188 | 189 | func trunc(c int, s string) string { 190 | if c < 0 && len(s)+c > 0 { 191 | return s[len(s)+c:] 192 | } 193 | if c >= 0 && len(s) > c { 194 | return s[:c] 195 | } 196 | return s 197 | } 198 | 199 | func join(sep string, v interface{}) string { 200 | return strings.Join(strslice(v), sep) 201 | } 202 | 203 | func split(sep, orig string) map[string]string { 204 | parts := strings.Split(orig, sep) 205 | res := make(map[string]string, len(parts)) 206 | for i, v := range parts { 207 | res["_"+strconv.Itoa(i)] = v 208 | } 209 | return res 210 | } 211 | 212 | func splitn(sep string, n int, orig string) map[string]string { 213 | parts := strings.SplitN(orig, sep, n) 214 | res := make(map[string]string, len(parts)) 215 | for i, v := range parts { 216 | res["_"+strconv.Itoa(i)] = v 217 | } 218 | return res 219 | } 220 | 221 | // substring creates a substring of the given string. 222 | // 223 | // If start is < 0, this calls string[:end]. 224 | // 225 | // If start is >= 0 and end < 0 or end bigger than s length, this calls string[start:] 226 | // 227 | // Otherwise, this calls string[start, end]. 228 | func substring(start, end int, s string) string { 229 | if start < 0 { 230 | return s[:end] 231 | } 232 | if end < 0 || end > len(s) { 233 | return s[start:] 234 | } 235 | return s[start:end] 236 | } 237 | -------------------------------------------------------------------------------- /strings_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "encoding/base32" 5 | "encoding/base64" 6 | "fmt" 7 | "testing" 8 | "unicode/utf8" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestSubstr(t *testing.T) { 14 | tpl := `{{"fooo" | substr 0 3 }}` 15 | if err := runt(tpl, "foo"); err != nil { 16 | t.Error(err) 17 | } 18 | } 19 | 20 | func TestSubstr_shorterString(t *testing.T) { 21 | tpl := `{{"foo" | substr 0 10 }}` 22 | if err := runt(tpl, "foo"); err != nil { 23 | t.Error(err) 24 | } 25 | } 26 | 27 | func TestTrunc(t *testing.T) { 28 | tpl := `{{ "foooooo" | trunc 3 }}` 29 | if err := runt(tpl, "foo"); err != nil { 30 | t.Error(err) 31 | } 32 | tpl = `{{ "baaaaaar" | trunc -3 }}` 33 | if err := runt(tpl, "aar"); err != nil { 34 | t.Error(err) 35 | } 36 | tpl = `{{ "baaaaaar" | trunc -999 }}` 37 | if err := runt(tpl, "baaaaaar"); err != nil { 38 | t.Error(err) 39 | } 40 | tpl = `{{ "baaaaaz" | trunc 0 }}` 41 | if err := runt(tpl, ""); err != nil { 42 | t.Error(err) 43 | } 44 | } 45 | 46 | func TestQuote(t *testing.T) { 47 | tpl := `{{quote "a" "b" "c"}}` 48 | if err := runt(tpl, `"a" "b" "c"`); err != nil { 49 | t.Error(err) 50 | } 51 | tpl = `{{quote "\"a\"" "b" "c"}}` 52 | if err := runt(tpl, `"\"a\"" "b" "c"`); err != nil { 53 | t.Error(err) 54 | } 55 | tpl = `{{quote 1 2 3 }}` 56 | if err := runt(tpl, `"1" "2" "3"`); err != nil { 57 | t.Error(err) 58 | } 59 | tpl = `{{ .value | quote }}` 60 | values := map[string]interface{}{"value": nil} 61 | if err := runtv(tpl, ``, values); err != nil { 62 | t.Error(err) 63 | } 64 | } 65 | func TestSquote(t *testing.T) { 66 | tpl := `{{squote "a" "b" "c"}}` 67 | if err := runt(tpl, `'a' 'b' 'c'`); err != nil { 68 | t.Error(err) 69 | } 70 | tpl = `{{squote 1 2 3 }}` 71 | if err := runt(tpl, `'1' '2' '3'`); err != nil { 72 | t.Error(err) 73 | } 74 | tpl = `{{ .value | squote }}` 75 | values := map[string]interface{}{"value": nil} 76 | if err := runtv(tpl, ``, values); err != nil { 77 | t.Error(err) 78 | } 79 | } 80 | 81 | func TestContains(t *testing.T) { 82 | // Mainly, we're just verifying the paramater order swap. 83 | tests := []string{ 84 | `{{if contains "cat" "fair catch"}}1{{end}}`, 85 | `{{if hasPrefix "cat" "catch"}}1{{end}}`, 86 | `{{if hasSuffix "cat" "ducat"}}1{{end}}`, 87 | } 88 | for _, tt := range tests { 89 | if err := runt(tt, "1"); err != nil { 90 | t.Error(err) 91 | } 92 | } 93 | } 94 | 95 | func TestTrim(t *testing.T) { 96 | tests := []string{ 97 | `{{trim " 5.00 "}}`, 98 | `{{trimAll "$" "$5.00$"}}`, 99 | `{{trimPrefix "$" "$5.00"}}`, 100 | `{{trimSuffix "$" "5.00$"}}`, 101 | } 102 | for _, tt := range tests { 103 | if err := runt(tt, "5.00"); err != nil { 104 | t.Error(err) 105 | } 106 | } 107 | } 108 | 109 | func TestSplit(t *testing.T) { 110 | tpl := `{{$v := "foo$bar$baz" | split "$"}}{{$v._0}}` 111 | if err := runt(tpl, "foo"); err != nil { 112 | t.Error(err) 113 | } 114 | } 115 | 116 | func TestSplitn(t *testing.T) { 117 | tpl := `{{$v := "foo$bar$baz" | splitn "$" 2}}{{$v._0}}` 118 | if err := runt(tpl, "foo"); err != nil { 119 | t.Error(err) 120 | } 121 | } 122 | 123 | func TestToString(t *testing.T) { 124 | tpl := `{{ toString 1 | kindOf }}` 125 | assert.NoError(t, runt(tpl, "string")) 126 | } 127 | 128 | func TestToStrings(t *testing.T) { 129 | tpl := `{{ $s := list 1 2 3 | toStrings }}{{ index $s 1 | kindOf }}` 130 | assert.NoError(t, runt(tpl, "string")) 131 | tpl = `{{ list 1 .value 2 | toStrings }}` 132 | values := map[string]interface{}{"value": nil} 133 | if err := runtv(tpl, `[1 2]`, values); err != nil { 134 | t.Error(err) 135 | } 136 | } 137 | 138 | func TestJoin(t *testing.T) { 139 | assert.NoError(t, runt(`{{ tuple "a" "b" "c" | join "-" }}`, "a-b-c")) 140 | assert.NoError(t, runt(`{{ tuple 1 2 3 | join "-" }}`, "1-2-3")) 141 | assert.NoError(t, runtv(`{{ join "-" .V }}`, "a-b-c", map[string]interface{}{"V": []string{"a", "b", "c"}})) 142 | assert.NoError(t, runtv(`{{ join "-" .V }}`, "abc", map[string]interface{}{"V": "abc"})) 143 | assert.NoError(t, runtv(`{{ join "-" .V }}`, "1-2-3", map[string]interface{}{"V": []int{1, 2, 3}})) 144 | assert.NoError(t, runtv(`{{ join "-" .value }}`, "1-2", map[string]interface{}{"value": []interface{}{"1", nil, "2"}})) 145 | } 146 | 147 | func TestSortAlpha(t *testing.T) { 148 | // Named `append` in the function map 149 | tests := map[string]string{ 150 | `{{ list "c" "a" "b" | sortAlpha | join "" }}`: "abc", 151 | `{{ list 2 1 4 3 | sortAlpha | join "" }}`: "1234", 152 | } 153 | for tpl, expect := range tests { 154 | assert.NoError(t, runt(tpl, expect)) 155 | } 156 | } 157 | func TestBase64EncodeDecode(t *testing.T) { 158 | magicWord := "coffee" 159 | expect := base64.StdEncoding.EncodeToString([]byte(magicWord)) 160 | 161 | if expect == magicWord { 162 | t.Fatal("Encoder doesn't work.") 163 | } 164 | 165 | tpl := `{{b64enc "coffee"}}` 166 | if err := runt(tpl, expect); err != nil { 167 | t.Error(err) 168 | } 169 | tpl = fmt.Sprintf("{{b64dec %q}}", expect) 170 | if err := runt(tpl, magicWord); err != nil { 171 | t.Error(err) 172 | } 173 | } 174 | func TestBase32EncodeDecode(t *testing.T) { 175 | magicWord := "coffee" 176 | expect := base32.StdEncoding.EncodeToString([]byte(magicWord)) 177 | 178 | if expect == magicWord { 179 | t.Fatal("Encoder doesn't work.") 180 | } 181 | 182 | tpl := `{{b32enc "coffee"}}` 183 | if err := runt(tpl, expect); err != nil { 184 | t.Error(err) 185 | } 186 | tpl = fmt.Sprintf("{{b32dec %q}}", expect) 187 | if err := runt(tpl, magicWord); err != nil { 188 | t.Error(err) 189 | } 190 | } 191 | 192 | func TestGoutils(t *testing.T) { 193 | tests := map[string]string{ 194 | `{{abbrev 5 "hello world"}}`: "he...", 195 | `{{abbrevboth 5 10 "1234 5678 9123"}}`: "...5678...", 196 | `{{nospace "h e l l o "}}`: "hello", 197 | `{{untitle "First Try"}}`: "first try", //https://youtu.be/44-RsrF_V_w 198 | `{{initials "First Try"}}`: "FT", 199 | `{{wrap 5 "Hello World"}}`: "Hello\nWorld", 200 | `{{wrapWith 5 "\t" "Hello World"}}`: "Hello\tWorld", 201 | } 202 | for k, v := range tests { 203 | t.Log(k) 204 | if err := runt(k, v); err != nil { 205 | t.Errorf("Error on tpl %q: %s", k, err) 206 | } 207 | } 208 | } 209 | 210 | func TestRandomString(t *testing.T) { 211 | // Random strings are now using Masterminds/goutils's cryptographically secure random string functions 212 | // by default. Consequently, these tests now have no predictable character sequence. No checks for exact 213 | // string output are necessary. 214 | 215 | // {{randAlphaNum 5}} should yield five random characters 216 | if x, _ := runRaw(`{{randAlphaNum 5}}`, nil); utf8.RuneCountInString(x) != 5 { 217 | t.Errorf("String should be 5 characters; string was %v characters", utf8.RuneCountInString(x)) 218 | } 219 | 220 | // {{randAlpha 5}} should yield five random characters 221 | if x, _ := runRaw(`{{randAlpha 5}}`, nil); utf8.RuneCountInString(x) != 5 { 222 | t.Errorf("String should be 5 characters; string was %v characters", utf8.RuneCountInString(x)) 223 | } 224 | 225 | // {{randAscii 5}} should yield five random characters 226 | if x, _ := runRaw(`{{randAscii 5}}`, nil); utf8.RuneCountInString(x) != 5 { 227 | t.Errorf("String should be 5 characters; string was %v characters", utf8.RuneCountInString(x)) 228 | } 229 | 230 | // {{randNumeric 5}} should yield five random characters 231 | if x, _ := runRaw(`{{randNumeric 5}}`, nil); utf8.RuneCountInString(x) != 5 { 232 | t.Errorf("String should be 5 characters; string was %v characters", utf8.RuneCountInString(x)) 233 | } 234 | } 235 | 236 | func TestCat(t *testing.T) { 237 | tpl := `{{$b := "b"}}{{"c" | cat "a" $b}}` 238 | if err := runt(tpl, "a b c"); err != nil { 239 | t.Error(err) 240 | } 241 | tpl = `{{ .value | cat "a" "b"}}` 242 | values := map[string]interface{}{"value": nil} 243 | if err := runtv(tpl, "a b", values); err != nil { 244 | t.Error(err) 245 | } 246 | } 247 | 248 | func TestIndent(t *testing.T) { 249 | tpl := `{{indent 4 "a\nb\nc"}}` 250 | if err := runt(tpl, " a\n b\n c"); err != nil { 251 | t.Error(err) 252 | } 253 | } 254 | 255 | func TestNindent(t *testing.T) { 256 | tpl := `{{nindent 4 "a\nb\nc"}}` 257 | if err := runt(tpl, "\n a\n b\n c"); err != nil { 258 | t.Error(err) 259 | } 260 | } 261 | 262 | func TestReplace(t *testing.T) { 263 | tpl := `{{"I Am Henry VIII" | replace " " "-"}}` 264 | if err := runt(tpl, "I-Am-Henry-VIII"); err != nil { 265 | t.Error(err) 266 | } 267 | } 268 | 269 | func TestPlural(t *testing.T) { 270 | tpl := `{{$num := len "two"}}{{$num}} {{$num | plural "1 char" "chars"}}` 271 | if err := runt(tpl, "3 chars"); err != nil { 272 | t.Error(err) 273 | } 274 | tpl = `{{len "t" | plural "cheese" "%d chars"}}` 275 | if err := runt(tpl, "cheese"); err != nil { 276 | t.Error(err) 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /url.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "reflect" 7 | ) 8 | 9 | func dictGetOrEmpty(dict map[string]interface{}, key string) string { 10 | value, ok := dict[key] 11 | if !ok { 12 | return "" 13 | } 14 | tp := reflect.TypeOf(value).Kind() 15 | if tp != reflect.String { 16 | panic(fmt.Sprintf("unable to parse %s key, must be of type string, but %s found", key, tp.String())) 17 | } 18 | return reflect.ValueOf(value).String() 19 | } 20 | 21 | // parses given URL to return dict object 22 | func urlParse(v string) map[string]interface{} { 23 | dict := map[string]interface{}{} 24 | parsedURL, err := url.Parse(v) 25 | if err != nil { 26 | panic(fmt.Sprintf("unable to parse url: %s", err)) 27 | } 28 | dict["scheme"] = parsedURL.Scheme 29 | dict["host"] = parsedURL.Host 30 | dict["hostname"] = parsedURL.Hostname() 31 | dict["path"] = parsedURL.Path 32 | dict["query"] = parsedURL.RawQuery 33 | dict["opaque"] = parsedURL.Opaque 34 | dict["fragment"] = parsedURL.Fragment 35 | if parsedURL.User != nil { 36 | dict["userinfo"] = parsedURL.User.String() 37 | } else { 38 | dict["userinfo"] = "" 39 | } 40 | 41 | return dict 42 | } 43 | 44 | // join given dict to URL string 45 | func urlJoin(d map[string]interface{}) string { 46 | resURL := url.URL{ 47 | Scheme: dictGetOrEmpty(d, "scheme"), 48 | Host: dictGetOrEmpty(d, "host"), 49 | Path: dictGetOrEmpty(d, "path"), 50 | RawQuery: dictGetOrEmpty(d, "query"), 51 | Opaque: dictGetOrEmpty(d, "opaque"), 52 | Fragment: dictGetOrEmpty(d, "fragment"), 53 | } 54 | userinfo := dictGetOrEmpty(d, "userinfo") 55 | var user *url.Userinfo 56 | if userinfo != "" { 57 | tempURL, err := url.Parse(fmt.Sprintf("proto://%s@host", userinfo)) 58 | if err != nil { 59 | panic(fmt.Sprintf("unable to parse userinfo in dict: %s", err)) 60 | } 61 | user = tempURL.User 62 | } 63 | 64 | resURL.User = user 65 | return resURL.String() 66 | } 67 | -------------------------------------------------------------------------------- /url_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var urlTests = map[string]map[string]interface{}{ 10 | "proto://auth@host:80/path?query#fragment": { 11 | "fragment": "fragment", 12 | "host": "host:80", 13 | "hostname": "host", 14 | "opaque": "", 15 | "path": "/path", 16 | "query": "query", 17 | "scheme": "proto", 18 | "userinfo": "auth", 19 | }, 20 | "proto://host:80/path": { 21 | "fragment": "", 22 | "host": "host:80", 23 | "hostname": "host", 24 | "opaque": "", 25 | "path": "/path", 26 | "query": "", 27 | "scheme": "proto", 28 | "userinfo": "", 29 | }, 30 | "something": { 31 | "fragment": "", 32 | "host": "", 33 | "hostname": "", 34 | "opaque": "", 35 | "path": "something", 36 | "query": "", 37 | "scheme": "", 38 | "userinfo": "", 39 | }, 40 | "proto://user:passwor%20d@host:80/path": { 41 | "fragment": "", 42 | "host": "host:80", 43 | "hostname": "host", 44 | "opaque": "", 45 | "path": "/path", 46 | "query": "", 47 | "scheme": "proto", 48 | "userinfo": "user:passwor%20d", 49 | }, 50 | "proto://host:80/pa%20th?key=val%20ue": { 51 | "fragment": "", 52 | "host": "host:80", 53 | "hostname": "host", 54 | "opaque": "", 55 | "path": "/pa th", 56 | "query": "key=val%20ue", 57 | "scheme": "proto", 58 | "userinfo": "", 59 | }, 60 | } 61 | 62 | func TestUrlParse(t *testing.T) { 63 | // testing that function is exported and working properly 64 | assert.NoError(t, runt( 65 | `{{ index ( urlParse "proto://auth@host:80/path?query#fragment" ) "host" }}`, 66 | "host:80")) 67 | 68 | // testing scenarios 69 | for url, expected := range urlTests { 70 | assert.EqualValues(t, expected, urlParse(url)) 71 | } 72 | } 73 | 74 | func TestUrlJoin(t *testing.T) { 75 | tests := map[string]string{ 76 | `{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "proto") }}`: "proto://host:80/path?query#fragment", 77 | `{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "scheme" "proto" "userinfo" "ASDJKJSD") }}`: "proto://ASDJKJSD@host:80/path#fragment", 78 | } 79 | for tpl, expected := range tests { 80 | assert.NoError(t, runt(tpl, expected)) 81 | } 82 | 83 | for expected, urlMap := range urlTests { 84 | assert.EqualValues(t, expected, urlJoin(urlMap)) 85 | } 86 | 87 | } 88 | --------------------------------------------------------------------------------