simple-cli-options 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/option.rb +127 -90
  3. data/lib/options.rb +161 -48
  4. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3dc23fc107b31257b80f1152cea570f9908946c7374c015637d735c28609779
4
- data.tar.gz: 13bb26bad01ba80f643b0d64fae26a19a7d6c6852ef034274809031c626b00a9
3
+ metadata.gz: ce3397d023615c8ad267915d0d1517da8bf0cdaee39396d120c03ca5294f8605
4
+ data.tar.gz: d083348e721fbc03f5818a506a680cecbfb244f715d3d7797ce1d8576d6aba9e
5
5
  SHA512:
6
- metadata.gz: 98c31149080eef46edf5109578b988717fdd1e4e28a637f3b574e3ddc7af9203b44f99ea6c010d49ca012d2f0f58b1f8b7f37136212688a82222a5a8bd2a7416
7
- data.tar.gz: 1aea1b272cb5e6aac5bb004222f187030b09eb66cfaa4dd358cc5d60e63173e6e34b064f4e1a5bb808e43a1cc7f15089d9af56969a8537e814f53fc69020f4dd
6
+ metadata.gz: 8a41c2323a41059d2d3e71db6f981ca02007c0660cd5a6014ff34181508c403eae7a57b4a6b1283f05dea299d3c77b1aa1ec0ab6a21a1636028304ebb09a1ecc
7
+ data.tar.gz: 927616b95925f141dd3c569cecbda0d802f03deab01d967b51a9279a03b868c50f0cb6bdf660e875a7dc2e1fd066d30fa69b24910b1dba920e4befa479b31a59
data/lib/option.rb CHANGED
@@ -1,108 +1,145 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
2
+ # typed: strict
3
3
 
4
4
  require 'sorbet-runtime'
5
- require 'stringio'
6
-
7
- # ==========================================
8
- # 1. Option クラス(各フラグの定義と検証・変換)
9
- # ==========================================
10
- class Option
11
- extend T::Sig
12
-
13
- sig { returns(Symbol) }
14
- attr_reader :name
15
-
16
- sig { returns(String) }
17
- attr_reader :short, :long, :desc
18
-
19
- sig { returns(T::Boolean) }
20
- attr_reader :required_flag
21
-
22
- sig do
23
- params(
24
- name: Symbol,
25
- short: String,
26
- long: String,
27
- desc: String,
28
- required: T::Boolean,
29
- options: T.untyped
30
- ).void
31
- end
32
- def initialize(name, short:, long:, desc:, required: false, **options)
33
- @name = name
34
- @short = short
35
- @long = long
36
- @desc = desc
37
- @required_flag = required
38
-
39
- validate_structure!
40
-
41
- # バリデーションは追加(Array)可能、変換は上書き
42
- # 各バリデータは、成功時に nil、失敗時にエラーメッセージ(String)を返す想定
43
- raw_validators = options[:validate]
44
- validators = T.let(
45
- [],
46
- T::Array[T.proc.params(arg0: String).returns(T.nilable(String))]
47
- )
48
-
49
- unless raw_validators.nil?
50
- case raw_validators
51
- when Proc
52
- validators << raw_validators
53
- when Array
54
- raw_validators.each do |v|
55
- validators << v
56
- end
5
+
6
+ module SimpleOptions
7
+ class Option
8
+ extend T::Sig
9
+
10
+ sig { returns(Symbol) }
11
+ attr_reader :name
12
+
13
+ sig { returns(String) }
14
+ attr_reader :short, :long, :desc
15
+
16
+ sig { returns(T::Boolean) }
17
+ attr_reader :required_flag
18
+
19
+ sig { returns(T.untyped) }
20
+ attr_reader :default
21
+
22
+ sig { returns(Symbol) }
23
+ attr_reader :type
24
+
25
+ BOOLEAN_MAP = T.let({
26
+ 'true' => true, 't' => true, '1' => true, 'yes' => true, 'y' => true,
27
+ 'false' => false, 'f' => false, '0' => false, 'no' => false, 'n' => false
28
+ }.freeze, T::Hash[String, T::Boolean])
29
+
30
+ sig do
31
+ params(
32
+ name: Symbol,
33
+ desc: String,
34
+ short: String,
35
+ long: String,
36
+ required: T::Boolean,
37
+ default: T.untyped,
38
+ type: Symbol,
39
+ options: T.untyped
40
+ ).void
41
+ end
42
+ def initialize(name, desc:, short: '', long: '', required: false, default: nil, type: :string, **options)
43
+ @name = name
44
+ @desc = desc
45
+ @required_flag = required
46
+ @default = default
47
+ @type = type
48
+
49
+ # short/longが両方空の場合、-nameを使用
50
+ if short.empty? && long.empty?
51
+ @short = "-#{name}"
52
+ @long = ''
57
53
  else
58
- raise ArgumentError, "validate must be a Proc or an Array of Procs"
54
+ @short = short
55
+ @long = long
59
56
  end
57
+
58
+ validate_structure!
59
+
60
+ # バリデーション用Procの配列
61
+ @validators = T.let([], T::Array[T.proc.params(arg0: String).returns(T.nilable(String))])
62
+
63
+ # 型に基づいたデフォルトのバリデーションを追加
64
+ setup_default_validators
65
+
66
+ # カスタムバリデーターの取り込み
67
+ raw_validators = options[:validate]
68
+ if raw_validators.is_a?(Proc)
69
+ @validators << raw_validators
70
+ elsif raw_validators.is_a?(Array)
71
+ raw_validators.each { |v| @validators << v if v.is_a?(Proc) }
72
+ end
73
+
74
+ # 変換用Proc
75
+ @converter = T.let(
76
+ options[:convert] || ->(v) { convert_by_type(v) },
77
+ T.proc.params(arg0: String).returns(T.untyped)
78
+ )
60
79
  end
61
80
 
62
- @validators = validators
63
- @converter = T.let(options[:convert] || ->(v) { v }, T.proc.params(arg0: String).returns(T.untyped))
64
- end
81
+ # 実行時にバリデーションと変換をまとめて行う
82
+ sig { params(value: T.nilable(String)).returns(T.untyped) }
83
+ def process(value)
84
+ return @default if value.nil?
65
85
 
66
- sig { params(block: T.proc.params(arg0: String).returns(T.nilable(String))).returns(T.self_type) }
67
- def validate(&block)
68
- @validators << block
69
- self
70
- end
86
+ # 1. バリデーションの実行
87
+ @validators.each do |v|
88
+ msg = v.call(value)
89
+ next if msg.nil?
71
90
 
72
- sig { params(block: T.proc.params(arg0: String).returns(T.untyped)).returns(T.self_type) }
73
- def convert(&block)
74
- @converter = block
75
- self
76
- end
91
+ flags = [@short, @long].reject(&:empty?)
92
+ flags_part = flags.empty? ? "option '#{@name}'" : "#{flags.join(' or ')} option"
93
+ raise ArgumentError, "Validation failed: #{msg} for #{flags_part}"
94
+ end
77
95
 
78
- sig { params(value: String).returns(T.untyped) }
79
- def process(value)
80
- @validators.each do |v|
81
- msg = v.call(value)
82
- next if msg.nil?
83
-
84
- flags = [@short, @long].reject(&:empty?)
85
- flags_part =
86
- if flags.empty?
87
- "option '#{@name}'"
88
- else
89
- "#{flags.join(' or ')} option"
90
- end
91
-
92
- raise ArgumentError, "#{msg} for #{flags_part}"
96
+ # 2. 変換の実行
97
+ @converter.call(value)
93
98
  end
94
99
 
95
- @converter.call(value)
96
- end
100
+ # ビルダーパターン用メソッド
101
+ sig { params(block: T.proc.params(arg0: String).returns(T.nilable(String))).returns(T.self_type) }
102
+ def validate(&block)
103
+ @validators << block
104
+ self
105
+ end
106
+
107
+ private
108
+
109
+ sig { void }
110
+ def setup_default_validators
111
+ case @type
112
+ when :integer
113
+ @validators << ->(v) { v.match?(/\A-?\d+\z/) ? nil : 'must be an integer' }
114
+ when :number
115
+ @validators << ->(v) { v.match?(/\A-?\d+(\.\d+)?\z/) ? nil : 'must be a number' }
116
+ end
117
+ end
97
118
 
98
- private
119
+ sig { params(value: String).returns(T.untyped) }
120
+ def convert_by_type(value)
121
+ case @type
122
+ when :integer then value.to_i
123
+ when :number then smart_number(value)
124
+ when :boolean then BOOLEAN_MAP.fetch(value.downcase, true)
125
+ else value.to_s
126
+ end
127
+ end
99
128
 
100
- sig { void }
101
- def validate_structure!
102
- raise ArgumentError, "Property 'name' and 'desc' cannot be empty." if @name.to_s.empty? || @desc.empty?
129
+ sig { params(value: String).returns(T.any(Integer, Float, String)) }
130
+ def smart_number(value)
131
+ num = Float(value)
132
+ (num % 1).zero? ? num.to_i : num
133
+ rescue ArgumentError, TypeError
134
+ @default || value
135
+ end
103
136
 
104
- return unless @short.empty? && @long.empty?
137
+ sig { void }
138
+ def validate_structure!
139
+ raise ArgumentError, "Property 'name' and 'desc' cannot be empty." if @name.to_s.empty? || @desc.empty?
140
+ return unless @short.empty? && @long.empty?
105
141
 
106
- raise ArgumentError, "At least one of 'short' or 'long' flags must be provided for option '#{@name}'."
142
+ raise ArgumentError, "At least one of 'short' or 'long' flags must be provided for option '#{@name}'."
143
+ end
107
144
  end
108
145
  end
data/lib/options.rb CHANGED
@@ -1,71 +1,184 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
2
+ # typed: strict
3
3
 
4
4
  require 'sorbet-runtime'
5
5
  require_relative 'option'
6
6
 
7
- # ==========================================
8
- # 2. Options クラス(オプションの管理と解析)
9
- # ==========================================
10
- class Options
11
- extend T::Sig
12
-
13
- sig { params(description: String).void }
14
- def initialize(description: '')
15
- @description = T.let(description, String)
16
- @options = T.let([], T::Array[Option])
17
- @values = T.let({}, T::Hash[Symbol, T.untyped])
18
- @program_name = T.let(File.basename($0), String)
19
- end
7
+ module SimpleOptions
8
+ class Options
9
+ extend T::Sig
20
10
 
21
- sig { params(option: Option).void }
22
- def add(option)
23
- @options << option
24
- end
11
+ # initialize で引数をオプション(nil許容)に変更
12
+ sig { params(program_name: T.nilable(String), description: T.nilable(String)).void }
13
+ def initialize(program_name: nil, description: nil)
14
+ # 指定がなければ実行スクリプト名を取得
15
+ @program_name = T.let(program_name || File.basename($PROGRAM_NAME), String)
16
+ @description = T.let(description, T.nilable(String))
17
+ @options = T.let([], T::Array[Option])
18
+ @values = T.let({}, T::Hash[Symbol, T.untyped])
19
+ end
20
+
21
+ sig { params(name: String).void }
22
+ def program_name(name) = @program_name = name
25
23
 
26
- sig { void }
27
- def show_help
28
- puts "#{@description}\n\n" unless @description.empty?
29
- puts "Usage:\n #{@program_name} [flags]\n\n"
30
- puts 'Flags:'
31
- @options.each do |opt|
32
- flags = [
33
- opt.short.empty? ? nil : opt.short,
34
- opt.long.empty? ? nil : opt.long
35
- ].compact.join(', ')
36
- printf " %-20s %s\n", flags, opt.desc
24
+ sig { params(desc: String).void }
25
+ def description(desc) = @description = desc
26
+
27
+ # --- 型明示的な定義メソッド ---
28
+
29
+ sig { params(name: Symbol, desc: String, short: String, long: String, required: T::Boolean, default: T.nilable(Integer)).returns(Option) }
30
+ def integer(name, desc: '', short: '', long: '', required: false, default: nil)
31
+ add_internal(name, desc, short, long, required, default || 0, type: :integer)
37
32
  end
38
- end
39
33
 
40
- sig { params(argv: T.nilable(T::Array[String])).void }
41
- def parse!(argv = nil)
42
- argv = T.let(argv || ARGV, T::Array[String])
34
+ sig { params(name: Symbol, desc: String, short: String, long: String, required: T::Boolean, default: T.nilable(T::Boolean)).returns(Option) }
35
+ def boolean(name, desc: '', short: '', long: '', required: false, default: nil)
36
+ add_internal(name, desc, short, long, required, default.nil? ? false : default, type: :boolean)
37
+ end
38
+
39
+ sig { params(name: Symbol, desc: String, short: String, long: String, required: T::Boolean, default: T.untyped).returns(Option) }
40
+ def number(name, desc: '', short: '', long: '', required: false, default: nil)
41
+ add_internal(name, desc, short, long, required, default || 0, type: :number)
42
+ end
43
+
44
+ sig { params(name: Symbol, desc: String, short: String, long: String, required: T::Boolean, default: T.nilable(String)).returns(Option) }
45
+ def string(name, desc: '', short: '', long: '', required: false, default: nil)
46
+ add_internal(name, desc, short, long, required, default || '', type: :string)
47
+ end
48
+
49
+ # 汎用的なoptionメソッド(typeを省略可能、デフォルトは:string)
50
+ sig { params(name: Symbol, desc: String, short: String, long: String, required: T::Boolean, default: T.untyped, type: Symbol).returns(Option) }
51
+ def option(name, desc: '', short: '', long: '', required: false, default: nil, type: :string)
52
+ add_internal(name, desc, short, long, required, default, type: type)
53
+ end
54
+
55
+ # --- ヘルプ表示(レイアウト改良版) ---
56
+
57
+ sig { void }
58
+ def show_help
59
+ # 1. プログラム名
60
+ puts @program_name
43
61
 
44
- if argv.include?('-h') || argv.include?('--help')
45
- show_help
46
- exit 0
62
+ # 2. 説明文(存在する場合のみ、改行を入れて表示)
63
+ if @description && !@description.to_s.empty?
64
+ puts ''
65
+ puts @description
66
+ end
67
+
68
+ # 3. Usage
69
+ puts ''
70
+ puts 'Usage:'
71
+ puts " #{@program_name} [flags]"
72
+
73
+ # 4. Flags
74
+ puts ''
75
+ puts 'Flags:'
76
+ @options.each do |opt|
77
+ short_part = opt.short.empty? ? ' ' : "#{opt.short},"
78
+ long_part = opt.long.empty? ? '' : " #{opt.long}"
79
+
80
+ flag_str = " #{short_part}#{long_part}"
81
+ printf "%-25<flag>s %<desc>s\n", flag: flag_str, desc: opt.desc
82
+ end
47
83
  end
48
84
 
49
- @options.each do |opt|
50
- # short または long に一致する引数を探す
51
- idx = argv.find_index { |arg| arg == opt.short || arg == opt.long }
85
+ # --- パース処理 ---
86
+
87
+ sig { params(argv: T.nilable(T::Array[String])).void }
88
+ def parse(argv = nil)
89
+ args = T.let(argv || ARGV.dup, T::Array[String])
90
+
91
+ # ヘルプフラグが指定されている場合は、他のオプションをパースせず即座にヘルプを表示
92
+ if args.include?('-h') || args.include?('--help')
93
+ show_help
94
+ exit 0
95
+ end
96
+
97
+ @options.each { |opt| @values[opt.name] = opt.default }
98
+
99
+ while args.any?
100
+ arg = T.must(args.shift)
101
+
102
+ unless arg.start_with?('-')
103
+ (@values[:args] ||= []) << arg
104
+ next
105
+ end
106
+
107
+ opt = @options.find { |o| o.short == arg || o.long == arg }
108
+
109
+ if opt.nil?
110
+ warn "Error: Unknown option '#{arg}'"
111
+ puts ''
112
+ show_help
113
+ exit 1
114
+ end
115
+
116
+ # 既に値が設定されている場合はスキップ(最初の出現を優先)
117
+ if @values.key?(opt.name) && @values[opt.name] != opt.default
118
+ # 値を消費する必要がある
119
+ if opt.type == :boolean
120
+ next_arg = args.first
121
+ args.shift if next_arg && Option::BOOLEAN_MAP.key?(next_arg.downcase)
122
+ else
123
+ args.shift
124
+ end
125
+ next
126
+ end
52
127
 
53
- if idx && argv[idx + 1]
54
128
  begin
55
- @values[opt.name] = opt.process(T.must(argv[idx + 1]))
129
+ if opt.type == :boolean
130
+ next_arg = args.first
131
+ @values[opt.name] = if next_arg && Option::BOOLEAN_MAP.key?(next_arg.downcase)
132
+ opt.process(T.must(args.shift))
133
+ else
134
+ true
135
+ end
136
+ else
137
+ val = args.shift
138
+ if val.nil? || val.start_with?('-')
139
+ warn "Error: Missing value for option '#{arg}'"
140
+ exit 1
141
+ end
142
+ @values[opt.name] = opt.process(val)
143
+ end
56
144
  rescue ArgumentError => e
57
145
  warn "Error: #{e.message}"
58
146
  exit 1
59
147
  end
60
- elsif opt.required_flag
61
- warn "Error: Missing required option: #{opt.long.empty? ? opt.short : opt.long}"
62
- exit 1
148
+ end
149
+
150
+ @options.each do |opt|
151
+ if opt.required_flag && (@values[opt.name] == opt.default || @values[opt.name].nil?)
152
+ warn "Error: Missing required option: #{opt.long.empty? ? opt.short : opt.long}"
153
+ exit 1
154
+ end
63
155
  end
64
156
  end
65
- end
66
157
 
67
- sig { params(name: Symbol).returns(T.untyped) }
68
- def get(name)
69
- @values[name]
158
+ sig { params(name: Symbol).returns(T.untyped) }
159
+ def get(name)
160
+ @values[name]
161
+ end
162
+
163
+ # addメソッド(Optionオブジェクトを直接追加)
164
+ sig { params(option: Option).returns(Option) }
165
+ def add(option)
166
+ @options << option
167
+ option
168
+ end
169
+
170
+ private
171
+
172
+ sig do
173
+ params(
174
+ name: Symbol, desc: String, short: String, long: String,
175
+ required: T::Boolean, default: T.untyped, type: Symbol
176
+ ).returns(Option)
177
+ end
178
+ def add_internal(name, desc, short, long, required, default, type: :string)
179
+ opt = Option.new(name, desc: desc, short: short, long: long, required: required, default: default, type: type)
180
+ @options << opt
181
+ opt
182
+ end
70
183
  end
71
184
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple-cli-options
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - HIROAKI SATOU
@@ -42,7 +42,7 @@ licenses:
42
42
  metadata:
43
43
  homepage_uri: https://github.com/hiroakisatou/simple-options
44
44
  source_code_uri: https://github.com/hiroakisatou/simple-options
45
- changelog_uri: https://github.com/hiroakisatou/simple-options/blob/main/README.md
45
+ changelog_uri: https://github.com/hiroakisatou/simple-options/blob/main/CHANGELOG.md
46
46
  rdoc_options: []
47
47
  require_paths:
48
48
  - lib
@@ -57,7 +57,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
57
57
  - !ruby/object:Gem::Version
58
58
  version: '0'
59
59
  requirements: []
60
- rubygems_version: 4.0.6
60
+ rubygems_version: 3.6.9
61
61
  specification_version: 4
62
62
  summary: A small Ruby library for parsing command-line flags (short and long options
63
63
  with values).