milpa, pero para golang
This commit is contained in:
commit
c622ba72be
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# EditorConfig is awesome: http://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
max_line_length = 120
|
40
.golangci.yml
Normal file
40
.golangci.yml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
linters-settings:
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 21
|
||||||
|
tagliatelle:
|
||||||
|
case:
|
||||||
|
rules:
|
||||||
|
yaml: kebab
|
||||||
|
|
||||||
|
linters:
|
||||||
|
fast: false
|
||||||
|
enable:
|
||||||
|
- deadcode
|
||||||
|
- errcheck
|
||||||
|
- exportloopref
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- godot
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ifshort
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nilerr
|
||||||
|
- prealloc
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
|
- stylecheck
|
||||||
|
- tagliatelle
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
|
- whitespace
|
1
.tool-versions
Normal file
1
.tool-versions
Normal file
@ -0,0 +1 @@
|
|||||||
|
golang 1.18.2
|
13
LICENSE.txt
Normal file
13
LICENSE.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
41
go.mod
Normal file
41
go.mod
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
module git.rob.mx/nidito/chinampa
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/charmbracelet/glamour v0.6.0
|
||||||
|
github.com/fatih/color v1.13.0
|
||||||
|
github.com/go-playground/validator/v10 v10.11.1
|
||||||
|
github.com/sirupsen/logrus v1.9.0
|
||||||
|
github.com/spf13/cobra v1.6.1
|
||||||
|
github.com/spf13/pflag v1.0.5
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
|
github.com/gorilla/css v1.0.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.9 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.21 // indirect
|
||||||
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
|
github.com/muesli/termenv v0.13.0 // indirect
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.7.1 // indirect
|
||||||
|
github.com/yuin/goldmark v1.5.2 // indirect
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.1 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
|
||||||
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
)
|
114
go.sum
Normal file
114
go.sum
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||||
|
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||||
|
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
|
||||||
|
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
|
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
|
||||||
|
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||||
|
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
|
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||||
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||||
|
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||||
|
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||||
|
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||||
|
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||||
|
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||||
|
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||||
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||||
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
||||||
|
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||||
|
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||||
|
github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
|
||||||
|
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||||
|
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
|
||||||
|
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
|
||||||
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
|
||||||
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20221002022538-bcab6841153b h1:6e93nYa3hNqAvLr0pD4PN1fFS+gKzp2zAXqrnTCstqU=
|
||||||
|
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
|
||||||
|
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
83
internal/constants/constants.go
Normal file
83
internal/constants/constants.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package constants
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
// Embed requires an import so the compiler knows what's up. Golint requires a comment. Gotta please em both.
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
const HelpCommandName = "help"
|
||||||
|
|
||||||
|
// Environment Variables.
|
||||||
|
const EnvVarHelpUnstyled = "HELP_STYLE_PLAIN"
|
||||||
|
const EnvVarHelpStyle = "HELP_STYLE"
|
||||||
|
const EnvVarMilpaVerbose = "VERBOSE"
|
||||||
|
const EnvVarMilpaSilent = "SILENT"
|
||||||
|
const EnvVarMilpaUnstyled = "NO_COLOR"
|
||||||
|
const EnvVarMilpaForceColor = "COLOR"
|
||||||
|
const EnvVarValidationDisabled = "SKIP_VALIDATION"
|
||||||
|
const EnvVarDebug = "DEBUG"
|
||||||
|
|
||||||
|
// EnvFlagNames are flags also available as environment variables.
|
||||||
|
var EnvFlagNames = map[string]string{
|
||||||
|
"no-color": EnvVarMilpaUnstyled,
|
||||||
|
"color": EnvVarMilpaForceColor,
|
||||||
|
"silent": EnvVarMilpaSilent,
|
||||||
|
"verbose": EnvVarMilpaVerbose,
|
||||||
|
"skip-validation": EnvVarValidationDisabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit statuses
|
||||||
|
// see man sysexits || grep "#define EX" /usr/include/sysexits.h
|
||||||
|
// and https://tldp.org/LDP/abs/html/exitcodes.html
|
||||||
|
const (
|
||||||
|
// 0 means everything is fine.
|
||||||
|
ExitStatusOk = 0
|
||||||
|
// 42 provides answers to life, the universe and everything; also, renders help.
|
||||||
|
ExitStatusRenderHelp = 42
|
||||||
|
// 64 bad arguments
|
||||||
|
// EX_USAGE The command was used incorrectly, e.g., with the wrong number of arguments, a bad flag, a bad syntax in a parameter, or whatever.
|
||||||
|
ExitStatusUsage = 64
|
||||||
|
// EX_SOFTWARE An internal software error has been detected. This should be limited to non-operating system related errors as possible.
|
||||||
|
ExitStatusProgrammerError = 70
|
||||||
|
// EX_CONFIG Something was found in an unconfigured or misconfigured state.
|
||||||
|
ExitStatusConfigError = 78
|
||||||
|
// 127 command not found.
|
||||||
|
ExitStatusNotFound = 127
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContextKeyRuntimeIndex is the string key used to store context in a cobra Command.
|
||||||
|
const ContextKeyRuntimeIndex = "x-chinampa-runtime-index"
|
||||||
|
|
||||||
|
//go:embed help.md
|
||||||
|
var helpTemplateText string
|
||||||
|
|
||||||
|
// TemplateFuncs is a FuncMap with aliases to the strings package.
|
||||||
|
var TemplateFuncs = template.FuncMap{
|
||||||
|
"contains": strings.Contains,
|
||||||
|
"hasSuffix": strings.HasSuffix,
|
||||||
|
"hasPrefix": strings.HasPrefix,
|
||||||
|
"replace": strings.ReplaceAll,
|
||||||
|
"toUpper": strings.ToUpper,
|
||||||
|
"toLower": strings.ToLower,
|
||||||
|
"trim": strings.TrimSpace,
|
||||||
|
"trimSuffix": strings.TrimSuffix,
|
||||||
|
"trimPrefix": strings.TrimPrefix,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateCommandHelp holds a template for rendering command help.
|
||||||
|
var TemplateCommandHelp = template.Must(template.New("help").Funcs(TemplateFuncs).Parse(helpTemplateText))
|
70
internal/constants/help.md
Normal file
70
internal/constants/help.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{{- if not .HTMLOutput }}
|
||||||
|
# {{ if and (not (eq .Spec.FullName "joao")) (not (eq .Command.Name "help")) }}joao {{ end }}{{ .Spec.FullName }}{{if eq .Command.Name "help"}} help{{end}}
|
||||||
|
{{- else }}
|
||||||
|
---
|
||||||
|
description: {{ .Command.Short }}
|
||||||
|
---
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ .Command.Short }}
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
﹅{{ replace .Command.UseLine " [flags]" "" }}{{if .Command.HasAvailableSubCommands}} SUBCOMMAND{{end}}﹅
|
||||||
|
|
||||||
|
{{ if .Command.HasAvailableSubCommands -}}
|
||||||
|
## Subcommands
|
||||||
|
|
||||||
|
{{ $hh := .HTMLOutput -}}
|
||||||
|
{{ range .Command.Commands -}}
|
||||||
|
{{- if (or .IsAvailableCommand (eq .Name "help")) -}}
|
||||||
|
- {{ if $hh -}}
|
||||||
|
[﹅{{ .Name }}﹅]({{.Name}})
|
||||||
|
{{- else -}}
|
||||||
|
﹅{{ .Name }}﹅
|
||||||
|
{{- end }} - {{.Short}}
|
||||||
|
{{ end }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- if .Spec.Arguments -}}
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
{{ range .Spec.Arguments -}}
|
||||||
|
|
||||||
|
- ﹅{{ .Name | toUpper }}{{ if .Variadic}}...{{ end }}﹅{{ if .Required }} _required_{{ end }} - {{ .Description }}
|
||||||
|
{{ end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
|
||||||
|
{{ if and (eq .Spec.FullName "joao") (not (eq .Command.Name "help")) }}
|
||||||
|
## Description
|
||||||
|
|
||||||
|
{{ .Spec.Description }}
|
||||||
|
{{ end -}}
|
||||||
|
{{- if .Spec.HasAdditionalHelp }}
|
||||||
|
{{ .Spec.AdditionalHelp .HTMLOutput }}
|
||||||
|
{{ end -}}
|
||||||
|
|
||||||
|
|
||||||
|
{{- if .Command.HasAvailableLocalFlags}}
|
||||||
|
## Options
|
||||||
|
|
||||||
|
{{ range $name, $opt := .Spec.Options -}}
|
||||||
|
- ﹅--{{ $name }}﹅ (_{{$opt.Type}}_): {{ trimSuffix $opt.Description "."}}.{{ if $opt.Default }} Default: _{{ $opt.Default }}_.{{ end }}
|
||||||
|
{{ end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- if not (eq .Spec.FullName "joao") }}
|
||||||
|
## Description
|
||||||
|
|
||||||
|
{{ if not (eq .Command.Long "") }}{{ .Command.Long }}{{ else }}{{ .Spec.Description }}{{end}}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{- if .Command.HasAvailableInheritedFlags }}
|
||||||
|
## Global Options
|
||||||
|
|
||||||
|
{{ range $name, $opt := .GlobalOptions -}}
|
||||||
|
- ﹅--{{ $name }}﹅ (_{{$opt.Type}}_): {{$opt.Description}}.{{ if $opt.Default }} Default: _{{ $opt.Default }}_.{{ end }}
|
||||||
|
{{ end -}}
|
||||||
|
{{end}}
|
70
internal/errors/errors.go
Normal file
70
internal/errors/errors.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type NotFound struct {
|
||||||
|
Msg string
|
||||||
|
Group []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BadArguments struct {
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotExecutable struct {
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigError struct {
|
||||||
|
Err error
|
||||||
|
Config string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnvironmentError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubCommandExit struct {
|
||||||
|
Err error
|
||||||
|
ExitCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err NotFound) Error() string {
|
||||||
|
return err.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err BadArguments) Error() string {
|
||||||
|
return err.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err NotExecutable) Error() string {
|
||||||
|
return err.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err SubCommandExit) Error() string {
|
||||||
|
if err.Err != nil {
|
||||||
|
return err.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ConfigError) Error() string {
|
||||||
|
return fmt.Sprintf("Invalid configuration %s: %v", err.Config, err.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err EnvironmentError) Error() string {
|
||||||
|
return fmt.Sprintf("Invalid MILPA_ environment: %v", err.Err)
|
||||||
|
}
|
76
internal/errors/handler.go
Normal file
76
internal/errors/handler.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
_c "git.rob.mx/nidito/chinampa/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
func showHelp(cmd *cobra.Command) {
|
||||||
|
if cmd.Name() != _c.HelpCommandName {
|
||||||
|
err := cmd.Help()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(_c.ExitStatusProgrammerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleCobraExit(cmd *cobra.Command, err error) {
|
||||||
|
if err == nil {
|
||||||
|
ok, err := cmd.Flags().GetBool(_c.HelpCommandName)
|
||||||
|
if cmd.Name() == _c.HelpCommandName || err == nil && ok {
|
||||||
|
os.Exit(_c.ExitStatusRenderHelp)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(42)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tErr := err.(type) {
|
||||||
|
case SubCommandExit:
|
||||||
|
logrus.Debugf("Sub-command failed with: %s", err.Error())
|
||||||
|
os.Exit(tErr.ExitCode)
|
||||||
|
case BadArguments:
|
||||||
|
showHelp(cmd)
|
||||||
|
logrus.Error(err)
|
||||||
|
os.Exit(_c.ExitStatusUsage)
|
||||||
|
case NotFound:
|
||||||
|
showHelp(cmd)
|
||||||
|
logrus.Error(err)
|
||||||
|
os.Exit(_c.ExitStatusNotFound)
|
||||||
|
case ConfigError:
|
||||||
|
showHelp(cmd)
|
||||||
|
logrus.Error(err)
|
||||||
|
os.Exit(_c.ExitStatusConfigError)
|
||||||
|
case EnvironmentError:
|
||||||
|
logrus.Error(err)
|
||||||
|
os.Exit(_c.ExitStatusConfigError)
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(err.Error(), "unknown command") {
|
||||||
|
showHelp(cmd)
|
||||||
|
os.Exit(_c.ExitStatusNotFound)
|
||||||
|
} else if strings.HasPrefix(err.Error(), "unknown flag") || strings.HasPrefix(err.Error(), "unknown shorthand flag") {
|
||||||
|
showHelp(cmd)
|
||||||
|
logrus.Error(err)
|
||||||
|
os.Exit(_c.ExitStatusUsage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Errorf("Unknown error: %s", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
65
internal/exec/exec.go
Normal file
65
internal/exec/exec.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
os_exec "os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rob.mx/nidito/chinampa/internal/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExecFunc is replaced in tests.
|
||||||
|
var ExecFunc = WithSubshell
|
||||||
|
|
||||||
|
func WithSubshell(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
|
||||||
|
cmd := os_exec.CommandContext(ctx, executable, args...) // #nosec G204
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
cmd.Env = env
|
||||||
|
return stdout, stderr, cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec runs a subprocess and returns a list of lines from stdout.
|
||||||
|
func Exec(name string, args []string, env []string, timeout time.Duration) ([]string, cobra.ShellCompDirective, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel() // The cancel should be deferred so resources are cleaned up
|
||||||
|
|
||||||
|
logrus.Debugf("executing %s", args)
|
||||||
|
executable := args[0]
|
||||||
|
args = args[1:]
|
||||||
|
|
||||||
|
stdout, _, err := ExecFunc(ctx, env, executable, args...)
|
||||||
|
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
fmt.Println("Sub-command timed out")
|
||||||
|
logrus.Debugf("timeout running %s %s: %s", executable, args, stdout.String())
|
||||||
|
return []string{}, cobra.ShellCompDirectiveError, fmt.Errorf("timed out resolving %s %s", executable, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("error running %s %s: %s", executable, args, err)
|
||||||
|
return []string{}, cobra.ShellCompDirectiveError, errors.BadArguments{Msg: fmt.Sprintf("could not validate argument for command %s, ran <%s %s> failed: %s", name, executable, strings.Join(args, " "), err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("done running %s %s: %s", executable, args, stdout.String())
|
||||||
|
return strings.Split(strings.TrimSuffix(stdout.String(), "\n"), "\n"), cobra.ShellCompDirectiveDefault, nil
|
||||||
|
}
|
108
internal/exec/exec_test.go
Normal file
108
internal/exec/exec_test.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package exec_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
. "git.rob.mx/nidito/chinampa/internal/exec"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubshellExec(t *testing.T) {
|
||||||
|
ExecFunc = WithSubshell
|
||||||
|
stdout, directive, err := Exec("test-command", []string{"bash", "-c", `echo "stdout"; echo "stderr" >&2;`}, []string{}, 1*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("good subshell errored: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stdout) != 1 && stdout[0] == "stdout" {
|
||||||
|
t.Fatalf("good subshell returned wrong stdout: %v", stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if directive != cobra.ShellCompDirectiveDefault {
|
||||||
|
t.Fatalf("good subshell returned wrong directive: %v", directive)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, directive, err = Exec("test-command", []string{"bash", "-c", `echo "stdout"; echo "stderr" >&2; exit 2`}, []string{}, 1*time.Second)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("bad subshell did not error; stdout: %v", stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stdout) != 0 {
|
||||||
|
t.Fatalf("bad subshell returned non-empty stdout: %v", stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if directive != cobra.ShellCompDirectiveError {
|
||||||
|
t.Fatalf("bad subshell returned wrong directive: %v", directive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecTimesOut(t *testing.T) {
|
||||||
|
ExecFunc = func(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
|
||||||
|
time.Sleep(100 * time.Nanosecond)
|
||||||
|
return bytes.Buffer{}, bytes.Buffer{}, context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
_, _, err := Exec("test-command", []string{"bash", "-c", "sleep", "2"}, []string{}, 10*time.Nanosecond)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("timeout didn't happen after 10ms: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecWorksFine(t *testing.T) {
|
||||||
|
ExecFunc = func(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
|
||||||
|
var out bytes.Buffer
|
||||||
|
fmt.Fprint(&out, strings.Join([]string{
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"c",
|
||||||
|
}, "\n"))
|
||||||
|
return out, bytes.Buffer{}, nil
|
||||||
|
}
|
||||||
|
args := []string{"a", "b", "c"}
|
||||||
|
res, directive, err := Exec("test-command", append([]string{"bash", "-c", "echo"}, args...), []string{}, 1*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("good command failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if directive != 0 {
|
||||||
|
t.Fatalf("good command resulted in wrong directive, expected %d, got %d", 0, directive)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Join(args, "-") != strings.Join(res, "-") {
|
||||||
|
t.Fatalf("good command resulted in wrong results, expected %v, got %v", res, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecErrors(t *testing.T) {
|
||||||
|
ExecFunc = func(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
|
||||||
|
return bytes.Buffer{}, bytes.Buffer{}, fmt.Errorf("bad command is bad")
|
||||||
|
}
|
||||||
|
res, directive, err := Exec("test-command", []string{"bash", "-c", "bad-command"}, []string{}, 1*time.Second)
|
||||||
|
if err == fmt.Errorf("bad command is bad") {
|
||||||
|
t.Fatalf("bad command didn't fail: %v", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
if directive != cobra.ShellCompDirectiveError {
|
||||||
|
t.Fatalf("bad command resulted in wrong directive, expected %d, got %d", cobra.ShellCompDirectiveError, directive)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res) > 0 {
|
||||||
|
t.Fatalf("bad command returned values, got %v", res)
|
||||||
|
}
|
||||||
|
}
|
119
internal/registry/cobra.go
Normal file
119
internal/registry/cobra.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
_c "git.rob.mx/nidito/chinampa/internal/constants"
|
||||||
|
"git.rob.mx/nidito/chinampa/internal/errors"
|
||||||
|
"git.rob.mx/nidito/chinampa/pkg/command"
|
||||||
|
"git.rob.mx/nidito/chinampa/pkg/runtime"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ccRoot = &cobra.Command{
|
||||||
|
Use: "joao [--silent|-v|--verbose] [--[no-]color] [-h|--help] [--version]",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
_c.ContextKeyRuntimeIndex: "joao",
|
||||||
|
},
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
SilenceUsage: true,
|
||||||
|
SilenceErrors: true,
|
||||||
|
ValidArgs: []string{""},
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
err := cobra.OnlyValidArgs(cmd, args)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
suggestions := []string{}
|
||||||
|
bold := color.New(color.Bold)
|
||||||
|
for _, l := range cmd.SuggestionsFor(args[len(args)-1]) {
|
||||||
|
suggestions = append(suggestions, bold.Sprint(l))
|
||||||
|
}
|
||||||
|
errMessage := fmt.Sprintf("Unknown subcommand %s", bold.Sprint(strings.Join(args, " ")))
|
||||||
|
if len(suggestions) > 0 {
|
||||||
|
errMessage += ". Perhaps you meant " + strings.Join(suggestions, ", ") + "?"
|
||||||
|
}
|
||||||
|
return errors.NotFound{Msg: errMessage, Group: []string{}}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
if ok, err := cmd.Flags().GetBool("version"); err == nil && ok {
|
||||||
|
_, err := cmd.OutOrStdout().Write([]byte(cmd.Root().Annotations["version"]))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errors.NotFound{Msg: "No subcommand provided", Group: []string{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCobra(cmd *command.Command, globalOptions command.Options) *cobra.Command {
|
||||||
|
localName := cmd.Name()
|
||||||
|
useSpec := []string{localName, "[options]"}
|
||||||
|
for _, arg := range cmd.Arguments {
|
||||||
|
useSpec = append(useSpec, arg.ToDesc())
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := &cobra.Command{
|
||||||
|
Use: strings.Join(useSpec, " "),
|
||||||
|
Short: cmd.Summary,
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
SilenceUsage: true,
|
||||||
|
SilenceErrors: true,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
_c.ContextKeyRuntimeIndex: cmd.FullName(),
|
||||||
|
},
|
||||||
|
Args: func(cc *cobra.Command, supplied []string) error {
|
||||||
|
skipValidation, _ := cc.Flags().GetBool("skip-validation")
|
||||||
|
if !skipValidation && runtime.ValidationEnabled() {
|
||||||
|
cmd.Arguments.Parse(supplied)
|
||||||
|
return cmd.Arguments.AreValid()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
RunE: cmd.Run,
|
||||||
|
}
|
||||||
|
|
||||||
|
cc.SetFlagErrorFunc(func(c *cobra.Command, e error) error {
|
||||||
|
return errors.BadArguments{Msg: e.Error()}
|
||||||
|
})
|
||||||
|
|
||||||
|
cc.ValidArgsFunction = cmd.Arguments.CompletionFunction
|
||||||
|
|
||||||
|
cc.Flags().AddFlagSet(cmd.FlagSet())
|
||||||
|
|
||||||
|
for name, opt := range cmd.Options {
|
||||||
|
if err := cc.RegisterFlagCompletionFunc(name, opt.CompletionFunction); err != nil {
|
||||||
|
logrus.Errorf("Failed setting up autocompletion for option <%s> of command <%s>", name, cmd.FullName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cc.SetHelpFunc(cmd.HelpRenderer(globalOptions))
|
||||||
|
cmd.SetCobra(cc)
|
||||||
|
return cc
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromCobra(cc *cobra.Command) *command.Command {
|
||||||
|
rtidx, hasAnnotation := cc.Annotations[_c.ContextKeyRuntimeIndex]
|
||||||
|
if hasAnnotation {
|
||||||
|
return Get(rtidx)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
227
internal/registry/registry.go
Normal file
227
internal/registry/registry.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
_c "git.rob.mx/nidito/chinampa/internal/constants"
|
||||||
|
"git.rob.mx/nidito/chinampa/internal/errors"
|
||||||
|
"git.rob.mx/nidito/chinampa/pkg/command"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var registry = &CommandRegistry{
|
||||||
|
kv: map[string]*command.Command{},
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByPath []*command.Command
|
||||||
|
|
||||||
|
func (cmds ByPath) Len() int { return len(cmds) }
|
||||||
|
func (cmds ByPath) Swap(i, j int) { cmds[i], cmds[j] = cmds[j], cmds[i] }
|
||||||
|
func (cmds ByPath) Less(i, j int) bool { return cmds[i].FullName() < cmds[j].FullName() }
|
||||||
|
|
||||||
|
type CommandTree struct {
|
||||||
|
Command *command.Command `json:"command"`
|
||||||
|
Children []*CommandTree `json:"children"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *CommandTree) Traverse(fn func(cmd *command.Command) error) error {
|
||||||
|
for _, child := range t.Children {
|
||||||
|
if err := fn(child.Command); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := child.Traverse(fn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandRegistry struct {
|
||||||
|
kv map[string]*command.Command
|
||||||
|
byPath []*command.Command
|
||||||
|
tree *CommandTree
|
||||||
|
}
|
||||||
|
|
||||||
|
func Register(cmd *command.Command) {
|
||||||
|
logrus.Debugf("Registering %s", cmd.FullName())
|
||||||
|
registry.kv[cmd.FullName()] = cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(id string) *command.Command {
|
||||||
|
return registry.kv[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
func CommandList() []*command.Command {
|
||||||
|
if len(registry.byPath) == 0 {
|
||||||
|
list := []*command.Command{}
|
||||||
|
for _, v := range registry.kv {
|
||||||
|
list = append(list, v)
|
||||||
|
}
|
||||||
|
sort.Sort(ByPath(list))
|
||||||
|
registry.byPath = list
|
||||||
|
}
|
||||||
|
|
||||||
|
return registry.byPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildTree(cc *cobra.Command, depth int) {
|
||||||
|
tree := &CommandTree{
|
||||||
|
Command: fromCobra(cc),
|
||||||
|
Children: []*CommandTree{},
|
||||||
|
}
|
||||||
|
|
||||||
|
var populateTree func(cmd *cobra.Command, ct *CommandTree, maxDepth int, depth int)
|
||||||
|
populateTree = func(cmd *cobra.Command, ct *CommandTree, maxDepth int, depth int) {
|
||||||
|
newDepth := depth + 1
|
||||||
|
for _, subcc := range cmd.Commands() {
|
||||||
|
if subcc.Hidden {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd := fromCobra(subcc); cmd != nil {
|
||||||
|
leaf := &CommandTree{Children: []*CommandTree{}}
|
||||||
|
leaf.Command = cmd
|
||||||
|
ct.Children = append(ct.Children, leaf)
|
||||||
|
|
||||||
|
if newDepth < maxDepth {
|
||||||
|
populateTree(subcc, leaf, maxDepth, newDepth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
populateTree(cc, tree, depth, 0)
|
||||||
|
|
||||||
|
registry.tree = tree
|
||||||
|
}
|
||||||
|
|
||||||
|
func SerializeTree(serializationFn func(any) ([]byte, error)) (string, error) {
|
||||||
|
bytes, err := serializationFn(registry.tree)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChildrenNames() []string {
|
||||||
|
if registry.tree == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make([]string, len(registry.tree.Children))
|
||||||
|
for idx, cmd := range registry.tree.Children {
|
||||||
|
ret[idx] = cmd.Command.Name()
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute(version string) error {
|
||||||
|
cmdRoot := command.Root
|
||||||
|
ccRoot.Short = cmdRoot.Summary
|
||||||
|
ccRoot.Long = cmdRoot.Description
|
||||||
|
ccRoot.Annotations["version"] = version
|
||||||
|
ccRoot.CompletionOptions.HiddenDefaultCmd = true
|
||||||
|
ccRoot.Flags().AddFlagSet(cmdRoot.FlagSet())
|
||||||
|
|
||||||
|
for name, opt := range cmdRoot.Options {
|
||||||
|
if err := ccRoot.RegisterFlagCompletionFunc(name, opt.CompletionFunction); err != nil {
|
||||||
|
logrus.Errorf("Failed setting up autocompletion for option <%s> of command <%s>", name, cmdRoot.FullName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ccRoot.SetHelpFunc(cmdRoot.HelpRenderer(cmdRoot.Options))
|
||||||
|
|
||||||
|
for _, cmd := range CommandList() {
|
||||||
|
cmd := cmd
|
||||||
|
leaf := toCobra(cmd, cmdRoot.Options)
|
||||||
|
container := ccRoot
|
||||||
|
for idx, cp := range cmd.Path {
|
||||||
|
if idx == len(cmd.Path)-1 {
|
||||||
|
// logrus.Debugf("adding command %s to %s", leaf.Name(), cmd.Path[0:idx])
|
||||||
|
container.AddCommand(leaf)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
query := []string{cp}
|
||||||
|
if cc, _, err := container.Find(query); err == nil && cc != container {
|
||||||
|
container = cc
|
||||||
|
} else {
|
||||||
|
groupName := strings.Join(query, " ")
|
||||||
|
groupPath := append(cmd.Path[0:idx], query...) // nolint:gocritic
|
||||||
|
cc := &cobra.Command{
|
||||||
|
Use: cp,
|
||||||
|
Short: fmt.Sprintf("%s subcommands", groupName),
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
SuggestionsMinimumDistance: 2,
|
||||||
|
SilenceUsage: true,
|
||||||
|
SilenceErrors: true,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
_c.ContextKeyRuntimeIndex: strings.Join(groupPath, " "),
|
||||||
|
},
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := cobra.OnlyValidArgs(cmd, args); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestions := []string{}
|
||||||
|
bold := color.New(color.Bold)
|
||||||
|
for _, l := range cmd.SuggestionsFor(args[len(args)-1]) {
|
||||||
|
suggestions = append(suggestions, bold.Sprint(l))
|
||||||
|
}
|
||||||
|
last := len(args) - 1
|
||||||
|
parent := cmd.CommandPath()
|
||||||
|
errMessage := fmt.Sprintf("Unknown subcommand %s of known command %s", bold.Sprint(args[last]), bold.Sprint(parent))
|
||||||
|
if len(suggestions) > 0 {
|
||||||
|
errMessage += ". Perhaps you meant " + strings.Join(suggestions, ", ") + "?"
|
||||||
|
}
|
||||||
|
return errors.NotFound{Msg: errMessage, Group: []string{}}
|
||||||
|
},
|
||||||
|
ValidArgs: []string{""},
|
||||||
|
RunE: func(cc *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.NotFound{Msg: "No subcommand provided", Group: []string{}}
|
||||||
|
}
|
||||||
|
os.Exit(_c.ExitStatusNotFound)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
groupParent := &command.Command{
|
||||||
|
Path: cmd.Path[0 : len(cmd.Path)-1],
|
||||||
|
Summary: fmt.Sprintf("%s subcommands", groupName),
|
||||||
|
Description: fmt.Sprintf("Runs subcommands within %s", groupName),
|
||||||
|
Arguments: command.Arguments{},
|
||||||
|
Options: command.Options{},
|
||||||
|
}
|
||||||
|
Register(groupParent)
|
||||||
|
cc.SetHelpFunc(groupParent.HelpRenderer(command.Options{}))
|
||||||
|
container.AddCommand(cc)
|
||||||
|
container = cc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmdRoot.SetCobra(ccRoot)
|
||||||
|
|
||||||
|
err := ccRoot.Execute()
|
||||||
|
if err != nil {
|
||||||
|
errors.HandleCobraExit(ccRoot, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
70
internal/render/render.go
Normal file
70
internal/render/render.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
_c "git.rob.mx/nidito/chinampa/internal/constants"
|
||||||
|
"git.rob.mx/nidito/chinampa/pkg/runtime"
|
||||||
|
"github.com/charmbracelet/glamour"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addBackticks(str []byte) []byte {
|
||||||
|
return bytes.ReplaceAll(str, []byte("﹅"), []byte("`"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Markdown(content []byte, withColor bool) ([]byte, error) {
|
||||||
|
content = addBackticks(content)
|
||||||
|
|
||||||
|
if runtime.UnstyledHelpEnabled() {
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
width, _, err := term.GetSize(0)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("Could not get terminal width")
|
||||||
|
width = 80
|
||||||
|
}
|
||||||
|
|
||||||
|
var styleFunc glamour.TermRendererOption
|
||||||
|
|
||||||
|
if withColor {
|
||||||
|
style := os.Getenv(_c.EnvVarHelpStyle)
|
||||||
|
switch style {
|
||||||
|
case "dark":
|
||||||
|
styleFunc = glamour.WithStandardStyle("dark")
|
||||||
|
case "light":
|
||||||
|
styleFunc = glamour.WithStandardStyle("light")
|
||||||
|
default:
|
||||||
|
styleFunc = glamour.WithStandardStyle("auto")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
styleFunc = glamour.WithStandardStyle("notty")
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer, err := glamour.NewTermRenderer(
|
||||||
|
styleFunc,
|
||||||
|
glamour.WithEmoji(),
|
||||||
|
glamour.WithWordWrap(width),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return content, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderer.RenderBytes(content)
|
||||||
|
}
|
87
internal/render/render_test.go
Normal file
87
internal/render/render_test.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package render_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_c "git.rob.mx/nidito/chinampa/internal/constants"
|
||||||
|
"git.rob.mx/nidito/chinampa/internal/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarkdownUnstyled(t *testing.T) {
|
||||||
|
content := []byte("# hello")
|
||||||
|
os.Setenv(_c.EnvVarHelpUnstyled, "true")
|
||||||
|
res, err := render.Markdown(content, false)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []byte("# hello") // nolint:ifshort
|
||||||
|
if !reflect.DeepEqual(res, expected) {
|
||||||
|
t.Fatalf("Unexpected response ---\n%s\n---\n wanted:\n---\n%s\n---", res, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarkdownNoColor(t *testing.T) {
|
||||||
|
os.Unsetenv(_c.EnvVarHelpUnstyled)
|
||||||
|
content := []byte("# hello ﹅world﹅")
|
||||||
|
res, err := render.Markdown(content, false)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// account for 80 character width word wrapping
|
||||||
|
// our string is 15 characters, there's 2 for padding at the start
|
||||||
|
spaces := " "
|
||||||
|
|
||||||
|
expected := []byte("\n # hello `world`" + spaces + "\n\n") // nolint:ifshort
|
||||||
|
if !reflect.DeepEqual(res, expected) {
|
||||||
|
t.Fatalf("Unexpected response ---\n%s\n---\n wanted:\n---\n%s\n---", res, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var autoStyleTestRender = "\n\x1b[38;5;228;48;5;63;1m\x1b[0m\x1b[38;5;228;48;5;63;1m\x1b[0m \x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;228;48;5;63;1mhello\x1b[0m\x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;252m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[0m\n\x1b[0m\n"
|
||||||
|
|
||||||
|
const lightStyleTestRender = "\n\x1b[38;5;228;48;5;63;1m\x1b[0m\x1b[38;5;228;48;5;63;1m\x1b[0m \x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;228;48;5;63;1mhello\x1b[0m\x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;234m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[0m\n\x1b[0m\n"
|
||||||
|
|
||||||
|
func TestMarkdownColor(t *testing.T) {
|
||||||
|
os.Unsetenv(_c.EnvVarHelpUnstyled)
|
||||||
|
content := []byte("# hello")
|
||||||
|
|
||||||
|
styles := map[string][]byte{
|
||||||
|
"": []byte(autoStyleTestRender),
|
||||||
|
"dark": []byte(autoStyleTestRender),
|
||||||
|
"auto": []byte(autoStyleTestRender),
|
||||||
|
"light": []byte(lightStyleTestRender),
|
||||||
|
}
|
||||||
|
for style, expected := range styles {
|
||||||
|
t.Run(fmt.Sprintf("style %s", style), func(t *testing.T) {
|
||||||
|
os.Setenv(_c.EnvVarHelpStyle, style)
|
||||||
|
res, err := render.Markdown(content, true)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(res, expected) {
|
||||||
|
t.Fatalf("Unexpected response ---\n%v\n---\n wanted:\n---\n%v\n---", res, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
26
main.go
Normal file
26
main.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package chinampa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.rob.mx/nidito/chinampa/internal/registry"
|
||||||
|
"git.rob.mx/nidito/chinampa/pkg/command"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Register(cmd *command.Command) {
|
||||||
|
registry.Register(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute(version string) error {
|
||||||
|
return registry.Execute(version)
|
||||||
|
}
|
283
pkg/command/arguments.go
Normal file
283
pkg/command/arguments.go
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.rob.mx/nidito/chinampa/internal/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func contains(haystack []string, needle string) bool {
|
||||||
|
for _, validValue := range haystack {
|
||||||
|
if needle == validValue {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arguments is an ordered list of Argument.
|
||||||
|
type Arguments []*Argument
|
||||||
|
|
||||||
|
func (args *Arguments) AllKnown() map[string]any {
|
||||||
|
col := map[string]any{}
|
||||||
|
for _, arg := range *args {
|
||||||
|
col[arg.Name] = arg.ToValue()
|
||||||
|
}
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args *Arguments) AllKnownStr() map[string]string {
|
||||||
|
col := map[string]string{}
|
||||||
|
for _, arg := range *args {
|
||||||
|
col[arg.Name] = arg.ToString()
|
||||||
|
}
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args *Arguments) Parse(supplied []string) {
|
||||||
|
for idx, arg := range *args {
|
||||||
|
argumentProvided := idx < len(supplied)
|
||||||
|
|
||||||
|
if !argumentProvided {
|
||||||
|
if arg.Default != nil {
|
||||||
|
if arg.Variadic {
|
||||||
|
defaultSlice := []string{}
|
||||||
|
for _, valI := range arg.Default.([]any) {
|
||||||
|
defaultSlice = append(defaultSlice, valI.(string))
|
||||||
|
}
|
||||||
|
arg.provided = &defaultSlice
|
||||||
|
} else {
|
||||||
|
defaultString := arg.Default.(string)
|
||||||
|
if defaultString != "" {
|
||||||
|
arg.provided = &[]string{defaultString}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if arg.Variadic {
|
||||||
|
values := append([]string{}, supplied[idx:]...)
|
||||||
|
arg.SetValue(values)
|
||||||
|
} else {
|
||||||
|
arg.SetValue([]string{supplied[idx]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args *Arguments) AreValid() error {
|
||||||
|
for _, arg := range *args {
|
||||||
|
if err := arg.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompletionFunction is called by cobra when asked to complete arguments.
|
||||||
|
func (args *Arguments) CompletionFunction(cc *cobra.Command, provided []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
expectedArgLen := len(*args)
|
||||||
|
values := []string{}
|
||||||
|
directive := cobra.ShellCompDirectiveError
|
||||||
|
|
||||||
|
if expectedArgLen > 0 {
|
||||||
|
argsCompleted := len(provided)
|
||||||
|
lastArg := (*args)[len(*args)-1]
|
||||||
|
hasVariadicArg := expectedArgLen > 0 && lastArg.Variadic
|
||||||
|
lastArg.Command.Options.Parse(cc.Flags())
|
||||||
|
args.Parse(provided)
|
||||||
|
|
||||||
|
directive = cobra.ShellCompDirectiveDefault
|
||||||
|
if argsCompleted < expectedArgLen || hasVariadicArg {
|
||||||
|
var arg *Argument
|
||||||
|
if hasVariadicArg && argsCompleted >= expectedArgLen {
|
||||||
|
// completing a variadic argument
|
||||||
|
arg = lastArg
|
||||||
|
} else {
|
||||||
|
// completing regular argument (maybe variadic!)
|
||||||
|
arg = (*args)[argsCompleted]
|
||||||
|
}
|
||||||
|
|
||||||
|
if arg.Values != nil {
|
||||||
|
var err error
|
||||||
|
arg.Values.command = lastArg.Command
|
||||||
|
arg.Command = lastArg.Command
|
||||||
|
values, directive, err = arg.Resolve(toComplete)
|
||||||
|
if err != nil {
|
||||||
|
return []string{err.Error()}, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
directive = cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
values = cobra.AppendActiveHelp(values, arg.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if toComplete != "" {
|
||||||
|
filtered := []string{}
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.HasPrefix(value, toComplete) {
|
||||||
|
filtered = append(filtered, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values = filtered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, directive
|
||||||
|
}
|
||||||
|
|
||||||
|
// Argument represents a single command-line argument.
|
||||||
|
type Argument struct {
|
||||||
|
// Name is how this variable will be exposed to the underlying command.
|
||||||
|
Name string `json:"name" yaml:"name" validate:"required,excludesall=!$\\/%^@#?:'\""`
|
||||||
|
// Description is what this argument is for.
|
||||||
|
Description string `json:"description" yaml:"description" validate:"required"`
|
||||||
|
// Default is the default value for this argument if none is provided.
|
||||||
|
Default any `json:"default,omitempty" yaml:"default,omitempty" validate:"excluded_with=Required"`
|
||||||
|
// Variadic makes an argument a list of all values from this one on.
|
||||||
|
Variadic bool `json:"variadic" yaml:"variadic"`
|
||||||
|
// Required raises an error if an argument is not provided.
|
||||||
|
Required bool `json:"required" yaml:"required" validate:"excluded_with=Default"`
|
||||||
|
// Values describes autocompletion and validation for an argument
|
||||||
|
Values *ValueSource `json:"values,omitempty" yaml:"values" validate:"omitempty"`
|
||||||
|
Command *Command `json:"-" yaml:"-" validate:"-"`
|
||||||
|
provided *[]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arg *Argument) EnvName() string {
|
||||||
|
return strings.ToUpper(strings.ReplaceAll(arg.Name, "-", "_"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arg *Argument) SetValue(value []string) {
|
||||||
|
arg.provided = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arg *Argument) IsKnown() bool {
|
||||||
|
return arg.provided != nil && len(*arg.provided) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arg *Argument) ToString() string {
|
||||||
|
val := arg.ToValue()
|
||||||
|
|
||||||
|
if arg.Variadic {
|
||||||
|
val := val.([]string)
|
||||||
|
return strings.Join(val, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arg *Argument) ToValue() any {
|
||||||
|
var value any
|
||||||
|
if arg.IsKnown() {
|
||||||
|
if arg.Variadic {
|
||||||
|
value = *arg.provided
|
||||||
|
} else {
|
||||||
|
vals := *arg.provided
|
||||||
|
value = vals[0]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if arg.Default != nil {
|
||||||
|
if arg.Variadic {
|
||||||
|
defaultSlice := []string{}
|
||||||
|
for _, valI := range arg.Default.([]any) {
|
||||||
|
valStr := valI.(string)
|
||||||
|
defaultSlice = append(defaultSlice, valStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
value = defaultSlice
|
||||||
|
} else {
|
||||||
|
value = arg.Default.(string)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if arg.Variadic {
|
||||||
|
value = []string{}
|
||||||
|
} else {
|
||||||
|
value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arg *Argument) Validate() error {
|
||||||
|
if !arg.IsKnown() {
|
||||||
|
if arg.Required {
|
||||||
|
return errors.BadArguments{Msg: fmt.Sprintf("Missing argument for %s", strings.ToUpper(arg.Name))}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !arg.Validates() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
validValues, _, err := arg.Resolve(strings.Join(*arg.provided, " "))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if arg.Variadic {
|
||||||
|
for _, current := range *arg.provided {
|
||||||
|
if !contains(validValues, current) {
|
||||||
|
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for argument <%s>. Valid options are: %s", current, arg.Name, strings.Join(validValues, ", "))}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current := arg.ToValue().(string)
|
||||||
|
if !contains(validValues, current) {
|
||||||
|
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for argument <%s>. Valid options are: %s", current, arg.Name, strings.Join(validValues, ", "))}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates tells if the user-supplied value needs validation.
|
||||||
|
func (arg *Argument) Validates() bool {
|
||||||
|
return arg.Values != nil && arg.Values.Validates()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDesc prints out the description of an argument for help and docs.
|
||||||
|
func (arg *Argument) ToDesc() string {
|
||||||
|
spec := arg.EnvName()
|
||||||
|
if arg.Variadic {
|
||||||
|
spec = fmt.Sprintf("%s...", spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !arg.Required {
|
||||||
|
spec = fmt.Sprintf("[%s]", spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve returns autocomplete values for an argument.
|
||||||
|
func (arg *Argument) Resolve(current string) (values []string, flag cobra.ShellCompDirective, err error) {
|
||||||
|
if arg.Values != nil {
|
||||||
|
values, flag, err = arg.Values.Resolve(current)
|
||||||
|
if err != nil {
|
||||||
|
flag = cobra.ShellCompDirectiveError
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
425
pkg/command/arguments_test.go
Normal file
425
pkg/command/arguments_test.go
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package command_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "git.rob.mx/nidito/chinampa/pkg/command"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testCommand() *Command {
|
||||||
|
return (&Command{
|
||||||
|
Arguments: []*Argument{
|
||||||
|
{
|
||||||
|
Name: "first",
|
||||||
|
Default: "default",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "variadic",
|
||||||
|
Default: []any{"defaultVariadic0", "defaultVariadic1"},
|
||||||
|
Variadic: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Options: Options{
|
||||||
|
"option": {
|
||||||
|
Default: "default",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
"bool": {
|
||||||
|
Type: "bool",
|
||||||
|
Default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).SetBindings()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
cmd := testCommand()
|
||||||
|
cmd.Arguments.Parse([]string{"asdf", "one", "two", "three"})
|
||||||
|
known := cmd.Arguments.AllKnown()
|
||||||
|
|
||||||
|
if !cmd.Arguments[0].IsKnown() {
|
||||||
|
t.Fatalf("first argument isn't known")
|
||||||
|
}
|
||||||
|
val, exists := known["first"]
|
||||||
|
if !exists {
|
||||||
|
t.Fatalf("first argument isn't on AllKnown map: %v", known)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val != "asdf" {
|
||||||
|
t.Fatalf("first argument does not match. expected: %s, got %s", "asdf", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmd.Arguments[1].IsKnown() {
|
||||||
|
t.Fatalf("variadic argument isn't known")
|
||||||
|
}
|
||||||
|
val, exists = known["variadic"]
|
||||||
|
if !exists {
|
||||||
|
t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(val, []string{"one", "two", "three"}) {
|
||||||
|
t.Fatalf("Known argument does not match. expected: %s, got %s", "one two three", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = testCommand()
|
||||||
|
cmd.Arguments.Parse([]string{"asdf"})
|
||||||
|
known = cmd.Arguments.AllKnown()
|
||||||
|
|
||||||
|
if !cmd.Arguments[0].IsKnown() {
|
||||||
|
t.Fatalf("first argument is not known")
|
||||||
|
}
|
||||||
|
|
||||||
|
val, exists = known["first"]
|
||||||
|
if !exists {
|
||||||
|
t.Fatalf("first argument isn't on AllKnown map: %v", known)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val != "asdf" {
|
||||||
|
t.Fatalf("first argument does not match. expected: %s, got %s", "asdf", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
val, exists = known["variadic"]
|
||||||
|
if !exists {
|
||||||
|
t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{"defaultVariadic0", "defaultVariadic1"}
|
||||||
|
if !reflect.DeepEqual(val, expected) {
|
||||||
|
t.Fatalf("variadic argument does not match. expected: %s, got %s", expected, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBeforeParse(t *testing.T) {
|
||||||
|
cmd := testCommand()
|
||||||
|
known := cmd.Arguments.AllKnown()
|
||||||
|
|
||||||
|
if cmd.Arguments[0].IsKnown() {
|
||||||
|
t.Fatalf("first argument is known")
|
||||||
|
}
|
||||||
|
|
||||||
|
val, exists := known["first"]
|
||||||
|
if !exists {
|
||||||
|
t.Fatalf("first argument isn't on AllKnown map: %v", known)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val != "default" {
|
||||||
|
t.Fatalf("first argument does not match. expected: %s, got %s", "asdf", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
val, exists = known["variadic"]
|
||||||
|
if !exists {
|
||||||
|
t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{"defaultVariadic0", "defaultVariadic1"}
|
||||||
|
if !reflect.DeepEqual(val, expected) {
|
||||||
|
t.Fatalf("variadic argument does not match. expected: %s, got %s", expected, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgumentsValidate(t *testing.T) {
|
||||||
|
staticArgument := func(name string, def string, values []string, variadic bool) *Argument {
|
||||||
|
return &Argument{
|
||||||
|
Name: name,
|
||||||
|
Default: def,
|
||||||
|
Variadic: variadic,
|
||||||
|
Required: def == "",
|
||||||
|
Values: &ValueSource{
|
||||||
|
Static: &values,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
Command *Command
|
||||||
|
Args []string
|
||||||
|
ErrorSuffix string
|
||||||
|
Env []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Command: (&Command{
|
||||||
|
// Name: []string{"test", "required", "failure"},
|
||||||
|
Arguments: []*Argument{
|
||||||
|
{
|
||||||
|
Name: "first",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).SetBindings(),
|
||||||
|
ErrorSuffix: "Missing argument for FIRST",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Args: []string{"bad"},
|
||||||
|
ErrorSuffix: "bad is not a valid value for argument <first>. Valid options are: good, default",
|
||||||
|
Command: (&Command{
|
||||||
|
// Name: []string{"test", "script", "bad"},
|
||||||
|
Arguments: []*Argument{
|
||||||
|
{
|
||||||
|
Name: "first",
|
||||||
|
Default: "default",
|
||||||
|
Values: &ValueSource{
|
||||||
|
Script: "echo good; echo default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).SetBindings(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Args: []string{"bad"},
|
||||||
|
ErrorSuffix: "bad is not a valid value for argument <first>. Valid options are: default, good",
|
||||||
|
Command: (&Command{
|
||||||
|
// Name: []string{"test", "static", "errors"},
|
||||||
|
Arguments: []*Argument{staticArgument("first", "default", []string{"default", "good"}, false)},
|
||||||
|
}).SetBindings(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Args: []string{"default", "good", "bad"},
|
||||||
|
ErrorSuffix: "bad is not a valid value for argument <first>. Valid options are: default, good",
|
||||||
|
Command: (&Command{
|
||||||
|
// Name: []string{"test", "static", "errors"},
|
||||||
|
Arguments: []*Argument{staticArgument("first", "default", []string{"default", "good"}, true)},
|
||||||
|
}).SetBindings(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Args: []string{"good"},
|
||||||
|
ErrorSuffix: "could not validate argument for command test script bad-exit, ran",
|
||||||
|
Command: (&Command{
|
||||||
|
Path: []string{"test", "script", "bad-exit"},
|
||||||
|
Arguments: []*Argument{
|
||||||
|
{
|
||||||
|
Name: "first",
|
||||||
|
Default: "default",
|
||||||
|
Values: &ValueSource{
|
||||||
|
Script: "echo good; echo default; exit 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).SetBindings(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("good command is good", func(t *testing.T) {
|
||||||
|
cmd := testCommand()
|
||||||
|
cmd.Arguments[0] = staticArgument("first", "default", []string{"default", "good"}, false)
|
||||||
|
cmd.Arguments[1] = staticArgument("second", "", []string{"one", "two", "three"}, true)
|
||||||
|
cmd.SetBindings()
|
||||||
|
|
||||||
|
cmd.Arguments.Parse([]string{"first", "one", "three", "two"})
|
||||||
|
|
||||||
|
err := cmd.Arguments.AreValid()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Unexpected failure validating: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.Command.FullName(), func(t *testing.T) {
|
||||||
|
c.Command.Arguments.Parse(c.Args)
|
||||||
|
|
||||||
|
err := c.Command.Arguments.AreValid()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected failure but got none")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(err.Error(), c.ErrorSuffix) {
|
||||||
|
t.Fatalf("Could not find error <%s> got <%s>", c.ErrorSuffix, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func TestArgumentsToEnv(t *testing.T) {
|
||||||
|
// cases := []struct {
|
||||||
|
// Command *Command
|
||||||
|
// Args []string
|
||||||
|
// Expect []string
|
||||||
|
// Env []string
|
||||||
|
// }{
|
||||||
|
// {
|
||||||
|
// Args: []string{"something"},
|
||||||
|
// Expect: []string{"export MILPA_ARG_FIRST=something"},
|
||||||
|
// Command: &Command{
|
||||||
|
// // Name: []string{"test", "required", "present"},
|
||||||
|
// Arguments: []*Argument{
|
||||||
|
// {
|
||||||
|
// Name: "first",
|
||||||
|
// Required: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// Args: []string{},
|
||||||
|
// Expect: []string{"export MILPA_ARG_FIRST=default"},
|
||||||
|
// Command: &Command{
|
||||||
|
// // Name: []string{"test", "default", "present"},
|
||||||
|
// Arguments: []*Argument{
|
||||||
|
// {
|
||||||
|
// Name: "first",
|
||||||
|
// Default: "default",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// Args: []string{"zero", "one", "two", "three"},
|
||||||
|
// Expect: []string{
|
||||||
|
// "export MILPA_ARG_FIRST=zero",
|
||||||
|
// "declare -a MILPA_ARG_VARIADIC='( one two three )'",
|
||||||
|
// },
|
||||||
|
// Command: &Command{
|
||||||
|
// // Name: []string{"test", "variadic"},
|
||||||
|
// Arguments: []*Argument{
|
||||||
|
// {
|
||||||
|
// Name: "first",
|
||||||
|
// Default: "default",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// Name: "variadic",
|
||||||
|
// Variadic: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// Args: []string{},
|
||||||
|
// Expect: []string{"export MILPA_ARG_FIRST=default"},
|
||||||
|
// Command: &Command{
|
||||||
|
// // Name: []string{"test", "static", "default"},
|
||||||
|
// Arguments: []*Argument{
|
||||||
|
// {
|
||||||
|
// Name: "first",
|
||||||
|
// Default: "default",
|
||||||
|
// Values: &ValueSource{
|
||||||
|
// Static: &[]string{
|
||||||
|
// "default",
|
||||||
|
// "good",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// Args: []string{"good"},
|
||||||
|
// Expect: []string{"export MILPA_ARG_FIRST=good"},
|
||||||
|
// Command: &Command{
|
||||||
|
// // Name: []string{"test", "static", "good"},
|
||||||
|
// Arguments: []*Argument{
|
||||||
|
// {
|
||||||
|
// Name: "first",
|
||||||
|
// Default: "default",
|
||||||
|
// Values: &ValueSource{
|
||||||
|
// Static: &[]string{
|
||||||
|
// "default",
|
||||||
|
// "good",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// Args: []string{"good"},
|
||||||
|
// Expect: []string{"export MILPA_ARG_FIRST=good"},
|
||||||
|
// Command: &Command{
|
||||||
|
// // Name: []string{"test", "script", "good"},
|
||||||
|
// Arguments: []*Argument{
|
||||||
|
// {
|
||||||
|
// Name: "first",
|
||||||
|
// Default: "default",
|
||||||
|
// Values: &ValueSource{
|
||||||
|
// Script: "echo good; echo default",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for _, c := range cases {
|
||||||
|
// t.Run(c.Command.FullName(), func(t *testing.T) {
|
||||||
|
// dst := []string{}
|
||||||
|
// c.Command.SetBindings()
|
||||||
|
// c.Command.Arguments.Parse(c.Args)
|
||||||
|
// c.Command.Arguments.ToEnv(c.Command, &dst, "export ")
|
||||||
|
|
||||||
|
// err := c.Command.Arguments.AreValid()
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatalf("Unexpected failure validating: %s", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for _, expected := range c.Expect {
|
||||||
|
// found := false
|
||||||
|
// for _, actual := range dst {
|
||||||
|
// if strings.HasPrefix(actual, expected) {
|
||||||
|
// found = true
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if !found {
|
||||||
|
// t.Fatalf("Expected line %v not found in %v", expected, dst)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
func TestArgumentToDesc(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Arg *Argument
|
||||||
|
Spec string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Arg: &Argument{
|
||||||
|
Name: "regular",
|
||||||
|
},
|
||||||
|
Spec: "[REGULAR]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Arg: &Argument{
|
||||||
|
Name: "required",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
Spec: "REQUIRED",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Arg: &Argument{
|
||||||
|
Name: "variadic-regular",
|
||||||
|
Variadic: true,
|
||||||
|
},
|
||||||
|
Spec: "[VARIADIC_REGULAR...]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Arg: &Argument{
|
||||||
|
Name: "variadic-required",
|
||||||
|
Variadic: true,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
Spec: "VARIADIC_REQUIRED...",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.Arg.Name, func(t *testing.T) {
|
||||||
|
res := c.Arg.ToDesc()
|
||||||
|
if res != c.Spec {
|
||||||
|
t.Fatalf("Expected %s got %s", c.Spec, res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
139
pkg/command/command.go
Normal file
139
pkg/command/command.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.rob.mx/nidito/chinampa/internal/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HelpFunc func(printLinks bool) string
|
||||||
|
type Action func(cmd *Command) error
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
Path []string
|
||||||
|
// Summary is a short description of a command, on supported shells this is part of the autocomplete prompt
|
||||||
|
Summary string `json:"summary" yaml:"summary" validate:"required"`
|
||||||
|
// Description is a long form explanation of how a command works its magic. Markdown is supported
|
||||||
|
Description string `json:"description" yaml:"description" validate:"required"`
|
||||||
|
// A list of arguments for a command
|
||||||
|
Arguments Arguments `json:"arguments" yaml:"arguments" validate:"dive"`
|
||||||
|
// A map of option names to option definitions
|
||||||
|
Options Options `json:"options" yaml:"options" validate:"dive"`
|
||||||
|
HelpFunc HelpFunc `json:"-" yaml:"-"`
|
||||||
|
// The action to take upon running
|
||||||
|
Action Action
|
||||||
|
runtimeFlags *pflag.FlagSet
|
||||||
|
Cobra *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) SetBindings() *Command {
|
||||||
|
ptr := cmd
|
||||||
|
for _, opt := range cmd.Options {
|
||||||
|
opt.Command = ptr
|
||||||
|
if opt.Validates() {
|
||||||
|
opt.Values.command = ptr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arg := range cmd.Arguments {
|
||||||
|
arg.Command = ptr
|
||||||
|
if arg.Validates() {
|
||||||
|
arg.Values.command = ptr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ptr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) Name() string {
|
||||||
|
return cmd.Path[len(cmd.Path)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) FullName() string {
|
||||||
|
return strings.Join(cmd.Path, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) FlagSet() *pflag.FlagSet {
|
||||||
|
if cmd.runtimeFlags == nil {
|
||||||
|
fs := pflag.NewFlagSet(strings.Join(cmd.Path, " "), pflag.ContinueOnError)
|
||||||
|
fs.SortFlags = false
|
||||||
|
fs.Usage = func() {}
|
||||||
|
|
||||||
|
for name, opt := range cmd.Options {
|
||||||
|
switch opt.Type {
|
||||||
|
case ValueTypeBoolean:
|
||||||
|
def := false
|
||||||
|
if opt.Default != nil {
|
||||||
|
def = opt.Default.(bool)
|
||||||
|
}
|
||||||
|
fs.Bool(name, def, opt.Description)
|
||||||
|
case ValueTypeDefault, ValueTypeString:
|
||||||
|
opt.Type = ValueTypeString
|
||||||
|
def := ""
|
||||||
|
if opt.Default != nil {
|
||||||
|
def = fmt.Sprintf("%s", opt.Default)
|
||||||
|
}
|
||||||
|
fs.String(name, def, opt.Description)
|
||||||
|
default:
|
||||||
|
// ignore flag
|
||||||
|
logrus.Warnf("Ignoring unknown option type <%s> for option <%s>", opt.Type, name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.runtimeFlags = fs
|
||||||
|
}
|
||||||
|
return cmd.runtimeFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) ParseInput(cc *cobra.Command, args []string) error {
|
||||||
|
cmd.Arguments.Parse(args)
|
||||||
|
skipValidation, _ := cc.Flags().GetBool("skip-validation")
|
||||||
|
cmd.Options.Parse(cc.Flags())
|
||||||
|
if !skipValidation {
|
||||||
|
logrus.Debug("Validating arguments")
|
||||||
|
if err := cmd.Arguments.AreValid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debug("Validating flags")
|
||||||
|
if err := cmd.Options.AreValid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) Run(cc *cobra.Command, args []string) error {
|
||||||
|
logrus.Debugf("running command %s", cmd.FullName())
|
||||||
|
|
||||||
|
if err := cmd.ParseInput(cc, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Action(cmd)
|
||||||
|
if err != nil {
|
||||||
|
errors.HandleCobraExit(cmd.Cobra, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) SetCobra(cc *cobra.Command) {
|
||||||
|
cmd.Cobra = cc
|
||||||
|
}
|
88
pkg/command/help.go
Normal file
88
pkg/command/help.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
_c "git.rob.mx/nidito/chinampa/internal/constants"
|
||||||
|
"git.rob.mx/nidito/chinampa/internal/render"
|
||||||
|
"git.rob.mx/nidito/chinampa/pkg/runtime"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type combinedCommand struct {
|
||||||
|
Spec *Command
|
||||||
|
Command *cobra.Command
|
||||||
|
GlobalOptions Options
|
||||||
|
HTMLOutput bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) HasAdditionalHelp() bool {
|
||||||
|
return cmd.HelpFunc != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) AdditionalHelp(printLinks bool) *string {
|
||||||
|
if cmd.HelpFunc != nil {
|
||||||
|
str := cmd.HelpFunc(printLinks)
|
||||||
|
return &str
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) HelpRenderer(globalOptions Options) func(cc *cobra.Command, args []string) {
|
||||||
|
return func(cc *cobra.Command, args []string) {
|
||||||
|
// some commands don't have a binding until help is rendered
|
||||||
|
// like virtual ones (sub command groups)
|
||||||
|
cmd.SetCobra(cc)
|
||||||
|
content, err := cmd.ShowHelp(globalOptions, args)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = cc.OutOrStderr().Write(content)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) ShowHelp(globalOptions Options, args []string) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
c := &combinedCommand{
|
||||||
|
Spec: cmd,
|
||||||
|
Command: cmd.Cobra,
|
||||||
|
GlobalOptions: globalOptions,
|
||||||
|
HTMLOutput: runtime.UnstyledHelpEnabled(),
|
||||||
|
}
|
||||||
|
err := _c.TemplateCommandHelp.Execute(&buf, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
colorEnabled := runtime.ColorEnabled()
|
||||||
|
flags := cmd.Cobra.Flags()
|
||||||
|
ncf := cmd.Cobra.Flag("no-color") // nolint:ifshort
|
||||||
|
cf := cmd.Cobra.Flag("color") // nolint:ifshort
|
||||||
|
|
||||||
|
if noColorFlag, err := flags.GetBool("no-color"); err == nil && ncf.Changed {
|
||||||
|
colorEnabled = !noColorFlag
|
||||||
|
} else if colorFlag, err := flags.GetBool("color"); err == nil && cf.Changed {
|
||||||
|
colorEnabled = colorFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := render.Markdown(buf.Bytes(), colorEnabled)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return content, nil
|
||||||
|
}
|
234
pkg/command/options.go
Normal file
234
pkg/command/options.go
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.rob.mx/nidito/chinampa/internal/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options is a map of name to Option.
|
||||||
|
type Options map[string]*Option
|
||||||
|
|
||||||
|
func (opts *Options) AllKnown() map[string]any {
|
||||||
|
col := map[string]any{}
|
||||||
|
for name, opt := range *opts {
|
||||||
|
col[name] = opt.ToValue()
|
||||||
|
}
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts *Options) AllKnownStr() map[string]string {
|
||||||
|
col := map[string]string{}
|
||||||
|
for name, opt := range *opts {
|
||||||
|
col[name] = opt.ToString()
|
||||||
|
}
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
// func envValue(opts Options, f *pflag.Flag) (*string, *string) {
|
||||||
|
// name := f.Name
|
||||||
|
// if name == _c.HelpCommandName {
|
||||||
|
// return nil, nil
|
||||||
|
// }
|
||||||
|
// envName := ""
|
||||||
|
// value := f.Value.String()
|
||||||
|
|
||||||
|
// if cname, ok := _c.EnvFlagNames[name]; ok {
|
||||||
|
// if value == "false" {
|
||||||
|
// return nil, nil
|
||||||
|
// }
|
||||||
|
// envName = cname
|
||||||
|
// } else {
|
||||||
|
// envName = fmt.Sprintf("%s%s", _c.OutputPrefixOpt, strings.ToUpper(strings.ReplaceAll(name, "-", "_")))
|
||||||
|
// opt := opts[name]
|
||||||
|
// if opt != nil {
|
||||||
|
// value = opt.ToString(true)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if value == "false" && opt.Type == ValueTypeBoolean {
|
||||||
|
// // makes dealing with false flags in shell easier
|
||||||
|
// value = ""
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return &envName, &value
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // ToEnv writes shell variables to dst.
|
||||||
|
// func (opts *Options) ToEnv(command *Command, dst *[]string, prefix string) {
|
||||||
|
// command.cc.Flags().VisitAll(func(f *pflag.Flag) {
|
||||||
|
// envName, value := envValue(*opts, f)
|
||||||
|
// if envName != nil && value != nil {
|
||||||
|
// *dst = append(*dst, fmt.Sprintf("%s%s=%s", prefix, *envName, *value))
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (opts *Options) EnvMap(command *Command, dst *map[string]string) {
|
||||||
|
// command.cc.Flags().VisitAll(func(f *pflag.Flag) {
|
||||||
|
// envName, value := envValue(*opts, f)
|
||||||
|
// if envName != nil && value != nil {
|
||||||
|
// (*dst)[*envName] = *value
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (opts *Options) Parse(supplied *pflag.FlagSet) {
|
||||||
|
// logrus.Debugf("Parsing supplied flags, %v", supplied)
|
||||||
|
for name, opt := range *opts {
|
||||||
|
switch opt.Type {
|
||||||
|
case ValueTypeBoolean:
|
||||||
|
if val, err := supplied.GetBool(name); err == nil {
|
||||||
|
opt.provided = val
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
opt.Type = ValueTypeString
|
||||||
|
if val, err := supplied.GetString(name); err == nil {
|
||||||
|
opt.provided = val
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts *Options) AreValid() error {
|
||||||
|
for name, opt := range *opts {
|
||||||
|
if err := opt.Validate(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option represents a command line flag.
|
||||||
|
type Option struct {
|
||||||
|
ShortName string `json:"short-name,omitempty" yaml:"short-name,omitempty"` // nolint:tagliatelle
|
||||||
|
Type ValueType `json:"type" yaml:"type" validate:"omitempty,oneof=string bool"`
|
||||||
|
Description string `json:"description" yaml:"description" validate:"required"`
|
||||||
|
Default any `json:"default,omitempty" yaml:"default,omitempty"`
|
||||||
|
Values *ValueSource `json:"values,omitempty" yaml:"values,omitempty" validate:"omitempty"`
|
||||||
|
Repeated bool `json:"repeated" yaml:"repeated" validate:"omitempty"`
|
||||||
|
Command *Command `json:"-" yaml:"-" validate:"-"`
|
||||||
|
provided any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *Option) IsKnown() bool {
|
||||||
|
return opt.provided != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *Option) ToValue() any {
|
||||||
|
if opt.IsKnown() {
|
||||||
|
return opt.provided
|
||||||
|
}
|
||||||
|
return opt.Default
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *Option) ToString() string {
|
||||||
|
value := opt.ToValue()
|
||||||
|
stringValue := ""
|
||||||
|
if opt.Type == "bool" {
|
||||||
|
if value == nil {
|
||||||
|
stringValue = ""
|
||||||
|
} else {
|
||||||
|
stringValue = strconv.FormatBool(value.(bool))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if value != nil {
|
||||||
|
stringValue = value.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *Option) Validate(name string) error {
|
||||||
|
if !opt.Validates() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
current := opt.ToString() // nolint:ifshort
|
||||||
|
|
||||||
|
if current == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
validValues, _, err := opt.Resolve(current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains(validValues, current) {
|
||||||
|
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for option <%s>. Valid options are: %s", current, name, strings.Join(validValues, ", "))}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates tells if the user-supplied value needs validation.
|
||||||
|
func (opt *Option) Validates() bool {
|
||||||
|
return opt.Values != nil && opt.Values.Validates()
|
||||||
|
}
|
||||||
|
|
||||||
|
// providesAutocomplete tells if this option provides autocomplete values.
|
||||||
|
func (opt *Option) providesAutocomplete() bool {
|
||||||
|
return opt.Values != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve returns autocomplete values for an option.
|
||||||
|
func (opt *Option) Resolve(currentValue string) (values []string, flag cobra.ShellCompDirective, err error) {
|
||||||
|
if opt.Values != nil {
|
||||||
|
if opt.Values.command == nil {
|
||||||
|
opt.Values.command = opt.Command
|
||||||
|
}
|
||||||
|
return opt.Values.Resolve(currentValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompletionFunction is called by cobra when asked to complete an option.
|
||||||
|
func (opt *Option) CompletionFunction(cmd *cobra.Command, args []string, toComplete string) (values []string, flag cobra.ShellCompDirective) {
|
||||||
|
if !opt.providesAutocomplete() {
|
||||||
|
flag = cobra.ShellCompDirectiveNoFileComp
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opt.Command.Arguments.Parse(args)
|
||||||
|
opt.Command.Options.Parse(cmd.Flags())
|
||||||
|
|
||||||
|
var err error
|
||||||
|
values, flag, err = opt.Resolve(toComplete)
|
||||||
|
if err != nil {
|
||||||
|
return values, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
|
||||||
|
if toComplete != "" {
|
||||||
|
filtered := []string{}
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.HasPrefix(value, toComplete) {
|
||||||
|
filtered = append(filtered, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
return cobra.AppendActiveHelp(values, opt.Description), flag
|
||||||
|
}
|
60
pkg/command/root.go
Normal file
60
pkg/command/root.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
_c "git.rob.mx/nidito/chinampa/internal/constants"
|
||||||
|
"git.rob.mx/nidito/chinampa/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Root = &Command{
|
||||||
|
Summary: "Helps organize config for roberto",
|
||||||
|
Description: `﹅joao﹅ makes yaml, json, 1password and vault play along nicely.`,
|
||||||
|
Path: []string{"joao"},
|
||||||
|
Options: Options{
|
||||||
|
_c.HelpCommandName: &Option{
|
||||||
|
ShortName: "h",
|
||||||
|
Type: "bool",
|
||||||
|
Description: "Display help for any command",
|
||||||
|
},
|
||||||
|
"verbose": &Option{
|
||||||
|
ShortName: "v",
|
||||||
|
Type: "bool",
|
||||||
|
Default: runtime.VerboseEnabled(),
|
||||||
|
Description: "Log verbose output to stderr",
|
||||||
|
},
|
||||||
|
"version": &Option{
|
||||||
|
Type: "bool",
|
||||||
|
Default: false,
|
||||||
|
Description: "Display program version and exit",
|
||||||
|
},
|
||||||
|
"no-color": &Option{
|
||||||
|
Type: "bool",
|
||||||
|
Description: "Disable printing of colors to stderr",
|
||||||
|
Default: !runtime.ColorEnabled(),
|
||||||
|
},
|
||||||
|
"color": &Option{
|
||||||
|
Type: "bool",
|
||||||
|
Description: "Always print colors to stderr",
|
||||||
|
Default: runtime.ColorEnabled(),
|
||||||
|
},
|
||||||
|
"silent": &Option{
|
||||||
|
Type: "bool",
|
||||||
|
Description: "Silence non-error logging",
|
||||||
|
},
|
||||||
|
"skip-validation": &Option{
|
||||||
|
Type: "bool",
|
||||||
|
Description: "Do not validate any arguments or options",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
54
pkg/command/validation.go
Normal file
54
pkg/command/validation.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
type varSearchMap struct {
|
||||||
|
Status int
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) Validate() (report map[string]int) {
|
||||||
|
report = map[string]int{}
|
||||||
|
|
||||||
|
validate := validator.New()
|
||||||
|
if err := validate.Struct(cmd); err != nil {
|
||||||
|
verrs := err.(validator.ValidationErrors)
|
||||||
|
for _, issue := range verrs {
|
||||||
|
// todo: output better errors, see validator.FieldError
|
||||||
|
report[fmt.Sprint(issue)] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := map[string]map[string]*varSearchMap{
|
||||||
|
"argument": {},
|
||||||
|
"option": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arg := range cmd.Arguments {
|
||||||
|
vars["argument"][strings.ToUpper(strings.ReplaceAll(arg.Name, "-", "_"))] = &varSearchMap{2, arg.Name, ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range cmd.Options {
|
||||||
|
vars["option"][strings.ToUpper(strings.ReplaceAll(name, "-", "_"))] = &varSearchMap{2, name, ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
return report
|
||||||
|
}
|
234
pkg/command/value.go
Normal file
234
pkg/command/value.go
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_c "git.rob.mx/nidito/chinampa/internal/constants"
|
||||||
|
"git.rob.mx/nidito/chinampa/internal/exec"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValueType represent the kinds of or option.
|
||||||
|
type ValueType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ValueTypeDefault is the empty string, maps to ValueTypeString.
|
||||||
|
ValueTypeDefault ValueType = ""
|
||||||
|
// ValueTypeString a value treated like a string.
|
||||||
|
ValueTypeString ValueType = "string"
|
||||||
|
// ValueTypeBoolean is a value treated like a boolean.
|
||||||
|
ValueTypeBoolean ValueType = "bool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SourceCommand struct {
|
||||||
|
Path []string
|
||||||
|
Args string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompletionFunc func(cmd *Command, currentValue string) (values []string, flag cobra.ShellCompDirective, err error)
|
||||||
|
|
||||||
|
// ValueSource represents the source for an auto-completed and/or validated option/argument.
|
||||||
|
type ValueSource struct {
|
||||||
|
// Directories prompts for directories with the given prefix.
|
||||||
|
Directories *string `json:"dirs,omitempty" yaml:"dirs,omitempty" validate:"omitempty,excluded_with=Command Files Func Script Static"`
|
||||||
|
// Files prompts for files with the given extensions
|
||||||
|
Files *[]string `json:"files,omitempty" yaml:"files,omitempty" validate:"omitempty,excluded_with=Command Func Directories Script Static"`
|
||||||
|
// Script runs the provided command with `bash -c "$script"` and returns an option for every line of stdout.
|
||||||
|
Script string `json:"script,omitempty" yaml:"script,omitempty" validate:"omitempty,excluded_with=Command Directories Files Func Static"`
|
||||||
|
// Static returns the given list.
|
||||||
|
Static *[]string `json:"static,omitempty" yaml:"static,omitempty" validate:"omitempty,excluded_with=Command Directories Files Func Script"`
|
||||||
|
// Command runs a subcommand and returns an option for every line of stdout.
|
||||||
|
Command *SourceCommand `json:"command,omitempty" yaml:"command,omitempty" validate:"omitempty,excluded_with=Directories Files Func Script Static"`
|
||||||
|
// Func runs a function
|
||||||
|
Func CompletionFunc `json:"func,omitempty" yaml:"func,omitempty" validate:"omitempty,excluded_with=Command Directories Files Script Static"`
|
||||||
|
// Timeout is the maximum amount of time we will wait for a Script, Command, or Func before giving up on completions/validations.
|
||||||
|
Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty" validate:"omitempty,excluded_with=Directories Files Static"`
|
||||||
|
// Suggestion if provided will only suggest autocomplete values but will not perform validation of a given value
|
||||||
|
Suggestion bool `json:"suggest-only" yaml:"suggest-only" validate:"omitempty"` // nolint:tagliatelle
|
||||||
|
// SuggestRaw if provided the shell will not add a space after autocompleting
|
||||||
|
SuggestRaw bool `json:"suggest-raw" yaml:"suggest-raw" validate:"omitempty"` // nolint:tagliatelle
|
||||||
|
command *Command `json:"-" yaml:"-" validate:"-"`
|
||||||
|
computed *[]string
|
||||||
|
flag cobra.ShellCompDirective
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates tells if a value needs to be validated.
|
||||||
|
func (vs *ValueSource) Validates() bool {
|
||||||
|
if vs.Directories != nil || vs.Files != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !vs.Suggestion
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve returns the values for autocomplete and validation.
|
||||||
|
func (vs *ValueSource) Resolve(currentValue string) (values []string, flag cobra.ShellCompDirective, err error) {
|
||||||
|
if vs.computed != nil {
|
||||||
|
return *vs.computed, vs.flag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if vs.Timeout == 0 {
|
||||||
|
vs.Timeout = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
flag = cobra.ShellCompDirectiveDefault
|
||||||
|
timeout := time.Duration(vs.Timeout)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case vs.Static != nil:
|
||||||
|
values = *vs.Static
|
||||||
|
case vs.Files != nil:
|
||||||
|
flag = cobra.ShellCompDirectiveFilterFileExt
|
||||||
|
values = *vs.Files
|
||||||
|
case vs.Directories != nil:
|
||||||
|
flag = cobra.ShellCompDirectiveFilterDirs
|
||||||
|
values = []string{*vs.Directories}
|
||||||
|
case vs.Func != nil:
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
panicChan := make(chan any, 1)
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
panicChan <- p
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
values, flag, err = vs.Func(vs.command, currentValue)
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case err = <-done:
|
||||||
|
return
|
||||||
|
case p := <-panicChan:
|
||||||
|
panic(p)
|
||||||
|
case <-ctx.Done():
|
||||||
|
flag = cobra.ShellCompDirectiveError
|
||||||
|
err = ctx.Err()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case vs.Command != nil:
|
||||||
|
if vs.command == nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError, fmt.Errorf("bug: command is nil")
|
||||||
|
}
|
||||||
|
argString, err := vs.command.ResolveTemplate(vs.Command.Args, currentValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError, err
|
||||||
|
}
|
||||||
|
args := strings.Split(argString, " ")
|
||||||
|
sub, _, err := vs.command.Cobra.Root().Find(vs.Command.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError, fmt.Errorf("could not find a command named %s", vs.Command.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Second)
|
||||||
|
defer cancel() // The cancel should be deferred so resources are cleaned up
|
||||||
|
|
||||||
|
sub.SetArgs(args)
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
sub.SetOut(&stdout)
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
sub.SetErr(&stderr)
|
||||||
|
err = sub.ExecuteContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
values = strings.Split(stdout.String(), "\n")
|
||||||
|
flag = cobra.ShellCompDirectiveDefault
|
||||||
|
case vs.Script != "":
|
||||||
|
if vs.command == nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError, fmt.Errorf("bug: command is nil")
|
||||||
|
}
|
||||||
|
cmd, err := vs.command.ResolveTemplate(vs.Script, currentValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
args := append([]string{"/bin/bash", "-c"}, cmd)
|
||||||
|
|
||||||
|
values, flag, err = exec.Exec(vs.command.FullName(), args, os.Environ(), timeout*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return nil, flag, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vs.computed = &values
|
||||||
|
|
||||||
|
if vs.SuggestRaw {
|
||||||
|
flag |= cobra.ShellCompDirectiveNoSpace
|
||||||
|
}
|
||||||
|
|
||||||
|
vs.flag = flag
|
||||||
|
return values, flag, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type AutocompleteTemplate struct {
|
||||||
|
Args map[string]string
|
||||||
|
Opts map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tpl *AutocompleteTemplate) Opt(name string) string {
|
||||||
|
if val, ok := tpl.Opts[name]; ok {
|
||||||
|
return fmt.Sprintf("--%s %s", name, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tpl *AutocompleteTemplate) Arg(name string) string {
|
||||||
|
return tpl.Args[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) ResolveTemplate(templateString string, currentValue string) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
tplData := &AutocompleteTemplate{
|
||||||
|
Args: cmd.Arguments.AllKnownStr(),
|
||||||
|
Opts: cmd.Options.AllKnownStr(),
|
||||||
|
}
|
||||||
|
|
||||||
|
fnMap := template.FuncMap{
|
||||||
|
"Opt": tplData.Opt,
|
||||||
|
"Arg": tplData.Arg,
|
||||||
|
"Current": func() string { return currentValue },
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range _c.TemplateFuncs {
|
||||||
|
fnMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl, err := template.New("subcommand").Funcs(fnMap).Parse(templateString)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tpl.Execute(&buf, tplData)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
164
pkg/command/value_test.go
Normal file
164
pkg/command/value_test.go
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package command_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "git.rob.mx/nidito/chinampa/pkg/command"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveTemplate(t *testing.T) {
|
||||||
|
overrideFlags := &pflag.FlagSet{}
|
||||||
|
overrideFlags.String("option", "override", "stuff")
|
||||||
|
overrideFlags.Bool("bool", false, "stuff")
|
||||||
|
overrideFlags.Bool("help", false, "stuff")
|
||||||
|
overrideFlags.Bool("no-color", false, "stuff")
|
||||||
|
overrideFlags.Bool("skip-validation", false, "stuff")
|
||||||
|
err := overrideFlags.Parse([]string{"--option", "override", "--bool", "--help", "--no-color", "--skip-validation"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not parse test flags")
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
Tpl string
|
||||||
|
Expected string
|
||||||
|
Args []string
|
||||||
|
Flags *pflag.FlagSet
|
||||||
|
Errors bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Tpl: "adds nothing to nothing",
|
||||||
|
Expected: "adds nothing to nothing",
|
||||||
|
Errors: false,
|
||||||
|
Args: []string{},
|
||||||
|
Flags: &pflag.FlagSet{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tpl: `prints default option as {{ Opt "option" }}`,
|
||||||
|
Expected: "prints default option as --option default",
|
||||||
|
Errors: false,
|
||||||
|
Args: []string{},
|
||||||
|
Flags: &pflag.FlagSet{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tpl: `prints default option value as {{ .Opts.option }}`,
|
||||||
|
Expected: "prints default option value as default",
|
||||||
|
Errors: false,
|
||||||
|
Args: []string{},
|
||||||
|
Flags: &pflag.FlagSet{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tpl: `prints default argument as {{ Arg "argument_0" }}`,
|
||||||
|
Expected: "prints default argument as default",
|
||||||
|
Errors: false,
|
||||||
|
Args: []string{},
|
||||||
|
Flags: &pflag.FlagSet{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tpl: `prints default argument value as {{ .Args.argument_0 }}`,
|
||||||
|
Expected: "prints default argument value as default",
|
||||||
|
Errors: false,
|
||||||
|
Args: []string{},
|
||||||
|
Flags: &pflag.FlagSet{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tpl: `overrides default option as {{ Opt "option" }}`,
|
||||||
|
Expected: "overrides default option as --option override",
|
||||||
|
Errors: false,
|
||||||
|
Args: []string{},
|
||||||
|
Flags: overrideFlags,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tpl: `overrides default argument as {{ Arg "argument_0" }}`,
|
||||||
|
Expected: "overrides default argument as override",
|
||||||
|
Errors: false,
|
||||||
|
Args: []string{"override"},
|
||||||
|
Flags: &pflag.FlagSet{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tpl: `combines defaults as {{ Opt "option" }} {{ Opt "bool"}} {{ Arg "argument_0" }}`,
|
||||||
|
Expected: "combines defaults as --option default --bool false default",
|
||||||
|
Errors: false,
|
||||||
|
Args: []string{},
|
||||||
|
Flags: &pflag.FlagSet{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tpl: `combines overrides as {{ Opt "option" }} {{ Opt "bool" }} {{ Arg "argument_0" }}`,
|
||||||
|
Expected: "combines overrides as --option override --bool true twice",
|
||||||
|
Errors: false,
|
||||||
|
Args: []string{"twice"},
|
||||||
|
Flags: overrideFlags,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tpl: `prints variadic as {{ Arg "argument_0" }} {{ Arg "argument_n" }}`,
|
||||||
|
Expected: "prints variadic as override a b",
|
||||||
|
Errors: false,
|
||||||
|
Args: []string{"override", "a", "b"},
|
||||||
|
Flags: &pflag.FlagSet{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tpl: `doesn't error on bad names {{ Opt "bad-option" }} {{ Arg "bad-argument" }}`,
|
||||||
|
Expected: "doesn't error on bad names ",
|
||||||
|
Errors: false,
|
||||||
|
Args: []string{},
|
||||||
|
Flags: &pflag.FlagSet{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Tpl: `errors on bad templates {{ BadFunc }}`,
|
||||||
|
Args: []string{},
|
||||||
|
Flags: &pflag.FlagSet{},
|
||||||
|
Errors: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range cases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.Expected, func(t *testing.T) {
|
||||||
|
cmd := (&Command{
|
||||||
|
Arguments: []*Argument{
|
||||||
|
{
|
||||||
|
Name: "argument_0",
|
||||||
|
Default: "default",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "argument_n",
|
||||||
|
Variadic: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Options: Options{
|
||||||
|
"option": {
|
||||||
|
Default: "default",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
"bool": {
|
||||||
|
Type: "bool",
|
||||||
|
Default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).SetBindings()
|
||||||
|
cmd.Arguments.Parse(test.Args)
|
||||||
|
cmd.Options.Parse(test.Flags)
|
||||||
|
res, err := cmd.ResolveTemplate(test.Tpl, "")
|
||||||
|
|
||||||
|
if err != nil && !test.Errors {
|
||||||
|
t.Fatalf("good template failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res != test.Expected {
|
||||||
|
t.Fatalf("expected '%s' got '%s'", test.Expected, res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
105
pkg/runtime/runtime.go
Normal file
105
pkg/runtime/runtime.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
_c "git.rob.mx/nidito/chinampa/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
var falseIshValues = []string{
|
||||||
|
"",
|
||||||
|
"0",
|
||||||
|
"no",
|
||||||
|
"false",
|
||||||
|
"disable",
|
||||||
|
"disabled",
|
||||||
|
"off",
|
||||||
|
"never",
|
||||||
|
}
|
||||||
|
|
||||||
|
var trueIshValues = []string{
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
"true",
|
||||||
|
"enable",
|
||||||
|
"enabled",
|
||||||
|
"on",
|
||||||
|
"always",
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFalseIsh(val string) bool {
|
||||||
|
for _, negative := range falseIshValues {
|
||||||
|
if val == negative {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTrueIsh(val string) bool {
|
||||||
|
for _, positive := range trueIshValues {
|
||||||
|
if val == positive {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func DebugEnabled() bool {
|
||||||
|
return isTrueIsh(os.Getenv(_c.EnvVarDebug))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidationEnabled() bool {
|
||||||
|
return isFalseIsh(os.Getenv(_c.EnvVarValidationDisabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerboseEnabled() bool {
|
||||||
|
return isTrueIsh(os.Getenv(_c.EnvVarMilpaVerbose))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ColorEnabled() bool {
|
||||||
|
return isFalseIsh(os.Getenv(_c.EnvVarMilpaUnstyled)) && !UnstyledHelpEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnstyledHelpEnabled() bool {
|
||||||
|
return isTrueIsh(os.Getenv(_c.EnvVarHelpUnstyled))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvironmentMap returns the resolved environment map.
|
||||||
|
func EnvironmentMap() map[string]string {
|
||||||
|
env := map[string]string{}
|
||||||
|
trueString := strconv.FormatBool(true)
|
||||||
|
|
||||||
|
if !ColorEnabled() {
|
||||||
|
env[_c.EnvVarMilpaUnstyled] = trueString
|
||||||
|
} else if isTrueIsh(os.Getenv(_c.EnvVarMilpaForceColor)) {
|
||||||
|
env[_c.EnvVarMilpaForceColor] = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
if DebugEnabled() {
|
||||||
|
env[_c.EnvVarDebug] = trueString
|
||||||
|
}
|
||||||
|
|
||||||
|
if VerboseEnabled() {
|
||||||
|
env[_c.EnvVarMilpaVerbose] = trueString
|
||||||
|
} else if isTrueIsh(os.Getenv(_c.EnvVarMilpaSilent)) {
|
||||||
|
env[_c.EnvVarMilpaSilent] = trueString
|
||||||
|
}
|
||||||
|
|
||||||
|
return env
|
||||||
|
}
|
136
pkg/runtime/runtime_test.go
Normal file
136
pkg/runtime/runtime_test.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package runtime_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_c "git.rob.mx/nidito/chinampa/internal/constants"
|
||||||
|
. "git.rob.mx/nidito/chinampa/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnabled(t *testing.T) {
|
||||||
|
defer func() { os.Setenv(_c.EnvVarMilpaVerbose, "") }()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
Name string
|
||||||
|
Func func() bool
|
||||||
|
Expects bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: _c.EnvVarMilpaVerbose,
|
||||||
|
Func: VerboseEnabled,
|
||||||
|
Expects: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: _c.EnvVarValidationDisabled,
|
||||||
|
Func: ValidationEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: _c.EnvVarMilpaUnstyled,
|
||||||
|
Func: ColorEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: _c.EnvVarHelpUnstyled,
|
||||||
|
Func: ColorEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: _c.EnvVarDebug,
|
||||||
|
Func: DebugEnabled,
|
||||||
|
Expects: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: _c.EnvVarHelpUnstyled,
|
||||||
|
Func: UnstyledHelpEnabled,
|
||||||
|
Expects: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
fname := runtime.FuncForPC(reflect.ValueOf(c.Func).Pointer()).Name()
|
||||||
|
name := fmt.Sprintf("%v/%s", fname, c.Name)
|
||||||
|
enabled := []string{
|
||||||
|
"yes", "true", "1", "enabled",
|
||||||
|
}
|
||||||
|
for _, val := range enabled {
|
||||||
|
t.Run("enabled-"+val, func(t *testing.T) {
|
||||||
|
os.Setenv(c.Name, val)
|
||||||
|
if c.Func() != c.Expects {
|
||||||
|
t.Fatalf("%s wasn't enabled with a valid value: %s", name, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disabled := []string{"", "no", "false", "0", "disabled"}
|
||||||
|
for _, val := range disabled {
|
||||||
|
t.Run("disabled-"+val, func(t *testing.T) {
|
||||||
|
os.Setenv(c.Name, val)
|
||||||
|
if c.Func() == c.Expects {
|
||||||
|
t.Fatalf("%s was enabled with falsy value: %s", name, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvironmentMapEnabled(t *testing.T) {
|
||||||
|
trueString := strconv.FormatBool(true)
|
||||||
|
os.Setenv(_c.EnvVarMilpaForceColor, trueString)
|
||||||
|
os.Setenv(_c.EnvVarDebug, trueString)
|
||||||
|
os.Setenv(_c.EnvVarMilpaVerbose, trueString)
|
||||||
|
|
||||||
|
res := EnvironmentMap()
|
||||||
|
if res == nil {
|
||||||
|
t.Fatalf("Expected map, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := map[string]string{
|
||||||
|
_c.EnvVarMilpaForceColor: "always",
|
||||||
|
_c.EnvVarDebug: trueString,
|
||||||
|
_c.EnvVarMilpaVerbose: trueString,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(res, expected) {
|
||||||
|
t.Fatalf("Unexpected result from enabled environment. Wanted %v, got %v", res, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvironmentMapDisabled(t *testing.T) {
|
||||||
|
trueString := strconv.FormatBool(true)
|
||||||
|
// clear COLOR
|
||||||
|
os.Unsetenv(_c.EnvVarMilpaForceColor)
|
||||||
|
// set NO_COLOR
|
||||||
|
os.Setenv(_c.EnvVarMilpaUnstyled, trueString)
|
||||||
|
os.Unsetenv(_c.EnvVarDebug)
|
||||||
|
os.Unsetenv(_c.EnvVarMilpaVerbose)
|
||||||
|
os.Setenv(_c.EnvVarMilpaSilent, trueString)
|
||||||
|
|
||||||
|
res := EnvironmentMap()
|
||||||
|
if res == nil {
|
||||||
|
t.Fatalf("Expected map, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := map[string]string{
|
||||||
|
_c.EnvVarMilpaUnstyled: trueString,
|
||||||
|
_c.EnvVarMilpaSilent: trueString,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(res, expected) {
|
||||||
|
t.Fatalf("Unexpected result from disabled environment. Wanted %v, got %v", res, expected)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user