mortar 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/README.md +36 -0
  2. data/bin/mortar +13 -0
  3. data/lib/mortar.rb +23 -0
  4. data/lib/mortar/auth.rb +312 -0
  5. data/lib/mortar/cli.rb +54 -0
  6. data/lib/mortar/command.rb +267 -0
  7. data/lib/mortar/command/auth.rb +96 -0
  8. data/lib/mortar/command/base.rb +319 -0
  9. data/lib/mortar/command/clusters.rb +41 -0
  10. data/lib/mortar/command/describe.rb +97 -0
  11. data/lib/mortar/command/generate.rb +121 -0
  12. data/lib/mortar/command/help.rb +166 -0
  13. data/lib/mortar/command/illustrate.rb +97 -0
  14. data/lib/mortar/command/jobs.rb +174 -0
  15. data/lib/mortar/command/pigscripts.rb +45 -0
  16. data/lib/mortar/command/projects.rb +128 -0
  17. data/lib/mortar/command/validate.rb +94 -0
  18. data/lib/mortar/command/version.rb +42 -0
  19. data/lib/mortar/errors.rb +24 -0
  20. data/lib/mortar/generators/generator_base.rb +107 -0
  21. data/lib/mortar/generators/macro_generator.rb +37 -0
  22. data/lib/mortar/generators/pigscript_generator.rb +40 -0
  23. data/lib/mortar/generators/project_generator.rb +67 -0
  24. data/lib/mortar/generators/udf_generator.rb +28 -0
  25. data/lib/mortar/git.rb +233 -0
  26. data/lib/mortar/helpers.rb +488 -0
  27. data/lib/mortar/project.rb +156 -0
  28. data/lib/mortar/snapshot.rb +39 -0
  29. data/lib/mortar/templates/macro/macro.pig +14 -0
  30. data/lib/mortar/templates/pigscript/pigscript.pig +38 -0
  31. data/lib/mortar/templates/pigscript/python_udf.py +13 -0
  32. data/lib/mortar/templates/project/Gemfile +3 -0
  33. data/lib/mortar/templates/project/README.md +8 -0
  34. data/lib/mortar/templates/project/gitignore +4 -0
  35. data/lib/mortar/templates/project/macros/gitkeep +0 -0
  36. data/lib/mortar/templates/project/pigscripts/pigscript.pig +35 -0
  37. data/lib/mortar/templates/project/udfs/python/python_udf.py +13 -0
  38. data/lib/mortar/templates/udf/python_udf.py +13 -0
  39. data/lib/mortar/version.rb +20 -0
  40. data/lib/vendor/mortar/okjson.rb +598 -0
  41. data/lib/vendor/mortar/uuid.rb +312 -0
  42. data/spec/mortar/auth_spec.rb +156 -0
  43. data/spec/mortar/command/auth_spec.rb +46 -0
  44. data/spec/mortar/command/base_spec.rb +82 -0
  45. data/spec/mortar/command/clusters_spec.rb +61 -0
  46. data/spec/mortar/command/describe_spec.rb +135 -0
  47. data/spec/mortar/command/generate_spec.rb +139 -0
  48. data/spec/mortar/command/illustrate_spec.rb +140 -0
  49. data/spec/mortar/command/jobs_spec.rb +364 -0
  50. data/spec/mortar/command/pigscripts_spec.rb +70 -0
  51. data/spec/mortar/command/projects_spec.rb +165 -0
  52. data/spec/mortar/command/validate_spec.rb +119 -0
  53. data/spec/mortar/command_spec.rb +122 -0
  54. data/spec/mortar/git_spec.rb +278 -0
  55. data/spec/mortar/helpers_spec.rb +82 -0
  56. data/spec/mortar/project_spec.rb +76 -0
  57. data/spec/mortar/snapshot_spec.rb +46 -0
  58. data/spec/spec.opts +1 -0
  59. data/spec/spec_helper.rb +278 -0
  60. data/spec/support/display_message_matcher.rb +68 -0
  61. metadata +259 -0
@@ -0,0 +1,46 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Portions of this code from heroku (https://github.com/heroku/heroku/) Copyright Heroku 2008 - 2012,
17
+ # used under an MIT license (https://github.com/heroku/heroku/blob/master/LICENSE).
18
+ #
19
+
20
+ require "spec_helper"
21
+ require "mortar/command/auth"
22
+
23
+ describe Mortar::Command::Auth do
24
+
25
+ describe "auth:key" do
26
+ it "displays the user's api key" do
27
+ mock(Mortar::Auth).password {"foo_api_key"}
28
+ stderr, stdout = execute("auth:key")
29
+ stderr.should == ""
30
+ stdout.should == <<-STDOUT
31
+ foo_api_key
32
+ STDOUT
33
+ end
34
+ end
35
+
36
+ describe "auth:whoami" do
37
+ it "displays the user's email address" do
38
+ mock(Mortar::Auth).user {"sam@mortardata.com"}
39
+ stderr, stdout = execute("auth:whoami")
40
+ stderr.should == ""
41
+ stdout.should == <<-STDOUT
42
+ sam@mortardata.com
43
+ STDOUT
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,82 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Portions of this code from heroku (https://github.com/heroku/heroku/) Copyright Heroku 2008 - 2012,
17
+ # used under an MIT license (https://github.com/heroku/heroku/blob/master/LICENSE).
18
+ #
19
+
20
+ require "spec_helper"
21
+ require "mortar/command/base"
22
+
23
+ module Mortar::Command
24
+ describe Base do
25
+ before do
26
+ @base = Base.new
27
+ stub(@base).display
28
+ @client = Object.new
29
+ stub(@client).host {'mortar.com'}
30
+ end
31
+
32
+ context "error message context" do
33
+ it "get context for missing parameter error message" do
34
+ message = "Undefined parameter : INPUT"
35
+ @base.get_error_message_context(message).should == "Use -p, --parameter NAME=VALUE to set parameter NAME to value VALUE."
36
+ end
37
+
38
+ it "get context for unhandled error message" do
39
+ message = "special kind of error"
40
+ @base.get_error_message_context(message).should == ""
41
+ end
42
+ end
43
+
44
+ context "detecting the project" do
45
+ it "read remotes from git config" do
46
+ stub(Dir).chdir
47
+ stub(@base.git).has_dot_git? {true}
48
+ mock(@base.git).git("remote -v").returns(<<-REMOTES)
49
+ staging\tgit@github.com:mortarcode/4dbbd83cae8d5bf8a4000000_myproject-staging.git (fetch)
50
+ staging\tgit@github.com:mortarcode/4dbbd83cae8d5bf8a4000000_myproject-staging.git (push)
51
+ production\tgit@github.com:mortarcode/4dbbd83cae8d5bf8a4000000_myproject.git (fetch)
52
+ production\tgit@github.com:mortarcode/4dbbd83cae8d5bf8a4000000_myproject.git (push)
53
+ other\tgit@github.com:other.git (fetch)
54
+ other\tgit@github.com:other.git (push)
55
+ REMOTES
56
+
57
+ @mortar = Object.new
58
+ stub(@mortar).host {'mortar.com'}
59
+ stub(@base).mortar { @mortar }
60
+
61
+ # need a better way to test internal functionality
62
+ @base.git.send(:remotes, 'mortarcode').should == { 'staging' => 'myproject-staging', 'production' => 'myproject' }
63
+ end
64
+
65
+ it "gets the project from remotes when there's only one project" do
66
+ stub(@base.git).has_dot_git? {true}
67
+ stub(@base.git).remotes {{ 'mortar' => 'myproject' }}
68
+ mock(@base.git).git("config mortar.remote", false).returns("")
69
+ @base.project.name.should == 'myproject'
70
+ end
71
+
72
+ it "accepts a --remote argument to choose the project from the remote name" do
73
+ stub(@base.git).has_dot_git?.returns(true)
74
+ stub(@base.git).remotes.returns({ 'staging' => 'myproject-staging', 'production' => 'myproject' })
75
+ stub(@base).options.returns(:remote => "staging")
76
+ @base.project.name.should == 'myproject-staging'
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,61 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'spec_helper'
18
+ require 'mortar/command/clusters'
19
+
20
+ module Mortar::Command
21
+ describe Clusters do
22
+
23
+ before(:each) do
24
+ stub_core
25
+ end
26
+
27
+ context("index") do
28
+
29
+ it "lists running and recently started clusters" do
30
+ # stub api request
31
+ clusters = [{"cluster_id" => "50fbe5a23004292547fc2224",
32
+ "size" => 2,
33
+ "status_description" => "Running",
34
+ "start_timestamp" => "2012-08-27T21:27:15.669000+00:00",
35
+ "duration" => "2 mins"},
36
+ {"cluster_id" => "50fbe5a23004292547fc2225",
37
+ "size" => 10,
38
+ "status_description" => "Shut Down",
39
+ "start_timestamp" => "2011-08-27T21:27:15.669000+00:00",
40
+ "duration" => "20 mins"}]
41
+ mock(Mortar::Auth.api).get_clusters().returns(Excon::Response.new(:body => {"clusters" => clusters}))
42
+ stderr, stdout = execute("clusters", nil, nil)
43
+ stdout.should == <<-STDOUT
44
+ cluster_id Size (# of Nodes) Status Type Start Timestamp Elapsed Time
45
+ ------------------------ ----------------- --------- ---- -------------------------------- ------------
46
+ 50fbe5a23004292547fc2224 2 Running 2012-08-27T21:27:15.669000+00:00 2 mins
47
+ 50fbe5a23004292547fc2225 10 Shut Down 2011-08-27T21:27:15.669000+00:00 20 mins
48
+ STDOUT
49
+ end
50
+
51
+ it "handles no clusters running" do
52
+ mock(Mortar::Auth.api).get_clusters().returns(Excon::Response.new(:body => {"clusters" => []}))
53
+ stderr, stdout = execute("clusters", nil, nil)
54
+ stdout.should == <<-STDOUT
55
+ cluster_id Size (# of Nodes) Status Type Start Timestamp Elapsed Time
56
+ ---------- ----------------- ------ ---- --------------- ------------
57
+ STDOUT
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,135 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'spec_helper'
18
+ require 'fakefs/spec_helpers'
19
+ require 'mortar/command/describe'
20
+ require 'mortar/api/describe'
21
+ require 'launchy'
22
+
23
+ module Mortar::Command
24
+ describe Describe do
25
+
26
+ before(:each) do
27
+ stub_core
28
+ @git = Mortar::Git::Git.new
29
+ end
30
+
31
+ context("index") do
32
+
33
+ it "errors when an alias is not provided" do
34
+ with_git_initialized_project do |p|
35
+ write_file(File.join(p.pigscripts_path, "my_script.pig"))
36
+ stderr, stdout = execute("describe my_script", p)
37
+ stderr.should == <<-STDERR
38
+ ! Usage: mortar describe PIGSCRIPT ALIAS
39
+ ! Must specify PIGSCRIPT and ALIAS.
40
+ STDERR
41
+ end
42
+ end
43
+
44
+ it "errors when no remote exists in the project" do
45
+ with_git_initialized_project do |p|
46
+ @git.git('remote rm mortar')
47
+ p.remote = nil
48
+ write_file(File.join(p.pigscripts_path, "my_script.pig"))
49
+ stderr, stdout = execute("describe my_script my_alias", p, @git)
50
+ stderr.should == <<-STDERR
51
+ ! Unable to find git remote for project myproject
52
+ STDERR
53
+ end
54
+ end
55
+
56
+ it "errors when requested pigscript cannot be found" do
57
+ with_git_initialized_project do |p|
58
+ stderr, stdout = execute("describe does_not_exist my_alias", p, @git)
59
+ stderr.should == <<-STDERR
60
+ ! Unable to find pigscript does_not_exist
61
+ ! No pigscripts found
62
+ STDERR
63
+ end
64
+ end
65
+
66
+ it "requests and reports on a successful describe" do
67
+ with_git_initialized_project do |p|
68
+ # stub api requests
69
+ describe_id = "c571a8c7f76a4fd4a67c103d753e2dd5"
70
+ describe_url = "https://api.mortardata.com/describe/#{describe_id}"
71
+ parameters = ["name"=>"key", "value"=>"value" ]
72
+
73
+ mock(Mortar::Auth.api).post_describe("myproject", "my_script", "my_alias", is_a(String), :parameters => parameters) {Excon::Response.new(:body => {"describe_id" => describe_id})}
74
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_QUEUED, "status_description" => "Pending"})).ordered
75
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_GATEWAY_STARTING, "status_description" => "Gateway starting"})).ordered
76
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_PROGRESS, "status_description" => "Starting pig"})).ordered
77
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_SUCCESS, "status_description" => "Success", "web_result_url" => describe_url})).ordered
78
+
79
+ # stub launchy
80
+ mock(Launchy).open(describe_url) {Thread.new {}}
81
+
82
+ write_file(File.join(p.pigscripts_path, "my_script.pig"))
83
+ stderr, stdout = execute("describe my_script my_alias --polling_interval 0.05 -p key=value", p, @git)
84
+ stdout.should == <<-STDOUT
85
+ Taking code snapshot... done
86
+ Sending code snapshot to Mortar... done
87
+ Starting describe... done
88
+
89
+ \r\e[0KStatus: Pending... /\r\e[0KStatus: Gateway starting... -\r\e[0KStatus: Starting pig... \\\r\e[0KStatus: Success
90
+
91
+ Results available at https://api.mortardata.com/describe/c571a8c7f76a4fd4a67c103d753e2dd5
92
+ Opening web browser to show results... done
93
+ STDOUT
94
+ end
95
+ end
96
+
97
+ it "requests and reports on a failed describe" do
98
+ with_git_initialized_project do |p|
99
+ # stub api requests
100
+ describe_id = "c571a8c7f76a4fd4a67c103d753e2dd5"
101
+
102
+ error_message = "This is my error message\nWith multiple lines."
103
+ line_number = 23
104
+ column_number = 32
105
+ error_type = 'PigError'
106
+
107
+ mock(Mortar::Auth.api).post_describe("myproject", "my_script", "my_alias", is_a(String), :parameters => []) {Excon::Response.new(:body => {"describe_id" => describe_id})}
108
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_QUEUED, "status_description" => "Pending"})).ordered
109
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_FAILURE, "status_description" => "Failed",
110
+ "error_message" => error_message,
111
+ "line_number" => line_number,
112
+ "column_number" => column_number,
113
+ "error_type" => error_type})).ordered
114
+
115
+ write_file(File.join(p.pigscripts_path, "my_script.pig"))
116
+ stderr, stdout = execute("describe my_script my_alias --polling_interval 0.05", p, @git)
117
+ stdout.should == <<-STDOUT
118
+ Taking code snapshot... done
119
+ Sending code snapshot to Mortar... done
120
+ Starting describe... done
121
+
122
+ \r\e[0KStatus: Pending... /\r\e[0KStatus: Failed
123
+
124
+ STDOUT
125
+ stderr.should == <<-STDERR
126
+ ! Describe failed with PigError at Line 23, Column 32:
127
+ !
128
+ ! This is my error message
129
+ ! With multiple lines.
130
+ STDERR
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,139 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require "spec_helper"
18
+ require "mortar/command/generate"
19
+ require "fileutils"
20
+ require "tmpdir"
21
+
22
+ describe Mortar::Command::Generate do
23
+
24
+ before(:each) do
25
+ @tmpdir = Dir.mktmpdir
26
+ Dir.chdir(@tmpdir)
27
+ end
28
+
29
+ describe "generate:project" do
30
+ it "creates new project" do
31
+ stderr, stdout = execute("generate:project Test")
32
+ File.exists?("Test").should be_true
33
+ File.exists?("Test/macros").should be_true
34
+ File.exists?("Test/pigscripts").should be_true
35
+ File.exists?("Test/udfs").should be_true
36
+ File.exists?("Test/README.md").should be_true
37
+ File.exists?("Test/Gemfile").should be_true
38
+ #File.exists?("Test/Gemfile.lock").should be_true
39
+ File.exists?("Test/macros/.gitkeep").should be_true
40
+ File.exists?("Test/pigscripts/Test.pig").should be_true
41
+ File.exists?("Test/udfs/python/Test.py").should be_true
42
+
43
+ File.read("Test/pigscripts/Test.pig").each_line { |line| line.match(/<%.*%>/).should be_nil }
44
+ end
45
+ it "error when name isn't provided" do
46
+ stderr, stdout = execute("generate:project")
47
+ stderr.should == <<-STDERR
48
+ ! Usage: mortar new PROJECTNAME
49
+ ! Must specify PROJECTNAME.
50
+ STDERR
51
+ end
52
+ end
53
+
54
+ describe "new" do
55
+ it "create new project using alias" do
56
+ stderr, stdout = execute("new Test")
57
+ File.exists?("Test").should be_true
58
+ File.exists?("Test/macros").should be_true
59
+ File.exists?("Test/pigscripts").should be_true
60
+ File.exists?("Test/udfs").should be_true
61
+ File.exists?("Test/README.md").should be_true
62
+ File.exists?("Test/Gemfile").should be_true
63
+ #File.exists?("Test/Gemfile.lock").should be_true
64
+ File.exists?("Test/macros/.gitkeep").should be_true
65
+ File.exists?("Test/pigscripts/Test.pig").should be_true
66
+ File.exists?("Test/udfs/python/Test.py").should be_true
67
+
68
+ File.read("Test/pigscripts/Test.pig").each_line { |line| line.match(/<%.*%>/).should be_nil }
69
+ end
70
+
71
+ it "error when name isn't provided" do
72
+ stderr, stdout = execute("new")
73
+ stderr.should == <<-STDERR
74
+ ! Usage: mortar new PROJECTNAME
75
+ ! Must specify PROJECTNAME.
76
+ STDERR
77
+ end
78
+ end
79
+
80
+ describe "generate:pigscript" do
81
+ it "Generate a new pigscript in a project" do
82
+ with_blank_project do |p|
83
+ stderr, stdout = execute("generate:pigscript Oink", p)
84
+ File.exists?(File.join(p.root_path, "pigscripts/Oink.pig")).should be_true
85
+ File.read("pigscripts/Oink.pig").each_line { |line| line.match(/<%.*%>/).should be_nil }
86
+ end
87
+ end
88
+
89
+ it "error when pigscript name isn't provided" do
90
+ with_blank_project do |p|
91
+ stderr, stdout = execute("generate:pigscript")
92
+ stderr.should == <<-STDERR
93
+ ! Usage: mortar generate:pigscript SCRIPTNAME
94
+ ! Must specify SCRIPTNAME.
95
+ STDERR
96
+ end
97
+ end
98
+ end
99
+
100
+ describe "generate:python_udf" do
101
+ it "Generate a new python udf in a project" do
102
+ with_blank_project do |p|
103
+ stderr, stdout = execute("generate:python_udf slither", p)
104
+ File.exists?(File.join(p.root_path, "udfs/python/slither.py")).should be_true
105
+ File.read("udfs/python/slither.py").each_line { |line| line.match(/<%.*%>/).should be_nil }
106
+ end
107
+ end
108
+
109
+ it "error when udf name isn't provided" do
110
+ with_blank_project do |p|
111
+ stderr, stdout = execute("generate:python_udf")
112
+ stderr.should == <<-STDERR
113
+ ! Usage: mortar generate:python_udf UDFNAME
114
+ ! Must specify UDFNAME.
115
+ STDERR
116
+ end
117
+ end
118
+ end
119
+
120
+ describe "generate:macro" do
121
+ it "Generate a new macro in a project" do
122
+ with_blank_project do |p|
123
+ stderr, stdout = execute("generate:macro big_mac", p)
124
+ File.exists?(File.join(p.root_path, "macros/big_mac.pig")).should be_true
125
+ File.read("macros/big_mac.pig").each_line { |line| line.match(/<%.*%>/).should be_nil }
126
+ end
127
+ end
128
+
129
+ it "error when udf name isn't provided" do
130
+ with_blank_project do |p|
131
+ stderr, stdout = execute("generate:macro")
132
+ stderr.should == <<-STDERR
133
+ ! Usage: mortar generate:macro MACRONAME
134
+ ! Must specify MACRONAME.
135
+ STDERR
136
+ end
137
+ end
138
+ end
139
+ end