configliere 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +6 -0
- data/.gitignore +38 -0
- data/LICENSE +20 -0
- data/README.textile +196 -0
- data/Rakefile +91 -0
- data/VERSION +1 -0
- data/bin/configliere +85 -0
- data/configliere.gemspec +123 -0
- data/examples/commandline_script.rb +74 -0
- data/examples/commandline_script.yaml +8 -0
- data/examples/foo.yaml +15 -0
- data/examples/simple_script.rb +23 -0
- data/examples/simple_script.yaml +5 -0
- data/lib/configliere.rb +36 -0
- data/lib/configliere/commandline.rb +102 -0
- data/lib/configliere/commandline/commands.rb +30 -0
- data/lib/configliere/commandline/options.rb +4 -0
- data/lib/configliere/config_blocks.rb +41 -0
- data/lib/configliere/core_ext.rb +1 -0
- data/lib/configliere/core_ext/hash.rb +98 -0
- data/lib/configliere/crypter.rb +72 -0
- data/lib/configliere/define.rb +151 -0
- data/lib/configliere/encrypted.rb +78 -0
- data/lib/configliere/environment.rb +38 -0
- data/lib/configliere/param.rb +97 -0
- data/lib/configliere/param_store.rb +70 -0
- data/spec/configliere/commandline_spec.rb +62 -0
- data/spec/configliere/config_blocks_spec.rb +26 -0
- data/spec/configliere/crypter_spec.rb +18 -0
- data/spec/configliere/define_spec.rb +97 -0
- data/spec/configliere/encrypted_spec.rb +71 -0
- data/spec/configliere/environment_spec.rb +23 -0
- data/spec/configliere/param_spec.rb +43 -0
- data/spec/configliere/param_store_spec.rb +81 -0
- data/spec/configliere_spec.rb +21 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +16 -0
- metadata +164 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'digest/sha2'
|
3
|
+
module Configliere
|
4
|
+
#
|
5
|
+
# Encrypt and decrypt values in configliere stores
|
6
|
+
#
|
7
|
+
module Crypter
|
8
|
+
CIPHER_TYPE = "aes-256-cbc" unless defined?(CIPHER_TYPE)
|
9
|
+
|
10
|
+
#
|
11
|
+
# Encrypt the given string
|
12
|
+
#
|
13
|
+
# @param plaintext the text to encrypt
|
14
|
+
# @param [String] encrypt_pass secret passphrase to encrypt with
|
15
|
+
# @return [String] encrypted text, suitable for deciphering with Crypter#decrypt
|
16
|
+
#
|
17
|
+
def self.encrypt plaintext, encrypt_pass, options={}
|
18
|
+
# The cipher's IV (Initialization Vector) is prepended (unencrypted) to
|
19
|
+
# the ciphertext, which as far as I can tell is safe for our purposes:
|
20
|
+
# http://www.ciphersbyritter.com/NEWS6/CBCIV.HTM
|
21
|
+
cipher = new_cipher :encrypt, encrypt_pass, options
|
22
|
+
cipher.iv = iv = cipher.random_iv
|
23
|
+
ciphertext = cipher.update(plaintext)
|
24
|
+
ciphertext << cipher.final
|
25
|
+
combine_iv_and_ciphertext(iv, ciphertext)
|
26
|
+
end
|
27
|
+
#
|
28
|
+
# Decrypt the given string, using the key and iv supplied
|
29
|
+
#
|
30
|
+
# @param ciphertext the text to decrypt, probably produced with Crypter#decrypt
|
31
|
+
# @param [String] encrypt_pass secret passphrase to decrypt with
|
32
|
+
# @return [String] the decrypted plaintext
|
33
|
+
#
|
34
|
+
def self.decrypt iv_and_ciphertext, encrypt_pass, options={}
|
35
|
+
cipher = new_cipher :decrypt, encrypt_pass, options
|
36
|
+
cipher.iv, ciphertext = separate_iv_and_ciphertext(cipher, iv_and_ciphertext)
|
37
|
+
plaintext = cipher.update(ciphertext)
|
38
|
+
plaintext << cipher.final
|
39
|
+
plaintext
|
40
|
+
end
|
41
|
+
protected
|
42
|
+
#
|
43
|
+
# Create a new cipher machine, with its dials set in the given direction
|
44
|
+
#
|
45
|
+
# @param [:encrypt, :decrypt] direction whether to encrypt or decrypt
|
46
|
+
# @param [String] encrypt_pass secret passphrase to decrypt with
|
47
|
+
#
|
48
|
+
def self.new_cipher direction, encrypt_pass, options={}
|
49
|
+
cipher = OpenSSL::Cipher::Cipher.new(CIPHER_TYPE)
|
50
|
+
case direction when :encrypt then cipher.encrypt when :decrypt then cipher.decrypt else raise "Bad cipher direction #{direction}" end
|
51
|
+
cipher.key = encrypt_key(encrypt_pass, options)
|
52
|
+
cipher
|
53
|
+
end
|
54
|
+
|
55
|
+
# prepend the initialization vector to the encoded message
|
56
|
+
def self.combine_iv_and_ciphertext iv, message
|
57
|
+
iv + message
|
58
|
+
end
|
59
|
+
# pull the initialization vector from the front of the encoded message
|
60
|
+
def self.separate_iv_and_ciphertext cipher, iv_and_ciphertext
|
61
|
+
idx = cipher.iv_len
|
62
|
+
[ iv_and_ciphertext[0..(idx-1)], iv_and_ciphertext[idx..-1] ]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Convert the encrypt_pass passphrase into the key used for encryption
|
66
|
+
def self.encrypt_key encrypt_pass, options={}
|
67
|
+
raise 'Blank encryption password!' if encrypt_pass.to_s == ''
|
68
|
+
# this provides the required 256 bits of key for the aes-256-cbc cipher
|
69
|
+
Digest::SHA256.digest(encrypt_pass)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Configliere
|
2
|
+
module Define
|
3
|
+
# Definitions for params: :description, :type, :encrypted, etc.
|
4
|
+
attr_accessor :param_definitions
|
5
|
+
|
6
|
+
# @param param the setting to describe. Either a simple symbol or a dotted param string.
|
7
|
+
# @param definitions the defineables to set (:description, :type, :encrypted, etc.)
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# Settings.define :dest_time, :type => Date, :description => 'Arrival time. If only a date is given, the current time of day on that date is assumed.'
|
11
|
+
# Settings.define 'delorean.power_source', :description => 'Delorean subsytem supplying power to the Flux Capacitor.'
|
12
|
+
# Settings.define :password, :required => true, :obscure => true
|
13
|
+
#
|
14
|
+
def define param, definitions={}
|
15
|
+
self.param_definitions[param].merge! definitions
|
16
|
+
end
|
17
|
+
|
18
|
+
def param_definitions
|
19
|
+
# initialize the param_definitions as an auto-vivifying hash if it's never been set
|
20
|
+
@param_definitions ||= Hash.new{|hsh, key| hsh[key] = {} }
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
# all params with a value for the definable aspect
|
25
|
+
#
|
26
|
+
# @param definable the aspect to list (:description, :type, :encrypted, etc.)
|
27
|
+
def params_with defineable
|
28
|
+
param_definitions.keys.find_all{|param| param_definitions[param][defineable] } || []
|
29
|
+
end
|
30
|
+
|
31
|
+
def definitions_for defineable
|
32
|
+
hsh = {}
|
33
|
+
param_definitions.each do |param, defs|
|
34
|
+
hsh[param] = defs[defineable] if defs[defineable]
|
35
|
+
end
|
36
|
+
hsh
|
37
|
+
end
|
38
|
+
public
|
39
|
+
|
40
|
+
# performs type coercion
|
41
|
+
def resolve!
|
42
|
+
resolve_types!
|
43
|
+
begin ; super() ; rescue NoMethodError ; nil ; end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate!
|
48
|
+
validate_requireds!
|
49
|
+
begin ; super() ; rescue NoMethodError ; nil ; end
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
# ===========================================================================
|
54
|
+
#
|
55
|
+
# Describe params with
|
56
|
+
#
|
57
|
+
# Settings.define :param, :description => '...'
|
58
|
+
#
|
59
|
+
|
60
|
+
# gets the description (if any) for the param
|
61
|
+
# @param param the setting to describe. Either a simple symbol or a dotted param string.
|
62
|
+
def description_for param
|
63
|
+
param_definitions[param][:description]
|
64
|
+
end
|
65
|
+
|
66
|
+
# All described params with their descriptions
|
67
|
+
def descriptions
|
68
|
+
definitions_for(:description)
|
69
|
+
end
|
70
|
+
|
71
|
+
# List of params that have descriptions
|
72
|
+
def described_params
|
73
|
+
params_with(:description)
|
74
|
+
end
|
75
|
+
|
76
|
+
# ===========================================================================
|
77
|
+
#
|
78
|
+
# Type coercion
|
79
|
+
#
|
80
|
+
# Define types with
|
81
|
+
#
|
82
|
+
# Settings.define :param, :type => Date
|
83
|
+
#
|
84
|
+
|
85
|
+
def type_for param
|
86
|
+
param_definitions[param][:type]
|
87
|
+
end
|
88
|
+
|
89
|
+
# All described params with their descriptions
|
90
|
+
def types
|
91
|
+
definitions_for(:type)
|
92
|
+
end
|
93
|
+
|
94
|
+
# List of params that have descriptions
|
95
|
+
def typed_params
|
96
|
+
params_with(:type)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Coerce all params with types defined to their proper form
|
100
|
+
def resolve_types!
|
101
|
+
types.each do |param, type|
|
102
|
+
val = self[param]
|
103
|
+
case
|
104
|
+
when val.nil? then val = nil
|
105
|
+
when (type == :boolean) then
|
106
|
+
if ['false', '0', ''].include?(val.to_s) then val = false else val = true end
|
107
|
+
when (val.to_s == '') then val = nil
|
108
|
+
when (type == Float) then val = val.to_f
|
109
|
+
when (type == Integer) then val = val.to_i
|
110
|
+
when (type == Date) then val = Date.parse(val) rescue nil
|
111
|
+
when (type == DateTime) then val = DateTime.parse(val) rescue nil
|
112
|
+
when (type == Time) then
|
113
|
+
require 'time'
|
114
|
+
val = Time.parse(val) rescue nil
|
115
|
+
when (type == Symbol) then val = val.to_s.to_sym rescue nil
|
116
|
+
end
|
117
|
+
self[param] = val
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# ===========================================================================
|
122
|
+
#
|
123
|
+
# Required params
|
124
|
+
#
|
125
|
+
# Define requireds with
|
126
|
+
#
|
127
|
+
# Settings.define :param, :required => true
|
128
|
+
#
|
129
|
+
|
130
|
+
# List of params that are required
|
131
|
+
# @return [Array] list of required params
|
132
|
+
def required_params
|
133
|
+
params_with(:required)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Check that all required params are present.
|
137
|
+
def validate_requireds!
|
138
|
+
missing = []
|
139
|
+
required_params.each do |param|
|
140
|
+
missing << param if self[param].nil?
|
141
|
+
end
|
142
|
+
raise "Missing values for #{missing.map{|s| s.to_s }.sort.join(", ")}" if (! missing.empty?)
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
Param.class_eval do
|
148
|
+
include Configliere::Define
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
@@ -0,0 +1,78 @@
|
|
1
|
+
Configliere.use :param_store, :define, :crypter
|
2
|
+
|
3
|
+
module Configliere
|
4
|
+
module EncryptedParam
|
5
|
+
# The password used in encrypting params during serialization
|
6
|
+
attr_accessor :encrypt_pass
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
# @example
|
11
|
+
# Settings.defaults :username=>"mysql_username", :password=>"mysql_password"
|
12
|
+
# Settings.define :password, :encrypted => true
|
13
|
+
# Settings.exportable
|
14
|
+
# #=> {:username => 'mysql_username', :password=>"\345?r`\222\021"\210\312\331\256\356\351\037\367\326" }
|
15
|
+
def export
|
16
|
+
hsh = super()
|
17
|
+
encrypted_params.each do |param|
|
18
|
+
val = hsh.deep_delete(*dotted_to_deep_keys(param)) or next
|
19
|
+
hsh.deep_set( *(dotted_to_encrypted_keys(param) | [encrypted(val)]) )
|
20
|
+
end
|
21
|
+
hsh
|
22
|
+
end
|
23
|
+
|
24
|
+
# decrypts any encrypted params
|
25
|
+
# then calls the next step in the resolve! chain.
|
26
|
+
def resolve!
|
27
|
+
resolve_encrypted!
|
28
|
+
begin ; super() ; rescue NoMethodError ; nil ; end
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
# import values, decrypting all params marked as encrypted
|
33
|
+
def resolve_encrypted!
|
34
|
+
remove_and_adopt_encrypt_pass_param_if_any!
|
35
|
+
encrypted_params.each do |param|
|
36
|
+
encrypted_val = deep_delete(*dotted_to_encrypted_keys(param)) or next
|
37
|
+
self[param] = self.decrypted(encrypted_val)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# if :encrypted_pass was set as a param, remove it from the hash and set it as an attribute
|
42
|
+
def remove_and_adopt_encrypt_pass_param_if_any!
|
43
|
+
@encrypt_pass = self.delete(:encrypt_pass) if self[:encrypt_pass]
|
44
|
+
end
|
45
|
+
|
46
|
+
# the chain of symbol keys for a dotted path key,
|
47
|
+
# prefixing the last one with "encrypted_"
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# dotted_to_encrypted_keys('amazon.api.key')
|
51
|
+
# #=> [:amazon, :api, :encrypted_key]
|
52
|
+
def dotted_to_encrypted_keys param
|
53
|
+
encrypted_path = dotted_to_deep_keys(param).dup
|
54
|
+
encrypted_path[-1] = "encrypted_#{encrypted_path.last}".to_sym
|
55
|
+
encrypted_path
|
56
|
+
end
|
57
|
+
|
58
|
+
# list of all params to encrypt on serialization
|
59
|
+
def encrypted_params
|
60
|
+
params_with(:encrypted)
|
61
|
+
end
|
62
|
+
|
63
|
+
def decrypted val
|
64
|
+
return val if val.to_s == ''
|
65
|
+
Configliere::Crypter.decrypt(val, encrypt_pass)
|
66
|
+
end
|
67
|
+
|
68
|
+
def encrypted(val)
|
69
|
+
return if ( !val )
|
70
|
+
Configliere::Crypter.encrypt(val, encrypt_pass)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Param
|
75
|
+
include EncryptedParam
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
Configliere.use :define
|
3
|
+
module Configliere
|
4
|
+
#
|
5
|
+
# Environment -- load configuration from environment variables
|
6
|
+
#
|
7
|
+
module Environment
|
8
|
+
def environment_variables *envs
|
9
|
+
envs.each do |env|
|
10
|
+
case env
|
11
|
+
when Hash
|
12
|
+
env.each do |env, param|
|
13
|
+
adopt_environment_variable! env, param
|
14
|
+
end
|
15
|
+
else
|
16
|
+
param = env.to_s.downcase.to_sym
|
17
|
+
adopt_environment_variable! env, param
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def adopt_environment_variable! env, param
|
23
|
+
define param, :environment => env
|
24
|
+
val = ENV[env]
|
25
|
+
self[param] = val if val
|
26
|
+
end
|
27
|
+
|
28
|
+
def params_from_environment
|
29
|
+
definitions_for(:environment)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Param.class_eval do
|
34
|
+
# include read / save operations
|
35
|
+
include Environment
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Configliere
|
2
|
+
#
|
3
|
+
# Hash of fields to store.
|
4
|
+
#
|
5
|
+
# Any field name beginning with 'decrypted_' automatically creates a
|
6
|
+
# counterpart 'encrypted_' field using the encrypt_pass.
|
7
|
+
#
|
8
|
+
class Param < ::Hash
|
9
|
+
|
10
|
+
# Initialize with the encrypt_pass and the initial contents of the hash.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# # Create a param for a hypothetical database with encrypt_pass "your_mom"
|
14
|
+
# Configliere::Param.new 'your_mom',
|
15
|
+
# :username=>"mysql_username", :decrypted_password=>"mysql_password"
|
16
|
+
#
|
17
|
+
def initialize hsh={}
|
18
|
+
super()
|
19
|
+
merge! hsh
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Incorporates the given settings.
|
24
|
+
# alias for deep_merge!
|
25
|
+
# Existing values not given in the hash
|
26
|
+
#
|
27
|
+
# @param hsh the defaults to set.
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# Settings.defaults :hat => :cat, :basket => :lotion, :moon => { :man => :smiling }
|
31
|
+
# Settings.defaults :basket => :tasket, :moon => { :cow => :smiling }
|
32
|
+
# Config #=> { :hat => :cat, :basket => :tasket, :moon => { :man => :smiling, :cow => :jumping } }
|
33
|
+
#
|
34
|
+
def defaults hsh
|
35
|
+
deep_merge! hsh
|
36
|
+
end
|
37
|
+
|
38
|
+
# Finalize and validate params
|
39
|
+
def resolve!
|
40
|
+
begin ; super() ; rescue NoMethodError ; nil ; end
|
41
|
+
validate!
|
42
|
+
end
|
43
|
+
# Check that all defined params are valid
|
44
|
+
def validate!
|
45
|
+
begin ; super() ; rescue NoMethodError ; nil ; end
|
46
|
+
end
|
47
|
+
|
48
|
+
def []= param, val
|
49
|
+
if param =~ /\./
|
50
|
+
return deep_set( *( dotted_to_deep_keys(param) | [val] ))
|
51
|
+
else
|
52
|
+
super param.to_sym, val
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def [] param
|
57
|
+
if param =~ /\./
|
58
|
+
return deep_get( *dotted_to_deep_keys(param) )
|
59
|
+
else
|
60
|
+
super param.to_sym
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def delete param
|
65
|
+
if param =~ /\./
|
66
|
+
return deep_delete( *dotted_to_deep_keys(param) )
|
67
|
+
else
|
68
|
+
super param.to_sym
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# returns an actual Hash, not a Param < Hash
|
73
|
+
def to_hash
|
74
|
+
{}.merge! self
|
75
|
+
end
|
76
|
+
|
77
|
+
def use *args
|
78
|
+
Configliere.use *args
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
# turns a dotted param ('moon.cheese.type') into
|
83
|
+
# an array of sequential keys for deep_set and deep_get
|
84
|
+
def dotted_to_deep_keys dotted
|
85
|
+
dotted.to_s.split(".").map{|key| key.to_sym}
|
86
|
+
end
|
87
|
+
|
88
|
+
# simple (no-arg) method_missing callse
|
89
|
+
def method_missing meth, *args
|
90
|
+
if args.empty? && meth.to_s =~ /^\w+$/
|
91
|
+
self[meth]
|
92
|
+
elsif args.size == 1 && meth.to_s =~ /^(\w+)=$/
|
93
|
+
self[$1] = args.first
|
94
|
+
else super(meth, *args) end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
module Configliere
|
3
|
+
#
|
4
|
+
# ParamStore -- load configuration from a simple YAML file
|
5
|
+
#
|
6
|
+
module ParamStore
|
7
|
+
# Load params from disk.
|
8
|
+
# * file is in YAML format, as a hash of handle => param_hash pairs
|
9
|
+
# * filename defaults to ParamStore::DEFAULT_CONFIG_FILE (~/.configliere, probably)
|
10
|
+
def read handle
|
11
|
+
filename = filename_for_handle(handle)
|
12
|
+
begin
|
13
|
+
params = YAML.load(File.open(filename)) || {}
|
14
|
+
rescue Errno::ENOENT => e
|
15
|
+
warn "Loading empty configliere settings file #{filename}"
|
16
|
+
params = {}
|
17
|
+
end
|
18
|
+
params = params[handle] if handle.is_a?(Symbol)
|
19
|
+
deep_merge! params
|
20
|
+
end
|
21
|
+
|
22
|
+
# save to disk.
|
23
|
+
# * file is in YAML format, as a hash of handle => param_hash pairs
|
24
|
+
# * filename defaults to ParamStore::DEFAULT_CONFIG_FILE (~/.configliere, probably)
|
25
|
+
def save! handle
|
26
|
+
filename = filename_for_handle(handle)
|
27
|
+
if handle.is_a?(Symbol)
|
28
|
+
ParamStore.merge_into_yaml_file filename, handle, self.export
|
29
|
+
else
|
30
|
+
ParamStore.write_yaml_file filename, self.export
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
# form suitable for serialization to disk
|
37
|
+
# (e.g. the encryption done in configliere/encrypted)
|
38
|
+
def export
|
39
|
+
to_hash
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.write_yaml_file filename, hsh
|
43
|
+
File.open(filename, 'w'){|f| f << YAML.dump(hsh) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.merge_into_yaml_file filename, handle, params
|
47
|
+
begin
|
48
|
+
all_settings = YAML.load(File.open(filename)) || {}
|
49
|
+
rescue Errno::ENOENT => e;
|
50
|
+
all_settings = {}
|
51
|
+
end
|
52
|
+
all_settings[handle] = params
|
53
|
+
write_yaml_file filename, all_settings
|
54
|
+
end
|
55
|
+
|
56
|
+
def filename_for_handle handle
|
57
|
+
case
|
58
|
+
when handle.is_a?(Symbol) then Configliere::DEFAULT_CONFIG_FILE
|
59
|
+
when handle.include?('/') then handle
|
60
|
+
else File.join(Configliere::DEFAULT_CONFIG_DIR, handle)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
Param.class_eval do
|
67
|
+
# include read / save operations
|
68
|
+
include ParamStore
|
69
|
+
end
|
70
|
+
end
|