sym 0.1.0 → 2.0.0
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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +25 -0
- data/.document +2 -0
- data/.gitignore +6 -2
- data/.rspec +1 -1
- data/.rubocop.yml +1156 -0
- data/.travis.yml +10 -2
- data/.yardopts +5 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/MANAGING-KEYS.md +67 -0
- data/README.md +444 -12
- data/Rakefile +10 -2
- data/bin/sym.bash-completion +24 -0
- data/exe/keychain +38 -0
- data/exe/sym +20 -0
- data/lib/sym.rb +110 -2
- data/lib/sym/app.rb +56 -0
- data/lib/sym/app/args.rb +42 -0
- data/lib/sym/app/cli.rb +192 -0
- data/lib/sym/app/commands.rb +56 -0
- data/lib/sym/app/commands/command.rb +77 -0
- data/lib/sym/app/commands/delete_keychain_item.rb +17 -0
- data/lib/sym/app/commands/encrypt_decrypt.rb +26 -0
- data/lib/sym/app/commands/generate_key.rb +37 -0
- data/lib/sym/app/commands/open_editor.rb +97 -0
- data/lib/sym/app/commands/print_key.rb +15 -0
- data/lib/sym/app/commands/show_examples.rb +76 -0
- data/lib/sym/app/commands/show_help.rb +16 -0
- data/lib/sym/app/commands/show_language_examples.rb +81 -0
- data/lib/sym/app/commands/show_version.rb +14 -0
- data/lib/sym/app/input/handler.rb +41 -0
- data/lib/sym/app/keychain.rb +135 -0
- data/lib/sym/app/nlp.rb +18 -0
- data/lib/sym/app/nlp/constants.rb +32 -0
- data/lib/sym/app/nlp/translator.rb +61 -0
- data/lib/sym/app/nlp/usage.rb +72 -0
- data/lib/sym/app/output.rb +15 -0
- data/lib/sym/app/output/base.rb +61 -0
- data/lib/sym/app/output/file.rb +18 -0
- data/lib/sym/app/output/noop.rb +14 -0
- data/lib/sym/app/output/stdout.rb +13 -0
- data/lib/sym/app/password/cache.rb +63 -0
- data/lib/sym/app/private_key/base64_decoder.rb +17 -0
- data/lib/sym/app/private_key/decryptor.rb +71 -0
- data/lib/sym/app/private_key/detector.rb +42 -0
- data/lib/sym/app/private_key/handler.rb +44 -0
- data/lib/sym/app/short_name.rb +10 -0
- data/lib/sym/application.rb +114 -0
- data/lib/sym/cipher_handler.rb +46 -0
- data/lib/sym/configuration.rb +39 -0
- data/lib/sym/data.rb +23 -0
- data/lib/sym/data/decoder.rb +28 -0
- data/lib/sym/data/encoder.rb +24 -0
- data/lib/sym/data/wrapper_struct.rb +43 -0
- data/lib/sym/encrypted_file.rb +34 -0
- data/lib/sym/errors.rb +37 -0
- data/lib/sym/extensions/class_methods.rb +12 -0
- data/lib/sym/extensions/instance_methods.rb +114 -0
- data/lib/sym/version.rb +1 -1
- data/sym.gemspec +34 -15
- metadata +224 -9
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'tsort'
|
3
|
+
require 'pp'
|
4
|
+
module Sym
|
5
|
+
module App
|
6
|
+
module Commands
|
7
|
+
|
8
|
+
class DependencyResolver < Hash
|
9
|
+
include TSort
|
10
|
+
alias tsort_each_node each_key
|
11
|
+
|
12
|
+
def tsort_each_child(node, &block)
|
13
|
+
fetch(node).each(&block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
@dependency = DependencyResolver.new
|
18
|
+
@commands = Set.new
|
19
|
+
|
20
|
+
class << self
|
21
|
+
attr_accessor :commands, :dependency
|
22
|
+
|
23
|
+
def register(command_class)
|
24
|
+
self.commands << command_class
|
25
|
+
self.dependency[command_class.short_name] ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
def order(command_class, after)
|
29
|
+
self.dependency[command_class.short_name].unshift(after) if after
|
30
|
+
self.dependency[command_class.short_name].flatten!
|
31
|
+
end
|
32
|
+
|
33
|
+
def dependencies
|
34
|
+
@dependencies ||= self.dependency.tsort
|
35
|
+
@dependencies
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sort commands based on the #dependencies array, which itself is sorted
|
39
|
+
# based on command dependencies.
|
40
|
+
def sorted_commands
|
41
|
+
@sorted_commands ||= self.commands.to_a.sort_by{|klass| dependencies.index(klass.short_name) }
|
42
|
+
@sorted_commands
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_command_class(opts)
|
46
|
+
self.sorted_commands.each do |command_class|
|
47
|
+
return command_class if command_class.options_satisfied_by?(opts)
|
48
|
+
end
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
require_dir 'sym/app/commands'
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'sym'
|
2
|
+
require 'sym/app'
|
3
|
+
|
4
|
+
require 'active_support/inflector'
|
5
|
+
|
6
|
+
module Sym
|
7
|
+
module App
|
8
|
+
module Commands
|
9
|
+
class Command
|
10
|
+
|
11
|
+
def self.inherited(klass)
|
12
|
+
klass.instance_eval do
|
13
|
+
class << self
|
14
|
+
attr_accessor :required, :incompatible
|
15
|
+
|
16
|
+
include Sym::App::ShortName
|
17
|
+
|
18
|
+
def try_after(*dependencies)
|
19
|
+
Sym::App::Commands.order(self, dependencies)
|
20
|
+
end
|
21
|
+
|
22
|
+
def required_options(*args)
|
23
|
+
self.required ||= Set.new
|
24
|
+
required.merge(args) if args
|
25
|
+
required
|
26
|
+
end
|
27
|
+
|
28
|
+
def incompatible_options(*args)
|
29
|
+
self.incompatible ||= Set.new
|
30
|
+
incompatible.merge(args) if args
|
31
|
+
incompatible
|
32
|
+
end
|
33
|
+
|
34
|
+
def options_satisfied_by?(opts_hash)
|
35
|
+
proc = required_options.find { |option| option.is_a?(Proc) }
|
36
|
+
return true if proc && proc.call(opts_hash)
|
37
|
+
return false if incompatible_options.any? { |option| opts_hash[option] }
|
38
|
+
required_options.to_a.delete_if { |o| o.is_a?(Proc) }.all? { |o|
|
39
|
+
o.is_a?(Array) ? o.any? { |opt| opts_hash[opt] } : opts_hash[o]
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Register this command with the global list.
|
45
|
+
Sym::App::Commands.register klass
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_accessor :application
|
50
|
+
|
51
|
+
def initialize(application)
|
52
|
+
self.application = application
|
53
|
+
end
|
54
|
+
|
55
|
+
def opts
|
56
|
+
application.opts
|
57
|
+
end
|
58
|
+
def opts_hash
|
59
|
+
application.opts_hash
|
60
|
+
end
|
61
|
+
|
62
|
+
def key
|
63
|
+
@key ||= application.key
|
64
|
+
end
|
65
|
+
|
66
|
+
def execute
|
67
|
+
raise Sym::Errors::AbstractMethodCalled.new(:run)
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
"#{self.class.short_name.to_s.bold.yellow}, with options: #{application.args.argv.join(' ').gsub(/--/, '').bold.green}"
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
require 'sym/app/keychain'
|
3
|
+
module Sym
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class DeleteKeychainItem < Command
|
7
|
+
|
8
|
+
required_options :keychain_del
|
9
|
+
try_after :generate_key, :open_editor, :encrypt_decrypt
|
10
|
+
|
11
|
+
def execute
|
12
|
+
Sym::App::KeyChain.new(opts[:keychain_del]).delete
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
module Sym
|
3
|
+
module App
|
4
|
+
module Commands
|
5
|
+
class EncryptDecrypt < Command
|
6
|
+
include Sym
|
7
|
+
|
8
|
+
required_options [ :private_key, :keyfile, :keychain, :interactive ],
|
9
|
+
[ :encrypt, :decrypt ],
|
10
|
+
[ :file, :string ]
|
11
|
+
|
12
|
+
try_after :generate_key
|
13
|
+
|
14
|
+
def execute
|
15
|
+
send(application.action, content, application.key)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def content
|
21
|
+
@content ||= (opts[:string] || (opts[:file].eql?('-') ? STDIN.read : File.read(opts[:file])))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
require 'sym/app/keychain'
|
3
|
+
module Sym
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class GenerateKey < Command
|
7
|
+
include Sym
|
8
|
+
|
9
|
+
required_options :generate
|
10
|
+
|
11
|
+
def execute
|
12
|
+
retries ||= 0
|
13
|
+
new_private_key = self.class.create_private_key
|
14
|
+
new_private_key = encr_password(new_private_key,
|
15
|
+
application.input_handler.new_password) if opts[:password]
|
16
|
+
|
17
|
+
clipboard_copy(new_private_key) if opts[:copy]
|
18
|
+
|
19
|
+
Sym::App::KeyChain.new(opts[:keychain], opts).
|
20
|
+
add(new_private_key) if opts[:keychain] && Sym::App.is_osx?
|
21
|
+
|
22
|
+
new_private_key
|
23
|
+
rescue Sym::Errors::PasswordsDontMatch, Sym::Errors::PasswordTooShort => e
|
24
|
+
STDERR.puts e.message.bold
|
25
|
+
retry if (retries += 1) < 3
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def clipboard_copy(key)
|
31
|
+
require 'clipboard'
|
32
|
+
Clipboard.copy(key)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'sym'
|
5
|
+
require 'sym/errors'
|
6
|
+
require_relative 'command'
|
7
|
+
module Sym
|
8
|
+
module App
|
9
|
+
module Commands
|
10
|
+
class OpenEditor < Command
|
11
|
+
include Sym
|
12
|
+
|
13
|
+
required_options [ :private_key, :keyfile, :keychain, :interactive ],
|
14
|
+
:edit,
|
15
|
+
:file
|
16
|
+
|
17
|
+
try_after :generate_key, :encrypt_decrypt
|
18
|
+
|
19
|
+
attr_accessor :tempfile
|
20
|
+
|
21
|
+
def execute
|
22
|
+
begin
|
23
|
+
self.tempfile = ::Tempfile.new(::Base64.urlsafe_encode64(opts[:file]))
|
24
|
+
decrypt_content(self.tempfile)
|
25
|
+
|
26
|
+
result = process launch_editor
|
27
|
+
ensure
|
28
|
+
self.tempfile.close if tempfile
|
29
|
+
self.tempfile.unlink rescue nil
|
30
|
+
end
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
def launch_editor
|
35
|
+
system("#{application.editor} #{tempfile.path}")
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def decrypt_content(file)
|
41
|
+
file.open
|
42
|
+
file.write(content)
|
43
|
+
file.flush
|
44
|
+
end
|
45
|
+
|
46
|
+
def content
|
47
|
+
@content ||= decr(File.read(opts[:file]), key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def timestamp
|
51
|
+
@timestamp ||= Time.now.to_a.select { |d| d.is_a?(Fixnum) }.map { |d| '%02d' % d }[0..-3].reverse.join
|
52
|
+
end
|
53
|
+
|
54
|
+
def process(code)
|
55
|
+
if code == true
|
56
|
+
content_edited = File.read(tempfile.path)
|
57
|
+
md5 = ::Base64.encode64(Digest::MD5.new.digest(content))
|
58
|
+
md5_edited = ::Base64.encode64(Digest::MD5.new.digest(content_edited))
|
59
|
+
return 'No changes have been made.' if md5 == md5_edited
|
60
|
+
|
61
|
+
FileUtils.cp opts[:file], "#{opts[:file]}.#{timestamp}" if opts[:backup]
|
62
|
+
|
63
|
+
diff = compute_diff
|
64
|
+
|
65
|
+
File.open(opts[:file], 'w') { |f| f.write(encr(content_edited, key)) }
|
66
|
+
|
67
|
+
out = ''
|
68
|
+
if opts[:verbose]
|
69
|
+
out << "Saved encrypted/compressed content to #{opts[:file].bold.blue}" +
|
70
|
+
" (#{File.size(opts[:file]) / 1024}Kb), unencrypted size #{content.length / 1024}Kb."
|
71
|
+
out << (opts[:backup] ? ",\nbacked up the last version to #{backup_file.bold.blue}." : '.')
|
72
|
+
end
|
73
|
+
out << "\n\nDiff:\n#{diff}"
|
74
|
+
out
|
75
|
+
else
|
76
|
+
raise Sym::Errors::EditorExitedAbnormally.new("#{application.editor} exited with #{$<}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Computes the diff between two unencrypted versions
|
81
|
+
def compute_diff
|
82
|
+
original_content_file = Tempfile.new(rand(1024).to_s)
|
83
|
+
original_content_file.open
|
84
|
+
original_content_file.write(content)
|
85
|
+
original_content_file.flush
|
86
|
+
diff = `diff #{original_content_file.path} #{tempfile.path}`
|
87
|
+
diff.gsub!(/> (.*\n)/m, '\1'.green)
|
88
|
+
diff.gsub!(/< (.*\n)/m, '\1'.red)
|
89
|
+
ensure
|
90
|
+
original_content_file.close
|
91
|
+
original_content_file.unlink
|
92
|
+
diff
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'colored2'
|
2
|
+
require_relative 'command'
|
3
|
+
module Sym
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class ShowExamples < Command
|
7
|
+
required_options :examples
|
8
|
+
try_after :show_help
|
9
|
+
|
10
|
+
def execute
|
11
|
+
output = []
|
12
|
+
|
13
|
+
output << example(comment: 'generate a new private key into an environment variable:',
|
14
|
+
command: 'export KEY=$(sym -g)',
|
15
|
+
echo: 'echo $KEY',
|
16
|
+
result: '75ngenJpB6zL47/8Wo7Ne6JN1pnOsqNEcIqblItpfg4='.green)
|
17
|
+
|
18
|
+
output << example(comment: 'generate a new password-protected key, copy to the clipboard & save to a file',
|
19
|
+
command: 'sym -gpc -o ~/.key',
|
20
|
+
echo: 'New Password : ' + '••••••••••'.green,
|
21
|
+
result: 'Confirm Password : ' + '••••••••••'.green)
|
22
|
+
|
23
|
+
output << example(comment: 'encrypt a plain text string with a key, and save the output to a file',
|
24
|
+
command: 'sym -e -s ' + '"secret string"'.bold.yellow + ' -k $KEY -o file.enc',
|
25
|
+
echo: 'cat file.enc',
|
26
|
+
result: 'Y09MNDUyczU1S0UvelgrLzV0RTYxZz09CkBDMEw4Q0R0TmpnTm9md1QwNUNy%T013PT0K'.green)
|
27
|
+
|
28
|
+
output << example(comment: 'decrypt a previously encrypted string:',
|
29
|
+
command: 'sym -d -s $(cat file.enc) -k $KEY',
|
30
|
+
result: 'secret string'.green)
|
31
|
+
|
32
|
+
output << example(comment: 'encrypt sym.yml and save it to sym.enc:',
|
33
|
+
command: 'sym -e -f sym.yml -o sym.enc -k $KEY')
|
34
|
+
|
35
|
+
output << example(comment: 'decrypt an encrypted file and print it to STDOUT:',
|
36
|
+
command: 'sym -df sym.enc -k $KEY')
|
37
|
+
|
38
|
+
output << example(comment: 'edit an encrypted file in $EDITOR, ask for key, create a backup',
|
39
|
+
command: 'sym -tibf ecrets.enc',
|
40
|
+
result: '
|
41
|
+
Private Key: ••••••••••••••••••••••••••••••••••••••••••••
|
42
|
+
Saved encrypted content to sym.enc.
|
43
|
+
|
44
|
+
Diff:
|
45
|
+
3c3
|
46
|
+
'.white.dark + '# (c) 2015 Konstantin Gredeskoul. All rights reserved.'.red.bold + '
|
47
|
+
---' + '
|
48
|
+
# (c) 2016 Konstantin Gredeskoul. All rights reserved.'.green.bold)
|
49
|
+
|
50
|
+
|
51
|
+
if Sym::App.is_osx?
|
52
|
+
output << example(comment: 'generate a new password-encrypted key, save it to your Keychain:',
|
53
|
+
command: 'sym -gpx mykey -o ~/.key')
|
54
|
+
|
55
|
+
output << example(comment: 'use the new key to encrypt a file:',
|
56
|
+
command: 'sym -x mykey -e -f password.txt -o passwords.enc')
|
57
|
+
|
58
|
+
output << example(comment: 'use the new key to inline-edit the encrypted file:',
|
59
|
+
command: 'sym -x mykey -t -f sym.yml')
|
60
|
+
end
|
61
|
+
|
62
|
+
output.flatten.compact.join("\n")
|
63
|
+
end
|
64
|
+
|
65
|
+
def example(comment: nil, command: nil, echo: nil, result: nil)
|
66
|
+
out = []
|
67
|
+
out << "# #{comment}".white.dark.italic if comment
|
68
|
+
out << "#{command}" if command
|
69
|
+
out << "#{echo}" if echo
|
70
|
+
out << "#{result}" if result
|
71
|
+
out << '—'*80
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
module Sym
|
3
|
+
module App
|
4
|
+
module Commands
|
5
|
+
class ShowHelp < Command
|
6
|
+
|
7
|
+
required_options :help, ->(opts) { opts.to_hash.keys.all? { |k| !opts[k] } }
|
8
|
+
try_after :generate_key, :open_editor, :encrypt_decrypt
|
9
|
+
|
10
|
+
def execute
|
11
|
+
opts.to_s(prefix: ' ' * 2)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'colored2'
|
2
|
+
require_relative 'command'
|
3
|
+
require_relative '../nlp'
|
4
|
+
module Sym
|
5
|
+
module App
|
6
|
+
module Commands
|
7
|
+
class ShowLanguageExamples < Command
|
8
|
+
required_options :language
|
9
|
+
try_after :show_help
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
def execute
|
14
|
+
output = []
|
15
|
+
|
16
|
+
output << Sym::App::NLP::Base.usage
|
17
|
+
|
18
|
+
output << example(comment: 'generate a new private key and copy to the clipboard but do not print to terminal',
|
19
|
+
command: 'sym create new key to clipboard quietly'
|
20
|
+
)
|
21
|
+
|
22
|
+
output << example(comment: 'generate and save to a file a password-protected key, silently',
|
23
|
+
command: 'sym create a secure key and save it to "my.key"',
|
24
|
+
)
|
25
|
+
|
26
|
+
output << example(comment: 'encrypt a plain text string with a key, and save the output to a file',
|
27
|
+
command: 'sym encrypt string "secret string" using $(cat my.key) save to file.enc')
|
28
|
+
|
29
|
+
output << example(comment: 'decrypt a previously encrypted string:',
|
30
|
+
command: 'sym decrypt string $ENC using $(cat my.key)')
|
31
|
+
|
32
|
+
output << example(comment: 'encrypt "file.txt" with key from my.key and save it to file.enc',
|
33
|
+
command: 'sym encrypt file file.txt with key from my.key and save it to file.enc')
|
34
|
+
|
35
|
+
output << example(comment: 'decrypt an encrypted file and print it to STDOUT:',
|
36
|
+
command: 'sym decrypt file file.enc with key from "my.key"')
|
37
|
+
|
38
|
+
output << example(comment: 'edit an encrypted file in $EDITOR, ask for key, and create a backup upon save',
|
39
|
+
command: 'sym edit file file.enc ask for a key and make a backup',
|
40
|
+
)
|
41
|
+
|
42
|
+
if Sym::App.is_osx?
|
43
|
+
output << example(comment: 'generate a new password-encrypted key, save it to your Keychain:',
|
44
|
+
command: 'sym create a new protected key store in keychain "my-keychain-key"')
|
45
|
+
|
46
|
+
output << example(comment: 'print the key stored in the keychain item "my-keychain-key"',
|
47
|
+
command: 'sym print keychain "my-keychain-key"')
|
48
|
+
|
49
|
+
output << example(comment: 'use the new key to encrypt a file:',
|
50
|
+
command: 'sym encrypt with keychain "my-keychain-key" file "password.txt" and write to "passwords.enc"')
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
output.flatten.compact.join("\n")
|
55
|
+
end
|
56
|
+
|
57
|
+
def example(comment: nil, command: nil, echo: nil, result: nil)
|
58
|
+
@dict ||= ::Sym::App::NLP::Constants::DICTIONARY.to_a.flatten!
|
59
|
+
_command = command.split(' ').map do |w|
|
60
|
+
_w = w.to_sym
|
61
|
+
if w == 'sym'
|
62
|
+
w.italic.yellow
|
63
|
+
elsif ::Sym::App::NLP::Constants::STRIPPED.include?(_w)
|
64
|
+
w.italic.red
|
65
|
+
elsif @dict.include?(_w)
|
66
|
+
w.blue
|
67
|
+
else
|
68
|
+
w
|
69
|
+
end
|
70
|
+
end.join(' ') if command
|
71
|
+
out = []
|
72
|
+
out << "# #{comment}".white.dark.italic if comment
|
73
|
+
out << "#{_command}" if command
|
74
|
+
out << "#{echo}" if echo
|
75
|
+
out << "#{result}" if result
|
76
|
+
out << (' '*80).dark
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|