{"id":19933023,"url":"https://github.com/kontena/opto","last_synced_at":"2025-05-03T11:33:14.405Z","repository":{"id":56886971,"uuid":"72852906","full_name":"kontena/opto","owner":"kontena","description":"Option parser and validator","archived":false,"fork":false,"pushed_at":"2018-05-30T09:56:28.000Z","size":173,"stargazers_count":0,"open_issues_count":3,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-18T12:34:39.330Z","etag":null,"topics":["argument-parser","option-parser","ruby","ruby-gem"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kontena.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-11-04T14:02:50.000Z","updated_at":"2018-05-30T09:56:26.000Z","dependencies_parsed_at":"2022-08-20T14:31:23.149Z","dependency_job_id":null,"html_url":"https://github.com/kontena/opto","commit_stats":null,"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kontena%2Fopto","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kontena%2Fopto/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kontena%2Fopto/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kontena%2Fopto/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kontena","download_url":"https://codeload.github.com/kontena/opto/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252185508,"owners_count":21708160,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["argument-parser","option-parser","ruby","ruby-gem"],"created_at":"2024-11-12T23:12:25.202Z","updated_at":"2025-05-03T11:33:14.097Z","avatar_url":"https://github.com/kontena.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Opto\n\n[![Build Status](https://travis-ci.org/kontena/opto.svg?branch=master)](https://travis-ci.org/kontena/opto)\n\nAn option parser, built for generating options from YAML based [Kontena](https://github.com/kontena/kontena/) stack\ndefinition files, but can be just as well used for other things, such as an API input validator.\n\nThe values for options can be resolved from files, env-variables, custom interactive prompts, random generators, etc.\n\nThe option type handlers can perform validations, such as defining a range or length requirements.\n\nTransformations can be performed on values, such as upcasing strings or removing white space.\n\nOptions can have simple conditionals for determining if it needs to be processed or not, for example: an option for defining a database\npassword can be processed only if a database has been selected.\n\nFinally the value for the option can be placed to some destination, such as an environment variable or sent to a command.\n\n## Installation\n\n```ruby\n# gem install opto\n\nrequire 'opto'\n```\n\n## YAML definition examples:\n\n```yaml\n# Enum type\n  remote_driver:\n    type: \"enum\"\n    required: true\n    label: \"Remote Driver\"\n    description: \"Remote Git and Auth scheme\"\n    options:\n      - github\n      - bitbucket\n      - gitlab\n      - gogs\n```\n\n```yaml\n# String validation and transformation\n  foo_username:\n    type: string\n    required: true\n    validate:\n      min_length: 1\n      max_length: 30\n    transform:\n      - strip # remove leading / trailing whitespace\n      - upcase # make UPCASE\n    from:\n      env: FOO_USER # read value from ENV variable FOO_USER\n```\n\n```yaml\n# Enum with prettier item descriptions\n  name: foo_os\n    type: enum\n    can_be_other: true # otherwise value has to be one of the options to be valid.\n    options:\n     - value: coreos\n       label: CoreOS\n       description: CoreOS Stable\n     - value: ubuntu\n       label: Ubuntu\n       description: Ubuntu Bubuntu\n```\n\n```yaml\n# Integer with default value and allowed range\n  foo_instances:\n    type: integer\n    default: 1\n    validate:\n      min: 1\n      max: 30\n```\n\n```yaml\n# Uri validator\n  host_url:\n    type: uri\n    validate:\n      schemes:\n        - file # only allow file:/// uris\n```\n\n## Resolvers\n\nSimple so far. Now let's mix in \"resolvers\" which can fetch the value from a number of sources or even generate new data:\n\n```yaml\n# Generate random strings\n  vault_iv:\n    type: string\n    from:\n      random_string:\n        length: 64\n        charset: ascii_printable # Other charsets include hex, hex_upcase, alphanumeric, etc.\n```\n\n```yaml\n# Try to get value from multiple sources\n  aws_secret:\n    type: string\n    strip: true # removes any leading / trailing whitespace from a string\n    upcase: true # turns the string to upcase\n    from:\n      env: 'FOOFOO'\n      file: /tmp/aws_secret.txt  # if env is not set, try to read it from this file, raises if not readable\n\n  aws_secret:\n    type: string\n    strip: true # removes any leading / trailing whitespace from a string\n    upcase: true # turns the string to upcase\n    from:\n      env: FOOFOO\n      file:  # if env is not set, try to read it from this file, returns nil if not readable\n        path: /tmp/aws_secret.txt\n        ignore_errors: true\n      random_string: 30 # not there either, generate a random string.\n```\n\n## Setters\n\nOk, so what to do with the values? There's setters for that.\n\n```yaml\n  aws_secret:\n    type: string\n    from:\n      env: AWS_TOKEN\n    to:\n      env: AWS_SECRET_TOKEN # once a valid value is set, set it to this variable.\n\n# There aren't any more setters right now, but one could imagine setters such as\n# output to a file, interpolate into a file, run a command, etc.\n```\n\n## Conditionals\n\nThere's also support for conditionals\n\n```yaml\n  - name: foo\n    type: string\n    value: 'hello'\n  - name: bar\n    type: integer\n    only_if:       # only process if 'foo' has the value 'hello'\n      - foo: hello\n```\n\n```ruby\n group.option('bar').skip?\n =\u003e false\n group.option('foo').value = 'world'\n group.option('bar').skip?\n =\u003e true\n```\n\n```yaml\n  - name: bar\n    type: integer\n    skip_if:   # same but reverse, do not process if 'foo' has value 'hello'\n      - foo: 'hello'\n```\n\n```ruby\n group.option('foo').value = 'world'\n group.option('bar').skip?\n =\u003e false\n group.option('foo').value = 'hello'\n group.option('bar').skip?\n =\u003e true\n```\n\n```yaml\n # These work too:\n\n  - name: bar\n    type: integer\n    skip_if:\n      - foo: hello # AND\n      - baz: world\n\n  - name: bar\n    type: integer\n    only_if: foo   # process if foo is not null, false or 'false'\n\n  - name: bar\n    type: integer\n    only_if:\n      - foo   # foo is not null\n      - baz   # AND baz is not null\n```\n\n### Complex conditionals\n\nYou can define more complicated conditionals by supplying a hash instead of a value:\n\n```yaml\n# value. same as foo == 5\nonly_if:\n  foo: 5\n```\n\n```yaml\n# hash. same as foo \u003e 5 \u0026\u0026 foo \u003c= 10\nonly_if:\n  foo:\n    gt: 5\n    lte: 10\n```\n\n### Complex conditional operators\n\n#### `lt`, `lte`\n\nless than / less than or equal to\n\n#### `gt`, `gte`\n\ngreater than / greater than or equal to\n\n#### `eq`, `ne`\n\nequals / not equals\n\n#### `start_with`, `end_with`\n\n\"foobar\" starts with \"foo\" and ends with \"bar\".\n\n#### `contain`\n\nCan be used for strings and arrays:\n\n```yaml\narr:\n  type: array\n  value:\n    - a\n    - b\n    - c\n\nstr:\n  type: string\n  value: foobar\n\nx:\n  type: boolean\n  from:\n    condition:\n      - if:\n        arr:\n          contain: b\n        str:\n          contain: b\n        then: true\n      - else: false\n      -\n# group.value_of('x')\n# =\u003e true\n```\n\n#### `any_of`\n\ntrue when the value is one of the supplied values. Input is either a comma separated string or an array.\n\n```yaml\nfoo:\n  skip_if:\n    bar:\n      any_of: foo,baz\n```\n\n```yaml\nfoo:\n  skip_if:\n    bar:\n      any_of:\n        - foo\n        - baz\n```\n\n## Examples\n\n```ruby\n# Read definitions from 'options' key inside a YAML:\nOpto.load('/tmp/stack.yml', :options)\n\n# Read definitions from root of YAML\nOpto.load('/tmp/stack.yml')\n\n# Create an option group:\nOpto.new( [ {name: 'foo', type: :string} ] )\n# or\ngroup = Opto::Group.new\ngroup.build_option(name: 'foo', type: :string, value: \"hello\")\ngroup.build_option(name: 'bar', type: :string, required: true)\ngroup.first\n=\u003e #\u003cOpto::Option:xxx\u003e\ngroup.size\n=\u003e 2\ngroup.each { .. }\ngroup.errors\n=\u003e { 'bar' =\u003e { :presence =\u003e \"Required value missing\" } }\ngroup.options_with_errors.each { ... }\ngroup.valid?\n=\u003e false\n```\n\n## Creating a custom resolver\n\nWant to prompt for values? Try something like this:\n\n```ruby\n# gem install tty-prompt\nrequire 'tty-prompt'\nclass Prompter \u003c Opto::Resolver\n  def resolve\n    # option = accessor to the option currently being resolved\n    # option.handler = accessor to the type handler\n    # hint = resolver options, for example the env variable name for env resolver, not used here.\n    return nil if option.skip?\n    if option.type == :enum\n      TTY::Prompt.new.select(\"Select #{option.label}\") do |menu|\n        option.handler.options[:options].each do |opt| # quite ugly way to access the option's value list definition\n          menu.choice opt[:label], opt[:value]\n        end\n      end\n    else\n      TTY::Prompt.new.ask(\"Enter value for #{option.label}\")\n    end\n  end\nend\n\n# And the option:\n- name: foo\n  type: enum\n  options:\n    - foo: Foo\n    - bar: Bar\n  from: prompter\n```\n\nYou can also use procs:\n\n```ruby\ngroup = Opto::Group.new(\n  resolvers: { prompt: proc { |hint, option| print \"Enter #{hint} (default: #{option.default}): \"; gets }\n)\n```\n\n## Subclassing a predefined type handler, setter, etc\n\n```ruby\nclass VersionNumber \u003c Opto::Types::String\n  Opto::Type.inherited(self) # need to call Opto::Type.inherited for registering the handler for now.\n\n  OPTIONS = Opto::Types::String::OPTIONS.merge(\n    min_version: nil,\n    max_version: nil\n  )\n\n  validate :min_version do |value|\n    if options[:min_version] \u0026\u0026 value \u003c options[:min_version]\n      \"Minimum version required: #{options[:min_version]}\"\n    end\n  end\n\n  validate :max_version do |value|\n    if options[:max_version] \u0026\u0026 value \u003e options[:max_version]\n      \"Maximum version: #{options[:max_version]}, yours is #{value}\"\n    end\n  end\n\n  sanitize :remove_build_info do |value|\n    value.split('+').first\n  end\nend\n\n# And to use:\n\u003e opt = Opto::Option.new(type: :version_number, name: 'foo', minimum_version: '1.0.0')\n\u003e opt.value = '0.1.0'\n\u003e opt.valid?\n=\u003e false\n\u003e opt.errors\n=\u003e { :validate_min_version =\u003e \"Minimum version required: 1.0.0\" }\n```\n\n## Default types\n\nGlobal validations:\n\n```yaml\n  in:  # only allow one of the following values\n    - a\n    - b\n    - c\n```\n\n### boolean\n\n```ruby\n{\n   truthy: ['true', 'yes', '1', 'on', 'enabled', 'enable'], # These strings will be turned into true\n   nil_is: false, # If the value is null, set to false\n   blank_is: false, # If the value is a blank string, set to false\n   false: 'false', # When outputting, emit this value when value is false\n   true: 'true',   # When outputting, emit this value when value is true\n   as: 'string'    # Output a string, can be 'boolean' or 'integer'\n}\n```\n\n### enum\n\n```ruby\n{\n  options: [],  # List of the possible option values\n  can_be_other: false  # Or allow values outside the option list\n}\n```\n\n### integer\n```ruby\n{\n  min: 0, # minimum value, can be negative\n  max: nil, # maximum value\n  nil_is_zero: false # null value will be turned into zero\n}\n```\n\n### string\n```ruby\n{\n  min_length: nil, # minimum length\n  max_length: nil, # maximum length\n  hexdigest: nil,  # hexdigest output. options: md5, sha1, sha256, sha384 or sha512.\n  empty_is_nil: true, # if string contains whitespace only, make value null\n  encode_64: false, # encode content to base64\n  decode_64: false, # decode content from base64\n  upcase: false, # convert to UPPERCASE\n  downcase: false, # convert to lowercase\n  strip: false, # remove leading/trailing whitespace,\n  chomp: false, # remove trailing linefeed\n  capitalize: false # convert to Capital case.\n}\n```\n\n### uri\n```ruby\n{\n  schemes: [ 'http', 'https' ] # only http and https urls are considered valid\n}\n```\n\n### array\n```ruby\n{\n  split: ',', # Use this pattern to split an incoming string into an array\n  join: false, # Set to a pattern such as ',' to output a comma separated string\n  empty_is_nil: false, # When true, an empty array will become nil\n  sort: false, # Sort the array before output\n  uniq: false, # Remove duplicates before output\n  count: false, # Instead of outputting the array, output the array size\n  compact: false # Remove nils before output\n}\n```\n\n### group\n\nAllows nesting of Opto::Groups:\n\n```yaml\nsubgroup:\n  type: group\n  value:\n    subvariable:\n      type: string\n      value: world\ngreeting:\n  type: string\n  from:\n    interpolate: hello, ${subgroup.subvariable} # becomes hello, world\n```\n\n## Default resolvers\nHint is the value that gets passed to the resolver when doing for example: `env: FOO` (FOO is the hint)\n\n### env\nHint is the environment variable name to read from. Defaults to the option's name.\n\nTo try multiple env variables, use:\n\n```yaml\nfrom:\n  env:\n    - KEY1\n    - KEY2\n```\n\n### file\nHint can be a string containing a path to the file, or a hash that defines `path: 'file_path', ignore_errors: true`\n\n### random_number\nHint must be a hash containing `min: minimum_number, max: maximum_number`\n\n### random_string\nHint can be a string/number that defines minimum length. Default charset is 'alphanumeric'\nHint can also be a hash that defines `length: length_of_generated_string, charset: 'charset_name'`\n\nDefined charsets:\n * numbers (0-9)\n * letters (a-z + A-Z)\n * downcase (a-z)\n * upcase (A-Z)\n * alphanumeric (0-9 + a-z + A-Z)\n * hex (0-9 + a-f)\n * hex_upcase (0-9 + A-F)\n * base64 (base64 charset (length has to be divisible by four when using base64))\n * ascii_printable (all printable ascii chars)\n * or a set of characters, for example: { length: 8, charset: '01' }  Will generate something like:  01001100\n\n### random_uuid\nIgnores hint completely.\n\nOutput is a 'random' UUID generated by `SecureRandom.uuid`, such as `78b6decf-e312-45a1-ac8c-d562270036ba`\n\n### variable\nHint is a name of another variable. Reads the value of another variable.\n\nExample:\n\n```yaml\ndb_host_a:\n  type: string\n  value: db.host.example.com\n\ndb_host_b:\n  type: string\n  from:\n    variable: db_host_a\n\n# group.value_of('db_host_b') =\u003e 'db.host.example.com'\n```\n\n### evaluate\nHint is a calculation. Uses values of other options to perform simple calculations.\n\nExample:\n\n```yaml\napples:\n  type: integer\n  value: 2\n\nbananas:\n  type: integer\n  value: 1\n\nfruits:\n  type: integer\n  from:\n    evaluate: ${apples} + ${bananas}\n\n# group.value_of('fruits') =\u003e 3\n```\n\n### interpolate\nHint is a template. Uses values from other options to build a string.\n\nExample:\n\n```yaml\nplace:\n  type: string\n  value: world\n\ngreeting:\n  type: string\n  from:\n    interpolate: Hello, ${place}!\n\n# group.value_of('greeting') =\u003e \"Hello, world!\"\n```\n\n### condition\n\nHint is an array containing if/then/elsif/else definitions. Sets the value based on the values of other variables.\n\nExample:\n```yaml\nint:\n  type: integer\n  value: 5\n\nstr:\n  type: string\n  from:\n    condition:\n      - if:\n          int: 5\n        then: \"five\"\n      - elsif:\n          int: 6\n        then: \"six\"\n      - else: \"not five or six\"\n```\n\n```ruby\ngroup.option('int').set(4)\ngroup.option('str').resolve\n=\u003e \"not five or six\"\ngroup.option('int').set(5)\ngroup.option('str').resolve\n=\u003e \"five\"\ngroup.option('int').set(6)\ngroup.option('str').resolve\n=\u003e \"six\"\n```\n\nWhen an \"else\" is not defined and none of the conditions match, a null value will be returned.\n\nThe syntax for conditionals and complex conditionals is documented above in the chapter about conditionals.\n\n### yaml\n\nHint is a hash defining filename or variable containing YAML source and optionally a key\n\nExample:\n```yaml\n# Read a string value from a key in YAML file\nstr:\n  type: string\n  from:\n    yaml:\n      file: .env\n      key: STR\n\n# Read an array from a nested key in a YAML file\nstr2:\n  type: array\n  from:\n    yaml:\n      file: variables.yml\n      key: variables.str2.value # assuming { variables: { str2: { value: [\"abcd\", \"defg\"] } } }\n\n# Read a YAML file into a string\nyaml_content:\n  type: string\n  from:\n    file: /etc/config.yml\n\n# Read YAML content from a variable and fetch a key from it\nstr3:\n  type: string\n  from:\n    yaml:\n      variable: yaml_content\n      key: settings.cpu_arch\n```\n\n\n## Default setters\n\n### env\nWorks exactly the same as env resolver, except in reverse.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/kontena/opto. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n\n## License\n\nThe gem is available as open source under the terms of the Apache License, Version 2.0. See [LICENSE](LICENSE.txt) for full license text.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkontena%2Fopto","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkontena%2Fopto","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkontena%2Fopto/lists"}