mirror of
https://github.com/lddsb/drone-dingtalk-message.git
synced 2026-06-16 14:50:42 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9e3dbbf4b | |||
| c82a6a8c79 | |||
| aec957818e | |||
| 6c497aa777 | |||
| 9e4a2ae0dc | |||
| 00b6d66762 | |||
| 26933681c4 | |||
| b6dd3953d2 |
+29
-70
@@ -1,75 +1,34 @@
|
|||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
name: testing
|
name: default
|
||||||
|
|
||||||
|
workspace:
|
||||||
|
base: /go
|
||||||
|
path: src/github.com/lddsb/drone-dingtalk-message
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: vet
|
- name: build
|
||||||
pull: always
|
image: golang
|
||||||
image: golang
|
commands:
|
||||||
commands:
|
- go get -u github.com/golang/dep/cmd/dep
|
||||||
- make vet
|
- dep ensure
|
||||||
environment:
|
- CGO_ENABLED=0 GOOS=linux go build -a -o drone-dingtalk-message .
|
||||||
GO111MODULE: "on"
|
- ./drone-dingtalk-message -h
|
||||||
volumes:
|
- name: publish
|
||||||
- name: gopath
|
image: plugins/docker
|
||||||
path: /go
|
when:
|
||||||
|
branch:
|
||||||
|
- beta
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
settings:
|
||||||
|
repo: lddsb/drone-dingtalk-message
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags: beta
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
|
||||||
- name: test
|
|
||||||
pull: always
|
|
||||||
image: golang
|
|
||||||
commands:
|
|
||||||
- make test
|
|
||||||
- make coverage
|
|
||||||
environment:
|
|
||||||
GO111MODULE: "on"
|
|
||||||
volumes:
|
|
||||||
- name: gopath
|
|
||||||
path: /go
|
|
||||||
|
|
||||||
- name: codecov
|
|
||||||
pull: always
|
|
||||||
image: plugins/codecov
|
|
||||||
settings:
|
|
||||||
token:
|
|
||||||
from_secret: codecov_token
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- name: gopath
|
|
||||||
temp: {}
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
event:
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
---
|
|
||||||
kind: pipeline
|
|
||||||
name: dryrun
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: build
|
|
||||||
pull: always
|
|
||||||
image: golang
|
|
||||||
commands:
|
|
||||||
- go build -a -o drone-dingtalk-message .
|
|
||||||
environment:
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GO111MODULE: "on"
|
|
||||||
|
|
||||||
- name: dryrun
|
|
||||||
pull: always
|
|
||||||
image: plugins/docker
|
|
||||||
settings:
|
|
||||||
cache_from: lddsb/drone-dingtalk-message
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
dry_run: true
|
|
||||||
repo: lddsb/drone-dingtalk-message
|
|
||||||
tags:
|
|
||||||
- latest
|
|
||||||
- 1.0.0
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
event:
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
depends_on:
|
|
||||||
- testing
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
[*.y*ml]
|
|
||||||
indent_size = 2
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: gomod
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
time: "10:00"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
name: Publish to DockerHub and Github Package
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
dockerhub:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Repo metadata
|
|
||||||
id: repo
|
|
||||||
uses: actions/github-script@v3
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const repo = await github.repos.get(context.repo)
|
|
||||||
return repo.data
|
|
||||||
- name: Prepare
|
|
||||||
id: prep
|
|
||||||
run: |
|
|
||||||
DOCKER_IMAGE=lddsb/drone-dingtalk-message
|
|
||||||
GITHUB_IMAGE=ghcr.io/lddsb/drone-dingtalk-message
|
|
||||||
VERSION=${GITHUB_REF#refs/tags/v}
|
|
||||||
TAGS="${DOCKER_IMAGE}:${VERSION}"
|
|
||||||
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
|
||||||
MINOR=${VERSION%.*}
|
|
||||||
MAJOR=${MINOR%.*}
|
|
||||||
TAGS="$TAGS,${DOCKER_IMAGE}:${MINOR},${DOCKER_IMAGE}:${MAJOR},${DOCKER_IMAGE}:latest"
|
|
||||||
TAGS="$TAGS,${GITHUB_IMAGE}:${VERSION},${GITHUB_IMAGE}:${MINOR},${GITHUB_IMAGE}:${MAJOR},${GITHUB_IMAGE}:latest"
|
|
||||||
fi
|
|
||||||
echo ::set-output name=tags::${TAGS}
|
|
||||||
- name: Setup Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- name: Login to GitHub Container Register
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.CR_PAT }}
|
|
||||||
- name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.prep.outputs.tags }}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
name: Publish release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: publish release, upload asset
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: stable
|
|
||||||
- name: Run GoReleaser
|
|
||||||
uses: goreleaser/goreleaser-action@v5
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
args: release --clean
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.CR_PAT }}
|
|
||||||
+1
-4
@@ -1,7 +1,4 @@
|
|||||||
dive.log
|
dive.log
|
||||||
drone-dingtalk-message
|
drone-dingtalk-message
|
||||||
.idea
|
.idea
|
||||||
vendor
|
vendor
|
||||||
coverage.txt
|
|
||||||
coverage.out
|
|
||||||
env.list
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
env:
|
|
||||||
- GO111MODULE=on
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
- go mod download
|
|
||||||
builds:
|
|
||||||
- env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
- darwin
|
|
||||||
- windows
|
|
||||||
goarch:
|
|
||||||
- 386
|
|
||||||
- amd64
|
|
||||||
- arm
|
|
||||||
- arm64
|
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
binary: dingtalk-message
|
|
||||||
flags:
|
|
||||||
- -trimpath
|
|
||||||
ldflags:
|
|
||||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser
|
|
||||||
checksum:
|
|
||||||
name_template: '{{ .ProjectName }}_checksums.txt'
|
|
||||||
changelog:
|
|
||||||
sort: asc
|
|
||||||
filters:
|
|
||||||
exclude:
|
|
||||||
- '^docs:'
|
|
||||||
- '^test:'
|
|
||||||
- Merge pull request
|
|
||||||
- Merge branch
|
|
||||||
- go mod tidy
|
|
||||||
archives:
|
|
||||||
- name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
|
|
||||||
replacements:
|
|
||||||
darwin: Darwin
|
|
||||||
linux: Linux
|
|
||||||
windows: Windows
|
|
||||||
386: i386
|
|
||||||
amd64: x86_64
|
|
||||||
format_overrides:
|
|
||||||
- goos: windows
|
|
||||||
format: zip
|
|
||||||
files:
|
|
||||||
- README.md
|
|
||||||
- LICENSE
|
|
||||||
- tpls/*
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
## [1.2.5] - 2020-12-16
|
|
||||||
### Added:
|
|
||||||
* The TPL can use environment variables.
|
|
||||||
* Debug mode.
|
|
||||||
* Use GitHub Actions for automation.
|
|
||||||
|
|
||||||
## [1.2.4] - 2020-04-28
|
|
||||||
### Fixed:
|
|
||||||
* kubernetes runner missing env, [details](https://docs.drone.io/runner/kubernetes/overview)
|
|
||||||
|
|
||||||
## [1.2.3] - 2020-01-20
|
|
||||||
### Fixed:
|
|
||||||
* CDN url of default image.
|
|
||||||
|
|
||||||
## [1.2.2] - 2019-12-07
|
|
||||||
### Added:
|
|
||||||
* Support DingTalk `sign secret`, please see the [README](README.md) for instructions.
|
|
||||||
|
|
||||||
## [1.2.1] - 2019-11-07
|
|
||||||
### Added:
|
|
||||||
* Support customize message tips title by `tips_title` option.
|
|
||||||
|
|
||||||
## [1.2.0] - 2019-09-24
|
|
||||||
### Added:
|
|
||||||
* Support custom TPL.
|
|
||||||
|
|
||||||
## [1.1.4] - 2020-04-28
|
|
||||||
### Fixed:
|
|
||||||
* kubernetes runner missing env, [details](https://docs.drone.io/runner/kubernetes/overview)
|
|
||||||
|
|
||||||
## [1.1.3] - 2020-01-20
|
|
||||||
### Fixed:
|
|
||||||
* CDN url of default image.
|
|
||||||
|
|
||||||
## [1.1.2] - 2019-11-07
|
|
||||||
### Added:
|
|
||||||
* Support customize message tips title by `tips_title` option.
|
|
||||||
|
|
||||||
## [1.1.1] - 2019-09-24
|
|
||||||
### Added:
|
|
||||||
* Support full token url as `token`.
|
|
||||||
|
|
||||||
## [1.1.0] - 2019-03-09
|
|
||||||
### Added:
|
|
||||||
* Package management tools migrate from dep to go mod, 1.0.x version stopped supporting new features.
|
|
||||||
|
|
||||||
## [1.0.2] - 2020-01-20
|
|
||||||
### Fixed:
|
|
||||||
* CDN url of default image.
|
|
||||||
|
|
||||||
## [1.0.1] - 2019-03-09
|
|
||||||
### Added:
|
|
||||||
* Auto publish image to DockerHub.
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.5...HEAD
|
|
||||||
[1.2.5]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.4...v1.2.5
|
|
||||||
[1.2.4]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.3...v1.2.4
|
|
||||||
[1.2.3]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.2...v1.2.3
|
|
||||||
[1.2.2]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.1...v1.2.2
|
|
||||||
[1.2.1]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.2.0...v1.2.1
|
|
||||||
[1.2.0]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.1.0...v1.2.0
|
|
||||||
[1.1.4]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.1.3...v1.1.4
|
|
||||||
[1.1.3]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.1.2...v1.1.3
|
|
||||||
[1.1.2]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.1.1...v1.1.2
|
|
||||||
[1.1.1]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.1.0...v1.1.1
|
|
||||||
[1.1.0]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.0.0...v1.1.0
|
|
||||||
[1.0.2]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.0.1...v1.0.2
|
|
||||||
[1.0.1]: https://github.com/lddsb/drone-dingtalk-message/compare/v1.0.0...v1.0.1
|
|
||||||
+7
-9
@@ -1,11 +1,9 @@
|
|||||||
FROM golang AS builder
|
|
||||||
WORKDIR /app
|
|
||||||
COPY . .
|
|
||||||
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux go build -a -o drone-dingtalk .
|
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
|
|
||||||
COPY --from=builder /app/drone-dingtalk /bin
|
|
||||||
COPY --from=builder /app/tpls /app/drone/dingtalk/message/tpls
|
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/drone-dingtalk"]
|
RUN apk update && \
|
||||||
|
apk add \
|
||||||
|
ca-certificates && \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
ADD drone-dingtalk-message /bin/
|
||||||
|
ENTRYPOINT ["/bin/drone-dingtalk-message"]
|
||||||
Generated
+31
@@ -0,0 +1,31 @@
|
|||||||
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:1f4b55d7c57681ad91b7ce3ac7c8e7b139e6dd2f477c5f87e6a7749d298645dc"
|
||||||
|
name = "github.com/joho/godotenv"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"autoload",
|
||||||
|
]
|
||||||
|
pruneopts = "UT"
|
||||||
|
revision = "23d116af351c84513e1946b527c88823e476be13"
|
||||||
|
version = "v1.3.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:b24d38b282bacf9791408a080f606370efa3d364e4b5fd9ba0f7b87786d3b679"
|
||||||
|
name = "github.com/urfave/cli"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "UT"
|
||||||
|
revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
|
||||||
|
version = "v1.20.0"
|
||||||
|
|
||||||
|
[solve-meta]
|
||||||
|
analyzer-name = "dep"
|
||||||
|
analyzer-version = 1
|
||||||
|
input-imports = [
|
||||||
|
"github.com/joho/godotenv/autoload",
|
||||||
|
"github.com/urfave/cli",
|
||||||
|
]
|
||||||
|
solver-name = "gps-cdcl"
|
||||||
|
solver-version = 1
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
# Gopkg.toml example
|
||||||
|
#
|
||||||
|
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||||
|
# for detailed Gopkg.toml documentation.
|
||||||
|
#
|
||||||
|
# required = ["github.com/user/thing/cmd/thing"]
|
||||||
|
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project"
|
||||||
|
# version = "1.0.0"
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project2"
|
||||||
|
# branch = "dev"
|
||||||
|
# source = "github.com/myfork/project2"
|
||||||
|
#
|
||||||
|
# [[override]]
|
||||||
|
# name = "github.com/x/y"
|
||||||
|
# version = "2.4.0"
|
||||||
|
#
|
||||||
|
# [prune]
|
||||||
|
# non-go = false
|
||||||
|
# go-tests = true
|
||||||
|
# unused-packages = true
|
||||||
|
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/joho/godotenv"
|
||||||
|
version = "1.3.0"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/urfave/cli"
|
||||||
|
version = "1.20.0"
|
||||||
|
|
||||||
|
[prune]
|
||||||
|
go-tests = true
|
||||||
|
unused-packages = true
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
GO ?= go
|
|
||||||
PACKAGES ?= $(shell $(GO) list ./...)
|
|
||||||
|
|
||||||
vet:
|
|
||||||
$(GO) vet $(PACKAGES)
|
|
||||||
|
|
||||||
test:
|
|
||||||
@$(GO) test -v -cover -coverprofile coverage.txt $(PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
|
||||||
|
|
||||||
coverage:
|
|
||||||
sed -i '/main.go/d' coverage.txt
|
|
||||||
@@ -1,61 +1,29 @@
|
|||||||
# Drone CI DingTalk Message Plugin
|
# Drone CI DingTalk Message Plugin
|
||||||
[](https://github.com/lddsb/drone-dingtalk-message/actions?query=workflow%3A%22Publish+to+DockerHub+and+Github+Package%22) [](https://goreportcard.com/report/github.com/lddsb/drone-dingtalk-message) [](https://codecov.io/gh/lddsb/drone-dingtalk-message) [](https://app.dependabot.com/accounts/lddsb/repos/159822771) [](LICENSE)
|
|
||||||
|
|
||||||
[中文说明](README_ZH.md)
|
|
||||||
|
|
||||||
<!-- toc -->
|
|
||||||
|
|
||||||
- [Drone CI Plugin Config](#drone-ci-plugin-config)
|
|
||||||
- [Plugin Parameter Reference](#plugin-parameter-reference)
|
|
||||||
- [TPL](#tpl)
|
|
||||||
- [Screen Shot](#screen-shot)
|
|
||||||
- [Development](#development)
|
|
||||||
- [Todo](#todo)
|
|
||||||
- [Kubernetes Users](#kubernetes-users)
|
|
||||||
|
|
||||||
<!-- tocstop -->
|
|
||||||
|
|
||||||
### Drone CI Plugin Config
|
### Drone CI Plugin Config
|
||||||
`0.8.x`
|
`0.8.x`
|
||||||
```yaml
|
```yaml
|
||||||
pipeline:
|
pipeline:
|
||||||
#...
|
...
|
||||||
notification:
|
notification:
|
||||||
image: lddsb/drone-dingtalk-message
|
image: lddsb/drone-dingtalk-message
|
||||||
token: your-group-bot-token
|
token: your-group-bot-token
|
||||||
type: markdown
|
type: markdown
|
||||||
```
|
```
|
||||||
|
|
||||||
`1.x`
|
`1.0.x`
|
||||||
```yaml
|
```yaml
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
#...
|
...
|
||||||
- name: notification
|
- name: notification
|
||||||
image: lddsb/drone-dingtalk-message
|
image: lddsb/drone-dingtalk-message
|
||||||
settings:
|
settings:
|
||||||
token: your-groupbot-token
|
token: your-groupbot-token
|
||||||
type: markdown
|
type: markdown
|
||||||
secret: your-secret-for-generate-sign
|
|
||||||
debug: true
|
|
||||||
```
|
|
||||||
|
|
||||||
`Use the "exec" type`
|
|
||||||
```yaml
|
|
||||||
kind: pipeline
|
|
||||||
type: exec
|
|
||||||
|
|
||||||
steps:
|
|
||||||
...
|
|
||||||
|
|
||||||
- name: notification
|
|
||||||
environment: # Using environment to pass parameters
|
|
||||||
PLUGIN_TOKEN:
|
|
||||||
from_secret: dingtalk_token
|
|
||||||
PLUGIN_TYPE: markdown
|
|
||||||
PLUGIN_DEBUG: false
|
|
||||||
PLUGIN_TPL: /data/drone/dingtalk/tpls/markdown.tpl # The actual location (absolute path) of the tpl.
|
|
||||||
commands:
|
|
||||||
- /data/drone/dingtalk/dingtalk-message # Location of the "dingtalk-message" file (absolute path)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Plugin Parameter Reference
|
### Plugin Parameter Reference
|
||||||
@@ -67,103 +35,34 @@ String. Access token for group bot. (you can get the access token when you add a
|
|||||||
|
|
||||||
String. Message type, plan support text, markdown, link and action card, but due to time issue, it's only support `markdown` and `text` now, and you can get the best experience by use markdown.
|
String. Message type, plan support text, markdown, link and action card, but due to time issue, it's only support `markdown` and `text` now, and you can get the best experience by use markdown.
|
||||||
|
|
||||||
`secret`
|
`message_color`(when `type=markdown`)
|
||||||
|
|
||||||
String. Secret for generate sign.
|
Boolean value. This option can change the title and commit message color if turn on.
|
||||||
|
|
||||||
`tpl`
|
`success_color`(when `message_color=true`)
|
||||||
|
|
||||||
String. Your custom `tpl`, it can be a local path, or a remote http link.
|
|
||||||
|
|
||||||
`debug`
|
|
||||||
Boolean. Debug mode.
|
|
||||||
|
|
||||||
`tips_title`
|
|
||||||
|
|
||||||
String. You can customize the title for the message tips, just work when message type is markdown.
|
|
||||||
|
|
||||||
`success_color`
|
|
||||||
|
|
||||||
String. You can customize the color for the `build success` message by this option, you should input a hex color, example: `008000`.
|
String. You can customize the color for the `build success` message by this option, you should input a hex color, example: `008000`.
|
||||||
|
|
||||||
`failure_color`
|
`failure_color`(when `message_color=true`)
|
||||||
|
|
||||||
String. You can customize the color for the `build success` message by this option, you should input a hex color, example: `FF0000`.
|
String. You can customize the color for the `build success` message by this option, you should input a hex color, example: `FF0000`.
|
||||||
|
|
||||||
`success_pic`
|
`sha_link`(when `type=markdown`)
|
||||||
|
|
||||||
|
Boolean value. This option can link the sha to your source page when it turn on.
|
||||||
|
|
||||||
|
`message_pic`(when `type=markdown`)
|
||||||
|
|
||||||
|
Boolean value. If this option turn on, it will embed a image into the message.
|
||||||
|
|
||||||
|
`success_pic`(when `message_pic=true`)
|
||||||
|
|
||||||
String. You can customize the picture for the `build success` message by this option.
|
String. You can customize the picture for the `build success` message by this option.
|
||||||
|
|
||||||
`failure_pic`
|
`failure_pic`(when `message_pic=true`)
|
||||||
|
|
||||||
String. You can customize the picture for the `build failure` message by this option.
|
String. You can customize the picture for the `build failure` message by this option.
|
||||||
|
|
||||||
`tpl_commit_branch_name`
|
|
||||||
|
|
||||||
String. You can customize the [TPL_COMMIT_BRANCH] by this configuration item.
|
|
||||||
|
|
||||||
`tpl_repo_short_name`
|
|
||||||
|
|
||||||
String. You can customize the [TPL_REPO_SHORT_NAME] by this configuration item.
|
|
||||||
|
|
||||||
`tpl_repo_full_name`
|
|
||||||
|
|
||||||
String. You can customize the [TPL_REPO_FULL_NAME] by this configuration item.
|
|
||||||
|
|
||||||
`tpl_build_status_success`
|
|
||||||
|
|
||||||
String. You can customize the [TPL_BUILD_STATUS] (when status=`success`) by this configuration item.
|
|
||||||
|
|
||||||
`tpl_build_status_failure`
|
|
||||||
|
|
||||||
String. You can customize the [TPL_BUILD_STATUS] (when status=`failure`) by this configuration item.
|
|
||||||
|
|
||||||
`msg_at_mobiles`
|
|
||||||
|
|
||||||
String. You want at's phone number in the group, if you need at multi phone numbers, you can use `,` to separate. (if you use markdown type, you need define the at content in your tpl file)
|
|
||||||
|
|
||||||
### TPL
|
|
||||||
> `tpl` won't work with message type `link` !!!
|
|
||||||
|
|
||||||
That's a good news, we support `tpl` now.This is an example for `markdown` message:
|
|
||||||
|
|
||||||
# [TPL_REPO_FULL_NAME] build [TPL_BUILD_STATUS], takes [TPL_BUILD_CONSUMING]s
|
|
||||||
@mobile1 @mobile2
|
|
||||||
[TPL_COMMIT_MSG]
|
|
||||||
|
|
||||||
[TPL_COMMIT_SHA]([TPL_COMMIT_LINK])
|
|
||||||
|
|
||||||
[[TPL_AUTHOR_NAME]([TPL_AUTHOR_EMAIL])](mailto:[TPL_AUTHOR_EMAIL])
|
|
||||||
|
|
||||||
[Click To The Build Detail Page [TPL_STATUS_EMOTICON)]]([TPL_BUILD_LINK])
|
|
||||||
You can write your own `tpl` what you want. The syntax of `tpl` is very simple, you can fill `tpl` with preset variables. It's a list of currently supported preset variables:
|
|
||||||
|
|
||||||
| Variable | Value |
|
|
||||||
| :-------------------: | :-------------------------------------------------: |
|
|
||||||
| [TPL_REPO_SHORT_NAME] | current repo name(bare name) |
|
|
||||||
| [TPL_REPO_FULL_NAME] | the full name(with group name) of current repo |
|
|
||||||
| [TPL_REPO_GROUP_NAME] | the group name of current repo |
|
|
||||||
| [TPL_REPO_OWNER_NAME] | the owner name of current repo |
|
|
||||||
| [TPL_REPO_REMOTE_URL] | the remote url of current repo |
|
|
||||||
| [TPL_BUILD_STATUS] | current build status(e.g., success, failure) |
|
|
||||||
| [TPL_BUILD_LINK] | current build link |
|
|
||||||
| [TPL_BUILD_EVENT] | current build event(e.g., push, pull request, etc.) |
|
|
||||||
| [TPL_BUILD_CONSUMING] | current build consuming, second |
|
|
||||||
| [TPL_COMMIT_SHA] | current commit sha |
|
|
||||||
| [TPL_COMMIT_REF] | current commit ref(e.g., refs/heads/master, etc.) |
|
|
||||||
| [TPL_COMMIT_LINK] | current commit remote url link |
|
|
||||||
| [TPL_COMMIT_BRANCH] | current branch name(e.g., dev, etc) |
|
|
||||||
| [TPL_COMMIT_MSG] | current commit message |
|
|
||||||
| [TPL_AUTHOR_NAME] | current commit author name |
|
|
||||||
| [TPL_AUTHOR_EMAIL] | current commit author email |
|
|
||||||
| [TPL_AUTHOR_USERNAME] | current commit author username |
|
|
||||||
| [TPL_AUTHOR_AVATAR] | current commit author avatar |
|
|
||||||
| [TPL_STATUS_PIC] | custom pic for build status |
|
|
||||||
| [TPL_STATUS_COLOR] | custom color for build status |
|
|
||||||
| [TPL_STATUS_EMOTICON] | custom emoticon for build status |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Screen Shot
|
### Screen Shot
|
||||||
- Send Success
|
- Send Success
|
||||||
|
|
||||||
@@ -189,35 +88,28 @@ You can write your own `tpl` what you want. The syntax of `tpl` is very simple,
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Multi-Type
|
||||||
|
- Multi-Lang
|
||||||
|
- More User Customization
|
||||||
|
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
We use `go mod` to manage dependencies, so it's easy to build.
|
|
||||||
|
|
||||||
- get this repo
|
- First get this repo
|
||||||
```shell
|
```shell
|
||||||
$ git clone https://github.com/lddsb/drone-dingtalk-message.git /path/to/you/want
|
go get github.com/lddsb/drone-dingtalk-message
|
||||||
|
```
|
||||||
|
- get dependent lib
|
||||||
|
```shell
|
||||||
|
dep ensure
|
||||||
```
|
```
|
||||||
- build
|
- build
|
||||||
```shell
|
```shell
|
||||||
$ cd /path/to/you/want && GO111MODULE=on go build .
|
cd $GOPATH/src/github.com/lddsb/drone-dingtalk-message && go build .
|
||||||
```
|
```
|
||||||
- run
|
- run
|
||||||
```shell
|
```shell
|
||||||
$ ./drone-dingtalk-message -h
|
./drone-dingtalk-message -h
|
||||||
```
|
```
|
||||||
|
|
||||||
### TODO
|
|
||||||
It's sad, just support `text`, `markdown` and `link` type now.
|
|
||||||
- implement all message type
|
|
||||||
- i18N
|
|
||||||
- batch send
|
|
||||||
- retry(e.g., network error, etc.)
|
|
||||||
|
|
||||||
### Kubernetes Users
|
|
||||||
Attention kubernetes users, [CHANGELOG](CHANGELOG.md#124---2020-04-28).It's the available versions:
|
|
||||||
|
|
||||||
- `1.1`(always latest for `1.1.x`)
|
|
||||||
- `>=1.1.4`
|
|
||||||
- `1.2`(always latest for `1.2.x`)
|
|
||||||
- `>=1.2.4`
|
|
||||||
- latest(always latest)
|
|
||||||
-226
@@ -1,226 +0,0 @@
|
|||||||
# Drone CI的钉钉群组机器人通知插件
|
|
||||||
[](https://github.com/lddsb/drone-dingtalk-message/actions?query=workflow%3A%22Publish+to+DockerHub+and+Github+Package%22) [](https://goreportcard.com/report/github.com/lddsb/drone-dingtalk-message) [](https://codecov.io/gh/lddsb/drone-dingtalk-message) [](https://app.dependabot.com/accounts/lddsb/repos/159822771) [](LICENSE)
|
|
||||||
|
|
||||||
|
|
||||||
<!-- toc -->
|
|
||||||
|
|
||||||
- [怎么使用本插件](#%E6%80%8E%E4%B9%88%E4%BD%BF%E7%94%A8%E6%9C%AC%E6%8F%92%E4%BB%B6)
|
|
||||||
- [插件参数](#%E6%8F%92%E4%BB%B6%E5%8F%82%E6%95%B0)
|
|
||||||
- [模版](#%E6%A8%A1%E7%89%88)
|
|
||||||
- [截图展示](#%E6%88%AA%E5%9B%BE%E5%B1%95%E7%A4%BA)
|
|
||||||
- [贡献代码](#%E8%B4%A1%E7%8C%AE%E4%BB%A3%E7%A0%81)
|
|
||||||
- [未来计划](#%E6%9C%AA%E6%9D%A5%E8%AE%A1%E5%88%92)
|
|
||||||
- [Kubernetes 用户请注意](#kubernetes-%E7%94%A8%E6%88%B7%E8%AF%B7%E6%B3%A8%E6%84%8F)
|
|
||||||
|
|
||||||
<!-- tocstop -->
|
|
||||||
|
|
||||||
### 怎么使用本插件
|
|
||||||
添加一个`step`到你的`.drone.yml`中,下面是例子:
|
|
||||||
|
|
||||||
`0.8.x`
|
|
||||||
```yaml
|
|
||||||
pipeline:
|
|
||||||
...
|
|
||||||
notification:
|
|
||||||
image: lddsb/drone-dingtalk-message
|
|
||||||
token: your-group-bot-token
|
|
||||||
type: markdown
|
|
||||||
```
|
|
||||||
|
|
||||||
`1.x`
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
...
|
|
||||||
- name: notification
|
|
||||||
image: lddsb/drone-dingtalk-message
|
|
||||||
settings:
|
|
||||||
token: your-groupbot-token
|
|
||||||
type: markdown
|
|
||||||
secret: your-secret-for-generate-sign
|
|
||||||
debug: true
|
|
||||||
```
|
|
||||||
|
|
||||||
`命令行版本`
|
|
||||||
```yaml
|
|
||||||
kind: pipeline
|
|
||||||
type: exec
|
|
||||||
|
|
||||||
steps:
|
|
||||||
...
|
|
||||||
|
|
||||||
- name: 通知
|
|
||||||
environment: # 使用 environment 传递参数
|
|
||||||
PLUGIN_TOKEN:
|
|
||||||
from_secret: dingtalk_token
|
|
||||||
PLUGIN_TYPE: markdown
|
|
||||||
PLUGIN_DEBUG: false
|
|
||||||
PLUGIN_TPL: /data/drone/dingtalk/tpls/markdown.tpl # tpl 的实际位置(绝对路径)
|
|
||||||
commands:
|
|
||||||
- /data/drone/dingtalk/dingtalk-message # dingtalk-message 文件的位置(绝对路径)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 插件参数
|
|
||||||
`token`(必须)
|
|
||||||
|
|
||||||
你可以通过加入和创建一个群组来添加钉钉自定义机器人,添加自定义机器人完成后即可获得所需要的`access token`。
|
|
||||||
|
|
||||||
`type`(必须)
|
|
||||||
|
|
||||||
消息类型,因个人能力有限,目前仅支持`markdown`和`text`,其中,使用`markdown`可以获得最好的体验。
|
|
||||||
|
|
||||||
`secret`
|
|
||||||
|
|
||||||
如果你设置了`加签`,可以把你的`加签`密钥填入此项完成`加签`操作。
|
|
||||||
|
|
||||||
`tpl`
|
|
||||||
|
|
||||||
你可以通过该字段来自定义你的消息模版。该字段可以是一个本地路径也可以是一个远程的URL。
|
|
||||||
|
|
||||||
`debug`
|
|
||||||
|
|
||||||
通过该值可以打开`debug`模式,打印所有环境变量。
|
|
||||||
|
|
||||||
`tips_title`
|
|
||||||
|
|
||||||
你可以通过该字段自定义钉钉机器人的消息通知提醒标题。(注意,不是消息内容的标题,是收到钉钉机器人发的消息后,会有一个外显的标题)
|
|
||||||
|
|
||||||
`success_color`
|
|
||||||
|
|
||||||
你可以通过该字段自定义打包成功的颜色。比如:`008000`。
|
|
||||||
|
|
||||||
`failure_color`
|
|
||||||
|
|
||||||
你可以通过该字段自定义打包失败的颜色。比如:`FF0000`。
|
|
||||||
|
|
||||||
`success_pic`
|
|
||||||
|
|
||||||
你可以通过该字段自定义打包成功的图片。
|
|
||||||
|
|
||||||
`failure_pic`
|
|
||||||
|
|
||||||
字符串,你可以通过该字段自定义打包失败的图片。
|
|
||||||
|
|
||||||
`tpl_commit_branch_name`
|
|
||||||
|
|
||||||
你可以通过该字段自定义分支的名称,可以在模版中通过[TPL_COMMIT_BRANCH]来使用该值。
|
|
||||||
|
|
||||||
`tpl_repo_short_name`
|
|
||||||
|
|
||||||
你可以通过该字段自定义仓库的名字,可以在模版中通过[TPL_REPO_SHORT_NAME]来使用该值。
|
|
||||||
|
|
||||||
`tpl_repo_full_name`
|
|
||||||
|
|
||||||
你可以通过该字段自定义仓库的全名(包含组织名称),可以在模版中通过[TPL_REPO_FULL_NAME]来使用该值。
|
|
||||||
|
|
||||||
`tpl_build_status_success`
|
|
||||||
|
|
||||||
你可以通过该字段自定义运行成功状态的值,可以在模版中通过[TPL_BUILD_STATUS]来使用该值。(仅当前方`step`运行结果为成功时该值会生效)
|
|
||||||
|
|
||||||
`tpl_build_status_failure`
|
|
||||||
|
|
||||||
你可以通过该字段自定义运行失败状态的值,可以在模版中通过[TPL_BUILD_STATUS]来使用该值。(仅当前方`step`运行结果为失败时该值会生效)
|
|
||||||
|
|
||||||
`msg_at_mobiles`
|
|
||||||
|
|
||||||
你需要@的群成员的手机号,多个时用英文逗号(`,`)分隔。如过你使用的是 `markdown` 类型的消息,则需要在 `tpl` 文件中加入 `@手机号` 的内容。
|
|
||||||
|
|
||||||
### 模版
|
|
||||||
> `tpl` 对 `link` 类型的消息并不支持 !!!
|
|
||||||
|
|
||||||
感天动地,我们终于支持自定义模版了!下面是一个`markdown`的自定义模版例子:
|
|
||||||
|
|
||||||
# [TPL_REPO_FULL_NAME] build [TPL_BUILD_STATUS], takes [TPL_BUILD_CONSUMING]s
|
|
||||||
@mobile1 @mobile2
|
|
||||||
[TPL_COMMIT_MSG]
|
|
||||||
|
|
||||||
[TPL_COMMIT_SHA]([TPL_COMMIT_LINK])
|
|
||||||
|
|
||||||
[[TPL_AUTHOR_NAME]([TPL_AUTHOR_EMAIL])](mailto:[TPL_AUTHOR_EMAIL])
|
|
||||||
|
|
||||||
[Click To The Build Detail Page [TPL_STATUS_EMOTICON)]]([TPL_BUILD_LINK])
|
|
||||||
|
|
||||||
`mobile1` 和 `mobile2` 应该为钉钉对应的手机号码,可以放在自己想要放的位置。
|
|
||||||
|
|
||||||
你可以写自己喜欢的模版,终于不用再对默认模版发愁啦!并且模版的语法非常简单!比较可惜的是目前支持的变量还比较少,下面是当前支持的变量的列表:
|
|
||||||
|
|
||||||
| Variable | Value |
|
|
||||||
| :-------------------: | :-------------------------------------------------: |
|
|
||||||
| [TPL_REPO_SHORT_NAME] | 当前仓库的名称,比如本仓库 `drone-dingtalk-message` |
|
|
||||||
| [TPL_REPO_FULL_NAME] | 当前仓库的名称,比如本仓库 `lddsb/drone-dingtalk-message` |
|
|
||||||
| [TPL_REPO_GROUP_NAME] | 当前仓库的组织名称,比如本仓库 `lddsb` |
|
|
||||||
| [TPL_REPO_OWNER_NAME] | 当前仓库拥有者的名称 |
|
|
||||||
| [TPL_REPO_REMOTE_URL] | 当前仓库的远程地址 |
|
|
||||||
| [TPL_BUILD_STATUS] | 当前编译的状态(比如, success, failure) |
|
|
||||||
| [TPL_BUILD_LINK] | 当前编译的链接 |
|
|
||||||
| [TPL_BUILD_EVENT] | 触发当前编译的动作(比如, push, pull request等) |
|
|
||||||
| [TPL_BUILD_CONSUMING] | 当前编译耗时,单位秒 |
|
|
||||||
| [TPL_COMMIT_SHA] | 当前提交的sha |
|
|
||||||
| [TPL_COMMIT_REF] | 当前提交的ref(比如, refs/heads/master等) |
|
|
||||||
| [TPL_COMMIT_LINK] | 当前提交的远程地址 |
|
|
||||||
| [TPL_COMMIT_BRANCH] | 当前分之名称(比如, dev, master等) |
|
|
||||||
| [TPL_COMMIT_MSG] | 当前提交的信息 |
|
|
||||||
| [TPL_AUTHOR_NAME] | 当前提交作者名称 |
|
|
||||||
| [TPL_AUTHOR_EMAIL] | 当前提交作者邮箱地址 |
|
|
||||||
| [TPL_AUTHOR_USERNAME] | 当前提交作者的用户名 |
|
|
||||||
| [TPL_AUTHOR_AVATAR] | 当前提交作者的头像 |
|
|
||||||
| [TPL_STATUS_PIC] | 根据编译状态显示不同的图片 |
|
|
||||||
| [TPL_STATUS_COLOR] | 根据编译状态显示不同的颜色 |
|
|
||||||
| [TPL_STATUS_EMOTICON] | 根据编译状态显示不同的表情,比如 `:)` `:(` |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 截图展示
|
|
||||||
- 发送成功(Drone Web)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- 忘记填写Access Token(Drone Web)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- 忘记填写消息类型或者不支持的消息类型
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- 默认的`markdown`消息
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- 带颜色和链接的`markdown`消息
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- 带颜色、链接和图片的`markdown`消息
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
### 贡献代码
|
|
||||||
本项目使用了`go mod`来管理依赖,因此要编译本项目相当简单。
|
|
||||||
|
|
||||||
- 先把项目代码拷贝到本地
|
|
||||||
```shell
|
|
||||||
$ git clone https://github.com/lddsb/drone-dingtalk-message.git /path/to/you/want
|
|
||||||
```
|
|
||||||
- 然后直接执行编译即可
|
|
||||||
```shell
|
|
||||||
$ cd /path/to/you/want && GO111MODULE=on go build .
|
|
||||||
```
|
|
||||||
- 跑个`help`
|
|
||||||
```shell
|
|
||||||
$ ./drone-dingtalk-message -h
|
|
||||||
```
|
|
||||||
|
|
||||||
### 未来计划
|
|
||||||
目前仅支持 `text`, `markdown` 以及 `link` 类型的消息,建议使用`markdown`类型。
|
|
||||||
- 实现更多的消息类型
|
|
||||||
- i18N国际化直接翻译环境变量
|
|
||||||
- 批量发送给多个群机器人
|
|
||||||
- 失败重试机制
|
|
||||||
|
|
||||||
### Kubernetes 用户请注意
|
|
||||||
因为`Drone CI` [官方缺陷](https://docs.drone.io/runner/kubernetes/overview) ,所以较早版本将无法正常获取到需要用到的变量,会导致部分功能异常。为了能正常使用,所以请使用以下版本:
|
|
||||||
- `1.1`(总会是`1.1.x`的最新版本)
|
|
||||||
- `>=1.1.4`
|
|
||||||
- `1.2`(总会是`1.2.x`的最新版本)
|
|
||||||
- `>=1.2.4`
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
module github.com/lddsb/drone-dingtalk-message
|
|
||||||
|
|
||||||
go 1.12
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/joho/godotenv v1.5.1
|
|
||||||
github.com/lddsb/dingtalk-webhook v0.0.5
|
|
||||||
github.com/urfave/cli v1.22.14
|
|
||||||
)
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/lddsb/dingtalk-webhook v0.0.5 h1:EOSXvcpN4IC7fXSAheI3OLJwwOGEPlGDV7xje1fQuJo=
|
|
||||||
github.com/lddsb/dingtalk-webhook v0.0.5/go.mod h1:dwNU75Sog87wJXAFcY5mDFM7eW4hIdX7bNemrN92pH0=
|
|
||||||
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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
|
|
||||||
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=
|
|
||||||
@@ -4,21 +4,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version of cli
|
var Version = "0.1.1202"
|
||||||
var Version = "0.2.1219"
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Name = "Drone DingTalk Message Plugin"
|
app.Name = "Drone Dingtalk Message Plugin"
|
||||||
app.Usage = "Sending message to DingTalk group by robot using WebHook"
|
app.Usage = "Sending message to Dingtalk group by robot using webhook"
|
||||||
year := time.Now().Year()
|
app.Copyright = "© 2018 Dee Luo"
|
||||||
app.Copyright = fmt.Sprintf("© 2018-%d Dee Luo", year)
|
|
||||||
app.Authors = []cli.Author{
|
app.Authors = []cli.Author{
|
||||||
{
|
{
|
||||||
Name: "Dee Luo",
|
Name: "Dee Luo",
|
||||||
@@ -28,45 +25,47 @@ func main() {
|
|||||||
app.Action = run
|
app.Action = run
|
||||||
app.Version = Version
|
app.Version = Version
|
||||||
app.Flags = []cli.Flag{
|
app.Flags = []cli.Flag{
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "config.debug,debug",
|
|
||||||
Usage: "debug mode",
|
|
||||||
EnvVar: "PLUGIN_DEBUG",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "config.tips.title",
|
|
||||||
Usage: "customize the tips title",
|
|
||||||
EnvVar: "PLUGIN_TIPS_TITLE",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "config.token,access_token,token",
|
Name: "config.token,access_token,token",
|
||||||
Usage: "DingTalk webhook access token",
|
Usage: "dingtalk webhook access token",
|
||||||
EnvVar: "PLUGIN_ACCESS_TOKEN,PLUGIN_TOKEN",
|
EnvVar: "PLUGIN_ACCESS_TOKEN,PLUGIN_TOKEN",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "config.secret,secret",
|
Name: "config.lang",
|
||||||
Usage: "DingTalk WebHook secret for generate sign",
|
Value: "zh_CN",
|
||||||
EnvVar: "PLUGIN_SECRET",
|
Usage: "the lang display (zh_CN or en_US, zh_CN is default)",
|
||||||
|
EnvVar: "PLUGIN_LANG",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "config.message.type,message_type,type",
|
Name: "config.message.type,msg_type,message_type,type",
|
||||||
Usage: "DingTalk message type, like text, markdown, action card, link and feed card...",
|
Usage: "dingtalk message type, like text, markdown, action card, link and feed card...",
|
||||||
EnvVar: "PLUGIN_MSG_TYPE,PLUGIN_TYPE,PLUGIN_MESSAGE_TYPE",
|
EnvVar: "PLUGIN_MSG_TYPE,PLUGIN_TYPE,PLUGIN_MESSAGE_TYPE",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "config.message.at.all,at.all",
|
Name: "config.message.at.all",
|
||||||
Usage: "at all in a message(only text and markdown type message can at)",
|
Usage: "at all in a message(only text and markdown type message can at)",
|
||||||
EnvVar: "PLUGIN_MSG_AT_ALL",
|
EnvVar: "PLUGIN_MSG_AT_ALL",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "config.message.at.mobiles,mobiles",
|
Name: "config.message.at.mobiles",
|
||||||
Usage: "at someone in a DingTalk group need this guy bind's mobile",
|
Usage: "at someone in a dingtalk group need this guy bind's mobile",
|
||||||
EnvVar: "PLUGIN_MSG_AT_MOBILES",
|
EnvVar: "PLUGIN_MSG_AT_MOBILES",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "drone",
|
||||||
|
Usage: "indicates the runtime environment is Drone",
|
||||||
|
EnvVar: "DRONE",
|
||||||
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "commit.author.username",
|
Name: "branch",
|
||||||
Usage: "providers the author username for the current commit",
|
Usage: "providers the branch for the current build",
|
||||||
EnvVar: "DRONE_COMMIT_AUTHOR",
|
EnvVar: "DRONE_BRANCH",
|
||||||
|
},
|
||||||
|
// commit args start
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "remote.url",
|
||||||
|
Usage: "git remote url",
|
||||||
|
EnvVar: "DRONE_REMOTE_URL",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "commit.author.avatar",
|
Name: "commit.author.avatar",
|
||||||
@@ -99,50 +98,160 @@ func main() {
|
|||||||
Usage: "providers the commit message for the current build",
|
Usage: "providers the commit message for the current build",
|
||||||
EnvVar: "DRONE_COMMIT_MESSAGE",
|
EnvVar: "DRONE_COMMIT_MESSAGE",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "commit.ref",
|
||||||
|
Usage: "providers the reference for the current build",
|
||||||
|
EnvVar: "DRONE_COMMIT_REF",
|
||||||
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "commit.sha",
|
Name: "commit.sha",
|
||||||
Usage: "providers the commit sha for the current build",
|
Usage: "providers the commit sha for the current build",
|
||||||
EnvVar: "DRONE_COMMIT_SHA",
|
EnvVar: "DRONE_COMMIT_SHA",
|
||||||
},
|
},
|
||||||
|
// commit args end
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "commit.ref",
|
Name: "git.url.http",
|
||||||
Usage: "provider the commit ref for the current build",
|
Usage: "providers the repository git+http url",
|
||||||
EnvVar: "DRONE_COMMIT_REF",
|
EnvVar: "DRONE_GIT_HTTP_URL",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "repo.full.name",
|
Name: "git.url.ssh",
|
||||||
|
Usage: "providers the repository git+ssh url",
|
||||||
|
EnvVar: "DRONE_GIT_SSH_URL",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "machine",
|
||||||
|
Usage: "providers the Drone agent hostname",
|
||||||
|
EnvVar: "DRONE_MACHINE",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "pull.request",
|
||||||
|
Usage: "providers the pull request number for the current build.This value is only set if the build event is of type pull request",
|
||||||
|
EnvVar: "DRONE_PULL_REQUEST",
|
||||||
|
},
|
||||||
|
// repo args start
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "repo.fullname",
|
||||||
Usage: "providers the full name of the repository",
|
Usage: "providers the full name of the repository",
|
||||||
EnvVar: "DRONE_REPO",
|
EnvVar: "DRONE_REPO",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "repo.owner",
|
||||||
|
Usage: "repository owner",
|
||||||
|
EnvVar: "DRONE_REPO_OWNER",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "repo.branch",
|
||||||
|
Usage: "providers the default repository branch(e.g.master)",
|
||||||
|
EnvVar: "DRONE_REPO_BRANCH",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "repo.link",
|
||||||
|
Usage: "providers the repository http link",
|
||||||
|
EnvVar: "DRONE_REPO_LINK",
|
||||||
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "repo.name",
|
Name: "repo.name",
|
||||||
Usage: "provider the name of the repository",
|
Usage: "providers the repository name",
|
||||||
EnvVar: "DRONE_REPO_NAME",
|
EnvVar: "DRONE_REPO_NAME",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "repo.group",
|
Name: "repo.avatar",
|
||||||
Usage: "provider the group of the repository",
|
Usage: "repository avatar",
|
||||||
|
EnvVar: "DRONE_REPO_AVATAR",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "repo.namespace",
|
||||||
|
Usage: "providers the repository namespace(e.g. account owner)",
|
||||||
EnvVar: "DRONE_REPO_NAMESPACE",
|
EnvVar: "DRONE_REPO_NAMESPACE",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "repo.private",
|
||||||
|
Usage: "indicates the repository is public or private",
|
||||||
|
EnvVar: "DRONE_REPO_PRIVATE",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "repo.trusted",
|
||||||
|
Usage: "repository is trusted",
|
||||||
|
EnvVar: "DRONE_REPO_TRUSTED",
|
||||||
|
},
|
||||||
|
// repo args end
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "repo.remote.url",
|
Name: "runner.host",
|
||||||
Usage: "provider the remote url of the repository",
|
Usage: "provider are Drone agent hostname",
|
||||||
EnvVar: "DRONE_REMOTE_URL",
|
EnvVar: "DRONE_RUNNER_HOST",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "repo.owner",
|
Name: "runner.hostname",
|
||||||
Usage: "provider the owner of the repository",
|
Usage: "providers the Drone agent hostname",
|
||||||
EnvVar: "DRONE_REPO_OWNER",
|
EnvVar: "DRONE_RUNNER_HOSTNAME",
|
||||||
},
|
},
|
||||||
cli.Uint64Flag{
|
cli.StringFlag{
|
||||||
Name: "stage.started",
|
Name: "runner.platform",
|
||||||
Usage: "stage started ",
|
Usage: "providers the Drone agent os and architecture",
|
||||||
EnvVar: "DRONE_STAGE_STARTED",
|
EnvVar: "DRONE_RUNNER_PLATFORM",
|
||||||
},
|
},
|
||||||
cli.Uint64Flag{
|
cli.StringFlag{
|
||||||
Name: "stage.finished",
|
Name: "runner.label",
|
||||||
Usage: "stage finished",
|
Usage: "404 not found",
|
||||||
EnvVar: "DRONE_STAGE_FINISHED",
|
EnvVar: "DRONE_RUNNER_LABEL",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "source.branch",
|
||||||
|
Usage: "providers the source branch for a pull request",
|
||||||
|
EnvVar: "DRONE_SOURCE_BRANCH",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "target.branch",
|
||||||
|
Usage: "providers the target branch for a pull request",
|
||||||
|
EnvVar: "DRONE_TARGET_BRANCH",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "system.host",
|
||||||
|
Usage: "providers the Drone server hostname",
|
||||||
|
EnvVar: "DRONE_SYSTEM_HOST",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "system.hostname",
|
||||||
|
Usage: "providers the Drone server hostname",
|
||||||
|
EnvVar: "DRONE_SYSTEM_HOSTNAME",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "system.version",
|
||||||
|
Usage: "providers the Drone server version",
|
||||||
|
EnvVar: "DRONE_SYSTEM_VERSION",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "tag",
|
||||||
|
Usage: "providers the tag name for the current build.This value is only set if the build event is of type tag",
|
||||||
|
EnvVar: "DRONE_TAG",
|
||||||
|
},
|
||||||
|
// build args start
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "build.event",
|
||||||
|
Value: "push",
|
||||||
|
Usage: "build event",
|
||||||
|
EnvVar: "DRONE_BUILD_EVENT",
|
||||||
|
},
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "build.number",
|
||||||
|
Usage: "build number",
|
||||||
|
EnvVar: "DRONE_BUILD_NUMBER",
|
||||||
|
},
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "build.created",
|
||||||
|
Usage: "build created",
|
||||||
|
EnvVar: "DRONE_BUILD_CREATED",
|
||||||
|
},
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "build.started",
|
||||||
|
Usage: "build started",
|
||||||
|
EnvVar: "DRONE_BUILD_STARTED",
|
||||||
|
},
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "build.finished",
|
||||||
|
Usage: "build finished",
|
||||||
|
EnvVar: "DRONE_BUILD_FINISHED",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "build.status",
|
Name: "build.status",
|
||||||
@@ -156,168 +265,137 @@ func main() {
|
|||||||
EnvVar: "DRONE_BUILD_LINK",
|
EnvVar: "DRONE_BUILD_LINK",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "build.event",
|
Name: "build.deploy",
|
||||||
Usage: "build event",
|
Usage: "build deployment target",
|
||||||
EnvVar: "DRONE_BUILD_EVENT",
|
EnvVar: "DRONE_DEPLOY_TO",
|
||||||
},
|
},
|
||||||
cli.Uint64Flag{
|
cli.BoolFlag{
|
||||||
Name: "build.started",
|
Name: "yaml.verified",
|
||||||
Usage: "build started",
|
Usage: "build yaml is verified",
|
||||||
EnvVar: "DRONE_BUILD_STARTED",
|
EnvVar: "DRONE_YAML_VERIFIED",
|
||||||
},
|
},
|
||||||
cli.Uint64Flag{
|
cli.BoolFlag{
|
||||||
Name: "build.finished",
|
Name: "yaml.signed",
|
||||||
Usage: "build finished",
|
Usage: "build yaml is signed",
|
||||||
EnvVar: "DRONE_BUILD_FINISHED",
|
EnvVar: "DRONE_YAML_SIGNED",
|
||||||
|
},
|
||||||
|
// build args end
|
||||||
|
cli.Float64Flag{
|
||||||
|
Name: "job.started",
|
||||||
|
Usage: "job started",
|
||||||
|
EnvVar: "DRONE_JOB_STARTED",
|
||||||
|
},
|
||||||
|
cli.Float64Flag{
|
||||||
|
Name: "job.finished",
|
||||||
|
Usage: "job finished",
|
||||||
|
EnvVar: "DRONE_JOB_FINISHED",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "tpl.build.status.success",
|
Name: "ci.repo.link",
|
||||||
Usage: "tpl.build status for replace success",
|
Usage: "ci repo link",
|
||||||
EnvVar: "TPL_BUILD_STATUS_SUCCESS, PLUGIN_TPL_BUILD_STATUS_SUCCESS",
|
EnvVar: "CI_REPO_LINK",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "tpl.build.status.failure",
|
Name: "config.success.pic.url",
|
||||||
Usage: "tpl.build status for replace failure",
|
Usage: "config success picture url",
|
||||||
EnvVar: "TPL_BUILD_STATUS_FAILURE, PLUGIN_TPL_BUILD_STATUS_FAILURE",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "custom.pic.url.success",
|
|
||||||
Usage: "custom success picture url",
|
|
||||||
EnvVar: "SUCCESS_PICTURE_URL,PLUGIN_SUCCESS_PIC",
|
EnvVar: "SUCCESS_PICTURE_URL,PLUGIN_SUCCESS_PIC",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "custom.pic.url.failure",
|
Name: "config.failure.pic.url",
|
||||||
Usage: "custom failure picture url",
|
Usage: "config failure picture url",
|
||||||
EnvVar: "FAILURE_PICTURE_URL,PLUGIN_FAILURE_PIC",
|
EnvVar: "FAILURE_PICTURE_URL,PLUGIN_FAILURE_PIC",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "custom.color.success",
|
Name: "config.success.color",
|
||||||
Usage: "custom success color for title in markdown",
|
Usage: "config success color for title in markdown",
|
||||||
EnvVar: "SUCCESS_COLOR,PLUGIN_SUCCESS_COLOR",
|
EnvVar: "SUCCESS_COLOR,PLUGIN_SUCCESS_COLOR",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "custom.color.failure",
|
Name: "config.failure.color",
|
||||||
Usage: "custom failure color for title in markdown",
|
Usage: "config failure color for title in markdown",
|
||||||
EnvVar: "FAILURE_COLOR,PLUGIN_FAILURE_COLOR",
|
EnvVar: "FAILURE_COLOR,PLUGIN_FAILURE_COLOR",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.BoolFlag{
|
||||||
Name: "custom.tpl,tpl",
|
Name: "config.message.color",
|
||||||
Usage: "custom tpl",
|
Usage: "configure the message with color or not",
|
||||||
EnvVar: "PLUGIN_TPL,PLUGIN_CUSTOM_TPL",
|
EnvVar: "PLUGIN_COLOR,PLUGIN_MESSAGE_COLOR",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.BoolFlag{
|
||||||
Name: "tpl.repo.full.name",
|
Name: "config.message.pic",
|
||||||
Usage: "tpl custom repo full name",
|
Usage: "configure the message with picture or not",
|
||||||
EnvVar: "PLUGIN_TPL_REPO_FULL_NAME,TPL_REPO_FULL_NAME",
|
EnvVar: "PLUGIN_PIC,PLUGIN_MESSAGE_PIC",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.BoolFlag{
|
||||||
Name: "tpl.repo.short.name",
|
Name: "config.message.sha.link",
|
||||||
Usage: "tpl custom repo short name",
|
Usage: "link sha source page or not",
|
||||||
EnvVar: "PLUGIN_TPL_REPO_SHORT_NAME,TPL_REPO_SHORT_NAME",
|
EnvVar: "PLUGIN_SHA_LINK,PLUGIN_MESSAGE_SHA_LINK",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.IntFlag{
|
||||||
Name: "tpl.commit.branch.name",
|
Name: "config.retry.time",
|
||||||
Usage: "tpl custom commit branch name",
|
Usage: "time out retry times, default 3",
|
||||||
EnvVar: "PLUGIN_TPL_COMMIT_BRANCH_NAME,TPL_COMMIT_BRANCH_NAME",
|
Value: 3,
|
||||||
|
EnvVar: "PLUGIN_RETRY_TIME,PLUGIN_RETRY",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
|
||||||
Name: "custom.started,started",
|
|
||||||
Usage: "started custom env name, eg., BUILD_STARTED",
|
|
||||||
EnvVar: "PLUGIN_CUSTOM_STARTED",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "custom.finished,finished",
|
|
||||||
Usage: "finished custom env name, eg., BUILD_FINISHED",
|
|
||||||
EnvVar: "PLUGIN_CUSTOM_FINISHED",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// kubernetes runner patch
|
|
||||||
if _, err := os.Stat("/run/drone/env"); err == nil {
|
|
||||||
godotenv.Overload("/run/drone/env")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Run(os.Args); nil != err {
|
if err := app.Run(os.Args); nil != err {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// run with args
|
// run with args
|
||||||
func run(c *cli.Context) {
|
func run(c *cli.Context) {
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
Drone: Drone{
|
// repo info
|
||||||
// repo info
|
Repo: Repo{
|
||||||
Repo: Repo{
|
FullName: c.String("repo.fullname"),
|
||||||
ShortName: c.String("repo.name"),
|
Owner: c.String("repo.owner"),
|
||||||
GroupName: c.String("repo.group"),
|
Name: c.String("repo.name"),
|
||||||
OwnerName: c.String("repo.owner"),
|
},
|
||||||
RemoteURL: c.String("repo.remote.url"),
|
// build info
|
||||||
FullName: c.String("repo.full.name"),
|
Build: Build{
|
||||||
},
|
Action: c.String("build.action"),
|
||||||
// build info
|
Number: c.Int("build.number"),
|
||||||
Build: Build{
|
Started: c.Float64("build.started"),
|
||||||
Status: c.String("build.status"),
|
Created: c.Float64("build.created"),
|
||||||
Link: c.String("build.link"),
|
Event: c.String("build.event"),
|
||||||
Event: c.String("build.event"),
|
Status: c.String("build.status"),
|
||||||
StartAt: c.Uint64("build.started"),
|
Link: c.String("build.link"),
|
||||||
FinishedAt: c.Uint64("build.finished"),
|
},
|
||||||
},
|
Commit: Commit{
|
||||||
Commit: Commit{
|
Sha: c.String("commit.sha"),
|
||||||
Sha: c.String("commit.sha"),
|
Branch: c.String("commit.branch"),
|
||||||
Branch: c.String("commit.branch"),
|
Message: c.String("commit.message"),
|
||||||
Message: c.String("commit.message"),
|
Link: c.String("commit.link"),
|
||||||
Link: c.String("commit.link"),
|
Authors: struct {
|
||||||
Author: CommitAuthor{
|
Avatar string
|
||||||
Avatar: c.String("commit.author.avatar"),
|
Email string
|
||||||
Email: c.String("commit.author.email"),
|
Name string
|
||||||
Name: c.String("commit.author.name"),
|
}{
|
||||||
Username: c.String("commit.author.username"),
|
Avatar: c.String("commit.author.avatar"),
|
||||||
},
|
Email: c.String("commit.author.email"),
|
||||||
},
|
Name: c.String("commit.author.name"),
|
||||||
Stage: Stage{
|
|
||||||
StartedAt: c.Uint64("stage.started"),
|
|
||||||
FinishedAt: c.Uint64("stage.finished"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// custom config
|
// custom config
|
||||||
Config: Config{
|
Config: Config{
|
||||||
AccessToken: c.String("config.token"),
|
AccessToken: c.String("config.token"),
|
||||||
Secret: c.String("config.secret"),
|
Lang: c.String("config.lang"),
|
||||||
IsAtALL: c.Bool("config.message.at.all"),
|
IsAtALL: c.Bool("config.message.at.all"),
|
||||||
MsgType: c.String("config.message.type"),
|
MsgType: c.String("config.message.type"),
|
||||||
Mobiles: c.String("config.message.at.mobiles"),
|
Mobiles: c.String("config.message.at.mobiles"),
|
||||||
Debug: c.Bool("config.debug"),
|
SuccessPicUrl: c.String("config.success.pic.url"),
|
||||||
TipsTitle: c.String("config.tips.title"),
|
FailurePicUrl: c.String("config.failure.pic.url"),
|
||||||
|
SuccessColor: c.String("config.success.color"),
|
||||||
|
FailureColor: c.String("config.failure.color"),
|
||||||
|
WithColor: c.Bool("config.message.color"),
|
||||||
|
WithPic: c.Bool("config.message.pic"),
|
||||||
|
LinkSha: c.Bool("config.message.sha.link"),
|
||||||
|
RetryTime: c.Int("config.retry.time"),
|
||||||
},
|
},
|
||||||
Custom: Custom{
|
CI: CI{
|
||||||
Pic: Pic{
|
RepoLink: c.String("ci.repo.link"),
|
||||||
SuccessPicURL: c.String("custom.pic.url.success"),
|
|
||||||
FailurePicURL: c.String("custom.pic.url.failure"),
|
|
||||||
},
|
|
||||||
Color: Color{
|
|
||||||
SuccessColor: c.String("custom.color.success"),
|
|
||||||
FailureColor: c.String("custom.color.failure"),
|
|
||||||
},
|
|
||||||
Tpl: c.String("custom.tpl"),
|
|
||||||
Consuming: Consuming{
|
|
||||||
StartedEnv: c.String("custom.started"),
|
|
||||||
FinishedEnv: c.String("custom.finished"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tpl: Tpl{
|
|
||||||
Repo: TplRepo{
|
|
||||||
FullName: c.String("tpl.repo.full.name"),
|
|
||||||
ShortName: c.String("tpl.repo.short.name"),
|
|
||||||
},
|
|
||||||
Commit: TplCommit{
|
|
||||||
Branch: c.String("tpl.commit.branch.name"),
|
|
||||||
},
|
|
||||||
Build: TplBuild{
|
|
||||||
Status: Status{
|
|
||||||
Success: c.String("tpl.build.status.success"),
|
|
||||||
Failure: c.String("tpl.build.status.failure"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,383 +3,257 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net"
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
webhook "github.com/lddsb/dingtalk-webhook"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Repo repo base info
|
// repo base info
|
||||||
Repo struct {
|
Repo struct {
|
||||||
ShortName string // short name
|
Owner string // providers the repository owner name
|
||||||
GroupName string // group name
|
Name string // providers the repository name
|
||||||
FullName string // repository full name
|
Branch string // providers the default repository branch(e.g.master)
|
||||||
OwnerName string // repo owner
|
Link string // providers the repository http link
|
||||||
RemoteURL string // repo remote url
|
NameSpace string // providers the repository namespace(e.g.account owner)
|
||||||
|
Private bool // indicates the repository is public or private
|
||||||
|
Visibility string // providers the repository visibility level.Possible values are public,private and internal
|
||||||
|
SCM string // providers the repository version control system
|
||||||
|
FullName string // repository full name
|
||||||
}
|
}
|
||||||
|
// build info
|
||||||
// Build info
|
|
||||||
Build struct {
|
Build struct {
|
||||||
Status string // providers the current build status
|
Action string // document description not found
|
||||||
Link string // providers the current build link
|
Created float64 // providers the date and time when the build was created in the system
|
||||||
Event string // trigger event
|
Event string // providers the current build event
|
||||||
StartAt uint64 // build start at ( unix timestamp )
|
Number int // providers the current build number
|
||||||
FinishedAt uint64 // build finish at ( unix timestamp )
|
Started float64 // providers the date and time when the build was started
|
||||||
|
Status string // providers the current build status
|
||||||
|
Link string // providers the current build link
|
||||||
}
|
}
|
||||||
|
// commit info
|
||||||
// Commit info
|
|
||||||
Commit struct {
|
Commit struct {
|
||||||
|
After string // providers the commit sha for the current build
|
||||||
|
Author string // providers the author username for the current commit
|
||||||
|
Before string // providers the parent commit sha for the current build
|
||||||
Branch string // providers the branch for the current commit
|
Branch string // providers the branch for the current commit
|
||||||
Link string // providers the http link to the current commit in the remote source code management system(e.g.GitHub)
|
Link string // providers the http link to the current commit in the remote source code management system(e.g.GitHub)
|
||||||
Message string // providers the commit message for the current build
|
Message string // providers the commit message for the current build
|
||||||
|
Ref string // providers the reference for the current build
|
||||||
Sha string // providers the commit sha for the current build
|
Sha string // providers the commit sha for the current build
|
||||||
Ref string // commit ref
|
// repo author info
|
||||||
Author CommitAuthor
|
Authors struct {
|
||||||
|
Avatar string // providers the author avatar for the current commit
|
||||||
|
Email string // providers the author email for the current commit
|
||||||
|
Name string // providers the author name for the current commit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// git url info
|
||||||
// Stage drone stage env
|
Git struct {
|
||||||
Stage struct {
|
HttpUrl string // providers the repository git+http url
|
||||||
StartedAt uint64
|
SSHUrl string // providers the repository git+ssh url
|
||||||
FinishedAt uint64
|
|
||||||
}
|
}
|
||||||
|
// Drone runner info
|
||||||
// CommitAuthor commit author info
|
Runner struct {
|
||||||
CommitAuthor struct {
|
Host string // providers the Drone agent hostname
|
||||||
Avatar string // providers the author avatar for the current commit
|
Hostname string // providers the Drone agent hostname
|
||||||
Email string // providers the author email for the current commit
|
Platform string // providers the Drone agent os and architecture
|
||||||
Name string // providers the author name for the current commit
|
Label string // document description not found
|
||||||
Username string // the author username for the current commit
|
|
||||||
}
|
}
|
||||||
|
// Drone system info
|
||||||
// Drone drone info
|
System struct {
|
||||||
Drone struct {
|
Host string // providers the Drone server hostname
|
||||||
Repo Repo
|
Hostname string // providers the Drone server hostname
|
||||||
Build Build
|
Version string // providers the Drone server version
|
||||||
Commit Commit
|
|
||||||
Stage Stage
|
|
||||||
}
|
}
|
||||||
|
// Drone CI Info
|
||||||
// Config plugin private config
|
CI struct {
|
||||||
|
RepoLink string
|
||||||
|
}
|
||||||
|
// plugin private config
|
||||||
Config struct {
|
Config struct {
|
||||||
Debug bool
|
AccessToken string
|
||||||
AccessToken string
|
Message string
|
||||||
Secret string
|
Lang string
|
||||||
IsAtALL bool
|
IsAtALL bool
|
||||||
Mobiles string
|
Mobiles string
|
||||||
Username string
|
Username string
|
||||||
MsgType string
|
AvatarURL string
|
||||||
TipsTitle string
|
MsgType string
|
||||||
}
|
|
||||||
|
|
||||||
// MessageConfig DingTalk message struct
|
|
||||||
MessageConfig struct {
|
|
||||||
ActionCard ActionCard
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActionCard action card message struct
|
|
||||||
ActionCard struct {
|
|
||||||
LinkUrls string
|
LinkUrls string
|
||||||
LinkTitles string
|
LinkTitles string
|
||||||
HideAvatar bool
|
HideAvatar bool
|
||||||
BtnOrientation bool
|
BtnOrientation bool
|
||||||
|
PicURL string
|
||||||
|
MsgURL string
|
||||||
|
SuccessPicUrl string
|
||||||
|
FailurePicUrl string
|
||||||
|
SuccessColor string
|
||||||
|
FailureColor string
|
||||||
|
WithColor bool
|
||||||
|
WithPic bool
|
||||||
|
LinkSha bool
|
||||||
|
RetryTime int
|
||||||
}
|
}
|
||||||
|
// plugin all config
|
||||||
// Pic extra config for pic
|
|
||||||
Pic struct {
|
|
||||||
SuccessPicURL string
|
|
||||||
FailurePicURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Color extra config for color
|
|
||||||
Color struct {
|
|
||||||
SuccessColor string
|
|
||||||
FailureColor string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugin plugin all config
|
|
||||||
Plugin struct {
|
Plugin struct {
|
||||||
Tpl Tpl
|
CI CI
|
||||||
Drone Drone
|
Git Git
|
||||||
|
Runner Runner
|
||||||
|
System System
|
||||||
|
Commit Commit
|
||||||
|
Repo Repo
|
||||||
|
Build Build
|
||||||
Config Config
|
Config Config
|
||||||
Custom Custom
|
WebHook *WebHook
|
||||||
Message MessageConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom user custom env
|
|
||||||
Custom struct {
|
|
||||||
Tpl string
|
|
||||||
Color Color
|
|
||||||
Pic Pic
|
|
||||||
Consuming Consuming
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tpl base
|
|
||||||
Tpl struct {
|
|
||||||
Repo TplRepo
|
|
||||||
Commit TplCommit
|
|
||||||
Build TplBuild
|
|
||||||
}
|
|
||||||
|
|
||||||
// TplRepo TPL repo
|
|
||||||
TplRepo struct {
|
|
||||||
FullName string
|
|
||||||
ShortName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TplCommit TPL commit
|
|
||||||
TplCommit struct {
|
|
||||||
Branch string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TplBuild TPL build
|
|
||||||
TplBuild struct {
|
|
||||||
Status Status
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status status
|
|
||||||
Status struct {
|
|
||||||
Success string
|
|
||||||
Failure string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consuming custom consuming env
|
|
||||||
Consuming struct {
|
|
||||||
StartedEnv string
|
|
||||||
FinishedEnv string
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Exec execute WebHook
|
|
||||||
func (p *Plugin) Exec() error {
|
func (p *Plugin) Exec() error {
|
||||||
if p.Config.Debug {
|
log.Println("start execute sending...")
|
||||||
for _, e := range os.Environ() {
|
if 0 == len(p.Config.AccessToken) {
|
||||||
log.Println(e)
|
msg := "missing dingtalk access token"
|
||||||
}
|
log.Println(msg)
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if "" == p.Config.AccessToken {
|
|
||||||
msg := "missing DingTalk access token"
|
|
||||||
return errors.New(msg)
|
return errors.New(msg)
|
||||||
}
|
}
|
||||||
|
log.Println("access token pass...")
|
||||||
|
p.WebHook = NewWebHook(p.Config.AccessToken)
|
||||||
|
mobiles := strings.Split(p.Config.Mobiles, ",")
|
||||||
|
linkUrls := strings.Split(p.Config.LinkUrls, ",")
|
||||||
|
linkTitles := strings.Split(p.Config.LinkTitles, ",")
|
||||||
|
log.Println("sending message type: " + p.Config.MsgType)
|
||||||
|
var err error
|
||||||
|
retryTime := 1
|
||||||
|
for retryTime <= p.Config.RetryTime {
|
||||||
|
log.Printf("start a %d try", retryTime)
|
||||||
|
switch strings.ToLower(p.Config.MsgType) {
|
||||||
|
case "markdown":
|
||||||
|
err = p.WebHook.SendMarkdownMsg(
|
||||||
|
"You have a new message...",
|
||||||
|
p.baseTpl(),
|
||||||
|
p.Config.IsAtALL,
|
||||||
|
mobiles...
|
||||||
|
)
|
||||||
|
case "text":
|
||||||
|
err = p.WebHook.SendTextMsg(p.baseTpl(), p.Config.IsAtALL, mobiles...)
|
||||||
|
case "actioncard":
|
||||||
|
err = p.WebHook.SendActionCardMsg(
|
||||||
|
"A actionCard title",
|
||||||
|
p.baseTpl(),
|
||||||
|
linkUrls,
|
||||||
|
linkTitles,
|
||||||
|
p.Config.HideAvatar,
|
||||||
|
p.Config.BtnOrientation,
|
||||||
|
)
|
||||||
|
case "link":
|
||||||
|
err = p.WebHook.SendLinkMsg(p.Build.Status, p.baseTpl(), p.Commit.Authors.Avatar, p.Build.Link)
|
||||||
|
default:
|
||||||
|
err = errors.New("not support message type")
|
||||||
|
}
|
||||||
|
|
||||||
tpl, err := p.getMessage()
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
if err != nil {
|
retryTime++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nil != err {
|
||||||
|
log.Println(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log.Println("send " + p.Config.MsgType + " message success!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if p.Config.TipsTitle == "" {
|
func (p *Plugin) markdownTpl() string {
|
||||||
p.Config.TipsTitle = "you have a new message"
|
var tpl string
|
||||||
|
|
||||||
|
// title
|
||||||
|
title := fmt.Sprintf(" %s *Branch Build %s*",
|
||||||
|
strings.Title(p.Commit.Branch),
|
||||||
|
strings.Title(p.Build.Status))
|
||||||
|
// with color on title
|
||||||
|
if p.Config.WithColor {
|
||||||
|
title = fmt.Sprintf("<font color=%s>%s</font>", p.getColor(), title)
|
||||||
}
|
}
|
||||||
|
|
||||||
newWebHook := webhook.NewWebHook(p.Config.AccessToken)
|
tpl = fmt.Sprintf("# %s \n", title)
|
||||||
|
|
||||||
// add sign
|
// with pic
|
||||||
if "" != p.Config.Secret {
|
if p.Config.WithPic {
|
||||||
newWebHook.Secret = p.Config.Secret
|
tpl += fmt.Sprintf("\n\n",
|
||||||
|
p.Build.Status,
|
||||||
|
p.getPicUrl())
|
||||||
}
|
}
|
||||||
|
|
||||||
mobiles := strings.Split(p.Config.Mobiles, ",")
|
// commit message
|
||||||
|
commitMsg := fmt.Sprintf("%s", p.Commit.Message)
|
||||||
|
if p.Config.WithColor {
|
||||||
|
commitMsg = fmt.Sprintf("<font color=%s>%s</font>", p.getColor(), commitMsg)
|
||||||
|
}
|
||||||
|
tpl += commitMsg + "\n\n"
|
||||||
|
|
||||||
|
// sha info
|
||||||
|
commitSha := p.Commit.Sha
|
||||||
|
if p.Config.LinkSha {
|
||||||
|
commitSha = fmt.Sprintf("[Click To %s Commit Detail Page](%s)", commitSha[:6], p.Commit.Link)
|
||||||
|
}
|
||||||
|
tpl += commitSha + "\n\n"
|
||||||
|
|
||||||
|
// author info
|
||||||
|
authorInfo := fmt.Sprintf("`%s(%s)`", p.Commit.Authors.Name, p.Commit.Authors.Email)
|
||||||
|
tpl += authorInfo + "\n\n"
|
||||||
|
|
||||||
|
// build detail link
|
||||||
|
buildDetail := fmt.Sprintf("[Click To The Build Detail Page %s](%s)",
|
||||||
|
p.getEmoticon(),
|
||||||
|
p.Build.Link)
|
||||||
|
tpl += buildDetail
|
||||||
|
return tpl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) baseTpl() string {
|
||||||
|
tpl := ""
|
||||||
switch strings.ToLower(p.Config.MsgType) {
|
switch strings.ToLower(p.Config.MsgType) {
|
||||||
case "markdown":
|
case "markdown":
|
||||||
err = newWebHook.SendMarkdownMsg(p.Config.TipsTitle, tpl, p.Config.IsAtALL, mobiles...)
|
tpl = p.markdownTpl()
|
||||||
case "text":
|
case "text":
|
||||||
err = newWebHook.SendTextMsg(tpl, p.Config.IsAtALL, mobiles...)
|
tpl = fmt.Sprintf(`[%s] %s
|
||||||
|
%s (%s)
|
||||||
|
@%s
|
||||||
|
%s (%s)
|
||||||
|
`,
|
||||||
|
p.Build.Status,
|
||||||
|
strings.TrimSpace(p.Commit.Message),
|
||||||
|
p.Repo.FullName,
|
||||||
|
p.Commit.Branch,
|
||||||
|
p.Commit.Sha,
|
||||||
|
p.Commit.Authors.Name,
|
||||||
|
p.Commit.Authors.Email)
|
||||||
case "link":
|
case "link":
|
||||||
err = newWebHook.SendLinkMsg(p.Drone.Build.Status, tpl, p.Drone.Commit.Author.Avatar, p.Drone.Build.Link)
|
tpl = fmt.Sprintf(`%s(%s) @%s %s(%s)`,
|
||||||
default:
|
p.Repo.FullName,
|
||||||
msg := "not support message type"
|
p.Commit.Branch,
|
||||||
err = errors.New(msg)
|
p.Commit.Sha[:6],
|
||||||
}
|
p.Commit.Authors.Name,
|
||||||
|
p.Commit.Authors.Email)
|
||||||
|
case "actionCard":
|
||||||
|
// coming soon
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
log.Println("send message success!")
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fileExists check file is exists
|
|
||||||
func fileExists(filePath string) bool {
|
|
||||||
_, err := os.Stat(filePath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsExist(err) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// getTpl get tpl from local file or remote file
|
|
||||||
func (p *Plugin) getTpl() (tpl string, err error) {
|
|
||||||
//var tpl string
|
|
||||||
tplDir := "/app/drone/dingtalk/message/tpls"
|
|
||||||
if "" == p.Custom.Tpl {
|
|
||||||
p.Custom.Tpl = fmt.Sprintf("%s/%s.tpl", tplDir, strings.ToLower(p.Config.MsgType))
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(p.Custom.Tpl)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Scheme != "" {
|
|
||||||
resp, err := http.Get(p.Custom.Tpl)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check response
|
|
||||||
if u.Path != resp.Request.URL.Path {
|
|
||||||
return "", errors.New("cannot get tpl from url")
|
|
||||||
}
|
|
||||||
|
|
||||||
// defer close
|
|
||||||
defer func() {
|
|
||||||
_ = resp.Body.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl = string(body)
|
|
||||||
} else {
|
|
||||||
if !fileExists(p.Custom.Tpl) {
|
|
||||||
// it must be a tpl stream
|
|
||||||
return p.Custom.Tpl, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tplStr, err := ioutil.ReadFile(p.Custom.Tpl)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl = string(tplStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tpl, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fillTpl fill the tpl by valid keyword
|
|
||||||
func (p *Plugin) fillTpl(tpl string) string {
|
|
||||||
envs := p.getEnvs()
|
|
||||||
// replace regex
|
|
||||||
reg := regexp.MustCompile(`\[([^\[\]]*)]`)
|
|
||||||
match := reg.FindAllStringSubmatch(tpl, -1)
|
|
||||||
for _, m := range match {
|
|
||||||
// from environment
|
|
||||||
if envStr := os.Getenv(m[1]); envStr != "" {
|
|
||||||
tpl = strings.ReplaceAll(tpl, m[0], envStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the keyword is legal
|
|
||||||
if _, ok := envs[m[1]]; ok {
|
|
||||||
// replace keyword
|
|
||||||
tpl = strings.ReplaceAll(tpl, m[0], envs[m[1]].(string))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tpl
|
return tpl
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEnvs get available envs
|
/**
|
||||||
func (p *Plugin) getEnvs() map[string]interface{} {
|
get emoticon
|
||||||
var envs map[string]interface{}
|
*/
|
||||||
envs = make(map[string]interface{})
|
|
||||||
envs["TPL_REPO_FULL_NAME"] = p.Drone.Repo.FullName
|
|
||||||
if p.Tpl.Repo.FullName != "" {
|
|
||||||
envs["TPL_REPO_FULL_NAME"] = p.Tpl.Repo.FullName
|
|
||||||
}
|
|
||||||
envs["TPL_REPO_SHORT_NAME"] = p.Drone.Repo.ShortName
|
|
||||||
if p.Tpl.Repo.ShortName != "" {
|
|
||||||
envs["TPL_REPO_SHORT_NAME"] = p.Tpl.Repo.ShortName
|
|
||||||
}
|
|
||||||
envs["TPL_REPO_GROUP_NAME"] = p.Drone.Repo.GroupName
|
|
||||||
envs["TPL_REPO_OWNER_NAME"] = p.Drone.Repo.OwnerName
|
|
||||||
envs["TPL_REPO_REMOTE_URL"] = p.Drone.Repo.RemoteURL
|
|
||||||
|
|
||||||
envs["TPL_BUILD_STATUS"] = p.getStatus()
|
|
||||||
envs["TPL_BUILD_LINK"] = p.Drone.Build.Link
|
|
||||||
envs["TPL_BUILD_EVENT"] = p.Drone.Build.Event
|
|
||||||
|
|
||||||
var consuming uint64
|
|
||||||
// custom consuming env
|
|
||||||
if p.Custom.Consuming.FinishedEnv != "" && p.Custom.Consuming.StartedEnv != "" {
|
|
||||||
finishedAt, _ := strconv.ParseUint(os.Getenv(p.Custom.Consuming.FinishedEnv), 10, 64)
|
|
||||||
startedAt, _ := strconv.ParseUint(os.Getenv(p.Custom.Consuming.StartedEnv), 10, 64)
|
|
||||||
consuming = finishedAt - startedAt
|
|
||||||
} else {
|
|
||||||
consuming = p.Drone.Build.FinishedAt - p.Drone.Build.StartAt
|
|
||||||
if consuming == 0 {
|
|
||||||
consuming = p.Drone.Stage.FinishedAt - p.Drone.Stage.StartedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
envs["TPL_BUILD_CONSUMING"] = fmt.Sprintf("%v", consuming)
|
|
||||||
|
|
||||||
envs["TPL_COMMIT_SHA"] = p.Drone.Commit.Sha
|
|
||||||
envs["TPL_COMMIT_REF"] = p.Drone.Commit.Ref
|
|
||||||
envs["TPL_COMMIT_LINK"] = p.Drone.Commit.Link
|
|
||||||
envs["TPL_COMMIT_MSG"] = p.Drone.Commit.Message
|
|
||||||
envs["TPL_COMMIT_BRANCH"] = p.Drone.Commit.Branch
|
|
||||||
if p.Tpl.Commit.Branch != "" {
|
|
||||||
envs["TPL_COMMIT_BRANCH"] = p.Tpl.Commit.Branch
|
|
||||||
}
|
|
||||||
|
|
||||||
envs["TPL_AUTHOR_NAME"] = p.Drone.Commit.Author.Name
|
|
||||||
envs["TPL_AUTHOR_USERNAME"] = p.Drone.Commit.Author.Username
|
|
||||||
envs["TPL_AUTHOR_EMAIL"] = p.Drone.Commit.Author.Email
|
|
||||||
envs["TPL_AUTHOR_AVATAR"] = p.Drone.Commit.Author.Avatar
|
|
||||||
|
|
||||||
envs["TPL_STATUS_PIC"] = p.getPicURL()
|
|
||||||
envs["TPL_STATUS_COLOR"] = p.getColor()
|
|
||||||
envs["TPL_STATUS_EMOTICON"] = p.getEmoticon()
|
|
||||||
|
|
||||||
return envs
|
|
||||||
}
|
|
||||||
|
|
||||||
// getMessage get message tpl
|
|
||||||
func (p *Plugin) getMessage() (tpl string, err error) {
|
|
||||||
tpl, err = p.getTpl()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return p.fillTpl(tpl), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getStatus
|
|
||||||
func (p *Plugin) getStatus() string {
|
|
||||||
if p.Drone.Build.Status == "success" {
|
|
||||||
if p.Tpl.Build.Status.Success != "" {
|
|
||||||
return p.Tpl.Build.Status.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.Drone.Build.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Tpl.Build.Status.Failure != "" {
|
|
||||||
return p.Tpl.Build.Status.Failure
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.Drone.Build.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
// get emoticon
|
|
||||||
func (p *Plugin) getEmoticon() string {
|
func (p *Plugin) getEmoticon() string {
|
||||||
emoticons := make(map[string]string)
|
emoticons := make(map[string]string)
|
||||||
emoticons["success"] = ":)"
|
emoticons["success"] = ":)"
|
||||||
emoticons["failure"] = ":("
|
emoticons["failure"] = ":("
|
||||||
|
|
||||||
emoticon, ok := emoticons[p.Drone.Build.Status]
|
emoticon, ok := emoticons[p.Build.Status]
|
||||||
if ok {
|
if ok {
|
||||||
return emoticon
|
return emoticon
|
||||||
}
|
}
|
||||||
@@ -387,50 +261,47 @@ func (p *Plugin) getEmoticon() string {
|
|||||||
return ":("
|
return ":("
|
||||||
}
|
}
|
||||||
|
|
||||||
// get picture url
|
/**
|
||||||
func (p *Plugin) getPicURL() string {
|
get picture url
|
||||||
|
*/
|
||||||
|
func (p *Plugin) getPicUrl() string {
|
||||||
pics := make(map[string]string)
|
pics := make(map[string]string)
|
||||||
// success picture url
|
// success picture url
|
||||||
pics["success"] = "https://wx1.sinaimg.cn/large/006tNc79gy1fz05g5a7utj30he0bfjry.jpg"
|
pics["success"] = "https://ws4.sinaimg.cn/large/006tNc79gy1fz05g5a7utj30he0bfjry.jpg"
|
||||||
if p.Custom.Pic.SuccessPicURL != "" {
|
if p.Config.SuccessPicUrl != "" {
|
||||||
pics["success"] = p.Custom.Pic.SuccessPicURL
|
pics["success"] = p.Config.SuccessPicUrl
|
||||||
}
|
}
|
||||||
// failure picture url
|
// failure picture url
|
||||||
pics["failure"] = "https://wx1.sinaimg.cn/large/006tNc79gy1fz0b4fghpnj30hd0bdmxn.jpg"
|
pics["failure"] = "https://ws1.sinaimg.cn/large/006tNc79gy1fz0b4fghpnj30hd0bdmxn.jpg"
|
||||||
if p.Custom.Pic.FailurePicURL != "" {
|
if p.Config.FailurePicUrl != "" {
|
||||||
pics["failure"] = p.Custom.Pic.FailurePicURL
|
pics["failure"] = p.Config.FailurePicUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
picURL, ok := pics[p.Drone.Build.Status]
|
url, ok := pics[p.Build.Status]
|
||||||
if ok {
|
if ok {
|
||||||
return picURL
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// get color for message title
|
/**
|
||||||
|
get color for message title
|
||||||
|
*/
|
||||||
func (p *Plugin) getColor() string {
|
func (p *Plugin) getColor() string {
|
||||||
colors := make(map[string]string)
|
colors := make(map[string]string)
|
||||||
// success color
|
// success color
|
||||||
colors["success"] = "#008000"
|
colors["success"] = "#008000"
|
||||||
if p.Custom.Color.SuccessColor != "" {
|
if p.Config.SuccessColor != "" {
|
||||||
if p.Custom.Color.SuccessColor[0] != '#' {
|
colors["success"] = "#" + p.Config.SuccessColor
|
||||||
p.Custom.Color.SuccessColor = "#" + p.Custom.Color.SuccessColor
|
|
||||||
}
|
|
||||||
colors["success"] = p.Custom.Color.SuccessColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// failure color
|
// failure color
|
||||||
colors["failure"] = "#FF0000"
|
colors["failure"] = "#FF0000"
|
||||||
if p.Custom.Color.FailureColor != "" {
|
if p.Config.FailureColor != "" {
|
||||||
if p.Custom.Color.FailureColor[0] != '#' {
|
colors["failure"] = "#" + p.Config.FailureColor
|
||||||
p.Custom.Color.FailureColor = "#" + p.Custom.Color.FailureColor
|
|
||||||
}
|
|
||||||
colors["failure"] = p.Custom.Color.FailureColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
color, ok := colors[p.Drone.Build.Status]
|
color, ok := colors[p.Build.Status]
|
||||||
if ok {
|
if ok {
|
||||||
return color
|
return color
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPlugin(t *testing.T) {
|
|
||||||
p := Plugin{}
|
|
||||||
err := p.Exec()
|
|
||||||
if nil == err {
|
|
||||||
t.Error("access token empty error should be catch!")
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Config.AccessToken = "example-access-token"
|
|
||||||
p.Custom.Tpl = "tpls/markdown.tpl"
|
|
||||||
err = p.Exec()
|
|
||||||
if nil == err {
|
|
||||||
t.Error("not support message type error should be catch!")
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Config.MsgType = "link"
|
|
||||||
err = p.Exec()
|
|
||||||
if nil == err {
|
|
||||||
t.Error("access token invalid error should be catch!")
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Custom.Tpl = "https://aaa.com"
|
|
||||||
p.Config.MsgType = "text"
|
|
||||||
err = p.Exec()
|
|
||||||
if nil == err {
|
|
||||||
t.Error("access token invalid error should be catch!")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
p.Custom.Tpl = ""
|
|
||||||
p.Config.MsgType = "link"
|
|
||||||
err = p.Exec()
|
|
||||||
if nil == err {
|
|
||||||
t.Error("access token invalid error should be catch!")
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Custom.Color.FailureColor = "#555555"
|
|
||||||
p.Custom.Color.SuccessColor = "#222222"
|
|
||||||
p.Custom.Pic.FailurePicURL = "https://www.baidu.com"
|
|
||||||
p.Custom.Pic.SuccessPicURL = "https://www.baidu.com"
|
|
||||||
p.Config.MsgType = "markdown"
|
|
||||||
p.Custom.Tpl = "tpls/markdown.tpl"
|
|
||||||
err = p.Exec()
|
|
||||||
if nil == err {
|
|
||||||
t.Error("access token invalid error should be catch!")
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Custom.Tpl = "https://gist.githubusercontent.com/lddsb/87065e73678dcf56cd222a3c2f1f32b0/raw/fce9fb28b2c8c768eb93df5598beee8c98cba610/md.tpl"
|
|
||||||
p.Drone.Build.Status = "failure"
|
|
||||||
err = p.Exec()
|
|
||||||
if nil == err {
|
|
||||||
t.Error("access token invalid error should be catch!")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("plugin testing finished")
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
### **[CI_PROJECT_TITLE]**'s **[CI_COMMIT_BRANCH]** build **[TPL_BUILD_STATUS]**
|
|
||||||
|
|
||||||
Message: [CI_COMMIT_MESSAGE]
|
|
||||||
|
|
||||||
Detail: [[CI_COMMIT_SHA]]([CI_PROJECT_URL]/commit/[CI_COMMIT_SHA])
|
|
||||||
|
|
||||||
Author: [[GITLAB_USER_NAME]([GITLAB_USER_EMAIL])](mailto:[GITLAB_USER_EMAIL])
|
|
||||||
|
|
||||||
[Click To The Build Detail Page [TPL_STATUS_EMOTICON]]([CI_PIPELINE_URL])
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
### [TPL_REPO_SHORT_NAME] build [TPL_BUILD_STATUS] (`takes [TPL_BUILD_CONSUMING]s`)
|
|
||||||
|
|
||||||
Message: [TPL_COMMIT_MSG]
|
|
||||||
|
|
||||||
Detail: [[TPL_COMMIT_SHA]]([TPL_COMMIT_LINK])
|
|
||||||
|
|
||||||
Author: [[TPL_AUTHOR_NAME]([TPL_AUTHOR_EMAIL])](mailto:[TPL_AUTHOR_EMAIL])
|
|
||||||
|
|
||||||
[Click To The Build Detail Page [TPL_STATUS_EMOTICON]]([TPL_BUILD_LINK])
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
### [TPL_REPO_SHORT_NAME] build [TPL_BUILD_STATUS] (`takes [TPL_BUILD_CONSUMING]s`)
|
|
||||||
|
|
||||||
@mobile1 @mobile2
|
|
||||||
|
|
||||||
Message: [TPL_COMMIT_MSG]
|
|
||||||
|
|
||||||
Detail: [[TPL_COMMIT_SHA]]([TPL_COMMIT_LINK])
|
|
||||||
|
|
||||||
Author: [[TPL_AUTHOR_NAME]([TPL_AUTHOR_EMAIL])](mailto:[TPL_AUTHOR_EMAIL])
|
|
||||||
|
|
||||||
[Click To The Build Detail Page [TPL_STATUS_EMOTICON]]([TPL_BUILD_LINK])
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
[TPL_REPO_NAME] build [TPL_BUILD_STATUS] (takes [TPL_BUILD_CONSUMING]s)
|
|
||||||
[TPL_COMMIT_MSG]
|
|
||||||
[TPL_COMMIT_SHA] ([TPL_COMMIT_LINK])
|
|
||||||
[TPL_AUTHOR_NAME] ([TPL_AUTHOR_EMAIL])
|
|
||||||
Click To The Build Detail Page [TPL_STATUS_EMOTICON] ([TPL_BUILD_LINK])
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.x
|
||||||
|
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- osx
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
Copyright (c) 2013 John Barton
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
+163
@@ -0,0 +1,163 @@
|
|||||||
|
# GoDotEnv [](https://travis-ci.org/joho/godotenv) [](https://ci.appveyor.com/project/joho/godotenv) [](https://goreportcard.com/report/github.com/joho/godotenv)
|
||||||
|
|
||||||
|
A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file)
|
||||||
|
|
||||||
|
From the original Library:
|
||||||
|
|
||||||
|
> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables.
|
||||||
|
>
|
||||||
|
> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped.
|
||||||
|
|
||||||
|
It can be used as a library (for loading in env for your own daemons etc) or as a bin command.
|
||||||
|
|
||||||
|
There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
As a library
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go get github.com/joho/godotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
or if you want to use it as a bin command
|
||||||
|
```shell
|
||||||
|
go get github.com/joho/godotenv/cmd/godotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Add your application configuration to your `.env` file in the root of your project:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
S3_BUCKET=YOURS3BUCKET
|
||||||
|
SECRET_KEY=YOURSECRETKEYGOESHERE
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in your Go app you can do something like
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error loading .env file")
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Bucket := os.Getenv("S3_BUCKET")
|
||||||
|
secretKey := os.Getenv("SECRET_KEY")
|
||||||
|
|
||||||
|
// now do something with s3 or whatever
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import
|
||||||
|
|
||||||
|
```go
|
||||||
|
import _ "github.com/joho/godotenv/autoload"
|
||||||
|
```
|
||||||
|
|
||||||
|
While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit
|
||||||
|
|
||||||
|
```go
|
||||||
|
_ = godotenv.Load("somerandomfile")
|
||||||
|
_ = godotenv.Load("filenumberone.env", "filenumbertwo.env")
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to be really fancy with your env file you can do comments and exports (below is a valid env file)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# I am a comment and that is OK
|
||||||
|
SOME_VAR=someval
|
||||||
|
FOO=BAR # comments at line end are OK too
|
||||||
|
export BAR=BAZ
|
||||||
|
```
|
||||||
|
|
||||||
|
Or finally you can do YAML(ish) style
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
FOO: bar
|
||||||
|
BAR: baz
|
||||||
|
```
|
||||||
|
|
||||||
|
as a final aside, if you don't want godotenv munging your env you can just get a map back instead
|
||||||
|
|
||||||
|
```go
|
||||||
|
var myEnv map[string]string
|
||||||
|
myEnv, err := godotenv.Read()
|
||||||
|
|
||||||
|
s3Bucket := myEnv["S3_BUCKET"]
|
||||||
|
```
|
||||||
|
|
||||||
|
... or from an `io.Reader` instead of a local file
|
||||||
|
|
||||||
|
```go
|
||||||
|
reader := getRemoteFile()
|
||||||
|
myEnv, err := godotenv.Parse(reader)
|
||||||
|
```
|
||||||
|
|
||||||
|
... or from a `string` if you so desire
|
||||||
|
|
||||||
|
```go
|
||||||
|
content := getRemoteFileContent()
|
||||||
|
myEnv, err := godotenv.Unmarshal(content)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Mode
|
||||||
|
|
||||||
|
Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`
|
||||||
|
|
||||||
|
```
|
||||||
|
godotenv -f /some/path/to/.env some_command with some args
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD`
|
||||||
|
|
||||||
|
### Writing Env Files
|
||||||
|
|
||||||
|
Godotenv can also write a map representing the environment to a correctly-formatted and escaped file
|
||||||
|
|
||||||
|
```go
|
||||||
|
env, err := godotenv.Unmarshal("KEY=value")
|
||||||
|
err := godotenv.Write(env, "./.env")
|
||||||
|
```
|
||||||
|
|
||||||
|
... or to a string
|
||||||
|
|
||||||
|
```go
|
||||||
|
env, err := godotenv.Unmarshal("KEY=value")
|
||||||
|
content, err := godotenv.Marshal(env)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases.
|
||||||
|
|
||||||
|
*code changes without tests will not be accepted*
|
||||||
|
|
||||||
|
1. Fork it
|
||||||
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||||
|
3. Commit your changes (`git commit -am 'Added some feature'`)
|
||||||
|
4. Push to the branch (`git push origin my-new-feature`)
|
||||||
|
5. Create new Pull Request
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`.
|
||||||
|
|
||||||
|
Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1`
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
Linux: [](https://travis-ci.org/joho/godotenv) Windows: [](https://ci.appveyor.com/project/joho/godotenv)
|
||||||
|
|
||||||
|
## Who?
|
||||||
|
|
||||||
|
The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library.
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package autoload
|
||||||
|
|
||||||
|
/*
|
||||||
|
You can just read the .env file on import just by doing
|
||||||
|
|
||||||
|
import _ "github.com/joho/godotenv/autoload"
|
||||||
|
|
||||||
|
And bob's your mother's brother
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "github.com/joho/godotenv"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
godotenv.Load()
|
||||||
|
}
|
||||||
+346
@@ -0,0 +1,346 @@
|
|||||||
|
// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)
|
||||||
|
//
|
||||||
|
// Examples/readme can be found on the github page at https://github.com/joho/godotenv
|
||||||
|
//
|
||||||
|
// The TL;DR is that you make a .env file that looks something like
|
||||||
|
//
|
||||||
|
// SOME_ENV_VAR=somevalue
|
||||||
|
//
|
||||||
|
// and then in your go code you can call
|
||||||
|
//
|
||||||
|
// godotenv.Load()
|
||||||
|
//
|
||||||
|
// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR")
|
||||||
|
package godotenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
|
||||||
|
|
||||||
|
// Load will read your env file(s) and load them into ENV for this process.
|
||||||
|
//
|
||||||
|
// Call this function as close as possible to the start of your program (ideally in main)
|
||||||
|
//
|
||||||
|
// If you call Load without any args it will default to loading .env in the current path
|
||||||
|
//
|
||||||
|
// You can otherwise tell it which files to load (there can be more than one) like
|
||||||
|
//
|
||||||
|
// godotenv.Load("fileone", "filetwo")
|
||||||
|
//
|
||||||
|
// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults
|
||||||
|
func Load(filenames ...string) (err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
err = loadFile(filename, false)
|
||||||
|
if err != nil {
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overload will read your env file(s) and load them into ENV for this process.
|
||||||
|
//
|
||||||
|
// Call this function as close as possible to the start of your program (ideally in main)
|
||||||
|
//
|
||||||
|
// If you call Overload without any args it will default to loading .env in the current path
|
||||||
|
//
|
||||||
|
// You can otherwise tell it which files to load (there can be more than one) like
|
||||||
|
//
|
||||||
|
// godotenv.Overload("fileone", "filetwo")
|
||||||
|
//
|
||||||
|
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
|
||||||
|
func Overload(filenames ...string) (err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
err = loadFile(filename, true)
|
||||||
|
if err != nil {
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all env (with same file loading semantics as Load) but return values as
|
||||||
|
// a map rather than automatically writing values into env
|
||||||
|
func Read(filenames ...string) (envMap map[string]string, err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
envMap = make(map[string]string)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
individualEnvMap, individualErr := readFile(filename)
|
||||||
|
|
||||||
|
if individualErr != nil {
|
||||||
|
err = individualErr
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range individualEnvMap {
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse reads an env file from io.Reader, returning a map of keys and values.
|
||||||
|
func Parse(r io.Reader) (envMap map[string]string, err error) {
|
||||||
|
envMap = make(map[string]string)
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
lines = append(lines, scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = scanner.Err(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fullLine := range lines {
|
||||||
|
if !isIgnoredLine(fullLine) {
|
||||||
|
var key, value string
|
||||||
|
key, value, err = parseLine(fullLine, envMap)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Unmarshal reads an env file from a string, returning a map of keys and values.
|
||||||
|
func Unmarshal(str string) (envMap map[string]string, err error) {
|
||||||
|
return Parse(strings.NewReader(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec loads env vars from the specified filenames (empty map falls back to default)
|
||||||
|
// then executes the cmd specified.
|
||||||
|
//
|
||||||
|
// Simply hooks up os.Stdin/err/out to the command and calls Run()
|
||||||
|
//
|
||||||
|
// If you want more fine grained control over your command it's recommended
|
||||||
|
// that you use `Load()` or `Read()` and the `os/exec` package yourself.
|
||||||
|
func Exec(filenames []string, cmd string, cmdArgs []string) error {
|
||||||
|
Load(filenames...)
|
||||||
|
|
||||||
|
command := exec.Command(cmd, cmdArgs...)
|
||||||
|
command.Stdin = os.Stdin
|
||||||
|
command.Stdout = os.Stdout
|
||||||
|
command.Stderr = os.Stderr
|
||||||
|
return command.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write serializes the given environment and writes it to a file
|
||||||
|
func Write(envMap map[string]string, filename string) error {
|
||||||
|
content, error := Marshal(envMap)
|
||||||
|
if error != nil {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
file, error := os.Create(filename)
|
||||||
|
if error != nil {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
_, err := file.WriteString(content)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal outputs the given environment as a dotenv-formatted environment file.
|
||||||
|
// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
|
||||||
|
func Marshal(envMap map[string]string) (string, error) {
|
||||||
|
lines := make([]string, 0, len(envMap))
|
||||||
|
for k, v := range envMap {
|
||||||
|
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
|
||||||
|
}
|
||||||
|
sort.Strings(lines)
|
||||||
|
return strings.Join(lines, "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filenamesOrDefault(filenames []string) []string {
|
||||||
|
if len(filenames) == 0 {
|
||||||
|
return []string{".env"}
|
||||||
|
}
|
||||||
|
return filenames
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFile(filename string, overload bool) error {
|
||||||
|
envMap, err := readFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEnv := map[string]bool{}
|
||||||
|
rawEnv := os.Environ()
|
||||||
|
for _, rawEnvLine := range rawEnv {
|
||||||
|
key := strings.Split(rawEnvLine, "=")[0]
|
||||||
|
currentEnv[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range envMap {
|
||||||
|
if !currentEnv[key] || overload {
|
||||||
|
os.Setenv(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(filename string) (envMap map[string]string, err error) {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
return Parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLine(line string, envMap map[string]string) (key string, value string, err error) {
|
||||||
|
if len(line) == 0 {
|
||||||
|
err = errors.New("zero length string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ditch the comments (but keep quoted hashes)
|
||||||
|
if strings.Contains(line, "#") {
|
||||||
|
segmentsBetweenHashes := strings.Split(line, "#")
|
||||||
|
quotesAreOpen := false
|
||||||
|
var segmentsToKeep []string
|
||||||
|
for _, segment := range segmentsBetweenHashes {
|
||||||
|
if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
|
||||||
|
if quotesAreOpen {
|
||||||
|
quotesAreOpen = false
|
||||||
|
segmentsToKeep = append(segmentsToKeep, segment)
|
||||||
|
} else {
|
||||||
|
quotesAreOpen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(segmentsToKeep) == 0 || quotesAreOpen {
|
||||||
|
segmentsToKeep = append(segmentsToKeep, segment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.Join(segmentsToKeep, "#")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstEquals := strings.Index(line, "=")
|
||||||
|
firstColon := strings.Index(line, ":")
|
||||||
|
splitString := strings.SplitN(line, "=", 2)
|
||||||
|
if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) {
|
||||||
|
//this is a yaml-style line
|
||||||
|
splitString = strings.SplitN(line, ":", 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(splitString) != 2 {
|
||||||
|
err = errors.New("Can't separate key from value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the key
|
||||||
|
key = splitString[0]
|
||||||
|
if strings.HasPrefix(key, "export") {
|
||||||
|
key = strings.TrimPrefix(key, "export")
|
||||||
|
}
|
||||||
|
key = strings.Trim(key, " ")
|
||||||
|
|
||||||
|
// Parse the value
|
||||||
|
value = parseValue(splitString[1], envMap)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseValue(value string, envMap map[string]string) string {
|
||||||
|
|
||||||
|
// trim
|
||||||
|
value = strings.Trim(value, " ")
|
||||||
|
|
||||||
|
// check if we've got quoted values or possible escapes
|
||||||
|
if len(value) > 1 {
|
||||||
|
rs := regexp.MustCompile(`\A'(.*)'\z`)
|
||||||
|
singleQuotes := rs.FindStringSubmatch(value)
|
||||||
|
|
||||||
|
rd := regexp.MustCompile(`\A"(.*)"\z`)
|
||||||
|
doubleQuotes := rd.FindStringSubmatch(value)
|
||||||
|
|
||||||
|
if singleQuotes != nil || doubleQuotes != nil {
|
||||||
|
// pull the quotes off the edges
|
||||||
|
value = value[1 : len(value)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if doubleQuotes != nil {
|
||||||
|
// expand newlines
|
||||||
|
escapeRegex := regexp.MustCompile(`\\.`)
|
||||||
|
value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
|
||||||
|
c := strings.TrimPrefix(match, `\`)
|
||||||
|
switch c {
|
||||||
|
case "n":
|
||||||
|
return "\n"
|
||||||
|
case "r":
|
||||||
|
return "\r"
|
||||||
|
default:
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// unescape characters
|
||||||
|
e := regexp.MustCompile(`\\([^$])`)
|
||||||
|
value = e.ReplaceAllString(value, "$1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if singleQuotes == nil {
|
||||||
|
value = expandVariables(value, envMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandVariables(v string, m map[string]string) string {
|
||||||
|
r := regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
|
||||||
|
|
||||||
|
return r.ReplaceAllStringFunc(v, func(s string) string {
|
||||||
|
submatch := r.FindStringSubmatch(s)
|
||||||
|
|
||||||
|
if submatch == nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if submatch[1] == "\\" || submatch[2] == "(" {
|
||||||
|
return submatch[0][1:]
|
||||||
|
} else if submatch[4] != "" {
|
||||||
|
return m[submatch[4]]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIgnoredLine(line string) bool {
|
||||||
|
trimmedLine := strings.Trim(line, " \n\t")
|
||||||
|
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
|
||||||
|
}
|
||||||
|
|
||||||
|
func doubleQuoteEscape(line string) string {
|
||||||
|
for _, c := range doubleQuoteSpecialChars {
|
||||||
|
toReplace := "\\" + string(c)
|
||||||
|
if c == '\n' {
|
||||||
|
toReplace = `\n`
|
||||||
|
}
|
||||||
|
if c == '\r' {
|
||||||
|
toReplace = `\r`
|
||||||
|
}
|
||||||
|
line = strings.Replace(line, string(c), toReplace, -1)
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
[flake8]
|
||||||
|
max-line-length = 120
|
||||||
Generated
+2
@@ -0,0 +1,2 @@
|
|||||||
|
*.coverprofile
|
||||||
|
node_modules/
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
dist: trusty
|
||||||
|
osx_image: xcode8.3
|
||||||
|
go: 1.8.x
|
||||||
|
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- osx
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- node_modules
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- go get github.com/urfave/gfmrun/... || true
|
||||||
|
- go get golang.org/x/tools/cmd/goimports
|
||||||
|
- if [ ! -f node_modules/.bin/markdown-toc ] ; then
|
||||||
|
npm install markdown-toc ;
|
||||||
|
fi
|
||||||
|
|
||||||
|
script:
|
||||||
|
- ./runtests gen
|
||||||
|
- ./runtests vet
|
||||||
|
- ./runtests test
|
||||||
|
- ./runtests gfmrun
|
||||||
|
- ./runtests toc
|
||||||
+435
@@ -0,0 +1,435 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
**ATTN**: This project uses [semantic versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## 1.20.0 - 2017-08-10
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* `HandleExitCoder` is now correctly iterates over all errors in
|
||||||
|
a `MultiError`. The exit code is the exit code of the last error or `1` if
|
||||||
|
there are no `ExitCoder`s in the `MultiError`.
|
||||||
|
* Fixed YAML file loading on Windows (previously would fail validate the file path)
|
||||||
|
* Subcommand `Usage`, `Description`, `ArgsUsage`, `OnUsageError` correctly
|
||||||
|
propogated
|
||||||
|
* `ErrWriter` is now passed downwards through command structure to avoid the
|
||||||
|
need to redefine it
|
||||||
|
* Pass `Command` context into `OnUsageError` rather than parent context so that
|
||||||
|
all fields are avaiable
|
||||||
|
* Errors occuring in `Before` funcs are no longer double printed
|
||||||
|
* Use `UsageText` in the help templates for commands and subcommands if
|
||||||
|
defined; otherwise build the usage as before (was previously ignoring this
|
||||||
|
field)
|
||||||
|
* `IsSet` and `GlobalIsSet` now correctly return whether a flag is set if
|
||||||
|
a program calls `Set` or `GlobalSet` directly after flag parsing (would
|
||||||
|
previously only return `true` if the flag was set during parsing)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* No longer exit the program on command/subcommand error if the error raised is
|
||||||
|
not an `OsExiter`. This exiting behavior was introduced in 1.19.0, but was
|
||||||
|
determined to be a regression in functionality. See [the
|
||||||
|
PR](https://github.com/urfave/cli/pull/595) for discussion.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* `CommandsByName` type was added to make it easy to sort `Command`s by name,
|
||||||
|
alphabetically
|
||||||
|
* `altsrc` now handles loading of string and int arrays from TOML
|
||||||
|
* Support for definition of custom help templates for `App` via
|
||||||
|
`CustomAppHelpTemplate`
|
||||||
|
* Support for arbitrary key/value fields on `App` to be used with
|
||||||
|
`CustomAppHelpTemplate` via `ExtraInfo`
|
||||||
|
* `HelpFlag`, `VersionFlag`, and `BashCompletionFlag` changed to explictly be
|
||||||
|
`cli.Flag`s allowing for the use of custom flags satisfying the `cli.Flag`
|
||||||
|
interface to be used.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.19.1] - 2016-11-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixes regression introduced in 1.19.0 where using an `ActionFunc` as
|
||||||
|
the `Action` for a command would cause it to error rather than calling the
|
||||||
|
function. Should not have a affected declarative cases using `func(c
|
||||||
|
*cli.Context) err)`.
|
||||||
|
- Shell completion now handles the case where the user specifies
|
||||||
|
`--generate-bash-completion` immediately after a flag that takes an argument.
|
||||||
|
Previously it call the application with `--generate-bash-completion` as the
|
||||||
|
flag value.
|
||||||
|
|
||||||
|
## [1.19.0] - 2016-11-19
|
||||||
|
### Added
|
||||||
|
- `FlagsByName` was added to make it easy to sort flags (e.g. `sort.Sort(cli.FlagsByName(app.Flags))`)
|
||||||
|
- A `Description` field was added to `App` for a more detailed description of
|
||||||
|
the application (similar to the existing `Description` field on `Command`)
|
||||||
|
- Flag type code generation via `go generate`
|
||||||
|
- Write to stderr and exit 1 if action returns non-nil error
|
||||||
|
- Added support for TOML to the `altsrc` loader
|
||||||
|
- `SkipArgReorder` was added to allow users to skip the argument reordering.
|
||||||
|
This is useful if you want to consider all "flags" after an argument as
|
||||||
|
arguments rather than flags (the default behavior of the stdlib `flag`
|
||||||
|
library). This is backported functionality from the [removal of the flag
|
||||||
|
reordering](https://github.com/urfave/cli/pull/398) in the unreleased version
|
||||||
|
2
|
||||||
|
- For formatted errors (those implementing `ErrorFormatter`), the errors will
|
||||||
|
be formatted during output. Compatible with `pkg/errors`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Raise minimum tested/supported Go version to 1.2+
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Consider empty environment variables as set (previously environment variables
|
||||||
|
with the equivalent of `""` would be skipped rather than their value used).
|
||||||
|
- Return an error if the value in a given environment variable cannot be parsed
|
||||||
|
as the flag type. Previously these errors were silently swallowed.
|
||||||
|
- Print full error when an invalid flag is specified (which includes the invalid flag)
|
||||||
|
- `App.Writer` defaults to `stdout` when `nil`
|
||||||
|
- If no action is specified on a command or app, the help is now printed instead of `panic`ing
|
||||||
|
- `App.Metadata` is initialized automatically now (previously was `nil` unless initialized)
|
||||||
|
- Correctly show help message if `-h` is provided to a subcommand
|
||||||
|
- `context.(Global)IsSet` now respects environment variables. Previously it
|
||||||
|
would return `false` if a flag was specified in the environment rather than
|
||||||
|
as an argument
|
||||||
|
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
|
||||||
|
- `altsrc`s import paths were updated to use `gopkg.in/urfave/cli.v1`. This
|
||||||
|
fixes issues that occurred when `gopkg.in/urfave/cli.v1` was imported as well
|
||||||
|
as `altsrc` where Go would complain that the types didn't match
|
||||||
|
|
||||||
|
## [1.18.1] - 2016-08-28
|
||||||
|
### Fixed
|
||||||
|
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user (backported)
|
||||||
|
|
||||||
|
## [1.18.0] - 2016-06-27
|
||||||
|
### Added
|
||||||
|
- `./runtests` test runner with coverage tracking by default
|
||||||
|
- testing on OS X
|
||||||
|
- testing on Windows
|
||||||
|
- `UintFlag`, `Uint64Flag`, and `Int64Flag` types and supporting code
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use spaces for alignment in help/usage output instead of tabs, making the
|
||||||
|
output alignment consistent regardless of tab width
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Printing of command aliases in help text
|
||||||
|
- Printing of visible flags for both struct and struct pointer flags
|
||||||
|
- Display the `help` subcommand when using `CommandCategories`
|
||||||
|
- No longer swallows `panic`s that occur within the `Action`s themselves when
|
||||||
|
detecting the signature of the `Action` field
|
||||||
|
|
||||||
|
## [1.17.1] - 2016-08-28
|
||||||
|
### Fixed
|
||||||
|
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
|
||||||
|
|
||||||
|
## [1.17.0] - 2016-05-09
|
||||||
|
### Added
|
||||||
|
- Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc`
|
||||||
|
- `context.GlobalBoolT` was added as an analogue to `context.GlobalBool`
|
||||||
|
- Support for hiding commands by setting `Hidden: true` -- this will hide the
|
||||||
|
commands in help output
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer
|
||||||
|
quoted in help text output.
|
||||||
|
- All flag types now include `(default: {value})` strings following usage when a
|
||||||
|
default value can be (reasonably) detected.
|
||||||
|
- `IntSliceFlag` and `StringSliceFlag` usage strings are now more consistent
|
||||||
|
with non-slice flag types
|
||||||
|
- Apps now exit with a code of 3 if an unknown subcommand is specified
|
||||||
|
(previously they printed "No help topic for...", but still exited 0. This
|
||||||
|
makes it easier to script around apps built using `cli` since they can trust
|
||||||
|
that a 0 exit code indicated a successful execution.
|
||||||
|
- cleanups based on [Go Report Card
|
||||||
|
feedback](https://goreportcard.com/report/github.com/urfave/cli)
|
||||||
|
|
||||||
|
## [1.16.1] - 2016-08-28
|
||||||
|
### Fixed
|
||||||
|
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
|
||||||
|
|
||||||
|
## [1.16.0] - 2016-05-02
|
||||||
|
### Added
|
||||||
|
- `Hidden` field on all flag struct types to omit from generated help text
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `BashCompletionFlag` (`--enable-bash-completion`) is now omitted from
|
||||||
|
generated help text via the `Hidden` field
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- handling of error values in `HandleAction` and `HandleExitCoder`
|
||||||
|
|
||||||
|
## [1.15.0] - 2016-04-30
|
||||||
|
### Added
|
||||||
|
- This file!
|
||||||
|
- Support for placeholders in flag usage strings
|
||||||
|
- `App.Metadata` map for arbitrary data/state management
|
||||||
|
- `Set` and `GlobalSet` methods on `*cli.Context` for altering values after
|
||||||
|
parsing.
|
||||||
|
- Support for nested lookup of dot-delimited keys in structures loaded from
|
||||||
|
YAML.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- The `App.Action` and `Command.Action` now prefer a return signature of
|
||||||
|
`func(*cli.Context) error`, as defined by `cli.ActionFunc`. If a non-nil
|
||||||
|
`error` is returned, there may be two outcomes:
|
||||||
|
- If the error fulfills `cli.ExitCoder`, then `os.Exit` will be called
|
||||||
|
automatically
|
||||||
|
- Else the error is bubbled up and returned from `App.Run`
|
||||||
|
- Specifying an `Action` with the legacy return signature of
|
||||||
|
`func(*cli.Context)` will produce a deprecation message to stderr
|
||||||
|
- Specifying an `Action` that is not a `func` type will produce a non-zero exit
|
||||||
|
from `App.Run`
|
||||||
|
- Specifying an `Action` func that has an invalid (input) signature will
|
||||||
|
produce a non-zero exit from `App.Run`
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- <a name="deprecated-cli-app-runandexitonerror"></a>
|
||||||
|
`cli.App.RunAndExitOnError`, which should now be done by returning an error
|
||||||
|
that fulfills `cli.ExitCoder` to `cli.App.Run`.
|
||||||
|
- <a name="deprecated-cli-app-action-signature"></a> the legacy signature for
|
||||||
|
`cli.App.Action` of `func(*cli.Context)`, which should now have a return
|
||||||
|
signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Added missing `*cli.Context.GlobalFloat64` method
|
||||||
|
|
||||||
|
## [1.14.0] - 2016-04-03 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- Codebeat badge
|
||||||
|
- Support for categorization via `CategorizedHelp` and `Categories` on app.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use `filepath.Base` instead of `path.Base` in `Name` and `HelpName`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Ensure version is not shown in help text when `HideVersion` set.
|
||||||
|
|
||||||
|
## [1.13.0] - 2016-03-06 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- YAML file input support.
|
||||||
|
- `NArg` method on context.
|
||||||
|
|
||||||
|
## [1.12.0] - 2016-02-17 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- Custom usage error handling.
|
||||||
|
- Custom text support in `USAGE` section of help output.
|
||||||
|
- Improved help messages for empty strings.
|
||||||
|
- AppVeyor CI configuration.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Removed `panic` from default help printer func.
|
||||||
|
- De-duping and optimizations.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Correctly handle `Before`/`After` at command level when no subcommands.
|
||||||
|
- Case of literal `-` argument causing flag reordering.
|
||||||
|
- Environment variable hints on Windows.
|
||||||
|
- Docs updates.
|
||||||
|
|
||||||
|
## [1.11.1] - 2015-12-21 (backfilled 2016-04-25)
|
||||||
|
### Changed
|
||||||
|
- Use `path.Base` in `Name` and `HelpName`
|
||||||
|
- Export `GetName` on flag types.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Flag parsing when skipping is enabled.
|
||||||
|
- Test output cleanup.
|
||||||
|
- Move completion check to account for empty input case.
|
||||||
|
|
||||||
|
## [1.11.0] - 2015-11-15 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- Destination scan support for flags.
|
||||||
|
- Testing against `tip` in Travis CI config.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Go version in Travis CI config.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Removed redundant tests.
|
||||||
|
- Use correct example naming in tests.
|
||||||
|
|
||||||
|
## [1.10.2] - 2015-10-29 (backfilled 2016-04-25)
|
||||||
|
### Fixed
|
||||||
|
- Remove unused var in bash completion.
|
||||||
|
|
||||||
|
## [1.10.1] - 2015-10-21 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- Coverage and reference logos in README.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Use specified values in help and version parsing.
|
||||||
|
- Only display app version and help message once.
|
||||||
|
|
||||||
|
## [1.10.0] - 2015-10-06 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- More tests for existing functionality.
|
||||||
|
- `ArgsUsage` at app and command level for help text flexibility.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Honor `HideHelp` and `HideVersion` in `App.Run`.
|
||||||
|
- Remove juvenile word from README.
|
||||||
|
|
||||||
|
## [1.9.0] - 2015-09-08 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- `FullName` on command with accompanying help output update.
|
||||||
|
- Set default `$PROG` in bash completion.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Docs formatting.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Removed self-referential imports in tests.
|
||||||
|
|
||||||
|
## [1.8.0] - 2015-06-30 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- Support for `Copyright` at app level.
|
||||||
|
- `Parent` func at context level to walk up context lineage.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Global flag processing at top level.
|
||||||
|
|
||||||
|
## [1.7.1] - 2015-06-11 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- Aggregate errors from `Before`/`After` funcs.
|
||||||
|
- Doc comments on flag structs.
|
||||||
|
- Include non-global flags when checking version and help.
|
||||||
|
- Travis CI config updates.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Ensure slice type flags have non-nil values.
|
||||||
|
- Collect global flags from the full command hierarchy.
|
||||||
|
- Docs prose.
|
||||||
|
|
||||||
|
## [1.7.0] - 2015-05-03 (backfilled 2016-04-25)
|
||||||
|
### Changed
|
||||||
|
- `HelpPrinter` signature includes output writer.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Specify go 1.1+ in docs.
|
||||||
|
- Set `Writer` when running command as app.
|
||||||
|
|
||||||
|
## [1.6.0] - 2015-03-23 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- Multiple author support.
|
||||||
|
- `NumFlags` at context level.
|
||||||
|
- `Aliases` at command level.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- `ShortName` at command level.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Subcommand help output.
|
||||||
|
- Backward compatible support for deprecated `Author` and `Email` fields.
|
||||||
|
- Docs regarding `Names`/`Aliases`.
|
||||||
|
|
||||||
|
## [1.5.0] - 2015-02-20 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- `After` hook func support at app and command level.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Use parsed context when running command as subcommand.
|
||||||
|
- Docs prose.
|
||||||
|
|
||||||
|
## [1.4.1] - 2015-01-09 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- Support for hiding `-h / --help` flags, but not `help` subcommand.
|
||||||
|
- Stop flag parsing after `--`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Help text for generic flags to specify single value.
|
||||||
|
- Use double quotes in output for defaults.
|
||||||
|
- Use `ParseInt` instead of `ParseUint` for int environment var values.
|
||||||
|
- Use `0` as base when parsing int environment var values.
|
||||||
|
|
||||||
|
## [1.4.0] - 2014-12-12 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- Support for environment variable lookup "cascade".
|
||||||
|
- Support for `Stdout` on app for output redirection.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Print command help instead of app help in `ShowCommandHelp`.
|
||||||
|
|
||||||
|
## [1.3.1] - 2014-11-13 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- Docs and example code updates.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Default `-v / --version` flag made optional.
|
||||||
|
|
||||||
|
## [1.3.0] - 2014-08-10 (backfilled 2016-04-25)
|
||||||
|
### Added
|
||||||
|
- `FlagNames` at context level.
|
||||||
|
- Exposed `VersionPrinter` var for more control over version output.
|
||||||
|
- Zsh completion hook.
|
||||||
|
- `AUTHOR` section in default app help template.
|
||||||
|
- Contribution guidelines.
|
||||||
|
- `DurationFlag` type.
|
||||||
|
|
||||||
|
## [1.2.0] - 2014-08-02
|
||||||
|
### Added
|
||||||
|
- Support for environment variable defaults on flags plus tests.
|
||||||
|
|
||||||
|
## [1.1.0] - 2014-07-15
|
||||||
|
### Added
|
||||||
|
- Bash completion.
|
||||||
|
- Optional hiding of built-in help command.
|
||||||
|
- Optional skipping of flag parsing at command level.
|
||||||
|
- `Author`, `Email`, and `Compiled` metadata on app.
|
||||||
|
- `Before` hook func support at app and command level.
|
||||||
|
- `CommandNotFound` func support at app level.
|
||||||
|
- Command reference available on context.
|
||||||
|
- `GenericFlag` type.
|
||||||
|
- `Float64Flag` type.
|
||||||
|
- `BoolTFlag` type.
|
||||||
|
- `IsSet` flag helper on context.
|
||||||
|
- More flag lookup funcs at context level.
|
||||||
|
- More tests & docs.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Help template updates to account for presence/absence of flags.
|
||||||
|
- Separated subcommand help template.
|
||||||
|
- Exposed `HelpPrinter` var for more control over help output.
|
||||||
|
|
||||||
|
## [1.0.0] - 2013-11-01
|
||||||
|
### Added
|
||||||
|
- `help` flag in default app flag set and each command flag set.
|
||||||
|
- Custom handling of argument parsing errors.
|
||||||
|
- Command lookup by name at app level.
|
||||||
|
- `StringSliceFlag` type and supporting `StringSlice` type.
|
||||||
|
- `IntSliceFlag` type and supporting `IntSlice` type.
|
||||||
|
- Slice type flag lookups by name at context level.
|
||||||
|
- Export of app and command help functions.
|
||||||
|
- More tests & docs.
|
||||||
|
|
||||||
|
## 0.1.0 - 2013-07-22
|
||||||
|
### Added
|
||||||
|
- Initial implementation.
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/urfave/cli/compare/v1.18.0...HEAD
|
||||||
|
[1.18.0]: https://github.com/urfave/cli/compare/v1.17.0...v1.18.0
|
||||||
|
[1.17.0]: https://github.com/urfave/cli/compare/v1.16.0...v1.17.0
|
||||||
|
[1.16.0]: https://github.com/urfave/cli/compare/v1.15.0...v1.16.0
|
||||||
|
[1.15.0]: https://github.com/urfave/cli/compare/v1.14.0...v1.15.0
|
||||||
|
[1.14.0]: https://github.com/urfave/cli/compare/v1.13.0...v1.14.0
|
||||||
|
[1.13.0]: https://github.com/urfave/cli/compare/v1.12.0...v1.13.0
|
||||||
|
[1.12.0]: https://github.com/urfave/cli/compare/v1.11.1...v1.12.0
|
||||||
|
[1.11.1]: https://github.com/urfave/cli/compare/v1.11.0...v1.11.1
|
||||||
|
[1.11.0]: https://github.com/urfave/cli/compare/v1.10.2...v1.11.0
|
||||||
|
[1.10.2]: https://github.com/urfave/cli/compare/v1.10.1...v1.10.2
|
||||||
|
[1.10.1]: https://github.com/urfave/cli/compare/v1.10.0...v1.10.1
|
||||||
|
[1.10.0]: https://github.com/urfave/cli/compare/v1.9.0...v1.10.0
|
||||||
|
[1.9.0]: https://github.com/urfave/cli/compare/v1.8.0...v1.9.0
|
||||||
|
[1.8.0]: https://github.com/urfave/cli/compare/v1.7.1...v1.8.0
|
||||||
|
[1.7.1]: https://github.com/urfave/cli/compare/v1.7.0...v1.7.1
|
||||||
|
[1.7.0]: https://github.com/urfave/cli/compare/v1.6.0...v1.7.0
|
||||||
|
[1.6.0]: https://github.com/urfave/cli/compare/v1.5.0...v1.6.0
|
||||||
|
[1.5.0]: https://github.com/urfave/cli/compare/v1.4.1...v1.5.0
|
||||||
|
[1.4.1]: https://github.com/urfave/cli/compare/v1.4.0...v1.4.1
|
||||||
|
[1.4.0]: https://github.com/urfave/cli/compare/v1.3.1...v1.4.0
|
||||||
|
[1.3.1]: https://github.com/urfave/cli/compare/v1.3.0...v1.3.1
|
||||||
|
[1.3.0]: https://github.com/urfave/cli/compare/v1.2.0...v1.3.0
|
||||||
|
[1.2.0]: https://github.com/urfave/cli/compare/v1.1.0...v1.2.0
|
||||||
|
[1.1.0]: https://github.com/urfave/cli/compare/v1.0.0...v1.1.0
|
||||||
|
[1.0.0]: https://github.com/urfave/cli/compare/v0.1.0...v1.0.0
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2019 Dee Luo
|
Copyright (c) 2016 Jeremy Saenz & Contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
+1381
File diff suppressed because it is too large
Load Diff
+497
@@ -0,0 +1,497 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md"
|
||||||
|
appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL)
|
||||||
|
runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL)
|
||||||
|
|
||||||
|
contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you."
|
||||||
|
|
||||||
|
errInvalidActionType = NewExitError("ERROR invalid Action type. "+
|
||||||
|
fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+
|
||||||
|
fmt.Sprintf("See %s", appActionDeprecationURL), 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
// App is the main structure of a cli application. It is recommended that
|
||||||
|
// an app be created with the cli.NewApp() function
|
||||||
|
type App struct {
|
||||||
|
// The name of the program. Defaults to path.Base(os.Args[0])
|
||||||
|
Name string
|
||||||
|
// Full name of command for help, defaults to Name
|
||||||
|
HelpName string
|
||||||
|
// Description of the program.
|
||||||
|
Usage string
|
||||||
|
// Text to override the USAGE section of help
|
||||||
|
UsageText string
|
||||||
|
// Description of the program argument format.
|
||||||
|
ArgsUsage string
|
||||||
|
// Version of the program
|
||||||
|
Version string
|
||||||
|
// Description of the program
|
||||||
|
Description string
|
||||||
|
// List of commands to execute
|
||||||
|
Commands []Command
|
||||||
|
// List of flags to parse
|
||||||
|
Flags []Flag
|
||||||
|
// Boolean to enable bash completion commands
|
||||||
|
EnableBashCompletion bool
|
||||||
|
// Boolean to hide built-in help command
|
||||||
|
HideHelp bool
|
||||||
|
// Boolean to hide built-in version flag and the VERSION section of help
|
||||||
|
HideVersion bool
|
||||||
|
// Populate on app startup, only gettable through method Categories()
|
||||||
|
categories CommandCategories
|
||||||
|
// An action to execute when the bash-completion flag is set
|
||||||
|
BashComplete BashCompleteFunc
|
||||||
|
// An action to execute before any subcommands are run, but after the context is ready
|
||||||
|
// If a non-nil error is returned, no subcommands are run
|
||||||
|
Before BeforeFunc
|
||||||
|
// An action to execute after any subcommands are run, but after the subcommand has finished
|
||||||
|
// It is run even if Action() panics
|
||||||
|
After AfterFunc
|
||||||
|
|
||||||
|
// The action to execute when no subcommands are specified
|
||||||
|
// Expects a `cli.ActionFunc` but will accept the *deprecated* signature of `func(*cli.Context) {}`
|
||||||
|
// *Note*: support for the deprecated `Action` signature will be removed in a future version
|
||||||
|
Action interface{}
|
||||||
|
|
||||||
|
// Execute this function if the proper command cannot be found
|
||||||
|
CommandNotFound CommandNotFoundFunc
|
||||||
|
// Execute this function if an usage error occurs
|
||||||
|
OnUsageError OnUsageErrorFunc
|
||||||
|
// Compilation date
|
||||||
|
Compiled time.Time
|
||||||
|
// List of all authors who contributed
|
||||||
|
Authors []Author
|
||||||
|
// Copyright of the binary if any
|
||||||
|
Copyright string
|
||||||
|
// Name of Author (Note: Use App.Authors, this is deprecated)
|
||||||
|
Author string
|
||||||
|
// Email of Author (Note: Use App.Authors, this is deprecated)
|
||||||
|
Email string
|
||||||
|
// Writer writer to write output to
|
||||||
|
Writer io.Writer
|
||||||
|
// ErrWriter writes error output
|
||||||
|
ErrWriter io.Writer
|
||||||
|
// Other custom info
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
// Carries a function which returns app specific info.
|
||||||
|
ExtraInfo func() map[string]string
|
||||||
|
// CustomAppHelpTemplate the text template for app help topic.
|
||||||
|
// cli.go uses text/template to render templates. You can
|
||||||
|
// render custom help text by setting this variable.
|
||||||
|
CustomAppHelpTemplate string
|
||||||
|
|
||||||
|
didSetup bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to find out when this binary was compiled.
|
||||||
|
// Returns the current time if it fails to find it.
|
||||||
|
func compileTime() time.Time {
|
||||||
|
info, err := os.Stat(os.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
return info.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp creates a new cli Application with some reasonable defaults for Name,
|
||||||
|
// Usage, Version and Action.
|
||||||
|
func NewApp() *App {
|
||||||
|
return &App{
|
||||||
|
Name: filepath.Base(os.Args[0]),
|
||||||
|
HelpName: filepath.Base(os.Args[0]),
|
||||||
|
Usage: "A new cli application",
|
||||||
|
UsageText: "",
|
||||||
|
Version: "0.0.0",
|
||||||
|
BashComplete: DefaultAppComplete,
|
||||||
|
Action: helpCommand.Action,
|
||||||
|
Compiled: compileTime(),
|
||||||
|
Writer: os.Stdout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup runs initialization code to ensure all data structures are ready for
|
||||||
|
// `Run` or inspection prior to `Run`. It is internally called by `Run`, but
|
||||||
|
// will return early if setup has already happened.
|
||||||
|
func (a *App) Setup() {
|
||||||
|
if a.didSetup {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.didSetup = true
|
||||||
|
|
||||||
|
if a.Author != "" || a.Email != "" {
|
||||||
|
a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email})
|
||||||
|
}
|
||||||
|
|
||||||
|
newCmds := []Command{}
|
||||||
|
for _, c := range a.Commands {
|
||||||
|
if c.HelpName == "" {
|
||||||
|
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
|
||||||
|
}
|
||||||
|
newCmds = append(newCmds, c)
|
||||||
|
}
|
||||||
|
a.Commands = newCmds
|
||||||
|
|
||||||
|
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
|
||||||
|
a.Commands = append(a.Commands, helpCommand)
|
||||||
|
if (HelpFlag != BoolFlag{}) {
|
||||||
|
a.appendFlag(HelpFlag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.HideVersion {
|
||||||
|
a.appendFlag(VersionFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.categories = CommandCategories{}
|
||||||
|
for _, command := range a.Commands {
|
||||||
|
a.categories = a.categories.AddCommand(command.Category, command)
|
||||||
|
}
|
||||||
|
sort.Sort(a.categories)
|
||||||
|
|
||||||
|
if a.Metadata == nil {
|
||||||
|
a.Metadata = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Writer == nil {
|
||||||
|
a.Writer = os.Stdout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run is the entry point to the cli app. Parses the arguments slice and routes
|
||||||
|
// to the proper flag/args combination
|
||||||
|
func (a *App) Run(arguments []string) (err error) {
|
||||||
|
a.Setup()
|
||||||
|
|
||||||
|
// handle the completion flag separately from the flagset since
|
||||||
|
// completion could be attempted after a flag, but before its value was put
|
||||||
|
// on the command line. this causes the flagset to interpret the completion
|
||||||
|
// flag name as the value of the flag before it which is undesirable
|
||||||
|
// note that we can only do this because the shell autocomplete function
|
||||||
|
// always appends the completion flag at the end of the command
|
||||||
|
shellComplete, arguments := checkShellCompleteFlag(a, arguments)
|
||||||
|
|
||||||
|
// parse flags
|
||||||
|
set, err := flagSet(a.Name, a.Flags)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
set.SetOutput(ioutil.Discard)
|
||||||
|
err = set.Parse(arguments[1:])
|
||||||
|
nerr := normalizeFlags(a.Flags, set)
|
||||||
|
context := NewContext(a, set, nil)
|
||||||
|
if nerr != nil {
|
||||||
|
fmt.Fprintln(a.Writer, nerr)
|
||||||
|
ShowAppHelp(context)
|
||||||
|
return nerr
|
||||||
|
}
|
||||||
|
context.shellComplete = shellComplete
|
||||||
|
|
||||||
|
if checkCompletions(context) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if a.OnUsageError != nil {
|
||||||
|
err := a.OnUsageError(context, err, false)
|
||||||
|
HandleExitCoder(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
|
||||||
|
ShowAppHelp(context)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.HideHelp && checkHelp(context) {
|
||||||
|
ShowAppHelp(context)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.HideVersion && checkVersion(context) {
|
||||||
|
ShowVersion(context)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.After != nil {
|
||||||
|
defer func() {
|
||||||
|
if afterErr := a.After(context); afterErr != nil {
|
||||||
|
if err != nil {
|
||||||
|
err = NewMultiError(err, afterErr)
|
||||||
|
} else {
|
||||||
|
err = afterErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Before != nil {
|
||||||
|
beforeErr := a.Before(context)
|
||||||
|
if beforeErr != nil {
|
||||||
|
ShowAppHelp(context)
|
||||||
|
HandleExitCoder(beforeErr)
|
||||||
|
err = beforeErr
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := context.Args()
|
||||||
|
if args.Present() {
|
||||||
|
name := args.First()
|
||||||
|
c := a.Command(name)
|
||||||
|
if c != nil {
|
||||||
|
return c.Run(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Action == nil {
|
||||||
|
a.Action = helpCommand.Action
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run default Action
|
||||||
|
err = HandleAction(a.Action, context)
|
||||||
|
|
||||||
|
HandleExitCoder(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunAndExitOnError calls .Run() and exits non-zero if an error was returned
|
||||||
|
//
|
||||||
|
// Deprecated: instead you should return an error that fulfills cli.ExitCoder
|
||||||
|
// to cli.App.Run. This will cause the application to exit with the given eror
|
||||||
|
// code in the cli.ExitCoder
|
||||||
|
func (a *App) RunAndExitOnError() {
|
||||||
|
if err := a.Run(os.Args); err != nil {
|
||||||
|
fmt.Fprintln(a.errWriter(), err)
|
||||||
|
OsExiter(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to
|
||||||
|
// generate command-specific flags
|
||||||
|
func (a *App) RunAsSubcommand(ctx *Context) (err error) {
|
||||||
|
// append help to commands
|
||||||
|
if len(a.Commands) > 0 {
|
||||||
|
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
|
||||||
|
a.Commands = append(a.Commands, helpCommand)
|
||||||
|
if (HelpFlag != BoolFlag{}) {
|
||||||
|
a.appendFlag(HelpFlag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newCmds := []Command{}
|
||||||
|
for _, c := range a.Commands {
|
||||||
|
if c.HelpName == "" {
|
||||||
|
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
|
||||||
|
}
|
||||||
|
newCmds = append(newCmds, c)
|
||||||
|
}
|
||||||
|
a.Commands = newCmds
|
||||||
|
|
||||||
|
// parse flags
|
||||||
|
set, err := flagSet(a.Name, a.Flags)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
set.SetOutput(ioutil.Discard)
|
||||||
|
err = set.Parse(ctx.Args().Tail())
|
||||||
|
nerr := normalizeFlags(a.Flags, set)
|
||||||
|
context := NewContext(a, set, ctx)
|
||||||
|
|
||||||
|
if nerr != nil {
|
||||||
|
fmt.Fprintln(a.Writer, nerr)
|
||||||
|
fmt.Fprintln(a.Writer)
|
||||||
|
if len(a.Commands) > 0 {
|
||||||
|
ShowSubcommandHelp(context)
|
||||||
|
} else {
|
||||||
|
ShowCommandHelp(ctx, context.Args().First())
|
||||||
|
}
|
||||||
|
return nerr
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkCompletions(context) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if a.OnUsageError != nil {
|
||||||
|
err = a.OnUsageError(context, err, true)
|
||||||
|
HandleExitCoder(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
|
||||||
|
ShowSubcommandHelp(context)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a.Commands) > 0 {
|
||||||
|
if checkSubcommandHelp(context) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if checkCommandHelp(ctx, context.Args().First()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.After != nil {
|
||||||
|
defer func() {
|
||||||
|
afterErr := a.After(context)
|
||||||
|
if afterErr != nil {
|
||||||
|
HandleExitCoder(err)
|
||||||
|
if err != nil {
|
||||||
|
err = NewMultiError(err, afterErr)
|
||||||
|
} else {
|
||||||
|
err = afterErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Before != nil {
|
||||||
|
beforeErr := a.Before(context)
|
||||||
|
if beforeErr != nil {
|
||||||
|
HandleExitCoder(beforeErr)
|
||||||
|
err = beforeErr
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := context.Args()
|
||||||
|
if args.Present() {
|
||||||
|
name := args.First()
|
||||||
|
c := a.Command(name)
|
||||||
|
if c != nil {
|
||||||
|
return c.Run(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run default Action
|
||||||
|
err = HandleAction(a.Action, context)
|
||||||
|
|
||||||
|
HandleExitCoder(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command returns the named command on App. Returns nil if the command does not exist
|
||||||
|
func (a *App) Command(name string) *Command {
|
||||||
|
for _, c := range a.Commands {
|
||||||
|
if c.HasName(name) {
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Categories returns a slice containing all the categories with the commands they contain
|
||||||
|
func (a *App) Categories() CommandCategories {
|
||||||
|
return a.categories
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisibleCategories returns a slice of categories and commands that are
|
||||||
|
// Hidden=false
|
||||||
|
func (a *App) VisibleCategories() []*CommandCategory {
|
||||||
|
ret := []*CommandCategory{}
|
||||||
|
for _, category := range a.categories {
|
||||||
|
if visible := func() *CommandCategory {
|
||||||
|
for _, command := range category.Commands {
|
||||||
|
if !command.Hidden {
|
||||||
|
return category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); visible != nil {
|
||||||
|
ret = append(ret, visible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisibleCommands returns a slice of the Commands with Hidden=false
|
||||||
|
func (a *App) VisibleCommands() []Command {
|
||||||
|
ret := []Command{}
|
||||||
|
for _, command := range a.Commands {
|
||||||
|
if !command.Hidden {
|
||||||
|
ret = append(ret, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisibleFlags returns a slice of the Flags with Hidden=false
|
||||||
|
func (a *App) VisibleFlags() []Flag {
|
||||||
|
return visibleFlags(a.Flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) hasFlag(flag Flag) bool {
|
||||||
|
for _, f := range a.Flags {
|
||||||
|
if flag == f {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) errWriter() io.Writer {
|
||||||
|
|
||||||
|
// When the app ErrWriter is nil use the package level one.
|
||||||
|
if a.ErrWriter == nil {
|
||||||
|
return ErrWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.ErrWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) appendFlag(flag Flag) {
|
||||||
|
if !a.hasFlag(flag) {
|
||||||
|
a.Flags = append(a.Flags, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author represents someone who has contributed to a cli project.
|
||||||
|
type Author struct {
|
||||||
|
Name string // The Authors name
|
||||||
|
Email string // The Authors email
|
||||||
|
}
|
||||||
|
|
||||||
|
// String makes Author comply to the Stringer interface, to allow an easy print in the templating process
|
||||||
|
func (a Author) String() string {
|
||||||
|
e := ""
|
||||||
|
if a.Email != "" {
|
||||||
|
e = " <" + a.Email + ">"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%v%v", a.Name, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleAction attempts to figure out which Action signature was used. If
|
||||||
|
// it's an ActionFunc or a func with the legacy signature for Action, the func
|
||||||
|
// is run!
|
||||||
|
func HandleAction(action interface{}, context *Context) (err error) {
|
||||||
|
if a, ok := action.(ActionFunc); ok {
|
||||||
|
return a(context)
|
||||||
|
} else if a, ok := action.(func(*Context) error); ok {
|
||||||
|
return a(context)
|
||||||
|
} else if a, ok := action.(func(*Context)); ok { // deprecated function signature
|
||||||
|
a(context)
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return errInvalidActionType
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
version: "{build}"
|
||||||
|
|
||||||
|
os: Windows Server 2016
|
||||||
|
|
||||||
|
image: Visual Studio 2017
|
||||||
|
|
||||||
|
clone_folder: c:\gopath\src\github.com\urfave\cli
|
||||||
|
|
||||||
|
environment:
|
||||||
|
GOPATH: C:\gopath
|
||||||
|
GOVERSION: 1.8.x
|
||||||
|
PYTHON: C:\Python36-x64
|
||||||
|
PYTHON_VERSION: 3.6.x
|
||||||
|
PYTHON_ARCH: 64
|
||||||
|
|
||||||
|
install:
|
||||||
|
- set PATH=%GOPATH%\bin;C:\go\bin;%PATH%
|
||||||
|
- go version
|
||||||
|
- go env
|
||||||
|
- go get github.com/urfave/gfmrun/...
|
||||||
|
- go get -v -t ./...
|
||||||
|
|
||||||
|
build_script:
|
||||||
|
- python runtests vet
|
||||||
|
- python runtests test
|
||||||
|
- python runtests gfmrun
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
// CommandCategories is a slice of *CommandCategory.
|
||||||
|
type CommandCategories []*CommandCategory
|
||||||
|
|
||||||
|
// CommandCategory is a category containing commands.
|
||||||
|
type CommandCategory struct {
|
||||||
|
Name string
|
||||||
|
Commands Commands
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CommandCategories) Less(i, j int) bool {
|
||||||
|
return c[i].Name < c[j].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CommandCategories) Len() int {
|
||||||
|
return len(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CommandCategories) Swap(i, j int) {
|
||||||
|
c[i], c[j] = c[j], c[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCommand adds a command to a category.
|
||||||
|
func (c CommandCategories) AddCommand(category string, command Command) CommandCategories {
|
||||||
|
for _, commandCategory := range c {
|
||||||
|
if commandCategory.Name == category {
|
||||||
|
commandCategory.Commands = append(commandCategory.Commands, command)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(c, &CommandCategory{Name: category, Commands: []Command{command}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisibleCommands returns a slice of the Commands with Hidden=false
|
||||||
|
func (c *CommandCategory) VisibleCommands() []Command {
|
||||||
|
ret := []Command{}
|
||||||
|
for _, command := range c.Commands {
|
||||||
|
if !command.Hidden {
|
||||||
|
ret = append(ret, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
// Package cli provides a minimal framework for creating and organizing command line
|
||||||
|
// Go applications. cli is designed to be easy to understand and write, the most simple
|
||||||
|
// cli application can be written as follows:
|
||||||
|
// func main() {
|
||||||
|
// cli.NewApp().Run(os.Args)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Of course this application does not do much, so let's make this an actual application:
|
||||||
|
// func main() {
|
||||||
|
// app := cli.NewApp()
|
||||||
|
// app.Name = "greet"
|
||||||
|
// app.Usage = "say a greeting"
|
||||||
|
// app.Action = func(c *cli.Context) error {
|
||||||
|
// println("Greetings")
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// app.Run(os.Args)
|
||||||
|
// }
|
||||||
|
package cli
|
||||||
|
|
||||||
|
//go:generate python ./generate-flag-types cli -i flag-types.json -o flag_generated.go
|
||||||
+304
@@ -0,0 +1,304 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command is a subcommand for a cli.App.
|
||||||
|
type Command struct {
|
||||||
|
// The name of the command
|
||||||
|
Name string
|
||||||
|
// short name of the command. Typically one character (deprecated, use `Aliases`)
|
||||||
|
ShortName string
|
||||||
|
// A list of aliases for the command
|
||||||
|
Aliases []string
|
||||||
|
// A short description of the usage of this command
|
||||||
|
Usage string
|
||||||
|
// Custom text to show on USAGE section of help
|
||||||
|
UsageText string
|
||||||
|
// A longer explanation of how the command works
|
||||||
|
Description string
|
||||||
|
// A short description of the arguments of this command
|
||||||
|
ArgsUsage string
|
||||||
|
// The category the command is part of
|
||||||
|
Category string
|
||||||
|
// The function to call when checking for bash command completions
|
||||||
|
BashComplete BashCompleteFunc
|
||||||
|
// An action to execute before any sub-subcommands are run, but after the context is ready
|
||||||
|
// If a non-nil error is returned, no sub-subcommands are run
|
||||||
|
Before BeforeFunc
|
||||||
|
// An action to execute after any subcommands are run, but after the subcommand has finished
|
||||||
|
// It is run even if Action() panics
|
||||||
|
After AfterFunc
|
||||||
|
// The function to call when this command is invoked
|
||||||
|
Action interface{}
|
||||||
|
// TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind
|
||||||
|
// of deprecation period has passed, maybe?
|
||||||
|
|
||||||
|
// Execute this function if a usage error occurs.
|
||||||
|
OnUsageError OnUsageErrorFunc
|
||||||
|
// List of child commands
|
||||||
|
Subcommands Commands
|
||||||
|
// List of flags to parse
|
||||||
|
Flags []Flag
|
||||||
|
// Treat all flags as normal arguments if true
|
||||||
|
SkipFlagParsing bool
|
||||||
|
// Skip argument reordering which attempts to move flags before arguments,
|
||||||
|
// but only works if all flags appear after all arguments. This behavior was
|
||||||
|
// removed n version 2 since it only works under specific conditions so we
|
||||||
|
// backport here by exposing it as an option for compatibility.
|
||||||
|
SkipArgReorder bool
|
||||||
|
// Boolean to hide built-in help command
|
||||||
|
HideHelp bool
|
||||||
|
// Boolean to hide this command from help or completion
|
||||||
|
Hidden bool
|
||||||
|
|
||||||
|
// Full name of command for help, defaults to full command name, including parent commands.
|
||||||
|
HelpName string
|
||||||
|
commandNamePath []string
|
||||||
|
|
||||||
|
// CustomHelpTemplate the text template for the command help topic.
|
||||||
|
// cli.go uses text/template to render templates. You can
|
||||||
|
// render custom help text by setting this variable.
|
||||||
|
CustomHelpTemplate string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandsByName []Command
|
||||||
|
|
||||||
|
func (c CommandsByName) Len() int {
|
||||||
|
return len(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CommandsByName) Less(i, j int) bool {
|
||||||
|
return c[i].Name < c[j].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CommandsByName) Swap(i, j int) {
|
||||||
|
c[i], c[j] = c[j], c[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullName returns the full name of the command.
|
||||||
|
// For subcommands this ensures that parent commands are part of the command path
|
||||||
|
func (c Command) FullName() string {
|
||||||
|
if c.commandNamePath == nil {
|
||||||
|
return c.Name
|
||||||
|
}
|
||||||
|
return strings.Join(c.commandNamePath, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands is a slice of Command
|
||||||
|
type Commands []Command
|
||||||
|
|
||||||
|
// Run invokes the command given the context, parses ctx.Args() to generate command-specific flags
|
||||||
|
func (c Command) Run(ctx *Context) (err error) {
|
||||||
|
if len(c.Subcommands) > 0 {
|
||||||
|
return c.startApp(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.HideHelp && (HelpFlag != BoolFlag{}) {
|
||||||
|
// append help to flags
|
||||||
|
c.Flags = append(
|
||||||
|
c.Flags,
|
||||||
|
HelpFlag,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
set, err := flagSet(c.Name, c.Flags)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
set.SetOutput(ioutil.Discard)
|
||||||
|
|
||||||
|
if c.SkipFlagParsing {
|
||||||
|
err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...))
|
||||||
|
} else if !c.SkipArgReorder {
|
||||||
|
firstFlagIndex := -1
|
||||||
|
terminatorIndex := -1
|
||||||
|
for index, arg := range ctx.Args() {
|
||||||
|
if arg == "--" {
|
||||||
|
terminatorIndex = index
|
||||||
|
break
|
||||||
|
} else if arg == "-" {
|
||||||
|
// Do nothing. A dash alone is not really a flag.
|
||||||
|
continue
|
||||||
|
} else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 {
|
||||||
|
firstFlagIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstFlagIndex > -1 {
|
||||||
|
args := ctx.Args()
|
||||||
|
regularArgs := make([]string, len(args[1:firstFlagIndex]))
|
||||||
|
copy(regularArgs, args[1:firstFlagIndex])
|
||||||
|
|
||||||
|
var flagArgs []string
|
||||||
|
if terminatorIndex > -1 {
|
||||||
|
flagArgs = args[firstFlagIndex:terminatorIndex]
|
||||||
|
regularArgs = append(regularArgs, args[terminatorIndex:]...)
|
||||||
|
} else {
|
||||||
|
flagArgs = args[firstFlagIndex:]
|
||||||
|
}
|
||||||
|
|
||||||
|
err = set.Parse(append(flagArgs, regularArgs...))
|
||||||
|
} else {
|
||||||
|
err = set.Parse(ctx.Args().Tail())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = set.Parse(ctx.Args().Tail())
|
||||||
|
}
|
||||||
|
|
||||||
|
nerr := normalizeFlags(c.Flags, set)
|
||||||
|
if nerr != nil {
|
||||||
|
fmt.Fprintln(ctx.App.Writer, nerr)
|
||||||
|
fmt.Fprintln(ctx.App.Writer)
|
||||||
|
ShowCommandHelp(ctx, c.Name)
|
||||||
|
return nerr
|
||||||
|
}
|
||||||
|
|
||||||
|
context := NewContext(ctx.App, set, ctx)
|
||||||
|
context.Command = c
|
||||||
|
if checkCommandCompletions(context, c.Name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if c.OnUsageError != nil {
|
||||||
|
err := c.OnUsageError(context, err, false)
|
||||||
|
HandleExitCoder(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error())
|
||||||
|
fmt.Fprintln(context.App.Writer)
|
||||||
|
ShowCommandHelp(context, c.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkCommandHelp(context, c.Name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.After != nil {
|
||||||
|
defer func() {
|
||||||
|
afterErr := c.After(context)
|
||||||
|
if afterErr != nil {
|
||||||
|
HandleExitCoder(err)
|
||||||
|
if err != nil {
|
||||||
|
err = NewMultiError(err, afterErr)
|
||||||
|
} else {
|
||||||
|
err = afterErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Before != nil {
|
||||||
|
err = c.Before(context)
|
||||||
|
if err != nil {
|
||||||
|
ShowCommandHelp(context, c.Name)
|
||||||
|
HandleExitCoder(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Action == nil {
|
||||||
|
c.Action = helpSubcommand.Action
|
||||||
|
}
|
||||||
|
|
||||||
|
err = HandleAction(c.Action, context)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
HandleExitCoder(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names returns the names including short names and aliases.
|
||||||
|
func (c Command) Names() []string {
|
||||||
|
names := []string{c.Name}
|
||||||
|
|
||||||
|
if c.ShortName != "" {
|
||||||
|
names = append(names, c.ShortName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(names, c.Aliases...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasName returns true if Command.Name or Command.ShortName matches given name
|
||||||
|
func (c Command) HasName(name string) bool {
|
||||||
|
for _, n := range c.Names() {
|
||||||
|
if n == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Command) startApp(ctx *Context) error {
|
||||||
|
app := NewApp()
|
||||||
|
app.Metadata = ctx.App.Metadata
|
||||||
|
// set the name and usage
|
||||||
|
app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name)
|
||||||
|
if c.HelpName == "" {
|
||||||
|
app.HelpName = c.HelpName
|
||||||
|
} else {
|
||||||
|
app.HelpName = app.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Usage = c.Usage
|
||||||
|
app.Description = c.Description
|
||||||
|
app.ArgsUsage = c.ArgsUsage
|
||||||
|
|
||||||
|
// set CommandNotFound
|
||||||
|
app.CommandNotFound = ctx.App.CommandNotFound
|
||||||
|
app.CustomAppHelpTemplate = c.CustomHelpTemplate
|
||||||
|
|
||||||
|
// set the flags and commands
|
||||||
|
app.Commands = c.Subcommands
|
||||||
|
app.Flags = c.Flags
|
||||||
|
app.HideHelp = c.HideHelp
|
||||||
|
|
||||||
|
app.Version = ctx.App.Version
|
||||||
|
app.HideVersion = ctx.App.HideVersion
|
||||||
|
app.Compiled = ctx.App.Compiled
|
||||||
|
app.Author = ctx.App.Author
|
||||||
|
app.Email = ctx.App.Email
|
||||||
|
app.Writer = ctx.App.Writer
|
||||||
|
app.ErrWriter = ctx.App.ErrWriter
|
||||||
|
|
||||||
|
app.categories = CommandCategories{}
|
||||||
|
for _, command := range c.Subcommands {
|
||||||
|
app.categories = app.categories.AddCommand(command.Category, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(app.categories)
|
||||||
|
|
||||||
|
// bash completion
|
||||||
|
app.EnableBashCompletion = ctx.App.EnableBashCompletion
|
||||||
|
if c.BashComplete != nil {
|
||||||
|
app.BashComplete = c.BashComplete
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the actions
|
||||||
|
app.Before = c.Before
|
||||||
|
app.After = c.After
|
||||||
|
if c.Action != nil {
|
||||||
|
app.Action = c.Action
|
||||||
|
} else {
|
||||||
|
app.Action = helpSubcommand.Action
|
||||||
|
}
|
||||||
|
app.OnUsageError = c.OnUsageError
|
||||||
|
|
||||||
|
for index, cc := range app.Commands {
|
||||||
|
app.Commands[index].commandNamePath = []string{c.Name, cc.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.RunAsSubcommand(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisibleFlags returns a slice of the Flags with Hidden=false
|
||||||
|
func (c Command) VisibleFlags() []Flag {
|
||||||
|
return visibleFlags(c.Flags)
|
||||||
|
}
|
||||||
+278
@@ -0,0 +1,278 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context is a type that is passed through to
|
||||||
|
// each Handler action in a cli application. Context
|
||||||
|
// can be used to retrieve context-specific Args and
|
||||||
|
// parsed command-line options.
|
||||||
|
type Context struct {
|
||||||
|
App *App
|
||||||
|
Command Command
|
||||||
|
shellComplete bool
|
||||||
|
flagSet *flag.FlagSet
|
||||||
|
setFlags map[string]bool
|
||||||
|
parentContext *Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContext creates a new context. For use in when invoking an App or Command action.
|
||||||
|
func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context {
|
||||||
|
c := &Context{App: app, flagSet: set, parentContext: parentCtx}
|
||||||
|
|
||||||
|
if parentCtx != nil {
|
||||||
|
c.shellComplete = parentCtx.shellComplete
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumFlags returns the number of flags set
|
||||||
|
func (c *Context) NumFlags() int {
|
||||||
|
return c.flagSet.NFlag()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets a context flag to a value.
|
||||||
|
func (c *Context) Set(name, value string) error {
|
||||||
|
c.setFlags = nil
|
||||||
|
return c.flagSet.Set(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalSet sets a context flag to a value on the global flagset
|
||||||
|
func (c *Context) GlobalSet(name, value string) error {
|
||||||
|
globalContext(c).setFlags = nil
|
||||||
|
return globalContext(c).flagSet.Set(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSet determines if the flag was actually set
|
||||||
|
func (c *Context) IsSet(name string) bool {
|
||||||
|
if c.setFlags == nil {
|
||||||
|
c.setFlags = make(map[string]bool)
|
||||||
|
|
||||||
|
c.flagSet.Visit(func(f *flag.Flag) {
|
||||||
|
c.setFlags[f.Name] = true
|
||||||
|
})
|
||||||
|
|
||||||
|
c.flagSet.VisitAll(func(f *flag.Flag) {
|
||||||
|
if _, ok := c.setFlags[f.Name]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.setFlags[f.Name] = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// XXX hack to support IsSet for flags with EnvVar
|
||||||
|
//
|
||||||
|
// There isn't an easy way to do this with the current implementation since
|
||||||
|
// whether a flag was set via an environment variable is very difficult to
|
||||||
|
// determine here. Instead, we intend to introduce a backwards incompatible
|
||||||
|
// change in version 2 to add `IsSet` to the Flag interface to push the
|
||||||
|
// responsibility closer to where the information required to determine
|
||||||
|
// whether a flag is set by non-standard means such as environment
|
||||||
|
// variables is avaliable.
|
||||||
|
//
|
||||||
|
// See https://github.com/urfave/cli/issues/294 for additional discussion
|
||||||
|
flags := c.Command.Flags
|
||||||
|
if c.Command.Name == "" { // cannot == Command{} since it contains slice types
|
||||||
|
if c.App != nil {
|
||||||
|
flags = c.App.Flags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range flags {
|
||||||
|
eachName(f.GetName(), func(name string) {
|
||||||
|
if isSet, ok := c.setFlags[name]; isSet || !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val := reflect.ValueOf(f)
|
||||||
|
if val.Kind() == reflect.Ptr {
|
||||||
|
val = val.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
envVarValue := val.FieldByName("EnvVar")
|
||||||
|
if !envVarValue.IsValid() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(envVarValue.String(), func(envVar string) {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if _, ok := syscall.Getenv(envVar); ok {
|
||||||
|
c.setFlags[name] = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.setFlags[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalIsSet determines if the global flag was actually set
|
||||||
|
func (c *Context) GlobalIsSet(name string) bool {
|
||||||
|
ctx := c
|
||||||
|
if ctx.parentContext != nil {
|
||||||
|
ctx = ctx.parentContext
|
||||||
|
}
|
||||||
|
|
||||||
|
for ; ctx != nil; ctx = ctx.parentContext {
|
||||||
|
if ctx.IsSet(name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlagNames returns a slice of flag names used in this context.
|
||||||
|
func (c *Context) FlagNames() (names []string) {
|
||||||
|
for _, flag := range c.Command.Flags {
|
||||||
|
name := strings.Split(flag.GetName(), ",")[0]
|
||||||
|
if name == "help" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalFlagNames returns a slice of global flag names used by the app.
|
||||||
|
func (c *Context) GlobalFlagNames() (names []string) {
|
||||||
|
for _, flag := range c.App.Flags {
|
||||||
|
name := strings.Split(flag.GetName(), ",")[0]
|
||||||
|
if name == "help" || name == "version" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent returns the parent context, if any
|
||||||
|
func (c *Context) Parent() *Context {
|
||||||
|
return c.parentContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// value returns the value of the flag coressponding to `name`
|
||||||
|
func (c *Context) value(name string) interface{} {
|
||||||
|
return c.flagSet.Lookup(name).Value.(flag.Getter).Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args contains apps console arguments
|
||||||
|
type Args []string
|
||||||
|
|
||||||
|
// Args returns the command line arguments associated with the context.
|
||||||
|
func (c *Context) Args() Args {
|
||||||
|
args := Args(c.flagSet.Args())
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// NArg returns the number of the command line arguments.
|
||||||
|
func (c *Context) NArg() int {
|
||||||
|
return len(c.Args())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the nth argument, or else a blank string
|
||||||
|
func (a Args) Get(n int) string {
|
||||||
|
if len(a) > n {
|
||||||
|
return a[n]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// First returns the first argument, or else a blank string
|
||||||
|
func (a Args) First() string {
|
||||||
|
return a.Get(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tail returns the rest of the arguments (not the first one)
|
||||||
|
// or else an empty string slice
|
||||||
|
func (a Args) Tail() []string {
|
||||||
|
if len(a) >= 2 {
|
||||||
|
return []string(a)[1:]
|
||||||
|
}
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present checks if there are any arguments present
|
||||||
|
func (a Args) Present() bool {
|
||||||
|
return len(a) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps arguments at the given indexes
|
||||||
|
func (a Args) Swap(from, to int) error {
|
||||||
|
if from >= len(a) || to >= len(a) {
|
||||||
|
return errors.New("index out of range")
|
||||||
|
}
|
||||||
|
a[from], a[to] = a[to], a[from]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func globalContext(ctx *Context) *Context {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if ctx.parentContext == nil {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
ctx = ctx.parentContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet {
|
||||||
|
if ctx.parentContext != nil {
|
||||||
|
ctx = ctx.parentContext
|
||||||
|
}
|
||||||
|
for ; ctx != nil; ctx = ctx.parentContext {
|
||||||
|
if f := ctx.flagSet.Lookup(name); f != nil {
|
||||||
|
return ctx.flagSet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
|
||||||
|
switch ff.Value.(type) {
|
||||||
|
case *StringSlice:
|
||||||
|
default:
|
||||||
|
set.Set(name, ff.Value.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
set.Visit(func(f *flag.Flag) {
|
||||||
|
visited[f.Name] = true
|
||||||
|
})
|
||||||
|
for _, f := range flags {
|
||||||
|
parts := strings.Split(f.GetName(), ",")
|
||||||
|
if len(parts) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var ff *flag.Flag
|
||||||
|
for _, name := range parts {
|
||||||
|
name = strings.Trim(name, " ")
|
||||||
|
if visited[name] {
|
||||||
|
if ff != nil {
|
||||||
|
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
|
||||||
|
}
|
||||||
|
ff = set.Lookup(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ff == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, name := range parts {
|
||||||
|
name = strings.Trim(name, " ")
|
||||||
|
if !visited[name] {
|
||||||
|
copyFlag(name, ff, set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OsExiter is the function used when the app exits. If not set defaults to os.Exit.
|
||||||
|
var OsExiter = os.Exit
|
||||||
|
|
||||||
|
// ErrWriter is used to write errors to the user. This can be anything
|
||||||
|
// implementing the io.Writer interface and defaults to os.Stderr.
|
||||||
|
var ErrWriter io.Writer = os.Stderr
|
||||||
|
|
||||||
|
// MultiError is an error that wraps multiple errors.
|
||||||
|
type MultiError struct {
|
||||||
|
Errors []error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMultiError creates a new MultiError. Pass in one or more errors.
|
||||||
|
func NewMultiError(err ...error) MultiError {
|
||||||
|
return MultiError{Errors: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface.
|
||||||
|
func (m MultiError) Error() string {
|
||||||
|
errs := make([]string, len(m.Errors))
|
||||||
|
for i, err := range m.Errors {
|
||||||
|
errs[i] = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(errs, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorFormatter interface {
|
||||||
|
Format(s fmt.State, verb rune)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExitCoder is the interface checked by `App` and `Command` for a custom exit
|
||||||
|
// code
|
||||||
|
type ExitCoder interface {
|
||||||
|
error
|
||||||
|
ExitCode() int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExitError fulfills both the builtin `error` interface and `ExitCoder`
|
||||||
|
type ExitError struct {
|
||||||
|
exitCode int
|
||||||
|
message interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExitError makes a new *ExitError
|
||||||
|
func NewExitError(message interface{}, exitCode int) *ExitError {
|
||||||
|
return &ExitError{
|
||||||
|
exitCode: exitCode,
|
||||||
|
message: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the string message, fulfilling the interface required by
|
||||||
|
// `error`
|
||||||
|
func (ee *ExitError) Error() string {
|
||||||
|
return fmt.Sprintf("%v", ee.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExitCode returns the exit code, fulfilling the interface required by
|
||||||
|
// `ExitCoder`
|
||||||
|
func (ee *ExitError) ExitCode() int {
|
||||||
|
return ee.exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if
|
||||||
|
// so prints the error to stderr (if it is non-empty) and calls OsExiter with the
|
||||||
|
// given exit code. If the given error is a MultiError, then this func is
|
||||||
|
// called on all members of the Errors slice and calls OsExiter with the last exit code.
|
||||||
|
func HandleExitCoder(err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exitErr, ok := err.(ExitCoder); ok {
|
||||||
|
if err.Error() != "" {
|
||||||
|
if _, ok := exitErr.(ErrorFormatter); ok {
|
||||||
|
fmt.Fprintf(ErrWriter, "%+v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(ErrWriter, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OsExiter(exitErr.ExitCode())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if multiErr, ok := err.(MultiError); ok {
|
||||||
|
code := handleMultiError(multiErr)
|
||||||
|
OsExiter(code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMultiError(multiErr MultiError) int {
|
||||||
|
code := 1
|
||||||
|
for _, merr := range multiErr.Errors {
|
||||||
|
if multiErr2, ok := merr.(MultiError); ok {
|
||||||
|
code = handleMultiError(multiErr2)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(ErrWriter, merr)
|
||||||
|
if exitErr, ok := merr.(ExitCoder); ok {
|
||||||
|
code = exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return code
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Bool",
|
||||||
|
"type": "bool",
|
||||||
|
"value": false,
|
||||||
|
"context_default": "false",
|
||||||
|
"parser": "strconv.ParseBool(f.Value.String())"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BoolT",
|
||||||
|
"type": "bool",
|
||||||
|
"value": false,
|
||||||
|
"doctail": " that is true by default",
|
||||||
|
"context_default": "false",
|
||||||
|
"parser": "strconv.ParseBool(f.Value.String())"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Duration",
|
||||||
|
"type": "time.Duration",
|
||||||
|
"doctail": " (see https://golang.org/pkg/time/#ParseDuration)",
|
||||||
|
"context_default": "0",
|
||||||
|
"parser": "time.ParseDuration(f.Value.String())"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Float64",
|
||||||
|
"type": "float64",
|
||||||
|
"context_default": "0",
|
||||||
|
"parser": "strconv.ParseFloat(f.Value.String(), 64)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Generic",
|
||||||
|
"type": "Generic",
|
||||||
|
"dest": false,
|
||||||
|
"context_default": "nil",
|
||||||
|
"context_type": "interface{}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Int64",
|
||||||
|
"type": "int64",
|
||||||
|
"context_default": "0",
|
||||||
|
"parser": "strconv.ParseInt(f.Value.String(), 0, 64)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Int",
|
||||||
|
"type": "int",
|
||||||
|
"context_default": "0",
|
||||||
|
"parser": "strconv.ParseInt(f.Value.String(), 0, 64)",
|
||||||
|
"parser_cast": "int(parsed)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IntSlice",
|
||||||
|
"type": "*IntSlice",
|
||||||
|
"dest": false,
|
||||||
|
"context_default": "nil",
|
||||||
|
"context_type": "[]int",
|
||||||
|
"parser": "(f.Value.(*IntSlice)).Value(), error(nil)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Int64Slice",
|
||||||
|
"type": "*Int64Slice",
|
||||||
|
"dest": false,
|
||||||
|
"context_default": "nil",
|
||||||
|
"context_type": "[]int64",
|
||||||
|
"parser": "(f.Value.(*Int64Slice)).Value(), error(nil)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "String",
|
||||||
|
"type": "string",
|
||||||
|
"context_default": "\"\"",
|
||||||
|
"parser": "f.Value.String(), error(nil)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "StringSlice",
|
||||||
|
"type": "*StringSlice",
|
||||||
|
"dest": false,
|
||||||
|
"context_default": "nil",
|
||||||
|
"context_type": "[]string",
|
||||||
|
"parser": "(f.Value.(*StringSlice)).Value(), error(nil)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Uint64",
|
||||||
|
"type": "uint64",
|
||||||
|
"context_default": "0",
|
||||||
|
"parser": "strconv.ParseUint(f.Value.String(), 0, 64)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Uint",
|
||||||
|
"type": "uint",
|
||||||
|
"context_default": "0",
|
||||||
|
"parser": "strconv.ParseUint(f.Value.String(), 0, 64)",
|
||||||
|
"parser_cast": "uint(parsed)"
|
||||||
|
}
|
||||||
|
]
|
||||||
+799
@@ -0,0 +1,799 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultPlaceholder = "value"
|
||||||
|
|
||||||
|
// BashCompletionFlag enables bash-completion for all commands and subcommands
|
||||||
|
var BashCompletionFlag Flag = BoolFlag{
|
||||||
|
Name: "generate-bash-completion",
|
||||||
|
Hidden: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// VersionFlag prints the version for the application
|
||||||
|
var VersionFlag Flag = BoolFlag{
|
||||||
|
Name: "version, v",
|
||||||
|
Usage: "print the version",
|
||||||
|
}
|
||||||
|
|
||||||
|
// HelpFlag prints the help for all commands and subcommands
|
||||||
|
// Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand
|
||||||
|
// unless HideHelp is set to true)
|
||||||
|
var HelpFlag Flag = BoolFlag{
|
||||||
|
Name: "help, h",
|
||||||
|
Usage: "show help",
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlagStringer converts a flag definition to a string. This is used by help
|
||||||
|
// to display a flag.
|
||||||
|
var FlagStringer FlagStringFunc = stringifyFlag
|
||||||
|
|
||||||
|
// FlagsByName is a slice of Flag.
|
||||||
|
type FlagsByName []Flag
|
||||||
|
|
||||||
|
func (f FlagsByName) Len() int {
|
||||||
|
return len(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FlagsByName) Less(i, j int) bool {
|
||||||
|
return f[i].GetName() < f[j].GetName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FlagsByName) Swap(i, j int) {
|
||||||
|
f[i], f[j] = f[j], f[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag is a common interface related to parsing flags in cli.
|
||||||
|
// For more advanced flag parsing techniques, it is recommended that
|
||||||
|
// this interface be implemented.
|
||||||
|
type Flag interface {
|
||||||
|
fmt.Stringer
|
||||||
|
// Apply Flag settings to the given flag set
|
||||||
|
Apply(*flag.FlagSet)
|
||||||
|
GetName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorableFlag is an interface that allows us to return errors during apply
|
||||||
|
// it allows flags defined in this library to return errors in a fashion backwards compatible
|
||||||
|
// TODO remove in v2 and modify the existing Flag interface to return errors
|
||||||
|
type errorableFlag interface {
|
||||||
|
Flag
|
||||||
|
|
||||||
|
ApplyWithError(*flag.FlagSet) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
|
||||||
|
set := flag.NewFlagSet(name, flag.ContinueOnError)
|
||||||
|
|
||||||
|
for _, f := range flags {
|
||||||
|
//TODO remove in v2 when errorableFlag is removed
|
||||||
|
if ef, ok := f.(errorableFlag); ok {
|
||||||
|
if err := ef.ApplyWithError(set); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f.Apply(set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func eachName(longName string, fn func(string)) {
|
||||||
|
parts := strings.Split(longName, ",")
|
||||||
|
for _, name := range parts {
|
||||||
|
name = strings.Trim(name, " ")
|
||||||
|
fn(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic is a generic parseable type identified by a specific flag
|
||||||
|
type Generic interface {
|
||||||
|
Set(value string) error
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply takes the flagset and calls Set on the generic flag with the value
|
||||||
|
// provided by the user for parsing by the flag
|
||||||
|
// Ignores parsing errors
|
||||||
|
func (f GenericFlag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError takes the flagset and calls Set on the generic flag with the value
|
||||||
|
// provided by the user for parsing by the flag
|
||||||
|
func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
val := f.Value
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
if err := val.Set(envVal); err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as value for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
set.Var(f.Value, name, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSlice is an opaque type for []string to satisfy flag.Value and flag.Getter
|
||||||
|
type StringSlice []string
|
||||||
|
|
||||||
|
// Set appends the string value to the list of values
|
||||||
|
func (f *StringSlice) Set(value string) error {
|
||||||
|
*f = append(*f, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value (for usage defaults)
|
||||||
|
func (f *StringSlice) String() string {
|
||||||
|
return fmt.Sprintf("%s", *f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the slice of strings set by this flag
|
||||||
|
func (f *StringSlice) Value() []string {
|
||||||
|
return *f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the slice of strings set by this flag
|
||||||
|
func (f *StringSlice) Get() interface{} {
|
||||||
|
return *f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f StringSliceFlag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
newVal := &StringSlice{}
|
||||||
|
for _, s := range strings.Split(envVal, ",") {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if err := newVal.Set(s); err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as string value for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Value = newVal
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Value == nil {
|
||||||
|
f.Value = &StringSlice{}
|
||||||
|
}
|
||||||
|
set.Var(f.Value, name, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntSlice is an opaque type for []int to satisfy flag.Value and flag.Getter
|
||||||
|
type IntSlice []int
|
||||||
|
|
||||||
|
// Set parses the value into an integer and appends it to the list of values
|
||||||
|
func (f *IntSlice) Set(value string) error {
|
||||||
|
tmp, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*f = append(*f, tmp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value (for usage defaults)
|
||||||
|
func (f *IntSlice) String() string {
|
||||||
|
return fmt.Sprintf("%#v", *f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the slice of ints set by this flag
|
||||||
|
func (f *IntSlice) Value() []int {
|
||||||
|
return *f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the slice of ints set by this flag
|
||||||
|
func (f *IntSlice) Get() interface{} {
|
||||||
|
return *f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f IntSliceFlag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
newVal := &IntSlice{}
|
||||||
|
for _, s := range strings.Split(envVal, ",") {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if err := newVal.Set(s); err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Value = newVal
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Value == nil {
|
||||||
|
f.Value = &IntSlice{}
|
||||||
|
}
|
||||||
|
set.Var(f.Value, name, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64Slice is an opaque type for []int to satisfy flag.Value and flag.Getter
|
||||||
|
type Int64Slice []int64
|
||||||
|
|
||||||
|
// Set parses the value into an integer and appends it to the list of values
|
||||||
|
func (f *Int64Slice) Set(value string) error {
|
||||||
|
tmp, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*f = append(*f, tmp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value (for usage defaults)
|
||||||
|
func (f *Int64Slice) String() string {
|
||||||
|
return fmt.Sprintf("%#v", *f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the slice of ints set by this flag
|
||||||
|
func (f *Int64Slice) Value() []int64 {
|
||||||
|
return *f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the slice of ints set by this flag
|
||||||
|
func (f *Int64Slice) Get() interface{} {
|
||||||
|
return *f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f Int64SliceFlag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
newVal := &Int64Slice{}
|
||||||
|
for _, s := range strings.Split(envVal, ",") {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if err := newVal.Set(s); err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Value = newVal
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Value == nil {
|
||||||
|
f.Value = &Int64Slice{}
|
||||||
|
}
|
||||||
|
set.Var(f.Value, name, f.Usage)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f BoolFlag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
val := false
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
if envVal == "" {
|
||||||
|
val = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
envValBool, err := strconv.ParseBool(envVal)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val = envValBool
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Destination != nil {
|
||||||
|
set.BoolVar(f.Destination, name, val, f.Usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set.Bool(name, val, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f BoolTFlag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
val := true
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
if envVal == "" {
|
||||||
|
val = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
envValBool, err := strconv.ParseBool(envVal)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val = envValBool
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Destination != nil {
|
||||||
|
set.BoolVar(f.Destination, name, val, f.Usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set.Bool(name, val, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f StringFlag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f StringFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
f.Value = envVal
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Destination != nil {
|
||||||
|
set.StringVar(f.Destination, name, f.Value, f.Usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set.String(name, f.Value, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f IntFlag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f IntFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
envValInt, err := strconv.ParseInt(envVal, 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
f.Value = int(envValInt)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Destination != nil {
|
||||||
|
set.IntVar(f.Destination, name, f.Value, f.Usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set.Int(name, f.Value, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f Int64Flag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
envValInt, err := strconv.ParseInt(envVal, 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Value = envValInt
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Destination != nil {
|
||||||
|
set.Int64Var(f.Destination, name, f.Value, f.Usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set.Int64(name, f.Value, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f UintFlag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f UintFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
envValInt, err := strconv.ParseUint(envVal, 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as uint value for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Value = uint(envValInt)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Destination != nil {
|
||||||
|
set.UintVar(f.Destination, name, f.Value, f.Usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set.Uint(name, f.Value, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f Uint64Flag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
envValInt, err := strconv.ParseUint(envVal, 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Value = uint64(envValInt)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Destination != nil {
|
||||||
|
set.Uint64Var(f.Destination, name, f.Value, f.Usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set.Uint64(name, f.Value, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f DurationFlag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
envValDuration, err := time.ParseDuration(envVal)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as duration for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Value = envValDuration
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Destination != nil {
|
||||||
|
set.DurationVar(f.Destination, name, f.Value, f.Usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set.Duration(name, f.Value, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply populates the flag given the flag set and environment
|
||||||
|
// Ignores errors
|
||||||
|
func (f Float64Flag) Apply(set *flag.FlagSet) {
|
||||||
|
f.ApplyWithError(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyWithError populates the flag given the flag set and environment
|
||||||
|
func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
for _, envVar := range strings.Split(f.EnvVar, ",") {
|
||||||
|
envVar = strings.TrimSpace(envVar)
|
||||||
|
if envVal, ok := syscall.Getenv(envVar); ok {
|
||||||
|
envValFloat, err := strconv.ParseFloat(envVal, 10)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", envVal, f.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Value = float64(envValFloat)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
if f.Destination != nil {
|
||||||
|
set.Float64Var(f.Destination, name, f.Value, f.Usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set.Float64(name, f.Value, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func visibleFlags(fl []Flag) []Flag {
|
||||||
|
visible := []Flag{}
|
||||||
|
for _, flag := range fl {
|
||||||
|
field := flagValue(flag).FieldByName("Hidden")
|
||||||
|
if !field.IsValid() || !field.Bool() {
|
||||||
|
visible = append(visible, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return visible
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefixFor(name string) (prefix string) {
|
||||||
|
if len(name) == 1 {
|
||||||
|
prefix = "-"
|
||||||
|
} else {
|
||||||
|
prefix = "--"
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the placeholder, if any, and the unquoted usage string.
|
||||||
|
func unquoteUsage(usage string) (string, string) {
|
||||||
|
for i := 0; i < len(usage); i++ {
|
||||||
|
if usage[i] == '`' {
|
||||||
|
for j := i + 1; j < len(usage); j++ {
|
||||||
|
if usage[j] == '`' {
|
||||||
|
name := usage[i+1 : j]
|
||||||
|
usage = usage[:i] + name + usage[j+1:]
|
||||||
|
return name, usage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", usage
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefixedNames(fullName, placeholder string) string {
|
||||||
|
var prefixed string
|
||||||
|
parts := strings.Split(fullName, ",")
|
||||||
|
for i, name := range parts {
|
||||||
|
name = strings.Trim(name, " ")
|
||||||
|
prefixed += prefixFor(name) + name
|
||||||
|
if placeholder != "" {
|
||||||
|
prefixed += " " + placeholder
|
||||||
|
}
|
||||||
|
if i < len(parts)-1 {
|
||||||
|
prefixed += ", "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefixed
|
||||||
|
}
|
||||||
|
|
||||||
|
func withEnvHint(envVar, str string) string {
|
||||||
|
envText := ""
|
||||||
|
if envVar != "" {
|
||||||
|
prefix := "$"
|
||||||
|
suffix := ""
|
||||||
|
sep := ", $"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
prefix = "%"
|
||||||
|
suffix = "%"
|
||||||
|
sep = "%, %"
|
||||||
|
}
|
||||||
|
envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(strings.Split(envVar, ","), sep), suffix)
|
||||||
|
}
|
||||||
|
return str + envText
|
||||||
|
}
|
||||||
|
|
||||||
|
func flagValue(f Flag) reflect.Value {
|
||||||
|
fv := reflect.ValueOf(f)
|
||||||
|
for fv.Kind() == reflect.Ptr {
|
||||||
|
fv = reflect.Indirect(fv)
|
||||||
|
}
|
||||||
|
return fv
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyFlag(f Flag) string {
|
||||||
|
fv := flagValue(f)
|
||||||
|
|
||||||
|
switch f.(type) {
|
||||||
|
case IntSliceFlag:
|
||||||
|
return withEnvHint(fv.FieldByName("EnvVar").String(),
|
||||||
|
stringifyIntSliceFlag(f.(IntSliceFlag)))
|
||||||
|
case Int64SliceFlag:
|
||||||
|
return withEnvHint(fv.FieldByName("EnvVar").String(),
|
||||||
|
stringifyInt64SliceFlag(f.(Int64SliceFlag)))
|
||||||
|
case StringSliceFlag:
|
||||||
|
return withEnvHint(fv.FieldByName("EnvVar").String(),
|
||||||
|
stringifyStringSliceFlag(f.(StringSliceFlag)))
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String())
|
||||||
|
|
||||||
|
needsPlaceholder := false
|
||||||
|
defaultValueString := ""
|
||||||
|
|
||||||
|
if val := fv.FieldByName("Value"); val.IsValid() {
|
||||||
|
needsPlaceholder = true
|
||||||
|
defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface())
|
||||||
|
|
||||||
|
if val.Kind() == reflect.String && val.String() != "" {
|
||||||
|
defaultValueString = fmt.Sprintf(" (default: %q)", val.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaultValueString == " (default: )" {
|
||||||
|
defaultValueString = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsPlaceholder && placeholder == "" {
|
||||||
|
placeholder = defaultPlaceholder
|
||||||
|
}
|
||||||
|
|
||||||
|
usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultValueString))
|
||||||
|
|
||||||
|
return withEnvHint(fv.FieldByName("EnvVar").String(),
|
||||||
|
fmt.Sprintf("%s\t%s", prefixedNames(fv.FieldByName("Name").String(), placeholder), usageWithDefault))
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyIntSliceFlag(f IntSliceFlag) string {
|
||||||
|
defaultVals := []string{}
|
||||||
|
if f.Value != nil && len(f.Value.Value()) > 0 {
|
||||||
|
for _, i := range f.Value.Value() {
|
||||||
|
defaultVals = append(defaultVals, fmt.Sprintf("%d", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyInt64SliceFlag(f Int64SliceFlag) string {
|
||||||
|
defaultVals := []string{}
|
||||||
|
if f.Value != nil && len(f.Value.Value()) > 0 {
|
||||||
|
for _, i := range f.Value.Value() {
|
||||||
|
defaultVals = append(defaultVals, fmt.Sprintf("%d", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyStringSliceFlag(f StringSliceFlag) string {
|
||||||
|
defaultVals := []string{}
|
||||||
|
if f.Value != nil && len(f.Value.Value()) > 0 {
|
||||||
|
for _, s := range f.Value.Value() {
|
||||||
|
if len(s) > 0 {
|
||||||
|
defaultVals = append(defaultVals, fmt.Sprintf("%q", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifySliceFlag(usage, name string, defaultVals []string) string {
|
||||||
|
placeholder, usage := unquoteUsage(usage)
|
||||||
|
if placeholder == "" {
|
||||||
|
placeholder = defaultPlaceholder
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultVal := ""
|
||||||
|
if len(defaultVals) > 0 {
|
||||||
|
defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal))
|
||||||
|
return fmt.Sprintf("%s\t%s", prefixedNames(name, placeholder), usageWithDefault)
|
||||||
|
}
|
||||||
+627
@@ -0,0 +1,627 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WARNING: This file is generated!
|
||||||
|
|
||||||
|
// BoolFlag is a flag with type bool
|
||||||
|
type BoolFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Destination *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f BoolFlag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f BoolFlag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool looks up the value of a local BoolFlag, returns
|
||||||
|
// false if not found
|
||||||
|
func (c *Context) Bool(name string) bool {
|
||||||
|
return lookupBool(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalBool looks up the value of a global BoolFlag, returns
|
||||||
|
// false if not found
|
||||||
|
func (c *Context) GlobalBool(name string) bool {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupBool(name, fs)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupBool(name string, set *flag.FlagSet) bool {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := strconv.ParseBool(f.Value.String())
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoolTFlag is a flag with type bool that is true by default
|
||||||
|
type BoolTFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Destination *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f BoolTFlag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f BoolTFlag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoolT looks up the value of a local BoolTFlag, returns
|
||||||
|
// false if not found
|
||||||
|
func (c *Context) BoolT(name string) bool {
|
||||||
|
return lookupBoolT(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalBoolT looks up the value of a global BoolTFlag, returns
|
||||||
|
// false if not found
|
||||||
|
func (c *Context) GlobalBoolT(name string) bool {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupBoolT(name, fs)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupBoolT(name string, set *flag.FlagSet) bool {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := strconv.ParseBool(f.Value.String())
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration)
|
||||||
|
type DurationFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Value time.Duration
|
||||||
|
Destination *time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f DurationFlag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f DurationFlag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration looks up the value of a local DurationFlag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) Duration(name string) time.Duration {
|
||||||
|
return lookupDuration(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalDuration looks up the value of a global DurationFlag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) GlobalDuration(name string) time.Duration {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupDuration(name, fs)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupDuration(name string, set *flag.FlagSet) time.Duration {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := time.ParseDuration(f.Value.String())
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64Flag is a flag with type float64
|
||||||
|
type Float64Flag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Value float64
|
||||||
|
Destination *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f Float64Flag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f Float64Flag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 looks up the value of a local Float64Flag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) Float64(name string) float64 {
|
||||||
|
return lookupFloat64(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalFloat64 looks up the value of a global Float64Flag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) GlobalFloat64(name string) float64 {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupFloat64(name, fs)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupFloat64(name string, set *flag.FlagSet) float64 {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := strconv.ParseFloat(f.Value.String(), 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenericFlag is a flag with type Generic
|
||||||
|
type GenericFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Value Generic
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f GenericFlag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f GenericFlag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic looks up the value of a local GenericFlag, returns
|
||||||
|
// nil if not found
|
||||||
|
func (c *Context) Generic(name string) interface{} {
|
||||||
|
return lookupGeneric(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalGeneric looks up the value of a global GenericFlag, returns
|
||||||
|
// nil if not found
|
||||||
|
func (c *Context) GlobalGeneric(name string) interface{} {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupGeneric(name, fs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupGeneric(name string, set *flag.FlagSet) interface{} {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := f.Value, error(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64Flag is a flag with type int64
|
||||||
|
type Int64Flag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Value int64
|
||||||
|
Destination *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f Int64Flag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f Int64Flag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 looks up the value of a local Int64Flag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) Int64(name string) int64 {
|
||||||
|
return lookupInt64(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalInt64 looks up the value of a global Int64Flag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) GlobalInt64(name string) int64 {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupInt64(name, fs)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupInt64(name string, set *flag.FlagSet) int64 {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := strconv.ParseInt(f.Value.String(), 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntFlag is a flag with type int
|
||||||
|
type IntFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Value int
|
||||||
|
Destination *int
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f IntFlag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f IntFlag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int looks up the value of a local IntFlag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) Int(name string) int {
|
||||||
|
return lookupInt(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalInt looks up the value of a global IntFlag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) GlobalInt(name string) int {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupInt(name, fs)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupInt(name string, set *flag.FlagSet) int {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := strconv.ParseInt(f.Value.String(), 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(parsed)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntSliceFlag is a flag with type *IntSlice
|
||||||
|
type IntSliceFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Value *IntSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f IntSliceFlag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f IntSliceFlag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntSlice looks up the value of a local IntSliceFlag, returns
|
||||||
|
// nil if not found
|
||||||
|
func (c *Context) IntSlice(name string) []int {
|
||||||
|
return lookupIntSlice(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalIntSlice looks up the value of a global IntSliceFlag, returns
|
||||||
|
// nil if not found
|
||||||
|
func (c *Context) GlobalIntSlice(name string) []int {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupIntSlice(name, fs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupIntSlice(name string, set *flag.FlagSet) []int {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := (f.Value.(*IntSlice)).Value(), error(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64SliceFlag is a flag with type *Int64Slice
|
||||||
|
type Int64SliceFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Value *Int64Slice
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f Int64SliceFlag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f Int64SliceFlag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64Slice looks up the value of a local Int64SliceFlag, returns
|
||||||
|
// nil if not found
|
||||||
|
func (c *Context) Int64Slice(name string) []int64 {
|
||||||
|
return lookupInt64Slice(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalInt64Slice looks up the value of a global Int64SliceFlag, returns
|
||||||
|
// nil if not found
|
||||||
|
func (c *Context) GlobalInt64Slice(name string) []int64 {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupInt64Slice(name, fs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupInt64Slice(name string, set *flag.FlagSet) []int64 {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := (f.Value.(*Int64Slice)).Value(), error(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringFlag is a flag with type string
|
||||||
|
type StringFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Value string
|
||||||
|
Destination *string
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f StringFlag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f StringFlag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// String looks up the value of a local StringFlag, returns
|
||||||
|
// "" if not found
|
||||||
|
func (c *Context) String(name string) string {
|
||||||
|
return lookupString(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalString looks up the value of a global StringFlag, returns
|
||||||
|
// "" if not found
|
||||||
|
func (c *Context) GlobalString(name string) string {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupString(name, fs)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupString(name string, set *flag.FlagSet) string {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := f.Value.String(), error(nil)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSliceFlag is a flag with type *StringSlice
|
||||||
|
type StringSliceFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Value *StringSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f StringSliceFlag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f StringSliceFlag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSlice looks up the value of a local StringSliceFlag, returns
|
||||||
|
// nil if not found
|
||||||
|
func (c *Context) StringSlice(name string) []string {
|
||||||
|
return lookupStringSlice(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalStringSlice looks up the value of a global StringSliceFlag, returns
|
||||||
|
// nil if not found
|
||||||
|
func (c *Context) GlobalStringSlice(name string) []string {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupStringSlice(name, fs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupStringSlice(name string, set *flag.FlagSet) []string {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := (f.Value.(*StringSlice)).Value(), error(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint64Flag is a flag with type uint64
|
||||||
|
type Uint64Flag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Value uint64
|
||||||
|
Destination *uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f Uint64Flag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f Uint64Flag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint64 looks up the value of a local Uint64Flag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) Uint64(name string) uint64 {
|
||||||
|
return lookupUint64(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalUint64 looks up the value of a global Uint64Flag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) GlobalUint64(name string) uint64 {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupUint64(name, fs)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupUint64(name string, set *flag.FlagSet) uint64 {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := strconv.ParseUint(f.Value.String(), 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UintFlag is a flag with type uint
|
||||||
|
type UintFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
Value uint
|
||||||
|
Destination *uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f UintFlag) String() string {
|
||||||
|
return FlagStringer(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f UintFlag) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint looks up the value of a local UintFlag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) Uint(name string) uint {
|
||||||
|
return lookupUint(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalUint looks up the value of a global UintFlag, returns
|
||||||
|
// 0 if not found
|
||||||
|
func (c *Context) GlobalUint(name string) uint {
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {
|
||||||
|
return lookupUint(name, fs)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupUint(name string, set *flag.FlagSet) uint {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
parsed, err := strconv.ParseUint(f.Value.String(), 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uint(parsed)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
// BashCompleteFunc is an action to execute when the bash-completion flag is set
|
||||||
|
type BashCompleteFunc func(*Context)
|
||||||
|
|
||||||
|
// BeforeFunc is an action to execute before any subcommands are run, but after
|
||||||
|
// the context is ready if a non-nil error is returned, no subcommands are run
|
||||||
|
type BeforeFunc func(*Context) error
|
||||||
|
|
||||||
|
// AfterFunc is an action to execute after any subcommands are run, but after the
|
||||||
|
// subcommand has finished it is run even if Action() panics
|
||||||
|
type AfterFunc func(*Context) error
|
||||||
|
|
||||||
|
// ActionFunc is the action to execute when no subcommands are specified
|
||||||
|
type ActionFunc func(*Context) error
|
||||||
|
|
||||||
|
// CommandNotFoundFunc is executed if the proper command cannot be found
|
||||||
|
type CommandNotFoundFunc func(*Context, string)
|
||||||
|
|
||||||
|
// OnUsageErrorFunc is executed if an usage error occurs. This is useful for displaying
|
||||||
|
// customized usage error messages. This function is able to replace the
|
||||||
|
// original error messages. If this function is not set, the "Incorrect usage"
|
||||||
|
// is displayed and the execution is interrupted.
|
||||||
|
type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error
|
||||||
|
|
||||||
|
// FlagStringFunc is used by the help generation to display a flag, which is
|
||||||
|
// expected to be a single line.
|
||||||
|
type FlagStringFunc func(Flag) string
|
||||||
+255
@@ -0,0 +1,255 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
The flag types that ship with the cli library have many things in common, and
|
||||||
|
so we can take advantage of the `go generate` command to create much of the
|
||||||
|
source code from a list of definitions. These definitions attempt to cover
|
||||||
|
the parts that vary between flag types, and should evolve as needed.
|
||||||
|
|
||||||
|
An example of the minimum definition needed is:
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "SomeType",
|
||||||
|
"type": "sometype",
|
||||||
|
"context_default": "nil"
|
||||||
|
}
|
||||||
|
|
||||||
|
In this example, the code generated for the `cli` package will include a type
|
||||||
|
named `SomeTypeFlag` that is expected to wrap a value of type `sometype`.
|
||||||
|
Fetching values by name via `*cli.Context` will default to a value of `nil`.
|
||||||
|
|
||||||
|
A more complete, albeit somewhat redundant, example showing all available
|
||||||
|
definition keys is:
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "VeryMuchType",
|
||||||
|
"type": "*VeryMuchType",
|
||||||
|
"value": true,
|
||||||
|
"dest": false,
|
||||||
|
"doctail": " which really only wraps a []float64, oh well!",
|
||||||
|
"context_type": "[]float64",
|
||||||
|
"context_default": "nil",
|
||||||
|
"parser": "parseVeryMuchType(f.Value.String())",
|
||||||
|
"parser_cast": "[]float64(parsed)"
|
||||||
|
}
|
||||||
|
|
||||||
|
The meaning of each field is as follows:
|
||||||
|
|
||||||
|
name (string) - The type "name", which will be suffixed with
|
||||||
|
`Flag` when generating the type definition
|
||||||
|
for `cli` and the wrapper type for `altsrc`
|
||||||
|
type (string) - The type that the generated `Flag` type for `cli`
|
||||||
|
is expected to "contain" as its `.Value` member
|
||||||
|
value (bool) - Should the generated `cli` type have a `Value`
|
||||||
|
member?
|
||||||
|
dest (bool) - Should the generated `cli` type support a
|
||||||
|
destination pointer?
|
||||||
|
doctail (string) - Additional docs for the `cli` flag type comment
|
||||||
|
context_type (string) - The literal type used in the `*cli.Context`
|
||||||
|
reader func signature
|
||||||
|
context_default (string) - The literal value used as the default by the
|
||||||
|
`*cli.Context` reader funcs when no value is
|
||||||
|
present
|
||||||
|
parser (string) - Literal code used to parse the flag `f`,
|
||||||
|
expected to have a return signature of
|
||||||
|
(value, error)
|
||||||
|
parser_cast (string) - Literal code used to cast the `parsed` value
|
||||||
|
returned from the `parser` code
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
|
||||||
|
class _FancyFormatter(argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
argparse.RawDescriptionHelpFormatter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main(sysargs=sys.argv[:]):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Generate flag type code!',
|
||||||
|
formatter_class=_FancyFormatter)
|
||||||
|
parser.add_argument(
|
||||||
|
'package',
|
||||||
|
type=str, default='cli', choices=_WRITEFUNCS.keys(),
|
||||||
|
help='Package for which flag types will be generated'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-i', '--in-json',
|
||||||
|
type=argparse.FileType('r'),
|
||||||
|
default=sys.stdin,
|
||||||
|
help='Input JSON file which defines each type to be generated'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-o', '--out-go',
|
||||||
|
type=argparse.FileType('w'),
|
||||||
|
default=sys.stdout,
|
||||||
|
help='Output file/stream to which generated source will be written'
|
||||||
|
)
|
||||||
|
parser.epilog = __doc__
|
||||||
|
|
||||||
|
args = parser.parse_args(sysargs[1:])
|
||||||
|
_generate_flag_types(_WRITEFUNCS[args.package], args.out_go, args.in_json)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_flag_types(writefunc, output_go, input_json):
|
||||||
|
types = json.load(input_json)
|
||||||
|
|
||||||
|
tmp = tempfile.NamedTemporaryFile(suffix='.go', delete=False)
|
||||||
|
writefunc(tmp, types)
|
||||||
|
tmp.close()
|
||||||
|
|
||||||
|
new_content = subprocess.check_output(
|
||||||
|
['goimports', tmp.name]
|
||||||
|
).decode('utf-8')
|
||||||
|
|
||||||
|
print(new_content, file=output_go, end='')
|
||||||
|
output_go.flush()
|
||||||
|
os.remove(tmp.name)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_typedef_defaults(typedef):
|
||||||
|
typedef.setdefault('doctail', '')
|
||||||
|
typedef.setdefault('context_type', typedef['type'])
|
||||||
|
typedef.setdefault('dest', True)
|
||||||
|
typedef.setdefault('value', True)
|
||||||
|
typedef.setdefault('parser', 'f.Value, error(nil)')
|
||||||
|
typedef.setdefault('parser_cast', 'parsed')
|
||||||
|
|
||||||
|
|
||||||
|
def _write_cli_flag_types(outfile, types):
|
||||||
|
_fwrite(outfile, """\
|
||||||
|
package cli
|
||||||
|
|
||||||
|
// WARNING: This file is generated!
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
for typedef in types:
|
||||||
|
_set_typedef_defaults(typedef)
|
||||||
|
|
||||||
|
_fwrite(outfile, """\
|
||||||
|
// {name}Flag is a flag with type {type}{doctail}
|
||||||
|
type {name}Flag struct {{
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
Hidden bool
|
||||||
|
""".format(**typedef))
|
||||||
|
|
||||||
|
if typedef['value']:
|
||||||
|
_fwrite(outfile, """\
|
||||||
|
Value {type}
|
||||||
|
""".format(**typedef))
|
||||||
|
|
||||||
|
if typedef['dest']:
|
||||||
|
_fwrite(outfile, """\
|
||||||
|
Destination *{type}
|
||||||
|
""".format(**typedef))
|
||||||
|
|
||||||
|
_fwrite(outfile, "\n}\n\n")
|
||||||
|
|
||||||
|
_fwrite(outfile, """\
|
||||||
|
// String returns a readable representation of this value
|
||||||
|
// (for usage defaults)
|
||||||
|
func (f {name}Flag) String() string {{
|
||||||
|
return FlagStringer(f)
|
||||||
|
}}
|
||||||
|
|
||||||
|
// GetName returns the name of the flag
|
||||||
|
func (f {name}Flag) GetName() string {{
|
||||||
|
return f.Name
|
||||||
|
}}
|
||||||
|
|
||||||
|
// {name} looks up the value of a local {name}Flag, returns
|
||||||
|
// {context_default} if not found
|
||||||
|
func (c *Context) {name}(name string) {context_type} {{
|
||||||
|
return lookup{name}(name, c.flagSet)
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Global{name} looks up the value of a global {name}Flag, returns
|
||||||
|
// {context_default} if not found
|
||||||
|
func (c *Context) Global{name}(name string) {context_type} {{
|
||||||
|
if fs := lookupGlobalFlagSet(name, c); fs != nil {{
|
||||||
|
return lookup{name}(name, fs)
|
||||||
|
}}
|
||||||
|
return {context_default}
|
||||||
|
}}
|
||||||
|
|
||||||
|
func lookup{name}(name string, set *flag.FlagSet) {context_type} {{
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {{
|
||||||
|
parsed, err := {parser}
|
||||||
|
if err != nil {{
|
||||||
|
return {context_default}
|
||||||
|
}}
|
||||||
|
return {parser_cast}
|
||||||
|
}}
|
||||||
|
return {context_default}
|
||||||
|
}}
|
||||||
|
""".format(**typedef))
|
||||||
|
|
||||||
|
|
||||||
|
def _write_altsrc_flag_types(outfile, types):
|
||||||
|
_fwrite(outfile, """\
|
||||||
|
package altsrc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/urfave/cli.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WARNING: This file is generated!
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
for typedef in types:
|
||||||
|
_set_typedef_defaults(typedef)
|
||||||
|
|
||||||
|
_fwrite(outfile, """\
|
||||||
|
// {name}Flag is the flag type that wraps cli.{name}Flag to allow
|
||||||
|
// for other values to be specified
|
||||||
|
type {name}Flag struct {{
|
||||||
|
cli.{name}Flag
|
||||||
|
set *flag.FlagSet
|
||||||
|
}}
|
||||||
|
|
||||||
|
// New{name}Flag creates a new {name}Flag
|
||||||
|
func New{name}Flag(fl cli.{name}Flag) *{name}Flag {{
|
||||||
|
return &{name}Flag{{{name}Flag: fl, set: nil}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Apply saves the flagSet for later usage calls, then calls the
|
||||||
|
// wrapped {name}Flag.Apply
|
||||||
|
func (f *{name}Flag) Apply(set *flag.FlagSet) {{
|
||||||
|
f.set = set
|
||||||
|
f.{name}Flag.Apply(set)
|
||||||
|
}}
|
||||||
|
|
||||||
|
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||||
|
// wrapped {name}Flag.ApplyWithError
|
||||||
|
func (f *{name}Flag) ApplyWithError(set *flag.FlagSet) error {{
|
||||||
|
f.set = set
|
||||||
|
return f.{name}Flag.ApplyWithError(set)
|
||||||
|
}}
|
||||||
|
""".format(**typedef))
|
||||||
|
|
||||||
|
|
||||||
|
def _fwrite(outfile, text):
|
||||||
|
print(textwrap.dedent(text), end='', file=outfile)
|
||||||
|
|
||||||
|
|
||||||
|
_WRITEFUNCS = {
|
||||||
|
'cli': _write_cli_flag_types,
|
||||||
|
'altsrc': _write_altsrc_flag_types
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
+338
@@ -0,0 +1,338 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppHelpTemplate is the text template for the Default help topic.
|
||||||
|
// cli.go uses text/template to render templates. You can
|
||||||
|
// render custom help text by setting this variable.
|
||||||
|
var AppHelpTemplate = `NAME:
|
||||||
|
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}
|
||||||
|
|
||||||
|
VERSION:
|
||||||
|
{{.Version}}{{end}}{{end}}{{if .Description}}
|
||||||
|
|
||||||
|
DESCRIPTION:
|
||||||
|
{{.Description}}{{end}}{{if len .Authors}}
|
||||||
|
|
||||||
|
AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
|
||||||
|
{{range $index, $author := .Authors}}{{if $index}}
|
||||||
|
{{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}}
|
||||||
|
|
||||||
|
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
|
||||||
|
{{.Name}}:{{end}}{{range .VisibleCommands}}
|
||||||
|
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
|
||||||
|
|
||||||
|
GLOBAL OPTIONS:
|
||||||
|
{{range $index, $option := .VisibleFlags}}{{if $index}}
|
||||||
|
{{end}}{{$option}}{{end}}{{end}}{{if .Copyright}}
|
||||||
|
|
||||||
|
COPYRIGHT:
|
||||||
|
{{.Copyright}}{{end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
// CommandHelpTemplate is the text template for the command help topic.
|
||||||
|
// cli.go uses text/template to render templates. You can
|
||||||
|
// render custom help text by setting this variable.
|
||||||
|
var CommandHelpTemplate = `NAME:
|
||||||
|
{{.HelpName}} - {{.Usage}}
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
|
||||||
|
|
||||||
|
CATEGORY:
|
||||||
|
{{.Category}}{{end}}{{if .Description}}
|
||||||
|
|
||||||
|
DESCRIPTION:
|
||||||
|
{{.Description}}{{end}}{{if .VisibleFlags}}
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
{{range .VisibleFlags}}{{.}}
|
||||||
|
{{end}}{{end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
// SubcommandHelpTemplate is the text template for the subcommand help topic.
|
||||||
|
// cli.go uses text/template to render templates. You can
|
||||||
|
// render custom help text by setting this variable.
|
||||||
|
var SubcommandHelpTemplate = `NAME:
|
||||||
|
{{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}}
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}
|
||||||
|
|
||||||
|
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
|
||||||
|
{{.Name}}:{{end}}{{range .VisibleCommands}}
|
||||||
|
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}
|
||||||
|
{{end}}{{if .VisibleFlags}}
|
||||||
|
OPTIONS:
|
||||||
|
{{range .VisibleFlags}}{{.}}
|
||||||
|
{{end}}{{end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
var helpCommand = Command{
|
||||||
|
Name: "help",
|
||||||
|
Aliases: []string{"h"},
|
||||||
|
Usage: "Shows a list of commands or help for one command",
|
||||||
|
ArgsUsage: "[command]",
|
||||||
|
Action: func(c *Context) error {
|
||||||
|
args := c.Args()
|
||||||
|
if args.Present() {
|
||||||
|
return ShowCommandHelp(c, args.First())
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowAppHelp(c)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var helpSubcommand = Command{
|
||||||
|
Name: "help",
|
||||||
|
Aliases: []string{"h"},
|
||||||
|
Usage: "Shows a list of commands or help for one command",
|
||||||
|
ArgsUsage: "[command]",
|
||||||
|
Action: func(c *Context) error {
|
||||||
|
args := c.Args()
|
||||||
|
if args.Present() {
|
||||||
|
return ShowCommandHelp(c, args.First())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ShowSubcommandHelp(c)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints help for the App or Command
|
||||||
|
type helpPrinter func(w io.Writer, templ string, data interface{})
|
||||||
|
|
||||||
|
// Prints help for the App or Command with custom template function.
|
||||||
|
type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{})
|
||||||
|
|
||||||
|
// HelpPrinter is a function that writes the help output. If not set a default
|
||||||
|
// is used. The function signature is:
|
||||||
|
// func(w io.Writer, templ string, data interface{})
|
||||||
|
var HelpPrinter helpPrinter = printHelp
|
||||||
|
|
||||||
|
// HelpPrinterCustom is same as HelpPrinter but
|
||||||
|
// takes a custom function for template function map.
|
||||||
|
var HelpPrinterCustom helpPrinterCustom = printHelpCustom
|
||||||
|
|
||||||
|
// VersionPrinter prints the version for the App
|
||||||
|
var VersionPrinter = printVersion
|
||||||
|
|
||||||
|
// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code.
|
||||||
|
func ShowAppHelpAndExit(c *Context, exitCode int) {
|
||||||
|
ShowAppHelp(c)
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowAppHelp is an action that displays the help.
|
||||||
|
func ShowAppHelp(c *Context) (err error) {
|
||||||
|
if c.App.CustomAppHelpTemplate == "" {
|
||||||
|
HelpPrinter(c.App.Writer, AppHelpTemplate, c.App)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
customAppData := func() map[string]interface{} {
|
||||||
|
if c.App.ExtraInfo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"ExtraInfo": c.App.ExtraInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HelpPrinterCustom(c.App.Writer, c.App.CustomAppHelpTemplate, c.App, customAppData())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAppComplete prints the list of subcommands as the default app completion method
|
||||||
|
func DefaultAppComplete(c *Context) {
|
||||||
|
for _, command := range c.App.Commands {
|
||||||
|
if command.Hidden {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, name := range command.Names() {
|
||||||
|
fmt.Fprintln(c.App.Writer, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowCommandHelpAndExit - exits with code after showing help
|
||||||
|
func ShowCommandHelpAndExit(c *Context, command string, code int) {
|
||||||
|
ShowCommandHelp(c, command)
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowCommandHelp prints help for the given command
|
||||||
|
func ShowCommandHelp(ctx *Context, command string) error {
|
||||||
|
// show the subcommand help for a command with subcommands
|
||||||
|
if command == "" {
|
||||||
|
HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range ctx.App.Commands {
|
||||||
|
if c.HasName(command) {
|
||||||
|
if c.CustomHelpTemplate != "" {
|
||||||
|
HelpPrinterCustom(ctx.App.Writer, c.CustomHelpTemplate, c, nil)
|
||||||
|
} else {
|
||||||
|
HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.App.CommandNotFound == nil {
|
||||||
|
return NewExitError(fmt.Sprintf("No help topic for '%v'", command), 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.App.CommandNotFound(ctx, command)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowSubcommandHelp prints help for the given subcommand
|
||||||
|
func ShowSubcommandHelp(c *Context) error {
|
||||||
|
return ShowCommandHelp(c, c.Command.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowVersion prints the version number of the App
|
||||||
|
func ShowVersion(c *Context) {
|
||||||
|
VersionPrinter(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printVersion(c *Context) {
|
||||||
|
fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowCompletions prints the lists of commands within a given context
|
||||||
|
func ShowCompletions(c *Context) {
|
||||||
|
a := c.App
|
||||||
|
if a != nil && a.BashComplete != nil {
|
||||||
|
a.BashComplete(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowCommandCompletions prints the custom completions for a given command
|
||||||
|
func ShowCommandCompletions(ctx *Context, command string) {
|
||||||
|
c := ctx.App.Command(command)
|
||||||
|
if c != nil && c.BashComplete != nil {
|
||||||
|
c.BashComplete(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) {
|
||||||
|
funcMap := template.FuncMap{
|
||||||
|
"join": strings.Join,
|
||||||
|
}
|
||||||
|
if customFunc != nil {
|
||||||
|
for key, value := range customFunc {
|
||||||
|
funcMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0)
|
||||||
|
t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
|
||||||
|
err := t.Execute(w, data)
|
||||||
|
if err != nil {
|
||||||
|
// If the writer is closed, t.Execute will fail, and there's nothing
|
||||||
|
// we can do to recover.
|
||||||
|
if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" {
|
||||||
|
fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp(out io.Writer, templ string, data interface{}) {
|
||||||
|
printHelpCustom(out, templ, data, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkVersion(c *Context) bool {
|
||||||
|
found := false
|
||||||
|
if VersionFlag.GetName() != "" {
|
||||||
|
eachName(VersionFlag.GetName(), func(name string) {
|
||||||
|
if c.GlobalBool(name) || c.Bool(name) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHelp(c *Context) bool {
|
||||||
|
found := false
|
||||||
|
if HelpFlag.GetName() != "" {
|
||||||
|
eachName(HelpFlag.GetName(), func(name string) {
|
||||||
|
if c.GlobalBool(name) || c.Bool(name) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCommandHelp(c *Context, name string) bool {
|
||||||
|
if c.Bool("h") || c.Bool("help") {
|
||||||
|
ShowCommandHelp(c, name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSubcommandHelp(c *Context) bool {
|
||||||
|
if c.Bool("h") || c.Bool("help") {
|
||||||
|
ShowSubcommandHelp(c)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) {
|
||||||
|
if !a.EnableBashCompletion {
|
||||||
|
return false, arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
pos := len(arguments) - 1
|
||||||
|
lastArg := arguments[pos]
|
||||||
|
|
||||||
|
if lastArg != "--"+BashCompletionFlag.GetName() {
|
||||||
|
return false, arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, arguments[:pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCompletions(c *Context) bool {
|
||||||
|
if !c.shellComplete {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if args := c.Args(); args.Present() {
|
||||||
|
name := args.First()
|
||||||
|
if cmd := c.App.Command(name); cmd != nil {
|
||||||
|
// let the command handle the completion
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowCompletions(c)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCommandCompletions(c *Context, name string) bool {
|
||||||
|
if !c.shellComplete {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowCommandCompletions(c, name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from subprocess import check_call, check_output
|
||||||
|
|
||||||
|
|
||||||
|
PACKAGE_NAME = os.environ.get(
|
||||||
|
'CLI_PACKAGE_NAME', 'github.com/urfave/cli'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main(sysargs=sys.argv[:]):
|
||||||
|
targets = {
|
||||||
|
'vet': _vet,
|
||||||
|
'test': _test,
|
||||||
|
'gfmrun': _gfmrun,
|
||||||
|
'toc': _toc,
|
||||||
|
'gen': _gen,
|
||||||
|
}
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
'target', nargs='?', choices=tuple(targets.keys()), default='test'
|
||||||
|
)
|
||||||
|
args = parser.parse_args(sysargs[1:])
|
||||||
|
|
||||||
|
targets[args.target]()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _test():
|
||||||
|
if check_output('go version'.split()).split()[2] < 'go1.2':
|
||||||
|
_run('go test -v .')
|
||||||
|
return
|
||||||
|
|
||||||
|
coverprofiles = []
|
||||||
|
for subpackage in ['', 'altsrc']:
|
||||||
|
coverprofile = 'cli.coverprofile'
|
||||||
|
if subpackage != '':
|
||||||
|
coverprofile = '{}.coverprofile'.format(subpackage)
|
||||||
|
|
||||||
|
coverprofiles.append(coverprofile)
|
||||||
|
|
||||||
|
_run('go test -v'.split() + [
|
||||||
|
'-coverprofile={}'.format(coverprofile),
|
||||||
|
('{}/{}'.format(PACKAGE_NAME, subpackage)).rstrip('/')
|
||||||
|
])
|
||||||
|
|
||||||
|
combined_name = _combine_coverprofiles(coverprofiles)
|
||||||
|
_run('go tool cover -func={}'.format(combined_name))
|
||||||
|
os.remove(combined_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _gfmrun():
|
||||||
|
go_version = check_output('go version'.split()).split()[2]
|
||||||
|
if go_version < 'go1.3':
|
||||||
|
print('runtests: skip on {}'.format(go_version), file=sys.stderr)
|
||||||
|
return
|
||||||
|
_run(['gfmrun', '-c', str(_gfmrun_count()), '-s', 'README.md'])
|
||||||
|
|
||||||
|
|
||||||
|
def _vet():
|
||||||
|
_run('go vet ./...')
|
||||||
|
|
||||||
|
|
||||||
|
def _toc():
|
||||||
|
_run('node_modules/.bin/markdown-toc -i README.md')
|
||||||
|
_run('git diff --exit-code')
|
||||||
|
|
||||||
|
|
||||||
|
def _gen():
|
||||||
|
go_version = check_output('go version'.split()).split()[2]
|
||||||
|
if go_version < 'go1.5':
|
||||||
|
print('runtests: skip on {}'.format(go_version), file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
_run('go generate ./...')
|
||||||
|
_run('git diff --exit-code')
|
||||||
|
|
||||||
|
|
||||||
|
def _run(command):
|
||||||
|
if hasattr(command, 'split'):
|
||||||
|
command = command.split()
|
||||||
|
print('runtests: {}'.format(' '.join(command)), file=sys.stderr)
|
||||||
|
check_call(command)
|
||||||
|
|
||||||
|
|
||||||
|
def _gfmrun_count():
|
||||||
|
with open('README.md') as infile:
|
||||||
|
lines = infile.read().splitlines()
|
||||||
|
return len(filter(_is_go_runnable, lines))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_go_runnable(line):
|
||||||
|
return line.startswith('package main')
|
||||||
|
|
||||||
|
|
||||||
|
def _combine_coverprofiles(coverprofiles):
|
||||||
|
combined = tempfile.NamedTemporaryFile(
|
||||||
|
suffix='.coverprofile', delete=False
|
||||||
|
)
|
||||||
|
combined.write('mode: set\n')
|
||||||
|
|
||||||
|
for coverprofile in coverprofiles:
|
||||||
|
with open(coverprofile, 'r') as infile:
|
||||||
|
for line in infile.readlines():
|
||||||
|
if not line.startswith('mode: '):
|
||||||
|
combined.write(line)
|
||||||
|
|
||||||
|
combined.flush()
|
||||||
|
name = combined.name
|
||||||
|
combined.close()
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
+241
@@ -0,0 +1,241 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// link message struct
|
||||||
|
type LinkMsg struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
MessageURL string `json:"messageURL"`
|
||||||
|
PicURL string `json:"picURL"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// action card message struct
|
||||||
|
type ActionCard struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
SingleTitle string `json:"singleTitle"`
|
||||||
|
SingleURL string `json:"singleURL"`
|
||||||
|
BtnOrientation string `json:"btnOrientation"`
|
||||||
|
HideAvatar string `json:"hideAvatar"` // robot message avatar
|
||||||
|
Buttons []struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
ActionURL string `json:"actionURL"`
|
||||||
|
} `json:"btns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// payload
|
||||||
|
type PayLoad struct {
|
||||||
|
MsgType string `json:"msgtype"`
|
||||||
|
Text struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
} `json:"text"`
|
||||||
|
Link struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
PicUrl string `json:"picUrl"`
|
||||||
|
MessageUrl string `json:"messageUrl"`
|
||||||
|
} `json:"link"`
|
||||||
|
Markdown struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
} `json:"markdown"`
|
||||||
|
ActionCard ActionCard `json:"actionCard"`
|
||||||
|
FeedCard struct {
|
||||||
|
Links []LinkMsg `json:"links"`
|
||||||
|
} `json:"feedCard"`
|
||||||
|
At struct {
|
||||||
|
AtMobiles []string `json:"atMobiles"`
|
||||||
|
IsAtAll bool `json:"isAtAll"`
|
||||||
|
} `json:"at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// web hook base config
|
||||||
|
type WebHook struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebHook(accessToken string) *WebHook {
|
||||||
|
return &WebHook{AccessToken: accessToken}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
ErrorCode int `json:"errcode"`
|
||||||
|
ErrorMessage string `json:"errmsg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseApi = "https://oapi.dingtalk.com/robot/send?access_token="
|
||||||
|
var reg = `^1([38][0-9]|14[57]|5[^4])\d{8}$`
|
||||||
|
var regx = regexp.MustCompile(reg)
|
||||||
|
|
||||||
|
// real send request to api
|
||||||
|
func (w *WebHook) sendPayload(payload *PayLoad) error {
|
||||||
|
// get config
|
||||||
|
bs, err := json.Marshal(payload)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// request api
|
||||||
|
resp, err := http.Post(baseApi+w.AccessToken, "application/json", bytes.NewReader(bs))
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// read response body
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// api unusual
|
||||||
|
if 200 != resp.StatusCode {
|
||||||
|
return fmt.Errorf("%d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result Response
|
||||||
|
// json decode
|
||||||
|
err = json.Unmarshal(body, &result)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if 0 != result.ErrorCode {
|
||||||
|
return fmt.Errorf("%d: %s", result.ErrorCode, result.ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// text message
|
||||||
|
func (w *WebHook) SendTextMsg(content string, isAtAll bool, mobiles ...string) error {
|
||||||
|
// send request
|
||||||
|
return w.sendPayload(&PayLoad{
|
||||||
|
MsgType: "text",
|
||||||
|
Text: struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
}{
|
||||||
|
Content: content,
|
||||||
|
},
|
||||||
|
At: struct {
|
||||||
|
AtMobiles []string `json:"atMobiles"`
|
||||||
|
IsAtAll bool `json:"isAtAll"`
|
||||||
|
}{
|
||||||
|
AtMobiles: mobiles,
|
||||||
|
IsAtAll: isAtAll,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// with link message
|
||||||
|
func (w *WebHook) SendLinkMsg(title, content, picURL, msgURL string) error {
|
||||||
|
return w.sendPayload(&PayLoad{
|
||||||
|
MsgType: "link",
|
||||||
|
Link: struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
PicUrl string `json:"picUrl"`
|
||||||
|
MessageUrl string `json:"messageUrl"`
|
||||||
|
}{
|
||||||
|
Title: title,
|
||||||
|
Text: content,
|
||||||
|
PicUrl: picURL,
|
||||||
|
MessageUrl: msgURL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// send markdown msg
|
||||||
|
func (w *WebHook) SendMarkdownMsg(title, content string, isAtAll bool, mobiles ...string) error {
|
||||||
|
firstLine := false
|
||||||
|
for _, mobile := range mobiles {
|
||||||
|
if regx.MatchString(mobile) {
|
||||||
|
if false == firstLine {
|
||||||
|
content += "#####"
|
||||||
|
}
|
||||||
|
content += " @" + mobile
|
||||||
|
firstLine = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// send request
|
||||||
|
return w.sendPayload(&PayLoad{
|
||||||
|
MsgType: "markdown",
|
||||||
|
Markdown: struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}{
|
||||||
|
Title: title,
|
||||||
|
Text: content,
|
||||||
|
},
|
||||||
|
At: struct {
|
||||||
|
AtMobiles []string `json:"atMobiles"`
|
||||||
|
IsAtAll bool `json:"isAtAll"`
|
||||||
|
}{
|
||||||
|
AtMobiles: mobiles,
|
||||||
|
IsAtAll: isAtAll,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// send single action card
|
||||||
|
func (w *WebHook) SendActionCardMsg(title, content string, linkTitles, linkUrls []string, hideAvatar, btnOrientation bool) error {
|
||||||
|
// validation is empty
|
||||||
|
if 0 == len(linkTitles) || 0 == len(linkUrls) {
|
||||||
|
return errors.New("links or titles is empty!")
|
||||||
|
}
|
||||||
|
// validation is equal
|
||||||
|
if len(linkUrls) != len(linkTitles) {
|
||||||
|
return errors.New("links length and titles length is not equal!")
|
||||||
|
}
|
||||||
|
// hide robot avatar
|
||||||
|
var strHideAvatar = "0"
|
||||||
|
if hideAvatar {
|
||||||
|
strHideAvatar = "1"
|
||||||
|
}
|
||||||
|
// button sort
|
||||||
|
var strBtnOrientation = "0"
|
||||||
|
if btnOrientation {
|
||||||
|
strBtnOrientation = "1"
|
||||||
|
}
|
||||||
|
// button struct
|
||||||
|
var buttons []struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
ActionURL string `json:"actionURL"`
|
||||||
|
}
|
||||||
|
// inject to button
|
||||||
|
for i := 0; i < len(linkTitles); i++ {
|
||||||
|
buttons = append(buttons, struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
ActionURL string `json:"actionURL"`
|
||||||
|
}{
|
||||||
|
Title: linkTitles[i],
|
||||||
|
ActionURL: linkUrls[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// send request
|
||||||
|
return w.sendPayload(&PayLoad{
|
||||||
|
MsgType: "actionCard",
|
||||||
|
ActionCard: ActionCard{
|
||||||
|
Title: title,
|
||||||
|
Text: content,
|
||||||
|
HideAvatar: strHideAvatar,
|
||||||
|
BtnOrientation: strBtnOrientation,
|
||||||
|
Buttons: buttons,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// send link card message
|
||||||
|
func (w *WebHook) SendLinkCardMsg(messages []LinkMsg) error {
|
||||||
|
return w.sendPayload(&PayLoad{
|
||||||
|
MsgType: "feedCard",
|
||||||
|
FeedCard: struct {
|
||||||
|
Links []LinkMsg `json:"links"`
|
||||||
|
}{
|
||||||
|
Links: messages,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user