├── .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 "