simple_scripting 0.9.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.
@@ -0,0 +1,87 @@
1
+ [![Build Status][BS img]](https://travis-ci.org/saveriomiroddi/simple_scripting)
2
+
3
+ # SimpleScripting
4
+
5
+ `SS` is a library composed of two modules (`Argv` and `Configuration`) which simplify two common scripting tasks:
6
+
7
+ - implementing the commandline options parsing (and the related help)
8
+ - loading and decoding the configuration for the script/application
9
+
10
+ `SS` is an interesting (and useful) exercise in design, aimed at finding the simplest and most expressive data/structures which accomplish the given task(s). For this reason, the library can be useful for people who frequently write small scripts (eg. devops or nerds).
11
+
12
+ ## SimpleScripting::Argv
13
+
14
+ `SS::A` is a module which acts as frontend to the standard Option Parser library (`optparse`), giving a very convenient format for specifying the arguments. `SS::A` also generates the help.
15
+
16
+ This is a definition example:
17
+
18
+ result = SimpleOptParse::Argv.decode(
19
+ ['-s', '--only-scheduled-days', 'Only print scheduled days' ],
20
+ ['-d', '--print-defaults TEMPLATE', 'Print the default activities from the named template'],
21
+ 'schedule',
22
+ '[weeks]',
23
+ long_help: 'This is the long help! It can span multiple lines.'
24
+ )
25
+
26
+ which:
27
+
28
+ - optionally accepts the `-s`/`--only-scheduled-days` switch, interpreting it as boolean,
29
+ - optionally accepts the `-d`/`--print-defaults` switch, interpreting it as string,
30
+ - requires the `schedule` argument,
31
+ - optionally accepts the `weeks` argument,
32
+ - automatically adds the `-h` and `--help` switches,
33
+ - prints all the options and the long help if the help is invoked,
34
+ - prints the help and exits if invalid parameters are passed (eg. too many).
35
+
36
+ This is a sample result:
37
+
38
+ {
39
+ only_scheduled_days: true,
40
+ print_defaults: 'my_defaults',
41
+ schedule: 'schedule.txt',
42
+ weeks: '3',
43
+ }
44
+
45
+ This is the corresponding help:
46
+
47
+ Usage: tmpfile [options] <schedule> [<weeks>]
48
+ -s, --only-scheduled-days Only print scheduled days
49
+ -d, --print-defaults TEMPLATE Print the default activities from the named template
50
+ -h, --help Help
51
+
52
+ This is the long help! It can span multiple lines.
53
+
54
+ For the guide, see the [wiki page](https://github.com/saveriomiroddi/simple_scripting/wiki/SimpleScripting::Argv-Guide).
55
+
56
+ ## SimpleScripting::Configuration
57
+
58
+ `SS::C` is a module which acts as frontend to the ParseConfig gem (`parseconfig`), giving compact access to the configuration and its values, and adding a few helpers for common tasks.
59
+
60
+ Say one writes a script (`foo_my_bar.rb`), with a corresponding (`$HOME/.foo_my_bar`) configuration, which contains:
61
+
62
+ some_relative_file_path=foo
63
+ some_absolute_file_path=/path/to/bar
64
+ my_password=uTxllKRD2S+IH92oi30luwu0JIqp7kKA
65
+
66
+ [a_group]
67
+ group_key=baz
68
+
69
+ This is the workflow and functionality offered by `SS::C`:
70
+
71
+ # Picks up automatically the configuration file name, based on the calling program
72
+ #
73
+ configuration = SimpleScripting::Configuration.load(passwords_key: 'encryption_key')
74
+
75
+ configuration.some_relative_file_path.full_path # '$HOME/foo'
76
+ configuration.some_absolute_file_path # '/path/to/bar'
77
+ configuration.some_absolute_file_path.full_path # '/path/to/bar' (recognized as absolute)
78
+
79
+ configuration.my_password.decrypted # 'encrypted_value'
80
+
81
+ configuration.a_group.group_key # 'baz'; also supports #full_path and #decrypted
82
+
83
+ ### Encryption note
84
+
85
+ The purpose of encryption in this library is just to avoid displaying passwords in plaintext; it's not considered safe against attacks.
86
+
87
+ [BS img]: https://travis-ci.org/saveriomiroddi/simple_scripting.svg?branch=master
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task default: :spec
@@ -0,0 +1,126 @@
1
+ require 'optparse'
2
+
3
+ module SimpleScripting
4
+
5
+ module Argv
6
+
7
+ extend self
8
+
9
+ def decode(*params_definition, arguments: ARGV, long_help: nil, output: $stdout)
10
+ # If the param is a Hash, we have multiple commands. We check and if the command is correct,
11
+ # recursively call the function with the specific parameters.
12
+ #
13
+ if params_definition.first.is_a?(Hash)
14
+ command = arguments.shift
15
+ commands_definition = params_definition.first
16
+
17
+ if command == '-h' || command == '--help'
18
+ print_optparse_commands_help(commands_definition, output, false)
19
+ output == $stdout ? exit : return
20
+ end
21
+
22
+ command_params_definition = commands_definition[command]
23
+
24
+ if command_params_definition.nil?
25
+ print_optparse_commands_help(commands_definition, output, true)
26
+ output == $stdout ? exit : return
27
+ else
28
+ return [command, decode(*command_params_definition, arguments: arguments, output: output)]
29
+ end
30
+ end
31
+
32
+ result = {}
33
+ parser_opts_ref = nil # not available outside the block
34
+ args = {} # { 'name' => mandatory? }
35
+
36
+ OptionParser.new do | parser_opts |
37
+ params_definition.each do | param_definition |
38
+ case param_definition
39
+ when Array
40
+ if param_definition[1] && param_definition[1].start_with?('--')
41
+ key = param_definition[1].split(' ')[0][2 .. -1].gsub('-', '_').to_sym
42
+ else
43
+ key = param_definition[0][1 .. -1].to_sym
44
+ end
45
+
46
+ parser_opts.on(*param_definition) do |value|
47
+ result[key] = value || true
48
+ end
49
+ when String
50
+ if param_definition.start_with?('[')
51
+ arg_name = param_definition[1 .. -2].to_sym
52
+
53
+ args[arg_name] = false
54
+ else
55
+ arg_name = param_definition.to_sym
56
+
57
+ args[arg_name] = true
58
+ end
59
+ else
60
+ raise "Unrecognized value: #{param_definition}"
61
+ end
62
+ end
63
+
64
+ parser_opts.on( '-h', '--help', 'Help' ) do
65
+ print_optparse_help( parser_opts, args, long_help, output )
66
+ output == $stdout ? exit : return
67
+ end
68
+
69
+ parser_opts_ref = parser_opts
70
+ end.parse!(arguments)
71
+
72
+ first_arg_name = args.keys.first.to_s
73
+
74
+ # Varargs
75
+ if first_arg_name.start_with?('*')
76
+ # Mandatory?
77
+ if args.fetch(first_arg_name.to_sym)
78
+ if arguments.empty?
79
+ print_optparse_help( parser_opts_ref, args, long_help, output )
80
+ output == $stdout ? exit : return
81
+ else
82
+ name = args.keys.first[ 1 .. - 1 ].to_sym
83
+
84
+ result[ name ] = arguments
85
+ end
86
+ # Optional
87
+ else
88
+ name = args.keys.first[ 1 .. - 1 ].to_sym
89
+
90
+ result[ name ] = arguments
91
+ end
92
+ else
93
+ min_args_size = args.count { | name, mandatory | mandatory }
94
+
95
+ case arguments.size
96
+ when (min_args_size .. args.size)
97
+ arguments.zip(args) do | value, (name, mandatory) |
98
+ result[name] = value
99
+ end
100
+ else
101
+ print_optparse_help(parser_opts_ref, args, long_help, output)
102
+ output == $stdout ? exit : return
103
+ end
104
+ end
105
+
106
+ result
107
+ end
108
+
109
+ private
110
+
111
+ def print_optparse_commands_help(commands_definition, output, is_error)
112
+ output.print "Invalid command. " if is_error
113
+ output.puts "Valid commands:", "", " " + commands_definition.keys.join(', ')
114
+ end
115
+
116
+ def print_optparse_help(parser_opts, args, long_help, output)
117
+ args_display = args.map { | name, mandatory | mandatory ? "<#{ name }>" : "[<#{ name }>]" }.join(' ')
118
+ parser_opts_help = parser_opts.to_s.sub!(/^(Usage: .*)/, "\\1 #{args_display}")
119
+
120
+ output.puts parser_opts_help
121
+ output.puts "", long_help if long_help
122
+ end
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'configuration/value'
2
+
3
+ require 'ostruct'
4
+ require 'parseconfig'
5
+
6
+ module SimpleScripting
7
+
8
+ module Configuration
9
+
10
+ extend self
11
+
12
+ def load(config_file: default_config_file, passwords_key: nil)
13
+ configuration = ParseConfig.new(config_file)
14
+
15
+ convert_to_cool_format(OpenStruct.new, configuration.params, passwords_key)
16
+ end
17
+
18
+ private
19
+
20
+ def default_config_file
21
+ base_config_filename = '.' + File.basename($PROGRAM_NAME).chomp('.rb')
22
+
23
+ File.expand_path(base_config_filename, '~')
24
+ end
25
+
26
+ # Performs two conversions:
27
+ #
28
+ # 1. the configuration as a whole is converted to an OpenStruct
29
+ # 2. the values are converted to SimpleScripting::Configuration::Value
30
+ #
31
+ def convert_to_cool_format(result_node, configuration_node, encryption_key)
32
+ configuration_node.each do |key, value|
33
+ if value.is_a?(Hash)
34
+ result_node[key] = OpenStruct.new
35
+ convert_to_cool_format(result_node[key], value, encryption_key)
36
+ else
37
+ result_node[key] = Value.new(value, encryption_key)
38
+ end
39
+ end
40
+
41
+ result_node
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,61 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module SimpleScripting
5
+
6
+ module Configuration
7
+
8
+ # The purpose of encryption in this library is just to avoid displaying passwords in
9
+ # plaintext; it's not considered safe against attacks.
10
+ #
11
+ class Value < String
12
+
13
+ ENCRYPTION_CIPHER = 'des3'
14
+
15
+ def initialize(string, encryption_key = nil)
16
+ super(string)
17
+
18
+ if encryption_key
19
+ @encryption_key = encryption_key + '*' * (24 - encryption_key.bytesize)
20
+ end
21
+ end
22
+
23
+ def full_path
24
+ start_with?('/') ? self : File.expand_path(self, '~')
25
+ end
26
+
27
+ def decrypted
28
+ raise "Encryption key not provided!" if @encryption_key.nil?
29
+
30
+ ciphertext = Base64.decode64(self)
31
+
32
+ cipher = OpenSSL::Cipher::Cipher.new(ENCRYPTION_CIPHER)
33
+ cipher.decrypt
34
+
35
+ cipher.key = @encryption_key
36
+
37
+ cipher.iv = ciphertext[0...cipher.iv_len]
38
+ plaintext = cipher.update(ciphertext[cipher.iv_len..-1]) + cipher.final
39
+
40
+ plaintext
41
+ end
42
+
43
+ def encrypted
44
+ cipher = OpenSSL::Cipher::Cipher.new(ENCRYPTION_CIPHER)
45
+ cipher.encrypt
46
+
47
+ iv = cipher.random_iv
48
+
49
+ cipher.key = @encryption_key
50
+ cipher.iv = iv
51
+
52
+ ciphertext = iv + cipher.update(self) + cipher.final
53
+
54
+ Base64.encode64(ciphertext).rstrip
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,5 @@
1
+ module SimpleScripting
2
+
3
+ VERSION = "0.9.0"
4
+
5
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: UTF-8
2
+
3
+ $LOAD_PATH << File.expand_path("../lib", __FILE__)
4
+
5
+ require "simple_scripting/version"
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "simple_scripting"
9
+ s.version = SimpleScripting::VERSION
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = ["Saverio Miroddi"]
12
+ s.date = "2017-06-27"
13
+ s.email = ["saverio.pub2@gmail.com"]
14
+ s.homepage = "https://github.com/saveriomiroddi/simple_scripting"
15
+ s.summary = "Library for simplifying some typical scripting functionalities."
16
+ s.description = "Simplifies options parsing and configuration loading."
17
+ s.license = "GPL-3.0"
18
+
19
+ s.add_runtime_dependency "parseconfig", "~> 1.0"
20
+
21
+ s.add_development_dependency "rake", "~> 12.0"
22
+ s.add_development_dependency "rspec", "~> 3.6"
23
+
24
+ s.files = `git ls-files`.split("\n")
25
+ s.test_files = `git ls-files -- spec/*`.split("\n")
26
+ s.executables = []
27
+ s.require_paths = ["lib"]
28
+ end
@@ -0,0 +1,222 @@
1
+ require_relative '../../lib/simple_scripting/argv.rb'
2
+
3
+ require 'stringio'
4
+
5
+ describe SimpleScripting::Argv do
6
+
7
+ let(:output_buffer) do
8
+ StringIO.new
9
+ end
10
+
11
+ describe 'Basic functionality' do
12
+
13
+ let(:decoder_params) {[
14
+ ['-a' ],
15
+ ['-b', '"-b" description'],
16
+ ['-c', '--c-switch' ],
17
+ ['-d', '--d-switch', '"-d" description'],
18
+ ['-e', '--e-switch VALUE' ],
19
+ ['-f', '--f-switch VALUE', '"-f" description'],
20
+ 'mandatory',
21
+ '[optional]',
22
+ long_help: 'This is the long help!',
23
+ output: output_buffer,
24
+ ]}
25
+
26
+ it 'should implement the help' do
27
+ decoder_params.last[:arguments] = ['-h']
28
+
29
+ described_class.decode(*decoder_params)
30
+
31
+ expected_output = %Q{\
32
+ Usage: rspec [options] <mandatory> [<optional>]
33
+ -a
34
+ -b "-b" description
35
+ -c, --c-switch
36
+ -d, --d-switch "-d" description
37
+ -e, --e-switch VALUE
38
+ -f, --f-switch VALUE "-f" description
39
+ -h, --help Help
40
+
41
+ This is the long help!
42
+ }
43
+
44
+ expect(output_buffer.string).to eql(expected_output)
45
+ end
46
+
47
+ it "should implement basic switches and arguments (all set)" do
48
+ decoder_params.last[:arguments] = ['-a', '-b', '-c', '-d', '-ev_swt', '-fv_swt', 'm_arg', 'o_arg']
49
+
50
+ actual_result = described_class.decode(*decoder_params)
51
+
52
+ expected_result = {
53
+ a: true,
54
+ b: true,
55
+ c_switch: true,
56
+ d_switch: true,
57
+ e_switch: 'v_swt',
58
+ f_switch: 'v_swt',
59
+ mandatory: 'm_arg',
60
+ optional: 'o_arg',
61
+ }
62
+
63
+ expect(actual_result).to eql(expected_result)
64
+ end
65
+
66
+ it "should implement basic switches and arguments (no optional argument)" do
67
+ decoder_params.last[:arguments] = ['m_arg']
68
+
69
+ actual_result = described_class.decode(*decoder_params)
70
+
71
+ expected_result = {
72
+ mandatory: 'm_arg',
73
+ }
74
+
75
+ expect(actual_result).to eql(expected_result)
76
+ end
77
+
78
+ end
79
+
80
+ describe 'Varargs' do
81
+
82
+ describe '(mandatory)' do
83
+
84
+ let(:decoder_params) {[
85
+ '*varargs',
86
+ output: output_buffer,
87
+ ]}
88
+
89
+ it "should be decoded" do
90
+ decoder_params.last[:arguments] = ['varval1', 'varval2']
91
+
92
+ actual_result = described_class.decode(*decoder_params)
93
+
94
+ expected_result = {
95
+ varargs: ['varval1', 'varval2'],
96
+ }
97
+
98
+ expect(actual_result).to eql(expected_result)
99
+ end
100
+
101
+ it "should exit when they are not specified" do
102
+ decoder_params.last[:arguments] = []
103
+
104
+ actual_result = described_class.decode(*decoder_params)
105
+
106
+ expected_result = nil
107
+
108
+ expect(actual_result).to eql(expected_result)
109
+ end
110
+
111
+ end
112
+
113
+ describe '(optional)' do
114
+
115
+ let(:decoder_params) {[
116
+ '[*varargs]',
117
+ output: output_buffer,
118
+ ]}
119
+
120
+ it "should be decoded" do
121
+ decoder_params.last[:arguments] = ['varval1', 'varval2']
122
+
123
+ actual_result = described_class.decode(*decoder_params)
124
+
125
+ expected_result = {
126
+ varargs: ['varval1', 'varval2'],
127
+ }
128
+
129
+ expect(actual_result).to eql(expected_result)
130
+ end
131
+
132
+ it "should be allowed not to be specified" do
133
+ decoder_params.last[:arguments] = []
134
+
135
+ actual_result = described_class.decode(*decoder_params)
136
+
137
+ expected_result = {
138
+ varargs: [],
139
+ }
140
+
141
+ expect(actual_result).to eql(expected_result)
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+
148
+ describe 'Multiple commands' do
149
+
150
+ describe 'regular case' do
151
+
152
+ let(:decoder_params) {{
153
+ 'command1' => [
154
+ 'arg1'
155
+ ],
156
+ 'command2' => [
157
+ 'arg2'
158
+ ],
159
+ output: output_buffer,
160
+ }}
161
+
162
+ it 'should be decoded' do
163
+ decoder_params[:arguments] = ['command1', 'value1']
164
+
165
+ actual_result = described_class.decode(decoder_params)
166
+
167
+ expected_result = ['command1', arg1: 'value1']
168
+
169
+ expect(actual_result).to eql(expected_result)
170
+ end
171
+
172
+ it 'print a message on wrong command' do
173
+ decoder_params[:arguments] = ['pizza']
174
+
175
+ described_class.decode(decoder_params)
176
+
177
+ expected_output = %Q{\
178
+ Invalid command. Valid commands:
179
+
180
+ command1, command2
181
+ }
182
+
183
+ expect(output_buffer.string).to eql(expected_output)
184
+ end
185
+
186
+ it 'should implement the help' do
187
+ decoder_params[:arguments] = ['-h']
188
+
189
+ described_class.decode(decoder_params)
190
+
191
+ expected_output = %Q{\
192
+ Valid commands:
193
+
194
+ command1, command2
195
+ }
196
+
197
+ expect(output_buffer.string).to eql(expected_output)
198
+ end
199
+
200
+ end
201
+
202
+ describe 'pitfall' do
203
+
204
+ let(:decoder_params) {{
205
+ output: output_buffer,
206
+ }}
207
+
208
+ # Make sure that the options (in this case, :output) are not interpreted as commands definition.
209
+ #
210
+ it 'should be avoided' do
211
+ decoder_params[:arguments] = ['pizza']
212
+
213
+ actual_result = described_class.decode(decoder_params)
214
+
215
+ expect(actual_result).to be(nil)
216
+ end
217
+
218
+ end
219
+
220
+ end
221
+
222
+ end