├── .document
├── .gitignore
├── HACKING
├── LICENSE
├── OWNERS
├── README.rdoc
├── Rakefile
├── VERSION
├── bin
└── scala-bootstrapper
├── lib
└── template
│ ├── .gitignore.erb
│ ├── Gemfile
│ ├── README.md
│ ├── TUTORIAL.md
│ ├── build.sbt.erb
│ ├── config
│ ├── development.scala.erb
│ ├── production.scala.erb
│ ├── staging.scala.erb
│ └── test.scala.erb
│ ├── console.erb
│ ├── project
│ └── plugins.sbt
│ ├── sbt
│ └── src
│ ├── main
│ ├── scala
│ │ └── com
│ │ │ └── twitter
│ │ │ └── birdname
│ │ │ ├── BirdNameConsoleClient.scala.erb
│ │ │ ├── BirdNameServiceImpl.scala.erb
│ │ │ ├── Main.scala.erb
│ │ │ └── config
│ │ │ └── BirdNameServiceConfig.scala.erb
│ └── thrift
│ │ └── birdname.thrift.erb
│ ├── scripts
│ ├── birdname.sh
│ ├── config.sh
│ ├── devel.sh
│ ├── server.sh
│ └── service.sh
│ └── test
│ └── scala
│ └── com
│ └── twitter
│ └── birdname
│ ├── AbstractSpec.scala.erb
│ └── BirdNameServiceSpec.scala.erb
├── scala-bootstrapper.gemspec
└── vendor
└── trollop.rb
/.document:
--------------------------------------------------------------------------------
1 | README.rdoc
2 | lib/**/*.rb
3 | bin/*
4 | features/**/*.feature
5 | LICENSE
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## MAC OS
2 | .DS_Store
3 |
4 | ## TEXTMATE
5 | *.tmproj
6 | tmtags
7 |
8 | ## EMACS
9 | *~
10 | \#*
11 | .\#*
12 |
13 | ## VIM
14 | *.swp
15 |
16 | ## PROJECT::GENERAL
17 | coverage
18 | rdoc
19 | pkg
20 |
21 | ## PROJECT::SPECIFIC
22 |
--------------------------------------------------------------------------------
/HACKING:
--------------------------------------------------------------------------------
1 | When you make changes to this project, please test by bootstrapping a new project and running `sbt update test` there.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009-2011 Twitter, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/OWNERS:
--------------------------------------------------------------------------------
1 | kmaxwell
2 | robey
3 | review_group:Scala-Cafe
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | Twitter is no longer maintaining this project or responding to issues or PRs.
2 | -----
3 |
4 | This gem produces a skeleton finagled service directory to be used with the new SBT 0.11 and Scala 2.9.1
5 |
6 | -----
7 |
8 | This creates a standard environment for your twitter-centric sbt/scala thrift service.
9 |
10 | Building:
11 |
12 | :$ rake build
13 | :$ gem install pkg/scala-bootstrapper-*.gem
14 |
15 | Usage:
16 |
17 | :$ mkdir birdname
18 | :$ cd birdname
19 | :$ scala-bootstrapper birdname
20 | :$ sbt update test
21 |
22 | Tutorial:
23 |
24 | :$ less TUTORIAL.md
25 |
26 | == Git support
27 |
28 | You can track files generated by scala-bootstrapper in a Git branch,
29 | and later merge changes from the branch (e.g. to rename a project, or
30 | to upgrade to a newer version of scala-bootstrapper.
31 |
32 | To get started:
33 |
34 | :$ scala-bootstrapper --git foo
35 |
36 | For a brand-new project (no .git directory) this will
37 | initialize a Git repo in the directory, generate files into the
38 | scala-bootstrapper branch, and merge the branch to
39 | master.
40 |
41 | For an existing project, this will generate files into the
42 | scala-bootstrapper branch, and merge it to the current branch
43 | *without* actually taking the changes (just making
44 | scala-bootstrapper a parent of the current branch to anchor
45 | future merges). This is to avoid clobbering files if you had
46 | previously run scala-bootstrapper without the --git
47 | option (or created files some other way). If you want to merge the
48 | changes and manually resolve any conflicts, do
49 |
50 | :$ git cherry-pick --no-commit scala-bootstrapper
51 |
52 | Once the scala-bootstrapper branch is created, subsequent
53 | runs will generate files into the branch and merge it to the current
54 | branch; if there are conflicts you can resolve them in the usual way.
55 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake'
3 |
4 | begin
5 | require 'jeweler'
6 | Jeweler::Tasks.new do |gem|
7 | gem.name = "scala-bootstrapper"
8 | gem.summary = %Q{Twitter scala project init}
9 | gem.description = %Q{Twitter scala project init}
10 | gem.email = "kmaxwell@twitter.com"
11 | gem.homepage = "http://github.com/fizx/scala-bootstrapper"
12 | gem.authors = ["Kyle Maxwell"]
13 | gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15 | end
16 | Jeweler::GemcutterTasks.new
17 | rescue LoadError
18 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19 | end
20 |
21 | require 'rake/testtask'
22 | Rake::TestTask.new(:test) do |test|
23 | test.libs << 'lib' << 'test'
24 | test.pattern = 'test/**/test_*.rb'
25 | test.verbose = true
26 | end
27 |
28 | begin
29 | require 'rcov/rcovtask'
30 | Rcov::RcovTask.new do |test|
31 | test.libs << 'test'
32 | test.pattern = 'test/**/test_*.rb'
33 | test.verbose = true
34 | end
35 | rescue LoadError
36 | task :rcov do
37 | abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38 | end
39 | end
40 |
41 | task :test => :check_dependencies
42 |
43 | task :default => :test
44 |
45 | require 'rake/rdoctask'
46 | Rake::RDocTask.new do |rdoc|
47 | version = File.exist?('VERSION') ? File.read('VERSION') : ""
48 |
49 | rdoc.rdoc_dir = 'rdoc'
50 | rdoc.title = "scala-bootstrapper #{version}"
51 | rdoc.rdoc_files.include('README*')
52 | rdoc.rdoc_files.include('lib/**/*.rb')
53 | end
54 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.11.0
2 |
--------------------------------------------------------------------------------
/bin/scala-bootstrapper:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require File.dirname(__FILE__) + "/../vendor/trollop"
4 |
5 | opts = Trollop::options do
6 | banner "Usage: #{$0} [options] PROJECT_NAME"
7 |
8 | opt :public, "Use the public twitter maven repo"
9 | opt :namespace, "Use something besides com.twitter", :type => :string
10 | opt :git, "Use Git to track updates to generated files"
11 | end
12 |
13 |
14 | if ARGV.length < 1
15 | Trollop::die "PROJECT_NAME is required"
16 | exit 1
17 | end
18 |
19 | class String
20 | def camelize(first_letter_in_uppercase = false)
21 | if first_letter_in_uppercase
22 | gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
23 | else
24 | self[0].chr.downcase + camelize(self)[1..-1]
25 | end
26 | end
27 | def underscore
28 | self.gsub(/::/, '/').
29 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
30 | gsub(/([a-z\d])([A-Z])/,'\1_\2').
31 | tr("-", "_").
32 | downcase
33 | end
34 | end
35 |
36 | def gsub_birds(haystack, name, namespace)
37 | haystack.
38 | gsub("com.twitter.birdname", "#{namespace}.#{name.downcase}").
39 | gsub("com/twitter/birdname", "#{namespace.gsub('.', '/')}/#{name.downcase}").
40 | gsub("BirdName", name).
41 | gsub("birdname", name.downcase).
42 | gsub("bird_name", name.underscore).
43 | gsub("birdName", name.camelize)
44 | end
45 |
46 | def sys(cmd, abort_on_fail=true)
47 | system(cmd + " &> /dev/null") || abort_on_fail && abort("failed: #{cmd}")
48 | end
49 |
50 | require "erb"
51 | require "fileutils"
52 | include FileUtils
53 |
54 | project_name = ARGV.pop.camelize(true)
55 | is_public = opts[:public]
56 | namespace = opts[:namespace] || "com.twitter"
57 | git = opts[:git]
58 | sbt11_requested = opts[:sbt11]
59 |
60 | $overwrite_all = true if git
61 | $ex_post_facto = false
62 | $branch = 'master'
63 | $files = []
64 |
65 | if git
66 | if !File.exists?('.git')
67 | if `ls -l` != ''
68 | abort('files in directory, no git repo.')
69 | end
70 | sys('git init')
71 | sys('touch README.md')
72 | sys('git add .')
73 | sys("git commit -m'first commit'")
74 | sys('git checkout -b scala-bootstrapper')
75 |
76 | else
77 | if `git status -s` != ''
78 | abort('uncommitted files in directory.')
79 | end
80 | $branch = `git branch`.grep(/^\*/).first.chomp.gsub(/^\* (.+)$/, '\1')
81 |
82 | if !sys('git checkout scala-bootstrapper', false)
83 | $ex_post_facto = true
84 | sys('git checkout -b scala-bootstrapper')
85 | end
86 | end
87 | end
88 |
89 | root = File.expand_path(File.dirname(__FILE__) + "/../lib/template")
90 |
91 | Dir["#{root}/**/*"].select { |path| File.file?(path) }.each do |path|
92 | relative = path.sub("#{root}/", "")
93 | content = File.read(path)
94 | template = ERB.new(content, nil, nil, "@output")
95 | target_path = gsub_birds(relative, project_name, namespace).sub(/\.erb$/, '')
96 | if File.exists?(target_path) && !$overwrite_all
97 | print "File exists `#{relative}`, replace? ([Y]es, [N]o, [A]ll, [Q]uit)"
98 | $stdout.flush
99 | case STDIN.gets
100 | when /^y/i
101 | #continue
102 | when /^n/i
103 | next
104 | when /^a/i
105 | $overwrite_all = true
106 | when /^q/i
107 | exit(2)
108 | else
109 | redo
110 | end
111 | end
112 | puts "writing #{target_path}"
113 | mkdir_p(File.dirname(target_path))
114 | File.open(target_path, "w") {|f| f.print(gsub_birds(template.result(binding), project_name, namespace)) }
115 | $files << target_path
116 | end
117 |
118 | if File.exists?("src/scripts/startup.sh")
119 | startup = "src/scripts/#{project_name.downcase}.sh"
120 | `mv src/scripts/startup.sh #{startup}`
121 | $files << startup
122 | end
123 |
124 | [
125 | "src/scripts/#{project_name.downcase}.sh",
126 | "src/scripts/devel.sh",
127 | "console",
128 | "sbt",
129 | ].each do |executable|
130 | `chmod +x #{executable}` if File.exists?(executable)
131 | end
132 |
133 | if git
134 | $files.each { |file| sys("git add #{file}") if File.exists?(file) }
135 | sys("git commit -m'scala-bootstrapper'", false) # fails if no change
136 | sys("git checkout #{$branch}")
137 | sys('git merge --no-ff --no-commit scala-bootstrapper')
138 |
139 | if $ex_post_facto
140 | # don't commit anything, just make scala-bootstrapper head a parent
141 | sys('rm .git/index')
142 | sys('git checkout HEAD .')
143 | sys('git clean -fdx')
144 | end
145 |
146 | sys("git commit -m'merged scala-bootstrapper'", false) # fails if no change
147 | end
148 |
149 | if $ex_post_facto
150 | puts < *sbtIdeaRepo at http://mpeltonen.github.com/maven/
17 | > *idea is com.github.mpeltonen sbt-idea-processor 0.4.0
18 | > update
19 | > idea
20 |
21 | # Documenting your project
22 |
23 | Add documentation here! Eventually, you'll be able to publish this to
24 | a web site for the world to easily find and read.
25 |
--------------------------------------------------------------------------------
/lib/template/TUTORIAL.md:
--------------------------------------------------------------------------------
1 | # Welcome to BirdName!
2 |
3 | ## Setup
4 |
5 | Scala-bootstrapper has created a fully-functional Scala service for
6 | you. You can verify that things are set up correctly by doing:
7 |
8 | $ sbt update test
9 |
10 | ## Tutorial
11 |
12 | ### Run your service!
13 |
14 | There are two ways to start your service. You can build a runnable
15 | jar and tell java to run it directly:
16 |
17 | $ sbt package-dist
18 | $ java -Dstage=development -jar ./dist/birdname/birdname-1.0.0-SNAPSHOT.jar
19 |
20 | or you can ask sbt to run your service:
21 |
22 | $ sbt 'run -f config/development.scala'
23 |
24 | ### Verify that the service is running
25 |
26 | The java/sbt command-lines will "hang" because the server is running in the
27 | foreground. (In production, we use libslack-daemon to wrap java processes into
28 | unix daemons.) Go to another terminal and check for a logfile. If your server
29 | is named "birdname", there should be a `birdname.log` with contents like this:
30 |
31 | INF [20110615-14:05:41.656] stats: Starting JsonStatsLogger
32 | INF [20110615-14:05:41.674] admin: Starting TimeSeriesCollector
33 | DEB [20110615-14:05:41.792] nio: Using the autodetected NIO constraint level: 0
34 |
35 | That's your indication that the server is running. :)
36 |
37 | ### View the Thrift IDL for your service
38 |
39 | The IDL for your service is in `src/main/thrift/birdname.thrift`. The
40 | Thrift compiler uses the IDL to generate bindings for various
41 | languages, making it easy for scripts in those languages to talk to
42 | your service. More information about Thrift and how to write an IDL
43 | for your service can be found [here](http://wiki.apache.org/thrift/Tutorial).
44 |
45 | ### Call your service from ruby
46 |
47 | Your service implements simple get() and put() methods. Once you have
48 | your server running, as above, bring up a different shell and:
49 |
50 | $ cd birdname
51 | $ bundle install
52 | $ ./dist/birdname/scripts/console
53 | >> $client
54 | >> $client.put("key1", "valueForKey")
55 | >> $client.get("key1")
56 |
57 | ### Look at the stats for your service
58 |
59 | By default, your project is configured to use
60 | [Ostrich](https://github.com/twitter/ostrich), a library for service
61 | configuration, administration, and stats reporting. Your config file
62 | in `config/development.scala` defines which port ostrich uses for admin
63 | requests. You can view the stats via that port:
64 |
65 | $ curl localhost:9900/stats.txt
66 | counters:
67 | BirdName/connects: 1
68 | BirdName/requests: 2
69 | BirdName/success: 2
70 | ...
71 |
72 | Ostrich also stores historial stats data and can build
73 | [graphs](http://localhost:9900/graph/) for you.
74 |
75 | ### Stop the service
76 |
77 | You can ask the server to shutdown over the admin port also:
78 |
79 | $ curl localhost:9900/shutdown.txt
80 | ok
81 |
82 | ### View the implementation of get() and put()
83 |
84 | In `src/main/scala`, take a look at `BirdNameServiceImpl.scala`. (This may
85 | have a different name, based on what you called your server.)
86 |
87 | The base interface is specified by thrift. Additionally, we're using Twitter's
88 | async I/O framework: finagle. Finagle (and a lot of great documentation about
89 | it) is hosted here: https://github.com/twitter/finagle
90 |
91 | ### Try adding some timers and counters
92 |
93 | At the top of BirdNameServiceImpl.scala, add:
94 |
95 | import com.twitter.ostrich.stats.Stats
96 |
97 | Then inside get():
98 |
99 | Stats.incr("birdname.gets")
100 |
101 | and inside put():
102 |
103 | Stats.incr("birdname.puts")
104 |
105 | Then restart your server, talk to the server via console, and check
106 | your stats:
107 |
108 | $ curl localhost:9900/stats.txt
109 | counters:
110 | BirdName/connects: 1
111 | BirdName/requests: 2
112 | BirdName/success: 2
113 | birdname.gets: 1
114 | birdname.puts: 1
115 |
116 | You can also time various things that your server is doing, for
117 | example:
118 |
119 | Stats.time("birdname.put.latency") {
120 | Thread.sleep(10) // so you can see it
121 | database(key) = value
122 | }
123 |
124 | ### Specs: let's add some tests
125 |
126 | [Specs](http://code.google.com/p/specs/) is a Behavior-Driven Design
127 | framework that allows you to write semi-human-readable descriptions of
128 | how your service should behave and test that those descriptions are
129 | valid. You already have some Specs code for your project in
130 | src/test/scala/com/twitter/birdname/BirdNameServiceSpec.scala. Check
131 | out the existing test and add a new one for the counter functionality
132 | we just added.
133 |
134 | import com.twitter.ostrich.stats.Stats
135 |
136 | ...
137 |
138 | "verify stats" in {
139 | val counters = Stats.getCounters
140 | birdname.put("name", "bluebird")()
141 | birdname.get("name")() mustEqual "bluebird"
142 | counters.getOrElse("birdname.gets", 1) must_==1
143 | counters.getOrElse("birdname.puts", 1) must_==1
144 | }
145 |
146 | TODO: add link to scala school lesson on Specs
147 |
148 | ### Automatically compile and test your server when you change code
149 |
150 | By now you've had to Ctrl-C your server and restart it to get changes
151 | to show up. This gets a little tiresome. The build tool we are
152 | using,
153 | [SBT (simple build tool)](http://code.google.com/p/simple-build-tool/)
154 | has a console that you can access by just running "sbt" from the
155 | command line.
156 |
157 | $ sbt
158 | [info] Standard project rules 0.11.4 loaded (2011-03-18).
159 | [warn] No .svnrepo file; no svn repo will be configured.
160 | [info] Building project birdname 1.0.0-SNAPSHOT against Scala 2.8.1
161 | [info] using BirdNameProject with sbt 0.7.4 and Scala 2.7.7
162 |
163 | SBT has a wide array of features, but a useful one right now is to
164 | use the "~ test" command.
165 |
166 | > ~ test
167 |
168 | The tilde tells SBT to look for changes to your source files and
169 | re-execute the command when it detects a change.
170 |
171 | TODO: add link to scala school lesson on SBT
172 |
173 | ### Add an admin / dashboard page.
174 |
175 | ### Add a new dependency to your project, perhaps twitter/util?
176 |
177 | ### Take a tour of the logs our service is producing.
178 |
179 | ### Add command-line parameters for your service.
180 | -D foo=bar
181 | runtime.arguments.get("foo")
182 |
183 | ### Storage: let's persist the data in Cassandra!
184 |
185 | ### Twitter API: let's listen to the Firehose!
186 |
187 | ### Twitter API: let's fetch some statuses & users & stuff.
188 |
--------------------------------------------------------------------------------
/lib/template/build.sbt.erb:
--------------------------------------------------------------------------------
1 | import com.twitter.sbt._
2 | import com.twitter.scalatest._
3 |
4 | seq((
5 | Project.defaultSettings ++
6 | StandardProject.newSettings ++
7 | SubversionPublisher.newSettings ++
8 | CompileThriftScrooge.newSettings ++
9 | ScalaTestMixins.testSettings
10 | ): _*)
11 |
12 | organization := "com.twitter"
13 |
14 | name := "birdname"
15 |
16 | version := "1.0.0-SNAPSHOT"
17 |
18 | libraryDependencies ++= Seq(
19 | "org.scala-lang" % "jline" % "2.9.1",
20 | "com.twitter" % "scrooge" % "3.0.1",
21 | "com.twitter" % "scrooge-runtime_2.9.2" % "3.0.1",
22 | "com.twitter" % "finagle-core" % "5.3.6",
23 | "com.twitter" % "finagle-thrift" % "5.3.6",
24 | "com.twitter" % "finagle-ostrich4" % "5.3.1",
25 | "org.scalatest" %% "scalatest" % "1.7.1" % "test",
26 | "com.twitter" %% "scalatest-mixins" % "1.1.0" % "test"
27 | )
28 |
29 | mainClass in (Compile, run) := Some("com.twitter.birdname.Main")
30 |
31 | mainClass in (Compile, packageBin) := Some("com.twitter.birdname.Main")
32 |
33 | CompileThriftScrooge.scroogeVersion := "3.0.1"
34 |
--------------------------------------------------------------------------------
/lib/template/config/development.scala.erb:
--------------------------------------------------------------------------------
1 | import com.twitter.conversions.time._
2 | import com.twitter.logging.config._
3 | import com.twitter.ostrich.admin.config._
4 | import com.twitter.birdname.config._
5 |
6 | // development mode.
7 | new BirdNameServiceConfig {
8 |
9 | // Add your own config here
10 |
11 | // Where your service will be exposed.
12 | thriftPort = 9999
13 |
14 | // Ostrich http admin port. Curl this for stats, etc
15 | admin.httpPort = 9900
16 |
17 | // End user configuration
18 |
19 | // Expert-only: Ostrich stats and logger configuration.
20 |
21 | admin.statsNodes = new StatsConfig {
22 | reporters = new TimeSeriesCollectorConfig
23 | }
24 |
25 | loggers =
26 | new LoggerConfig {
27 | level = Level.DEBUG
28 | handlers = new FileHandlerConfig {
29 | filename = "birdname.log"
30 | roll = Policy.SigHup
31 | }
32 | } :: new LoggerConfig {
33 | node = "stats"
34 | level = Level.INFO
35 | useParents = false
36 | handlers = new FileHandlerConfig {
37 | filename = "stats.log"
38 | formatter = BareFormatterConfig
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/template/config/production.scala.erb:
--------------------------------------------------------------------------------
1 | import com.twitter.conversions.time._
2 | import com.twitter.logging.config._
3 | import com.twitter.ostrich.admin.config._
4 | import com.twitter.birdname.config._
5 |
6 | // production mode.
7 | new BirdNameServiceConfig {
8 |
9 | // Add your own config here
10 |
11 | // Where your service will be exposed.
12 | thriftPort = 9999
13 |
14 | // Ostrich http admin port. Curl this for stats, etc
15 | admin.httpPort = 9900
16 |
17 | // End user configuration
18 |
19 | // Expert-only: Ostrich stats and logger configuration.
20 |
21 | admin.statsNodes = new StatsConfig {
22 | reporters = new TimeSeriesCollectorConfig
23 | }
24 |
25 | loggers =
26 | new LoggerConfig {
27 | level = Level.INFO
28 | handlers = new FileHandlerConfig {
29 | filename = "/var/log/birdname/production.log"
30 | roll = Policy.SigHup
31 | }
32 | } :: new LoggerConfig {
33 | node = "stats"
34 | level = Level.INFO
35 | useParents = false
36 | handlers = new FileHandlerConfig {
37 | filename = "stats.log"
38 | formatter = BareFormatterConfig
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/template/config/staging.scala.erb:
--------------------------------------------------------------------------------
1 | import com.twitter.ostrich.admin.config._
2 | import com.twitter.conversions.time._
3 | import com.twitter.logging.config._
4 | import com.twitter.birdname.config._
5 |
6 | // staging mode.
7 | new BirdNameServiceConfig {
8 |
9 | // Add your own config here
10 |
11 | // Where your service will be exposed.
12 | thriftPort = 9999
13 |
14 | // Ostrich http admin port. Curl this for stats, etc
15 | admin.httpPort = 9900
16 |
17 | // End user configuration
18 |
19 | // Expert-only: Ostrich stats and logger configuration.
20 |
21 | admin.statsNodes = new StatsConfig {
22 | reporters = new TimeSeriesCollectorConfig
23 | }
24 |
25 | loggers =
26 | new LoggerConfig {
27 | level = Level.INFO
28 | handlers = new FileHandlerConfig {
29 | filename = "/var/log/birdname/production.log"
30 | roll = Policy.SigHup
31 | }
32 | } :: new LoggerConfig {
33 | node = "stats"
34 | level = Level.INFO
35 | useParents = false
36 | handlers = new FileHandlerConfig {
37 | filename = "stats.log"
38 | formatter = BareFormatterConfig
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/template/config/test.scala.erb:
--------------------------------------------------------------------------------
1 | import com.twitter.conversions.time._
2 | import com.twitter.logging.config._
3 | import com.twitter.ostrich.admin.config._
4 | import com.twitter.birdname.config._
5 |
6 | // test mode.
7 | new BirdNameServiceConfig {
8 |
9 | // Add your own config here
10 |
11 | // Where your service will be exposed.
12 | thriftPort = 9999
13 |
14 | // Ostrich http admin port. Curl this for stats, etc
15 | admin.httpPort = 9900
16 |
17 | // End user configuration
18 |
19 | // Expert-only: Ostrich stats and logger configuration.
20 |
21 | loggers =
22 | new LoggerConfig {
23 | level = Level.FATAL
24 | handlers = new ConsoleHandlerConfig
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/template/console.erb:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ $# -lt 2 ] ; then
4 | echo "usage: console "
5 | exit 0
6 | fi
7 |
8 | ./sbt "run-main com.twitter.birdname.BirdNameConsoleClient $1 $2"
9 |
--------------------------------------------------------------------------------
/lib/template/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | sbtResolver <<= (sbtResolver) { r =>
2 | Option(System.getenv("SBT_PROXY_REPO")) map { x =>
3 | Resolver.url("proxy repo for sbt", url(x))(Resolver.ivyStylePatterns)
4 | } getOrElse r
5 | }
6 |
7 | resolvers <<= (resolvers) { r =>
8 | (Option(System.getenv("SBT_PROXY_REPO")) map { url =>
9 | Seq("proxy-repo" at url)
10 | } getOrElse {
11 | r ++ Seq(
12 | "twitter.com" at "http://maven.twttr.com/",
13 | "scala-tools" at "http://scala-tools.org/repo-releases/",
14 | "maven" at "http://repo1.maven.org/maven2/",
15 | "freemarker" at "http://freemarker.sourceforge.net/maven2/"
16 | )
17 | }) ++ Seq("local" at ("file:" + System.getProperty("user.home") + "/.m2/repo/"))
18 | }
19 |
20 | externalResolvers <<= (resolvers) map identity
21 |
22 | addSbtPlugin("com.twitter" %% "sbt-package-dist" % "1.0.5")
23 |
24 | addSbtPlugin("com.twitter" %% "sbt11-scrooge" % "3.0.0")
25 |
26 | addSbtPlugin("com.twitter" % "sbt-thrift2" % "0.0.1")
27 |
28 | libraryDependencies += "com.twitter" %% "scalatest-mixins" % "1.0.3"
29 |
--------------------------------------------------------------------------------
/lib/template/sbt:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | root=$(
4 | cd $(dirname $(readlink $0 || echo $0))/..
5 | /bin/pwd
6 | )
7 |
8 | sbtjar=sbt-launch.jar
9 |
10 | if [ ! -f $sbtjar ]; then
11 | echo 'downloading '$sbtjar 1>&2
12 | curl -O http://repo.typesafe.com/typesafe/ivy-releases/org.scala-tools.sbt/sbt-launch/0.11.2/$sbtjar
13 | fi
14 |
15 | test -f $sbtjar || exit 1
16 |
17 | sbtjar_md5=$(openssl md5 < $sbtjar|cut -f2 -d'='|awk '{print $1}')
18 |
19 | if [ "${sbtjar_md5}" != 2886cc391e38fa233b3e6c0ec9adfa1e ]; then
20 | echo 'bad sbtjar!' 1>&2
21 | exit 1
22 | fi
23 |
24 | test -f ~/.sbtconfig && . ~/.sbtconfig
25 |
26 | java -ea \
27 | $SBT_OPTS \
28 | $JAVA_OPTS \
29 | -Djava.net.preferIPv4Stack=true \
30 | -XX:+AggressiveOpts \
31 | -XX:+UseParNewGC \
32 | -XX:+UseConcMarkSweepGC \
33 | -XX:+CMSParallelRemarkEnabled \
34 | -XX:+CMSClassUnloadingEnabled \
35 | -XX:MaxPermSize=1024m \
36 | -XX:SurvivorRatio=128 \
37 | -XX:MaxTenuringThreshold=0 \
38 | -Xss8M \
39 | -Xms512M \
40 | -Xmx3G \
41 | -server \
42 | -jar $sbtjar "$@"
--------------------------------------------------------------------------------
/lib/template/src/main/scala/com/twitter/birdname/BirdNameConsoleClient.scala.erb:
--------------------------------------------------------------------------------
1 | package com.twitter.birdname
2 |
3 | import com.twitter.conversions.time._
4 | import com.twitter.finagle.builder.ClientBuilder
5 | import com.twitter.finagle.thrift.ThriftClientFramedCodec
6 | import java.net.InetSocketAddress
7 | import scala.tools.nsc.interpreter._
8 | import scala.tools.nsc.Settings
9 |
10 | object BirdNameConsoleClient extends App {
11 | val service = ClientBuilder()
12 | .hosts(new InetSocketAddress(args(0), args(1).toInt))
13 | .codec(ThriftClientFramedCodec())
14 | .hostConnectionLimit(1)
15 | .tcpConnectTimeout(3.seconds)
16 | .build()
17 |
18 | val client = new BirdNameService.FinagledClient(service)
19 |
20 | val intLoop = new ILoop()
21 |
22 | Console.println("'client' is bound to your thrift client.")
23 | intLoop.setPrompt("\nfinagle-client> ")
24 |
25 | intLoop.settings = {
26 | val s = new Settings(Console.println)
27 | s.embeddedDefaults[BirdNameService.FinagledClient]
28 | s.Yreplsync.value = true
29 | s
30 | }
31 |
32 | intLoop.createInterpreter()
33 | intLoop.in = new JLineReader(new JLineCompletion(intLoop))
34 |
35 | intLoop.intp.beQuietDuring {
36 | intLoop.intp.interpret("""def exit = println("Type :quit to resume program execution.")""")
37 | intLoop.intp.bind(NamedParam("client", client))
38 | }
39 |
40 | intLoop.loop()
41 | intLoop.closeInterpreter()
42 | }
43 |
--------------------------------------------------------------------------------
/lib/template/src/main/scala/com/twitter/birdname/BirdNameServiceImpl.scala.erb:
--------------------------------------------------------------------------------
1 | package com.twitter.birdname
2 |
3 | import com.twitter.conversions.time._
4 | import com.twitter.logging.Logger
5 | import com.twitter.util._
6 | import java.util.concurrent.Executors
7 | import scala.collection.mutable
8 | import config._
9 |
10 | class BirdNameServiceImpl(config: BirdNameServiceConfig) extends BirdNameService.ThriftServer {
11 | val serverName = "BirdName"
12 | val thriftPort = config.thriftPort
13 | override val tracerFactory = config.tracerFactory
14 |
15 | /**
16 | * These services are based on finagle, which implements a nonblocking server. If you
17 | * are making blocking rpc calls, it's really important that you run these actions in
18 | * a thread pool, so that you don't block the main event loop. This thread pool is only
19 | * needed for these blocking actions. The code looks like:
20 | *
21 | * val futurePool = new FuturePool(Executors.newFixedThreadPool(config.threadPoolSize))
22 | *
23 | * def hello() = futurePool {
24 | * someService.blockingRpcCall
25 | * }
26 | *
27 | */
28 |
29 | val database = new mutable.HashMap[String, String]()
30 |
31 | def get(key: String) = {
32 | database.get(key) match {
33 | case None =>
34 | log.debug("get %s: miss", key)
35 | Future.exception(BirdNameException("No such key"))
36 | case Some(value) =>
37 | log.debug("get %s: hit", key)
38 | Future(value)
39 | }
40 | }
41 |
42 | def put(key: String, value: String) = {
43 | log.debug("put %s", key)
44 | database(key) = value
45 | Future.Unit
46 | }
47 |
48 | def shutdown() = {
49 | super.shutdown(0.seconds)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lib/template/src/main/scala/com/twitter/birdname/Main.scala.erb:
--------------------------------------------------------------------------------
1 | package com.twitter.birdname
2 |
3 | import com.twitter.logging.Logger
4 | import com.twitter.ostrich.admin.{RuntimeEnvironment, ServiceTracker}
5 |
6 | object Main {
7 | private val log = Logger.get(getClass)
8 |
9 | def main(args: Array[String]) {
10 | val runtime = RuntimeEnvironment(this, args)
11 | val server = runtime.loadRuntimeConfig[BirdNameService.ThriftServer]
12 | try {
13 | log.info("Starting BirdNameService")
14 | server.start()
15 | } catch {
16 | case e: Exception =>
17 | log.error(e, "Failed starting BirdNameService, exiting")
18 | ServiceTracker.shutdown()
19 | System.exit(1)
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/template/src/main/scala/com/twitter/birdname/config/BirdNameServiceConfig.scala.erb:
--------------------------------------------------------------------------------
1 | package com.twitter.birdname
2 | package config
3 |
4 | import com.twitter.finagle.tracing.{NullTracer, Tracer}
5 | import com.twitter.logging.Logger
6 | import com.twitter.logging.config._
7 | import com.twitter.ostrich.admin.{RuntimeEnvironment, ServiceTracker}
8 | import com.twitter.ostrich.admin.config._
9 | import com.twitter.util.Config
10 |
11 | class BirdNameServiceConfig extends ServerConfig[BirdNameService.ThriftServer] {
12 | var thriftPort: Int = 9999
13 | var tracerFactory: Tracer.Factory = NullTracer.factory
14 |
15 | def apply(runtime: RuntimeEnvironment) = new BirdNameServiceImpl(this)
16 | }
17 |
--------------------------------------------------------------------------------
/lib/template/src/main/thrift/birdname.thrift.erb:
--------------------------------------------------------------------------------
1 | namespace java com.twitter.birdname
2 | namespace rb BirdName
3 |
4 | /**
5 | * It's considered good form to declare an exception type for your service.
6 | * Thrift will serialize and transmit them transparently.
7 | */
8 | exception BirdNameException {
9 | 1: string description
10 | }
11 |
12 | /**
13 | * A simple memcache-like service, which stores strings by key/value.
14 | * You should replace this with your actual service.
15 | */
16 | service BirdNameService {
17 | string get(1: string key) throws(1: BirdNameException ex)
18 |
19 | void put(1: string key, 2: string value)
20 | }
21 |
--------------------------------------------------------------------------------
/lib/template/src/scripts/birdname.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | sh `dirname "$0"`/service.sh "$@"
--------------------------------------------------------------------------------
/lib/template/src/scripts/config.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | export APP_NAME="birdname"
4 | export APP_HOME="/usr/local/$APP_NAME/current"
5 |
6 | export PIDFILE="/var/run/$APP_NAME/$APP_NAME.pid"
7 | export STDOUT_FILE="/var/log/$APP_NAME/stdout"
8 | export STDERR_FILE="/var/log/$APP_NAME/error"
9 |
10 | export MAIN_JAR="$APP_NAME-1.0-SNAPSHOT.jar"
11 | export ADMIN_PORT=9900
12 |
13 | export HEAP_OPTS="-Xmx4096m -Xms4096m -XX:NewSize=768m"
14 | export GC_OPTS="-XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy -XX:MaxGCPauseMillis=1000 -XX:GCTimeRatio=99"
15 |
16 | export STAGE=production
17 | export EXTRA_JAVA_OPTS="-Dstage=$STAGE"
18 |
19 | # you can also define the following functions to override behavior:
20 | # running - checks whether the process is running
21 | # start - executed after the process is verified to not be running and a start was requested
22 | # stop - executed after the process is verified to be running and a stop was requested
23 | # stopped - is passed the PID before ths top and checks if its still running
24 |
--------------------------------------------------------------------------------
/lib/template/src/scripts/devel.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | echo "Starting birdname in development mode..."
3 | java -server -Xmx1024m -Dstage=development -jar ./dist/birdname/@DIST_NAME@-@VERSION@.jar
4 |
--------------------------------------------------------------------------------
/lib/template/src/scripts/server.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | ## this file encapsulates starting the server including the JVM options
3 | ## and putting the pid into the pidfile. this is executed by service.sh
4 | ## on the start action.
5 | ## it is an expectation that this be run in your application home directory.
6 |
7 | SCRIPT_DIR=`dirname $0`
8 | source "$SCRIPT_DIR/config.sh"
9 |
10 | GC_LOGGING_OPTS="-verbosegc -Xloggc:/var/log/$APP_NAME/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintHeapAtGC"
11 | JAVA_OPTS="-server -XX:+DisableExplicitGC -XX:+UseNUMA $GC_OPTS $GC_LOGGING_OPTS $HEAP_OPTS $EXTRA_JAVA_OPTS"
12 |
13 | if [ -z $APP_HOME ]; then
14 | APP_HOME=`pwd`
15 | fi
16 |
17 | if [ -z $PIDFILE ]; then
18 | PIDFILE="/var/run/$APP_NAME/$APP_NAME.pid"
19 | fi
20 |
21 | if [ ! -f "$APP_HOME/$MAIN_JAR" ]; then
22 | echo "jar not found at $APP_HOME/$MAIN_JAR"
23 | exit 1
24 | fi
25 |
26 | if [ -z $JAVA_HOME ]; then
27 | JAVA_HOME="/usr/java/default"
28 | fi
29 |
30 | echo $$ > $PIDFILE
31 | exec ${JAVA_HOME}/bin/java ${JAVA_OPTS} -jar ${APP_HOME}/${MAIN_JAR} ${CMD_OPTS}
32 |
--------------------------------------------------------------------------------
/lib/template/src/scripts/service.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | ## this file is the control script for your jvm service.
3 | ## it is executed by monit with an argument of either start, stop or restart
4 | ## it is not responsible for options specific to the jvm but does background,
5 | ## disown, and redirect output.
6 |
7 | function start() {
8 | cd $APP_HOME
9 | $START_SCRIPT > $STDOUT_FILE 2> $STDERR_FILE &
10 | disown %1
11 |
12 | sleep $INITIAL_SLEEP # give it time to compile config files and bind to ports
13 | }
14 |
15 | function running() {
16 | curl -m 5 -s "http://localhost:$ADMIN_PORT/ping.txt" > /dev/null 2> /dev/null
17 | }
18 |
19 | function stop() {
20 | kill -TERM $(cat $PIDFILE)
21 | }
22 |
23 | function stopped() {
24 | ! kill -0 $1 > /dev/null 2> /dev/null
25 | }
26 |
27 | SCRIPT_DIR=`dirname $0`
28 | source "$SCRIPT_DIR/config.sh"
29 | START_SCRIPT="sh $SCRIPT_DIR/server.sh"
30 |
31 | if [ -z $INITIAL_SLEEP ]; then
32 | INITIAL_SLEEP=3
33 | fi
34 |
35 | if [ -z $MAX_START_TRIES ]; then
36 | MAX_START_TRIES=5
37 | fi
38 |
39 | case "$1" in
40 | start)
41 | printf "Starting %s... " "$APP_NAME"
42 |
43 | if running; then
44 | echo "already running."
45 | exit 0
46 | fi
47 |
48 | start
49 |
50 | tries=0
51 | while ! running; do
52 | tries=$((tries + 1))
53 | if [ $tries -ge $MAX_START_TRIES ]; then
54 | echo "ERROR: failed to start"
55 | exit 1
56 | fi
57 | sleep 1
58 | done
59 | echo "done."
60 | ;;
61 |
62 | stop)
63 | printf "Stopping %s... " "$APP_NAME"
64 |
65 | if [ ! -f $PIDFILE ]; then
66 | GUESSED_PID=`pgrep -u $USER java`
67 | if [ -z $GUESSED_PID ]; then
68 | echo "ERROR: no pid file and no java process found"
69 | else
70 | echo "ERROR: no pid file, but found java process: $GUESSED_PID"
71 | fi
72 | exit 1
73 | fi
74 |
75 | SNAPSHOT_PID=$(cat $PIDFILE)
76 |
77 | if $(stopped $SNAPSHOT_PID); then
78 | echo "already stopped."
79 | exit 0
80 | fi
81 |
82 | stop
83 |
84 | tries=0
85 | while ! $(stopped $SNAPSHOT_PID); do
86 | tries=$((tries + 1))
87 | if [ $tries -ge 5 ]; then
88 | echo "ERROR: failed to stop"
89 | exit 1
90 | fi
91 | sleep 1
92 | done
93 | echo "done."
94 | ;;
95 |
96 | status)
97 | if running; then
98 | echo "$APP_NAME is running."
99 | else
100 | echo "$APP_NAME is NOT running."
101 | fi
102 | ;;
103 |
104 | restart)
105 | $0 stop
106 | sleep 2
107 | $0 start
108 | ;;
109 |
110 | *)
111 | echo "Usage: /etc/init.d/$APP_NAME {start|stop|restart|status}"
112 | exit 1
113 | ;;
114 | esac
115 |
116 | exit 0
117 |
--------------------------------------------------------------------------------
/lib/template/src/test/scala/com/twitter/birdname/AbstractSpec.scala.erb:
--------------------------------------------------------------------------------
1 | package com.twitter.birdname
2 |
3 | import com.twitter.conversions.time._
4 | import com.twitter.ostrich.admin._
5 | import com.twitter.scalatest.TestLogging
6 | import com.twitter.util._
7 | import org.scalatest._
8 |
9 | abstract class AbstractSpec extends FunSpec with TestLogging {
10 | lazy val env = RuntimeEnvironment(this, Array("-f", "config/test.scala"))
11 |
12 | lazy val birdName = {
13 | val out = env.loadRuntimeConfig[BirdNameService.ThriftServer]
14 |
15 | // You don't really want the thrift server active, particularly if you
16 | // are running repetitively via ~test
17 | ServiceTracker.shutdown() // all services
18 | out
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/template/src/test/scala/com/twitter/birdname/BirdNameServiceSpec.scala.erb:
--------------------------------------------------------------------------------
1 | package com.twitter.birdname
2 |
3 | class BirdNameServiceSpec extends AbstractSpec {
4 | describe("BirdNameService") {
5 |
6 | // TODO: Please implement your own tests.
7 |
8 | it("sets a key, then gets it") {
9 | birdName.put("name", "bluebird")()
10 | assert(birdName.get("name")() === "bluebird")
11 | intercept[Exception] { birdName.get("what?")() }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/scala-bootstrapper.gemspec:
--------------------------------------------------------------------------------
1 | # Generated by jeweler
2 | # DO NOT EDIT THIS FILE DIRECTLY
3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4 | # -*- encoding: utf-8 -*-
5 |
6 | Gem::Specification.new do |s|
7 | s.name = "scala-bootstrapper"
8 | s.version = "0.11.0"
9 |
10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11 | s.authors = ["Kyle Maxwell"]
12 | s.date = "2011-11-18"
13 | s.description = "Twitter scala project init"
14 | s.email = "kmaxwell@twitter.com"
15 | s.default_executable = "scala-bootstrapper"
16 | s.executables = ["scala-bootstrapper"]
17 | s.extra_rdoc_files = [
18 | "LICENSE",
19 | "README.rdoc"
20 | ]
21 | s.files = Dir.glob("lib/template/**/*") + [
22 | ".document",
23 | "HACKING",
24 | "LICENSE",
25 | "OWNERS",
26 | "README.rdoc",
27 | "Rakefile",
28 | "VERSION",
29 | "bin/scala-bootstrapper",
30 | "scala-bootstrapper.gemspec",
31 | "vendor/trollop.rb"
32 | ]
33 | s.homepage = "http://github.com/twitter/scala-bootstrapper"
34 | s.require_paths = ["lib"]
35 | s.rubygems_version = "1.8.11"
36 | s.summary = "Twitter scala project init"
37 |
38 | if s.respond_to? :specification_version then
39 | s.specification_version = 3
40 |
41 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
42 | s.add_development_dependency(%q, [">= 0"])
43 | else
44 | s.add_dependency(%q, [">= 0"])
45 | end
46 | else
47 | s.add_dependency(%q, [">= 0"])
48 | end
49 | end
50 |
51 |
--------------------------------------------------------------------------------
/vendor/trollop.rb:
--------------------------------------------------------------------------------
1 | ## lib/trollop.rb -- trollop command-line processing library
2 | ## Author:: William Morgan (mailto: wmorgan-trollop@masanjin.net)
3 | ## Copyright:: Copyright 2007 William Morgan
4 | ## License:: the same terms as ruby itself
5 |
6 | require 'date'
7 |
8 | module Trollop
9 |
10 | VERSION = "1.16.2"
11 |
12 | ## Thrown by Parser in the event of a commandline error. Not needed if
13 | ## you're using the Trollop::options entry.
14 | class CommandlineError < StandardError; end
15 |
16 | ## Thrown by Parser if the user passes in '-h' or '--help'. Handled
17 | ## automatically by Trollop#options.
18 | class HelpNeeded < StandardError; end
19 |
20 | ## Thrown by Parser if the user passes in '-h' or '--version'. Handled
21 | ## automatically by Trollop#options.
22 | class VersionNeeded < StandardError; end
23 |
24 | ## Regex for floating point numbers
25 | FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/
26 |
27 | ## Regex for parameters
28 | PARAM_RE = /^-(-|\.$|[^\d\.])/
29 |
30 | ## The commandline parser. In typical usage, the methods in this class
31 | ## will be handled internally by Trollop::options. In this case, only the
32 | ## #opt, #banner and #version, #depends, and #conflicts methods will
33 | ## typically be called.
34 | ##
35 | ## If you want to instantiate this class yourself (for more complicated
36 | ## argument-parsing logic), call #parse to actually produce the output hash,
37 | ## and consider calling it from within
38 | ## Trollop::with_standard_exception_handling.
39 | class Parser
40 |
41 | ## The set of values that indicate a flag option when passed as the
42 | ## +:type+ parameter of #opt.
43 | FLAG_TYPES = [:flag, :bool, :boolean]
44 |
45 | ## The set of values that indicate a single-parameter (normal) option when
46 | ## passed as the +:type+ parameter of #opt.
47 | ##
48 | ## A value of +io+ corresponds to a readable IO resource, including
49 | ## a filename, URI, or the strings 'stdin' or '-'.
50 | SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date]
51 |
52 | ## The set of values that indicate a multiple-parameter option (i.e., that
53 | ## takes multiple space-separated values on the commandline) when passed as
54 | ## the +:type+ parameter of #opt.
55 | MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates]
56 |
57 | ## The complete set of legal values for the +:type+ parameter of #opt.
58 | TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES
59 |
60 | INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
61 |
62 | ## The values from the commandline that were not interpreted by #parse.
63 | attr_reader :leftovers
64 |
65 | ## The complete configuration hashes for each option. (Mainly useful
66 | ## for testing.)
67 | attr_reader :specs
68 |
69 | ## Initializes the parser, and instance-evaluates any block given.
70 | def initialize *a, &b
71 | @version = nil
72 | @leftovers = []
73 | @specs = {}
74 | @long = {}
75 | @short = {}
76 | @order = []
77 | @constraints = []
78 | @stop_words = []
79 | @stop_on_unknown = false
80 |
81 | #instance_eval(&b) if b # can't take arguments
82 | cloaker(&b).bind(self).call(*a) if b
83 | end
84 |
85 | ## Define an option. +name+ is the option name, a unique identifier
86 | ## for the option that you will use internally, which should be a
87 | ## symbol or a string. +desc+ is a string description which will be
88 | ## displayed in help messages.
89 | ##
90 | ## Takes the following optional arguments:
91 | ##
92 | ## [+:long+] Specify the long form of the argument, i.e. the form with two dashes. If unspecified, will be automatically derived based on the argument name by turning the +name+ option into a string, and replacing any _'s by -'s.
93 | ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+.
94 | ## [+:type+] Require that the argument take a parameter or parameters of type +type+. For a single parameter, the value can be a member of +SINGLE_ARG_TYPES+, or a corresponding Ruby class (e.g. +Integer+ for +:int+). For multiple-argument parameters, the value can be any member of +MULTI_ARG_TYPES+ constant. If unset, the default argument type is +:flag+, meaning that the argument does not take a parameter. The specification of +:type+ is not necessary if a +:default+ is given.
95 | ## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Trollop::options) will have a +nil+ value for this key unless the argument is given on the commandline. The argument type is derived automatically from the class of the default value given, so specifying a +:type+ is not necessary if a +:default+ is given. (But see below for an important caveat when +:multi+: is specified too.) If the argument is a flag, and the default is set to +true+, then if it is specified on the the commandline the value will be +false+.
96 | ## [+:required+] If set to +true+, the argument must be provided on the commandline.
97 | ## [+:multi+] If set to +true+, allows multiple occurrences of the option on the commandline. Otherwise, only a single instance of the option is allowed. (Note that this is different from taking multiple parameters. See below.)
98 | ##
99 | ## Note that there are two types of argument multiplicity: an argument
100 | ## can take multiple values, e.g. "--arg 1 2 3". An argument can also
101 | ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2".
102 | ##
103 | ## Arguments that take multiple values should have a +:type+ parameter
104 | ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+
105 | ## value of an array of the correct type (e.g. [String]). The
106 | ## value of this argument will be an array of the parameters on the
107 | ## commandline.
108 | ##
109 | ## Arguments that can occur multiple times should be marked with
110 | ## +:multi+ => +true+. The value of this argument will also be an array.
111 | ## In contrast with regular non-multi options, if not specified on
112 | ## the commandline, the default value will be [], not nil.
113 | ##
114 | ## These two attributes can be combined (e.g. +:type+ => +:strings+,
115 | ## +:multi+ => +true+), in which case the value of the argument will be
116 | ## an array of arrays.
117 | ##
118 | ## There's one ambiguous case to be aware of: when +:multi+: is true and a
119 | ## +:default+ is set to an array (of something), it's ambiguous whether this
120 | ## is a multi-value argument as well as a multi-occurrence argument.
121 | ## In thise case, Trollop assumes that it's not a multi-value argument.
122 | ## If you want a multi-value, multi-occurrence argument with a default
123 | ## value, you must specify +:type+ as well.
124 |
125 | def opt name, desc="", opts={}
126 | raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
127 |
128 | ## fill in :type
129 | opts[:type] = # normalize
130 | case opts[:type]
131 | when :boolean, :bool; :flag
132 | when :integer; :int
133 | when :integers; :ints
134 | when :double; :float
135 | when :doubles; :floats
136 | when Class
137 | case opts[:type].name
138 | when 'TrueClass', 'FalseClass'; :flag
139 | when 'String'; :string
140 | when 'Integer'; :int
141 | when 'Float'; :float
142 | when 'IO'; :io
143 | when 'Date'; :date
144 | else
145 | raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
146 | end
147 | when nil; nil
148 | else
149 | raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
150 | opts[:type]
151 | end
152 |
153 | ## for options with :multi => true, an array default doesn't imply
154 | ## a multi-valued argument. for that you have to specify a :type
155 | ## as well. (this is how we disambiguate an ambiguous situation;
156 | ## see the docs for Parser#opt for details.)
157 | disambiguated_default =
158 | if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type]
159 | opts[:default].first
160 | else
161 | opts[:default]
162 | end
163 |
164 | type_from_default =
165 | case disambiguated_default
166 | when Integer; :int
167 | when Numeric; :float
168 | when TrueClass, FalseClass; :flag
169 | when String; :string
170 | when IO; :io
171 | when Date; :date
172 | when Array
173 | if opts[:default].empty?
174 | raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
175 | end
176 | case opts[:default][0] # the first element determines the types
177 | when Integer; :ints
178 | when Numeric; :floats
179 | when String; :strings
180 | when IO; :ios
181 | when Date; :dates
182 | else
183 | raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
184 | end
185 | when nil; nil
186 | else
187 | raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
188 | end
189 |
190 | raise ArgumentError, ":type specification and default type don't match (default type is #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default
191 |
192 | opts[:type] = opts[:type] || type_from_default || :flag
193 |
194 | ## fill in :long
195 | opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
196 | opts[:long] =
197 | case opts[:long]
198 | when /^--([^-].*)$/
199 | $1
200 | when /^[^-]/
201 | opts[:long]
202 | else
203 | raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
204 | end
205 | raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
206 |
207 | ## fill in :short
208 | opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
209 | opts[:short] = case opts[:short]
210 | when /^-(.)$/; $1
211 | when nil, :none, /^.$/; opts[:short]
212 | else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
213 | end
214 |
215 | if opts[:short]
216 | raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
217 | raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
218 | end
219 |
220 | ## fill in :default for flags
221 | opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
222 |
223 | ## autobox :default for :multi (multi-occurrence) arguments
224 | opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array)
225 |
226 | ## fill in :multi
227 | opts[:multi] ||= false
228 |
229 | opts[:desc] ||= desc
230 | @long[opts[:long]] = name
231 | @short[opts[:short]] = name if opts[:short] && opts[:short] != :none
232 | @specs[name] = opts
233 | @order << [:opt, name]
234 | end
235 |
236 | ## Sets the version string. If set, the user can request the version
237 | ## on the commandline. Should probably be of the form "
238 | ## ".
239 | def version s=nil; @version = s if s; @version end
240 |
241 | ## Adds text to the help display. Can be interspersed with calls to
242 | ## #opt to build a multi-section help page.
243 | def banner s; @order << [:text, s] end
244 | alias :text :banner
245 |
246 | ## Marks two (or more!) options as requiring each other. Only handles
247 | ## undirected (i.e., mutual) dependencies. Directed dependencies are
248 | ## better modeled with Trollop::die.
249 | def depends *syms
250 | syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
251 | @constraints << [:depends, syms]
252 | end
253 |
254 | ## Marks two (or more!) options as conflicting.
255 | def conflicts *syms
256 | syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
257 | @constraints << [:conflicts, syms]
258 | end
259 |
260 | ## Defines a set of words which cause parsing to terminate when
261 | ## encountered, such that any options to the left of the word are
262 | ## parsed as usual, and options to the right of the word are left
263 | ## intact.
264 | ##
265 | ## A typical use case would be for subcommand support, where these
266 | ## would be set to the list of subcommands. A subsequent Trollop
267 | ## invocation would then be used to parse subcommand options, after
268 | ## shifting the subcommand off of ARGV.
269 | def stop_on *words
270 | @stop_words = [*words].flatten
271 | end
272 |
273 | ## Similar to #stop_on, but stops on any unknown word when encountered
274 | ## (unless it is a parameter for an argument). This is useful for
275 | ## cases where you don't know the set of subcommands ahead of time,
276 | ## i.e., without first parsing the global options.
277 | def stop_on_unknown
278 | @stop_on_unknown = true
279 | end
280 |
281 | ## Parses the commandline. Typically called by Trollop::options,
282 | ## but you can call it directly if you need more control.
283 | ##
284 | ## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions.
285 | def parse cmdline=ARGV
286 | vals = {}
287 | required = {}
288 |
289 | opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
290 | opt :help, "Show this message" unless @specs[:help] || @long["help"]
291 |
292 | @specs.each do |sym, opts|
293 | required[sym] = true if opts[:required]
294 | vals[sym] = opts[:default]
295 | vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil
296 | end
297 |
298 | resolve_default_short_options
299 |
300 | ## resolve symbols
301 | given_args = {}
302 | @leftovers = each_arg cmdline do |arg, params|
303 | sym = case arg
304 | when /^-([^-])$/
305 | @short[$1]
306 | when /^--([^-]\S*)$/
307 | @long[$1]
308 | else
309 | raise CommandlineError, "invalid argument syntax: '#{arg}'"
310 | end
311 | raise CommandlineError, "unknown argument '#{arg}'" unless sym
312 |
313 | if given_args.include?(sym) && !@specs[sym][:multi]
314 | raise CommandlineError, "option '#{arg}' specified multiple times"
315 | end
316 |
317 | given_args[sym] ||= {}
318 |
319 | given_args[sym][:arg] = arg
320 | given_args[sym][:params] ||= []
321 |
322 | # The block returns the number of parameters taken.
323 | num_params_taken = 0
324 |
325 | unless params.nil?
326 | if SINGLE_ARG_TYPES.include?(@specs[sym][:type])
327 | given_args[sym][:params] << params[0, 1] # take the first parameter
328 | num_params_taken = 1
329 | elsif MULTI_ARG_TYPES.include?(@specs[sym][:type])
330 | given_args[sym][:params] << params # take all the parameters
331 | num_params_taken = params.size
332 | end
333 | end
334 |
335 | num_params_taken
336 | end
337 |
338 | ## check for version and help args
339 | raise VersionNeeded if given_args.include? :version
340 | raise HelpNeeded if given_args.include? :help
341 |
342 | ## check constraint satisfaction
343 | @constraints.each do |type, syms|
344 | constraint_sym = syms.find { |sym| given_args[sym] }
345 | next unless constraint_sym
346 |
347 | case type
348 | when :depends
349 | syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym }
350 | when :conflicts
351 | syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) }
352 | end
353 | end
354 |
355 | required.each do |sym, val|
356 | raise CommandlineError, "option --#{@specs[sym][:long]} must be specified" unless given_args.include? sym
357 | end
358 |
359 | ## parse parameters
360 | given_args.each do |sym, given_data|
361 | arg = given_data[:arg]
362 | params = given_data[:params]
363 |
364 | opts = @specs[sym]
365 | raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag
366 |
367 | vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
368 |
369 | case opts[:type]
370 | when :flag
371 | vals[sym] = !opts[:default]
372 | when :int, :ints
373 | vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
374 | when :float, :floats
375 | vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
376 | when :string, :strings
377 | vals[sym] = params.map { |pg| pg.map { |p| p.to_s } }
378 | when :io, :ios
379 | vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } }
380 | when :date, :dates
381 | vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } }
382 | end
383 |
384 | if SINGLE_ARG_TYPES.include?(opts[:type])
385 | unless opts[:multi] # single parameter
386 | vals[sym] = vals[sym][0][0]
387 | else # multiple options, each with a single parameter
388 | vals[sym] = vals[sym].map { |p| p[0] }
389 | end
390 | elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi]
391 | vals[sym] = vals[sym][0] # single option, with multiple parameters
392 | end
393 | # else: multiple options, with multiple parameters
394 | end
395 |
396 | ## modify input in place with only those
397 | ## arguments we didn't process
398 | cmdline.clear
399 | @leftovers.each { |l| cmdline << l }
400 |
401 | ## allow openstruct-style accessors
402 | class << vals
403 | def method_missing(m, *args)
404 | self[m] || self[m.to_s]
405 | end
406 | end
407 | vals
408 | end
409 |
410 | def parse_date_parameter param, arg #:nodoc:
411 | begin
412 | begin
413 | time = Chronic.parse(param)
414 | rescue NameError
415 | # chronic is not available
416 | end
417 | time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
418 | rescue ArgumentError => e
419 | raise CommandlineError, "option '#{arg}' needs a date"
420 | end
421 | end
422 |
423 | ## Print the help message to +stream+.
424 | def educate stream=$stdout
425 | width # just calculate it now; otherwise we have to be careful not to
426 | # call this unless the cursor's at the beginning of a line.
427 |
428 | left = {}
429 | @specs.each do |name, spec|
430 | left[name] = "--#{spec[:long]}" +
431 | (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") +
432 | case spec[:type]
433 | when :flag; ""
434 | when :int; " "
435 | when :ints; " "
436 | when :string; " "
437 | when :strings; " "
438 | when :float; " "
439 | when :floats; " "
440 | when :io; " "
441 | when :ios; " "
442 | when :date; " "
443 | when :dates; " "
444 | end
445 | end
446 |
447 | leftcol_width = left.values.map { |s| s.length }.max || 0
448 | rightcol_start = leftcol_width + 6 # spaces
449 |
450 | unless @order.size > 0 && @order.first.first == :text
451 | stream.puts "#@version\n" if @version
452 | stream.puts "Options:"
453 | end
454 |
455 | @order.each do |what, opt|
456 | if what == :text
457 | stream.puts wrap(opt)
458 | next
459 | end
460 |
461 | spec = @specs[opt]
462 | stream.printf " %#{leftcol_width}s: ", left[opt]
463 | desc = spec[:desc] + begin
464 | default_s = case spec[:default]
465 | when $stdout; ""
466 | when $stdin; ""
467 | when $stderr; ""
468 | when Array
469 | spec[:default].join(", ")
470 | else
471 | spec[:default].to_s
472 | end
473 |
474 | if spec[:default]
475 | if spec[:desc] =~ /\.$/
476 | " (Default: #{default_s})"
477 | else
478 | " (default: #{default_s})"
479 | end
480 | else
481 | ""
482 | end
483 | end
484 | stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
485 | end
486 | end
487 |
488 | def width #:nodoc:
489 | @width ||= if $stdout.tty?
490 | begin
491 | require 'curses'
492 | Curses::init_screen
493 | x = Curses::cols
494 | Curses::close_screen
495 | x
496 | rescue Exception
497 | 80
498 | end
499 | else
500 | 80
501 | end
502 | end
503 |
504 | def wrap str, opts={} # :nodoc:
505 | if str == ""
506 | [""]
507 | else
508 | str.split("\n").map { |s| wrap_line s, opts }.flatten
509 | end
510 | end
511 |
512 | ## The per-parser version of Trollop::die (see that for documentation).
513 | def die arg, msg
514 | if msg
515 | $stderr.puts "Error: argument --#{@specs[arg][:long]} #{msg}."
516 | else
517 | $stderr.puts "Error: #{arg}."
518 | end
519 | $stderr.puts "Try --help for help."
520 | exit(-1)
521 | end
522 |
523 | private
524 |
525 | ## yield successive arg, parameter pairs
526 | def each_arg args
527 | remains = []
528 | i = 0
529 |
530 | until i >= args.length
531 | if @stop_words.member? args[i]
532 | remains += args[i .. -1]
533 | return remains
534 | end
535 | case args[i]
536 | when /^--$/ # arg terminator
537 | remains += args[(i + 1) .. -1]
538 | return remains
539 | when /^--(\S+?)=(.*)$/ # long argument with equals
540 | yield "--#{$1}", [$2]
541 | i += 1
542 | when /^--(\S+)$/ # long argument
543 | params = collect_argument_parameters(args, i + 1)
544 | unless params.empty?
545 | num_params_taken = yield args[i], params
546 | unless num_params_taken
547 | if @stop_on_unknown
548 | remains += args[i + 1 .. -1]
549 | return remains
550 | else
551 | remains += params
552 | end
553 | end
554 | i += 1 + num_params_taken
555 | else # long argument no parameter
556 | yield args[i], nil
557 | i += 1
558 | end
559 | when /^-(\S+)$/ # one or more short arguments
560 | shortargs = $1.split(//)
561 | shortargs.each_with_index do |a, j|
562 | if j == (shortargs.length - 1)
563 | params = collect_argument_parameters(args, i + 1)
564 | unless params.empty?
565 | num_params_taken = yield "-#{a}", params
566 | unless num_params_taken
567 | if @stop_on_unknown
568 | remains += args[i + 1 .. -1]
569 | return remains
570 | else
571 | remains += params
572 | end
573 | end
574 | i += 1 + num_params_taken
575 | else # argument no parameter
576 | yield "-#{a}", nil
577 | i += 1
578 | end
579 | else
580 | yield "-#{a}", nil
581 | end
582 | end
583 | else
584 | if @stop_on_unknown
585 | remains += args[i .. -1]
586 | return remains
587 | else
588 | remains << args[i]
589 | i += 1
590 | end
591 | end
592 | end
593 |
594 | remains
595 | end
596 |
597 | def parse_integer_parameter param, arg
598 | raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
599 | param.to_i
600 | end
601 |
602 | def parse_float_parameter param, arg
603 | raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
604 | param.to_f
605 | end
606 |
607 | def parse_io_parameter param, arg
608 | case param
609 | when /^(stdin|-)$/i; $stdin
610 | else
611 | require 'open-uri'
612 | begin
613 | open param
614 | rescue SystemCallError => e
615 | raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}"
616 | end
617 | end
618 | end
619 |
620 | def collect_argument_parameters args, start_at
621 | params = []
622 | pos = start_at
623 | while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do
624 | params << args[pos]
625 | pos += 1
626 | end
627 | params
628 | end
629 |
630 | def resolve_default_short_options
631 | @order.each do |type, name|
632 | next unless type == :opt
633 | opts = @specs[name]
634 | next if opts[:short]
635 |
636 | c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) }
637 | if c # found a character to use
638 | opts[:short] = c
639 | @short[c] = name
640 | end
641 | end
642 | end
643 |
644 | def wrap_line str, opts={}
645 | prefix = opts[:prefix] || 0
646 | width = opts[:width] || (self.width - 1)
647 | start = 0
648 | ret = []
649 | until start > str.length
650 | nextt =
651 | if start + width >= str.length
652 | str.length
653 | else
654 | x = str.rindex(/\s/, start + width)
655 | x = str.index(/\s/, start) if x && x < start
656 | x || str.length
657 | end
658 | ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
659 | start = nextt + 1
660 | end
661 | ret
662 | end
663 |
664 | ## instance_eval but with ability to handle block arguments
665 | ## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html
666 | def cloaker &b
667 | (class << self; self; end).class_eval do
668 | define_method :cloaker_, &b
669 | meth = instance_method :cloaker_
670 | remove_method :cloaker_
671 | meth
672 | end
673 | end
674 | end
675 |
676 | ## The easy, syntactic-sugary entry method into Trollop. Creates a Parser,
677 | ## passes the block to it, then parses +args+ with it, handling any errors or
678 | ## requests for help or version information appropriately (and then exiting).
679 | ## Modifies +args+ in place. Returns a hash of option values.
680 | ##
681 | ## The block passed in should contain zero or more calls to +opt+
682 | ## (Parser#opt), zero or more calls to +text+ (Parser#text), and
683 | ## probably a call to +version+ (Parser#version).
684 | ##
685 | ## The returned block contains a value for every option specified with
686 | ## +opt+. The value will be the value given on the commandline, or the
687 | ## default value if the option was not specified on the commandline. For
688 | ## every option specified on the commandline, a key "