simple-cli-options 0.1.2 → 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.
- checksums.yaml +4 -4
- data/lib/option.rb +131 -78
- data/lib/options.rb +161 -48
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce3397d023615c8ad267915d0d1517da8bf0cdaee39396d120c03ca5294f8605
|
|
4
|
+
data.tar.gz: d083348e721fbc03f5818a506a680cecbfb244f715d3d7797ce1d8576d6aba9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a41c2323a41059d2d3e71db6f981ca02007c0660cd5a6014ff34181508c403eae7a57b4a6b1283f05dea299d3c77b1aa1ec0ab6a21a1636028304ebb09a1ecc
|
|
7
|
+
data.tar.gz: 927616b95925f141dd3c569cecbda0d802f03deab01d967b51a9279a03b868c50f0cb6bdf660e875a7dc2e1fd066d30fa69b24910b1dba920e4befa479b31a59
|
data/lib/option.rb
CHANGED
|
@@ -1,92 +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
|
-
@validators = T.let(
|
|
44
|
-
[options[:validate]].compact,
|
|
45
|
-
T::Array[T.proc.params(arg0: String).returns(T.nilable(String))]
|
|
46
|
-
)
|
|
47
|
-
@converter = T.let(options[:convert] || ->(v) { v }, T.proc.params(arg0: String).returns(T.untyped))
|
|
48
|
-
end
|
|
49
5
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 = ''
|
|
53
|
+
else
|
|
54
|
+
@short = short
|
|
55
|
+
@long = long
|
|
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
|
+
)
|
|
79
|
+
end
|
|
55
80
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
# 実行時にバリデーションと変換をまとめて行う
|
|
82
|
+
sig { params(value: T.nilable(String)).returns(T.untyped) }
|
|
83
|
+
def process(value)
|
|
84
|
+
return @default if value.nil?
|
|
85
|
+
|
|
86
|
+
# 1. バリデーションの実行
|
|
87
|
+
@validators.each do |v|
|
|
88
|
+
msg = v.call(value)
|
|
89
|
+
next if msg.nil?
|
|
61
90
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
flags_part =
|
|
70
|
-
if flags.empty?
|
|
71
|
-
"option '#{@name}'"
|
|
72
|
-
else
|
|
73
|
-
"#{flags.join(' or ')} option"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
raise ArgumentError, "#{msg} for #{flags_part}"
|
|
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
|
|
95
|
+
|
|
96
|
+
# 2. 変換の実行
|
|
97
|
+
@converter.call(value)
|
|
77
98
|
end
|
|
78
99
|
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
81
106
|
|
|
82
|
-
|
|
107
|
+
private
|
|
83
108
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
87
118
|
|
|
88
|
-
|
|
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
|
|
89
128
|
|
|
90
|
-
|
|
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
|
|
136
|
+
|
|
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?
|
|
141
|
+
|
|
142
|
+
raise ArgumentError, "At least one of 'short' or 'long' flags must be provided for option '#{@name}'."
|
|
143
|
+
end
|
|
91
144
|
end
|
|
92
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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/
|
|
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:
|
|
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).
|