Commit 3b3c9598d0afbbfea46a6512ca005b76847f08ea

New command parser that takes care of a lot of things. Uses OptionParser instead of GetoptLong.

git-svn-id: svn+ssh://rubyforge.org/var/svn/piston/trunk@86 d6c2ea82-c31b-0410-8381-e9c44f9824c5

Commit diff

bin/piston

 
77end
88
99require 'piston'
10require 'piston/ui/command_line'
11
12Piston::Ui::CommandLine.start
10PistonCommandLineProcessor.parse_and_execute
toggle raw diff

lib/piston.rb

 
3030 require file
3131end
3232
33require "piston/version"
34require File.join(PISTON_ROOT, "transat", "parser")
35require File.join(PISTON_ROOT, 'piston', 'command')
36Dir[File.join(PISTON_ROOT, "piston", "commands", "*.rb")].each do |file|
37 require file.gsub(PISTON_ROOT, "")[1..-4]
38end
39
3340module Piston
3441 ROOT = "piston:root"
3542 UUID = "piston:uuid"
4545 LOCKED = "piston:locked"
4646end
4747
48require File.join(PISTON_ROOT, 'piston', 'command')
49require File.join(PISTON_ROOT, 'piston', 'command_error')
50require File.join(PISTON_ROOT, 'piston', 'ui', 'command_line')
48PistonCommandLineProcessor = Transat::Parser.new do
49 program_name "Piston"
50 version [Piston::VERSION::STRING]
51
52 option :verbose, :short => :v, :default => true, :message => "Show subversion commands and results as they are executed"
53 option :quiet, :short => :q, :default => false, :message => "Do not output any messages except errors"
54 option :revision, :short => :r, :param_name => "REVISION", :type => :int
55 option :show_updates, :short => :u, :message => "Query the remote repository for out of dateness information"
56 option :lock, :short => :l, :message => "Close down and lock the imported directory from further changes"
57 option :dry_run, :message => "Does not actually execute any commands"
58 option :force, :message => "Force the command to run, even if Piston thinks it would cause a problem"
59
60 command :switch, Piston::Commands::Switch, :valid_options => %w(lock dry_run force revision quiet verbose)
61 command :update, Piston::Commands::Update, :valid_options => %w(lock dry_run force revision quiet verbose)
62 command :import, Piston::Commands::Import, :valid_options => %w(lock dry_run force revision quiet verbose)
63 command :convert, Piston::Commands::Convert, :valid_options => %w(lock verbose dry_run)
64 command :unlock, Piston::Commands::Unlock, :valid_options => %w(force dry_run verbose)
65 command :lock, Piston::Commands::Lock, :valid_options => %w(force dry_run revision verbose)
66 command :status, Piston::Commands::Status, :valid_options => %w(show_updates verbose)
67end
toggle raw diff

lib/piston/command.rb

 
22 # The base class which all commands subclass to obtain services from.
33 class Command
44 attr_accessor :revision, :dry_run, :quiet, :verbose, :force, :lock,
5 :recursive, :logging_stream, :show_updates
5 :recursive, :show_updates
6 attr_reader :args
7 attr_writer :logging_stream
8
9 def initialize(non_options, options)
10 @args = non_options
11 options.each do |option, value|
12 self.send("#{option}=", value)
13 end
14 end
615
716 # Execute this command. The arguments are pre-processed to expand any
817 # wildcards using Dir#[]. This is because the Windows shell does not
toggle raw diff

lib/piston/commands/convert.rb

 
33module Piston
44 module Commands
55 class Convert < Piston::Command
6 def run(args)
6 def run
77 if args.empty? then
88 svn(:propget, '--recursive', 'svn:externals').each_line do |line|
99 next unless line =~ /^([^ ]+)\s-\s/
6565 "Converts existing svn:externals into Piston managed folders"
6666 end
6767
68 def self.detailed_help(stream)
69 stream.puts <<EOF
70convert: #{help}
68 def self.detailed_help
69 <<EOF
7170usage: convert [DIR [...]]
7271
7372 Converts folders which have the svn:externals property set to Piston managed
7473 folders.
75
76 Valid options:
77 --verbose : Show Subversion commands and results as they
78 are executed
79
8074EOF
8175 end
82
83 def self.aliases
84 %w(convert)
85 end
8676 end
8777 end
8878end
toggle raw diff

lib/piston/commands/help.rb

 
0module Piston
1 module Commands
2 class Help < Piston::Command
3 def run(targets=nil)
4 command = targets.shift
5
6 return help_on_command(command) if command
7 general_help
8 end
9
10 def help_on_command(command_name)
11 begin
12 require File.join(PISTON_ROOT, 'piston', 'commands', command_name)
13 command = Piston::Commands.const_get(command_name.capitalize)
14 command.detailed_help(logging_stream)
15 rescue LoadError
16 logging_stream.puts "No help available for '#{command_name}'"
17 general_help
18 end
19 end
20
21 def general_help
22 logging_stream.puts "Available commands are:"
23 commands = Array.new
24 Dir[File.join(PISTON_ROOT, 'piston', 'commands', '*.rb')].each do |file|
25 require file
26 commands << Piston::Commands.const_get(File.basename(file).gsub(/\.rb$/, '').capitalize)
27 end
28
29 commands.each do |command|
30 logging_stream.printf " %-12s %s\n", command.aliases.first, command.help
31 end
32 end
33
34 def self.help
35 "Returns detailed help on a specific command"
36 end
37
38 def self.aliases
39 %w(help)
40 end
41 end
42 end
43end
toggle raw diff

lib/piston/commands/import.rb

 
11module Piston
22 module Commands
33 class Import < Piston::Command
4 def run(args)
4 def run
55 raise Piston::CommandError, "Missing REPOS_URL argument" if args.empty?
66
77 repos, dir = args.shift, args.shift
5353 "Prepares a folder for merge tracking"
5454 end
5555
56 def self.detailed_help(stream)
57 stream.puts <<EOF
58import (init): #{help}
56 def self.detailed_help
57 <<EOF
5958usage: import REPOS_URL [DIR]
6059
6160 Exports the specified REPOS_URL (which must be a Subversion repository) to
6261 DIR, defaulting to the last component of REPOS_URL if DIR is not present.
6362
6463 If the local folder already exists, this command will abort with an error.
65
66 Valid options:
67 -r [--revision] arg : Start merge tracking at ARG instead of HEAD
68 --lock : Close down and lock the folder from future
69 updates immediately
70 --verbose : Show Subversion commands and results as they
71 are executed
72
7364EOF
7465 end
7566
7667 def self.aliases
77 %w(import init)
68 %w(init)
7869 end
7970 end
8071 end
toggle raw diff

lib/piston/commands/lock.rb

 
11module Piston
22 module Commands
33 class Lock < Piston::Command
4 def run(args)
4 def run
55 raise Piston::CommandError, "No targets to run against" if args.empty?
66
77 args.each do |dir|
1515 "Lock one or more folders to their current revision"
1616 end
1717
18 def self.detailed_help(stream)
19 stream.puts <<EOF
20lock: #{help}
18 def self.detailed_help
19 <<EOF
2120usage: lock DIR [DIR [...]]
2221
2322 Locked folders will not be updated to the latest revision when updating.
24
25 Valid options:
26 --verbose : Show Subversion commands and results as they
27 are executed
28
2923EOF
3024 end
31
32 def self.aliases
33 %w(lock)
34 end
3525 end
3626 end
3727end
toggle raw diff

lib/piston/commands/status.rb

 
33module Piston
44 module Commands
55 class Status < Piston::Command
6 def run(args)
6 def run
77 # First, find the list of pistoned folders
88 folders = svn(:propget, '--recursive', Piston::ROOT, *args)
99 repos = Hash.new
4949 "Determines the current status of each pistoned directory"
5050 end
5151
52 def self.detailed_help(stream)
53 stream.puts <<EOF
54status: #{help}
52 def self.detailed_help
53 <<EOF
5554usage: status [DIR [DIR...]]
5655
5756 Shows the status of one, many or all pistoned folders. The status is
6262
6363 The second column's values are blanks, unless the --show-updates is passed.
6464 M: Remotely modified since importing
65
66 Valid options:
67 --show-updates : Queries the remote repositories to determine
68 if they have been updated from our revision.
69
7065EOF
7166 end
7267
7368 def self.aliases
74 %w(status st)
69 %w(st)
7570 end
7671 end
7772 end
toggle raw diff

lib/piston/commands/switch.rb

 
11module Piston
22 module Commands
33 class Switch < Piston::Command
4 def run(args)
4 def run
55 new_root, dir = args.shift, args.shift
66 raise Piston::CommandError, "Expected two arguments only to switch. Unrecognized arguments: #{args.inspect}" unless args.empty?
77 switch(dir, new_root)
113113 "Switches a single directory to a new repository root"
114114 end
115115
116 def self.detailed_help(stream)
117 stream.puts <<EOF
118switch: #{help}
116 def self.detailed_help
117 <<EOF
119118usage: switch NEW_REPOSITORY_ROOT DIR
120119
121120 This operation changes the remote location from A to B, keeping local
125125 Piston will refuse to update a folder if it has pending updates. Run
126126 'svn update' on the target folder to update it before running Piston
127127 again.
128
129 Valid options:
130 -r [--revision] arg : Update to ARG instead of HEAD
131 --lock : Close down and lock the folder from future
132 updates immediately
133 --verbose : Show Subversion commands and results as they
134 are executed
135
136128EOF
137129 end
138130
139131 def self.aliases
140 %w(switch sw)
132 %w(sw)
141133 end
142134 end
143135 end
toggle raw diff

lib/piston/commands/unlock.rb

 
11module Piston
22 module Commands
33 class Unlock < Piston::Command
4 def run(args)
4 def run
55 raise Piston::CommandError, "No targets to run against" if args.empty?
66 svn :propdel, Piston::LOCKED, *args
77 args.each do |dir|
1313 "Undoes the changes enabled by lock"
1414 end
1515
16 def self.detailed_help(stream)
17 stream.puts <<EOF
18unlock: #{help}
16 def self.detailed_help
17 <<EOF
1918usage: unlock DIR [DIR [...]]
2019
2120 Unlocked folders are free to be updated to the latest revision when
2221 updating.
23
24 Valid options:
25 --verbose : Show Subversion commands and results as they
26 are executed
27
2822EOF
2923 end
30
31 def self.aliases
32 %w(unlock)
33 end
3424 end
3525 end
3626end
toggle raw diff

lib/piston/commands/update.rb

 
33module Piston
44 module Commands
55 class Update < Piston::Command
6 def run(args)
6 def run
77 (args.empty? ? find_targets : args).each do |dir|
88 update dir
99 end
121121 "Updates all or specified folders to the latest revision"
122122 end
123123
124 def self.detailed_help(stream)
125 stream.puts <<EOF
126update: #{help}
124 def self.detailed_help
125 <<EOF
127126usage: update [DIR [...]]
128127
129128 This operation has the effect of downloading all remote changes back to our
133133 Piston will refuse to update a folder if it has pending updates. Run
134134 'svn update' on the target folder to update it before running Piston
135135 again.
136
137 Valid options:
138 -r [--revision] arg : Update to ARG instead of HEAD
139 --lock : Close down and lock the folder from future
140 updates immediately
141 --verbose : Show Subversion commands and results as they
142 are executed
143
144136EOF
145137 end
146138
147139 def self.aliases
148 %w(update up)
140 %w(up)
149141 end
150142 end
151143 end
toggle raw diff

lib/piston/ui/command_line.rb

 
0require 'getoptlong'
1
2module Piston
3 module Ui
4 module CommandLine
5 def self.start
6 opts = ::GetoptLong.new(
7 [ '--revision', '-r', GetoptLong::REQUIRED_ARGUMENT ],
8 [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
9 [ '--dry-run', GetoptLong::NO_ARGUMENT ],
10 [ '--quiet', '-q', GetoptLong::NO_ARGUMENT ],
11 [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
12 [ '--show-updates', '-u', GetoptLong::NO_ARGUMENT ],
13 [ '--lock', GetoptLong::NO_ARGUMENT ],
14 [ '--force', '-f', GetoptLong::NO_ARGUMENT ],
15 [ '--version', GetoptLong::NO_ARGUMENT ]
16 )
17
18 options = Hash.new
19
20 opts.each do |opt, arg|
21 case opt
22 when '--revision'
23 options[:revision] = arg.to_i
24
25 when '--help'
26 return help
27
28 when '--version'
29 require 'piston/version'
30 puts "Piston #{Piston::VERSION::STRING}"
31 puts "Copyright 2006, Francois Beausoleil"
32 puts "\nSee the LICENSE file for details"
33 exit
34
35 when /--([-\w]+)$/
36 options[$1.gsub('-', '_').to_sym] = true
37 end
38 end
39
40 return help if ARGV.empty?
41
42 command_name = ARGV.shift.downcase
43 begin
44 require File.join(PISTON_ROOT, 'piston', 'commands', command_name)
45 rescue LoadError
46 return help
47 end
48
49 command_class = Piston::Commands.const_get("#{command_name.capitalize}")
50 command = command_class.new
51 options.each do |key, value|
52 command.send "#{key}=", value
53 end
54
55 begin
56 command.execute(ARGV)
57 rescue Piston::CommandError
58 $stderr.puts "ERROR: #{$!.message}"
59 exit 1
60 end
61 end
62
63 def self.help
64 require File.join(PISTON_ROOT, 'piston', 'commands', 'help')
65 Piston::Commands::Help.new.run(ARGV)
66 end
67 end
68 end
69end
70
71Piston::Ui::CommandLine.start if $0 == __FILE__
toggle raw diff

lib/transat/parser.rb

 
1require "optparse"
2
3module Transat
4 class VersionNeeded < StandardError; end
5
6 class HelpNeeded < StandardError
7 attr_reader :command
8
9 def initialize(command)
10 @command = command
11 end
12 end
13
14 class NoCommandGiven < StandardError
15 def message
16 "No command given"
17 end
18 end
19
20 class UnknownOptions < StandardError
21 attr_reader :command
22
23 def initialize(command, unrecognized_options)
24 @command, @unrecognized_options = command, unrecognized_options
25 end
26
27 def message
28 "Command #{@command} does not accept options #{@unrecognized_options.join(", ")}"
29 end
30 end
31
32 class UnknownCommand < StandardError
33 def initialize(command, parser)
34 @command, @parser = command, parser
35 end
36
37 def message
38 "Unknown command: #{@command.inspect}"
39 end
40 end
41
42 class BaseCommand
43 attr_reader :non_options, :options
44 def initialize(non_options, options)
45 @non_options, @options = non_options, options
46 end
47 end
48
49 class VersionCommand < BaseCommand
50 def run
51 raise VersionNeeded
52 end
53 end
54
55 class HelpCommand < BaseCommand
56 def run
57 raise HelpNeeded.new(non_options.first)
58 end
59 end
60
61 class Parser
62 def initialize(&block)
63 @valid_options, @received_options, @commands = [], {}, {}
64 @option_parser = OptionParser.new
65
66 command(:help, Transat::HelpCommand)
67 command(:version, Transat::VersionCommand)
68 instance_eval(&block) if block_given?
69 end
70
71 def option(name, options={})
72 options[:long] = name.to_s.gsub("_", "-") unless options[:long]
73 @valid_options << name
74 @received_options[name] = nil
75
76 opt_args = []
77 opt_args << "-#{options[:short]}" if options.has_key?(:short)
78 opt_args << "--#{options[:long] || name}"
79 opt_args << "=#{options[:param_name]}" if options.has_key?(:param_name)
80 opt_args << options[:message]
81 case options[:type]
82 when :int, :integer
83 opt_args << Integer
84 when :float
85 opt_args << Float
86 when nil
87 # NOP
88 else
89 raise ArgumentError, "Option #{name} has a bad :type parameter: #{options[:type].inspect}"
90 end
91
92 @option_parser.on(*opt_args.compact) do |value|
93 @received_options[name] = value
94 end
95 end
96
97 def command(name, klass, options={})
98 @commands[name.to_s] = options.merge(:class => klass)
99 end
100
101 def parse_and_execute(args=ARGV)
102 begin
103 command, non_options = parse(args)
104 execute(command, non_options)
105 rescue HelpNeeded
106 $stderr.puts usage($!.command)
107 exit 1
108 rescue VersionNeeded
109 puts "#{program_name} #{version}"
110 exit 0
111 rescue NoCommandGiven, UnknownOptions, UnknownCommand
112 $stderr.puts "ERROR: #{$!.message}"
113 $stderr.puts usage($!.respond_to?(:command) ? $!.command : nil)
114 exit 1
115 end
116 end
117
118 def parse(args)
119 non_options = @option_parser.parse(args)
120 command = non_options.shift
121 raise NoCommandGiven unless command
122 return command, non_options
123 end
124
125 def execute(command, non_options)
126 found = false
127 @commands.each do |command_name, options|
128 command_klass = options[:class]
129 aliases = [command_name]
130 aliases += command_klass.aliases if command_klass.respond_to?(:aliases)
131 return command_klass.new(non_options, @received_options).run if aliases.include?(command)
132 end
133
134 raise UnknownCommand.new(command, self)
135 end
136
137 def usage(command=nil)
138 message = []
139
140 if command then
141 command_klass = @commands[command][:class]
142 help =
143 if command_klass.respond_to?(:aliases) then
144 "#{command} (#{command_klass.aliases.join(", ")})"
145 else
146 "#{command}"
147 end
148 help = "#{help}: #{command_klass.help}" if command_klass.respond_to?(:help)
149 message << help
150 message << command_klass.detailed_help if command_klass.respond_to?(:detailed_help)
151 message << ""
152 message << "Valid options:"
153 @option_parser.summarize(message)
154 else
155 message << "usage: #{program_name.downcase} <SUBCOMMAND> [OPTIONS] [ARGS...]"
156 message << "Type '#{program_name.downcase} help <SUBCOMMAND>' for help on a specific subcommand."
157 message << "Type '#{program_name.downcase} version' to get this program's version."
158 message << ""
159 message << "Available subcommands are:"
160 @commands.sort.each do |command, options|
161 command_klass = options[:class]
162 if command_klass.respond_to?(:aliases) then
163 message << " #{command} (#{command_klass.aliases.join(", ")})"
164 else
165 message << " #{command}"
166 end
167 end
168 end
169
170 message.map {|line| line.chomp}.join("\n")
171 end
172
173 def program_name(value=nil)
174 value ? @program_name = value : @program_name
175 end
176
177 def version(value=nil)
178 if value then
179 @version = value.respond_to?(:join) ? value.join(".") : value
180 else
181 @version
182 end
183 end
184
185 def self.parse_and_execute(args=ARGV, &block)
186 self.new(&block).parse_and_execute(args)
187 end
188 end
189end
toggle raw diff