├── .github
└── workflows
│ └── test.yml
├── LICENSE
├── Makefile
├── README.md
├── autocomplete.go
├── cli.go
├── cli_test.go
├── command.go
├── command_mock.go
├── command_mock_test.go
├── go.mod
├── go.sum
├── help.go
├── ui.go
├── ui_colored.go
├── ui_concurrent.go
├── ui_concurrent_test.go
├── ui_mock.go
├── ui_mock_test.go
├── ui_test.go
├── ui_writer.go
└── ui_writer_test.go
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | on: [push, pull_request]
2 | name: Test
3 | jobs:
4 | test:
5 | strategy:
6 | matrix:
7 | go-version: [1.18.x]
8 | os: [ubuntu-latest]
9 | runs-on: ${{ matrix.os }}
10 | steps:
11 | - name: Install Go
12 | uses: actions/setup-go@v2
13 | with:
14 | go-version: ${{ matrix.go-version }}
15 | - name: Checkout code
16 | uses: actions/checkout@v2
17 | - name: Test
18 | run: go test ./...
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License, version 2.0
2 |
3 | 1. Definitions
4 |
5 | 1.1. “Contributor”
6 |
7 | means each individual or legal entity that creates, contributes to the
8 | creation of, or owns Covered Software.
9 |
10 | 1.2. “Contributor Version”
11 |
12 | means the combination of the Contributions of others (if any) used by a
13 | Contributor and that particular Contributor’s Contribution.
14 |
15 | 1.3. “Contribution”
16 |
17 | means Covered Software of a particular Contributor.
18 |
19 | 1.4. “Covered Software”
20 |
21 | means Source Code Form to which the initial Contributor has attached the
22 | notice in Exhibit A, the Executable Form of such Source Code Form, and
23 | Modifications of such Source Code Form, in each case including portions
24 | thereof.
25 |
26 | 1.5. “Incompatible With Secondary Licenses”
27 | means
28 |
29 | a. that the initial Contributor has attached the notice described in
30 | Exhibit B to the Covered Software; or
31 |
32 | b. that the Covered Software was made available under the terms of version
33 | 1.1 or earlier of the License, but not also under the terms of a
34 | Secondary License.
35 |
36 | 1.6. “Executable Form”
37 |
38 | means any form of the work other than Source Code Form.
39 |
40 | 1.7. “Larger Work”
41 |
42 | means a work that combines Covered Software with other material, in a separate
43 | file or files, that is not Covered Software.
44 |
45 | 1.8. “License”
46 |
47 | means this document.
48 |
49 | 1.9. “Licensable”
50 |
51 | means having the right to grant, to the maximum extent possible, whether at the
52 | time of the initial grant or subsequently, any and all of the rights conveyed by
53 | this License.
54 |
55 | 1.10. “Modifications”
56 |
57 | means any of the following:
58 |
59 | a. any file in Source Code Form that results from an addition to, deletion
60 | from, or modification of the contents of Covered Software; or
61 |
62 | b. any new file in Source Code Form that contains any Covered Software.
63 |
64 | 1.11. “Patent Claims” of a Contributor
65 |
66 | means any patent claim(s), including without limitation, method, process,
67 | and apparatus claims, in any patent Licensable by such Contributor that
68 | would be infringed, but for the grant of the License, by the making,
69 | using, selling, offering for sale, having made, import, or transfer of
70 | either its Contributions or its Contributor Version.
71 |
72 | 1.12. “Secondary License”
73 |
74 | means either the GNU General Public License, Version 2.0, the GNU Lesser
75 | General Public License, Version 2.1, the GNU Affero General Public
76 | License, Version 3.0, or any later versions of those licenses.
77 |
78 | 1.13. “Source Code Form”
79 |
80 | means the form of the work preferred for making modifications.
81 |
82 | 1.14. “You” (or “Your”)
83 |
84 | means an individual or a legal entity exercising rights under this
85 | License. For legal entities, “You” includes any entity that controls, is
86 | controlled by, or is under common control with You. For purposes of this
87 | definition, “control” means (a) the power, direct or indirect, to cause
88 | the direction or management of such entity, whether by contract or
89 | otherwise, or (b) ownership of more than fifty percent (50%) of the
90 | outstanding shares or beneficial ownership of such entity.
91 |
92 |
93 | 2. License Grants and Conditions
94 |
95 | 2.1. Grants
96 |
97 | Each Contributor hereby grants You a world-wide, royalty-free,
98 | non-exclusive license:
99 |
100 | a. under intellectual property rights (other than patent or trademark)
101 | Licensable by such Contributor to use, reproduce, make available,
102 | modify, display, perform, distribute, and otherwise exploit its
103 | Contributions, either on an unmodified basis, with Modifications, or as
104 | part of a Larger Work; and
105 |
106 | b. under Patent Claims of such Contributor to make, use, sell, offer for
107 | sale, have made, import, and otherwise transfer either its Contributions
108 | or its Contributor Version.
109 |
110 | 2.2. Effective Date
111 |
112 | The licenses granted in Section 2.1 with respect to any Contribution become
113 | effective for each Contribution on the date the Contributor first distributes
114 | such Contribution.
115 |
116 | 2.3. Limitations on Grant Scope
117 |
118 | The licenses granted in this Section 2 are the only rights granted under this
119 | License. No additional rights or licenses will be implied from the distribution
120 | or licensing of Covered Software under this License. Notwithstanding Section
121 | 2.1(b) above, no patent license is granted by a Contributor:
122 |
123 | a. for any code that a Contributor has removed from Covered Software; or
124 |
125 | b. for infringements caused by: (i) Your and any other third party’s
126 | modifications of Covered Software, or (ii) the combination of its
127 | Contributions with other software (except as part of its Contributor
128 | Version); or
129 |
130 | c. under Patent Claims infringed by Covered Software in the absence of its
131 | Contributions.
132 |
133 | This License does not grant any rights in the trademarks, service marks, or
134 | logos of any Contributor (except as may be necessary to comply with the
135 | notice requirements in Section 3.4).
136 |
137 | 2.4. Subsequent Licenses
138 |
139 | No Contributor makes additional grants as a result of Your choice to
140 | distribute the Covered Software under a subsequent version of this License
141 | (see Section 10.2) or under the terms of a Secondary License (if permitted
142 | under the terms of Section 3.3).
143 |
144 | 2.5. Representation
145 |
146 | Each Contributor represents that the Contributor believes its Contributions
147 | are its original creation(s) or it has sufficient rights to grant the
148 | rights to its Contributions conveyed by this License.
149 |
150 | 2.6. Fair Use
151 |
152 | This License is not intended to limit any rights You have under applicable
153 | copyright doctrines of fair use, fair dealing, or other equivalents.
154 |
155 | 2.7. Conditions
156 |
157 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
158 | Section 2.1.
159 |
160 |
161 | 3. Responsibilities
162 |
163 | 3.1. Distribution of Source Form
164 |
165 | All distribution of Covered Software in Source Code Form, including any
166 | Modifications that You create or to which You contribute, must be under the
167 | terms of this License. You must inform recipients that the Source Code Form
168 | of the Covered Software is governed by the terms of this License, and how
169 | they can obtain a copy of this License. You may not attempt to alter or
170 | restrict the recipients’ rights in the Source Code Form.
171 |
172 | 3.2. Distribution of Executable Form
173 |
174 | If You distribute Covered Software in Executable Form then:
175 |
176 | a. such Covered Software must also be made available in Source Code Form,
177 | as described in Section 3.1, and You must inform recipients of the
178 | Executable Form how they can obtain a copy of such Source Code Form by
179 | reasonable means in a timely manner, at a charge no more than the cost
180 | of distribution to the recipient; and
181 |
182 | b. You may distribute such Executable Form under the terms of this License,
183 | or sublicense it under different terms, provided that the license for
184 | the Executable Form does not attempt to limit or alter the recipients’
185 | rights in the Source Code Form under this License.
186 |
187 | 3.3. Distribution of a Larger Work
188 |
189 | You may create and distribute a Larger Work under terms of Your choice,
190 | provided that You also comply with the requirements of this License for the
191 | Covered Software. If the Larger Work is a combination of Covered Software
192 | with a work governed by one or more Secondary Licenses, and the Covered
193 | Software is not Incompatible With Secondary Licenses, this License permits
194 | You to additionally distribute such Covered Software under the terms of
195 | such Secondary License(s), so that the recipient of the Larger Work may, at
196 | their option, further distribute the Covered Software under the terms of
197 | either this License or such Secondary License(s).
198 |
199 | 3.4. Notices
200 |
201 | You may not remove or alter the substance of any license notices (including
202 | copyright notices, patent notices, disclaimers of warranty, or limitations
203 | of liability) contained within the Source Code Form of the Covered
204 | Software, except that You may alter any license notices to the extent
205 | required to remedy known factual inaccuracies.
206 |
207 | 3.5. Application of Additional Terms
208 |
209 | You may choose to offer, and to charge a fee for, warranty, support,
210 | indemnity or liability obligations to one or more recipients of Covered
211 | Software. However, You may do so only on Your own behalf, and not on behalf
212 | of any Contributor. You must make it absolutely clear that any such
213 | warranty, support, indemnity, or liability obligation is offered by You
214 | alone, and You hereby agree to indemnify every Contributor for any
215 | liability incurred by such Contributor as a result of warranty, support,
216 | indemnity or liability terms You offer. You may include additional
217 | disclaimers of warranty and limitations of liability specific to any
218 | jurisdiction.
219 |
220 | 4. Inability to Comply Due to Statute or Regulation
221 |
222 | If it is impossible for You to comply with any of the terms of this License
223 | with respect to some or all of the Covered Software due to statute, judicial
224 | order, or regulation then You must: (a) comply with the terms of this License
225 | to the maximum extent possible; and (b) describe the limitations and the code
226 | they affect. Such description must be placed in a text file included with all
227 | distributions of the Covered Software under this License. Except to the
228 | extent prohibited by statute or regulation, such description must be
229 | sufficiently detailed for a recipient of ordinary skill to be able to
230 | understand it.
231 |
232 | 5. Termination
233 |
234 | 5.1. The rights granted under this License will terminate automatically if You
235 | fail to comply with any of its terms. However, if You become compliant,
236 | then the rights granted under this License from a particular Contributor
237 | are reinstated (a) provisionally, unless and until such Contributor
238 | explicitly and finally terminates Your grants, and (b) on an ongoing basis,
239 | if such Contributor fails to notify You of the non-compliance by some
240 | reasonable means prior to 60 days after You have come back into compliance.
241 | Moreover, Your grants from a particular Contributor are reinstated on an
242 | ongoing basis if such Contributor notifies You of the non-compliance by
243 | some reasonable means, this is the first time You have received notice of
244 | non-compliance with this License from such Contributor, and You become
245 | compliant prior to 30 days after Your receipt of the notice.
246 |
247 | 5.2. If You initiate litigation against any entity by asserting a patent
248 | infringement claim (excluding declaratory judgment actions, counter-claims,
249 | and cross-claims) alleging that a Contributor Version directly or
250 | indirectly infringes any patent, then the rights granted to You by any and
251 | all Contributors for the Covered Software under Section 2.1 of this License
252 | shall terminate.
253 |
254 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
255 | license agreements (excluding distributors and resellers) which have been
256 | validly granted by You or Your distributors under this License prior to
257 | termination shall survive termination.
258 |
259 | 6. Disclaimer of Warranty
260 |
261 | Covered Software is provided under this License on an “as is” basis, without
262 | warranty of any kind, either expressed, implied, or statutory, including,
263 | without limitation, warranties that the Covered Software is free of defects,
264 | merchantable, fit for a particular purpose or non-infringing. The entire
265 | risk as to the quality and performance of the Covered Software is with You.
266 | Should any Covered Software prove defective in any respect, You (not any
267 | Contributor) assume the cost of any necessary servicing, repair, or
268 | correction. This disclaimer of warranty constitutes an essential part of this
269 | License. No use of any Covered Software is authorized under this License
270 | except under this disclaimer.
271 |
272 | 7. Limitation of Liability
273 |
274 | Under no circumstances and under no legal theory, whether tort (including
275 | negligence), contract, or otherwise, shall any Contributor, or anyone who
276 | distributes Covered Software as permitted above, be liable to You for any
277 | direct, indirect, special, incidental, or consequential damages of any
278 | character including, without limitation, damages for lost profits, loss of
279 | goodwill, work stoppage, computer failure or malfunction, or any and all
280 | other commercial damages or losses, even if such party shall have been
281 | informed of the possibility of such damages. This limitation of liability
282 | shall not apply to liability for death or personal injury resulting from such
283 | party’s negligence to the extent applicable law prohibits such limitation.
284 | Some jurisdictions do not allow the exclusion or limitation of incidental or
285 | consequential damages, so this exclusion and limitation may not apply to You.
286 |
287 | 8. Litigation
288 |
289 | Any litigation relating to this License may be brought only in the courts of
290 | a jurisdiction where the defendant maintains its principal place of business
291 | and such litigation shall be governed by laws of that jurisdiction, without
292 | reference to its conflict-of-law provisions. Nothing in this Section shall
293 | prevent a party’s ability to bring cross-claims or counter-claims.
294 |
295 | 9. Miscellaneous
296 |
297 | This License represents the complete agreement concerning the subject matter
298 | hereof. If any provision of this License is held to be unenforceable, such
299 | provision shall be reformed only to the extent necessary to make it
300 | enforceable. Any law or regulation which provides that the language of a
301 | contract shall be construed against the drafter shall not be used to construe
302 | this License against a Contributor.
303 |
304 |
305 | 10. Versions of the License
306 |
307 | 10.1. New Versions
308 |
309 | Mozilla Foundation is the license steward. Except as provided in Section
310 | 10.3, no one other than the license steward has the right to modify or
311 | publish new versions of this License. Each version will be given a
312 | distinguishing version number.
313 |
314 | 10.2. Effect of New Versions
315 |
316 | You may distribute the Covered Software under the terms of the version of
317 | the License under which You originally received the Covered Software, or
318 | under the terms of any subsequent version published by the license
319 | steward.
320 |
321 | 10.3. Modified Versions
322 |
323 | If you create software not governed by this License, and you want to
324 | create a new license for such software, you may create and use a modified
325 | version of this License if you rename the license and remove any
326 | references to the name of the license steward (except to note that such
327 | modified license differs from this License).
328 |
329 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
330 | If You choose to distribute Source Code Form that is Incompatible With
331 | Secondary Licenses under the terms of this version of the License, the
332 | notice described in Exhibit B of this License must be attached.
333 |
334 | Exhibit A - Source Code Form License Notice
335 |
336 | This Source Code Form is subject to the
337 | terms of the Mozilla Public License, v.
338 | 2.0. If a copy of the MPL was not
339 | distributed with this file, You can
340 | obtain one at
341 | http://mozilla.org/MPL/2.0/.
342 |
343 | If it is not possible or desirable to put the notice in a particular file, then
344 | You may include the notice in a location (such as a LICENSE file in a relevant
345 | directory) where a recipient would be likely to look for such a notice.
346 |
347 | You may add additional accurate notices of copyright ownership.
348 |
349 | Exhibit B - “Incompatible With Secondary Licenses” Notice
350 |
351 | This Source Code Form is “Incompatible
352 | With Secondary Licenses”, as defined by
353 | the Mozilla Public License, v. 2.0.
354 |
355 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TEST?=./...
2 |
3 | default: test
4 |
5 | # test runs the test suite and vets the code
6 | test:
7 | go list $(TEST) | xargs -n1 go test -timeout=60s -parallel=10 $(TESTARGS)
8 |
9 | # testrace runs the race checker
10 | testrace:
11 | go list $(TEST) | xargs -n1 go test -race $(TESTARGS)
12 |
13 | # updatedeps installs all the dependencies to run and build
14 | updatedeps:
15 | go mod download
16 |
17 | .PHONY: test testrace updatedeps
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 🚨 This project is archived. 🚨 Learn More
3 |
4 |
5 |
6 |
7 | # Go CLI Library [](https://pkg.go.dev/github.com/mitchellh/cli)
8 |
9 | cli is a library for implementing command-line interfaces in Go.
10 | cli is the library that powers the CLI for
11 | [Packer](https://github.com/mitchellh/packer),
12 | [Consul](https://github.com/hashicorp/consul),
13 | [Vault](https://github.com/hashicorp/vault),
14 | [Terraform](https://github.com/hashicorp/terraform),
15 | [Nomad](https://github.com/hashicorp/nomad), and more.
16 |
17 | ## Features
18 |
19 | * Easy sub-command based CLIs: `cli foo`, `cli bar`, etc.
20 |
21 | * Support for nested subcommands such as `cli foo bar`.
22 |
23 | * Optional support for default subcommands so `cli` does something
24 | other than error.
25 |
26 | * Support for shell autocompletion of subcommands, flags, and arguments
27 | with callbacks in Go. You don't need to write any shell code.
28 |
29 | * Automatic help generation for listing subcommands.
30 |
31 | * Automatic help flag recognition of `-h`, `--help`, etc.
32 |
33 | * Automatic version flag recognition of `-v`, `--version`.
34 |
35 | * Helpers for interacting with the terminal, such as outputting information,
36 | asking for input, etc. These are optional, you can always interact with the
37 | terminal however you choose.
38 |
39 | * Use of Go interfaces/types makes augmenting various parts of the library a
40 | piece of cake.
41 |
42 | ## Example
43 |
44 | Below is a simple example of creating and running a CLI
45 |
46 | ```go
47 | package main
48 |
49 | import (
50 | "log"
51 | "os"
52 |
53 | "github.com/mitchellh/cli"
54 | )
55 |
56 | func main() {
57 | c := cli.NewCLI("app", "1.0.0")
58 | c.Args = os.Args[1:]
59 | c.Commands = map[string]cli.CommandFactory{
60 | "foo": fooCommandFactory,
61 | "bar": barCommandFactory,
62 | }
63 |
64 | exitStatus, err := c.Run()
65 | if err != nil {
66 | log.Println(err)
67 | }
68 |
69 | os.Exit(exitStatus)
70 | }
71 | ```
72 |
73 |
--------------------------------------------------------------------------------
/autocomplete.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "github.com/posener/complete/cmd/install"
5 | )
6 |
7 | // autocompleteInstaller is an interface to be implemented to perform the
8 | // autocomplete installation and uninstallation with a CLI.
9 | //
10 | // This interface is not exported because it only exists for unit tests
11 | // to be able to test that the installation is called properly.
12 | type autocompleteInstaller interface {
13 | Install(string) error
14 | Uninstall(string) error
15 | }
16 |
17 | // realAutocompleteInstaller uses the real install package to do the
18 | // install/uninstall.
19 | type realAutocompleteInstaller struct{}
20 |
21 | func (i *realAutocompleteInstaller) Install(cmd string) error {
22 | return install.Install(cmd)
23 | }
24 |
25 | func (i *realAutocompleteInstaller) Uninstall(cmd string) error {
26 | return install.Uninstall(cmd)
27 | }
28 |
29 | // mockAutocompleteInstaller is used for tests to record the install/uninstall.
30 | type mockAutocompleteInstaller struct {
31 | InstallCalled bool
32 | UninstallCalled bool
33 | }
34 |
35 | func (i *mockAutocompleteInstaller) Install(cmd string) error {
36 | i.InstallCalled = true
37 | return nil
38 | }
39 |
40 | func (i *mockAutocompleteInstaller) Uninstall(cmd string) error {
41 | i.UninstallCalled = true
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/cli.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "io/ioutil"
7 | "os"
8 | "regexp"
9 | "sort"
10 | "strings"
11 | "sync"
12 | "text/template"
13 |
14 | "github.com/Masterminds/sprig/v3"
15 | "github.com/armon/go-radix"
16 | "github.com/posener/complete"
17 | )
18 |
19 | // CLI contains the state necessary to run subcommands and parse the
20 | // command line arguments.
21 | //
22 | // CLI also supports nested subcommands, such as "cli foo bar". To use
23 | // nested subcommands, the key in the Commands mapping below contains the
24 | // full subcommand. In this example, it would be "foo bar".
25 | //
26 | // If you use a CLI with nested subcommands, some semantics change due to
27 | // ambiguities:
28 | //
29 | // * We use longest prefix matching to find a matching subcommand. This
30 | // means if you register "foo bar" and the user executes "cli foo qux",
31 | // the "foo" command will be executed with the arg "qux". It is up to
32 | // you to handle these args. One option is to just return the special
33 | // help return code `RunResultHelp` to display help and exit.
34 | //
35 | // * The help flag "-h" or "-help" will look at all args to determine
36 | // the help function. For example: "otto apps list -h" will show the
37 | // help for "apps list" but "otto apps -h" will show it for "apps".
38 | // In the normal CLI, only the first subcommand is used.
39 | //
40 | // * The help flag will list any subcommands that a command takes
41 | // as well as the command's help itself. If there are no subcommands,
42 | // it will note this. If the CLI itself has no subcommands, this entire
43 | // section is omitted.
44 | //
45 | // * Any parent commands that don't exist are automatically created as
46 | // no-op commands that just show help for other subcommands. For example,
47 | // if you only register "foo bar", then "foo" is automatically created.
48 | //
49 | type CLI struct {
50 | // Args is the list of command-line arguments received excluding
51 | // the name of the app. For example, if the command "./cli foo bar"
52 | // was invoked, then Args should be []string{"foo", "bar"}.
53 | Args []string
54 |
55 | // Commands is a mapping of subcommand names to a factory function
56 | // for creating that Command implementation. If there is a command
57 | // with a blank string "", then it will be used as the default command
58 | // if no subcommand is specified.
59 | //
60 | // If the key has a space in it, this will create a nested subcommand.
61 | // For example, if the key is "foo bar", then to access it our CLI
62 | // must be accessed with "./cli foo bar". See the docs for CLI for
63 | // notes on how this changes some other behavior of the CLI as well.
64 | //
65 | // The factory should be as cheap as possible, ideally only allocating
66 | // a struct. The factory may be called multiple times in the course
67 | // of a command execution and certain events such as help require the
68 | // instantiation of all commands. Expensive initialization should be
69 | // deferred to function calls within the interface implementation.
70 | Commands map[string]CommandFactory
71 |
72 | // HiddenCommands is a list of commands that are "hidden". Hidden
73 | // commands are not given to the help function callback and do not
74 | // show up in autocomplete. The values in the slice should be equivalent
75 | // to the keys in the command map.
76 | HiddenCommands []string
77 |
78 | // Name defines the name of the CLI.
79 | Name string
80 |
81 | // Version of the CLI.
82 | Version string
83 |
84 | // Autocomplete enables or disables subcommand auto-completion support.
85 | // This is enabled by default when NewCLI is called. Otherwise, this
86 | // must enabled explicitly.
87 | //
88 | // Autocomplete requires the "Name" option to be set on CLI. This name
89 | // should be set exactly to the binary name that is autocompleted.
90 | //
91 | // Autocompletion is supported via the github.com/posener/complete
92 | // library. This library supports bash, zsh and fish. To add support
93 | // for other shells, please see that library.
94 | //
95 | // AutocompleteInstall and AutocompleteUninstall are the global flag
96 | // names for installing and uninstalling the autocompletion handlers
97 | // for the user's shell. The flag should omit the hyphen(s) in front of
98 | // the value. Both single and double hyphens will automatically be supported
99 | // for the flag name. These default to `autocomplete-install` and
100 | // `autocomplete-uninstall` respectively.
101 | //
102 | // AutocompleteNoDefaultFlags is a boolean which controls if the default auto-
103 | // complete flags like -help and -version are added to the output.
104 | //
105 | // AutocompleteGlobalFlags are a mapping of global flags for
106 | // autocompletion. The help and version flags are automatically added.
107 | Autocomplete bool
108 | AutocompleteInstall string
109 | AutocompleteUninstall string
110 | AutocompleteNoDefaultFlags bool
111 | AutocompleteGlobalFlags complete.Flags
112 | autocompleteInstaller autocompleteInstaller // For tests
113 |
114 | // HelpFunc is the function called to generate the generic help
115 | // text that is shown if help must be shown for the CLI that doesn't
116 | // pertain to a specific command.
117 | HelpFunc HelpFunc
118 |
119 | // HelpWriter is used to print help text and version when requested.
120 | // Defaults to os.Stderr for backwards compatibility.
121 | // It is recommended that you set HelpWriter to os.Stdout, and
122 | // ErrorWriter to os.Stderr.
123 | HelpWriter io.Writer
124 |
125 | // ErrorWriter used to output errors when a command can not be run.
126 | // Defaults to the value of HelpWriter for backwards compatibility.
127 | // It is recommended that you set HelpWriter to os.Stdout, and
128 | // ErrorWriter to os.Stderr.
129 | ErrorWriter io.Writer
130 |
131 | //---------------------------------------------------------------
132 | // Internal fields set automatically
133 |
134 | once sync.Once
135 | autocomplete *complete.Complete
136 | commandTree *radix.Tree
137 | commandNested bool
138 | commandHidden map[string]struct{}
139 | subcommand string
140 | subcommandArgs []string
141 | topFlags []string
142 |
143 | // These are true when special global flags are set. We can/should
144 | // probably use a bitset for this one day.
145 | isHelp bool
146 | isVersion bool
147 | isAutocompleteInstall bool
148 | isAutocompleteUninstall bool
149 | }
150 |
151 | // NewClI returns a new CLI instance with sensible defaults.
152 | func NewCLI(app, version string) *CLI {
153 | return &CLI{
154 | Name: app,
155 | Version: version,
156 | HelpFunc: BasicHelpFunc(app),
157 | Autocomplete: true,
158 | }
159 |
160 | }
161 |
162 | // IsHelp returns whether or not the help flag is present within the
163 | // arguments.
164 | func (c *CLI) IsHelp() bool {
165 | c.once.Do(c.init)
166 | return c.isHelp
167 | }
168 |
169 | // IsVersion returns whether or not the version flag is present within the
170 | // arguments.
171 | func (c *CLI) IsVersion() bool {
172 | c.once.Do(c.init)
173 | return c.isVersion
174 | }
175 |
176 | // Run runs the actual CLI based on the arguments given.
177 | func (c *CLI) Run() (int, error) {
178 | c.once.Do(c.init)
179 |
180 | // If this is a autocompletion request, satisfy it. This must be called
181 | // first before anything else since its possible to be autocompleting
182 | // -help or -version or other flags and we want to show completions
183 | // and not actually write the help or version.
184 | if c.Autocomplete && c.autocomplete.Complete() {
185 | return 0, nil
186 | }
187 |
188 | // Just show the version and exit if instructed.
189 | if c.IsVersion() && c.Version != "" {
190 | c.HelpWriter.Write([]byte(c.Version + "\n"))
191 | return 0, nil
192 | }
193 |
194 | // Just print the help when only '-h' or '--help' is passed.
195 | if c.IsHelp() && c.Subcommand() == "" {
196 | c.HelpWriter.Write([]byte(c.HelpFunc(c.helpCommands(c.Subcommand())) + "\n"))
197 | return 0, nil
198 | }
199 |
200 | // If we're attempting to install or uninstall autocomplete then handle
201 | if c.Autocomplete {
202 | // Autocomplete requires the "Name" to be set so that we know what
203 | // command to setup the autocomplete on.
204 | if c.Name == "" {
205 | return 1, fmt.Errorf(
206 | "internal error: CLI.Name must be specified for autocomplete to work")
207 | }
208 |
209 | // If both install and uninstall flags are specified, then error
210 | if c.isAutocompleteInstall && c.isAutocompleteUninstall {
211 | return 1, fmt.Errorf(
212 | "Either the autocomplete install or uninstall flag may " +
213 | "be specified, but not both.")
214 | }
215 |
216 | // If the install flag is specified, perform the install or uninstall
217 | if c.isAutocompleteInstall {
218 | if err := c.autocompleteInstaller.Install(c.Name); err != nil {
219 | return 1, err
220 | }
221 |
222 | return 0, nil
223 | }
224 |
225 | if c.isAutocompleteUninstall {
226 | if err := c.autocompleteInstaller.Uninstall(c.Name); err != nil {
227 | return 1, err
228 | }
229 |
230 | return 0, nil
231 | }
232 | }
233 |
234 | // Attempt to get the factory function for creating the command
235 | // implementation. If the command is invalid or blank, it is an error.
236 | raw, ok := c.commandTree.Get(c.Subcommand())
237 | if !ok {
238 | c.ErrorWriter.Write([]byte(c.HelpFunc(c.helpCommands(c.subcommandParent())) + "\n"))
239 | return 127, nil
240 | }
241 |
242 | command, err := raw.(CommandFactory)()
243 | if err != nil {
244 | return 1, err
245 | }
246 |
247 | // If we've been instructed to just print the help, then print it
248 | if c.IsHelp() {
249 | c.commandHelp(c.HelpWriter, command)
250 | return 0, nil
251 | }
252 |
253 | // If there is an invalid flag, then error
254 | if len(c.topFlags) > 0 {
255 | c.ErrorWriter.Write([]byte(
256 | "Invalid flags before the subcommand. If these flags are for\n" +
257 | "the subcommand, please put them after the subcommand.\n\n"))
258 | c.commandHelp(c.ErrorWriter, command)
259 | return 1, nil
260 | }
261 |
262 | code := command.Run(c.SubcommandArgs())
263 | if code == RunResultHelp {
264 | // Requesting help
265 | c.commandHelp(c.ErrorWriter, command)
266 | return 1, nil
267 | }
268 |
269 | return code, nil
270 | }
271 |
272 | // Subcommand returns the subcommand that the CLI would execute. For
273 | // example, a CLI from "--version version --help" would return a Subcommand
274 | // of "version"
275 | func (c *CLI) Subcommand() string {
276 | c.once.Do(c.init)
277 | return c.subcommand
278 | }
279 |
280 | // SubcommandArgs returns the arguments that will be passed to the
281 | // subcommand.
282 | func (c *CLI) SubcommandArgs() []string {
283 | c.once.Do(c.init)
284 | return c.subcommandArgs
285 | }
286 |
287 | // subcommandParent returns the parent of this subcommand, if there is one.
288 | // If there isn't on, "" is returned.
289 | func (c *CLI) subcommandParent() string {
290 | // Get the subcommand, if it is "" alread just return
291 | sub := c.Subcommand()
292 | if sub == "" {
293 | return sub
294 | }
295 |
296 | // Clear any trailing spaces and find the last space
297 | sub = strings.TrimRight(sub, " ")
298 | idx := strings.LastIndex(sub, " ")
299 |
300 | if idx == -1 {
301 | // No space means our parent is root
302 | return ""
303 | }
304 |
305 | return sub[:idx]
306 | }
307 |
308 | func (c *CLI) init() {
309 | if c.HelpFunc == nil {
310 | c.HelpFunc = BasicHelpFunc("app")
311 |
312 | if c.Name != "" {
313 | c.HelpFunc = BasicHelpFunc(c.Name)
314 | }
315 | }
316 |
317 | if c.HelpWriter == nil {
318 | c.HelpWriter = os.Stderr
319 | }
320 | if c.ErrorWriter == nil {
321 | c.ErrorWriter = c.HelpWriter
322 | }
323 |
324 | // Build our hidden commands
325 | if len(c.HiddenCommands) > 0 {
326 | c.commandHidden = make(map[string]struct{})
327 | for _, h := range c.HiddenCommands {
328 | c.commandHidden[h] = struct{}{}
329 | }
330 | }
331 |
332 | // Build our command tree
333 | c.commandTree = radix.New()
334 | c.commandNested = false
335 | for k, v := range c.Commands {
336 | k = strings.TrimSpace(k)
337 | c.commandTree.Insert(k, v)
338 | if strings.ContainsRune(k, ' ') {
339 | c.commandNested = true
340 | }
341 | }
342 |
343 | // Go through the key and fill in any missing parent commands
344 | if c.commandNested {
345 | var walkFn radix.WalkFn
346 | toInsert := make(map[string]struct{})
347 | walkFn = func(k string, raw interface{}) bool {
348 | idx := strings.LastIndex(k, " ")
349 | if idx == -1 {
350 | // If there is no space, just ignore top level commands
351 | return false
352 | }
353 |
354 | // Trim up to that space so we can get the expected parent
355 | k = k[:idx]
356 | if _, ok := c.commandTree.Get(k); ok {
357 | // Yay we have the parent!
358 | return false
359 | }
360 |
361 | // We're missing the parent, so let's insert this
362 | toInsert[k] = struct{}{}
363 |
364 | // Call the walk function recursively so we check this one too
365 | return walkFn(k, nil)
366 | }
367 |
368 | // Walk!
369 | c.commandTree.Walk(walkFn)
370 |
371 | // Insert any that we're missing
372 | for k := range toInsert {
373 | var f CommandFactory = func() (Command, error) {
374 | return &MockCommand{
375 | HelpText: "This command is accessed by using one of the subcommands below.",
376 | RunResult: RunResultHelp,
377 | }, nil
378 | }
379 |
380 | c.commandTree.Insert(k, f)
381 | }
382 | }
383 |
384 | // Setup autocomplete if we have it enabled. We have to do this after
385 | // the command tree is setup so we can use the radix tree to easily find
386 | // all subcommands.
387 | if c.Autocomplete {
388 | c.initAutocomplete()
389 | }
390 |
391 | // Process the args
392 | c.processArgs()
393 | }
394 |
395 | func (c *CLI) initAutocomplete() {
396 | if c.AutocompleteInstall == "" {
397 | c.AutocompleteInstall = defaultAutocompleteInstall
398 | }
399 |
400 | if c.AutocompleteUninstall == "" {
401 | c.AutocompleteUninstall = defaultAutocompleteUninstall
402 | }
403 |
404 | if c.autocompleteInstaller == nil {
405 | c.autocompleteInstaller = &realAutocompleteInstaller{}
406 | }
407 |
408 | // We first set c.autocomplete to a noop autocompleter that outputs
409 | // to nul so that we can detect if we're autocompleting or not. If we're
410 | // not, then we do nothing. This saves a LOT of compute cycles since
411 | // initAutoCompleteSub has to walk every command.
412 | c.autocomplete = complete.New(c.Name, complete.Command{})
413 | c.autocomplete.Out = ioutil.Discard
414 | if !c.autocomplete.Complete() {
415 | return
416 | }
417 |
418 | // Build the root command
419 | cmd := c.initAutocompleteSub("")
420 |
421 | // For the root, we add the global flags to the "Flags". This way
422 | // they don't show up on every command.
423 | if !c.AutocompleteNoDefaultFlags {
424 | cmd.Flags = map[string]complete.Predictor{
425 | "-" + c.AutocompleteInstall: complete.PredictNothing,
426 | "-" + c.AutocompleteUninstall: complete.PredictNothing,
427 | "-help": complete.PredictNothing,
428 | "-version": complete.PredictNothing,
429 | }
430 | }
431 | cmd.GlobalFlags = c.AutocompleteGlobalFlags
432 |
433 | c.autocomplete = complete.New(c.Name, cmd)
434 | }
435 |
436 | // initAutocompleteSub creates the complete.Command for a subcommand with
437 | // the given prefix. This will continue recursively for all subcommands.
438 | // The prefix "" (empty string) can be used for the root command.
439 | func (c *CLI) initAutocompleteSub(prefix string) complete.Command {
440 | var cmd complete.Command
441 | walkFn := func(k string, raw interface{}) bool {
442 | // Ignore the empty key which can be present for default commands.
443 | if k == "" {
444 | return false
445 | }
446 |
447 | // Keep track of the full key so that we can nest further if necessary
448 | fullKey := k
449 |
450 | if len(prefix) > 0 {
451 | // If we have a prefix, trim the prefix + 1 (for the space)
452 | // Example: turns "sub one" to "one" with prefix "sub"
453 | k = k[len(prefix)+1:]
454 | }
455 |
456 | if idx := strings.Index(k, " "); idx >= 0 {
457 | // If there is a space, we trim up to the space. This turns
458 | // "sub sub2 sub3" into "sub". The prefix trim above will
459 | // trim our current depth properly.
460 | k = k[:idx]
461 | }
462 |
463 | if _, ok := cmd.Sub[k]; ok {
464 | // If we already tracked this subcommand then ignore
465 | return false
466 | }
467 |
468 | // If the command is hidden, don't record it at all
469 | if _, ok := c.commandHidden[fullKey]; ok {
470 | return false
471 | }
472 |
473 | if cmd.Sub == nil {
474 | cmd.Sub = complete.Commands(make(map[string]complete.Command))
475 | }
476 | subCmd := c.initAutocompleteSub(fullKey)
477 |
478 | // Instantiate the command so that we can check if the command is
479 | // a CommandAutocomplete implementation. If there is an error
480 | // creating the command, we just ignore it since that will be caught
481 | // later.
482 | impl, err := raw.(CommandFactory)()
483 | if err != nil {
484 | impl = nil
485 | }
486 |
487 | // Check if it implements ComandAutocomplete. If so, setup the autocomplete
488 | if c, ok := impl.(CommandAutocomplete); ok {
489 | subCmd.Args = c.AutocompleteArgs()
490 | subCmd.Flags = c.AutocompleteFlags()
491 | }
492 |
493 | cmd.Sub[k] = subCmd
494 | return false
495 | }
496 |
497 | walkPrefix := prefix
498 | if walkPrefix != "" {
499 | walkPrefix += " "
500 | }
501 |
502 | c.commandTree.WalkPrefix(walkPrefix, walkFn)
503 | return cmd
504 | }
505 |
506 | func (c *CLI) commandHelp(out io.Writer, command Command) {
507 | // Get the template to use
508 | tpl := strings.TrimSpace(defaultHelpTemplate)
509 | if t, ok := command.(CommandHelpTemplate); ok {
510 | tpl = t.HelpTemplate()
511 | }
512 | if !strings.HasSuffix(tpl, "\n") {
513 | tpl += "\n"
514 | }
515 |
516 | // Parse it
517 | t, err := template.New("root").Funcs(sprig.TxtFuncMap()).Parse(tpl)
518 | if err != nil {
519 | t = template.Must(template.New("root").Parse(fmt.Sprintf(
520 | "Internal error! Failed to parse command help template: %s\n", err)))
521 | }
522 |
523 | // Template data
524 | data := map[string]interface{}{
525 | "Name": c.Name,
526 | "SubcommandName": c.Subcommand(),
527 | "Help": command.Help(),
528 | }
529 |
530 | // Build subcommand list if we have it
531 | var subcommandsTpl []map[string]interface{}
532 | if c.commandNested {
533 | // Get the matching keys
534 | subcommands := c.helpCommands(c.Subcommand())
535 | keys := make([]string, 0, len(subcommands))
536 | for k := range subcommands {
537 | keys = append(keys, k)
538 | }
539 |
540 | // Sort the keys
541 | sort.Strings(keys)
542 |
543 | // Figure out the padding length
544 | var longest int
545 | for _, k := range keys {
546 | if v := len(k); v > longest {
547 | longest = v
548 | }
549 | }
550 |
551 | // Go through and create their structures
552 | subcommandsTpl = make([]map[string]interface{}, 0, len(subcommands))
553 | for _, k := range keys {
554 | // Get the command
555 | raw, ok := subcommands[k]
556 | if !ok {
557 | c.ErrorWriter.Write([]byte(fmt.Sprintf(
558 | "Error getting subcommand %q", k)))
559 | }
560 | sub, err := raw()
561 | if err != nil {
562 | c.ErrorWriter.Write([]byte(fmt.Sprintf(
563 | "Error instantiating %q: %s", k, err)))
564 | }
565 |
566 | // Find the last space and make sure we only include that last part
567 | name := k
568 | if idx := strings.LastIndex(k, " "); idx > -1 {
569 | name = name[idx+1:]
570 | }
571 |
572 | subcommandsTpl = append(subcommandsTpl, map[string]interface{}{
573 | "Name": name,
574 | "NameAligned": name + strings.Repeat(" ", longest-len(k)),
575 | "Help": sub.Help(),
576 | "Synopsis": sub.Synopsis(),
577 | })
578 | }
579 | }
580 | data["Subcommands"] = subcommandsTpl
581 |
582 | // Write
583 | err = t.Execute(out, data)
584 | if err == nil {
585 | return
586 | }
587 |
588 | // An error, just output...
589 | c.ErrorWriter.Write([]byte(fmt.Sprintf(
590 | "Internal error rendering help: %s", err)))
591 | }
592 |
593 | // helpCommands returns the subcommands for the HelpFunc argument.
594 | // This will only contain immediate subcommands.
595 | func (c *CLI) helpCommands(prefix string) map[string]CommandFactory {
596 | // If our prefix isn't empty, make sure it ends in ' '
597 | if prefix != "" && prefix[len(prefix)-1] != ' ' {
598 | prefix += " "
599 | }
600 |
601 | // Get all the subkeys of this command
602 | var keys []string
603 | c.commandTree.WalkPrefix(prefix, func(k string, raw interface{}) bool {
604 | // Ignore any sub-sub keys, i.e. "foo bar baz" when we want "foo bar"
605 | if !strings.Contains(k[len(prefix):], " ") {
606 | keys = append(keys, k)
607 | }
608 |
609 | return false
610 | })
611 |
612 | // For each of the keys return that in the map
613 | result := make(map[string]CommandFactory, len(keys))
614 | for _, k := range keys {
615 | raw, ok := c.commandTree.Get(k)
616 | if !ok {
617 | // We just got it via WalkPrefix above, so we just panic
618 | panic("not found: " + k)
619 | }
620 |
621 | // If this is a hidden command, don't show it
622 | if _, ok := c.commandHidden[k]; ok {
623 | continue
624 | }
625 |
626 | result[k] = raw.(CommandFactory)
627 | }
628 |
629 | return result
630 | }
631 |
632 | func (c *CLI) processArgs() {
633 | for i, arg := range c.Args {
634 | if arg == "--" {
635 | break
636 | }
637 |
638 | // Check for help flags.
639 | if arg == "-h" || arg == "-help" || arg == "--help" {
640 | c.isHelp = true
641 | continue
642 | }
643 |
644 | // Check for autocomplete flags
645 | if c.Autocomplete {
646 | if arg == "-"+c.AutocompleteInstall || arg == "--"+c.AutocompleteInstall {
647 | c.isAutocompleteInstall = true
648 | continue
649 | }
650 |
651 | if arg == "-"+c.AutocompleteUninstall || arg == "--"+c.AutocompleteUninstall {
652 | c.isAutocompleteUninstall = true
653 | continue
654 | }
655 | }
656 |
657 | if c.subcommand == "" {
658 | // Check for version flags if not in a subcommand.
659 | if arg == "-v" || arg == "-version" || arg == "--version" {
660 | c.isVersion = true
661 | continue
662 | }
663 |
664 | if arg != "" && arg[0] == '-' {
665 | // Record the arg...
666 | c.topFlags = append(c.topFlags, arg)
667 | }
668 | }
669 |
670 | // If we didn't find a subcommand yet and this is the first non-flag
671 | // argument, then this is our subcommand.
672 | if c.subcommand == "" && arg != "" && arg[0] != '-' {
673 | c.subcommand = arg
674 | if c.commandNested {
675 | // If the command has a space in it, then it is invalid.
676 | // Set a blank command so that it fails.
677 | if strings.ContainsRune(arg, ' ') {
678 | c.subcommand = ""
679 | return
680 | }
681 |
682 | // Determine the argument we look to to end subcommands.
683 | // We look at all arguments until one is a flag or has a space.
684 | // This disallows commands like: ./cli foo "bar baz". An
685 | // argument with a space is always an argument. A blank
686 | // argument is always an argument.
687 | j := 0
688 | for k, v := range c.Args[i:] {
689 | if strings.ContainsRune(v, ' ') || v == "" || v[0] == '-' {
690 | break
691 | }
692 |
693 | j = i + k + 1
694 | }
695 |
696 | // Nested CLI, the subcommand is actually the entire
697 | // arg list up to a flag that is still a valid subcommand.
698 | searchKey := strings.Join(c.Args[i:j], " ")
699 | k, _, ok := c.commandTree.LongestPrefix(searchKey)
700 | if ok {
701 | // k could be a prefix that doesn't contain the full
702 | // command such as "foo" instead of "foobar", so we
703 | // need to verify that we have an entire key. To do that,
704 | // we look for an ending in a space or an end of string.
705 | reVerify := regexp.MustCompile(regexp.QuoteMeta(k) + `( |$)`)
706 | if reVerify.MatchString(searchKey) {
707 | c.subcommand = k
708 | i += strings.Count(k, " ")
709 | }
710 | }
711 | }
712 |
713 | // The remaining args the subcommand arguments
714 | c.subcommandArgs = c.Args[i+1:]
715 | }
716 | }
717 |
718 | // If we never found a subcommand and support a default command, then
719 | // switch to using that.
720 | if c.subcommand == "" {
721 | if _, ok := c.Commands[""]; ok {
722 | args := c.topFlags
723 | args = append(args, c.subcommandArgs...)
724 | c.topFlags = nil
725 | c.subcommandArgs = args
726 | }
727 | }
728 | }
729 |
730 | // defaultAutocompleteInstall and defaultAutocompleteUninstall are the
731 | // default values for the autocomplete install and uninstall flags.
732 | const defaultAutocompleteInstall = "autocomplete-install"
733 | const defaultAutocompleteUninstall = "autocomplete-uninstall"
734 |
735 | const defaultHelpTemplate = `
736 | {{.Help}}{{if gt (len .Subcommands) 0}}
737 |
738 | Subcommands:
739 | {{- range $value := .Subcommands }}
740 | {{ $value.NameAligned }} {{ $value.Synopsis }}{{ end }}
741 | {{- end }}
742 | `
743 |
--------------------------------------------------------------------------------
/cli_test.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "os"
8 | "reflect"
9 | "sort"
10 | "strings"
11 | "testing"
12 |
13 | "github.com/posener/complete"
14 | )
15 |
16 | // envComplete is the env var that the complete library sets to specify
17 | // it should be calculating an auto-completion. This isn't exported so we
18 | // reproduce it here. If it changes then we'll have to update this.
19 | const envComplete = "COMP_LINE"
20 |
21 | func TestCLIIsHelp(t *testing.T) {
22 | testCases := []struct {
23 | args []string
24 | isHelp bool
25 | }{
26 | {[]string{"-h"}, true},
27 | {[]string{"-help"}, true},
28 | {[]string{"--help"}, true},
29 | {[]string{"-h", "foo"}, true},
30 | {[]string{"foo", "bar"}, false},
31 | {[]string{"-v", "bar"}, false},
32 | {[]string{"foo", "-h"}, true},
33 | {[]string{"foo", "-help"}, true},
34 | {[]string{"foo", "--help"}, true},
35 | {[]string{"foo", "bar", "-h"}, true},
36 | {[]string{"foo", "bar", "-help"}, true},
37 | {[]string{"foo", "bar", "--help"}, true},
38 | {[]string{"foo", "bar", "--", "zip", "-h"}, false},
39 | {[]string{"foo", "bar", "--", "zip", "-help"}, false},
40 | {[]string{"foo", "bar", "--", "zip", "--help"}, false},
41 | }
42 |
43 | for _, testCase := range testCases {
44 | cli := &CLI{Args: testCase.args}
45 | result := cli.IsHelp()
46 |
47 | if result != testCase.isHelp {
48 | t.Errorf("Expected '%#v'. Args: %#v", testCase.isHelp, testCase.args)
49 | }
50 | }
51 | }
52 |
53 | func TestCLIIsVersion(t *testing.T) {
54 | testCases := []struct {
55 | args []string
56 | isVersion bool
57 | }{
58 | {[]string{"--", "-v"}, false},
59 | {[]string{"--", "-version"}, false},
60 | {[]string{"--", "--version"}, false},
61 | {[]string{"-v"}, true},
62 | {[]string{"-version"}, true},
63 | {[]string{"--version"}, true},
64 | {[]string{"-v", "foo"}, true},
65 | {[]string{"foo", "bar"}, false},
66 | {[]string{"-h", "bar"}, false},
67 | {[]string{"foo", "-v"}, false},
68 | {[]string{"foo", "-version"}, false},
69 | {[]string{"foo", "--version"}, false},
70 | {[]string{"foo", "--", "zip", "-v"}, false},
71 | {[]string{"foo", "--", "zip", "-version"}, false},
72 | {[]string{"foo", "--", "zip", "--version"}, false},
73 | }
74 |
75 | for _, testCase := range testCases {
76 | cli := &CLI{Args: testCase.args}
77 | result := cli.IsVersion()
78 |
79 | if result != testCase.isVersion {
80 | t.Errorf("Expected '%#v'. Args: %#v", testCase.isVersion, testCase.args)
81 | }
82 | }
83 | }
84 |
85 | func TestCLIRun(t *testing.T) {
86 | command := new(MockCommand)
87 | cli := &CLI{
88 | Args: []string{"foo", "-bar", "-baz"},
89 | Commands: map[string]CommandFactory{
90 | "foo": func() (Command, error) {
91 | return command, nil
92 | },
93 | },
94 | }
95 |
96 | exitCode, err := cli.Run()
97 | if err != nil {
98 | t.Fatalf("err: %s", err)
99 | }
100 |
101 | if exitCode != command.RunResult {
102 | t.Fatalf("bad: %d", exitCode)
103 | }
104 |
105 | if !command.RunCalled {
106 | t.Fatalf("run should be called")
107 | }
108 |
109 | if !reflect.DeepEqual(command.RunArgs, []string{"-bar", "-baz"}) {
110 | t.Fatalf("bad args: %#v", command.RunArgs)
111 | }
112 | }
113 |
114 | func TestCLIRun_blank(t *testing.T) {
115 | command := new(MockCommand)
116 | cli := &CLI{
117 | Args: []string{"", "foo", "-bar", "-baz"},
118 | Commands: map[string]CommandFactory{
119 | "foo": func() (Command, error) {
120 | return command, nil
121 | },
122 | },
123 | }
124 |
125 | exitCode, err := cli.Run()
126 | if err != nil {
127 | t.Fatalf("err: %s", err)
128 | }
129 |
130 | if exitCode != command.RunResult {
131 | t.Fatalf("bad: %d", exitCode)
132 | }
133 |
134 | if !command.RunCalled {
135 | t.Fatalf("run should be called")
136 | }
137 |
138 | if !reflect.DeepEqual(command.RunArgs, []string{"-bar", "-baz"}) {
139 | t.Fatalf("bad args: %#v", command.RunArgs)
140 | }
141 | }
142 |
143 | func TestCLIRun_prefix(t *testing.T) {
144 | buf := new(bytes.Buffer)
145 | command := new(MockCommand)
146 | cli := &CLI{
147 | Args: []string{"foobar"},
148 | Commands: map[string]CommandFactory{
149 | "foo": func() (Command, error) {
150 | return command, nil
151 | },
152 |
153 | "foo bar": func() (Command, error) {
154 | return command, nil
155 | },
156 | },
157 | ErrorWriter: buf,
158 | }
159 |
160 | exitCode, err := cli.Run()
161 | if err != nil {
162 | t.Fatalf("err: %s", err)
163 | }
164 |
165 | if exitCode != 127 {
166 | t.Fatalf("bad: %d", exitCode)
167 | }
168 |
169 | if command.RunCalled {
170 | t.Fatalf("run should not be called")
171 | }
172 | }
173 |
174 | func TestCLIRun_subcommandSuffix(t *testing.T) {
175 | buf := new(bytes.Buffer)
176 | command := new(MockCommand)
177 | cli := &CLI{
178 | Args: []string{"fooasdf", "-o=foo"},
179 | Commands: map[string]CommandFactory{
180 | "foo": func() (Command, error) {
181 | return command, nil
182 | },
183 |
184 | "foo bar": func() (Command, error) {
185 | return command, nil
186 | },
187 | },
188 | ErrorWriter: buf,
189 | }
190 |
191 | exitCode, err := cli.Run()
192 | if err != nil {
193 | t.Fatalf("err: %s", err)
194 | }
195 |
196 | if exitCode != 127 {
197 | t.Fatalf("expected to get exit code 127, but got %d", exitCode)
198 | }
199 |
200 | if command.RunCalled {
201 | t.Fatalf("run should not be called")
202 | }
203 | }
204 |
205 | func TestCLIRun_default(t *testing.T) {
206 | commandBar := new(MockCommand)
207 | commandBar.RunResult = 42
208 |
209 | cli := &CLI{
210 | Args: []string{"-bar", "-baz"},
211 | Commands: map[string]CommandFactory{
212 | "": func() (Command, error) {
213 | return commandBar, nil
214 | },
215 | "foo": func() (Command, error) {
216 | return new(MockCommand), nil
217 | },
218 | },
219 | }
220 |
221 | exitCode, err := cli.Run()
222 | if err != nil {
223 | t.Fatalf("err: %s", err)
224 | }
225 |
226 | if exitCode != commandBar.RunResult {
227 | t.Fatalf("bad: %d", exitCode)
228 | }
229 |
230 | if !commandBar.RunCalled {
231 | t.Fatalf("run should be called")
232 | }
233 |
234 | if !reflect.DeepEqual(commandBar.RunArgs, []string{"-bar", "-baz"}) {
235 | t.Fatalf("bad args: %#v", commandBar.RunArgs)
236 | }
237 | }
238 |
239 | // GH-74: When using NewCLI with a default command only, Run would
240 | // stack overflow and crash.
241 | func TestCLIRun_defaultFromNew(t *testing.T) {
242 | commandBar := new(MockCommand)
243 |
244 | cli := NewCLI("test", "0.1.0")
245 | cli.Commands = map[string]CommandFactory{
246 | "": func() (Command, error) {
247 | return commandBar, nil
248 | },
249 | }
250 |
251 | exitCode, err := cli.Run()
252 | if err != nil {
253 | t.Fatalf("err: %s", err)
254 | }
255 |
256 | if exitCode != commandBar.RunResult {
257 | t.Fatalf("bad: %d", exitCode)
258 | }
259 |
260 | if !commandBar.RunCalled {
261 | t.Fatalf("run should be called")
262 | }
263 | }
264 |
265 | func TestCLIRun_helpNested(t *testing.T) {
266 | helpCalled := false
267 | buf := new(bytes.Buffer)
268 | cli := &CLI{
269 | Args: []string{"--help"},
270 | Commands: map[string]CommandFactory{
271 | "foo sub42": func() (Command, error) {
272 | return new(MockCommand), nil
273 | },
274 | },
275 | HelpFunc: func(m map[string]CommandFactory) string {
276 | helpCalled = true
277 |
278 | var keys []string
279 | for k := range m {
280 | keys = append(keys, k)
281 | }
282 | sort.Strings(keys)
283 |
284 | expected := []string{"foo"}
285 | if !reflect.DeepEqual(keys, expected) {
286 | return fmt.Sprintf("error: contained sub: %#v", keys)
287 | }
288 |
289 | return ""
290 | },
291 | ErrorWriter: buf,
292 | }
293 |
294 | code, err := cli.Run()
295 | if err != nil {
296 | t.Fatalf("Error: %s", err)
297 | }
298 |
299 | if code != 0 {
300 | t.Fatalf("Code: %d", code)
301 | }
302 |
303 | if !helpCalled {
304 | t.Fatal("help not called")
305 | }
306 | }
307 |
308 | func TestCLIRun_nested(t *testing.T) {
309 | command := new(MockCommand)
310 | cli := &CLI{
311 | Args: []string{"foo", "bar", "-bar", "-baz"},
312 | Commands: map[string]CommandFactory{
313 | "foo": func() (Command, error) {
314 | return new(MockCommand), nil
315 | },
316 | "foo bar": func() (Command, error) {
317 | return command, nil
318 | },
319 | },
320 | }
321 |
322 | exitCode, err := cli.Run()
323 | if err != nil {
324 | t.Fatalf("err: %s", err)
325 | }
326 |
327 | if exitCode != command.RunResult {
328 | t.Fatalf("bad: %d", exitCode)
329 | }
330 |
331 | if !command.RunCalled {
332 | t.Fatalf("run should be called")
333 | }
334 |
335 | if !reflect.DeepEqual(command.RunArgs, []string{"-bar", "-baz"}) {
336 | t.Fatalf("bad args: %#v", command.RunArgs)
337 | }
338 | }
339 |
340 | func TestCLIRun_nestedTopLevel(t *testing.T) {
341 | command := new(MockCommand)
342 | cli := &CLI{
343 | Args: []string{"foo"},
344 | Commands: map[string]CommandFactory{
345 | "foo": func() (Command, error) {
346 | return command, nil
347 | },
348 | "foo bar": func() (Command, error) {
349 | return new(MockCommand), nil
350 | },
351 | },
352 | }
353 |
354 | exitCode, err := cli.Run()
355 | if err != nil {
356 | t.Fatalf("err: %s", err)
357 | }
358 |
359 | if exitCode != command.RunResult {
360 | t.Fatalf("bad: %d", exitCode)
361 | }
362 |
363 | if !command.RunCalled {
364 | t.Fatalf("run should be called")
365 | }
366 |
367 | if !reflect.DeepEqual(command.RunArgs, []string{}) {
368 | t.Fatalf("bad args: %#v", command.RunArgs)
369 | }
370 | }
371 |
372 | func TestCLIRun_nestedMissingParent(t *testing.T) {
373 | buf := new(bytes.Buffer)
374 | cli := &CLI{
375 | Args: []string{"foo"},
376 | Commands: map[string]CommandFactory{
377 | "foo bar": func() (Command, error) {
378 | return &MockCommand{SynopsisText: "hi!"}, nil
379 | },
380 | },
381 | ErrorWriter: buf,
382 | }
383 |
384 | exitCode, err := cli.Run()
385 | if err != nil {
386 | t.Fatalf("err: %s", err)
387 | }
388 |
389 | if exitCode != 1 {
390 | t.Fatalf("bad exit code: %d", exitCode)
391 | }
392 |
393 | if buf.String() != testCommandNestedMissingParent {
394 | t.Fatalf("bad: %#v", buf.String())
395 | }
396 | }
397 |
398 | func TestCLIRun_nestedNoArgs(t *testing.T) {
399 | command := new(MockCommand)
400 | cli := &CLI{
401 | Args: []string{"foo", "bar"},
402 | Commands: map[string]CommandFactory{
403 | "foo": func() (Command, error) {
404 | return new(MockCommand), nil
405 | },
406 | "foo bar": func() (Command, error) {
407 | return command, nil
408 | },
409 | },
410 | }
411 |
412 | exitCode, err := cli.Run()
413 | if err != nil {
414 | t.Fatalf("err: %s", err)
415 | }
416 |
417 | if exitCode != command.RunResult {
418 | t.Fatalf("bad: %d", exitCode)
419 | }
420 |
421 | if !command.RunCalled {
422 | t.Fatalf("run should be called")
423 | }
424 |
425 | if !reflect.DeepEqual(command.RunArgs, []string{}) {
426 | t.Fatalf("bad args: %#v", command.RunArgs)
427 | }
428 | }
429 |
430 | func TestCLIRun_nestedBlankArg(t *testing.T) {
431 | command := new(MockCommand)
432 | cli := &CLI{
433 | Args: []string{"foo", "", "bar", "-baz"},
434 | Commands: map[string]CommandFactory{
435 | "foo": func() (Command, error) {
436 | return command, nil
437 | },
438 | "foo bar": func() (Command, error) {
439 | return new(MockCommand), nil
440 | },
441 | },
442 | }
443 |
444 | exitCode, err := cli.Run()
445 | if err != nil {
446 | t.Fatalf("err: %s", err)
447 | }
448 |
449 | if exitCode != command.RunResult {
450 | t.Fatalf("bad: %d", exitCode)
451 | }
452 |
453 | if !command.RunCalled {
454 | t.Fatalf("run should be called")
455 | }
456 |
457 | if !reflect.DeepEqual(command.RunArgs, []string{"", "bar", "-baz"}) {
458 | t.Fatalf("bad args: %#v", command.RunArgs)
459 | }
460 | }
461 |
462 | func TestCLIRun_nestedQuotedCommand(t *testing.T) {
463 | command := new(MockCommand)
464 | cli := &CLI{
465 | Args: []string{"foo bar"},
466 | Commands: map[string]CommandFactory{
467 | "foo": func() (Command, error) {
468 | return new(MockCommand), nil
469 | },
470 | "foo bar": func() (Command, error) {
471 | return command, nil
472 | },
473 | },
474 | }
475 |
476 | exitCode, err := cli.Run()
477 | if err != nil {
478 | t.Fatalf("err: %s", err)
479 | }
480 |
481 | if exitCode != 127 {
482 | t.Fatalf("bad: %d", exitCode)
483 | }
484 | }
485 |
486 | func TestCLIRun_nestedQuotedArg(t *testing.T) {
487 | command := new(MockCommand)
488 | cli := &CLI{
489 | Args: []string{"foo", "bar baz"},
490 | Commands: map[string]CommandFactory{
491 | "foo": func() (Command, error) {
492 | return command, nil
493 | },
494 | "foo bar": func() (Command, error) {
495 | return new(MockCommand), nil
496 | },
497 | },
498 | }
499 |
500 | exitCode, err := cli.Run()
501 | if err != nil {
502 | t.Fatalf("err: %s", err)
503 | }
504 |
505 | if exitCode != command.RunResult {
506 | t.Fatalf("bad: %d", exitCode)
507 | }
508 |
509 | if !command.RunCalled {
510 | t.Fatalf("run should be called")
511 | }
512 |
513 | if !reflect.DeepEqual(command.RunArgs, []string{"bar baz"}) {
514 | t.Fatalf("bad args: %#v", command.RunArgs)
515 | }
516 | }
517 |
518 | func TestCLIRun_printHelp(t *testing.T) {
519 | testCases := [][]string{
520 | {"-h"},
521 | {"--help"},
522 | }
523 |
524 | for _, testCase := range testCases {
525 | buf := new(bytes.Buffer)
526 | helpText := "foo"
527 |
528 | cli := &CLI{
529 | Args: testCase,
530 | Commands: map[string]CommandFactory{
531 | "foo": func() (Command, error) {
532 | return new(MockCommand), nil
533 | },
534 | },
535 | HelpFunc: func(map[string]CommandFactory) string {
536 | return helpText
537 | },
538 | HelpWriter: buf,
539 | }
540 |
541 | code, err := cli.Run()
542 | if err != nil {
543 | t.Errorf("Args: %#v. Error: %s", testCase, err)
544 | continue
545 | }
546 |
547 | if code != 0 {
548 | t.Errorf("Args: %#v. Code: %d", testCase, code)
549 | continue
550 | }
551 |
552 | if !strings.Contains(buf.String(), helpText) {
553 | t.Errorf("Args: %#v. Text: %v", testCase, buf.String())
554 | }
555 | }
556 | }
557 |
558 | func TestCLIRun_printHelpIllegal(t *testing.T) {
559 | testCases := []struct {
560 | args []string
561 | exit int
562 | }{
563 | {nil, 127},
564 | {[]string{"i-dont-exist"}, 127},
565 | {[]string{"-bad-flag", "foo"}, 1},
566 | }
567 |
568 | for _, testCase := range testCases {
569 | buf := new(bytes.Buffer)
570 | helpText := "foo"
571 |
572 | cli := &CLI{
573 | Args: testCase.args,
574 | Commands: map[string]CommandFactory{
575 | "foo": func() (Command, error) {
576 | return &MockCommand{HelpText: helpText}, nil
577 | },
578 | "foo sub42": func() (Command, error) {
579 | return new(MockCommand), nil
580 | },
581 | },
582 | HelpFunc: func(m map[string]CommandFactory) string {
583 | var keys []string
584 | for k := range m {
585 | keys = append(keys, k)
586 | }
587 | sort.Strings(keys)
588 |
589 | expected := []string{"foo"}
590 | if !reflect.DeepEqual(keys, expected) {
591 | return fmt.Sprintf("error: contained sub: %#v", keys)
592 | }
593 |
594 | return helpText
595 | },
596 | ErrorWriter: buf,
597 | }
598 |
599 | code, err := cli.Run()
600 | if err != nil {
601 | t.Errorf("Args: %#v. Error: %s", testCase, err)
602 | continue
603 | }
604 |
605 | if code != testCase.exit {
606 | t.Errorf("Args: %#v. Code: %d", testCase, code)
607 | continue
608 | }
609 |
610 | if strings.Contains(buf.String(), "error") {
611 | t.Errorf("Args: %#v. Text: %v", testCase, buf.String())
612 | }
613 |
614 | if !strings.Contains(buf.String(), helpText) {
615 | t.Errorf("Args: %#v. Text: %v", testCase, buf.String())
616 | }
617 | }
618 | }
619 |
620 | func TestCLIRun_printCommandHelp(t *testing.T) {
621 | testCases := [][]string{
622 | {"--help", "foo"},
623 | {"-h", "foo"},
624 | }
625 |
626 | for _, args := range testCases {
627 | command := &MockCommand{
628 | HelpText: "donuts",
629 | }
630 |
631 | buf := new(bytes.Buffer)
632 | cli := &CLI{
633 | Args: args,
634 | Commands: map[string]CommandFactory{
635 | "foo": func() (Command, error) {
636 | return command, nil
637 | },
638 | },
639 | HelpWriter: buf,
640 | }
641 |
642 | exitCode, err := cli.Run()
643 | if err != nil {
644 | t.Fatalf("err: %s", err)
645 | }
646 |
647 | if exitCode != 0 {
648 | t.Fatalf("bad exit code: %d", exitCode)
649 | }
650 |
651 | if buf.String() != (command.HelpText + "\n") {
652 | t.Fatalf("bad: %#v", buf.String())
653 | }
654 | }
655 | }
656 |
657 | func TestCLIRun_printCommandHelpNested(t *testing.T) {
658 | testCases := [][]string{
659 | {"--help", "foo", "bar"},
660 | {"-h", "foo", "bar"},
661 | }
662 |
663 | for _, args := range testCases {
664 | command := &MockCommand{
665 | HelpText: "donuts",
666 | }
667 |
668 | buf := new(bytes.Buffer)
669 | cli := &CLI{
670 | Args: args,
671 | Commands: map[string]CommandFactory{
672 | "foo bar": func() (Command, error) {
673 | return command, nil
674 | },
675 | },
676 | HelpWriter: buf,
677 | }
678 |
679 | exitCode, err := cli.Run()
680 | if err != nil {
681 | t.Fatalf("err: %s", err)
682 | }
683 |
684 | if exitCode != 0 {
685 | t.Fatalf("bad exit code: %d", exitCode)
686 | }
687 |
688 | if buf.String() != (command.HelpText + "\n") {
689 | t.Fatalf("bad: %#v", buf.String())
690 | }
691 | }
692 | }
693 |
694 | func TestCLIRun_printCommandHelpSubcommands(t *testing.T) {
695 | testCases := [][]string{
696 | {"--help", "foo"},
697 | {"-h", "foo"},
698 | }
699 |
700 | for _, args := range testCases {
701 | command := &MockCommand{
702 | HelpText: "donuts",
703 | }
704 |
705 | buf := new(bytes.Buffer)
706 | cli := &CLI{
707 | Args: args,
708 | Commands: map[string]CommandFactory{
709 | "foo": func() (Command, error) {
710 | return command, nil
711 | },
712 | "foo bar": func() (Command, error) {
713 | return &MockCommand{SynopsisText: "hi!"}, nil
714 | },
715 | "foo zip": func() (Command, error) {
716 | return &MockCommand{SynopsisText: "hi!"}, nil
717 | },
718 | "foo zap": func() (Command, error) {
719 | return &MockCommand{SynopsisText: "hi!"}, nil
720 | },
721 | "foo banana": func() (Command, error) {
722 | return &MockCommand{SynopsisText: "hi!"}, nil
723 | },
724 | "foo longer": func() (Command, error) {
725 | return &MockCommand{SynopsisText: "hi!"}, nil
726 | },
727 | "foo longer longest": func() (Command, error) {
728 | return &MockCommand{SynopsisText: "hi!"}, nil
729 | },
730 | },
731 | HelpWriter: buf,
732 | }
733 |
734 | exitCode, err := cli.Run()
735 | if err != nil {
736 | t.Fatalf("err: %s", err)
737 | }
738 |
739 | if exitCode != 0 {
740 | t.Fatalf("bad exit code: %d", exitCode)
741 | }
742 |
743 | if buf.String() != testCommandHelpSubcommandsOutput {
744 | t.Fatalf("bad: %#v\n\n'%#v'\n\n'%#v'", args, buf.String(), testCommandHelpSubcommandsOutput)
745 | }
746 | }
747 | }
748 |
749 | func TestCLIRun_printCommandHelpSubcommandsNestedTwoLevel(t *testing.T) {
750 | testCases := [][]string{
751 | {"--help", "L1"},
752 | {"-h", "L1"},
753 | }
754 |
755 | for _, args := range testCases {
756 | command := &MockCommand{
757 | HelpText: "donuts",
758 | }
759 |
760 | buf := new(bytes.Buffer)
761 | cli := &CLI{
762 | Args: args,
763 | Commands: map[string]CommandFactory{
764 | "L1": func() (Command, error) {
765 | return command, nil
766 | },
767 | "L1 L2A": func() (Command, error) {
768 | return &MockCommand{SynopsisText: "hi!"}, nil
769 | },
770 | "L1 L2B": func() (Command, error) {
771 | return &MockCommand{SynopsisText: "hi!"}, nil
772 | },
773 | "L1 L2A L3A": func() (Command, error) {
774 | return &MockCommand{SynopsisText: "hi!"}, nil
775 | },
776 | "L1 L2A L3B": func() (Command, error) {
777 | return &MockCommand{SynopsisText: "hi!"}, nil
778 | },
779 | },
780 | HelpWriter: buf,
781 | }
782 |
783 | exitCode, err := cli.Run()
784 | if err != nil {
785 | t.Fatalf("err: %s", err)
786 | }
787 |
788 | if exitCode != 0 {
789 | t.Fatalf("bad exit code: %d", exitCode)
790 | }
791 |
792 | if buf.String() != testCommandHelpSubcommandsTwoLevelOutput {
793 | t.Fatalf("bad: %#v\n\n%s\n\n%s", args, buf.String(), testCommandHelpSubcommandsOutput)
794 | }
795 | }
796 | }
797 |
798 | // Test that the root help only prints the root level.
799 | func TestCLIRun_printHelpRootSubcommands(t *testing.T) {
800 | testCases := [][]string{
801 | {"--help"},
802 | {"-h"},
803 | }
804 |
805 | for _, args := range testCases {
806 | buf := new(bytes.Buffer)
807 | cli := &CLI{
808 | Args: args,
809 | Commands: map[string]CommandFactory{
810 | "bar": func() (Command, error) {
811 | return &MockCommand{SynopsisText: "hi!"}, nil
812 | },
813 | "foo": func() (Command, error) {
814 | return &MockCommand{SynopsisText: "hi!"}, nil
815 | },
816 | "foo bar": func() (Command, error) {
817 | return &MockCommand{SynopsisText: "hi!"}, nil
818 | },
819 | "foo zip": func() (Command, error) {
820 | return &MockCommand{SynopsisText: "hi!"}, nil
821 | },
822 | },
823 | HelpWriter: buf,
824 | }
825 |
826 | exitCode, err := cli.Run()
827 | if err != nil {
828 | t.Fatalf("err: %s", err)
829 | }
830 |
831 | if exitCode != 0 {
832 | t.Fatalf("bad exit code: %d", exitCode)
833 | }
834 |
835 | expected := `Usage: app [--version] [--help] []
836 |
837 | Available commands are:
838 | bar hi!
839 | foo hi!
840 |
841 | `
842 | if buf.String() != expected {
843 | t.Fatalf("bad: %#v\n\n'%#v'\n\n'%#v'", args, buf.String(), expected)
844 | }
845 | }
846 | }
847 |
848 | func TestCLIRun_printCommandHelpTemplate(t *testing.T) {
849 | testCases := [][]string{
850 | {"--help", "foo"},
851 | {"-h", "foo"},
852 | }
853 |
854 | for _, args := range testCases {
855 | command := &MockCommandHelpTemplate{
856 | MockCommand: MockCommand{
857 | HelpText: "donuts",
858 | },
859 |
860 | HelpTemplateText: "hello {{.Help}}",
861 | }
862 |
863 | buf := new(bytes.Buffer)
864 | cli := &CLI{
865 | Args: args,
866 | Commands: map[string]CommandFactory{
867 | "foo": func() (Command, error) {
868 | return command, nil
869 | },
870 | },
871 | HelpWriter: buf,
872 | }
873 |
874 | exitCode, err := cli.Run()
875 | if err != nil {
876 | t.Fatalf("err: %s", err)
877 | }
878 |
879 | if exitCode != 0 {
880 | t.Fatalf("bad exit code: %d", exitCode)
881 | }
882 |
883 | if buf.String() != "hello "+command.HelpText+"\n" {
884 | t.Fatalf("bad: %#v", buf.String())
885 | }
886 | }
887 | }
888 |
889 | func TestCLIRun_helpHiddenRoot(t *testing.T) {
890 | helpCalled := false
891 | buf := new(bytes.Buffer)
892 | cli := &CLI{
893 | Args: []string{"--help"},
894 | HiddenCommands: []string{"bar"},
895 | Commands: map[string]CommandFactory{
896 | "foo": func() (Command, error) {
897 | return &MockCommand{}, nil
898 | },
899 | "bar": func() (Command, error) {
900 | return &MockCommand{}, nil
901 | },
902 | },
903 | HelpFunc: func(m map[string]CommandFactory) string {
904 | helpCalled = true
905 |
906 | if _, ok := m["foo"]; !ok {
907 | t.Fatal("should have foo")
908 | }
909 | if _, ok := m["bar"]; ok {
910 | t.Fatal("should not have bar")
911 | }
912 |
913 | return ""
914 | },
915 | ErrorWriter: buf,
916 | }
917 |
918 | code, err := cli.Run()
919 | if err != nil {
920 | t.Fatalf("Error: %s", err)
921 | }
922 |
923 | if code != 0 {
924 | t.Fatalf("Code: %d", code)
925 | }
926 |
927 | if !helpCalled {
928 | t.Fatal("help not called")
929 | }
930 | }
931 |
932 | func TestCLIRun_helpHiddenNested(t *testing.T) {
933 | command := &MockCommand{
934 | HelpText: "donuts",
935 | }
936 |
937 | buf := new(bytes.Buffer)
938 | cli := &CLI{
939 | Args: []string{"foo", "--help"},
940 | Commands: map[string]CommandFactory{
941 | "foo": func() (Command, error) {
942 | return command, nil
943 | },
944 | "foo bar": func() (Command, error) {
945 | return &MockCommand{SynopsisText: "hi!"}, nil
946 | },
947 | "foo zip": func() (Command, error) {
948 | return &MockCommand{SynopsisText: "hi!"}, nil
949 | },
950 | "foo longer": func() (Command, error) {
951 | return &MockCommand{SynopsisText: "hi!"}, nil
952 | },
953 | "foo longer longest": func() (Command, error) {
954 | return &MockCommand{SynopsisText: "hi!"}, nil
955 | },
956 | },
957 | HiddenCommands: []string{"foo zip", "foo longer longest"},
958 | HelpWriter: buf,
959 | }
960 |
961 | exitCode, err := cli.Run()
962 | if err != nil {
963 | t.Fatalf("err: %s", err)
964 | }
965 |
966 | if exitCode != 0 {
967 | t.Fatalf("bad exit code: %d", exitCode)
968 | }
969 |
970 | if buf.String() != testCommandHelpSubcommandsHiddenOutput {
971 | t.Fatalf("bad: '%#v'\n\n'%#v'", buf.String(), testCommandHelpSubcommandsOutput)
972 | }
973 | }
974 |
975 | func TestCLIRun_autocompleteBoth(t *testing.T) {
976 | command := new(MockCommand)
977 | cli := &CLI{
978 | Args: []string{
979 | "-" + defaultAutocompleteInstall,
980 | "-" + defaultAutocompleteUninstall,
981 | },
982 | Commands: map[string]CommandFactory{
983 | "foo": func() (Command, error) {
984 | return command, nil
985 | },
986 | },
987 |
988 | Name: "foo",
989 | Autocomplete: true,
990 | autocompleteInstaller: &mockAutocompleteInstaller{},
991 | }
992 |
993 | exitCode, err := cli.Run()
994 | if err == nil {
995 | t.Fatal("should error")
996 | }
997 |
998 | if exitCode != 1 {
999 | t.Fatalf("bad: %d", exitCode)
1000 | }
1001 |
1002 | if command.RunCalled {
1003 | t.Fatalf("run should not be called")
1004 | }
1005 | }
1006 |
1007 | func TestCLIRun_autocompleteInstall(t *testing.T) {
1008 | command := new(MockCommand)
1009 | installer := new(mockAutocompleteInstaller)
1010 | cli := &CLI{
1011 | Args: []string{
1012 | "-" + defaultAutocompleteInstall,
1013 | },
1014 | Commands: map[string]CommandFactory{
1015 | "foo": func() (Command, error) {
1016 | return command, nil
1017 | },
1018 | },
1019 |
1020 | Name: "foo",
1021 | Autocomplete: true,
1022 | autocompleteInstaller: installer,
1023 | }
1024 |
1025 | exitCode, err := cli.Run()
1026 | if err != nil {
1027 | t.Fatalf("err: %s", err)
1028 | }
1029 |
1030 | if exitCode != 0 {
1031 | t.Fatalf("bad: %d", exitCode)
1032 | }
1033 |
1034 | if command.RunCalled {
1035 | t.Fatalf("run should not be called")
1036 | }
1037 |
1038 | if !installer.InstallCalled {
1039 | t.Fatal("should call install")
1040 | }
1041 | }
1042 |
1043 | func TestCLIRun_autocompleteUninstall(t *testing.T) {
1044 | command := new(MockCommand)
1045 | installer := new(mockAutocompleteInstaller)
1046 | cli := &CLI{
1047 | Args: []string{
1048 | "-" + defaultAutocompleteUninstall,
1049 | },
1050 | Commands: map[string]CommandFactory{
1051 | "foo": func() (Command, error) {
1052 | return command, nil
1053 | },
1054 | },
1055 |
1056 | Name: "foo",
1057 | Autocomplete: true,
1058 | autocompleteInstaller: installer,
1059 | }
1060 |
1061 | exitCode, err := cli.Run()
1062 | if err != nil {
1063 | t.Fatalf("err: %s", err)
1064 | }
1065 |
1066 | if exitCode != 0 {
1067 | t.Fatalf("bad: %d", exitCode)
1068 | }
1069 |
1070 | if command.RunCalled {
1071 | t.Fatalf("run should not be called")
1072 | }
1073 |
1074 | if !installer.UninstallCalled {
1075 | t.Fatal("should call uninstall")
1076 | }
1077 | }
1078 |
1079 | func TestCLIRun_autocompleteNoName(t *testing.T) {
1080 | command := new(MockCommand)
1081 | installer := new(mockAutocompleteInstaller)
1082 | cli := &CLI{
1083 | Args: []string{"foo"},
1084 | Commands: map[string]CommandFactory{
1085 | "foo": func() (Command, error) {
1086 | return command, nil
1087 | },
1088 | },
1089 |
1090 | Autocomplete: true,
1091 | autocompleteInstaller: installer,
1092 | }
1093 |
1094 | exitCode, err := cli.Run()
1095 | if err == nil {
1096 | t.Fatal("should error")
1097 | }
1098 |
1099 | if exitCode != 1 {
1100 | t.Fatalf("bad: %d", exitCode)
1101 | }
1102 |
1103 | if command.RunCalled {
1104 | t.Fatalf("run should not be called")
1105 | }
1106 | }
1107 |
1108 | // Test that running `-autocomplete-install` doesn't execute
1109 | // the autocomplete installer. This was a bug reported by Nomad.
1110 | func TestCLIRun_autocompleteInstallTab(t *testing.T) {
1111 | command := new(MockCommand)
1112 | installer := new(mockAutocompleteInstaller)
1113 | cli := &CLI{
1114 | Args: []string{
1115 | "-" + defaultAutocompleteInstall,
1116 | },
1117 | Commands: map[string]CommandFactory{
1118 | "foo": func() (Command, error) {
1119 | return command, nil
1120 | },
1121 | },
1122 |
1123 | Name: "foo",
1124 | Autocomplete: true,
1125 | autocompleteInstaller: installer,
1126 | }
1127 |
1128 | defer testAutocomplete(t, "foo -autocomplete-install")()
1129 |
1130 | exitCode, err := cli.Run()
1131 | if err != nil {
1132 | t.Fatalf("err: %s", err)
1133 | }
1134 |
1135 | if exitCode != 0 {
1136 | t.Fatalf("bad: %d", exitCode)
1137 | }
1138 |
1139 | if command.RunCalled {
1140 | t.Fatalf("run should not be called")
1141 | }
1142 |
1143 | if installer.InstallCalled {
1144 | t.Fatal("should not call install")
1145 | }
1146 | }
1147 |
1148 | func TestCLIRun_autocompleteHelpTab(t *testing.T) {
1149 | buf := new(bytes.Buffer)
1150 | command := new(MockCommand)
1151 | installer := new(mockAutocompleteInstaller)
1152 | cli := &CLI{
1153 | Args: []string{
1154 | "-help",
1155 | },
1156 | Commands: map[string]CommandFactory{
1157 | "foo": func() (Command, error) {
1158 | return command, nil
1159 | },
1160 | },
1161 |
1162 | Name: "foo",
1163 | ErrorWriter: buf,
1164 | Autocomplete: true,
1165 | autocompleteInstaller: installer,
1166 | }
1167 |
1168 | defer testAutocomplete(t, "foo -help")()
1169 |
1170 | exitCode, err := cli.Run()
1171 | if err != nil {
1172 | t.Fatalf("err: %s", err)
1173 | }
1174 |
1175 | if exitCode != 0 {
1176 | t.Fatalf("bad: %d", exitCode)
1177 | }
1178 |
1179 | if command.RunCalled {
1180 | t.Fatalf("run should not be called")
1181 | }
1182 |
1183 | if buf.String() != "" {
1184 | t.Fatal("help should be empty")
1185 | }
1186 | }
1187 |
1188 | func TestCLIAutocomplete_root(t *testing.T) {
1189 | cases := []struct {
1190 | Completed []string
1191 | Last string
1192 | Expected []string
1193 | }{
1194 | {nil, "-v", []string{"-version"}},
1195 | {nil, "-h", []string{"-help"}},
1196 | {nil, "-a", []string{
1197 | "-" + defaultAutocompleteInstall,
1198 | "-" + defaultAutocompleteUninstall,
1199 | }},
1200 |
1201 | {nil, "f", []string{"foo"}},
1202 | {nil, "n", []string{"nodes", "noodles"}},
1203 | {nil, "noo", []string{"noodles"}},
1204 | {nil, "su", []string{"sub"}},
1205 | {nil, "h", nil},
1206 |
1207 | // Make sure global flags work on subcommands
1208 | {[]string{"sub"}, "-v", nil},
1209 | {[]string{"sub"}, "o", []string{"one"}},
1210 | {[]string{"sub"}, "su", []string{"sub2"}},
1211 | {[]string{"sub", "sub2"}, "o", []string{"one"}},
1212 | {[]string{"deep", "deep2"}, "a", []string{"a1"}},
1213 | }
1214 |
1215 | for _, tc := range cases {
1216 | t.Run(tc.Last, func(t *testing.T) {
1217 | command := new(MockCommand)
1218 | cli := &CLI{
1219 | Commands: map[string]CommandFactory{
1220 | "foo": func() (Command, error) { return command, nil },
1221 | "nodes": func() (Command, error) { return command, nil },
1222 | "noodles": func() (Command, error) { return command, nil },
1223 | "hidden": func() (Command, error) { return command, nil },
1224 | "sub one": func() (Command, error) { return command, nil },
1225 | "sub two": func() (Command, error) { return command, nil },
1226 | "sub sub2 one": func() (Command, error) { return command, nil },
1227 | "sub sub2 two": func() (Command, error) { return command, nil },
1228 | "deep deep2 a1": func() (Command, error) { return command, nil },
1229 | "deep deep2 b2": func() (Command, error) { return command, nil },
1230 | },
1231 | HiddenCommands: []string{"hidden"},
1232 |
1233 | Autocomplete: true,
1234 | }
1235 |
1236 | // Setup the autocomplete line
1237 | var input bytes.Buffer
1238 | input.WriteString("cli ")
1239 | if len(tc.Completed) > 0 {
1240 | input.WriteString(strings.Join(tc.Completed, " "))
1241 | input.WriteString(" ")
1242 | }
1243 | input.WriteString(tc.Last)
1244 | defer testAutocomplete(t, input.String())()
1245 |
1246 | // Setup the output so that we can read it. We don't need to
1247 | // reset os.Stdout because testAutocomplete will do that for us.
1248 | r, w, err := os.Pipe()
1249 | if err != nil {
1250 | t.Fatalf("err: %s", err)
1251 | }
1252 | defer r.Close() // Only defer reader since writer is closed below
1253 | os.Stdout = w
1254 |
1255 | // Run
1256 | exitCode, err := cli.Run()
1257 | w.Close()
1258 | if err != nil {
1259 | t.Fatalf("err: %s", err)
1260 | }
1261 |
1262 | if exitCode != 0 {
1263 | t.Fatalf("bad: %d", exitCode)
1264 | }
1265 |
1266 | // Copy the output and get the autocompletions. We trim the last
1267 | // element if we have one since we usually output a final newline
1268 | // which results in a blank.
1269 | var outBuf bytes.Buffer
1270 | io.Copy(&outBuf, r)
1271 | actual := strings.Split(outBuf.String(), "\n")
1272 | if len(actual) > 0 {
1273 | actual = actual[:len(actual)-1]
1274 | }
1275 | if len(actual) == 0 {
1276 | // If we have no elements left, make the value nil since
1277 | // this is what we use in tests.
1278 | actual = nil
1279 | }
1280 |
1281 | sort.Strings(actual)
1282 | sort.Strings(tc.Expected)
1283 | if !reflect.DeepEqual(actual, tc.Expected) {
1284 | t.Fatalf("bad:\n\n%#v\n\n%#v", actual, tc.Expected)
1285 | }
1286 | })
1287 | }
1288 | }
1289 |
1290 | func TestCLIAutocomplete_rootGlobalFlags(t *testing.T) {
1291 | cases := []struct {
1292 | Completed []string
1293 | Last string
1294 | Expected []string
1295 | }{
1296 | {nil, "-v", []string{"-version"}},
1297 | {nil, "-t", []string{"-tubes"}},
1298 | }
1299 |
1300 | for _, tc := range cases {
1301 | t.Run(tc.Last, func(t *testing.T) {
1302 | command := new(MockCommand)
1303 | cli := &CLI{
1304 | Commands: map[string]CommandFactory{
1305 | "foo": func() (Command, error) { return command, nil },
1306 | },
1307 |
1308 | Autocomplete: true,
1309 | AutocompleteGlobalFlags: map[string]complete.Predictor{
1310 | "-tubes": complete.PredictNothing,
1311 | },
1312 | }
1313 |
1314 | // Setup the autocomplete line
1315 | var input bytes.Buffer
1316 | input.WriteString("cli ")
1317 | if len(tc.Completed) > 0 {
1318 | input.WriteString(strings.Join(tc.Completed, " "))
1319 | input.WriteString(" ")
1320 | }
1321 | input.WriteString(tc.Last)
1322 | defer testAutocomplete(t, input.String())()
1323 |
1324 | // Setup the output so that we can read it. We don't need to
1325 | // reset os.Stdout because testAutocomplete will do that for us.
1326 | r, w, err := os.Pipe()
1327 | if err != nil {
1328 | t.Fatalf("err: %s", err)
1329 | }
1330 | defer r.Close() // Only defer reader since writer is closed below
1331 | os.Stdout = w
1332 |
1333 | // Run
1334 | exitCode, err := cli.Run()
1335 | w.Close()
1336 | if err != nil {
1337 | t.Fatalf("err: %s", err)
1338 | }
1339 |
1340 | if exitCode != 0 {
1341 | t.Fatalf("bad: %d", exitCode)
1342 | }
1343 |
1344 | // Copy the output and get the autocompletions. We trim the last
1345 | // element if we have one since we usually output a final newline
1346 | // which results in a blank.
1347 | var outBuf bytes.Buffer
1348 | io.Copy(&outBuf, r)
1349 | actual := strings.Split(outBuf.String(), "\n")
1350 | if len(actual) > 0 {
1351 | actual = actual[:len(actual)-1]
1352 | }
1353 | if len(actual) == 0 {
1354 | // If we have no elements left, make the value nil since
1355 | // this is what we use in tests.
1356 | actual = nil
1357 | }
1358 |
1359 | sort.Strings(actual)
1360 | sort.Strings(tc.Expected)
1361 | if !reflect.DeepEqual(actual, tc.Expected) {
1362 | t.Fatalf("bad:\n\n%#v\n\n%#v", actual, tc.Expected)
1363 | }
1364 | })
1365 | }
1366 | }
1367 |
1368 | func TestCLIAutocomplete_rootDisableDefaultFlags(t *testing.T) {
1369 | cases := []struct {
1370 | Completed []string
1371 | Last string
1372 | Expected []string
1373 | }{
1374 | {nil, "-v", nil},
1375 | {nil, "-h", nil},
1376 | {nil, "-auto", nil},
1377 | {nil, "-t", []string{"-tubes"}},
1378 | }
1379 |
1380 | for _, tc := range cases {
1381 | t.Run(tc.Last, func(t *testing.T) {
1382 | command := new(MockCommand)
1383 | cli := &CLI{
1384 | Commands: map[string]CommandFactory{
1385 | "foo": func() (Command, error) { return command, nil },
1386 | },
1387 |
1388 | Autocomplete: true,
1389 | AutocompleteNoDefaultFlags: true,
1390 | AutocompleteGlobalFlags: map[string]complete.Predictor{
1391 | "-tubes": complete.PredictNothing,
1392 | },
1393 | }
1394 | // Setup the autocomplete line
1395 | var input bytes.Buffer
1396 | input.WriteString("cli ")
1397 | if len(tc.Completed) > 0 {
1398 | input.WriteString(strings.Join(tc.Completed, " "))
1399 | input.WriteString(" ")
1400 | }
1401 | input.WriteString(tc.Last)
1402 | defer testAutocomplete(t, input.String())()
1403 |
1404 | // Setup the output so that we can read it. We don't need to
1405 | // reset os.Stdout because testAutocomplete will do that for us.
1406 | r, w, err := os.Pipe()
1407 | if err != nil {
1408 | t.Fatalf("err: %s", err)
1409 | }
1410 | defer r.Close() // Only defer reader since writer is closed below
1411 | os.Stdout = w
1412 |
1413 | // Run
1414 | exitCode, err := cli.Run()
1415 | w.Close()
1416 | if err != nil {
1417 | t.Fatalf("err: %s", err)
1418 | }
1419 |
1420 | if exitCode != 0 {
1421 | t.Fatalf("bad: %d", exitCode)
1422 | }
1423 |
1424 | // Copy the output and get the autocompletions. We trim the last
1425 | // element if we have one since we usually output a final newline
1426 | // which results in a blank.
1427 | var outBuf bytes.Buffer
1428 | io.Copy(&outBuf, r)
1429 | actual := strings.Split(outBuf.String(), "\n")
1430 | if len(actual) > 0 {
1431 | actual = actual[:len(actual)-1]
1432 | }
1433 | if len(actual) == 0 {
1434 | // If we have no elements left, make the value nil since
1435 | // this is what we use in tests.
1436 | actual = nil
1437 | }
1438 |
1439 | sort.Strings(actual)
1440 | sort.Strings(tc.Expected)
1441 | if !reflect.DeepEqual(actual, tc.Expected) {
1442 | t.Fatalf("bad:\n\n%#v\n\n%#v", actual, tc.Expected)
1443 | }
1444 | })
1445 | }
1446 | }
1447 |
1448 | func TestCLIAutocomplete_subcommandArgs(t *testing.T) {
1449 | cases := []struct {
1450 | Completed []string
1451 | Last string
1452 | Expected []string
1453 | }{
1454 | {[]string{"foo"}, "RE", []string{"README.md"}},
1455 | {[]string{"foo", "-go"}, "asdf", []string{"yo"}},
1456 | }
1457 |
1458 | for _, tc := range cases {
1459 | t.Run(tc.Last, func(t *testing.T) {
1460 | command := new(MockCommandAutocomplete)
1461 | command.AutocompleteArgsValue = complete.PredictFiles("*")
1462 | command.AutocompleteFlagsValue = map[string]complete.Predictor{
1463 | "-go": complete.PredictFunc(func(complete.Args) []string {
1464 | return []string{"yo"}
1465 | }),
1466 | }
1467 |
1468 | cli := &CLI{
1469 | Commands: map[string]CommandFactory{
1470 | "foo": func() (Command, error) {
1471 | return command, nil
1472 | },
1473 | },
1474 |
1475 | Autocomplete: true,
1476 | }
1477 |
1478 | // We need to initialize the autocomplete environment so that
1479 | // the cli doesn't no-op the autocomplete init
1480 | defer testAutocomplete(t, "must be non-empty")()
1481 |
1482 | // Initialize
1483 | cli.init()
1484 |
1485 | // Test the autocompleter
1486 | actual := cli.autocomplete.Command.Predict(complete.Args{
1487 | Completed: tc.Completed,
1488 | Last: tc.Last,
1489 | LastCompleted: tc.Completed[len(tc.Completed)-1],
1490 | })
1491 | sort.Strings(actual)
1492 |
1493 | if !reflect.DeepEqual(actual, tc.Expected) {
1494 | t.Fatalf("bad prediction: %#v", actual)
1495 | }
1496 | })
1497 | }
1498 | }
1499 |
1500 | func TestCLISubcommand(t *testing.T) {
1501 | testCases := []struct {
1502 | args []string
1503 | subcommand string
1504 | }{
1505 | {[]string{"bar"}, "bar"},
1506 | {[]string{"foo", "-h"}, "foo"},
1507 | {[]string{"-h", "bar"}, "bar"},
1508 | {[]string{"foo", "bar", "-h"}, "foo"},
1509 | }
1510 |
1511 | for _, testCase := range testCases {
1512 | cli := &CLI{Args: testCase.args}
1513 | result := cli.Subcommand()
1514 |
1515 | if result != testCase.subcommand {
1516 | t.Errorf("Expected %#v, got %#v. Args: %#v",
1517 | testCase.subcommand, result, testCase.args)
1518 | }
1519 | }
1520 | }
1521 |
1522 | func TestCLISubcommand_nested(t *testing.T) {
1523 | testCases := []struct {
1524 | args []string
1525 | subcommand string
1526 | }{
1527 | {[]string{"bar"}, "bar"},
1528 | {[]string{"foo", "-h"}, "foo"},
1529 | {[]string{"-h", "bar"}, "bar"},
1530 | {[]string{"foo", "bar", "-h"}, "foo bar"},
1531 | {[]string{"foo", "bar", "baz", "-h"}, "foo bar"},
1532 | {[]string{"foo", "bar", "-h", "baz"}, "foo bar"},
1533 | {[]string{"-h", "foo", "bar"}, "foo bar"},
1534 | }
1535 |
1536 | for _, testCase := range testCases {
1537 | cli := &CLI{
1538 | Args: testCase.args,
1539 | Commands: map[string]CommandFactory{
1540 | "foo bar": func() (Command, error) {
1541 | return new(MockCommand), nil
1542 | },
1543 | },
1544 | }
1545 | result := cli.Subcommand()
1546 |
1547 | if result != testCase.subcommand {
1548 | t.Errorf("Expected %#v, got %#v. Args: %#v",
1549 | testCase.subcommand, result, testCase.args)
1550 | }
1551 | }
1552 | }
1553 |
1554 | // testAutocomplete sets up the environment to behave like a was
1555 | // pressed in a shell to autocomplete a command.
1556 | func testAutocomplete(t *testing.T, input string) func() {
1557 | // This env var is used to trigger autocomplete
1558 | os.Setenv(envComplete, input)
1559 |
1560 | // Change stdout/stderr since the autocompleter writes directly to them.
1561 | oldStdout := os.Stdout
1562 | oldStderr := os.Stderr
1563 |
1564 | r, w, err := os.Pipe()
1565 | if err != nil {
1566 | t.Fatalf("err: %s", err)
1567 | }
1568 |
1569 | os.Stdout = w
1570 | os.Stderr = w
1571 |
1572 | return func() {
1573 | // Reset our env
1574 | os.Unsetenv(envComplete)
1575 |
1576 | // Reset stdout, stderr
1577 | os.Stdout = oldStdout
1578 | os.Stderr = oldStderr
1579 |
1580 | // Close our pipe
1581 | r.Close()
1582 | w.Close()
1583 | }
1584 | }
1585 |
1586 | const testCommandNestedMissingParent = `This command is accessed by using one of the subcommands below.
1587 |
1588 | Subcommands:
1589 | bar hi!
1590 | `
1591 |
1592 | const testCommandHelpSubcommandsOutput = `donuts
1593 |
1594 | Subcommands:
1595 | banana hi!
1596 | bar hi!
1597 | longer hi!
1598 | zap hi!
1599 | zip hi!
1600 | `
1601 |
1602 | const testCommandHelpSubcommandsHiddenOutput = `donuts
1603 |
1604 | Subcommands:
1605 | bar hi!
1606 | longer hi!
1607 | `
1608 |
1609 | const testCommandHelpSubcommandsTwoLevelOutput = `donuts
1610 |
1611 | Subcommands:
1612 | L2A hi!
1613 | L2B hi!
1614 | `
1615 |
--------------------------------------------------------------------------------
/command.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "github.com/posener/complete"
5 | )
6 |
7 | const (
8 | // RunResultHelp is a value that can be returned from Run to signal
9 | // to the CLI to render the help output.
10 | RunResultHelp = -18511
11 | )
12 |
13 | // A command is a runnable sub-command of a CLI.
14 | type Command interface {
15 | // Help should return long-form help text that includes the command-line
16 | // usage, a brief few sentences explaining the function of the command,
17 | // and the complete list of flags the command accepts.
18 | Help() string
19 |
20 | // Run should run the actual command with the given CLI instance and
21 | // command-line arguments. It should return the exit status when it is
22 | // finished.
23 | //
24 | // There are a handful of special exit codes this can return documented
25 | // above that change behavior.
26 | Run(args []string) int
27 |
28 | // Synopsis should return a one-line, short synopsis of the command.
29 | // This should be less than 50 characters ideally.
30 | Synopsis() string
31 | }
32 |
33 | // CommandAutocomplete is an extension of Command that enables fine-grained
34 | // autocompletion. Subcommand autocompletion will work even if this interface
35 | // is not implemented. By implementing this interface, more advanced
36 | // autocompletion is enabled.
37 | type CommandAutocomplete interface {
38 | // AutocompleteArgs returns the argument predictor for this command.
39 | // If argument completion is not supported, this should return
40 | // complete.PredictNothing.
41 | AutocompleteArgs() complete.Predictor
42 |
43 | // AutocompleteFlags returns a mapping of supported flags and autocomplete
44 | // options for this command. The map key for the Flags map should be the
45 | // complete flag such as "-foo" or "--foo".
46 | AutocompleteFlags() complete.Flags
47 | }
48 |
49 | // CommandHelpTemplate is an extension of Command that also has a function
50 | // for returning a template for the help rather than the help itself. In
51 | // this scenario, both Help and HelpTemplate should be implemented.
52 | //
53 | // If CommandHelpTemplate isn't implemented, the Help is output as-is.
54 | type CommandHelpTemplate interface {
55 | // HelpTemplate is the template in text/template format to use for
56 | // displaying the Help. The keys available are:
57 | //
58 | // * ".Help" - The help text itself
59 | // * ".Subcommands"
60 | //
61 | HelpTemplate() string
62 | }
63 |
64 | // CommandFactory is a type of function that is a factory for commands.
65 | // We need a factory because we may need to setup some state on the
66 | // struct that implements the command itself.
67 | type CommandFactory func() (Command, error)
68 |
--------------------------------------------------------------------------------
/command_mock.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "github.com/posener/complete"
5 | )
6 |
7 | // MockCommand is an implementation of Command that can be used for tests.
8 | // It is publicly exported from this package in case you want to use it
9 | // externally.
10 | type MockCommand struct {
11 | // Settable
12 | HelpText string
13 | RunResult int
14 | SynopsisText string
15 |
16 | // Set by the command
17 | RunCalled bool
18 | RunArgs []string
19 | }
20 |
21 | func (c *MockCommand) Help() string {
22 | return c.HelpText
23 | }
24 |
25 | func (c *MockCommand) Run(args []string) int {
26 | c.RunCalled = true
27 | c.RunArgs = args
28 |
29 | return c.RunResult
30 | }
31 |
32 | func (c *MockCommand) Synopsis() string {
33 | return c.SynopsisText
34 | }
35 |
36 | // MockCommandAutocomplete is an implementation of CommandAutocomplete.
37 | type MockCommandAutocomplete struct {
38 | MockCommand
39 |
40 | // Settable
41 | AutocompleteArgsValue complete.Predictor
42 | AutocompleteFlagsValue complete.Flags
43 | }
44 |
45 | func (c *MockCommandAutocomplete) AutocompleteArgs() complete.Predictor {
46 | return c.AutocompleteArgsValue
47 | }
48 |
49 | func (c *MockCommandAutocomplete) AutocompleteFlags() complete.Flags {
50 | return c.AutocompleteFlagsValue
51 | }
52 |
53 | // MockCommandHelpTemplate is an implementation of CommandHelpTemplate.
54 | type MockCommandHelpTemplate struct {
55 | MockCommand
56 |
57 | // Settable
58 | HelpTemplateText string
59 | }
60 |
61 | func (c *MockCommandHelpTemplate) HelpTemplate() string {
62 | return c.HelpTemplateText
63 | }
64 |
--------------------------------------------------------------------------------
/command_mock_test.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestMockCommand_implements(t *testing.T) {
8 | var _ Command = new(MockCommand)
9 | }
10 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mitchellh/cli
2 |
3 | go 1.11
4 |
5 | require (
6 | github.com/Masterminds/sprig/v3 v3.2.1
7 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310
8 | github.com/bgentry/speakeasy v0.1.0
9 | github.com/fatih/color v1.7.0
10 | github.com/google/uuid v1.1.2 // indirect
11 | github.com/hashicorp/go-multierror v1.0.0 // indirect
12 | github.com/huandu/xstrings v1.3.2 // indirect
13 | github.com/mattn/go-colorable v0.0.9 // indirect
14 | github.com/mattn/go-isatty v0.0.3
15 | github.com/posener/complete v1.1.1
16 | github.com/stretchr/testify v1.6.1 // indirect
17 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
18 | )
19 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
2 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
3 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
4 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
5 | github.com/Masterminds/sprig/v3 v3.2.1 h1:n6EPaDyLSvCEa3frruQvAiHuNp2dhBlMSmkEr+HuzGc=
6 | github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
7 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to=
8 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
9 | github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
10 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
15 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
16 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
17 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
18 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
19 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
20 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
21 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
22 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
23 | github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
24 | github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
25 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
26 | github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
27 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
28 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
29 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
30 | github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
31 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
32 | github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
33 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
34 | github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
35 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
36 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
37 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
38 | github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w=
39 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
40 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
41 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
42 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
43 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
45 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
46 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
47 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
48 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
49 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
50 | golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
51 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
52 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
53 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
54 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
55 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
56 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
57 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
60 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
61 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
62 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
63 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
64 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
65 |
--------------------------------------------------------------------------------
/help.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "log"
7 | "sort"
8 | "strings"
9 | )
10 |
11 | // HelpFunc is the type of the function that is responsible for generating
12 | // the help output when the CLI must show the general help text.
13 | type HelpFunc func(map[string]CommandFactory) string
14 |
15 | // BasicHelpFunc generates some basic help output that is usually good enough
16 | // for most CLI applications.
17 | func BasicHelpFunc(app string) HelpFunc {
18 | return func(commands map[string]CommandFactory) string {
19 | var buf bytes.Buffer
20 | buf.WriteString(fmt.Sprintf(
21 | "Usage: %s [--version] [--help] []\n\n",
22 | app))
23 | buf.WriteString("Available commands are:\n")
24 |
25 | // Get the list of keys so we can sort them, and also get the maximum
26 | // key length so they can be aligned properly.
27 | keys := make([]string, 0, len(commands))
28 | maxKeyLen := 0
29 | for key := range commands {
30 | if len(key) > maxKeyLen {
31 | maxKeyLen = len(key)
32 | }
33 |
34 | keys = append(keys, key)
35 | }
36 | sort.Strings(keys)
37 |
38 | for _, key := range keys {
39 | commandFunc, ok := commands[key]
40 | if !ok {
41 | // This should never happen since we JUST built the list of
42 | // keys.
43 | panic("command not found: " + key)
44 | }
45 |
46 | command, err := commandFunc()
47 | if err != nil {
48 | log.Printf("[ERR] cli: Command '%s' failed to load: %s",
49 | key, err)
50 | continue
51 | }
52 |
53 | key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
54 | buf.WriteString(fmt.Sprintf(" %s %s\n", key, command.Synopsis()))
55 | }
56 |
57 | return buf.String()
58 | }
59 | }
60 |
61 | // FilteredHelpFunc will filter the commands to only include the keys
62 | // in the include parameter.
63 | func FilteredHelpFunc(include []string, f HelpFunc) HelpFunc {
64 | return func(commands map[string]CommandFactory) string {
65 | set := make(map[string]struct{})
66 | for _, k := range include {
67 | set[k] = struct{}{}
68 | }
69 |
70 | filtered := make(map[string]CommandFactory)
71 | for k, f := range commands {
72 | if _, ok := set[k]; ok {
73 | filtered[k] = f
74 | }
75 | }
76 |
77 | return f(filtered)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/ui.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "os"
9 | "os/signal"
10 | "strings"
11 |
12 | "github.com/bgentry/speakeasy"
13 | "github.com/mattn/go-isatty"
14 | )
15 |
16 | // Ui is an interface for interacting with the terminal, or "interface"
17 | // of a CLI. This abstraction doesn't have to be used, but helps provide
18 | // a simple, layerable way to manage user interactions.
19 | type Ui interface {
20 | // Ask asks the user for input using the given query. The response is
21 | // returned as the given string, or an error.
22 | Ask(string) (string, error)
23 |
24 | // AskSecret asks the user for input using the given query, but does not echo
25 | // the keystrokes to the terminal.
26 | AskSecret(string) (string, error)
27 |
28 | // Output is called for normal standard output.
29 | Output(string)
30 |
31 | // Info is called for information related to the previous output.
32 | // In general this may be the exact same as Output, but this gives
33 | // Ui implementors some flexibility with output formats.
34 | Info(string)
35 |
36 | // Error is used for any error messages that might appear on standard
37 | // error.
38 | Error(string)
39 |
40 | // Warn is used for any warning messages that might appear on standard
41 | // error.
42 | Warn(string)
43 | }
44 |
45 | // BasicUi is an implementation of Ui that just outputs to the given
46 | // writer. This UI is not threadsafe by default, but you can wrap it
47 | // in a ConcurrentUi to make it safe.
48 | type BasicUi struct {
49 | Reader io.Reader
50 | Writer io.Writer
51 | ErrorWriter io.Writer
52 | }
53 |
54 | func (u *BasicUi) Ask(query string) (string, error) {
55 | return u.ask(query, false)
56 | }
57 |
58 | func (u *BasicUi) AskSecret(query string) (string, error) {
59 | return u.ask(query, true)
60 | }
61 |
62 | func (u *BasicUi) ask(query string, secret bool) (string, error) {
63 | if _, err := fmt.Fprint(u.Writer, query+" "); err != nil {
64 | return "", err
65 | }
66 |
67 | // Register for interrupts so that we can catch it and immediately
68 | // return...
69 | sigCh := make(chan os.Signal, 1)
70 | signal.Notify(sigCh, os.Interrupt)
71 | defer signal.Stop(sigCh)
72 |
73 | // Ask for input in a go-routine so that we can ignore it.
74 | errCh := make(chan error, 1)
75 | lineCh := make(chan string, 1)
76 | go func() {
77 | var line string
78 | var err error
79 | if secret && isatty.IsTerminal(os.Stdin.Fd()) {
80 | line, err = speakeasy.Ask("")
81 | } else {
82 | r := bufio.NewReader(u.Reader)
83 | line, err = r.ReadString('\n')
84 | }
85 | if err != nil {
86 | errCh <- err
87 | return
88 | }
89 |
90 | lineCh <- strings.TrimRight(line, "\r\n")
91 | }()
92 |
93 | select {
94 | case err := <-errCh:
95 | return "", err
96 | case line := <-lineCh:
97 | return line, nil
98 | case <-sigCh:
99 | // Print a newline so that any further output starts properly
100 | // on a new line.
101 | fmt.Fprintln(u.Writer)
102 |
103 | return "", errors.New("interrupted")
104 | }
105 | }
106 |
107 | func (u *BasicUi) Error(message string) {
108 | w := u.Writer
109 | if u.ErrorWriter != nil {
110 | w = u.ErrorWriter
111 | }
112 |
113 | fmt.Fprint(w, message)
114 | fmt.Fprint(w, "\n")
115 | }
116 |
117 | func (u *BasicUi) Info(message string) {
118 | u.Output(message)
119 | }
120 |
121 | func (u *BasicUi) Output(message string) {
122 | fmt.Fprint(u.Writer, message)
123 | fmt.Fprint(u.Writer, "\n")
124 | }
125 |
126 | func (u *BasicUi) Warn(message string) {
127 | u.Error(message)
128 | }
129 |
130 | // PrefixedUi is an implementation of Ui that prefixes messages.
131 | type PrefixedUi struct {
132 | AskPrefix string
133 | AskSecretPrefix string
134 | OutputPrefix string
135 | InfoPrefix string
136 | ErrorPrefix string
137 | WarnPrefix string
138 | Ui Ui
139 | }
140 |
141 | func (u *PrefixedUi) Ask(query string) (string, error) {
142 | if query != "" {
143 | query = fmt.Sprintf("%s%s", u.AskPrefix, query)
144 | }
145 |
146 | return u.Ui.Ask(query)
147 | }
148 |
149 | func (u *PrefixedUi) AskSecret(query string) (string, error) {
150 | if query != "" {
151 | query = fmt.Sprintf("%s%s", u.AskSecretPrefix, query)
152 | }
153 |
154 | return u.Ui.AskSecret(query)
155 | }
156 |
157 | func (u *PrefixedUi) Error(message string) {
158 | if message != "" {
159 | message = fmt.Sprintf("%s%s", u.ErrorPrefix, message)
160 | }
161 |
162 | u.Ui.Error(message)
163 | }
164 |
165 | func (u *PrefixedUi) Info(message string) {
166 | if message != "" {
167 | message = fmt.Sprintf("%s%s", u.InfoPrefix, message)
168 | }
169 |
170 | u.Ui.Info(message)
171 | }
172 |
173 | func (u *PrefixedUi) Output(message string) {
174 | if message != "" {
175 | message = fmt.Sprintf("%s%s", u.OutputPrefix, message)
176 | }
177 |
178 | u.Ui.Output(message)
179 | }
180 |
181 | func (u *PrefixedUi) Warn(message string) {
182 | if message != "" {
183 | message = fmt.Sprintf("%s%s", u.WarnPrefix, message)
184 | }
185 |
186 | u.Ui.Warn(message)
187 | }
188 |
--------------------------------------------------------------------------------
/ui_colored.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "github.com/fatih/color"
5 | )
6 |
7 | const (
8 | noColor = -1
9 | )
10 |
11 | // UiColor is a posix shell color code to use.
12 | type UiColor struct {
13 | Code int
14 | Bold bool
15 | }
16 |
17 | // A list of colors that are useful. These are all non-bolded by default.
18 | var (
19 | UiColorNone UiColor = UiColor{noColor, false}
20 | UiColorRed = UiColor{int(color.FgHiRed), false}
21 | UiColorGreen = UiColor{int(color.FgHiGreen), false}
22 | UiColorYellow = UiColor{int(color.FgHiYellow), false}
23 | UiColorBlue = UiColor{int(color.FgHiBlue), false}
24 | UiColorMagenta = UiColor{int(color.FgHiMagenta), false}
25 | UiColorCyan = UiColor{int(color.FgHiCyan), false}
26 | )
27 |
28 | // ColoredUi is a Ui implementation that colors its output according
29 | // to the given color schemes for the given type of output.
30 | type ColoredUi struct {
31 | OutputColor UiColor
32 | InfoColor UiColor
33 | ErrorColor UiColor
34 | WarnColor UiColor
35 | Ui Ui
36 | }
37 |
38 | func (u *ColoredUi) Ask(query string) (string, error) {
39 | return u.Ui.Ask(u.colorize(query, u.OutputColor))
40 | }
41 |
42 | func (u *ColoredUi) AskSecret(query string) (string, error) {
43 | return u.Ui.AskSecret(u.colorize(query, u.OutputColor))
44 | }
45 |
46 | func (u *ColoredUi) Output(message string) {
47 | u.Ui.Output(u.colorize(message, u.OutputColor))
48 | }
49 |
50 | func (u *ColoredUi) Info(message string) {
51 | u.Ui.Info(u.colorize(message, u.InfoColor))
52 | }
53 |
54 | func (u *ColoredUi) Error(message string) {
55 | u.Ui.Error(u.colorize(message, u.ErrorColor))
56 | }
57 |
58 | func (u *ColoredUi) Warn(message string) {
59 | u.Ui.Warn(u.colorize(message, u.WarnColor))
60 | }
61 |
62 | func (u *ColoredUi) colorize(message string, uc UiColor) string {
63 | if uc.Code == noColor {
64 | return message
65 | }
66 |
67 | attr := []color.Attribute{color.Attribute(uc.Code)}
68 | if uc.Bold {
69 | attr = append(attr, color.Bold)
70 | }
71 |
72 | return color.New(attr...).SprintFunc()(message)
73 | }
74 |
--------------------------------------------------------------------------------
/ui_concurrent.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | // ConcurrentUi is a wrapper around a Ui interface (and implements that
8 | // interface) making the underlying Ui concurrency safe.
9 | type ConcurrentUi struct {
10 | Ui Ui
11 | l sync.Mutex
12 | }
13 |
14 | func (u *ConcurrentUi) Ask(query string) (string, error) {
15 | u.l.Lock()
16 | defer u.l.Unlock()
17 |
18 | return u.Ui.Ask(query)
19 | }
20 |
21 | func (u *ConcurrentUi) AskSecret(query string) (string, error) {
22 | u.l.Lock()
23 | defer u.l.Unlock()
24 |
25 | return u.Ui.AskSecret(query)
26 | }
27 |
28 | func (u *ConcurrentUi) Error(message string) {
29 | u.l.Lock()
30 | defer u.l.Unlock()
31 |
32 | u.Ui.Error(message)
33 | }
34 |
35 | func (u *ConcurrentUi) Info(message string) {
36 | u.l.Lock()
37 | defer u.l.Unlock()
38 |
39 | u.Ui.Info(message)
40 | }
41 |
42 | func (u *ConcurrentUi) Output(message string) {
43 | u.l.Lock()
44 | defer u.l.Unlock()
45 |
46 | u.Ui.Output(message)
47 | }
48 |
49 | func (u *ConcurrentUi) Warn(message string) {
50 | u.l.Lock()
51 | defer u.l.Unlock()
52 |
53 | u.Ui.Warn(message)
54 | }
55 |
--------------------------------------------------------------------------------
/ui_concurrent_test.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestConcurrentUi_impl(t *testing.T) {
8 | var _ Ui = new(ConcurrentUi)
9 | }
10 |
--------------------------------------------------------------------------------
/ui_mock.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "io"
8 | "strings"
9 | "sync"
10 | )
11 |
12 | // NewMockUi returns a fully initialized MockUi instance
13 | // which is safe for concurrent use.
14 | func NewMockUi() *MockUi {
15 | m := new(MockUi)
16 | m.once.Do(m.init)
17 | return m
18 | }
19 |
20 | // MockUi is a mock UI that is used for tests and is exported publicly
21 | // for use in external tests if needed as well. Do not instantite this
22 | // directly since the buffers will be initialized on the first write. If
23 | // there is no write then you will get a nil panic. Please use the
24 | // NewMockUi() constructor function instead. You can fix your code with
25 | //
26 | // sed -i -e 's/new(cli.MockUi)/cli.NewMockUi()/g' *_test.go
27 | type MockUi struct {
28 | InputReader io.Reader
29 | ErrorWriter *syncBuffer
30 | OutputWriter *syncBuffer
31 |
32 | once sync.Once
33 | }
34 |
35 | func (u *MockUi) Ask(query string) (string, error) {
36 | u.once.Do(u.init)
37 |
38 | var result string
39 | fmt.Fprint(u.OutputWriter, query)
40 | r := bufio.NewReader(u.InputReader)
41 | line, err := r.ReadString('\n')
42 | if err != nil {
43 | return "", err
44 | }
45 | result = strings.TrimRight(line, "\r\n")
46 |
47 | return result, nil
48 | }
49 |
50 | func (u *MockUi) AskSecret(query string) (string, error) {
51 | return u.Ask(query)
52 | }
53 |
54 | func (u *MockUi) Error(message string) {
55 | u.once.Do(u.init)
56 |
57 | fmt.Fprint(u.ErrorWriter, message)
58 | fmt.Fprint(u.ErrorWriter, "\n")
59 | }
60 |
61 | func (u *MockUi) Info(message string) {
62 | u.Output(message)
63 | }
64 |
65 | func (u *MockUi) Output(message string) {
66 | u.once.Do(u.init)
67 |
68 | fmt.Fprint(u.OutputWriter, message)
69 | fmt.Fprint(u.OutputWriter, "\n")
70 | }
71 |
72 | func (u *MockUi) Warn(message string) {
73 | u.once.Do(u.init)
74 |
75 | fmt.Fprint(u.ErrorWriter, message)
76 | fmt.Fprint(u.ErrorWriter, "\n")
77 | }
78 |
79 | func (u *MockUi) init() {
80 | u.ErrorWriter = new(syncBuffer)
81 | u.OutputWriter = new(syncBuffer)
82 | }
83 |
84 | type syncBuffer struct {
85 | sync.RWMutex
86 | b bytes.Buffer
87 | }
88 |
89 | func (b *syncBuffer) Write(data []byte) (int, error) {
90 | b.Lock()
91 | defer b.Unlock()
92 | return b.b.Write(data)
93 | }
94 |
95 | func (b *syncBuffer) Read(data []byte) (int, error) {
96 | b.RLock()
97 | defer b.RUnlock()
98 | return b.b.Read(data)
99 | }
100 |
101 | func (b *syncBuffer) Reset() {
102 | b.Lock()
103 | b.b.Reset()
104 | b.Unlock()
105 | }
106 |
107 | func (b *syncBuffer) String() string {
108 | return string(b.Bytes())
109 | }
110 |
111 | func (b *syncBuffer) Bytes() []byte {
112 | b.RLock()
113 | data := b.b.Bytes()
114 | b.RUnlock()
115 | return data
116 | }
117 |
--------------------------------------------------------------------------------
/ui_mock_test.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "io"
5 | "testing"
6 | )
7 |
8 | func TestMockUi_implements(t *testing.T) {
9 | var _ Ui = new(MockUi)
10 | }
11 |
12 | func TestMockUi_Ask(t *testing.T) {
13 | tests := []struct {
14 | name string
15 | query, input string
16 | expectedResult string
17 | }{
18 | {"EmptyString", "Middle Name?", "\n", ""},
19 | {"NonEmptyString", "Name?", "foo bar\nbaz\n", "foo bar"},
20 | }
21 |
22 | for _, tc := range tests {
23 | t.Run(tc.name, func(t *testing.T) {
24 | in_r, in_w := io.Pipe()
25 | defer in_r.Close()
26 | defer in_w.Close()
27 |
28 | ui := &MockUi{
29 | InputReader: in_r,
30 | }
31 |
32 | go in_w.Write([]byte(tc.input))
33 |
34 | result, err := ui.Ask(tc.query)
35 | if err != nil {
36 | t.Fatalf("err: %s", err)
37 | }
38 |
39 | if result != tc.expectedResult {
40 | t.Fatalf("bad: %#v", result)
41 | }
42 | })
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/ui_test.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "testing"
7 | )
8 |
9 | func TestBasicUi_implements(t *testing.T) {
10 | var _ Ui = new(BasicUi)
11 | }
12 |
13 | func TestBasicUi_Ask(t *testing.T) {
14 | tests := []struct {
15 | name string
16 | query, input string
17 | expectedQuery, expectedResult string
18 | }{
19 | {"EmptyString", "Middle Name?", "\n", "Middle Name? ", ""},
20 | {"NonEmptyString", "Name?", "foo bar\nbaz\n", "Name? ", "foo bar"},
21 | }
22 |
23 | for _, tc := range tests {
24 | t.Run(tc.name, func(t *testing.T) {
25 | in_r, in_w := io.Pipe()
26 | defer in_r.Close()
27 | defer in_w.Close()
28 |
29 | writer := new(bytes.Buffer)
30 | ui := &BasicUi{
31 | Reader: in_r,
32 | Writer: writer,
33 | }
34 |
35 | go in_w.Write([]byte(tc.input))
36 |
37 | result, err := ui.Ask(tc.query)
38 | if err != nil {
39 | t.Fatalf("err: %s", err)
40 | }
41 |
42 | if writer.String() != tc.expectedQuery {
43 | t.Fatalf("bad: %#v", writer.String())
44 | }
45 |
46 | if result != tc.expectedResult {
47 | t.Fatalf("bad: %#v", result)
48 | }
49 | })
50 | }
51 | }
52 |
53 | func TestBasicUi_AskSecret(t *testing.T) {
54 | in_r, in_w := io.Pipe()
55 | defer in_r.Close()
56 | defer in_w.Close()
57 |
58 | writer := new(bytes.Buffer)
59 | ui := &BasicUi{
60 | Reader: in_r,
61 | Writer: writer,
62 | }
63 |
64 | go in_w.Write([]byte("foo bar\nbaz\n"))
65 |
66 | result, err := ui.AskSecret("Name?")
67 | if err != nil {
68 | t.Fatalf("err: %s", err)
69 | }
70 |
71 | if writer.String() != "Name? " {
72 | t.Fatalf("bad: %#v", writer.String())
73 | }
74 |
75 | if result != "foo bar" {
76 | t.Fatalf("bad: %#v", result)
77 | }
78 | }
79 |
80 | func TestBasicUi_Error(t *testing.T) {
81 | writer := new(bytes.Buffer)
82 | ui := &BasicUi{Writer: writer}
83 | ui.Error("HELLO")
84 |
85 | if writer.String() != "HELLO\n" {
86 | t.Fatalf("bad: %s", writer.String())
87 | }
88 | }
89 |
90 | func TestBasicUi_Error_ErrorWriter(t *testing.T) {
91 | writer := new(bytes.Buffer)
92 | ewriter := new(bytes.Buffer)
93 | ui := &BasicUi{Writer: writer, ErrorWriter: ewriter}
94 | ui.Error("HELLO")
95 |
96 | if ewriter.String() != "HELLO\n" {
97 | t.Fatalf("bad: %s", ewriter.String())
98 | }
99 | }
100 |
101 | func TestBasicUi_Output(t *testing.T) {
102 | writer := new(bytes.Buffer)
103 | ui := &BasicUi{Writer: writer}
104 | ui.Output("HELLO")
105 |
106 | if writer.String() != "HELLO\n" {
107 | t.Fatalf("bad: %s", writer.String())
108 | }
109 | }
110 |
111 | func TestBasicUi_Warn(t *testing.T) {
112 | writer := new(bytes.Buffer)
113 | ui := &BasicUi{Writer: writer}
114 | ui.Warn("HELLO")
115 |
116 | if writer.String() != "HELLO\n" {
117 | t.Fatalf("bad: %s", writer.String())
118 | }
119 | }
120 |
121 | func TestPrefixedUi_implements(t *testing.T) {
122 | var _ Ui = new(PrefixedUi)
123 | }
124 |
125 | func TestPrefixedUiError(t *testing.T) {
126 | ui := new(MockUi)
127 | p := &PrefixedUi{
128 | ErrorPrefix: "foo",
129 | Ui: ui,
130 | }
131 |
132 | p.Error("bar")
133 | if ui.ErrorWriter.String() != "foobar\n" {
134 | t.Fatalf("bad: %s", ui.ErrorWriter.String())
135 | }
136 | }
137 |
138 | func TestPrefixedUiInfo(t *testing.T) {
139 | ui := new(MockUi)
140 | p := &PrefixedUi{
141 | InfoPrefix: "foo",
142 | Ui: ui,
143 | }
144 |
145 | p.Info("bar")
146 | if ui.OutputWriter.String() != "foobar\n" {
147 | t.Fatalf("bad: %s", ui.OutputWriter.String())
148 | }
149 | }
150 |
151 | func TestPrefixedUiOutput(t *testing.T) {
152 | ui := new(MockUi)
153 | p := &PrefixedUi{
154 | OutputPrefix: "foo",
155 | Ui: ui,
156 | }
157 |
158 | p.Output("bar")
159 | if ui.OutputWriter.String() != "foobar\n" {
160 | t.Fatalf("bad: %s", ui.OutputWriter.String())
161 | }
162 | }
163 |
164 | func TestPrefixedUiWarn(t *testing.T) {
165 | ui := new(MockUi)
166 | p := &PrefixedUi{
167 | WarnPrefix: "foo",
168 | Ui: ui,
169 | }
170 |
171 | p.Warn("bar")
172 | if ui.ErrorWriter.String() != "foobar\n" {
173 | t.Fatalf("bad: %s", ui.ErrorWriter.String())
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/ui_writer.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | // UiWriter is an io.Writer implementation that can be used with
4 | // loggers that writes every line of log output data to a Ui at the
5 | // Info level.
6 | type UiWriter struct {
7 | Ui Ui
8 | }
9 |
10 | func (w *UiWriter) Write(p []byte) (n int, err error) {
11 | n = len(p)
12 | if n > 0 && p[n-1] == '\n' {
13 | p = p[:n-1]
14 | }
15 |
16 | w.Ui.Info(string(p))
17 | return n, nil
18 | }
19 |
--------------------------------------------------------------------------------
/ui_writer_test.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "io"
5 | "testing"
6 | )
7 |
8 | func TestUiWriter_impl(t *testing.T) {
9 | var _ io.Writer = new(UiWriter)
10 | }
11 |
12 | func TestUiWriter(t *testing.T) {
13 | ui := new(MockUi)
14 | w := &UiWriter{
15 | Ui: ui,
16 | }
17 |
18 | w.Write([]byte("foo\n"))
19 | w.Write([]byte("bar\n"))
20 |
21 | if ui.OutputWriter.String() != "foo\nbar\n" {
22 | t.Fatalf("bad: %s", ui.OutputWriter.String())
23 | }
24 | }
25 |
26 | func TestUiWriter_empty(t *testing.T) {
27 | ui := new(MockUi)
28 | w := &UiWriter{
29 | Ui: ui,
30 | }
31 |
32 | w.Write([]byte(""))
33 |
34 | if ui.OutputWriter.String() != "\n" {
35 | t.Fatalf("bad: %s", ui.OutputWriter.String())
36 | }
37 | }
38 |
--------------------------------------------------------------------------------