mysh 0.6.9 → 0.6.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/mysh.rb +5 -4
- data/lib/mysh/expression.rb +11 -31
- data/lib/mysh/external.rb +1 -1
- data/lib/mysh/globalize.rb +13 -0
- data/lib/mysh/handlebars.rb +9 -10
- data/lib/mysh/handlebars/string.rb +26 -12
- data/lib/mysh/internal/actions/help/hbar.txt +11 -0
- data/lib/mysh/internal/actions/load.rb +1 -1
- data/lib/mysh/internal/actions/show/env.rb +2 -4
- data/lib/mysh/internal/actions/show/gem.rb +3 -16
- data/lib/mysh/internal/actions/show/term.rb +5 -7
- data/lib/mysh/internal/actions/type.rb +1 -1
- data/lib/mysh/pre_processor.rb +2 -2
- data/lib/mysh/quick.rb +7 -1
- data/lib/mysh/version.rb +1 -1
- data/samples/show.txt +2 -0
- data/tests/my_shell_tests.rb +5 -6
- metadata +2 -3
- data/lib/mysh/binding_wrapper.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1aaea546a90b6d110dd8910757083c385a7e72d5
|
4
|
+
data.tar.gz: 4f82d70adf3479acf12ee7a670c6d3a247326777
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b6d81b4a6cce7307a7b1262ad4125afb1cc1c0083a2c128543ff43211bd3be70bf4c3372f43a01b1225b4668d077b3b29cac51c29741313c0ae757510398e3a
|
7
|
+
data.tar.gz: 1aff82db1052b83caf85b0e09f006eebebf80a0b0782da66660b09586fabe1527bdd72f4760c47eaa2a4387a5f16447acc6ce588f364101d3a692736c628bad1
|
data/README.md
CHANGED
@@ -26,7 +26,7 @@ See the original article at:
|
|
26
26
|
(http://www.blackbytes.info/2016/07/writing-a-shell-in-ruby/)
|
27
27
|
|
28
28
|
Oh, and one other little thing. A survey of the mysh reveals that it currently
|
29
|
-
contains
|
29
|
+
contains 2188 lines of code. It seems that there has been some growth beyond
|
30
30
|
the 25 lines in the original article.
|
31
31
|
|
32
32
|
## Installation
|
@@ -907,7 +907,7 @@ on the optional parms.
|
|
907
907
|
###### "string".preprocess(context=mysh_default_context)
|
908
908
|
Process the string for embedded variables and handlebars. By default,
|
909
909
|
any embedded ruby is evaluated in the mysh global expression binding. However,
|
910
|
-
another
|
910
|
+
another binding may be passed to access an alternative execution environment.
|
911
911
|
|
912
912
|
###### "string".to_host_spec
|
913
913
|
Given a string with a file spec, to_host_spec adjusts that string so that it is
|
data/lib/mysh.rb
CHANGED
@@ -1,17 +1,16 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
# mysh -- MY SHell -- a Ruby
|
3
|
+
# mysh -- MY SHell -- a Ruby inspired command line shell.
|
4
4
|
|
5
|
+
require 'pp'
|
5
6
|
require 'English'
|
6
7
|
require 'in_array'
|
7
8
|
require 'pause_output'
|
8
9
|
require 'format_output'
|
9
10
|
|
10
11
|
require_relative 'mysh/exceptions'
|
11
|
-
require_relative 'mysh/binding_wrapper'
|
12
12
|
require_relative 'mysh/input_wrapper'
|
13
13
|
require_relative 'mysh/user_input'
|
14
|
-
require_relative 'mysh/expression'
|
15
14
|
require_relative 'mysh/internal'
|
16
15
|
require_relative 'mysh/quick'
|
17
16
|
require_relative 'mysh/external'
|
@@ -22,7 +21,7 @@ require_relative 'mysh/pre_processor'
|
|
22
21
|
require_relative 'mysh/process'
|
23
22
|
require_relative 'mysh/globalize'
|
24
23
|
require_relative 'mysh/init'
|
25
|
-
|
24
|
+
require_relative 'mysh/expression'
|
26
25
|
require_relative 'mysh/version'
|
27
26
|
|
28
27
|
#The Mysh (MY SHell) module. A container for mysh and its functionality.
|
@@ -43,3 +42,5 @@ end
|
|
43
42
|
if __FILE__ == $0
|
44
43
|
Mysh.run(ARGV) #Run a shell if this file is run directly.
|
45
44
|
end
|
45
|
+
|
46
|
+
$VERBOSE = nil
|
data/lib/mysh/expression.rb
CHANGED
@@ -1,66 +1,46 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
require 'pp'
|
4
3
|
require_relative 'expression/lineage'
|
5
4
|
|
6
|
-
|
7
|
-
#<br>Endemic Code Smells
|
8
|
-
#* :reek:ModuleInitialize -- False positive
|
5
|
+
# Endemic Code Smells :reek:ModuleInitialize -- False positive
|
9
6
|
module Mysh
|
10
7
|
|
11
|
-
#Set up some popular constants
|
8
|
+
# Set up some popular constants
|
12
9
|
E = Math::E
|
13
10
|
PI = Math::PI
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
#
|
12
|
+
$mysh_exec_result = nil
|
13
|
+
|
14
|
+
# Reset the state of the execution hosting environment.
|
15
|
+
# Endemic Code Smells :reek:TooManyStatements -- False positive
|
18
16
|
def self.reset_host
|
19
17
|
exec_class = Class.new do
|
20
18
|
|
21
19
|
include Math
|
22
20
|
|
23
|
-
#Set up a new execution environment
|
21
|
+
# Set up a new execution environment
|
24
22
|
def initialize
|
25
|
-
$
|
26
|
-
$mysh_exec_binding = mysh_binding
|
27
|
-
end
|
28
|
-
|
29
|
-
#Do the actual work of executing an expression.
|
30
|
-
#<br>Note:
|
31
|
-
#* The expression string always begins with an '=' character.
|
32
|
-
def execute(expression)
|
33
|
-
pp $mysh_exec_binding.eval("$mysh_exec_result" + expression)
|
34
|
-
:expression
|
23
|
+
$mysh_exec_binding = binding
|
35
24
|
end
|
36
25
|
|
37
|
-
#Return a simple message for less convoluted error messages.
|
26
|
+
# Return a simple message for less convoluted error messages.
|
38
27
|
def inspect
|
39
28
|
"exec_host"
|
40
29
|
end
|
41
30
|
|
42
|
-
#Evaluate the string in the my shell context.
|
43
|
-
def mysh_eval(str)
|
44
|
-
$mysh_exec_binding.eval(str)
|
45
|
-
end
|
46
|
-
|
47
31
|
private
|
48
32
|
|
49
|
-
#Get the previous result
|
33
|
+
# Get the previous result
|
50
34
|
def result
|
51
35
|
$mysh_exec_result
|
52
36
|
end
|
53
37
|
|
54
|
-
#Reset the state of the execution host.
|
38
|
+
# Reset the state of the execution host.
|
55
39
|
def reset
|
56
40
|
Mysh.reset_host
|
57
41
|
nil
|
58
42
|
end
|
59
43
|
|
60
|
-
#Create a binding for mysh to execute expressions in.
|
61
|
-
def mysh_binding
|
62
|
-
binding
|
63
|
-
end
|
64
44
|
end
|
65
45
|
|
66
46
|
$mysh_exec_host = exec_class.new
|
data/lib/mysh/external.rb
CHANGED
data/lib/mysh/globalize.rb
CHANGED
@@ -27,4 +27,17 @@ class Object
|
|
27
27
|
err.to_s
|
28
28
|
end
|
29
29
|
|
30
|
+
# Get the latest version for the named gem. Patched code.
|
31
|
+
def latest_version_for(name, fetcher=nil)
|
32
|
+
dependency = Gem::Dependency.new(name)
|
33
|
+
fetcher ||= Gem::SpecFetcher.new
|
34
|
+
|
35
|
+
if specs = fetcher.spec_for_dependency(dependency)[0][-1]
|
36
|
+
spec = specs[0]
|
37
|
+
spec && spec.version
|
38
|
+
else
|
39
|
+
"<Not found in repository>"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
30
43
|
end
|
data/lib/mysh/handlebars.rb
CHANGED
@@ -2,21 +2,20 @@
|
|
2
2
|
|
3
3
|
require_relative 'handlebars/string'
|
4
4
|
|
5
|
-
|
5
|
+
# Handlebar embedded ruby support.
|
6
6
|
class Object
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
#
|
11
|
-
|
8
|
+
private
|
9
|
+
|
10
|
+
# Show a file with embedded ruby handlebars.
|
11
|
+
# Note: The message receiver is the evaluation host for the handlebar code.
|
12
|
+
def show_handlebar_file(name, evaluator = $mysh_exec_binding)
|
12
13
|
puts eval_handlebar_file(name, evaluator)
|
13
14
|
end
|
14
15
|
|
15
|
-
#Expand a file with embedded ruby handlebars.
|
16
|
-
|
17
|
-
#
|
18
|
-
#<br>Endemic Code Smells
|
19
|
-
#* :reek:UtilityFunction
|
16
|
+
# Expand a file with embedded ruby handlebars.
|
17
|
+
# Note: The message receiver is the evaluation host for the handlebar code.
|
18
|
+
# Endemic Code Smells :reek:UtilityFunction
|
20
19
|
def eval_handlebar_file(name, evaluator)
|
21
20
|
IO.read(name).preprocess(evaluator)
|
22
21
|
end
|
@@ -1,22 +1,36 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
#Monkey patches for Mysh handlebars
|
3
|
+
# Monkey patches for Mysh handlebars
|
4
4
|
class String
|
5
5
|
|
6
|
-
#Evaluate any variable substitutions in the input.
|
7
|
-
def eval_handlebars(evaluator=$
|
8
|
-
|
9
|
-
code = match[2...-2]
|
10
|
-
silent = code.end_with?("#")
|
11
|
-
result = evaluator.mysh_eval(code)
|
6
|
+
# Evaluate any variable substitutions in the input.
|
7
|
+
def eval_handlebars(evaluator=$mysh_exec_binding)
|
8
|
+
string, text, buffer = self, "", []
|
12
9
|
|
13
|
-
|
10
|
+
until string.empty?
|
11
|
+
text, code, string = string.partition(/{{.*?}}/m)
|
12
|
+
|
13
|
+
if not text.empty?
|
14
|
+
text = text.gsub(/\\[{}]/) {|found| found[1]}
|
15
|
+
buffer << "_m_<<#{text.inspect};"
|
16
|
+
elsif buffer.empty?
|
17
|
+
buffer << ""
|
18
|
+
end
|
19
|
+
|
20
|
+
unless code.empty?
|
21
|
+
if code[-3] == "#"
|
22
|
+
buffer << "#{code[2...-3]};"
|
23
|
+
else
|
24
|
+
buffer << "_m_<<(#{code[2...-2]}).to_s;"
|
25
|
+
end
|
26
|
+
end
|
14
27
|
end
|
15
|
-
end
|
16
28
|
|
17
|
-
|
18
|
-
|
19
|
-
|
29
|
+
if buffer.length > 1
|
30
|
+
evaluator.eval("_m_ = '';" + buffer.join + "_m_")
|
31
|
+
else
|
32
|
+
text
|
33
|
+
end
|
20
34
|
end
|
21
35
|
|
22
36
|
end
|
@@ -30,6 +30,17 @@ those cases, simply end the expression with a '#' character. For example:
|
|
30
30
|
mysh>echo \{\{ "not embedded" #\}\} \{\{ "embedded" \}\}
|
31
31
|
embedded
|
32
32
|
|
33
|
+
A common use for not embedded code snippets is to act as the control
|
34
|
+
structures. For example:
|
35
|
+
|
36
|
+
\{\{ 3.times do |i| #\}\} The count is: \{\{ i+1 \}\}
|
37
|
+
\{\{ end #\}\}
|
38
|
+
|
39
|
+
Yields the following output:
|
40
|
+
|
41
|
+
{{ 3.times do |i| #}} The count is: {{ i+1 }}
|
42
|
+
{{ end #}}
|
43
|
+
|
33
44
|
Finally, it may be that it is desired to embed braces into a text file or
|
34
45
|
the command line. In that case precede the brace with a backslash character
|
35
46
|
like: \\{ or \\}
|
@@ -8,14 +8,12 @@ module Mysh
|
|
8
8
|
|
9
9
|
# Execute the @env shell command.
|
10
10
|
def process_command(_args)
|
11
|
-
print WORKING
|
11
|
+
print WORKING
|
12
12
|
Gem.refresh
|
13
13
|
|
14
14
|
puts "Key mysh environment information.", "",
|
15
15
|
info.format_output_bullets, "",
|
16
16
|
path.format_output_bullets, ""
|
17
|
-
|
18
|
-
@ran_once = true
|
19
17
|
end
|
20
18
|
|
21
19
|
private
|
@@ -28,7 +26,7 @@ module Mysh
|
|
28
26
|
["installed", Gem::Specification.find_all_by_name("mysh")
|
29
27
|
.map{|s| s.version.to_s}
|
30
28
|
.join(", ")],
|
31
|
-
["latest", insouciant {
|
29
|
+
["latest", insouciant {latest_version_for("mysh").to_s}],
|
32
30
|
["init file", $mysh_init_file.to_host_spec],
|
33
31
|
["user", ENV['USER']],
|
34
32
|
["home", (ENV['HOME'] || "").to_host_spec],
|
@@ -8,7 +8,7 @@ module Mysh
|
|
8
8
|
|
9
9
|
#Execute the @gem shell command.
|
10
10
|
def process_command(input)
|
11
|
-
print WORKING
|
11
|
+
print WORKING
|
12
12
|
Gem.refresh
|
13
13
|
|
14
14
|
args = input.cooked_body.split(" ")[1..-1]
|
@@ -19,7 +19,6 @@ module Mysh
|
|
19
19
|
specific(args)
|
20
20
|
end
|
21
21
|
|
22
|
-
@ran_once = true
|
23
22
|
end
|
24
23
|
|
25
24
|
private
|
@@ -61,7 +60,7 @@ module Mysh
|
|
61
60
|
|
62
61
|
# Get gem info on the specified gems
|
63
62
|
def specific(args)
|
64
|
-
details = []
|
63
|
+
fetcher, details = Gem::SpecFetcher.new, []
|
65
64
|
|
66
65
|
args.each do |gem_name|
|
67
66
|
version_list = Gem::Specification.find_all_by_name(gem_name)
|
@@ -69,7 +68,7 @@ module Mysh
|
|
69
68
|
.join(", ")
|
70
69
|
details << [gem_name, version_list]
|
71
70
|
|
72
|
-
latest = insouciant {latest_version_for(gem_name).to_s}
|
71
|
+
latest = insouciant {latest_version_for(gem_name, fetcher).to_s}
|
73
72
|
details << ["latest", latest]
|
74
73
|
details << [" ", " "]
|
75
74
|
end
|
@@ -78,18 +77,6 @@ module Mysh
|
|
78
77
|
details.format_output_bullets
|
79
78
|
end
|
80
79
|
|
81
|
-
# Get the latest version for the named gem. Patched code.
|
82
|
-
def latest_version_for(name)
|
83
|
-
dependency = Gem::Dependency.new(name)
|
84
|
-
fetcher = Gem::SpecFetcher.fetcher
|
85
|
-
if specs = fetcher.spec_for_dependency(dependency)[0][-1]
|
86
|
-
spec = specs[0]
|
87
|
-
spec && spec.version
|
88
|
-
else
|
89
|
-
"<Not found in repository>"
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
80
|
end
|
94
81
|
|
95
82
|
desc = 'Get information about the current gem support or ' +
|
@@ -8,13 +8,11 @@ module Mysh
|
|
8
8
|
|
9
9
|
#Execute the @term shell command.
|
10
10
|
def process_command(_args)
|
11
|
-
print WORKING
|
11
|
+
print WORKING
|
12
12
|
Gem.refresh
|
13
13
|
|
14
14
|
puts "Key term information.", "",
|
15
15
|
info.format_output_bullets, ""
|
16
|
-
|
17
|
-
@ran_once = true
|
18
16
|
end
|
19
17
|
|
20
18
|
private
|
@@ -22,18 +20,20 @@ module Mysh
|
|
22
20
|
# Get the info
|
23
21
|
# Endemic Code Smells :reek:UtilityFunction
|
24
22
|
def info
|
23
|
+
fetcher = Gem::SpecFetcher.new
|
24
|
+
|
25
25
|
[["about", MiniReadline::DESCRIPTION],
|
26
26
|
["version", MiniReadline::VERSION],
|
27
27
|
["installed", Gem::Specification.find_all_by_name("mini_readline")
|
28
28
|
.map{|s| s.version.to_s}
|
29
29
|
.join(", ")],
|
30
|
-
["latest", insouciant {
|
30
|
+
["latest", insouciant {latest_version_for("mini_readline", fetcher).to_s}],
|
31
31
|
["about", MiniTerm::DESCRIPTION],
|
32
32
|
["version", MiniTerm::VERSION],
|
33
33
|
["installed", Gem::Specification.find_all_by_name("mini_term")
|
34
34
|
.map{|s| s.version.to_s}
|
35
35
|
.join(", ")],
|
36
|
-
["latest", insouciant {
|
36
|
+
["latest", insouciant {latest_version_for("mini_term", fetcher).to_s}],
|
37
37
|
["platform", MiniTerm::TERM_PLATFORM.inspect],
|
38
38
|
["term type", MiniTerm::TERM_TYPE.inspect],
|
39
39
|
["columns", MiniTerm.width.to_s],
|
@@ -45,8 +45,6 @@ module Mysh
|
|
45
45
|
]
|
46
46
|
end
|
47
47
|
|
48
|
-
|
49
|
-
|
50
48
|
end
|
51
49
|
|
52
50
|
desc = 'Get information about the console. See ?term for more.'
|
data/lib/mysh/pre_processor.rb
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
class String
|
5
5
|
|
6
6
|
#The mysh string pre-processor stack.
|
7
|
-
def preprocess(evaluator=$
|
8
|
-
self.eval_variables.eval_handlebars(evaluator)
|
7
|
+
def preprocess(evaluator=$mysh_exec_binding)
|
8
|
+
self.eval_variables.eval_handlebars(evaluator)
|
9
9
|
end
|
10
10
|
|
11
11
|
end
|
data/lib/mysh/quick.rb
CHANGED
@@ -9,10 +9,16 @@ module Mysh
|
|
9
9
|
QUICK['!'] = lambda {|input| HISTORY_COMMAND.process_quick_command(input)}
|
10
10
|
QUICK['#'] = lambda {|input| MYSH_COMMENT.process_command(input)}
|
11
11
|
QUICK['%'] = lambda {|input| TIMED_COMMAND.process_command(input)}
|
12
|
-
QUICK['='] = lambda {|input| $mysh_exec_host.execute(input.raw.preprocess)}
|
13
12
|
QUICK['?'] = lambda {|input| HELP_COMMAND.process_quick_command(input)}
|
14
13
|
QUICK['@'] = lambda {|input| SHOW_COMMAND.process_quick_command(input)}
|
15
14
|
|
15
|
+
QUICK['='] = lambda do |input|
|
16
|
+
cmd = "$mysh_exec_result=(" + input.raw[1..-1].preprocess + ")"
|
17
|
+
puts cmd if MNV[:debug].extract_boolean
|
18
|
+
pp $mysh_exec_binding.eval(cmd)
|
19
|
+
:expression
|
20
|
+
end
|
21
|
+
|
16
22
|
#Try to execute the inputing as a quick command.
|
17
23
|
def self.try_execute_quick(input)
|
18
24
|
QUICK[input.quick_command].call(input)
|
data/lib/mysh/version.rb
CHANGED
data/samples/show.txt
CHANGED
data/tests/my_shell_tests.rb
CHANGED
@@ -19,7 +19,6 @@ class MyShellTester < Minitest::Test
|
|
19
19
|
assert_equal(Class, Mysh::ActionPool.class)
|
20
20
|
assert_equal(Module, Mysh::MNV.class)
|
21
21
|
assert_equal(Class, Mysh::Keeper.class)
|
22
|
-
assert_equal(Class, Mysh::BindingWrapper.class)
|
23
22
|
assert_equal(Class, Mysh::InputWrapper.class)
|
24
23
|
|
25
24
|
assert_equal(Mysh::ActionPool, Mysh::COMMANDS.class)
|
@@ -44,13 +43,13 @@ class MyShellTester < Minitest::Test
|
|
44
43
|
|
45
44
|
def test_handlebars
|
46
45
|
assert_equal("ABC 123 DEF",
|
47
|
-
"ABC {{ (1..3).to_a.join }} DEF".
|
46
|
+
"ABC {{ (1..3).to_a.join }} DEF".preprocess)
|
48
47
|
|
49
|
-
assert_equal("ABC", "{{ 'ABC' }}".
|
50
|
-
assert_equal("", "{{ 'ABC' #}}".
|
48
|
+
assert_equal("ABC", "{{ 'ABC' }}".preprocess)
|
49
|
+
assert_equal("", "{{ 'ABC' #}}".preprocess)
|
51
50
|
|
52
|
-
assert_equal("{{ 'ABC' }}", "\\{\\{ 'ABC' \\}\\}".
|
53
|
-
assert_equal("{{A}}", "{{ '{'+'{A}'+'}' }}".
|
51
|
+
assert_equal("{{ 'ABC' }}", "\\{\\{ 'ABC' \\}\\}".preprocess)
|
52
|
+
assert_equal("{{A}}", "{{ '{'+'{A}'+'}' }}".preprocess)
|
54
53
|
end
|
55
54
|
|
56
55
|
def test_command_parsing
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mysh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.11
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Camilleri
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-12-
|
11
|
+
date: 2018-12-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -193,7 +193,6 @@ files:
|
|
193
193
|
- README.md
|
194
194
|
- bin/mysh
|
195
195
|
- lib/mysh.rb
|
196
|
-
- lib/mysh/binding_wrapper.rb
|
197
196
|
- lib/mysh/exceptions.rb
|
198
197
|
- lib/mysh/expression.rb
|
199
198
|
- lib/mysh/expression/lineage.rb
|
data/lib/mysh/binding_wrapper.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
|
3
|
-
#* mysh/internal/binding_wrapper.rb -- An action compatible wrapper for a binding.
|
4
|
-
module Mysh
|
5
|
-
|
6
|
-
#* mysh/internal/binding_wrapper.rb -- An action compatible wrapper for a binding.
|
7
|
-
class BindingWrapper
|
8
|
-
|
9
|
-
#Setup a binding wrapper
|
10
|
-
def initialize(binding)
|
11
|
-
@exec_binding = binding
|
12
|
-
end
|
13
|
-
|
14
|
-
#Evaluate the string in the wrapped context.
|
15
|
-
def mysh_eval(str)
|
16
|
-
@exec_binding.eval(str)
|
17
|
-
end
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
|
-
end
|