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.
- checksums.yaml +4 -4
- data/lib/option.rb +127 -90
- 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,108 +1,145 @@
|
|
|
1
|
-
# typed: strict
|
|
2
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
3
|
|
|
4
4
|
require 'sorbet-runtime'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
81
|
+
# 実行時にバリデーションと変換をまとめて行う
|
|
82
|
+
sig { params(value: T.nilable(String)).returns(T.untyped) }
|
|
83
|
+
def process(value)
|
|
84
|
+
return @default if value.nil?
|
|
65
85
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
end
|
|
86
|
+
# 1. バリデーションの実行
|
|
87
|
+
@validators.each do |v|
|
|
88
|
+
msg = v.call(value)
|
|
89
|
+
next if msg.nil?
|
|
71
90
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|