Merge lp:~jtv/gwacl/storage-tool-command-line into lp:gwacl

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 223
Merged at revision: 219
Proposed branch: lp:~jtv/gwacl/storage-tool-command-line
Merge into: lp:gwacl
Diff against target: 386 lines (+203/-130)
1 file modified
example/storage/run.go (+203/-130)
To merge this branch: bzr merge lp:~jtv/gwacl/storage-tool-command-line
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+180765@code.launchpad.net

Commit message

Clean up command-line parsing in storage tool.

Description of the change

Actually I just wanted to add a "storage" option in order to fix bug 1213059: StorageContext.AzureEndpoint is now required. But along the way I found that this program wasn't very welcoming to change. Any new operation would have to be defined in too many places (help string for valid operations; switch for arguments checking; help output; switch for actual implementation) and the descriptions for each were hard to find. This branch defines all that in a single array of structs describing the available operations.

(Go being Go, of course there's still some distraction in the code from complex, sophisticated procedures such as "now give me just the names").

And while working on that, I got irritated by the hopeless error output: you'd get one line of error, and then it would be completely drowned out by the usage output. And then, even if you were just asking for the usage information, you'd get a message about exit status. (Also, Go's flags package doesn't add the conventional "Usage:" to the usage output). So I added a -h option (Go's flags package doesn't seem to welcome GNU-style long and short forms for the same option, so no --help).

Now, invoking the program with -h prints usage and exits successfully. Getting the options wrong prints just a one-line pointer to the -h option, the actual error, and the exit status.

By the way, the Usage function was written as a variable instead of a regular function definition — probably because we were all new to Go at the time, but possibly also because the idea was to invoke the function through the flags package. In practice I find that the flags package adds no value at all (it doesn't add that "Usage:"!) so I simplified that a bit.

In some languages, you might wonder if "operation" shouldn't be a base class with different implementation types, or in Go, whether it should be an interface with different implementing structs. But that just adds massive overhead, for no particularly good reason. Yes, each has its own implementations of one or two "methods" but in this case I think it's perfectly valid to see that as an attribute rather than a type difference.

Branch with the actual fix for bug 1213059 follows.

Jeroen

To post a comment you must log in.
220. By Jeroen T. Vermeulen

Put option variables in a better place, group them for clarity, and document what they are.

221. By Jeroen T. Vermeulen

Identify an internal error as such.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Thanks for doing this, it was a bit messy in there. I've not given it massive scrutiny, since it's just an example program. If it runs and works that's fine by me.

You have two init() functions. I don't know if that's allowed or not but either way it's darn confusing, please collapse into a single func!

review: Approve
222. By Jeroen T. Vermeulen

Review change: combine init() functions.

223. By Jeroen T. Vermeulen

A change leaked between branches. I miss distributed revision control.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Thanks. I combined the init() functions, although in the long run there's no avoiding the fact that they're a weird special case in the language and have their own semantics.

Revision history for this message
Gavin Panella (allenap) wrote :

Next time we have flag issues we could consider using go-flags <https://github.com/jessevdk/go-flags> or gnuflag <https://launchpad.net/gnuflag>. The latter is used in Juju, and was created by Roger Peppe. The Go standard library's flag package feels about as modern as pocket protectors and should be deprecated with fire.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'example/storage/run.go'
--- example/storage/run.go 2013-07-12 14:42:08 +0000
+++ example/storage/run.go 2013-08-19 05:20:32 +0000
@@ -15,33 +15,170 @@
15 "io/ioutil"15 "io/ioutil"
16 "launchpad.net/gwacl"16 "launchpad.net/gwacl"
17 "os"17 "os"
18 "strings"
18)19)
1920
20var account string
21var key string
22var filename string
23var container string
24var prefix string
25var blobname string
26var blobtype string
27var size int
28var pagerange string
29var acl string
30
31func checkContainerAndFilename() error {
32 if container == "" || filename == "" {
33 return fmt.Errorf("Must supply container and filename")
34 }
35 return nil
36}
37
38var VALID_CMDS string = "<listcontainers|containeracl|list|getblob|addblock|deleteblob|putblob|putpage>"
39
40func badOperationError() error {21func badOperationError() error {
41 return fmt.Errorf("Must specify one of %s", VALID_CMDS)22 return fmt.Errorf("Must specify one of %v", operationNames)
23}
24
25// operation is something you can instruct this program to do, by specifying
26// its name on the command line.
27type operation struct {
28 // name is the operation name as used on the command line.
29 name string
30 // description holds a description of what the operation does.
31 description string
32 // example illustrates how the operation is used.
33 example string
34 // requiredArgs lists the command-line options that are required for this
35 // operation.
36 requiredArgs []string
37 // validate is an optional callback to perform more detailed checking on
38 // the operation's arguments.
39 validate func() error
40 // implementation is a function that performs the operation. If it fails,
41 // it just panics.
42 implementation func(gwacl.StorageContext)
43}
44
45// operations defines what operations are available to be invoked from the
46// command line.
47var operations = []operation{
48 {
49 name: "listcontainers",
50 description: "Show existing storage containers",
51 example: "listcontainers",
52 implementation: listcontainers,
53 },
54 {
55 name: "list",
56 description: "List files in a container",
57 example: "-container=<container> list",
58 requiredArgs: []string{"list"},
59 implementation: list,
60 },
61 {
62 name: "containeracl",
63 description: "Set access on a container",
64 example: "-container=<container> -acl <container|blob|private> containeracl",
65 requiredArgs: []string{"container", "key", "acl"},
66 validate: func() error {
67 if acl != "container" && acl != "blob" && acl != "private" {
68 return fmt.Errorf("Usage: containeracl -container=<container> <container|blob|private>")
69 }
70 return nil
71 },
72 implementation: containeracl,
73 },
74 {
75 name: "getblob",
76 description: "Get a file from a container (it's returned on stdout)",
77 example: "-container=<container> -filename=<filename> getblob",
78 requiredArgs: []string{"container", "filename"},
79 implementation: getblob,
80 },
81 {
82 name: "addblock",
83 description: "Upload a file to a block blob",
84 example: "-container=<container> -filename=<filename> addblock",
85 requiredArgs: []string{"key", "container", "filename"},
86 implementation: addblock,
87 },
88 {
89 name: "deleteblob",
90 description: "Delete a blob",
91 example: "-container=<container> -filename=<filename> deleteblob",
92 requiredArgs: []string{"key", "container", "filename"},
93 implementation: deleteblob,
94 },
95 {
96 name: "putblob",
97 description: "Create an empty page blob",
98 example: "-container=<container> -blobname=<blobname> -size=<bytes> " +
99 "-blobtype=\"page\" putblob",
100 requiredArgs: []string{"key", "blobname", "blobtype", "container", "size"},
101 implementation: putblob,
102 },
103 {
104 name: "putpage",
105 description: "Upload a file to a page blob's page. The range parameters must " +
106 "be (modulo 512)-(modulo 512 -1), eg: -pagerange=0-511",
107 example: "-container=<container> -blobname=<blobname> -pagerange=<N-N> " +
108 "-filename=<local file> putpage",
109 requiredArgs: []string{"key", "blobname", "container", "pagerange", "filename"},
110 implementation: putpage,
111 },
112}
113
114// operationsByName maps each opeation name to an operation struct that
115// describes it.
116var operationsByName map[string]operation
117
118// operationNames lists just the names of the oeprations, in the order in which
119// they are listed in "operations."
120var operationNames []string
121
122func init() {
123 operationsByName = make(map[string]operation, len(operations))
124 for _, op := range operations {
125 operationsByName[op.name] = op
126 }
127
128 operationNames = make([]string, len(operations))
129 for index, op := range operations {
130 operationNames[index] = op.name
131 }
132}
133
134// Variables set by command-line options.
135var (
136 help bool
137 account string
138 location string
139 key string
140 filename string
141 container string
142 prefix string
143 blobname string
144 blobtype string
145 size int
146 pagerange string
147 acl string
148)
149
150// argumentGiven returns whether the named argument was specified on the
151// command line.
152func argumentGiven(name string) bool {
153 // This is stupid. There must be a way to ask the flag module directly!
154 switch name {
155 case "account":
156 return account != ""
157 case "key":
158 return key != ""
159 case "container":
160 return container != ""
161 case "filename":
162 return filename != ""
163 case "prefix":
164 return prefix != ""
165 case "blobname":
166 return blobname != ""
167 case "blobtype":
168 return blobtype != ""
169 case "size":
170 return size != 0
171 case "pagerange":
172 return pagerange != ""
173 case "acl":
174 return acl != ""
175 }
176 panic(fmt.Errorf("internal error: unknown command-line option: %s", name))
42}177}
43178
44func getParams() (string, error) {179func getParams() (string, error) {
180 flag.BoolVar(&help, "h", false, "Show usage and exit")
181
45 flag.StringVar(&account, "account", "", "Storage account name")182 flag.StringVar(&account, "account", "", "Storage account name")
46 flag.StringVar(&key, "key", "", "A valid storage account key (base64 encoded), defaults to the empty string (i.e. anonymous access)")183 flag.StringVar(&key, "key", "", "A valid storage account key (base64 encoded), defaults to the empty string (i.e. anonymous access)")
47 flag.StringVar(&container, "container", "", "Name of the container to use")184 flag.StringVar(&container, "container", "", "Name of the container to use")
@@ -54,6 +191,15 @@
54 flag.StringVar(&acl, "acl", "", "When using 'containeracl', specify an ACL type")191 flag.StringVar(&acl, "acl", "", "When using 'containeracl', specify an ACL type")
55 flag.Parse()192 flag.Parse()
56193
194 if help {
195 return "", nil
196 }
197
198 opName := flag.Arg(0)
199 if opName == "" {
200 return "", fmt.Errorf("No operation specified")
201 }
202
57 if account == "" {203 if account == "" {
58 return "", fmt.Errorf("Must supply account parameter")204 return "", fmt.Errorf("Must supply account parameter")
59 }205 }
@@ -62,71 +208,33 @@
62 return "", badOperationError()208 return "", badOperationError()
63 }209 }
64210
65 switch flag.Arg(0) {211 op, isDefined := operationsByName[opName]
66 case "listcontainers":212 if !isDefined {
67 return "listcontainers", nil213 return "", badOperationError()
68 case "containeracl":214 }
69 if container == "" {215
70 return "", fmt.Errorf("Must supply container with containeracl")216 for _, arg := range op.requiredArgs {
71 }217 if !argumentGiven(arg) {
72 if key == "" {218 return "", fmt.Errorf("%q requires these options: %v", op.name, op.requiredArgs)
73 return "", fmt.Errorf("Must supply key with containeracl")219 }
74 }220 }
75 if acl != "container" && acl != "blob" && acl != "private" {221
76 return "", fmt.Errorf("Usage: containeracl -container=<container> <container|blob|private>")222 if op.validate != nil {
77 }223 err := op.validate()
78 return "containeracl", nil224 if err != nil {
79 case "list":225 return "", err
80 if container == "" {226 }
81 return "", fmt.Errorf("Must supply container with 'list'")227 }
82 }228
83 return "list", nil229 return op.name, nil
84 case "getblob":
85 if container == "" || filename == "" {
86 return "", fmt.Errorf("Must supply container and filename with 'list'")
87 }
88 return "getblob", nil
89 case "addblock":
90 if key == "" {
91 return "", fmt.Errorf("Must supply key with addblock")
92 }
93 err := checkContainerAndFilename()
94 if err != nil {
95 return "", err
96 }
97 return "addblock", nil
98 case "deleteblob":
99 if key == "" {
100 return "", fmt.Errorf("Must supply key with deleteblob")
101 }
102 err := checkContainerAndFilename()
103 if err != nil {
104 return "", err
105 }
106 return "deleteblob", nil
107 case "putblob":
108 if key == "" {
109 return "", fmt.Errorf("Must supply key with putblob")
110 }
111 if blobname == "" || blobtype == "" || container == "" || size == 0 {
112 return "", fmt.Errorf("Must supply container, blobname, blobtype and size with 'putblob'")
113 }
114 return "putblob", nil
115 case "putpage":
116 if key == "" {
117 return "", fmt.Errorf("Must supply key with putpage")
118 }
119 if blobname == "" || container == "" || pagerange == "" || filename == "" {
120 return "", fmt.Errorf("Must supply container, blobname, pagerange and filename")
121 }
122 return "putpage", nil
123 }
124
125 return "", badOperationError()
126}230}
127231
128var Usage = func() {232func Usage() {
129 fmt.Fprintf(os.Stderr, "%s [args] %s\n", os.Args[0], VALID_CMDS)233 fmt.Fprintf(
234 os.Stderr,
235 "Usage:\n %s [args] <%s>\n",
236 os.Args[0],
237 strings.Join(operationNames, "|"))
130 flag.PrintDefaults()238 flag.PrintDefaults()
131239
132 fmt.Fprintf(os.Stderr, `240 fmt.Fprintf(os.Stderr, `
@@ -138,34 +246,11 @@
138 operations that change things, (get these from the Azure web UI) otherwise246 operations that change things, (get these from the Azure web UI) otherwise
139 anonymous access is made. Additionally there are the following command247 anonymous access is made. Additionally there are the following command
140 invocation parameters:248 invocation parameters:
141
142 Show existing storage containers:
143 listcontainers
144
145 List files in a container:
146 -container=<container> list
147
148 Set access on a container:
149 -container=<container> -acl <container|blob|private> containeracl
150
151 Get a file from a container (it's returned on stdout):
152 -container=<container> -filename=<filename> getblob
153
154 Upload a file to a block blob:
155 -container=<container> -filename=<filename> addblock
156
157 Delete a blob:
158 -container=<container> -filename=<filename> deleteblob
159
160 Create an empty page blob:
161 -container=<container> -blobname=<blobname> -size=<bytes>
162 -blobtype="page" putblob
163
164 Upload a file to a page blob's page. The range parameters must be
165 (modulo 512)-(modulo 512 -1), eg: -pagerange=0-511
166 -container=<container> -blobname=<blobname> -pagerange=<N-N>
167 -filename=<local file> putpage
168 `)249 `)
250
251 for _, op := range operations {
252 fmt.Fprintf(os.Stderr, "\n %s:\n %s\n", op.description, op.example)
253 }
169}254}
170255
171func dumpError(err error) {256func dumpError(err error) {
@@ -285,31 +370,19 @@
285 op, err := getParams()370 op, err := getParams()
286 if err != nil {371 if err != nil {
287 fmt.Fprintf(os.Stderr, "%s\n", err.Error())372 fmt.Fprintf(os.Stderr, "%s\n", err.Error())
288 flag.Usage()373 fmt.Fprintf(os.Stderr, "Use -h for help with using this program.\n")
289 os.Exit(1)374 os.Exit(1)
290 }375 }
376 if help {
377 Usage()
378 os.Exit(0)
379 }
291380
292 storage := gwacl.StorageContext{381 storage := gwacl.StorageContext{
293 Account: account,382 Account: account,
294 Key: key,383 Key: key,
295 }384 }
296385
297 switch op {386 perform := operationsByName[op].implementation
298 case "listcontainers":387 perform(storage)
299 listcontainers(storage)
300 case "containeracl":
301 containeracl(storage)
302 case "list":
303 list(storage)
304 case "addblock":
305 addblock(storage)
306 case "deleteblob":
307 deleteblob(storage)
308 case "getblob":
309 getblob(storage)
310 case "putblob":
311 putblob(storage)
312 case "putpage":
313 putpage(storage)
314 }
315}388}

Subscribers

People subscribed via source and target branches