tty-config 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +103 -18
  4. data/lib/tty/config.rb +66 -129
  5. data/lib/tty/config/dependency_loader.rb +55 -0
  6. data/lib/tty/config/generator.rb +57 -0
  7. data/lib/tty/config/marshaller.rb +56 -0
  8. data/lib/tty/config/marshaller_registry.rb +43 -0
  9. data/lib/tty/config/marshallers.rb +35 -0
  10. data/lib/tty/config/marshallers/hcl_marshaller.rb +25 -0
  11. data/lib/tty/config/marshallers/ini_marshaller.rb +28 -0
  12. data/lib/tty/config/marshallers/java_props_marshaller.rb +25 -0
  13. data/lib/tty/config/marshallers/json_marshaller.rb +25 -0
  14. data/lib/tty/config/marshallers/toml_marshaller.rb +25 -0
  15. data/lib/tty/config/marshallers/yaml_marshaller.rb +29 -0
  16. data/lib/tty/config/version.rb +1 -1
  17. metadata +54 -32
  18. data/Rakefile +0 -8
  19. data/bin/console +0 -14
  20. data/bin/setup +0 -8
  21. data/spec/spec_helper.rb +0 -54
  22. data/spec/unit/alias_setting_spec.rb +0 -72
  23. data/spec/unit/append_spec.rb +0 -26
  24. data/spec/unit/autoload_env_spec.rb +0 -62
  25. data/spec/unit/delete_spec.rb +0 -22
  26. data/spec/unit/exist_spec.rb +0 -24
  27. data/spec/unit/fetch_spec.rb +0 -45
  28. data/spec/unit/generate_spec.rb +0 -70
  29. data/spec/unit/merge_spec.rb +0 -22
  30. data/spec/unit/new_spec.rb +0 -6
  31. data/spec/unit/normalize_hash_spec.rb +0 -21
  32. data/spec/unit/read_spec.rb +0 -118
  33. data/spec/unit/remove_spec.rb +0 -16
  34. data/spec/unit/set_from_env_spec.rb +0 -78
  35. data/spec/unit/set_if_empty_spec.rb +0 -26
  36. data/spec/unit/set_spec.rb +0 -62
  37. data/spec/unit/validate_spec.rb +0 -76
  38. data/spec/unit/write_spec.rb +0 -197
  39. data/tasks/console.rake +0 -11
  40. data/tasks/coverage.rake +0 -11
  41. data/tasks/spec.rake +0 -29
  42. data/tty-config.gemspec +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15acd31b643bbf86fbacabac790174050f401eb46fdb943365f7f05180d5487a
4
- data.tar.gz: 51d27ba31c438bd5dacfcfa2ee255bfc29ac2879cd9fa51512226a073da91a85
3
+ metadata.gz: 0f0321ad959e00b63f653d7f27a89964cdc24b103aab2052615e0ed3fdf284ce
4
+ data.tar.gz: 12df44d97821e84cb19620fadec565841d0de28abc1a4ba1cd8a05d9d1d6ec24
5
5
  SHA512:
6
- metadata.gz: d5fd67029f81ef16988f1892178c30e84ead7376f7ae863fc66acfe8f97dedc7a158a7cc7cbef282356ae42d9b7182338f54fcb27595a9474fa7d7cbec0e4211
7
- data.tar.gz: a0f36d8133ae66dd2bb94f051e2cf90299ae63ec10d4af4d0204564d055c51c4136ebc8fcadf1c4505cbc9b0ab26485009f1b9a6544c9d7338a7b1d1a7f8d992
6
+ metadata.gz: ebe68702be360238664adf912b42342df8e3e3be266d7e52d2be515d8f48373db53850688517f7c569f2b9674a88371a3205bad707d48a209c01d147af27c07f
7
+ data.tar.gz: af1c4f4586a6be640f65ebe01e0dcb88340860b4ae1645ba611135e618ca1b772c75eab2ab9744e0409dddac316154f2358d403e0950af20c25c1755ee7f16cc
@@ -1,5 +1,19 @@
1
1
  # Change log
2
2
 
3
+ ## [v0.4.0] - 2020-01-25
4
+
5
+ ### Added
6
+ * Add DependencyLoader for a generic interface for loading marshalling dependencies
7
+ * Add Marshaller as a generic interface for building marshalling plugins
8
+ * Add MarshallerRegistry for storing all marshallers
9
+ * Add Marshallers to allow configuration of marshallers via #register_marshaller
10
+ & #unregister_marshaller
11
+
12
+ ### Changed
13
+ * Change #initialize to accept hash as settings
14
+ * Change #marshal & #unmarshal to use marshalling plugins
15
+ * Change gemspec to add metadata and remove test artefacts
16
+
3
17
  ## [v0.3.2] - 2019-06-18
4
18
 
5
19
  ### Changed
@@ -41,6 +55,7 @@
41
55
 
42
56
  * Initial implementation and release
43
57
 
58
+ [v0.4.0]: https://github.com/piotrmurach/tty-config/compare/v0.3.2...v0.4.0
44
59
  [v0.3.2]: https://github.com/piotrmurach/tty-config/compare/v0.3.1...v0.3.2
45
60
  [v0.3.1]: https://github.com/piotrmurach/tty-config/compare/v0.3.0...v0.3.1
46
61
  [v0.3.0]: https://github.com/piotrmurach/tty-config/compare/v0.2.0...v0.3.0
data/README.md CHANGED
@@ -25,11 +25,15 @@
25
25
 
26
26
  ## Features
27
27
 
28
- * Read & write configurations in YAML, JSON, TOML, INI formats
29
- * Simple interface for adding and reading settings for deeply nested keys
30
- * Indifferent access for reading settings
31
- * Merging of configuration settings from other hashes
32
- * Reading values from environment variables
28
+ This is a one-stop shop for all your configuration needs:
29
+
30
+ * Read and write config files in YAML, JSON, TOML, INI, HCL and Java Properties formats
31
+ * Add custom marshallers or override the built-in ones
32
+ * Set and read settings for deeply nested keys
33
+ * Set defaults for undefined settings
34
+ * Read settings with indifferent access
35
+ * Merge configuration settings from other hash objects
36
+ * Read values from environment variables
33
37
 
34
38
  ## Installation
35
39
 
@@ -72,6 +76,8 @@ Or install it yourself as:
72
76
  * [2.18 write](#218-write)
73
77
  * [2.19 exist?](#219-exist)
74
78
  * [2.20 autoload_env](#220-autoload_env)
79
+ * [2.21 register_marshaller](#221-register_marshaller)
80
+ * [2.22 unregister_marshaller](#222-register_marshaller)
75
81
  * [3. Examples](#3-examples)
76
82
  * [3.1 Working with env vars](#31-working-with-env-vars)
77
83
  * [3.2 Working with optparse](#32-working-with-optparse)
@@ -85,7 +91,7 @@ config = TTY::Config.new
85
91
  config.filename = 'investments'
86
92
  ```
87
93
 
88
- then configure values for different nested keys with `set` and `append`:
94
+ Then configure values for different nested keys with `set` and `append`:
89
95
 
90
96
  ```ruby
91
97
  config.set(:settings, :base, value: 'USD')
@@ -95,7 +101,7 @@ config.set(:coins, value: ['BTC'])
95
101
  config.append('ETH', 'TRX', 'DASH', to: :coins)
96
102
  ```
97
103
 
98
- get any value by using `fetch`:
104
+ You can get any value by using `fetch`:
99
105
 
100
106
  ```ruby
101
107
  config.fetch(:settings, :base)
@@ -105,7 +111,7 @@ config.fetch(:coins)
105
111
  # => ['BTC', 'ETH', 'TRX', 'DASH']
106
112
  ```
107
113
 
108
- and `write` configration out to `investments.yml`:
114
+ And call `write` to persist the configuration to `investments.yml` file:
109
115
 
110
116
  ```ruby
111
117
  config.write
@@ -121,14 +127,14 @@ config.write
121
127
  # - DASH
122
128
  ```
123
129
 
124
- and then to read an `investments.yml` file, you need to provide the locations to search in:
130
+ To read an `investments.yml` file, you need to provide the locations to search in:
125
131
 
126
132
  ```ruby
127
133
  config.append_path Dir.pwd
128
134
  config.append_path Dir.home
129
135
  ```
130
136
 
131
- Finally, read in configuration back again:
137
+ Finally, call `read` to convert configuration file back into an object again:
132
138
 
133
139
  ```ruby
134
140
  config.read
@@ -175,7 +181,7 @@ You can also specify deeply nested configuration settings by passing sequence of
175
181
  config.set :settings, :base, value: 'USD'
176
182
  ```
177
183
 
178
- is equivalent to:
184
+ Which is equivalent to:
179
185
 
180
186
  ```ruby
181
187
  config.set 'settings.base', value: 'USD'
@@ -240,7 +246,7 @@ config.set_from_env('settings.base') { 'CURRENCY'}
240
246
  config.set_from_env('settings.base') { :currency}
241
247
  ```
242
248
 
243
- And asssuming `ENV['CURRENCY']=USD`:
249
+ And assuming `ENV['CURRENCY']=USD`:
244
250
 
245
251
  ```ruby
246
252
  config.fetch(:settings, :base)
@@ -266,7 +272,7 @@ Similar to `set` operation, `fetch` allows you to retrieve deeply nested values:
266
272
  config.fetch(:settings, :base) # => USD
267
273
  ```
268
274
 
269
- is equivalent to:
275
+ Which is equivalent to:
270
276
 
271
277
  ```ruby
272
278
  config.fetch('settings.base')
@@ -432,7 +438,7 @@ end
432
438
 
433
439
  You can assign multiple validations for a given key and each of them will be run in the order they were registered when checking a value.
434
440
 
435
- When setting value all the validaitons will be run:
441
+ When setting value all the validations will be run:
436
442
 
437
443
  ```ruby
438
444
  config.set(:settings, :base, value: 'PL')
@@ -529,8 +535,10 @@ Currently the supported file formats are:
529
535
  * `json` for `.json` extension
530
536
  * `toml` for `.toml` extension
531
537
  * `ini` for `.ini`, `.cnf`, `.conf`, `.cfg`, `.cf extensions`
538
+ * `hcl` for `.hcl` extensions
539
+ * `jprops` for `.properties`, `.props`, `.prop` extensions
532
540
 
533
- Calling `read` without any arguments searches through provided locations to find configuration file and reads it. Therefore, you need to specify at least one search path that contains the configuration file together with actual filename. When filename is specifed then all known extensions will be tried.
541
+ Calling `read` without any arguments searches through provided locations to find configuration file and reads it. Therefore, you need to specify at least one search path that contains the configuration file together with actual filename. When filename is specified then all known extensions will be tried.
534
542
 
535
543
  For example, to find file called investments in the current directory do:
536
544
 
@@ -599,9 +607,9 @@ config.exist? # => true
599
607
 
600
608
  ### 2.20 autoload_env
601
609
 
602
- The `autload_env` allows you to automatically read environment variables. In most cases you would combine it with [env_prefix=](#212-env_prefix) to only read a subset of variables. When using `autload_env`, anytime the `fetch` is called a corresponding enviornment variable will be checked.
610
+ The `autload_env` allows you to automatically read environment variables. In most cases you would combine it with [env_prefix=](#212-env_prefix) to only read a subset of variables. When using `autload_env`, anytime the `fetch` is called a corresponding environment variable will be checked.
603
611
 
604
- For example, given an evironment variable `MYTOOL_HOST` set to `localhost`:
612
+ For example, given an environment variable `MYTOOL_HOST` set to `localhost`:
605
613
 
606
614
  ```ruby
607
615
  ENV['MYTOOL_HOST']=localhost
@@ -621,6 +629,83 @@ config.fetch(:host)
621
629
  # => 'localhost'
622
630
  ```
623
631
 
632
+ ### 2.21 register_marshaller
633
+
634
+ There are number of built-in marshallers that handle the process of serializing internal configuration from and back into a desired format, for example, a `JSON` string.
635
+
636
+ Currently supported formats out-of-the-box are: `YAML`, `JSON`, `TOML`, `INI` & `HCL`.
637
+
638
+ To create your own marshaller use the `TTY::Config::Marshaller` interface. You need to provide the implementation for the following marshalling methods:
639
+
640
+ * `marshal`
641
+ * `unmarshal`
642
+
643
+ In addition, you will need to specify the extension types this marshaller will handle using the `extension` method. The method accepts a list of names preceded by a dot:
644
+
645
+ ```ruby
646
+ extension ".ext1", ".ext2", ".ext3"
647
+ ```
648
+
649
+ Optionally, you can provide a dependency or dependencies that will be lazy loaded if the extension is used. For this use the `dependency` method.
650
+
651
+ You can either specify dependencies as a list of names:
652
+
653
+ ```ruby
654
+ dependency "toml"
655
+ dependency "toml", "tomlrb"
656
+ ```
657
+
658
+ Or provide dependencies in a block:
659
+
660
+ ```ruby
661
+ dependency do
662
+ require "toml"
663
+ require "tomlrb"
664
+ end
665
+ ```
666
+
667
+ Putting it all together, you can create your own marshaller like so:
668
+
669
+ ```ruby
670
+ class MyCustomMarshaller
671
+ include TTY::Config::Marshaller
672
+
673
+ dependency "my_dep"
674
+
675
+ extension ".ext1", ".ext2"
676
+
677
+ def marshal(object)
678
+ MyDep.dump(object)
679
+ end
680
+
681
+ def unmarshal(content)
682
+ MyDep.parse(content)
683
+ end
684
+ end
685
+ ```
686
+
687
+ And then let the configuration know about your marshaller by calling the `register_marshaller`:
688
+
689
+ ```ruby
690
+ config.register_marshaller(:my_custom, MyCustomMarshaller)
691
+ ```
692
+
693
+ Bear in mind that you can also override the built-in implementation of a marshaller. For example, if you find a better performing Ruby gem for TOML parsing, register your custom marshaller under the `:toml` name like so:
694
+
695
+ ```ruby
696
+ config.register_marshaller(:toml, MyTOMLMarshaller)
697
+ ```
698
+
699
+ ### 2.22 unregister_marshaller
700
+
701
+ By default, the **TTY::Config** is ready to recognize various extensions. See (2.17 read)[#217-read] section for more details. But, you're free to remove the default marshallers from the internal registry with `unregister_marshaller` method.
702
+
703
+ For example, to remove all the built-in marshallers do:
704
+
705
+ ```ruby
706
+ config.unregister_marshaller :yaml, :json, :toml, :ini, :hcl
707
+ ```
708
+
624
709
  ## 3. Examples
625
710
 
626
711
  ### 3.1 Working with env vars
@@ -640,7 +725,7 @@ config.set_from_env(:host)
640
725
  config.set_from_env(:port)
641
726
  ```
642
727
 
643
- or automatically load all prefixed environment variables with [autoload_env](#220-autoload-env):
728
+ Or automatically load all prefixed environment variables with [autoload_env](#220-autoload-env):
644
729
 
645
730
  ```ruby
646
731
  config.env_prefix = 'mytool'
@@ -1,11 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pathname'
3
+ require "pathname"
4
4
 
5
- require_relative 'config/version'
5
+ require_relative "config/version"
6
+ require_relative "config/marshallers"
7
+ require_relative "config/marshallers/ini_marshaller"
8
+ require_relative "config/marshallers/json_marshaller"
9
+ require_relative "config/marshallers/yaml_marshaller"
10
+ require_relative "config/marshallers/toml_marshaller"
11
+ require_relative "config/marshallers/hcl_marshaller"
12
+ require_relative "config/marshallers/java_props_marshaller"
6
13
 
7
14
  module TTY
8
15
  class Config
16
+ include Marshallers
17
+
18
+ # Error raised when failed to load a dependency
19
+ DependencyLoadError = Class.new(StandardError)
9
20
  # Error raised when key fails validation
10
21
  ReadError = Class.new(StandardError)
11
22
  # Error raised when issues writing configuration to a file
@@ -30,65 +41,6 @@ module TTY
30
41
  end
31
42
  end
32
43
 
33
- # Generate file content based on the data hash
34
- #
35
- # @param [Hash] data
36
- #
37
- # @return [String]
38
- # the file content
39
- #
40
- # @api public
41
- def self.generate(data, separator: '=')
42
- content = []
43
- values = {}
44
- sections = {}
45
-
46
- data.keys.sort.each do |key|
47
- val = data[key]
48
- if val.is_a?(NilClass)
49
- next
50
- elsif val.is_a?(Hash) ||
51
- (val.is_a?(Array) && val.first.is_a?(Hash))
52
- sections[key] = val
53
- elsif val.is_a?(Array)
54
- values[key] = val.join(',')
55
- else
56
- values[key] = val
57
- end
58
- end
59
-
60
- # values
61
- values.each do |key, val|
62
- content << "#{key} #{separator} #{val}"
63
- end
64
- content << '' unless values.empty?
65
-
66
- # sections
67
- sections.each do |section, object|
68
- next if object.empty? # only add section if values present
69
-
70
- content << "[#{section}]"
71
- if object.is_a?(Array)
72
- object = object.reduce({}, :merge!)
73
- end
74
- object.each do |key, val|
75
- val = val.join(',') if val.is_a?(Array)
76
- content << "#{key} #{separator} #{val}" if val
77
- end
78
- content << ''
79
- end
80
- content.join("\n")
81
- end
82
-
83
- # Storage for suppported format & extensions pairs
84
- # @api public
85
- EXTENSIONS = {
86
- yaml: %w(.yaml .yml),
87
- json: %w(.json),
88
- toml: %w(.toml),
89
- ini: %w(.ini .cnf .conf .cfg .cf)
90
- }.freeze
91
-
92
44
  # A collection of config paths
93
45
  # @api public
94
46
  attr_reader :location_paths
@@ -116,19 +68,25 @@ module TTY
116
68
  # Create a configuration instance
117
69
  #
118
70
  # @api public
119
- def initialize(**settings)
71
+ def initialize(settings = {})
120
72
  @settings = settings
121
73
  @location_paths = []
122
74
  @validators = {}
123
75
  @filename = 'config'
124
76
  @extname = '.yml'
125
- @extensions = EXTENSIONS.values.flatten << ''
126
77
  @key_delim = '.'
127
78
  @envs = {}
128
79
  @env_prefix = ''
129
80
  @autoload_env = false
130
81
  @aliases = {}
131
82
 
83
+ register_marshaller :yaml, Marshallers::YAMLMarshaller
84
+ register_marshaller :json, Marshallers::JSONMarshaller
85
+ register_marshaller :toml, Marshallers::TOMLMarshaller
86
+ register_marshaller :ini, Marshallers::INIMarshaller
87
+ register_marshaller :hcl, Marshallers::HCLMarshaller
88
+ register_marshaller :jprops, Marshallers::JavaPropsMarshaller
89
+
132
90
  yield(self) if block_given?
133
91
  end
134
92
 
@@ -138,7 +96,7 @@ module TTY
138
96
  #
139
97
  # api public
140
98
  def extname=(name)
141
- unless @extensions.include?(name)
99
+ unless extensions.include?(name)
142
100
  raise UnsupportedExtError, "Config file format `#{name}` is not supported."
143
101
  end
144
102
  @extname = name
@@ -392,7 +350,12 @@ module TTY
392
350
  raise ReadError, "Configuration file `#{file}` does not exist!"
393
351
  end
394
352
 
395
- merge(unmarshal(file, format: format))
353
+ set_file_metadata(file)
354
+
355
+ ext = (format == :auto ? extname : ".#{format}")
356
+ content = ::File.read(file)
357
+
358
+ merge(unmarshal(content, ext: ext))
396
359
  end
397
360
 
398
361
  # Write current configuration to a file.
@@ -416,10 +379,24 @@ module TTY
416
379
  file = ::File.join(dir, "#{filename}#{@extname}")
417
380
  end
418
381
 
419
- content = marshal(file, @settings, format: format)
382
+ set_file_metadata(file)
383
+
384
+ ext = (format == :auto ? extname : ".#{format}")
385
+ content = marshal(@settings, ext: ext)
386
+
420
387
  ::File.write(file, content)
421
388
  end
422
389
 
390
+ # Set file name and extension
391
+ #
392
+ # @param [File] file
393
+ #
394
+ # @api public
395
+ def set_file_metadata(file)
396
+ self.extname = ::File.extname(file)
397
+ self.filename = ::File.basename(file, extname)
398
+ end
399
+
423
400
  # Current configuration
424
401
  #
425
402
  # @api public
@@ -583,7 +560,7 @@ module TTY
583
560
  # @api private
584
561
  def search_in_path(path)
585
562
  path = Pathname.new(path)
586
- @extensions.each do |ext|
563
+ extensions.each do |ext|
587
564
  if ::File.exist?(path.join("#{filename}#{ext}").to_s)
588
565
  return path.join("#{filename}#{ext}").to_s
589
566
  end
@@ -591,81 +568,41 @@ module TTY
591
568
  nil
592
569
  end
593
570
 
571
+ # Crate a marshaller instance based on the extension name
572
+ #
573
+ # @return [nil, Marshaller]
574
+ #
594
575
  # @api private
595
- def unmarshal(file, format: :auto)
596
- file_ext = ::File.extname(file)
597
- ext = (format == :auto ? file_ext : ".#{format}")
598
- self.extname = file_ext
599
- self.filename = ::File.basename(file, file_ext)
600
-
601
- case ext
602
- when *EXTENSIONS[:yaml]
603
- load_read_dep('yaml', ext)
604
- if YAML.respond_to?(:safe_load)
605
- YAML.safe_load(::File.read(file))
606
- else
607
- YAML.load(::File.read(file))
608
- end
609
- when *EXTENSIONS[:json]
610
- load_read_dep('json', ext)
611
- JSON.parse(::File.read(file))
612
- when *EXTENSIONS[:toml]
613
- load_read_dep('toml', ext)
614
- TOML.load(::File.read(file))
615
- when *EXTENSIONS[:ini]
616
- load_read_dep('inifile', ext)
617
- ini = IniFile.load(file).to_h
618
- global = ini.delete('global')
619
- ini.merge!(global)
620
- else
621
- raise ReadError, "Config file format `#{ext}` is not supported."
622
- end
576
+ def create_marshaller(ext)
577
+ marshaller = marshallers.find { |marsh| marsh.ext.include?(ext) }
578
+
579
+ return nil if marshaller.nil?
580
+
581
+ marshaller.new
623
582
  end
624
583
 
625
- # Try loading read dependency
626
584
  # @api private
627
- def load_read_dep(gem_name, format)
628
- require gem_name
629
- rescue LoadError
630
- raise ReadError, "Gem `#{gem_name}` is missing. Please install it " \
631
- "to read #{format} configuration format."
585
+ def unmarshal(content, ext: nil)
586
+ ext ||= extname
587
+ if marshaller = create_marshaller(ext)
588
+ marshaller.unmarshal(content)
589
+ else
590
+ raise ReadError, "Config file format `#{ext}` is not supported."
591
+ end
632
592
  end
633
593
 
634
- # Marshal data hash into a configuration file content
594
+ # Marshal hash object into a configuration file content
635
595
  #
636
596
  # @return [String]
637
597
  #
638
598
  # @api private
639
- def marshal(file, data, format: :auto)
640
- file_ext = ::File.extname(file)
641
- ext = (format == :auto ? file_ext : ".#{format}")
642
- self.extname = file_ext
643
- self.filename = ::File.basename(file, file_ext)
644
-
645
- case ext
646
- when *EXTENSIONS[:yaml]
647
- load_write_dep('yaml', ext)
648
- YAML.dump(self.class.normalize_hash(data, :to_s))
649
- when *EXTENSIONS[:json]
650
- load_write_dep('json', ext)
651
- JSON.pretty_generate(data)
652
- when *EXTENSIONS[:toml]
653
- load_write_dep('toml', ext)
654
- TOML::Generator.new(data).body
655
- when *EXTENSIONS[:ini]
656
- Config.generate(data)
599
+ def marshal(object, ext: nil)
600
+ ext ||= extname
601
+ if marshaller = create_marshaller(ext)
602
+ marshaller.marshal(object)
657
603
  else
658
604
  raise WriteError, "Config file format `#{ext}` is not supported."
659
605
  end
660
606
  end
661
-
662
- # Try loading write depedency
663
- # @api private
664
- def load_write_dep(gem_name, format)
665
- require gem_name
666
- rescue LoadError
667
- raise WriteError, "Gem `#{gem_name}` is missing. Please install it " \
668
- "to read #{format} configuration format."
669
- end
670
607
  end # Config
671
608
  end # TTY