File: cli.rb

package info (click to toggle)
ruby-hocon 1.4.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 768 kB
  • sloc: ruby: 7,903; makefile: 4
file content (225 lines) | stat: -rw-r--r-- 7,642 bytes parent folder | download
1
2
3
4
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
require 'optparse'
require_relative '../hocon'
require_relative '../hocon/version'
require_relative '../hocon/config_render_options'
require_relative '../hocon/config_factory'
require_relative '../hocon/config_value_factory'
require_relative '../hocon/parser/config_document_factory'
require_relative '../hocon/config_error'

module Hocon::CLI
  # Aliases
  ConfigMissingError = Hocon::ConfigError::ConfigMissingError
  ConfigWrongTypeError = Hocon::ConfigError::ConfigWrongTypeError

  # List of valid subcommands
  SUBCOMMANDS = ['get', 'set', 'unset']

  # For when a path can't be found in a hocon config
  class MissingPathError < StandardError
  end

  # Parses the command line flags and argument
  # Returns a options hash with values for each option and argument
  def self.parse_args(args)
    options = {}
    opt_parser = OptionParser.new do |opts|
      subcommands = SUBCOMMANDS.join(',')
      opts.banner = "Usage: hocon [options] {#{subcommands}} PATH [VALUE]\n\n" +
          "Example usages:\n" +
          "  hocon -i settings.conf -o new_settings.conf set some.nested.value 42\n" +
          "  hocon -f settings.conf set some.nested.value 42\n" +
          "  cat settings.conf | hocon get some.nested.value\n\n" +
          "Subcommands:\n" +
          "  get PATH - Returns the value at the given path\n" +
          "  set PATH VALUE - Sets or adds the given value at the given path\n" +
          "  unset PATH - Removes the value at the given path"

      opts.separator('')
      opts.separator('Options:')

      in_file_description = 'HOCON file to read/modify. If omitted, STDIN assumed'
      opts.on('-i', '--in-file HOCON_FILE', in_file_description) do |in_file|
        options[:in_file] = in_file
      end

      out_file_description = 'File to be written to. If omitted, STDOUT assumed'
      opts.on('-o', '--out-file HOCON_FILE', out_file_description) do |out_file|
        options[:out_file] = out_file
      end

      file_description = 'File to read/write to. Equivalent to setting -i/-o to the same file'
      opts.on('-f', '--file HOCON_FILE', file_description) do |file|
        options[:file] = file
      end

      json_description = "Output values from the 'get' subcommand in json format"
      opts.on('-j', '--json', json_description) do |json|
        options[:json] = json
      end

      opts.on_tail('-h', '--help', 'Show this message') do
        puts opts
        exit
      end

      opts.on_tail('-v', '--version', 'Show version') do
        puts Hocon::Version::STRING
        exit
      end
    end
    # parse! returns the argument list minus all the flags it found
    remaining_args = opt_parser.parse!(args)

    # Ensure -i and -o aren't used at the same time as -f
    if (options[:in_file] || options[:out_file]) && options[:file]
      exit_with_usage_and_error(opt_parser, "--file can't be used with --in-file or --out-file")
    end

    # If --file is used, set --in/out-file to the same file
    if options[:file]
      options[:in_file] = options[:file]
      options[:out_file] = options[:file]
    end

    no_subcommand_error(opt_parser) unless remaining_args.size > 0

    # Assume the first arg is the subcommand
    subcommand = remaining_args.shift
    options[:subcommand] = subcommand

    case subcommand
      when 'set'
        subcommand_arguments_error(subcommand, opt_parser) unless remaining_args.size >= 2
        options[:path] = remaining_args.shift
        options[:new_value] = remaining_args.shift

      when 'get', 'unset'
        subcommand_arguments_error(subcommand, opt_parser) unless remaining_args.size >= 1
        options[:path] = remaining_args.shift

      else
        invalid_subcommand_error(subcommand, opt_parser)
    end

    options
  end

  # Main entry point into the script
  # Calls the appropriate subcommand and handles errors raised from the subcommands
  def self.main(opts)
    hocon_text = get_hocon_file(opts[:in_file])

    begin
      case opts[:subcommand]
        when 'get'
          puts do_get(opts, hocon_text)
        when 'set'
          print_or_write(do_set(opts, hocon_text), opts[:out_file])
        when 'unset'
          print_or_write(do_unset(opts, hocon_text), opts[:out_file])
      end

    rescue MissingPathError
      exit_with_error("Can't find the given path: '#{opts[:path]}'")
    end

    exit
  end

  # Entry point for the 'get' subcommand
  # Returns a string representation of the the value at the path given on the
  # command line
  def self.do_get(opts, hocon_text)
    config = Hocon::ConfigFactory.parse_string(hocon_text)
    unless config.has_path?(opts[:path])
      raise MissingPathError.new
    end

    value = config.get_any_ref(opts[:path])

    render_options = Hocon::ConfigRenderOptions.defaults
    # Otherwise weird comments show up in the output
    render_options.origin_comments = false
    # If json is false, the hocon format is used
    render_options.json = opts[:json]
    # Output colons between keys and values
    render_options.key_value_separator = :colon

    Hocon::ConfigValueFactory.from_any_ref(value).render(render_options)
  end

  # Entry point for the 'set' subcommand
  # Returns a string representation of the HOCON config after adding/replacing
  # the value at the given path with the given value
  def self.do_set(opts, hocon_text)
    config_doc = Hocon::Parser::ConfigDocumentFactory.parse_string(hocon_text)
    modified_config_doc = config_doc.set_value(opts[:path], opts[:new_value])

    modified_config_doc.render
  end

  # Entry point for the 'unset' subcommand
  # Returns a string representation of the HOCON config after removing the
  # value at the given path
  def self.do_unset(opts, hocon_text)
    config_doc = Hocon::Parser::ConfigDocumentFactory.parse_string(hocon_text)
    unless config_doc.has_value?(opts[:path])
      raise MissingPathError.new
    end

    modified_config_doc = config_doc.remove_value(opts[:path])

    modified_config_doc.render
  end

  # If a file is provided, return it's contents. Otherwise read from STDIN
  def self.get_hocon_file(in_file)
    if in_file
      File.read(in_file)
    else
      STDIN.read
    end
  end

  # Print an error message and exit the program
  def self.exit_with_error(message)
    STDERR.puts "Error: #{message}"
    exit(1)
  end

  # Print an error message and usage, then exit the program
  def self.exit_with_usage_and_error(opt_parser, message)
    STDERR.puts opt_parser
    exit_with_error(message)
  end

  # Exits with an error saying there aren't enough arguments found for a given
  # subcommand. Prints the usage
  def self.subcommand_arguments_error(subcommand, opt_parser)
    error_message = "Too few arguments for '#{subcommand}' subcommand"
    exit_with_usage_and_error(opt_parser, error_message)
  end

  # Exits with an error for when no subcommand is supplied on the command line.
  # Prints the usage
  def self.no_subcommand_error(opt_parser)
    error_message = "Must specify subcommand from [#{SUBCOMMANDS.join(', ')}]"
    exit_with_usage_and_error(opt_parser, error_message)
  end

  # Exits with an error for when a subcommand doesn't exist. Prints the usage
  def self.invalid_subcommand_error(subcommand, opt_parser)
    error_message = "Invalid subcommand '#{subcommand}', must be one of [#{SUBCOMMANDS.join(', ')}]"
    exit_with_usage_and_error(opt_parser, error_message)
  end

  # If out_file is not nil, write to that file. Otherwise print to STDOUT
  def self.print_or_write(string, out_file)
    if out_file
      File.open(out_file, 'w') { |file| file.write(string) }
    else
      puts string
    end
  end
end