Login
Artifact [0ff389b8ad]
Login

Artifact 0ff389b8ad9f04b8646b1077486b4e650895d28a13bb825d69cfcd3bb607f075:


#### 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