1
2
3[](https://circleci.com/gh/ory/viper/tree/master)
4
5Go configuration with fangs!
6
7> This is a fork. It resolves several issues that are left unresolved in [the upstream](https://github.com/ory/viper).
8> Issues resolved and features added include:
9>
10> - Fixed race conditions when reloading configs.
11> - Added `HasChanged(key string) bool` which returns true (once!) when a value has changed.
12> - Make sure that `viper.AllSettings()` always returns `map[string]interface{}` which was not the case and incompatible
13 with de/encoders like `json`.
14
15Many Go projects are built using Viper including:
16
17* [Hugo](http://gohugo.io)
18* [EMC RexRay](http://rexray.readthedocs.org/en/stable/)
19* [Imgur’s Incus](https://github.com/Imgur/incus)
20* [Nanobox](https://github.com/nanobox-io/nanobox)/[Nanopack](https://github.com/nanopack)
21* [Docker Notary](https://github.com/docker/Notary)
22* [BloomApi](https://www.bloomapi.com/)
23* [doctl](https://github.com/digitalocean/doctl)
24* [Clairctl](https://github.com/jgsqware/clairctl)
25* [Mercure](https://mercure.rocks)
26
27## Install
28
29```console
30go get -u github.com/ory/viper
31```
32
33
34## What is Viper?
35
36Viper is a complete configuration solution for Go applications including 12-Factor apps. It is designed
37to work within an application, and can handle all types of configuration needs
38and formats. It supports:
39
40* setting defaults
41* reading from JSON, TOML, YAML, HCL, envfile and Java properties config files
42* live watching and re-reading of config files (optional)
43* reading from environment variables
44* reading from remote config systems (etcd or Consul), and watching changes
45* reading from command line flags
46* reading from buffer
47* setting explicit values
48
49Viper can be thought of as a registry for all of your applications configuration needs.
50
51
52## Why Viper?
53
54When building a modern application, you don’t want to worry about
55configuration file formats; you want to focus on building awesome software.
56Viper is here to help with that.
57
58Viper does the following for you:
59
601. Find, load, and unmarshal a configuration file in JSON, TOML, YAML, HCL, INI, envfile or Java properties formats.
612. Provide a mechanism to set default values for your different configuration options.
623. Provide a mechanism to set override values for options specified through command line flags.
634. Provide an alias system to easily rename parameters without breaking existing code.
645. Make it easy to tell the difference between when a user has provided a command line or config file which is the same as the default.
65
66Viper uses the following precedence order. Each item takes precedence over the item below it:
67
68 * explicit call to `Set`
69 * flag
70 * env
71 * config
72 * key/value store
73 * default
74
75**Important:** Viper configuration keys are case insensitive.
76There are ongoing discussions about making that optional.
77
78
79## Putting Values into Viper
80
81### Establishing Defaults
82
83A good configuration system will support default values. A default value is not
84required for a key, but it’s useful in the event that a key hasn't been set via
85config file, environment variable, remote configuration or flag.
86
87Examples:
88
89```go
90viper.SetDefault("ContentDir", "content")
91viper.SetDefault("LayoutDir", "layouts")
92viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
93```
94
95### Reading Config Files
96
97Viper requires minimal configuration so it knows where to look for config files.
98Viper supports JSON, TOML, YAML, HCL, INI, envfile and Java Properties files. Viper can search multiple paths, but
99currently a single Viper instance only supports a single configuration file.
100Viper does not default to any configuration search paths leaving defaults decision
101to an application.
102
103Here is an example of how to use Viper to search for and read a configuration file.
104None of the specific paths are required, but at least one path should be provided
105where a configuration file is expected.
106
107```go
108viper.SetConfigName("config") // name of config file (without extension)
109viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
110viper.AddConfigPath("/etc/appname/") // path to look for the config file in
111viper.AddConfigPath("$HOME/.appname") // call multiple times to add many search paths
112viper.AddConfigPath(".") // optionally look for config in the working directory
113err := viper.ReadInConfig() // Find and read the config file
114if err != nil { // Handle errors reading the config file
115 panic(fmt.Errorf("Fatal error config file: %s \n", err))
116}
117```
118
119You can handle the specific case where no config file is found like this:
120
121```go
122if err := viper.ReadInConfig(); err != nil {
123 if _, ok := err.(viper.ConfigFileNotFoundError); ok {
124 // Config file not found; ignore error if desired
125 } else {
126 // Config file was found but another error was produced
127 }
128}
129
130// Config file found and successfully parsed
131```
132
133*NOTE [since 1.6]:* You can also have a file without an extension and specify the format programmaticaly. For those configuration files that lie in the home of the user without any extension like `.bashrc`
134
135### Writing Config Files
136
137Reading from config files is useful, but at times you want to store all modifications made at run time.
138For that, a bunch of commands are available, each with its own purpose:
139
140* WriteConfig - writes the current viper configuration to the predefined path, if exists. Errors if no predefined path. Will overwrite the current config file, if it exists.
141* SafeWriteConfig - writes the current viper configuration to the predefined path. Errors if no predefined path. Will not overwrite the current config file, if it exists.
142* WriteConfigAs - writes the current viper configuration to the given filepath. Will overwrite the given file, if it exists.
143* SafeWriteConfigAs - writes the current viper configuration to the given filepath. Will not overwrite the given file, if it exists.
144
145As a rule of the thumb, everything marked with safe won't overwrite any file, but just create if not existent, whilst the default behavior is to create or truncate.
146
147A small examples section:
148
149```go
150viper.WriteConfig() // writes current config to predefined path set by 'viper.AddConfigPath()' and 'viper.SetConfigName'
151viper.SafeWriteConfig()
152viper.WriteConfigAs("/path/to/my/.config")
153viper.SafeWriteConfigAs("/path/to/my/.config") // will error since it has already been written
154viper.SafeWriteConfigAs("/path/to/my/.other_config")
155```
156
157### Watching and re-reading config files
158
159Viper supports the ability to have your application live read a config file while running.
160
161Gone are the days of needing to restart a server to have a config take effect,
162viper powered applications can read an update to a config file while running and
163not miss a beat.
164
165Simply tell the viper instance to watchConfig.
166Optionally you can provide a function for Viper to run each time a change occurs.
167
168**Make sure you add all of the configPaths prior to calling `WatchConfig()`**
169
170```go
171viper.WatchConfig()
172viper.OnConfigChange(func(e fsnotify.Event) {
173 fmt.Println("Config file changed:", e.Name)
174})
175```
176
177### Reading Config from io.Reader
178
179Viper predefines many configuration sources such as files, environment
180variables, flags, and remote K/V store, but you are not bound to them. You can
181also implement your own required configuration source and feed it to viper.
182
183```go
184viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")
185
186// any approach to require this configuration into your program.
187var yamlExample = []byte(`
188Hacker: true
189name: steve
190hobbies:
191- skateboarding
192- snowboarding
193- go
194clothing:
195 jacket: leather
196 trousers: denim
197age: 35
198eyes : brown
199beard: true
200`)
201
202viper.ReadConfig(bytes.NewBuffer(yamlExample))
203
204viper.Get("name") // this would be "steve"
205```
206
207### Setting Overrides
208
209These could be from a command line flag, or from your own application logic.
210
211```go
212viper.Set("Verbose", true)
213viper.Set("LogFile", LogFile)
214```
215
216### Registering and Using Aliases
217
218Aliases permit a single value to be referenced by multiple keys
219
220```go
221viper.RegisterAlias("loud", "Verbose")
222
223viper.Set("verbose", true) // same result as next line
224viper.Set("loud", true) // same result as prior line
225
226viper.GetBool("loud") // true
227viper.GetBool("verbose") // true
228```
229
230### Working with Environment Variables
231
232Viper has full support for environment variables. This enables 12 factor
233applications out of the box. There are five methods that exist to aid working
234with ENV:
235
236 * `AutomaticEnv()`
237 * `BindEnv(string...) : error`
238 * `SetEnvPrefix(string)`
239 * `SetEnvKeyReplacer(string...) *strings.Replacer`
240 * `AllowEmptyEnv(bool)`
241
242_When working with ENV variables, it’s important to recognize that Viper
243treats ENV variables as case sensitive._
244
245Viper provides a mechanism to try to ensure that ENV variables are unique. By
246using `SetEnvPrefix`, you can tell Viper to use a prefix while reading from
247the environment variables. Both `BindEnv` and `AutomaticEnv` will use this
248prefix.
249
250`BindEnv` takes one or two parameters. The first parameter is the key name, the
251second is the name of the environment variable. The name of the environment
252variable is case sensitive. If the ENV variable name is not provided, then
253Viper will automatically assume that the ENV variable matches the following format: prefix + "_" + the key name in ALL CAPS. When you explicitly provide the ENV variable name (the second parameter),
254it **does not** automatically add the prefix. For example if the second parameter is "id",
255Viper will look for the ENV variable "ID".
256
257One important thing to recognize when working with ENV variables is that the
258value will be read each time it is accessed. Viper does not fix the value when
259the `BindEnv` is called.
260
261`AutomaticEnv` is a powerful helper especially when combined with
262`SetEnvPrefix`. When called, Viper will check for an environment variable any
263time a `viper.Get` request is made. It will apply the following rules. It will
264check for a environment variable with a name matching the key uppercased and
265prefixed with the `EnvPrefix` if set.
266
267`SetEnvKeyReplacer` allows you to use a `strings.Replacer` object to rewrite Env
268keys to an extent. This is useful if you want to use `-` or something in your
269`Get()` calls, but want your environmental variables to use `_` delimiters. An
270example of using it can be found in `viper_test.go`.
271
272Alternatively, you can use `EnvKeyReplacer` with `NewWithOptions` factory function.
273Unlike `SetEnvKeyReplacer`, it accepts a `StringReplacer` interface allowing you to write custom string replacing logic.
274
275By default empty environment variables are considered unset and will fall back to
276the next configuration source. To treat empty environment variables as set, use
277the `AllowEmptyEnv` method.
278
279#### Env example
280
281```go
282SetEnvPrefix("spf") // will be uppercased automatically
283BindEnv("id")
284
285os.Setenv("SPF_ID", "13") // typically done outside of the app
286
287id := Get("id") // 13
288```
289
290### Working with Flags
291
292Viper has the ability to bind to flags. Specifically, Viper supports `Pflags`
293as used in the [Cobra](https://github.com/spf13/cobra) library.
294
295Like `BindEnv`, the value is not set when the binding method is called, but when
296it is accessed. This means you can bind as early as you want, even in an
297`init()` function.
298
299For individual flags, the `BindPFlag()` method provides this functionality.
300
301Example:
302
303```go
304serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
305viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
306```
307
308You can also bind an existing set of pflags (pflag.FlagSet):
309
310Example:
311
312```go
313pflag.Int("flagname", 1234, "help message for flagname")
314
315pflag.Parse()
316viper.BindPFlags(pflag.CommandLine)
317
318i := viper.GetInt("flagname") // retrieve values from viper instead of pflag
319```
320
321The use of [pflag](https://github.com/spf13/pflag/) in Viper does not preclude
322the use of other packages that use the [flag](https://golang.org/pkg/flag/)
323package from the standard library. The pflag package can handle the flags
324defined for the flag package by importing these flags. This is accomplished
325by a calling a convenience function provided by the pflag package called
326AddGoFlagSet().
327
328Example:
329
330```go
331package main
332
333import (
334 "flag"
335 "github.com/spf13/pflag"
336)
337
338func main() {
339
340 // using standard library "flag" package
341 flag.Int("flagname", 1234, "help message for flagname")
342
343 pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
344 pflag.Parse()
345 viper.BindPFlags(pflag.CommandLine)
346
347 i := viper.GetInt("flagname") // retrieve value from viper
348
349 ...
350}
351```
352
353#### Flag interfaces
354
355Viper provides two Go interfaces to bind other flag systems if you don’t use `Pflags`.
356
357`FlagValue` represents a single flag. This is a very simple example on how to implement this interface:
358
359```go
360type myFlag struct {}
361func (f myFlag) HasChanged() bool { return false }
362func (f myFlag) Name() string { return "my-flag-name" }
363func (f myFlag) ValueString() string { return "my-flag-value" }
364func (f myFlag) ValueType() string { return "string" }
365```
366
367Once your flag implements this interface, you can simply tell Viper to bind it:
368
369```go
370viper.BindFlagValue("my-flag-name", myFlag{})
371```
372
373`FlagValueSet` represents a group of flags. This is a very simple example on how to implement this interface:
374
375```go
376type myFlagSet struct {
377 flags []myFlag
378}
379
380func (f myFlagSet) VisitAll(fn func(FlagValue)) {
381 for _, flag := range flags {
382 fn(flag)
383 }
384}
385```
386
387Once your flag set implements this interface, you can simply tell Viper to bind it:
388
389```go
390fSet := myFlagSet{
391 flags: []myFlag{myFlag{}, myFlag{}},
392}
393viper.BindFlagValues("my-flags", fSet)
394```
395
396### Remote Key/Value Store Support
397
398To enable remote support in Viper, do a blank import of the `viper/remote`
399package:
400
401`import _ "github.com/ory/viper/remote"`
402
403Viper will read a config string (as JSON, TOML, YAML, HCL or envfile) retrieved from a path
404in a Key/Value store such as etcd or Consul. These values take precedence over
405default values, but are overridden by configuration values retrieved from disk,
406flags, or environment variables.
407
408Viper uses [crypt](https://github.com/bketelsen/crypt) to retrieve
409configuration from the K/V store, which means that you can store your
410configuration values encrypted and have them automatically decrypted if you have
411the correct gpg keyring. Encryption is optional.
412
413You can use remote configuration in conjunction with local configuration, or
414independently of it.
415
416`crypt` has a command-line helper that you can use to put configurations in your
417K/V store. `crypt` defaults to etcd on http://127.0.0.1:4001.
418
419```bash
420$ go get github.com/bketelsen/crypt/bin/crypt
421$ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json
422```
423
424Confirm that your value was set:
425
426```bash
427$ crypt get -plaintext /config/hugo.json
428```
429
430See the `crypt` documentation for examples of how to set encrypted values, or
431how to use Consul.
432
433### Remote Key/Value Store Example - Unencrypted
434
435#### etcd
436```go
437viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
438viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
439err := viper.ReadRemoteConfig()
440```
441
442#### Consul
443You need to set a key to Consul key/value storage with JSON value containing your desired config.
444For example, create a Consul key/value store key `MY_CONSUL_KEY` with value:
445
446```json
447{
448 "port": 8080,
449 "hostname": "myhostname.com"
450}
451```
452
453```go
454viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
455viper.SetConfigType("json") // Need to explicitly set this to json
456err := viper.ReadRemoteConfig()
457
458fmt.Println(viper.Get("port")) // 8080
459fmt.Println(viper.Get("hostname")) // myhostname.com
460```
461
462#### Firestore
463
464```go
465viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
466viper.SetConfigType("json") // Config's format: "json", "toml", "yaml", "yml"
467err := viper.ReadRemoteConfig()
468```
469
470Of course, you're allowed to use `SecureRemoteProvider` also
471
472### Remote Key/Value Store Example - Encrypted
473
474```go
475viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
476viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
477err := viper.ReadRemoteConfig()
478```
479
480### Watching Changes in etcd - Unencrypted
481
482```go
483// alternatively, you can create a new viper instance.
484var runtime_viper = viper.New()
485
486runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
487runtime_viper.SetConfigType("yaml") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
488
489// read from remote config the first time.
490err := runtime_viper.ReadRemoteConfig()
491
492// unmarshal config
493runtime_viper.Unmarshal(&runtime_conf)
494
495// open a goroutine to watch remote changes forever
496go func(){
497 for {
498 time.Sleep(time.Second * 5) // delay after each request
499
500 // currently, only tested with etcd support
501 err := runtime_viper.WatchRemoteConfig()
502 if err != nil {
503 log.Errorf("unable to read remote config: %v", err)
504 continue
505 }
506
507 // unmarshal new config into our runtime config struct. you can also use channel
508 // to implement a signal to notify the system of the changes
509 runtime_viper.Unmarshal(&runtime_conf)
510 }
511}()
512```
513
514## Getting Values From Viper
515
516In Viper, there are a few ways to get a value depending on the value’s type.
517The following functions and methods exist:
518
519 * `Get(key string) : interface{}`
520 * `GetBool(key string) : bool`
521 * `GetFloat64(key string) : float64`
522 * `GetInt(key string) : int`
523 * `GetIntSlice(key string) : []int`
524 * `GetString(key string) : string`
525 * `GetStringMap(key string) : map[string]interface{}`
526 * `GetStringMapString(key string) : map[string]string`
527 * `GetStringSlice(key string) : []string`
528 * `GetTime(key string) : time.Time`
529 * `GetDuration(key string) : time.Duration`
530 * `IsSet(key string) : bool`
531 * `AllSettings() : map[string]interface{}`
532
533One important thing to recognize is that each Get function will return a zero
534value if it’s not found. To check if a given key exists, the `IsSet()` method
535has been provided.
536
537Example:
538```go
539viper.GetString("logfile") // case-insensitive Setting & Getting
540if viper.GetBool("verbose") {
541 fmt.Println("verbose enabled")
542}
543```
544### Accessing nested keys
545
546The accessor methods also accept formatted paths to deeply nested keys. For
547example, if the following JSON file is loaded:
548
549```json
550{
551 "host": {
552 "address": "localhost",
553 "port": 5799
554 },
555 "datastore": {
556 "metric": {
557 "host": "127.0.0.1",
558 "port": 3099
559 },
560 "warehouse": {
561 "host": "198.0.0.1",
562 "port": 2112
563 }
564 }
565}
566
567```
568
569Viper can access a nested field by passing a `.` delimited path of keys:
570
571```go
572GetString("datastore.metric.host") // (returns "127.0.0.1")
573```
574
575This obeys the precedence rules established above; the search for the path
576will cascade through the remaining configuration registries until found.
577
578For example, given this configuration file, both `datastore.metric.host` and
579`datastore.metric.port` are already defined (and may be overridden). If in addition
580`datastore.metric.protocol` was defined in the defaults, Viper would also find it.
581
582However, if `datastore.metric` was overridden (by a flag, an environment variable,
583the `Set()` method, …) with an immediate value, then all sub-keys of
584`datastore.metric` become undefined, they are “shadowed” by the higher-priority
585configuration level.
586
587Lastly, if there exists a key that matches the delimited key path, its value
588will be returned instead. E.g.
589
590```json
591{
592 "datastore.metric.host": "0.0.0.0",
593 "host": {
594 "address": "localhost",
595 "port": 5799
596 },
597 "datastore": {
598 "metric": {
599 "host": "127.0.0.1",
600 "port": 3099
601 },
602 "warehouse": {
603 "host": "198.0.0.1",
604 "port": 2112
605 }
606 }
607}
608
609GetString("datastore.metric.host") // returns "0.0.0.0"
610```
611
612### Extract sub-tree
613
614Extract sub-tree from Viper.
615
616For example, `viper` represents:
617
618```json
619app:
620 cache1:
621 max-items: 100
622 item-size: 64
623 cache2:
624 max-items: 200
625 item-size: 80
626```
627
628After executing:
629
630```go
631subv := viper.Sub("app.cache1")
632```
633
634`subv` represents:
635
636```json
637max-items: 100
638item-size: 64
639```
640
641Suppose we have:
642
643```go
644func NewCache(cfg *Viper) *Cache {...}
645```
646
647which creates a cache based on config information formatted as `subv`.
648Now it’s easy to create these 2 caches separately as:
649
650```go
651cfg1 := viper.Sub("app.cache1")
652cache1 := NewCache(cfg1)
653
654cfg2 := viper.Sub("app.cache2")
655cache2 := NewCache(cfg2)
656```
657
658### Unmarshaling
659
660You also have the option of Unmarshaling all or a specific value to a struct, map,
661etc.
662
663There are two methods to do this:
664
665 * `Unmarshal(rawVal interface{}) : error`
666 * `UnmarshalKey(key string, rawVal interface{}) : error`
667
668Example:
669
670```go
671type config struct {
672 Port int
673 Name string
674 PathMap string `mapstructure:"path_map"`
675}
676
677var C config
678
679err := viper.Unmarshal(&C)
680if err != nil {
681 t.Fatalf("unable to decode into struct, %v", err)
682}
683```
684
685If you want to unmarshal configuration where the keys themselves contain dot (the default key delimiter),
686you have to change the delimiter:
687
688```go
689v := viper.NewWithOptions(viper.KeyDelimiter("::"))
690
691v.SetDefault("chart::values", map[string]interface{}{
692 "ingress": map[string]interface{}{
693 "annotations": map[string]interface{}{
694 "traefik.frontend.rule.type": "PathPrefix",
695 "traefik.ingress.kubernetes.io/ssl-redirect": "true",
696 },
697 },
698})
699
700type config struct {
701 Chart struct{
702 Values map[string]interface{}
703 }
704}
705
706var C config
707
708v.Unmarshal(&C)
709```
710
711Viper also supports unmarshaling into embedded structs:
712
713```go
714/*
715Example config:
716
717module:
718 enabled: true
719 token: 89h3f98hbwf987h3f98wenf89ehf
720*/
721type config struct {
722 Module struct {
723 Enabled bool
724
725 moduleConfig `mapstructure:",squash"`
726 }
727}
728
729// moduleConfig could be in a module specific package
730type moduleConfig struct {
731 Token string
732}
733
734var C config
735
736err := viper.Unmarshal(&C)
737if err != nil {
738 t.Fatalf("unable to decode into struct, %v", err)
739}
740```
741
742Viper uses [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) under the hood for unmarshaling values which uses `mapstructure` tags by default.
743
744### Marshalling to string
745
746You may need to marshal all the settings held in viper into a string rather than write them to a file.
747You can use your favorite format's marshaller with the config returned by `AllSettings()`.
748
749```go
750import (
751 yaml "gopkg.in/yaml.v2"
752 // ...
753)
754
755func yamlStringSettings() string {
756 c := viper.AllSettings()
757 bs, err := yaml.Marshal(c)
758 if err != nil {
759 log.Fatalf("unable to marshal config to YAML: %v", err)
760 }
761 return string(bs)
762}
763```
764
765## Viper or Vipers?
766
767Viper comes ready to use out of the box. There is no configuration or
768initialization needed to begin using Viper. Since most applications will want
769to use a single central repository for their configuration, the viper package
770provides this. It is similar to a singleton.
771
772In all of the examples above, they demonstrate using viper in its singleton
773style approach.
774
775### Working with multiple vipers
776
777You can also create many different vipers for use in your application. Each will
778have its own unique set of configurations and values. Each can read from a
779different config file, key value store, etc. All of the functions that viper
780package supports are mirrored as methods on a viper.
781
782Example:
783
784```go
785x := viper.New()
786y := viper.New()
787
788x.SetDefault("ContentDir", "content")
789y.SetDefault("ContentDir", "foobar")
790
791//...
792```
793
794When working with multiple vipers, it is up to the user to keep track of the
795different vipers.
796
797## Q & A
798
799Q: Why is it called “Viper”?
800
801A: Viper is designed to be a [companion](http://en.wikipedia.org/wiki/Viper_(G.I._Joe))
802to [Cobra](https://github.com/spf13/cobra). While both can operate completely
803independently, together they make a powerful pair to handle much of your
804application foundation needs.
805
806Q: Why is it called “Cobra”?
807
808A: Is there a better name for a [commander](http://en.wikipedia.org/wiki/Cobra_Commander)?
View as plain text