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