├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── test.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── Taskfile.yml ├── 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 ├── string_slice.md ├── strings.md └── url.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 ├── 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 ├── strings.go ├── strings_test.go ├── url.go └── url_test.go /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | insert_final_newline = true 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | indent_style = tab 10 | indent_size = 8 11 | 12 | [*.{md,yml,yaml,json}] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.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 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.17.x, 1.18.x, 1.19.x] 8 | platform: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.platform }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v3 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | - name: Test 18 | env: 19 | GO111MODULE: on 20 | run: go test -cover . 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | /.glide 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Release 3.2.3 (2022-11-29) 4 | 5 | ### Changed 6 | 7 | - Updated docs (thanks @book987 @aJetHorn @neelayu @pellizzetti @apricote @SaigyoujiYuyuko233 @AlekSi) 8 | - #348: Updated huandu/xstrings which fixed a snake case bug (thanks @yxxhero) 9 | - #353: Updated masterminds/semver which included bug fixes 10 | - #354: Updated golang.org/x/crypto which included bug fixes 11 | 12 | ## Release 3.2.2 (2021-02-04) 13 | 14 | This is a re-release of 3.2.1 to satisfy something with the Go module system. 15 | 16 | ## Release 3.2.1 (2021-02-04) 17 | 18 | ### Changed 19 | 20 | - Upgraded `Masterminds/goutils` to `v1.1.1`. see the [Security Advisory](https://github.com/Masterminds/goutils/security/advisories/GHSA-xg2h-wx96-xgxr) 21 | 22 | ## Release 3.2.0 (2020-12-14) 23 | 24 | ### Added 25 | 26 | - #211: Added randInt function (thanks @kochurovro) 27 | - #223: Added fromJson and mustFromJson functions (thanks @mholt) 28 | - #242: Added a bcrypt function (thanks @robbiet480) 29 | - #253: Added randBytes function (thanks @MikaelSmith) 30 | - #254: Added dig function for dicts (thanks @nyarly) 31 | - #257: Added regexQuoteMeta for quoting regex metadata (thanks @rheaton) 32 | - #261: Added filepath functions osBase, osDir, osExt, osClean, osIsAbs (thanks @zugl) 33 | - #268: Added and and all functions for testing conditions (thanks @phuslu) 34 | - #181: Added float64 arithmetic addf, add1f, subf, divf, mulf, maxf, and minf 35 | (thanks @andrewmostello) 36 | - #265: Added chunk function to split array into smaller arrays (thanks @karelbilek) 37 | - #270: Extend certificate functions to handle non-RSA keys + add support for 38 | ed25519 keys (thanks @misberner) 39 | 40 | ### Changed 41 | 42 | - Removed testing and support for Go 1.12. ed25519 support requires Go 1.13 or newer 43 | - Using semver 3.1.1 and mergo 0.3.11 44 | 45 | ### Fixed 46 | 47 | - #249: Fix htmlDateInZone example (thanks @spawnia) 48 | 49 | NOTE: The dependency github.com/imdario/mergo reverted the breaking change in 50 | 0.3.9 via 0.3.10 release. 51 | 52 | ## Release 3.1.0 (2020-04-16) 53 | 54 | NOTE: The dependency github.com/imdario/mergo made a behavior change in 0.3.9 55 | that impacts sprig functionality. Do not use sprig with a version newer than 0.3.8. 56 | 57 | ### Added 58 | 59 | - #225: Added support for generating htpasswd hash (thanks @rustycl0ck) 60 | - #224: Added duration filter (thanks @frebib) 61 | - #205: Added `seq` function (thanks @thadc23) 62 | 63 | ### Changed 64 | 65 | - #203: Unlambda functions with correct signature (thanks @muesli) 66 | - #236: Updated the license formatting for GitHub display purposes 67 | - #238: Updated package dependency versions. Note, mergo not updated to 0.3.9 68 | as it causes a breaking change for sprig. That issue is tracked at 69 | https://github.com/imdario/mergo/issues/139 70 | 71 | ### Fixed 72 | 73 | - #229: Fix `seq` example in docs (thanks @kalmant) 74 | 75 | ## Release 3.0.2 (2019-12-13) 76 | 77 | ### Fixed 78 | 79 | - #220: Updating to semver v3.0.3 to fix issue with <= ranges 80 | - #218: fix typo elyptical->elliptic in ecdsa key description (thanks @laverya) 81 | 82 | ## Release 3.0.1 (2019-12-08) 83 | 84 | ### Fixed 85 | 86 | - #212: Updated semver fixing broken constraint checking with ^0.0 87 | 88 | ## Release 3.0.0 (2019-10-02) 89 | 90 | ### Added 91 | 92 | - #187: Added durationRound function (thanks @yjp20) 93 | - #189: Added numerous template functions that return errors rather than panic (thanks @nrvnrvn) 94 | - #193: Added toRawJson support (thanks @Dean-Coakley) 95 | - #197: Added get support to dicts (thanks @Dean-Coakley) 96 | 97 | ### Changed 98 | 99 | - #186: Moving dependency management to Go modules 100 | - #186: Updated semver to v3. This has changes in the way ^ is handled 101 | - #194: Updated documentation on merging and how it copies. Added example using deepCopy 102 | - #196: trunc now supports negative values (thanks @Dean-Coakley) 103 | 104 | ## Release 2.22.0 (2019-10-02) 105 | 106 | ### Added 107 | 108 | - #173: Added getHostByName function to resolve dns names to ips (thanks @fcgravalos) 109 | - #195: Added deepCopy function for use with dicts 110 | 111 | ### Changed 112 | 113 | - Updated merge and mergeOverwrite documentation to explain copying and how to 114 | use deepCopy with it 115 | 116 | ## Release 2.21.0 (2019-09-18) 117 | 118 | ### Added 119 | 120 | - #122: Added encryptAES/decryptAES functions (thanks @n0madic) 121 | - #128: Added toDecimal support (thanks @Dean-Coakley) 122 | - #169: Added list contcat (thanks @astorath) 123 | - #174: Added deepEqual function (thanks @bonifaido) 124 | - #170: Added url parse and join functions (thanks @astorath) 125 | 126 | ### Changed 127 | 128 | - #171: Updated glide config for Google UUID to v1 and to add ranges to semver and testify 129 | 130 | ### Fixed 131 | 132 | - #172: Fix semver wildcard example (thanks @piepmatz) 133 | - #175: Fix dateInZone doc example (thanks @s3than) 134 | 135 | ## Release 2.20.0 (2019-06-18) 136 | 137 | ### Added 138 | 139 | - #164: Adding function to get unix epoch for a time (@mattfarina) 140 | - #166: Adding tests for date_in_zone (@mattfarina) 141 | 142 | ### Changed 143 | 144 | - #144: Fix function comments based on best practices from Effective Go (@CodeLingoTeam) 145 | - #150: Handles pointer type for time.Time in "htmlDate" (@mapreal19) 146 | - #161, #157, #160, #153, #158, #156, #155, #159, #152 documentation updates (@badeadan) 147 | 148 | ### Fixed 149 | 150 | ## Release 2.19.0 (2019-03-02) 151 | 152 | IMPORTANT: This release reverts a change from 2.18.0 153 | 154 | 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. 155 | 156 | 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. 157 | 158 | ### Changed 159 | 160 | - Fix substr panic 35fb796 (Alexey igrychev) 161 | - Remove extra period 1eb7729 (Matthew Lorimor) 162 | - Make random string functions use crypto by default 6ceff26 (Matthew Lorimor) 163 | - README edits/fixes/suggestions 08fe136 (Lauri Apple) 164 | 165 | 166 | ## Release 2.18.0 (2019-02-12) 167 | 168 | ### Added 169 | 170 | - Added mergeOverwrite function 171 | - cryptographic functions that use secure random (see fe1de12) 172 | 173 | ### Changed 174 | 175 | - Improve documentation of regexMatch function, resolves #139 90b89ce (Jan Tagscherer) 176 | - Handle has for nil list 9c10885 (Daniel Cohen) 177 | - Document behaviour of mergeOverwrite fe0dbe9 (Lukas Rieder) 178 | - doc: adds missing documentation. 4b871e6 (Fernandez Ludovic) 179 | - Replace outdated goutils imports 01893d2 (Matthew Lorimor) 180 | - Surface crypto secure random strings from goutils fe1de12 (Matthew Lorimor) 181 | - Handle untyped nil values as paramters to string functions 2b2ec8f (Morten Torkildsen) 182 | 183 | ### Fixed 184 | 185 | - Fix dict merge issue and provide mergeOverwrite .dst .src1 to overwrite from src -> dst 4c59c12 (Lukas Rieder) 186 | - Fix substr var names and comments d581f80 (Dean Coakley) 187 | - Fix substr documentation 2737203 (Dean Coakley) 188 | 189 | ## Release 2.17.1 (2019-01-03) 190 | 191 | ### Fixed 192 | 193 | 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. 194 | 195 | ## Release 2.17.0 (2019-01-03) 196 | 197 | ### Added 198 | 199 | - adds alder32sum function and test 6908fc2 (marshallford) 200 | - Added kebabcase function ca331a1 (Ilyes512) 201 | 202 | ### Changed 203 | 204 | - Update goutils to 1.1.0 4e1125d (Matt Butcher) 205 | 206 | ### Fixed 207 | 208 | - Fix 'has' documentation e3f2a85 (dean-coakley) 209 | - docs(dict): fix typo in pick example dc424f9 (Dustin Specker) 210 | - fixes spelling errors... not sure how that happened 4cf188a (marshallford) 211 | 212 | ## Release 2.16.0 (2018-08-13) 213 | 214 | ### Added 215 | 216 | - add splitn function fccb0b0 (Helgi Þorbjörnsson) 217 | - Add slice func df28ca7 (gongdo) 218 | - Generate serial number a3bdffd (Cody Coons) 219 | - Extract values of dict with values function df39312 (Lawrence Jones) 220 | 221 | ### Changed 222 | 223 | - Modify panic message for list.slice ae38335 (gongdo) 224 | - Minor improvement in code quality - Removed an unreachable piece of code at defaults.go#L26:6 - Resolve formatting issues. 5834241 (Abhishek Kashyap) 225 | - Remove duplicated documentation 1d97af1 (Matthew Fisher) 226 | - Test on go 1.11 49df809 (Helgi Þormar Þorbjörnsson) 227 | 228 | ### Fixed 229 | 230 | - Fix file permissions c5f40b5 (gongdo) 231 | - Fix example for buildCustomCert 7779e0d (Tin Lam) 232 | 233 | ## Release 2.15.0 (2018-04-02) 234 | 235 | ### Added 236 | 237 | - #68 and #69: Add json helpers to docs (thanks @arunvelsriram) 238 | - #66: Add ternary function (thanks @binoculars) 239 | - #67: Allow keys function to take multiple dicts (thanks @binoculars) 240 | - #89: Added sha1sum to crypto function (thanks @benkeil) 241 | - #81: Allow customizing Root CA that used by genSignedCert (thanks @chenzhiwei) 242 | - #92: Add travis testing for go 1.10 243 | - #93: Adding appveyor config for windows testing 244 | 245 | ### Changed 246 | 247 | - #90: Updating to more recent dependencies 248 | - #73: replace satori/go.uuid with google/uuid (thanks @petterw) 249 | 250 | ### Fixed 251 | 252 | - #76: Fixed documentation typos (thanks @Thiht) 253 | - Fixed rounding issue on the `ago` function. Note, the removes support for Go 1.8 and older 254 | 255 | ## Release 2.14.1 (2017-12-01) 256 | 257 | ### Fixed 258 | 259 | - #60: Fix typo in function name documentation (thanks @neil-ca-moore) 260 | - #61: Removing line with {{ due to blocking github pages genertion 261 | - #64: Update the list functions to handle int, string, and other slices for compatibility 262 | 263 | ## Release 2.14.0 (2017-10-06) 264 | 265 | This new version of Sprig adds a set of functions for generating and working with SSL certificates. 266 | 267 | - `genCA` generates an SSL Certificate Authority 268 | - `genSelfSignedCert` generates an SSL self-signed certificate 269 | - `genSignedCert` generates an SSL certificate and key based on a given CA 270 | 271 | ## Release 2.13.0 (2017-09-18) 272 | 273 | This release adds new functions, including: 274 | 275 | - `regexMatch`, `regexFindAll`, `regexFind`, `regexReplaceAll`, `regexReplaceAllLiteral`, and `regexSplit` to work with regular expressions 276 | - `floor`, `ceil`, and `round` math functions 277 | - `toDate` converts a string to a date 278 | - `nindent` is just like `indent` but also prepends a new line 279 | - `ago` returns the time from `time.Now` 280 | 281 | ### Added 282 | 283 | - #40: Added basic regex functionality (thanks @alanquillin) 284 | - #41: Added ceil floor and round functions (thanks @alanquillin) 285 | - #48: Added toDate function (thanks @andreynering) 286 | - #50: Added nindent function (thanks @binoculars) 287 | - #46: Added ago function (thanks @slayer) 288 | 289 | ### Changed 290 | 291 | - #51: Updated godocs to include new string functions (thanks @curtisallen) 292 | - #49: Added ability to merge multiple dicts (thanks @binoculars) 293 | 294 | ## Release 2.12.0 (2017-05-17) 295 | 296 | - `snakecase`, `camelcase`, and `shuffle` are three new string functions 297 | - `fail` allows you to bail out of a template render when conditions are not met 298 | 299 | ## Release 2.11.0 (2017-05-02) 300 | 301 | - Added `toJson` and `toPrettyJson` 302 | - Added `merge` 303 | - Refactored documentation 304 | 305 | ## Release 2.10.0 (2017-03-15) 306 | 307 | - Added `semver` and `semverCompare` for Semantic Versions 308 | - `list` replaces `tuple` 309 | - Fixed issue with `join` 310 | - Added `first`, `last`, `intial`, `rest`, `prepend`, `append`, `toString`, `toStrings`, `sortAlpha`, `reverse`, `coalesce`, `pluck`, `pick`, `compact`, `keys`, `omit`, `uniq`, `has`, `without` 311 | 312 | ## Release 2.9.0 (2017-02-23) 313 | 314 | - Added `splitList` to split a list 315 | - Added crypto functions of `genPrivateKey` and `derivePassword` 316 | 317 | ## Release 2.8.0 (2016-12-21) 318 | 319 | - Added access to several path functions (`base`, `dir`, `clean`, `ext`, and `abs`) 320 | - Added functions for _mutating_ dictionaries (`set`, `unset`, `hasKey`) 321 | 322 | ## Release 2.7.0 (2016-12-01) 323 | 324 | - Added `sha256sum` to generate a hash of an input 325 | - Added functions to convert a numeric or string to `int`, `int64`, `float64` 326 | 327 | ## Release 2.6.0 (2016-10-03) 328 | 329 | - Added a `uuidv4` template function for generating UUIDs inside of a template. 330 | 331 | ## Release 2.5.0 (2016-08-19) 332 | 333 | - New `trimSuffix`, `trimPrefix`, `hasSuffix`, and `hasPrefix` functions 334 | - New aliases have been added for a few functions that didn't follow the naming conventions (`trimAll` and `abbrevBoth`) 335 | - `trimall` and `abbrevboth` (notice the case) are deprecated and will be removed in 3.0.0 336 | 337 | ## Release 2.4.0 (2016-08-16) 338 | 339 | - Adds two functions: `until` and `untilStep` 340 | 341 | ## Release 2.3.0 (2016-06-21) 342 | 343 | - cat: Concatenate strings with whitespace separators. 344 | - replace: Replace parts of a string: `replace " " "-" "Me First"` renders "Me-First" 345 | - plural: Format plurals: `len "foo" | plural "one foo" "many foos"` renders "many foos" 346 | - indent: Indent blocks of text in a way that is sensitive to "\n" characters. 347 | 348 | ## Release 2.2.0 (2016-04-21) 349 | 350 | - Added a `genPrivateKey` function (Thanks @bacongobbler) 351 | 352 | ## Release 2.1.0 (2016-03-30) 353 | 354 | - `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"}}`. 355 | - Added accessors for "hermetic" functions. These return only functions that, when given the same input, produce the same output. 356 | 357 | ## Release 2.0.0 (2016-03-29) 358 | 359 | 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. 360 | 361 | - `min` complements `max` (formerly `biggest`) 362 | - `empty` indicates that a value is the empty value for its type 363 | - `tuple` creates a tuple inside of a template: `{{$t := tuple "a", "b" "c"}}` 364 | - `dict` creates a dictionary inside of a template `{{$d := dict "key1" "val1" "key2" "val2"}}` 365 | - Date formatters have been added for HTML dates (as used in `date` input fields) 366 | - Integer math functions can convert from a number of types, including `string` (via `strconv.ParseInt`). 367 | 368 | ## Release 1.2.0 (2016-02-01) 369 | 370 | - Added quote and squote 371 | - Added b32enc and b32dec 372 | - add now takes varargs 373 | - biggest now takes varargs 374 | 375 | ## Release 1.1.0 (2015-12-29) 376 | 377 | - Added #4: Added contains function. strings.Contains, but with the arguments 378 | switched to simplify common pipelines. (thanks krancour) 379 | - Added Travis-CI testing support 380 | 381 | ## Release 1.0.0 (2015-12-23) 382 | 383 | - Initial release 384 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slim-Sprig: Template functions for Go templates [![Go Reference](https://pkg.go.dev/badge/github.com/go-task/slim-sprig/v3.svg)](https://pkg.go.dev/github.com/go-task/slim-sprig/v3) 2 | 3 | Slim-Sprig is a fork of [Sprig](https://github.com/Masterminds/sprig), but with 4 | all functions that depend on external (non standard library) or crypto packages 5 | removed. 6 | The reason for this is to make this library more lightweight. Most of these 7 | functions (specially crypto ones) are not needed on most apps, but costs a lot 8 | in terms of binary size and compilation time. 9 | 10 | ## Usage 11 | 12 | **Template developers**: Please use Slim-Sprig's [function documentation](https://go-task.github.io/slim-sprig/) for 13 | detailed instructions and code snippets for the >100 template functions available. 14 | 15 | **Go developers**: If you'd like to include Slim-Sprig as a library in your program, 16 | our API documentation is available [at pkg.go.dev](https://pkg.go.dev/github.com/go-task/slim-sprig/v3). 17 | 18 | For standard usage, read on. 19 | 20 | ### Load the Slim-Sprig library 21 | 22 | To load the Slim-Sprig `FuncMap`: 23 | 24 | ```go 25 | 26 | import ( 27 | "html/template" 28 | 29 | "github.com/go-task/slim-sprig/v3" 30 | ) 31 | 32 | // This example illustrates that the FuncMap *must* be set before the 33 | // templates themselves are loaded. 34 | tpl := template.Must( 35 | template.New("base").Funcs(sprig.FuncMap()).ParseGlob("*.html") 36 | ) 37 | ``` 38 | 39 | ### Calling the functions inside of templates 40 | 41 | By convention, all functions are lowercase. This seems to follow the Go 42 | idiom for template functions (as opposed to template methods, which are 43 | TitleCase). For example, this: 44 | 45 | ``` 46 | {{ "hello!" | upper | repeat 5 }} 47 | ``` 48 | 49 | produces this: 50 | 51 | ``` 52 | HELLO!HELLO!HELLO!HELLO!HELLO! 53 | ``` 54 | 55 | ## Principles Driving Our Function Selection 56 | 57 | We followed these principles to decide which functions to add and how to implement them: 58 | 59 | - Use template functions to build layout. The following 60 | types of operations are within the domain of template functions: 61 | - Formatting 62 | - Layout 63 | - Simple type conversions 64 | - Utilities that assist in handling common formatting and layout needs (e.g. arithmetic) 65 | - Template functions should not return errors unless there is no way to print 66 | a sensible value. For example, converting a string to an integer should not 67 | produce an error if conversion fails. Instead, it should display a default 68 | value. 69 | - Simple math is necessary for grid layouts, pagers, and so on. Complex math 70 | (anything other than arithmetic) should be done outside of templates. 71 | - Template functions only deal with the data passed into them. They never retrieve 72 | data from a source. 73 | - Finally, do not override core Go template functions. 74 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: '3' 4 | 5 | tasks: 6 | default: 7 | cmds: 8 | - task: test 9 | 10 | test: 11 | cmds: 12 | - go test -v . 13 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "crypto/sha1" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "fmt" 8 | "hash/adler32" 9 | ) 10 | 11 | func sha256sum(input string) string { 12 | hash := sha256.Sum256([]byte(input)) 13 | return hex.EncodeToString(hash[:]) 14 | } 15 | 16 | func sha1sum(input string) string { 17 | hash := sha1.Sum([]byte(input)) 18 | return hex.EncodeToString(hash[:]) 19 | } 20 | 21 | func adler32sum(input string) string { 22 | hash := adler32.Checksum([]byte(input)) 23 | return fmt.Sprintf("%d", hash) 24 | } 25 | -------------------------------------------------------------------------------- /crypto_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var ( 8 | // fastCertKeyAlgos is the list of private key algorithms that are supported for certificate use, and 9 | // are fast to generate. 10 | fastCertKeyAlgos = []string{ 11 | "ecdsa", 12 | "ed25519", 13 | } 14 | ) 15 | 16 | func TestSha256Sum(t *testing.T) { 17 | tpl := `{{"abc" | sha256sum}}` 18 | if err := runt(tpl, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); err != nil { 19 | t.Error(err) 20 | } 21 | } 22 | func TestSha1Sum(t *testing.T) { 23 | tpl := `{{"abc" | sha1sum}}` 24 | if err := runt(tpl, "a9993e364706816aba3e25717850c26c9cd0d89d"); err != nil { 25 | t.Error(err) 26 | } 27 | } 28 | 29 | func TestAdler32Sum(t *testing.T) { 30 | tpl := `{{"abc" | adler32sum}}` 31 | if err := runt(tpl, "38600999"); err != nil { 32 | t.Error(err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /date.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // Given a format and a date, format the date string. 9 | // 10 | // Date can be a `time.Time` or an `int, int32, int64`. 11 | // In the later case, it is treated as seconds since UNIX 12 | // epoch. 13 | func date(fmt string, date interface{}) string { 14 | return dateInZone(fmt, date, "Local") 15 | } 16 | 17 | func htmlDate(date interface{}) string { 18 | return dateInZone("2006-01-02", date, "Local") 19 | } 20 | 21 | func htmlDateInZone(date interface{}, zone string) string { 22 | return dateInZone("2006-01-02", date, zone) 23 | } 24 | 25 | func dateInZone(fmt string, date interface{}, zone string) string { 26 | var t time.Time 27 | switch date := date.(type) { 28 | default: 29 | t = time.Now() 30 | case time.Time: 31 | t = date 32 | case *time.Time: 33 | t = *date 34 | case int64: 35 | t = time.Unix(date, 0) 36 | case int: 37 | t = time.Unix(int64(date), 0) 38 | case int32: 39 | t = time.Unix(int64(date), 0) 40 | } 41 | 42 | loc, err := time.LoadLocation(zone) 43 | if err != nil { 44 | loc, _ = time.LoadLocation("UTC") 45 | } 46 | 47 | return t.In(loc).Format(fmt) 48 | } 49 | 50 | func dateModify(fmt string, date time.Time) time.Time { 51 | d, err := time.ParseDuration(fmt) 52 | if err != nil { 53 | return date 54 | } 55 | return date.Add(d) 56 | } 57 | 58 | func mustDateModify(fmt string, date time.Time) (time.Time, error) { 59 | d, err := time.ParseDuration(fmt) 60 | if err != nil { 61 | return time.Time{}, err 62 | } 63 | return date.Add(d), nil 64 | } 65 | 66 | func dateAgo(date interface{}) string { 67 | var t time.Time 68 | 69 | switch date := date.(type) { 70 | default: 71 | t = time.Now() 72 | case time.Time: 73 | t = date 74 | case int64: 75 | t = time.Unix(date, 0) 76 | case int: 77 | t = time.Unix(int64(date), 0) 78 | } 79 | // Drop resolution to seconds 80 | duration := time.Since(t).Round(time.Second) 81 | return duration.String() 82 | } 83 | 84 | func duration(sec interface{}) string { 85 | var n int64 86 | switch value := sec.(type) { 87 | default: 88 | n = 0 89 | case string: 90 | n, _ = strconv.ParseInt(value, 10, 64) 91 | case int64: 92 | n = value 93 | } 94 | return (time.Duration(n) * time.Second).String() 95 | } 96 | 97 | func durationRound(duration interface{}) string { 98 | var d time.Duration 99 | switch duration := duration.(type) { 100 | default: 101 | d = 0 102 | case string: 103 | d, _ = time.ParseDuration(duration) 104 | case int64: 105 | d = time.Duration(duration) 106 | case time.Time: 107 | d = time.Since(duration) 108 | } 109 | 110 | u := uint64(d) 111 | neg := d < 0 112 | if neg { 113 | u = -u 114 | } 115 | 116 | var ( 117 | year = uint64(time.Hour) * 24 * 365 118 | month = uint64(time.Hour) * 24 * 30 119 | day = uint64(time.Hour) * 24 120 | hour = uint64(time.Hour) 121 | minute = uint64(time.Minute) 122 | second = uint64(time.Second) 123 | ) 124 | switch { 125 | case u > 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 | func get(d map[string]interface{}, key string) interface{} { 4 | if val, ok := d[key]; ok { 5 | return val 6 | } 7 | return "" 8 | } 9 | 10 | func set(d map[string]interface{}, key string, value interface{}) map[string]interface{} { 11 | d[key] = value 12 | return d 13 | } 14 | 15 | func unset(d map[string]interface{}, key string) map[string]interface{} { 16 | delete(d, key) 17 | return d 18 | } 19 | 20 | func hasKey(d map[string]interface{}, key string) bool { 21 | _, ok := d[key] 22 | return ok 23 | } 24 | 25 | func pluck(key string, d ...map[string]interface{}) []interface{} { 26 | res := []interface{}{} 27 | for _, dict := range d { 28 | if val, ok := dict[key]; ok { 29 | res = append(res, val) 30 | } 31 | } 32 | return res 33 | } 34 | 35 | func keys(dicts ...map[string]interface{}) []string { 36 | k := []string{} 37 | for _, dict := range dicts { 38 | for key := range dict { 39 | k = append(k, key) 40 | } 41 | } 42 | return k 43 | } 44 | 45 | func pick(dict map[string]interface{}, keys ...string) map[string]interface{} { 46 | res := map[string]interface{}{} 47 | for _, k := range keys { 48 | if v, ok := dict[k]; ok { 49 | res[k] = v 50 | } 51 | } 52 | return res 53 | } 54 | 55 | func omit(dict map[string]interface{}, keys ...string) map[string]interface{} { 56 | res := map[string]interface{}{} 57 | 58 | omit := make(map[string]bool, len(keys)) 59 | for _, k := range keys { 60 | omit[k] = true 61 | } 62 | 63 | for k, v := range dict { 64 | if _, ok := omit[k]; !ok { 65 | res[k] = v 66 | } 67 | } 68 | return res 69 | } 70 | 71 | func dict(v ...interface{}) map[string]interface{} { 72 | dict := map[string]interface{}{} 73 | lenv := len(v) 74 | for i := 0; i < lenv; i += 2 { 75 | key := strval(v[i]) 76 | if i+1 >= lenv { 77 | dict[key] = "" 78 | continue 79 | } 80 | dict[key] = v[i+1] 81 | } 82 | return dict 83 | } 84 | 85 | func values(dict map[string]interface{}) []interface{} { 86 | values := []interface{}{} 87 | for _, value := range dict { 88 | values = append(values, value) 89 | } 90 | 91 | return values 92 | } 93 | 94 | func dig(ps ...interface{}) (interface{}, error) { 95 | if len(ps) < 3 { 96 | panic("dig needs at least three arguments") 97 | } 98 | dict := ps[len(ps)-1].(map[string]interface{}) 99 | def := ps[len(ps)-2] 100 | ks := make([]string, len(ps)-2) 101 | for i := 0; i < len(ks); i++ { 102 | ks[i] = ps[i].(string) 103 | } 104 | 105 | return digFromDict(dict, def, ks) 106 | } 107 | 108 | func digFromDict(dict map[string]interface{}, d interface{}, ks []string) (interface{}, error) { 109 | k, ns := ks[0], ks[1:len(ks)] 110 | step, has := dict[k] 111 | if !has { 112 | return d, nil 113 | } 114 | if len(ns) == 0 { 115 | return step, nil 116 | } 117 | return digFromDict(step.(map[string]interface{}), d, ns) 118 | } 119 | -------------------------------------------------------------------------------- /dict_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestDict(t *testing.T) { 9 | tpl := `{{$d := dict 1 2 "three" "four" 5}}{{range $k, $v := $d}}{{$k}}{{$v}}{{end}}` 10 | out, err := runRaw(tpl, nil) 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | if len(out) != 12 { 15 | t.Errorf("Expected length 12, got %d", len(out)) 16 | } 17 | // dict does not guarantee ordering because it is backed by a map. 18 | if !strings.Contains(out, "12") { 19 | t.Error("Expected grouping 12") 20 | } 21 | if !strings.Contains(out, "threefour") { 22 | t.Error("Expected grouping threefour") 23 | } 24 | if !strings.Contains(out, "5") { 25 | t.Error("Expected 5") 26 | } 27 | tpl = `{{$t := dict "I" "shot" "the" "albatross"}}{{$t.the}} {{$t.I}}` 28 | if err := runt(tpl, "albatross shot"); err != nil { 29 | t.Error(err) 30 | } 31 | } 32 | 33 | func TestUnset(t *testing.T) { 34 | tpl := `{{- $d := dict "one" 1 "two" 222222 -}} 35 | {{- $_ := unset $d "two" -}} 36 | {{- range $k, $v := $d}}{{$k}}{{$v}}{{- end -}} 37 | ` 38 | 39 | expect := "one1" 40 | if err := runt(tpl, expect); err != nil { 41 | t.Error(err) 42 | } 43 | } 44 | func TestHasKey(t *testing.T) { 45 | tpl := `{{- $d := dict "one" 1 "two" 222222 -}} 46 | {{- if hasKey $d "one" -}}1{{- end -}} 47 | ` 48 | 49 | expect := "1" 50 | if err := runt(tpl, expect); err != nil { 51 | t.Error(err) 52 | } 53 | } 54 | 55 | func TestPluck(t *testing.T) { 56 | tpl := ` 57 | {{- $d := dict "one" 1 "two" 222222 -}} 58 | {{- $d2 := dict "one" 1 "two" 33333 -}} 59 | {{- $d3 := dict "one" 1 -}} 60 | {{- $d4 := dict "one" 1 "two" 4444 -}} 61 | {{- pluck "two" $d $d2 $d3 $d4 -}} 62 | ` 63 | 64 | expect := "[222222 33333 4444]" 65 | if err := runt(tpl, expect); err != nil { 66 | t.Error(err) 67 | } 68 | } 69 | 70 | func TestKeys(t *testing.T) { 71 | tests := map[string]string{ 72 | `{{ dict "foo" 1 "bar" 2 | keys | sortAlpha }}`: "[bar foo]", 73 | `{{ dict | keys }}`: "[]", 74 | `{{ keys (dict "foo" 1) (dict "bar" 2) (dict "bar" 3) | uniq | sortAlpha }}`: "[bar foo]", 75 | } 76 | for tpl, expect := range tests { 77 | if err := runt(tpl, expect); err != nil { 78 | t.Error(err) 79 | } 80 | } 81 | } 82 | 83 | func TestPick(t *testing.T) { 84 | tests := map[string]string{ 85 | `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "two" | len -}}`: "1", 86 | `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "two" -}}`: "map[two:222222]", 87 | `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "one" "two" | len -}}`: "2", 88 | `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "one" "two" "three" | len -}}`: "2", 89 | `{{- $d := dict }}{{ pick $d "two" | len -}}`: "0", 90 | } 91 | for tpl, expect := range tests { 92 | if err := runt(tpl, expect); err != nil { 93 | t.Error(err) 94 | } 95 | } 96 | } 97 | func TestOmit(t *testing.T) { 98 | tests := map[string]string{ 99 | `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" | len -}}`: "1", 100 | `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" -}}`: "map[two:222222]", 101 | `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" "two" | len -}}`: "0", 102 | `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "two" "three" | len -}}`: "1", 103 | `{{- $d := dict }}{{ omit $d "two" | len -}}`: "0", 104 | } 105 | for tpl, expect := range tests { 106 | if err := runt(tpl, expect); err != nil { 107 | t.Error(err) 108 | } 109 | } 110 | } 111 | 112 | func TestGet(t *testing.T) { 113 | tests := map[string]string{ 114 | `{{- $d := dict "one" 1 }}{{ get $d "one" -}}`: "1", 115 | `{{- $d := dict "one" 1 "two" "2" }}{{ get $d "two" -}}`: "2", 116 | `{{- $d := dict }}{{ get $d "two" -}}`: "", 117 | } 118 | for tpl, expect := range tests { 119 | if err := runt(tpl, expect); err != nil { 120 | t.Error(err) 121 | } 122 | } 123 | } 124 | 125 | func TestSet(t *testing.T) { 126 | tpl := `{{- $d := dict "one" 1 "two" 222222 -}} 127 | {{- $_ := set $d "two" 2 -}} 128 | {{- $_ := set $d "three" 3 -}} 129 | {{- if hasKey $d "one" -}}{{$d.one}}{{- end -}} 130 | {{- if hasKey $d "two" -}}{{$d.two}}{{- end -}} 131 | {{- if hasKey $d "three" -}}{{$d.three}}{{- end -}} 132 | ` 133 | 134 | expect := "123" 135 | if err := runt(tpl, expect); err != nil { 136 | t.Error(err) 137 | } 138 | } 139 | 140 | func TestValues(t *testing.T) { 141 | tests := map[string]string{ 142 | `{{- $d := dict "a" 1 "b" 2 }}{{ values $d | sortAlpha | join "," }}`: "1,2", 143 | `{{- $d := dict "a" "first" "b" 2 }}{{ values $d | sortAlpha | join "," }}`: "2,first", 144 | } 145 | 146 | for tpl, expect := range tests { 147 | if err := runt(tpl, expect); err != nil { 148 | t.Error(err) 149 | } 150 | } 151 | } 152 | 153 | func TestDig(t *testing.T) { 154 | tests := map[string]string{ 155 | `{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "c" "" $d }}`: "1", 156 | `{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "z" "2" $d }}`: "2", 157 | `{{ dict "a" 1 | dig "a" "" }}`: "1", 158 | `{{ dict "a" 1 | dig "z" "2" }}`: "2", 159 | } 160 | 161 | for tpl, expect := range tests { 162 | if err := runt(tpl, expect); err != nil { 163 | t.Error(err) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /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 := templates.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 | ## adler32sum 25 | 26 | The `adler32sum` function receives a string, and computes its Adler-32 checksum. 27 | 28 | ``` 29 | adler32sum "Hello world!" 30 | ``` 31 | -------------------------------------------------------------------------------- /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 "key1" 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 | `mustMerge` will return an error in case of unsuccessful merge. 134 | 135 | ## mergeOverwrite, mustMergeOverwrite 136 | 137 | Merge two or more dictionaries into one, giving precedence from **right to left**, effectively 138 | overwriting values in the dest dictionary: 139 | 140 | Given: 141 | 142 | ``` 143 | dst: 144 | default: default 145 | overwrite: me 146 | key: true 147 | 148 | src: 149 | overwrite: overwritten 150 | key: false 151 | ``` 152 | 153 | will result in: 154 | 155 | ``` 156 | newdict: 157 | default: default 158 | overwrite: overwritten 159 | key: false 160 | ``` 161 | 162 | ``` 163 | $newdict := mergeOverwrite $dest $source1 $source2 164 | ``` 165 | 166 | `mustMergeOverwrite` will return an error in case of unsuccessful merge. 167 | 168 | ## keys 169 | 170 | The `keys` function will return a `list` of all of the keys in one or more `dict` 171 | types. Since a dictionary is _unordered_, the keys will not be in a predictable order. 172 | They can be sorted with `sortAlpha`. 173 | 174 | ``` 175 | keys $myDict | sortAlpha 176 | ``` 177 | 178 | When supplying multiple dictionaries, the keys will be concatenated. Use the `uniq` 179 | function along with `sortAlpha` to get a unqiue, sorted list of keys. 180 | 181 | ``` 182 | keys $myDict $myOtherDict | uniq | sortAlpha 183 | ``` 184 | 185 | ## pick 186 | 187 | The `pick` function selects just the given keys out of a dictionary, creating a 188 | new `dict`. 189 | 190 | ``` 191 | $new := pick $myDict "name1" "name2" 192 | ``` 193 | 194 | The above returns `{name1: value1, name2: value2}` 195 | 196 | ## omit 197 | 198 | The `omit` function is similar to `pick`, except it returns a new `dict` with all 199 | the keys that _do not_ match the given keys. 200 | 201 | ``` 202 | $new := omit $myDict "name1" "name3" 203 | ``` 204 | 205 | The above returns `{name2: value2}` 206 | 207 | ## values 208 | 209 | The `values` function is similar to `keys`, except it returns a new `list` with 210 | all the values of the source `dict` (only one dictionary is supported). 211 | 212 | ``` 213 | $vals := values $myDict 214 | ``` 215 | 216 | The above returns `list["value1", "value2", "value 3"]`. Note that the `values` 217 | function gives no guarantees about the result ordering- if you care about this, 218 | then use `sortAlpha`. 219 | 220 | ## A Note on Dict Internals 221 | 222 | A `dict` is implemented in Go as a `map[string]interface{}`. Go developers can 223 | pass `map[string]interface{}` values into the context to make them available 224 | to templates as `dict`s. 225 | -------------------------------------------------------------------------------- /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 | # Slim-Sprig Function Documentation 2 | 3 | The Slim-Sprig library provides over 70 template functions for Go's template language. 4 | 5 | - [String Functions](strings.md): `trim`, `plural`, etc. 6 | - [String List Functions](string_slice.md): `join`, `splitList`, `sortAlpha`, etc. 7 | - [Integer Math Functions](math.md): `add`, `max`, `mul`, etc. 8 | - [Integer Slice Functions](integer_slice.md): `until`, `untilStep` 9 | - [Date Functions](date.md): `now`, `date`, etc. 10 | - [Defaults Functions](defaults.md): `default`, `empty`, `coalesce`, `fromJson`, `toJson`, `toPrettyJson`, `toRawJson`, `ternary` 11 | - [Encoding Functions](encoding.md): `b64enc`, `b64dec`, etc. 12 | - [Lists and List Functions](lists.md): `list`, `first`, `uniq`, etc. 13 | - [Dictionaries and Dict Functions](dicts.md): `get`, `set`, `dict`, `hasKey`, `pluck`, `dig`, etc. 14 | - [Type Conversion Functions](conversion.md): `atoi`, `int64`, `toString`, etc. 15 | - [Path and Filepath Functions](paths.md): `base`, `dir`, `ext`, `clean`, `isAbs`, `osBase`, `osDir`, `osExt`, `osClean`, `osIsAbs` 16 | - [Flow Control Functions](flow_control.md): `fail` 17 | - Advanced Functions 18 | - [OS Functions](os.md): `env`, `expandenv` 19 | - [Reflection](reflection.md): `typeOf`, `kindIs`, `typeIsLike`, etc. 20 | - [Cryptographic and Security Functions](crypto.md): `sha256sum`, etc. 21 | -------------------------------------------------------------------------------- /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 | 69 | ## floor 70 | 71 | Returns the greatest float value less than or equal to input value 72 | 73 | `floor 123.9999` will return `123.0` 74 | 75 | ## ceil 76 | 77 | Returns the greatest float value greater than or equal to input value 78 | 79 | `ceil 123.001` will return `124.0` 80 | 81 | ## round 82 | 83 | Returns a float value with the remainder rounded to the given number to digits after the decimal point. 84 | 85 | `round 123.555555` will return `123.556` 86 | -------------------------------------------------------------------------------- /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/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 | ## repeat 76 | 77 | Repeat a string multiple times: 78 | 79 | ``` 80 | repeat 3 "hello" 81 | ``` 82 | 83 | The above returns `hellohellohello` 84 | 85 | ## substr 86 | 87 | Get a substring from a string. It takes three parameters: 88 | 89 | - start (int) 90 | - end (int) 91 | - string (string) 92 | 93 | ``` 94 | substr 0 5 "hello world" 95 | ``` 96 | 97 | The above returns `hello` 98 | 99 | ## trunc 100 | 101 | Truncate a string (and add no suffix) 102 | 103 | ``` 104 | trunc 5 "hello world" 105 | ``` 106 | 107 | The above produces `hello`. 108 | 109 | ``` 110 | trunc -5 "hello world" 111 | ``` 112 | 113 | The above produces `world`. 114 | 115 | ## contains 116 | 117 | Test to see if one string is contained inside of another: 118 | 119 | ``` 120 | contains "cat" "catch" 121 | ``` 122 | 123 | The above returns `true` because `catch` contains `cat`. 124 | 125 | ## hasPrefix and hasSuffix 126 | 127 | The `hasPrefix` and `hasSuffix` functions test whether a string has a given 128 | prefix or suffix: 129 | 130 | ``` 131 | hasPrefix "cat" "catch" 132 | ``` 133 | 134 | The above returns `true` because `catch` has the prefix `cat`. 135 | 136 | ## quote and squote 137 | 138 | These functions wrap a string in double quotes (`quote`) or single quotes 139 | (`squote`). 140 | 141 | ## cat 142 | 143 | The `cat` function concatenates multiple strings together into one, separating 144 | them with spaces: 145 | 146 | ``` 147 | cat "hello" "beautiful" "world" 148 | ``` 149 | 150 | The above produces `hello beautiful world` 151 | 152 | ## indent 153 | 154 | The `indent` function indents every line in a given string to the specified 155 | indent width. This is useful when aligning multi-line strings: 156 | 157 | ``` 158 | indent 4 $lots_of_text 159 | ``` 160 | 161 | The above will indent every line of text by 4 space characters. 162 | 163 | ## nindent 164 | 165 | The `nindent` function is the same as the indent function, but prepends a new 166 | line to the beginning of the string. 167 | 168 | ``` 169 | nindent 4 $lots_of_text 170 | ``` 171 | 172 | The above will indent every line of text by 4 space characters and add a new 173 | line to the beginning. 174 | 175 | ## replace 176 | 177 | Perform simple string replacement. 178 | 179 | It takes three arguments: 180 | 181 | - string to replace 182 | - string to replace with 183 | - source string 184 | 185 | ``` 186 | "I Am Henry VIII" | replace " " "-" 187 | ``` 188 | 189 | The above will produce `I-Am-Henry-VIII` 190 | 191 | ## plural 192 | 193 | Pluralize a string. 194 | 195 | ``` 196 | len $fish | plural "one anchovy" "many anchovies" 197 | ``` 198 | 199 | In the above, if the length of the string is 1, the first argument will be 200 | printed (`one anchovy`). Otherwise, the second argument will be printed 201 | (`many anchovies`). 202 | 203 | The arguments are: 204 | 205 | - singular string 206 | - plural string 207 | - length integer 208 | 209 | NOTE: Sprig does not currently support languages with more complex pluralization 210 | rules. And `0` is considered a plural because the English language treats it 211 | as such (`zero anchovies`). The Sprig developers are working on a solution for 212 | better internationalization. 213 | 214 | ## regexMatch, mustRegexMatch 215 | 216 | Returns true if the input string contains any match of the regular expression. 217 | 218 | ``` 219 | regexMatch "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" "test@acme.com" 220 | ``` 221 | 222 | The above produces `true` 223 | 224 | `regexMatch` panics if there is a problem and `mustRegexMatch` returns an error to the 225 | template engine if there is a problem. 226 | 227 | ## regexFindAll, mustRegexFindAll 228 | 229 | Returns a slice of all matches of the regular expression in the input string. 230 | The last parameter n determines the number of substrings to return, where -1 means return all matches 231 | 232 | ``` 233 | regexFindAll "[2,4,6,8]" "123456789" -1 234 | ``` 235 | 236 | The above produces `[2 4 6 8]` 237 | 238 | `regexFindAll` panics if there is a problem and `mustRegexFindAll` returns an error to the 239 | template engine if there is a problem. 240 | 241 | ## regexFind, mustRegexFind 242 | 243 | Return the first (left most) match of the regular expression in the input string 244 | 245 | ``` 246 | regexFind "[a-zA-Z][1-9]" "abcd1234" 247 | ``` 248 | 249 | The above produces `d1` 250 | 251 | `regexFind` panics if there is a problem and `mustRegexFind` returns an error to the 252 | template engine if there is a problem. 253 | 254 | ## regexReplaceAll, mustRegexReplaceAll 255 | 256 | Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. 257 | Inside string replacement, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch 258 | 259 | ``` 260 | regexReplaceAll "a(x*)b" "-ab-axxb-" "${1}W" 261 | ``` 262 | 263 | The above produces `-W-xxW-` 264 | 265 | `regexReplaceAll` panics if there is a problem and `mustRegexReplaceAll` returns an error to the 266 | template engine if there is a problem. 267 | 268 | ## regexReplaceAllLiteral, mustRegexReplaceAllLiteral 269 | 270 | Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement 271 | The replacement string is substituted directly, without using Expand 272 | 273 | ``` 274 | regexReplaceAllLiteral "a(x*)b" "-ab-axxb-" "${1}" 275 | ``` 276 | 277 | The above produces `-${1}-${1}-` 278 | 279 | `regexReplaceAllLiteral` panics if there is a problem and `mustRegexReplaceAllLiteral` returns an error to the 280 | template engine if there is a problem. 281 | 282 | ## regexSplit, mustRegexSplit 283 | 284 | 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 285 | 286 | ``` 287 | regexSplit "z+" "pizza" -1 288 | ``` 289 | 290 | The above produces `[pi a]` 291 | 292 | `regexSplit` panics if there is a problem and `mustRegexSplit` returns an error to the 293 | template engine if there is a problem. 294 | 295 | ## regexQuoteMeta 296 | 297 | Returns a string that escapes all regular expression metacharacters inside the argument text; 298 | the returned string is a regular expression matching the literal text. 299 | 300 | ``` 301 | regexQuoteMeta "1.2.3" 302 | ``` 303 | 304 | The above produces `1\.2\.3` 305 | 306 | ## See Also... 307 | 308 | The [Conversion Functions](conversion.html) contain functions for converting 309 | strings. The [String Slice Functions](string_slice.html) contains functions 310 | for working with an array of strings. 311 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 17 | // FuncMap produces the function map. 18 | // 19 | // Use this to pass the functions into the template engine: 20 | // 21 | // tpl := template.New("foo").Funcs(sprig.FuncMap())) 22 | // 23 | func FuncMap() template.FuncMap { 24 | return HtmlFuncMap() 25 | } 26 | 27 | // HermeticTxtFuncMap returns a 'text/template'.FuncMap with only repeatable functions. 28 | func HermeticTxtFuncMap() ttemplate.FuncMap { 29 | r := TxtFuncMap() 30 | for _, name := range nonhermeticFunctions { 31 | delete(r, name) 32 | } 33 | return r 34 | } 35 | 36 | // HermeticHtmlFuncMap returns an 'html/template'.Funcmap with only repeatable functions. 37 | func HermeticHtmlFuncMap() template.FuncMap { 38 | r := HtmlFuncMap() 39 | for _, name := range nonhermeticFunctions { 40 | delete(r, name) 41 | } 42 | return r 43 | } 44 | 45 | // TxtFuncMap returns a 'text/template'.FuncMap 46 | func TxtFuncMap() ttemplate.FuncMap { 47 | return ttemplate.FuncMap(GenericFuncMap()) 48 | } 49 | 50 | // HtmlFuncMap returns an 'html/template'.Funcmap 51 | func HtmlFuncMap() template.FuncMap { 52 | return template.FuncMap(GenericFuncMap()) 53 | } 54 | 55 | // GenericFuncMap returns a copy of the basic function map as a map[string]interface{}. 56 | func GenericFuncMap() map[string]interface{} { 57 | gfm := make(map[string]interface{}, len(genericMap)) 58 | for k, v := range genericMap { 59 | gfm[k] = v 60 | } 61 | return gfm 62 | } 63 | 64 | // These functions are not guaranteed to evaluate to the same result for given input, because they 65 | // refer to the environment or global state. 66 | var nonhermeticFunctions = []string{ 67 | // Date functions 68 | "date", 69 | "date_in_zone", 70 | "date_modify", 71 | "now", 72 | "htmlDate", 73 | "htmlDateInZone", 74 | "dateInZone", 75 | "dateModify", 76 | 77 | // Strings 78 | "randAlphaNum", 79 | "randAlpha", 80 | "randAscii", 81 | "randNumeric", 82 | "randBytes", 83 | "uuidv4", 84 | 85 | // OS 86 | "env", 87 | "expandenv", 88 | 89 | // Network 90 | "getHostByName", 91 | } 92 | 93 | var genericMap = map[string]interface{}{ 94 | "hello": func() string { return "Hello!" }, 95 | 96 | // Date functions 97 | "ago": dateAgo, 98 | "date": date, 99 | "date_in_zone": dateInZone, 100 | "date_modify": dateModify, 101 | "dateInZone": dateInZone, 102 | "dateModify": dateModify, 103 | "duration": duration, 104 | "durationRound": durationRound, 105 | "htmlDate": htmlDate, 106 | "htmlDateInZone": htmlDateInZone, 107 | "must_date_modify": mustDateModify, 108 | "mustDateModify": mustDateModify, 109 | "mustToDate": mustToDate, 110 | "now": time.Now, 111 | "toDate": toDate, 112 | "unixEpoch": unixEpoch, 113 | 114 | // Strings 115 | "trunc": trunc, 116 | "trim": strings.TrimSpace, 117 | "upper": strings.ToUpper, 118 | "lower": strings.ToLower, 119 | "title": strings.Title, 120 | "substr": substring, 121 | // Switch order so that "foo" | repeat 5 122 | "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, 123 | // Deprecated: Use trimAll. 124 | "trimall": func(a, b string) string { return strings.Trim(b, a) }, 125 | // Switch order so that "$foo" | trimall "$" 126 | "trimAll": func(a, b string) string { return strings.Trim(b, a) }, 127 | "trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) }, 128 | "trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) }, 129 | // Switch order so that "foobar" | contains "foo" 130 | "contains": func(substr string, str string) bool { return strings.Contains(str, substr) }, 131 | "hasPrefix": func(substr string, str string) bool { return strings.HasPrefix(str, substr) }, 132 | "hasSuffix": func(substr string, str string) bool { return strings.HasSuffix(str, substr) }, 133 | "quote": quote, 134 | "squote": squote, 135 | "cat": cat, 136 | "indent": indent, 137 | "nindent": nindent, 138 | "replace": replace, 139 | "plural": plural, 140 | "sha1sum": sha1sum, 141 | "sha256sum": sha256sum, 142 | "adler32sum": adler32sum, 143 | "toString": strval, 144 | 145 | // Wrap Atoi to stop errors. 146 | "atoi": func(a string) int { i, _ := strconv.Atoi(a); return i }, 147 | "int64": toInt64, 148 | "int": toInt, 149 | "float64": toFloat64, 150 | "seq": seq, 151 | "toDecimal": toDecimal, 152 | 153 | //"gt": func(a, b int) bool {return a > b}, 154 | //"gte": func(a, b int) bool {return a >= b}, 155 | //"lt": func(a, b int) bool {return a < b}, 156 | //"lte": func(a, b int) bool {return a <= b}, 157 | 158 | // split "/" foo/bar returns map[int]string{0: foo, 1: bar} 159 | "split": split, 160 | "splitList": func(sep, orig string) []string { return strings.Split(orig, sep) }, 161 | // splitn "/" foo/bar/fuu returns map[int]string{0: foo, 1: bar/fuu} 162 | "splitn": splitn, 163 | "toStrings": strslice, 164 | 165 | "until": until, 166 | "untilStep": untilStep, 167 | 168 | // VERY basic arithmetic. 169 | "add1": func(i interface{}) int64 { return toInt64(i) + 1 }, 170 | "add": func(i ...interface{}) int64 { 171 | var a int64 = 0 172 | for _, b := range i { 173 | a += toInt64(b) 174 | } 175 | return a 176 | }, 177 | "sub": func(a, b interface{}) int64 { return toInt64(a) - toInt64(b) }, 178 | "div": func(a, b interface{}) int64 { return toInt64(a) / toInt64(b) }, 179 | "mod": func(a, b interface{}) int64 { return toInt64(a) % toInt64(b) }, 180 | "mul": func(a interface{}, v ...interface{}) int64 { 181 | val := toInt64(a) 182 | for _, b := range v { 183 | val = val * toInt64(b) 184 | } 185 | return val 186 | }, 187 | "randInt": func(min, max int) int { return rand.Intn(max-min) + min }, 188 | "biggest": max, 189 | "max": max, 190 | "min": min, 191 | "maxf": maxf, 192 | "minf": minf, 193 | "ceil": ceil, 194 | "floor": floor, 195 | "round": round, 196 | 197 | // string slices. Note that we reverse the order b/c that's better 198 | // for template processing. 199 | "join": join, 200 | "sortAlpha": sortAlpha, 201 | 202 | // Defaults 203 | "default": dfault, 204 | "empty": empty, 205 | "coalesce": coalesce, 206 | "all": all, 207 | "any": any, 208 | "compact": compact, 209 | "mustCompact": mustCompact, 210 | "fromJson": fromJson, 211 | "toJson": toJson, 212 | "toPrettyJson": toPrettyJson, 213 | "toRawJson": toRawJson, 214 | "mustFromJson": mustFromJson, 215 | "mustToJson": mustToJson, 216 | "mustToPrettyJson": mustToPrettyJson, 217 | "mustToRawJson": mustToRawJson, 218 | "ternary": ternary, 219 | 220 | // Reflection 221 | "typeOf": typeOf, 222 | "typeIs": typeIs, 223 | "typeIsLike": typeIsLike, 224 | "kindOf": kindOf, 225 | "kindIs": kindIs, 226 | "deepEqual": reflect.DeepEqual, 227 | 228 | // OS: 229 | "env": os.Getenv, 230 | "expandenv": os.ExpandEnv, 231 | 232 | // Network: 233 | "getHostByName": getHostByName, 234 | 235 | // Paths: 236 | "base": path.Base, 237 | "dir": path.Dir, 238 | "clean": path.Clean, 239 | "ext": path.Ext, 240 | "isAbs": path.IsAbs, 241 | 242 | // Filepaths: 243 | "osBase": filepath.Base, 244 | "osClean": filepath.Clean, 245 | "osDir": filepath.Dir, 246 | "osExt": filepath.Ext, 247 | "osIsAbs": filepath.IsAbs, 248 | 249 | // Encoding: 250 | "b64enc": base64encode, 251 | "b64dec": base64decode, 252 | "b32enc": base32encode, 253 | "b32dec": base32decode, 254 | 255 | // Data Structures: 256 | "tuple": list, // FIXME: with the addition of append/prepend these are no longer immutable. 257 | "list": list, 258 | "dict": dict, 259 | "get": get, 260 | "set": set, 261 | "unset": unset, 262 | "hasKey": hasKey, 263 | "pluck": pluck, 264 | "keys": keys, 265 | "pick": pick, 266 | "omit": omit, 267 | "values": values, 268 | 269 | "append": push, "push": push, 270 | "mustAppend": mustPush, "mustPush": mustPush, 271 | "prepend": prepend, 272 | "mustPrepend": mustPrepend, 273 | "first": first, 274 | "mustFirst": mustFirst, 275 | "rest": rest, 276 | "mustRest": mustRest, 277 | "last": last, 278 | "mustLast": mustLast, 279 | "initial": initial, 280 | "mustInitial": mustInitial, 281 | "reverse": reverse, 282 | "mustReverse": mustReverse, 283 | "uniq": uniq, 284 | "mustUniq": mustUniq, 285 | "without": without, 286 | "mustWithout": mustWithout, 287 | "has": has, 288 | "mustHas": mustHas, 289 | "slice": slice, 290 | "mustSlice": mustSlice, 291 | "concat": concat, 292 | "dig": dig, 293 | "chunk": chunk, 294 | "mustChunk": mustChunk, 295 | 296 | // Flow Control: 297 | "fail": func(msg string) (string, error) { return "", errors.New(msg) }, 298 | 299 | // Regex 300 | "regexMatch": regexMatch, 301 | "mustRegexMatch": mustRegexMatch, 302 | "regexFindAll": regexFindAll, 303 | "mustRegexFindAll": mustRegexFindAll, 304 | "regexFind": regexFind, 305 | "mustRegexFind": mustRegexFind, 306 | "regexReplaceAll": regexReplaceAll, 307 | "mustRegexReplaceAll": mustRegexReplaceAll, 308 | "regexReplaceAllLiteral": regexReplaceAllLiteral, 309 | "mustRegexReplaceAllLiteral": mustRegexReplaceAllLiteral, 310 | "regexSplit": regexSplit, 311 | "mustRegexSplit": mustRegexSplit, 312 | "regexQuoteMeta": regexQuoteMeta, 313 | 314 | // URLs: 315 | "urlParse": urlParse, 316 | "urlJoin": urlJoin, 317 | } 318 | -------------------------------------------------------------------------------- /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 | "os" 7 | "testing" 8 | "text/template" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestEnv(t *testing.T) { 14 | os.Setenv("FOO", "bar") 15 | tpl := `{{env "FOO"}}` 16 | if err := runt(tpl, "bar"); err != nil { 17 | t.Error(err) 18 | } 19 | } 20 | 21 | func TestExpandEnv(t *testing.T) { 22 | os.Setenv("FOO", "bar") 23 | tpl := `{{expandenv "Hello $FOO"}}` 24 | if err := runt(tpl, "Hello bar"); err != nil { 25 | t.Error(err) 26 | } 27 | } 28 | 29 | func TestBase(t *testing.T) { 30 | assert.NoError(t, runt(`{{ base "foo/bar" }}`, "bar")) 31 | } 32 | 33 | func TestDir(t *testing.T) { 34 | assert.NoError(t, runt(`{{ dir "foo/bar/baz" }}`, "foo/bar")) 35 | } 36 | 37 | func TestIsAbs(t *testing.T) { 38 | assert.NoError(t, runt(`{{ isAbs "/foo" }}`, "true")) 39 | assert.NoError(t, runt(`{{ isAbs "foo" }}`, "false")) 40 | } 41 | 42 | func TestClean(t *testing.T) { 43 | assert.NoError(t, runt(`{{ clean "/foo/../foo/../bar" }}`, "/bar")) 44 | } 45 | 46 | func TestExt(t *testing.T) { 47 | assert.NoError(t, runt(`{{ ext "/foo/bar/baz.txt" }}`, ".txt")) 48 | } 49 | 50 | func TestRegex(t *testing.T) { 51 | assert.NoError(t, runt(`{{ regexQuoteMeta "1.2.3" }}`, "1\\.2\\.3")) 52 | assert.NoError(t, runt(`{{ regexQuoteMeta "pretzel" }}`, "pretzel")) 53 | } 54 | 55 | // runt runs a template and checks that the output exactly matches the expected string. 56 | func runt(tpl, expect string) error { 57 | return runtv(tpl, expect, map[string]string{}) 58 | } 59 | 60 | // runtv takes a template, and expected return, and values for substitution. 61 | // 62 | // It runs the template and verifies that the output is an exact match. 63 | func runtv(tpl, expect string, vars interface{}) error { 64 | fmap := TxtFuncMap() 65 | t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) 66 | var b bytes.Buffer 67 | err := t.Execute(&b, vars) 68 | if err != nil { 69 | return err 70 | } 71 | if expect != b.String() { 72 | return fmt.Errorf("Expected '%s', got '%s'", expect, b.String()) 73 | } 74 | return nil 75 | } 76 | 77 | // runRaw runs a template with the given variables and returns the result. 78 | func runRaw(tpl string, vars interface{}) (string, error) { 79 | fmap := TxtFuncMap() 80 | t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) 81 | var b bytes.Buffer 82 | err := t.Execute(&b, vars) 83 | if err != nil { 84 | return "", err 85 | } 86 | return b.String(), nil 87 | } 88 | -------------------------------------------------------------------------------- /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/go-task/slim-sprig/v3 2 | 3 | go 1.20 4 | 5 | require github.com/stretchr/testify v1.9.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /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 | "reflect" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // toFloat64 converts 64-bit floats 12 | func toFloat64(v interface{}) float64 { 13 | if str, ok := v.(string); ok { 14 | iv, err := strconv.ParseFloat(str, 64) 15 | if err != nil { 16 | return 0 17 | } 18 | return iv 19 | } 20 | 21 | val := reflect.Indirect(reflect.ValueOf(v)) 22 | switch val.Kind() { 23 | case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: 24 | return float64(val.Int()) 25 | case reflect.Uint8, reflect.Uint16, reflect.Uint32: 26 | return float64(val.Uint()) 27 | case reflect.Uint, reflect.Uint64: 28 | return float64(val.Uint()) 29 | case reflect.Float32, reflect.Float64: 30 | return val.Float() 31 | case reflect.Bool: 32 | if val.Bool() { 33 | return 1 34 | } 35 | return 0 36 | default: 37 | return 0 38 | } 39 | } 40 | 41 | func toInt(v interface{}) int { 42 | //It's not optimal. Bud I don't want duplicate toInt64 code. 43 | return int(toInt64(v)) 44 | } 45 | 46 | // toInt64 converts integer types to 64-bit integers 47 | func toInt64(v interface{}) int64 { 48 | if str, ok := v.(string); ok { 49 | iv, err := strconv.ParseInt(str, 10, 64) 50 | if err != nil { 51 | return 0 52 | } 53 | return iv 54 | } 55 | 56 | val := reflect.Indirect(reflect.ValueOf(v)) 57 | switch val.Kind() { 58 | case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: 59 | return val.Int() 60 | case reflect.Uint8, reflect.Uint16, reflect.Uint32: 61 | return int64(val.Uint()) 62 | case reflect.Uint, reflect.Uint64: 63 | tv := val.Uint() 64 | if tv <= math.MaxInt64 { 65 | return int64(tv) 66 | } 67 | // TODO: What is the sensible thing to do here? 68 | return math.MaxInt64 69 | case reflect.Float32, reflect.Float64: 70 | return int64(val.Float()) 71 | case reflect.Bool: 72 | if val.Bool() { 73 | return 1 74 | } 75 | return 0 76 | default: 77 | return 0 78 | } 79 | } 80 | 81 | func max(a interface{}, i ...interface{}) int64 { 82 | aa := toInt64(a) 83 | for _, b := range i { 84 | bb := toInt64(b) 85 | if bb > aa { 86 | aa = bb 87 | } 88 | } 89 | return aa 90 | } 91 | 92 | func maxf(a interface{}, i ...interface{}) float64 { 93 | aa := toFloat64(a) 94 | for _, b := range i { 95 | bb := toFloat64(b) 96 | aa = math.Max(aa, bb) 97 | } 98 | return aa 99 | } 100 | 101 | func min(a interface{}, i ...interface{}) int64 { 102 | aa := toInt64(a) 103 | for _, b := range i { 104 | bb := toInt64(b) 105 | if bb < aa { 106 | aa = bb 107 | } 108 | } 109 | return aa 110 | } 111 | 112 | func minf(a interface{}, i ...interface{}) float64 { 113 | aa := toFloat64(a) 114 | for _, b := range i { 115 | bb := toFloat64(b) 116 | aa = math.Min(aa, bb) 117 | } 118 | return aa 119 | } 120 | 121 | func until(count int) []int { 122 | step := 1 123 | if count < 0 { 124 | step = -1 125 | } 126 | return untilStep(0, count, step) 127 | } 128 | 129 | func untilStep(start, stop, step int) []int { 130 | v := []int{} 131 | 132 | if stop < start { 133 | if step >= 0 { 134 | return v 135 | } 136 | for i := start; i > stop; i += step { 137 | v = append(v, i) 138 | } 139 | return v 140 | } 141 | 142 | if step <= 0 { 143 | return v 144 | } 145 | for i := start; i < stop; i += step { 146 | v = append(v, i) 147 | } 148 | return v 149 | } 150 | 151 | func floor(a interface{}) float64 { 152 | aa := toFloat64(a) 153 | return math.Floor(aa) 154 | } 155 | 156 | func ceil(a interface{}) float64 { 157 | aa := toFloat64(a) 158 | return math.Ceil(aa) 159 | } 160 | 161 | func round(a interface{}, p int, rOpt ...float64) float64 { 162 | roundOn := .5 163 | if len(rOpt) > 0 { 164 | roundOn = rOpt[0] 165 | } 166 | val := toFloat64(a) 167 | places := toFloat64(p) 168 | 169 | var round float64 170 | pow := math.Pow(10, places) 171 | digit := pow * val 172 | _, div := math.Modf(digit) 173 | if div >= roundOn { 174 | round = math.Ceil(digit) 175 | } else { 176 | round = math.Floor(digit) 177 | } 178 | return round / pow 179 | } 180 | 181 | // converts unix octal to decimal 182 | func toDecimal(v interface{}) int64 { 183 | result, err := strconv.ParseInt(fmt.Sprint(v), 8, 64) 184 | if err != nil { 185 | return 0 186 | } 187 | return result 188 | } 189 | 190 | func seq(params ...int) string { 191 | increment := 1 192 | switch len(params) { 193 | case 0: 194 | return "" 195 | case 1: 196 | start := 1 197 | end := params[0] 198 | if end < start { 199 | increment = -1 200 | } 201 | return intArrayToString(untilStep(start, end+increment, increment), " ") 202 | case 3: 203 | start := params[0] 204 | end := params[2] 205 | step := params[1] 206 | if end < start { 207 | increment = -1 208 | if step > 0 { 209 | return "" 210 | } 211 | } 212 | return intArrayToString(untilStep(start, end+increment, step), " ") 213 | case 2: 214 | start := params[0] 215 | end := params[1] 216 | step := 1 217 | if end < start { 218 | step = -1 219 | } 220 | return intArrayToString(untilStep(start, end+step, step), " ") 221 | default: 222 | return "" 223 | } 224 | } 225 | 226 | func intArrayToString(slice []int, delimeter string) string { 227 | return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimeter), "[]") 228 | } 229 | -------------------------------------------------------------------------------- /numeric_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestUntil(t *testing.T) { 12 | tests := map[string]string{ 13 | `{{range $i, $e := until 5}}{{$i}}{{$e}}{{end}}`: "0011223344", 14 | `{{range $i, $e := until -5}}{{$i}}{{$e}} {{end}}`: "00 1-1 2-2 3-3 4-4 ", 15 | } 16 | for tpl, expect := range tests { 17 | if err := runt(tpl, expect); err != nil { 18 | t.Error(err) 19 | } 20 | } 21 | } 22 | func TestUntilStep(t *testing.T) { 23 | tests := map[string]string{ 24 | `{{range $i, $e := untilStep 0 5 1}}{{$i}}{{$e}}{{end}}`: "0011223344", 25 | `{{range $i, $e := untilStep 3 6 1}}{{$i}}{{$e}}{{end}}`: "031425", 26 | `{{range $i, $e := untilStep 0 -10 -2}}{{$i}}{{$e}} {{end}}`: "00 1-2 2-4 3-6 4-8 ", 27 | `{{range $i, $e := untilStep 3 0 1}}{{$i}}{{$e}}{{end}}`: "", 28 | `{{range $i, $e := untilStep 3 99 0}}{{$i}}{{$e}}{{end}}`: "", 29 | `{{range $i, $e := untilStep 3 99 -1}}{{$i}}{{$e}}{{end}}`: "", 30 | `{{range $i, $e := untilStep 3 0 0}}{{$i}}{{$e}}{{end}}`: "", 31 | } 32 | for tpl, expect := range tests { 33 | if err := runt(tpl, expect); err != nil { 34 | t.Error(err) 35 | } 36 | } 37 | 38 | } 39 | func TestBiggest(t *testing.T) { 40 | tpl := `{{ biggest 1 2 3 345 5 6 7}}` 41 | if err := runt(tpl, `345`); err != nil { 42 | t.Error(err) 43 | } 44 | 45 | tpl = `{{ max 345}}` 46 | if err := runt(tpl, `345`); err != nil { 47 | t.Error(err) 48 | } 49 | } 50 | func TestMaxf(t *testing.T) { 51 | tpl := `{{ maxf 1 2 3 345.7 5 6 7}}` 52 | if err := runt(tpl, `345.7`); err != nil { 53 | t.Error(err) 54 | } 55 | 56 | tpl = `{{ max 345 }}` 57 | if err := runt(tpl, `345`); err != nil { 58 | t.Error(err) 59 | } 60 | } 61 | func TestMin(t *testing.T) { 62 | tpl := `{{ min 1 2 3 345 5 6 7}}` 63 | if err := runt(tpl, `1`); err != nil { 64 | t.Error(err) 65 | } 66 | 67 | tpl = `{{ min 345}}` 68 | if err := runt(tpl, `345`); err != nil { 69 | t.Error(err) 70 | } 71 | } 72 | 73 | func TestMinf(t *testing.T) { 74 | tpl := `{{ minf 1.4 2 3 345.6 5 6 7}}` 75 | if err := runt(tpl, `1.4`); err != nil { 76 | t.Error(err) 77 | } 78 | 79 | tpl = `{{ minf 345 }}` 80 | if err := runt(tpl, `345`); err != nil { 81 | t.Error(err) 82 | } 83 | } 84 | 85 | func TestToFloat64(t *testing.T) { 86 | target := float64(102) 87 | if target != toFloat64(int8(102)) { 88 | t.Errorf("Expected 102") 89 | } 90 | if target != toFloat64(int(102)) { 91 | t.Errorf("Expected 102") 92 | } 93 | if target != toFloat64(int32(102)) { 94 | t.Errorf("Expected 102") 95 | } 96 | if target != toFloat64(int16(102)) { 97 | t.Errorf("Expected 102") 98 | } 99 | if target != toFloat64(int64(102)) { 100 | t.Errorf("Expected 102") 101 | } 102 | if target != toFloat64("102") { 103 | t.Errorf("Expected 102") 104 | } 105 | if 0 != toFloat64("frankie") { 106 | t.Errorf("Expected 0") 107 | } 108 | if target != toFloat64(uint16(102)) { 109 | t.Errorf("Expected 102") 110 | } 111 | if target != toFloat64(uint64(102)) { 112 | t.Errorf("Expected 102") 113 | } 114 | if 102.1234 != toFloat64(float64(102.1234)) { 115 | t.Errorf("Expected 102.1234") 116 | } 117 | if 1 != toFloat64(true) { 118 | t.Errorf("Expected 102") 119 | } 120 | } 121 | func TestToInt64(t *testing.T) { 122 | target := int64(102) 123 | if target != toInt64(int8(102)) { 124 | t.Errorf("Expected 102") 125 | } 126 | if target != toInt64(int(102)) { 127 | t.Errorf("Expected 102") 128 | } 129 | if target != toInt64(int32(102)) { 130 | t.Errorf("Expected 102") 131 | } 132 | if target != toInt64(int16(102)) { 133 | t.Errorf("Expected 102") 134 | } 135 | if target != toInt64(int64(102)) { 136 | t.Errorf("Expected 102") 137 | } 138 | if target != toInt64("102") { 139 | t.Errorf("Expected 102") 140 | } 141 | if 0 != toInt64("frankie") { 142 | t.Errorf("Expected 0") 143 | } 144 | if target != toInt64(uint16(102)) { 145 | t.Errorf("Expected 102") 146 | } 147 | if target != toInt64(uint64(102)) { 148 | t.Errorf("Expected 102") 149 | } 150 | if target != toInt64(float64(102.1234)) { 151 | t.Errorf("Expected 102") 152 | } 153 | if 1 != toInt64(true) { 154 | t.Errorf("Expected 102") 155 | } 156 | } 157 | 158 | func TestToInt(t *testing.T) { 159 | target := int(102) 160 | if target != toInt(int8(102)) { 161 | t.Errorf("Expected 102") 162 | } 163 | if target != toInt(int(102)) { 164 | t.Errorf("Expected 102") 165 | } 166 | if target != toInt(int32(102)) { 167 | t.Errorf("Expected 102") 168 | } 169 | if target != toInt(int16(102)) { 170 | t.Errorf("Expected 102") 171 | } 172 | if target != toInt(int64(102)) { 173 | t.Errorf("Expected 102") 174 | } 175 | if target != toInt("102") { 176 | t.Errorf("Expected 102") 177 | } 178 | if 0 != toInt("frankie") { 179 | t.Errorf("Expected 0") 180 | } 181 | if target != toInt(uint16(102)) { 182 | t.Errorf("Expected 102") 183 | } 184 | if target != toInt(uint64(102)) { 185 | t.Errorf("Expected 102") 186 | } 187 | if target != toInt(float64(102.1234)) { 188 | t.Errorf("Expected 102") 189 | } 190 | if 1 != toInt(true) { 191 | t.Errorf("Expected 102") 192 | } 193 | } 194 | 195 | func TestToDecimal(t *testing.T) { 196 | tests := map[interface{}]int64{ 197 | "777": 511, 198 | 777: 511, 199 | 770: 504, 200 | 755: 493, 201 | } 202 | 203 | for input, expectedResult := range tests { 204 | result := toDecimal(input) 205 | if result != expectedResult { 206 | t.Errorf("Expected %v but got %v", expectedResult, result) 207 | } 208 | } 209 | } 210 | 211 | func TestAdd1(t *testing.T) { 212 | tpl := `{{ 3 | add1 }}` 213 | if err := runt(tpl, `4`); err != nil { 214 | t.Error(err) 215 | } 216 | } 217 | 218 | func TestAdd(t *testing.T) { 219 | tpl := `{{ 3 | add 1 2}}` 220 | if err := runt(tpl, `6`); err != nil { 221 | t.Error(err) 222 | } 223 | } 224 | 225 | func TestDiv(t *testing.T) { 226 | tpl := `{{ 4 | div 5 }}` 227 | if err := runt(tpl, `1`); err != nil { 228 | t.Error(err) 229 | } 230 | } 231 | 232 | func TestMul(t *testing.T) { 233 | tpl := `{{ 1 | mul "2" 3 "4"}}` 234 | if err := runt(tpl, `24`); err != nil { 235 | t.Error(err) 236 | } 237 | } 238 | 239 | func TestSub(t *testing.T) { 240 | tpl := `{{ 3 | sub 14 }}` 241 | if err := runt(tpl, `11`); err != nil { 242 | t.Error(err) 243 | } 244 | } 245 | 246 | func TestCeil(t *testing.T) { 247 | assert.Equal(t, 123.0, ceil(123)) 248 | assert.Equal(t, 123.0, ceil("123")) 249 | assert.Equal(t, 124.0, ceil(123.01)) 250 | assert.Equal(t, 124.0, ceil("123.01")) 251 | } 252 | 253 | func TestFloor(t *testing.T) { 254 | assert.Equal(t, 123.0, floor(123)) 255 | assert.Equal(t, 123.0, floor("123")) 256 | assert.Equal(t, 123.0, floor(123.9999)) 257 | assert.Equal(t, 123.0, floor("123.9999")) 258 | } 259 | 260 | func TestRound(t *testing.T) { 261 | assert.Equal(t, 123.556, round(123.5555, 3)) 262 | assert.Equal(t, 123.556, round("123.55555", 3)) 263 | assert.Equal(t, 124.0, round(123.500001, 0)) 264 | assert.Equal(t, 123.0, round(123.49999999, 0)) 265 | assert.Equal(t, 123.23, round(123.2329999, 2, .3)) 266 | assert.Equal(t, 123.24, round(123.233, 2, .3)) 267 | } 268 | 269 | func TestRandomInt(t *testing.T) { 270 | var tests = []struct { 271 | min int 272 | max int 273 | }{ 274 | {10, 11}, 275 | {10, 13}, 276 | {0, 1}, 277 | {5, 50}, 278 | } 279 | for _, v := range tests { 280 | x, _ := runRaw(fmt.Sprintf(`{{ randInt %d %d }}`, v.min, v.max), nil) 281 | r, err := strconv.Atoi(x) 282 | assert.NoError(t, err) 283 | assert.True(t, func(min, max, r int) bool { 284 | return r >= v.min && r < v.max 285 | }(v.min, v.max, r)) 286 | } 287 | } 288 | 289 | func TestSeq(t *testing.T) { 290 | tests := map[string]string{ 291 | `{{seq 0 1 3}}`: "0 1 2 3", 292 | `{{seq 0 3 10}}`: "0 3 6 9", 293 | `{{seq 3 3 2}}`: "", 294 | `{{seq 3 -3 2}}`: "3", 295 | `{{seq}}`: "", 296 | `{{seq 0 4}}`: "0 1 2 3 4", 297 | `{{seq 5}}`: "1 2 3 4 5", 298 | `{{seq -5}}`: "1 0 -1 -2 -3 -4 -5", 299 | `{{seq 0}}`: "1 0", 300 | `{{seq 0 1 2 3}}`: "", 301 | `{{seq 0 -4}}`: "0 -1 -2 -3 -4", 302 | } 303 | for tpl, expect := range tests { 304 | if err := runt(tpl, expect); err != nil { 305 | t.Error(err) 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /strings.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "encoding/base32" 5 | "encoding/base64" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func base64encode(v string) string { 13 | return base64.StdEncoding.EncodeToString([]byte(v)) 14 | } 15 | 16 | func base64decode(v string) string { 17 | data, err := base64.StdEncoding.DecodeString(v) 18 | if err != nil { 19 | return err.Error() 20 | } 21 | return string(data) 22 | } 23 | 24 | func base32encode(v string) string { 25 | return base32.StdEncoding.EncodeToString([]byte(v)) 26 | } 27 | 28 | func base32decode(v string) string { 29 | data, err := base32.StdEncoding.DecodeString(v) 30 | if err != nil { 31 | return err.Error() 32 | } 33 | return string(data) 34 | } 35 | 36 | func quote(str ...interface{}) string { 37 | out := make([]string, 0, len(str)) 38 | for _, s := range str { 39 | if s != nil { 40 | out = append(out, fmt.Sprintf("%q", strval(s))) 41 | } 42 | } 43 | return strings.Join(out, " ") 44 | } 45 | 46 | func squote(str ...interface{}) string { 47 | out := make([]string, 0, len(str)) 48 | for _, s := range str { 49 | if s != nil { 50 | out = append(out, fmt.Sprintf("'%v'", s)) 51 | } 52 | } 53 | return strings.Join(out, " ") 54 | } 55 | 56 | func cat(v ...interface{}) string { 57 | v = removeNilElements(v) 58 | r := strings.TrimSpace(strings.Repeat("%v ", len(v))) 59 | return fmt.Sprintf(r, v...) 60 | } 61 | 62 | func indent(spaces int, v string) string { 63 | pad := strings.Repeat(" ", spaces) 64 | return pad + strings.Replace(v, "\n", "\n"+pad, -1) 65 | } 66 | 67 | func nindent(spaces int, v string) string { 68 | return "\n" + indent(spaces, v) 69 | } 70 | 71 | func replace(old, new, src string) string { 72 | return strings.Replace(src, old, new, -1) 73 | } 74 | 75 | func plural(one, many string, count int) string { 76 | if count == 1 { 77 | return one 78 | } 79 | return many 80 | } 81 | 82 | func strslice(v interface{}) []string { 83 | switch v := v.(type) { 84 | case []string: 85 | return v 86 | case []interface{}: 87 | b := make([]string, 0, len(v)) 88 | for _, s := range v { 89 | if s != nil { 90 | b = append(b, strval(s)) 91 | } 92 | } 93 | return b 94 | default: 95 | val := reflect.ValueOf(v) 96 | switch val.Kind() { 97 | case reflect.Array, reflect.Slice: 98 | l := val.Len() 99 | b := make([]string, 0, l) 100 | for i := 0; i < l; i++ { 101 | value := val.Index(i).Interface() 102 | if value != nil { 103 | b = append(b, strval(value)) 104 | } 105 | } 106 | return b 107 | default: 108 | if v == nil { 109 | return []string{} 110 | } 111 | 112 | return []string{strval(v)} 113 | } 114 | } 115 | } 116 | 117 | func removeNilElements(v []interface{}) []interface{} { 118 | newSlice := make([]interface{}, 0, len(v)) 119 | for _, i := range v { 120 | if i != nil { 121 | newSlice = append(newSlice, i) 122 | } 123 | } 124 | return newSlice 125 | } 126 | 127 | func strval(v interface{}) string { 128 | switch v := v.(type) { 129 | case string: 130 | return v 131 | case []byte: 132 | return string(v) 133 | case error: 134 | return v.Error() 135 | case fmt.Stringer: 136 | return v.String() 137 | default: 138 | return fmt.Sprintf("%v", v) 139 | } 140 | } 141 | 142 | func trunc(c int, s string) string { 143 | if c < 0 && len(s)+c > 0 { 144 | return s[len(s)+c:] 145 | } 146 | if c >= 0 && len(s) > c { 147 | return s[:c] 148 | } 149 | return s 150 | } 151 | 152 | func join(sep string, v interface{}) string { 153 | return strings.Join(strslice(v), sep) 154 | } 155 | 156 | func split(sep, orig string) map[string]string { 157 | parts := strings.Split(orig, sep) 158 | res := make(map[string]string, len(parts)) 159 | for i, v := range parts { 160 | res["_"+strconv.Itoa(i)] = v 161 | } 162 | return res 163 | } 164 | 165 | func splitn(sep string, n int, orig string) map[string]string { 166 | parts := strings.SplitN(orig, sep, n) 167 | res := make(map[string]string, len(parts)) 168 | for i, v := range parts { 169 | res["_"+strconv.Itoa(i)] = v 170 | } 171 | return res 172 | } 173 | 174 | // substring creates a substring of the given string. 175 | // 176 | // If start is < 0, this calls string[:end]. 177 | // 178 | // If start is >= 0 and end < 0 or end bigger than s length, this calls string[start:] 179 | // 180 | // Otherwise, this calls string[start, end]. 181 | func substring(start, end int, s string) string { 182 | if start < 0 { 183 | return s[:end] 184 | } 185 | if end < 0 || end > len(s) { 186 | return s[start:] 187 | } 188 | return s[start:end] 189 | } 190 | -------------------------------------------------------------------------------- /strings_test.go: -------------------------------------------------------------------------------- 1 | package sprig 2 | 3 | import ( 4 | "encoding/base32" 5 | "encoding/base64" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestSubstr(t *testing.T) { 13 | tpl := `{{"fooo" | substr 0 3 }}` 14 | if err := runt(tpl, "foo"); err != nil { 15 | t.Error(err) 16 | } 17 | } 18 | 19 | func TestSubstr_shorterString(t *testing.T) { 20 | tpl := `{{"foo" | substr 0 10 }}` 21 | if err := runt(tpl, "foo"); err != nil { 22 | t.Error(err) 23 | } 24 | } 25 | 26 | func TestTrunc(t *testing.T) { 27 | tpl := `{{ "foooooo" | trunc 3 }}` 28 | if err := runt(tpl, "foo"); err != nil { 29 | t.Error(err) 30 | } 31 | tpl = `{{ "baaaaaar" | trunc -3 }}` 32 | if err := runt(tpl, "aar"); err != nil { 33 | t.Error(err) 34 | } 35 | tpl = `{{ "baaaaaar" | trunc -999 }}` 36 | if err := runt(tpl, "baaaaaar"); err != nil { 37 | t.Error(err) 38 | } 39 | tpl = `{{ "baaaaaz" | trunc 0 }}` 40 | if err := runt(tpl, ""); err != nil { 41 | t.Error(err) 42 | } 43 | } 44 | 45 | func TestQuote(t *testing.T) { 46 | tpl := `{{quote "a" "b" "c"}}` 47 | if err := runt(tpl, `"a" "b" "c"`); err != nil { 48 | t.Error(err) 49 | } 50 | tpl = `{{quote "\"a\"" "b" "c"}}` 51 | if err := runt(tpl, `"\"a\"" "b" "c"`); err != nil { 52 | t.Error(err) 53 | } 54 | tpl = `{{quote 1 2 3 }}` 55 | if err := runt(tpl, `"1" "2" "3"`); err != nil { 56 | t.Error(err) 57 | } 58 | tpl = `{{ .value | quote }}` 59 | values := map[string]interface{}{"value": nil} 60 | if err := runtv(tpl, ``, values); err != nil { 61 | t.Error(err) 62 | } 63 | } 64 | func TestSquote(t *testing.T) { 65 | tpl := `{{squote "a" "b" "c"}}` 66 | if err := runt(tpl, `'a' 'b' 'c'`); err != nil { 67 | t.Error(err) 68 | } 69 | tpl = `{{squote 1 2 3 }}` 70 | if err := runt(tpl, `'1' '2' '3'`); err != nil { 71 | t.Error(err) 72 | } 73 | tpl = `{{ .value | squote }}` 74 | values := map[string]interface{}{"value": nil} 75 | if err := runtv(tpl, ``, values); err != nil { 76 | t.Error(err) 77 | } 78 | } 79 | 80 | func TestContains(t *testing.T) { 81 | // Mainly, we're just verifying the paramater order swap. 82 | tests := []string{ 83 | `{{if contains "cat" "fair catch"}}1{{end}}`, 84 | `{{if hasPrefix "cat" "catch"}}1{{end}}`, 85 | `{{if hasSuffix "cat" "ducat"}}1{{end}}`, 86 | } 87 | for _, tt := range tests { 88 | if err := runt(tt, "1"); err != nil { 89 | t.Error(err) 90 | } 91 | } 92 | } 93 | 94 | func TestTrim(t *testing.T) { 95 | tests := []string{ 96 | `{{trim " 5.00 "}}`, 97 | `{{trimAll "$" "$5.00$"}}`, 98 | `{{trimPrefix "$" "$5.00"}}`, 99 | `{{trimSuffix "$" "5.00$"}}`, 100 | } 101 | for _, tt := range tests { 102 | if err := runt(tt, "5.00"); err != nil { 103 | t.Error(err) 104 | } 105 | } 106 | } 107 | 108 | func TestSplit(t *testing.T) { 109 | tpl := `{{$v := "foo$bar$baz" | split "$"}}{{$v._0}}` 110 | if err := runt(tpl, "foo"); err != nil { 111 | t.Error(err) 112 | } 113 | } 114 | 115 | func TestSplitn(t *testing.T) { 116 | tpl := `{{$v := "foo$bar$baz" | splitn "$" 2}}{{$v._0}}` 117 | if err := runt(tpl, "foo"); err != nil { 118 | t.Error(err) 119 | } 120 | } 121 | 122 | func TestToString(t *testing.T) { 123 | tpl := `{{ toString 1 | kindOf }}` 124 | assert.NoError(t, runt(tpl, "string")) 125 | } 126 | 127 | func TestToStrings(t *testing.T) { 128 | tpl := `{{ $s := list 1 2 3 | toStrings }}{{ index $s 1 | kindOf }}` 129 | assert.NoError(t, runt(tpl, "string")) 130 | tpl = `{{ list 1 .value 2 | toStrings }}` 131 | values := map[string]interface{}{"value": nil} 132 | if err := runtv(tpl, `[1 2]`, values); err != nil { 133 | t.Error(err) 134 | } 135 | } 136 | 137 | func TestJoin(t *testing.T) { 138 | assert.NoError(t, runt(`{{ tuple "a" "b" "c" | join "-" }}`, "a-b-c")) 139 | assert.NoError(t, runt(`{{ tuple 1 2 3 | join "-" }}`, "1-2-3")) 140 | assert.NoError(t, runtv(`{{ join "-" .V }}`, "a-b-c", map[string]interface{}{"V": []string{"a", "b", "c"}})) 141 | assert.NoError(t, runtv(`{{ join "-" .V }}`, "abc", map[string]interface{}{"V": "abc"})) 142 | assert.NoError(t, runtv(`{{ join "-" .V }}`, "1-2-3", map[string]interface{}{"V": []int{1, 2, 3}})) 143 | assert.NoError(t, runtv(`{{ join "-" .value }}`, "1-2", map[string]interface{}{"value": []interface{}{"1", nil, "2"}})) 144 | } 145 | 146 | func TestSortAlpha(t *testing.T) { 147 | // Named `append` in the function map 148 | tests := map[string]string{ 149 | `{{ list "c" "a" "b" | sortAlpha | join "" }}`: "abc", 150 | `{{ list 2 1 4 3 | sortAlpha | join "" }}`: "1234", 151 | } 152 | for tpl, expect := range tests { 153 | assert.NoError(t, runt(tpl, expect)) 154 | } 155 | } 156 | func TestBase64EncodeDecode(t *testing.T) { 157 | magicWord := "coffee" 158 | expect := base64.StdEncoding.EncodeToString([]byte(magicWord)) 159 | 160 | if expect == magicWord { 161 | t.Fatal("Encoder doesn't work.") 162 | } 163 | 164 | tpl := `{{b64enc "coffee"}}` 165 | if err := runt(tpl, expect); err != nil { 166 | t.Error(err) 167 | } 168 | tpl = fmt.Sprintf("{{b64dec %q}}", expect) 169 | if err := runt(tpl, magicWord); err != nil { 170 | t.Error(err) 171 | } 172 | } 173 | func TestBase32EncodeDecode(t *testing.T) { 174 | magicWord := "coffee" 175 | expect := base32.StdEncoding.EncodeToString([]byte(magicWord)) 176 | 177 | if expect == magicWord { 178 | t.Fatal("Encoder doesn't work.") 179 | } 180 | 181 | tpl := `{{b32enc "coffee"}}` 182 | if err := runt(tpl, expect); err != nil { 183 | t.Error(err) 184 | } 185 | tpl = fmt.Sprintf("{{b32dec %q}}", expect) 186 | if err := runt(tpl, magicWord); err != nil { 187 | t.Error(err) 188 | } 189 | } 190 | 191 | func TestCat(t *testing.T) { 192 | tpl := `{{$b := "b"}}{{"c" | cat "a" $b}}` 193 | if err := runt(tpl, "a b c"); err != nil { 194 | t.Error(err) 195 | } 196 | tpl = `{{ .value | cat "a" "b"}}` 197 | values := map[string]interface{}{"value": nil} 198 | if err := runtv(tpl, "a b", values); err != nil { 199 | t.Error(err) 200 | } 201 | } 202 | 203 | func TestIndent(t *testing.T) { 204 | tpl := `{{indent 4 "a\nb\nc"}}` 205 | if err := runt(tpl, " a\n b\n c"); err != nil { 206 | t.Error(err) 207 | } 208 | } 209 | 210 | func TestNindent(t *testing.T) { 211 | tpl := `{{nindent 4 "a\nb\nc"}}` 212 | if err := runt(tpl, "\n a\n b\n c"); err != nil { 213 | t.Error(err) 214 | } 215 | } 216 | 217 | func TestReplace(t *testing.T) { 218 | tpl := `{{"I Am Henry VIII" | replace " " "-"}}` 219 | if err := runt(tpl, "I-Am-Henry-VIII"); err != nil { 220 | t.Error(err) 221 | } 222 | } 223 | 224 | func TestPlural(t *testing.T) { 225 | tpl := `{{$num := len "two"}}{{$num}} {{$num | plural "1 char" "chars"}}` 226 | if err := runt(tpl, "3 chars"); err != nil { 227 | t.Error(err) 228 | } 229 | tpl = `{{len "t" | plural "cheese" "%d chars"}}` 230 | if err := runt(tpl, "cheese"); err != nil { 231 | t.Error(err) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------