#### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU General Public License as published the Free
#### Software Foundation, either version 3 of the License, or (at your option)
#### any later version.
####
#### This program is distributed in the hope that it will be useful, but WITHOUT
#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#### FITNESS FOR A PARTICULAR PURPOSE.See the GNU General Public License for
#### more details.
####
#### You should have received a copy of the GNU General Public License along
#### with this program.If not, see<http:####www.gnu.org/licenses/.>
require "../strings"
module RemiLib::Args
# The base class for all Argument types for `ArgParser`.
abstract class Argument
# The full argument name. This is always prefixed with `"--"`.
getter longName : String = ""
# An `ArgCallbackFunc` that will be called at the end of parsing if the
# argument was called.
property callback : ArgCallbackFunc | Nil
# Returns `true` if the argument was called after parsing, or `false`
# otherwise.
getter? called : Bool = false
# :nodoc:
protected setter called
# The single `Char` name of the argument, or `nil`. For example, if an
# argument has the names `--foo` and '-f`, then the `#longName` is `"--foo"`
# and the `#shortName` is `f`.
property shortName : Char?
# The argument group this Argument is part of. This is mainly used for help
# printing.
property group : String
# The help string for this argument, as shown during help printing.
property help : String
# Initializes a new `Argument` subclass. The `#longName` is always
# required. Preprending a `"--"` prefix to the long name here is optional -
# if you don't add it yourself, it will be prepended for you by this method.
def initialize(newLongName, @shortName : Char? = nil, @group : String = "", @help : String = "")
raise "Argument long names cannot be blank" if newLongName.strip.empty?
if newLongName.starts_with?("--")
@longName = newLongName.downcase
else
@longName = "--#{newLongName}".downcase
end
@callback = nil
end
# Sets the long name of the argument. For example, if an argument has the
# names `--foo` and '-f`, then the `#longName` is `"--foo"` and the
# `#shortName` is `f`.
#
# If you do not include a prefix of `"--"`, it will be prepended for you.
@[AlwaysInline]
def longName=(newName : String) : Nil
newName = newName.strip
if newName.starts_with?("--")
@longName = newName.downcase
else
@longName = "--#{newName}".downcase
end
end
# :nodoc:
abstract def str : String
end
module ValArgument
abstract def value
abstract def value=(val) : Nil
abstract def setValue!(val) : Nil
end
module MultiArgument
property times : Int32 = 0
end
module MultiValArgument
property times : Int32 = 0
abstract def values
abstract def values=(vals) : Nil
abstract def setValues!(vals) : Nil
abstract def <<(newValue)
end
# A simple flag argument that always defaults to "off" and can be turned "on".
# If it's found on the command line, it's considered `#called`.
class FlagArgument < Argument
# Returns `"true"` if the argument was `#called`, or `"false"` otherwise.
def str : String
@called.to_s
end
end
# Similar to a `FlagArgument`, except that this can be called multiple times.
class MultiFlagArgument < Argument
include MultiArgument
# Returns the number of `#times` this argument was called as a String.
def str : String
@times.to_s
end
end
# An `Argument` that expects some sort of string value.
class StringArgument < Argument
include ValArgument
@value : String = ""
# A list of values that the argument is allowed to take. If the
# user provides a value that does not match any of these, an
# `ArgumentError` is raised during parsing.
property oneOf : Array(String) = [] of String
# Sets the value of the argument. This also sets `#called?` to `true`.
@[AlwaysInline]
def value=(val : String)
if !oneOf.empty? && !oneOf.includes?(val)
raise ArgumentError.new("#{longName} expects one of the following: #{@oneOf.join(", ")}")
end
@value = val
@called = true
end
# :ditto:
@[AlwaysInline]
def value=(val) : Nil
self.value = val.to_s
end
# Sets the value of the argument. This does not set `#called?` to `true`,
# and does not check `#oneOf`.
@[AlwaysInline]
def setValue!(val : String) : Nil
@value = val
end
# :inherit:
@[AlwaysInline]
def setValue!(val) : Nil
@value = val.to_s
end
# Returns the value of the argument.
@[AlwaysInline]
def value : String
@value
end
# Returns the value of the argument. This is interchangeable with `#value`
# for the `StringArgument` class.
@[AlwaysInline]
def str : String
@value
end
end
# Similar to a `StringArgument`, except that it can be called multiple times.
class MultiStringArgument < Argument
include MultiValArgument
@values = [] of String
# A list of values that the argument is allowed to take. If the
# user provides a value that does not match any of these, an
# `ArgumentError` is raised during parsing.
property oneOf : Array(String) = [] of String
# Appends a new value to the argument. This also sets `#called` to `true`,
# and increases `#times` by one.
@[AlwaysInline]
def <<(newVal : String)
if !oneOf.empty? && !oneOf.includes?(newVal)
raise ArgumentError.new("#{longName} expects one of the following: #{@oneOf.join(", ")}")
end
@values << newVal
@called = true
@times += 1
self
end
# :ditto:
@[AlwaysInline]
def <<(newValue)
self << val.to_s
end
# Sets the value of this argument. This also sets `#called` to `true` and
# sets `#times` to the length of `newVal`.
@[AlwaysInline]
def values=(vals : Array(String)) : Nil
if !oneOf.empty? && !(vals.all? { |str| oneOf.includes?(str) })
raise ArgumentError.new("#{longName} expects one of the following: #{@oneOf.join(", ")}")
end
@values = vals
@called = true
@times = vals.size
end
# :ditto:
@[AlwaysInline]
def values=(vals) : Nil
self.values = vals.as(Array(String))
end
# Sets the value of this argument. This does not set `#called` to `true`,
# but does set `#times` to the length of `newVal`. This does not check the
# value against `#oneOf`.
@[AlwaysInline]
def setValues!(newVal : Array(String)) : Nil
@values = newVal
end
# :ditto:
@[AlwaysInline]
def setValues!(vals) : Nil
setValues!(vals.as(Array(String)))
end
# Returns all values stored in this argument.
@[AlwaysInline]
def values
@values
end
# Returns all values stored in this argument as a string.
@[AlwaysInline]
def str : String
@values.to_s
end
end
# An `Argument` that expects an integer. This always stores its value as an
# `Int64`.
class IntArgument < Argument
include ValArgument
@value : Int64 = 0u64
# The minimum accepted value for this argument.
property minimum : Int64 = Int64::MIN
# The maximum accepted value for this argument.
property maximum : Int64 = Int64::MAX
# Sets the value of the argument. This is checked against the `#minimum`
# and `#maximum`. This sets `#called?` to `true`.
@[AlwaysInline]
def value=(val : Int64) : Nil
unless val >= @minimum && val <= @maximum
raise ArgumentError.new("#{longName} expects an integer between #{minimum} and #{maximum}")
end
@value = val
@called = true
end
# Sets the value of the argument. This is checked against the `#minimum`
# and `#maximum`. This sets `#called?` to `true`.
#
# `newVal` must respond to `#to_i64`.
@[AlwaysInline]
def value=(val) : Nil
if val.responds_to?(:to_i64)
begin
self.value = val.to_i64
rescue Exception
raise ArgumentError.new("#{longName} expects an integer between #{minimum} and #{maximum})")
end
else
raise "Cannot assign value to an IntArgument that does not respond to #to_i64"
end
end
# Sets the value of the argument. This does not set `#called?` to `true`,
# and does not check the `#minimum` and `#maximum`.
@[AlwaysInline]
def setValue!(val : Int64) : Nil
@value = val.to_i64!
end
# Sets the value of the argument. This does not set `#called?` to `true`,
# and does not check the `#minimum` and `#maximum`.
#
# `val` must respond to `#to_i64`.
@[AlwaysInline]
def setValue!(val) : Nil
if val.responds_to?(:to_i64)
begin
setValue!(val.to_i64)
rescue Exception
raise ArgumentError.new("#{longName} expects an integer between #{minimum} and #{maximum})")
end
else
raise "Cannot assign value to an IntArgument that does not respond to #to_i64"
end
end
# Returns the value of this argument.
def value : Int64
@value
end
# Returns the value of this argument as a string.
def str : String
@value.to_s
end
end
# Similar to an `IntArgument`, except that it can be called multiple times.
class MultiIntArgument < Argument
include MultiValArgument
@values = [] of Int64
# The minimum for all accepted values for this argument.
property minimum : Int64 = Int64::MIN
# The maximum for all accepted values for this argument.
property maximum : Int64 = Int64::MAX
# Appends `val` to this argument's values. `val` will be validated
# against `#constraint` first. This sets `#called` to `true`, and increases
# `#times` by one.
@[AlwaysInline]
def <<(val : Int64)
unless val >= @minimum && val <= @maximum
raise ArgumentError.new("#{longName} expects integers between #{minimum} and #{maximum}")
end
@values << val.to_i64!
@called = true
@times += 1
self
end
# Appends `newVal` to this argument's values. `newVal` will be validated
# against `#constraint` first. This sets `#called` to `true`, and increases
# `#times` by one.
#
# `newValue` must respond to `#to_i64`.
@[AlwaysInline]
def <<(newValue)
if newValue.responds_to?(:to_i64)
begin
self.<< newValue.to_i64
rescue Exception
raise ArgumentError.new("#{longName} expects an integer between #{minimum} and #{maximum})")
end
else
raise "Cannot assign a value to a MultiIntArgument that does not respond to #to_i64"
end
end
# Sets all of the values for this argument in one go. All arguments will be
# checked against the minimum and maximum. This sets `#called?` to `true`,
# and sets `#times`.
@[AlwaysInline]
def values=(vals : Array(Int64)) : Nil
unless vals.all? { |val| val >= @minimum && val <= @maximum }
raise ArgumentError.new("#{longName} expects integers between #{minimum} and #{maximum}")
end
@values = vals
@called = true
@times = vals.size
end
# :ditto:
@[AlwaysInline]
def values=(vals) : Nil
self.values = vals.as(Array(Int64))
end
# Sets all of the values for this argument in one go. All arguments will be
# checked against the minimum and maximum. This does not set `#called?` to
# `true`, but does set `#times`. It does not check `#minimum` and
# `#maximum`.
def setValues!(vals : Array(Int64)) : Nil
@values = vals
end
# :ditto:
@[AlwaysInline]
def setValues!(vals) : Nil
self.values! = vals.as(Array(Int64))
end
# Returns the values stored in this argument.
@[AlwaysInline]
def values
@values
end
# Returns all values stored in this argument as a string.
@[AlwaysInline]
def str : String
@values.to_s
end
end
# An `Argument` that expects a floating point number. This always stores its
# value as a `Float64`.
class FloatArgument < Argument
include ValArgument
@value : Float64 = 0.0f64
# The minimum accepted value for this argument.
property minimum : Float64 = Float64::MIN
# The maximum accepted value for this argument.
property maximum : Float64 = Float64::MAX
# Sets the value of the argument. This is checked against the `#minimum`
# and `#maximum`. This sets `#called?` to `true`.
@[AlwaysInline]
def value=(val : Float64) : Nil
unless val >= @minimum && val <= @maximum
raise ArgumentError.new("#{longName} expects a number between #{minimum} and #{maximum}")
end
@value = val
@called = true
end
# Sets the value of the argument. This is checked against the `#minimum`
# and `#maximum`. This sets `#called?` to `true`.
#
# `val` must respond to `#to_f`.
@[AlwaysInline]
def value=(val) : Nil
if val.responds_to?(:to_f64)
begin
self.value = val.to_f64
rescue Exception
raise ArgumentError.new("#{longName} expects a number between #{minimum} and #{maximum})")
end
@called = true
else
raise "Cannot assign a value to a FloatArgument that does not respond to #to_f"
end
end
# Sets the value of the argument. This is checked against the `#minimum`
# and `#maximum`. This does not set `#called?` to `true`.
@[AlwaysInline]
def setValue!(newVal : Float64) : Nil
@value = newVal.to_f64!
end
# Sets the value of the argument. This is checked against the `#minimum`
# and `#maximum`. This does not set `#called?` to `true`.
#
# `newVal` must respond to `#to_f`.
@[AlwaysInline]
def setValue!(val) : Nil
if val.responds_to?(:to_f)
begin
self.setValue!(val.to_f.to_f64)
rescue Exception
raise ArgumentError.new("#{longName} expects a number between #{minimum} and #{maximum})")
end
else
raise "Cannot assign a value to a FloatArgument that does not respond to #to_f"
end
end
# Returns this argument's value.
def value : Float64
@value
end
# Returns the value of this argument as a string.
def str : String
@value.to_s
end
end
# Similar to a `FloatArgument`, except that it can be called multiple times.
class MultiFloatArgument < Argument
include MultiValArgument
@values = [] of Float64
# The minimum for all accepted values for this argument.
property minimum : Float64 = Float64::MIN
# The maximum for all accepted values for this argument.
property maximum : Float64 = Float64::MAX
# Appends `newVal` to this argument's values. `newVal` will be validated
# against `#constraint` first. This sets `#called` to `true`, and increases
# `#times` by one.
@[AlwaysInline]
def <<(val : Float64)
unless val >= @minimum && val <= @maximum
raise ArgumentError.new("#{longName} expects numbers between #{minimum} and #{maximum}")
end
@values << val.to_f64!
@called = true
@times += 1
self
end
# Appends `newVal` to this argument's values. `newVal` will be validated
# against `#constraint` first. This sets `#called` to `true`, and increases
# `#times` by one.
#
# `newValue` must respond to `#to_f`.
@[AlwaysInline]
def <<(newValue)
if newValue.responds_to?(:to_f64)
begin
self.<< newValue.to_f64
rescue Exception
raise ArgumentError.new("#{longName} expects a number between #{minimum} and #{maximum})")
end
else
raise "Cannot assign a value to a MultiFloatArgument that does not respond to #to_f"
end
end
# Sets all of the values for this argument in one go. All arguments will be
# checked against the minimum and maximum. This sets `#called?` to `true`,
# and sets `#times`.
@[AlwaysInline]
def values=(vals : Array(Float64)) : Nil
unless vals.all? { |val| val >= @minimum && val <= @maximum }
raise ArgumentError.new("#{longName} expects numbers between #{minimum} and #{maximum}")
end
@values = vals
@called = true
@times = vals.size
end
# :ditto:
@[AlwaysInline]
def values=(vals) : Nil
self.values = vals.as(Array(Float64))
end
# Sets all of the values for this argument in one go. All arguments will be
# checked against the minimum and maximum. This does not set `#called?` to
# `true` and does not set `#times`.
def setValues!(vals : Array(Float64)) : Nil
@values = vals
end
# :ditto:
@[AlwaysInline]
def setValues!(vals) : Nil
self.setValues!(vals.as(Array(Float64)))
end
# Returns the values stored in this argument.
def values
@values
end
# Returns all values stored in this argument as a string.
def str : String
@values.to_s
end
end
end