balmora 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9f7d56a33f3e8d7e5e35beeb227e3881023ad7ca
4
+ data.tar.gz: 0a4e21d622e6a0c78c0ad5956d0b07b15221525e
5
+ SHA512:
6
+ metadata.gz: 766373a8b25c96fc88762099c517f6bbd8c66d1f404c95c4ed8dbdfe2286da69664c1b694d513fd55de58536d090d810a7ae4cd5e2996085af7c366866a8a8d8
7
+ data.tar.gz: ec88c1d20741eda666acf3083a63293d2b1730915cf0f3674b80bd624d80cb30e90ad3809cdb13287186d7ffb5e56cf1f0771b3df9c377cf82143cd562cdfbac
data/bin/balmora ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'balmora/cli'
4
+
5
+ result = Balmora::Cli.new().run(ARGV.clone())
6
+ exit(result)
data/lib/balmora.rb ADDED
@@ -0,0 +1,77 @@
1
+ class Balmora
2
+
3
+ class Error < StandardError; end
4
+ class Restart < StandardError; end
5
+ class Stop < StandardError
6
+
7
+ attr_reader :status
8
+
9
+ def initialize(message, status)
10
+ super(message)
11
+ @status = status
12
+ end
13
+
14
+ end
15
+
16
+ def self.run(task, arguments = {}, options = {})
17
+ state = Balmora::State.create(options, arguments)
18
+ state.balmora.run(task, state)
19
+ end
20
+
21
+ # def self.create(options = {}, arguments = {})
22
+ # state = Balmora::State.create(options, arguments)
23
+ # return state.balmora
24
+ # end
25
+
26
+ def self.factory(state)
27
+ return self.new(state.logger, state.extension, state)
28
+ end
29
+
30
+ def initialize(logger, extension, state)
31
+ @logger = logger
32
+ @extension = extension
33
+ @state = state
34
+ end
35
+
36
+ def run(task, state)
37
+ restarts = state.config.get([:max_restarts], default: 3)
38
+
39
+ restarts.
40
+ times() { |index|
41
+ begin
42
+ dir = state.config.get(:chdir, default: Dir.pwd)
43
+ dir = state.variables.inject(dir)
44
+ Dir.chdir(dir) {
45
+ commands = state.config.get([:tasks, task.to_sym(), :commands])
46
+ run_commands(state, commands)
47
+ }
48
+ return 0
49
+ rescue Restart
50
+ @logger.debug("Restarting task (#{restarts - index} attempts left)")
51
+ state = Balmora::State.create(@state.options, @state.arguments)
52
+ end
53
+ }
54
+
55
+
56
+ raise Error.new("Maximal restart attempts count (#{restarts}) reached")
57
+ rescue Stop => stop
58
+ @logger.debug("Stop with status #{stop.status} catched")
59
+ return stop.status
60
+ end
61
+
62
+ def run_commands(state, commands)
63
+ commands.
64
+ each() { |command|
65
+ run_command(state, command)
66
+ }
67
+ end
68
+
69
+ def run_command(state, command)
70
+ @extension.
71
+ create_command(state, command).
72
+ execute()
73
+ end
74
+
75
+ end
76
+
77
+ require 'balmora/require.rb'
@@ -0,0 +1,175 @@
1
+ class Balmora::Arguments
2
+
3
+ class Error < StandardError; end
4
+
5
+ def self.parse(options, argv)
6
+ shortcuts = _shortcuts(options)
7
+
8
+ result = {}
9
+ index = 0
10
+ while index < argv.length
11
+ arg = argv[index]
12
+
13
+ if arg.start_with?('--')
14
+ new_index = _parse_key(result, options, argv, index, arg[2..-1])
15
+ elsif arg.start_with?('-')
16
+ new_index = _parse_flags(result, options, argv, index, shortcuts, arg)
17
+ else
18
+ new_index = nil
19
+ end
20
+
21
+ if new_index.nil?()
22
+ return result, argv[index..-1]
23
+ end
24
+
25
+ index = new_index + 1
26
+ end
27
+
28
+ return result, nil
29
+ end
30
+
31
+ protected
32
+
33
+ def self._shortcuts(options)
34
+ shortcuts = Hash[
35
+ options.
36
+ collect() { |key, option|
37
+ if !option.has_key?(:shortcut)
38
+ next nil
39
+ end
40
+
41
+ [option[:shortcut].to_sym(), key]
42
+ }.
43
+ compact()
44
+ ]
45
+
46
+ return shortcuts
47
+ end
48
+
49
+ def self._parse_flags(result, options, argv, index, shortcuts, arg)
50
+ new_index = nil
51
+
52
+ if arg.length > 2
53
+ arg[1..-1].split('').each() { |flag|
54
+ new_index = _parse_key(result, options, argv, index,
55
+ shortcuts[flag.to_sym()], true)
56
+
57
+ if new_index.nil?()
58
+ return nil
59
+ end
60
+ }
61
+ else
62
+ new_index = _parse_key(result, options, argv, index,
63
+ shortcuts[arg[1..-1].to_sym()])
64
+ end
65
+
66
+ return new_index
67
+ end
68
+
69
+ def self._parse_key(result, options, argv, index, key, flag_required = false)
70
+ if key.nil?()
71
+ return nil
72
+ end
73
+
74
+ key = key.to_sym()
75
+
76
+ if !options.has_key?(key)
77
+ return nil
78
+ end
79
+
80
+ option = options[key]
81
+ if option[:flag] == true
82
+ result[key] = true
83
+ else
84
+ if flag_required
85
+ return nil
86
+ end
87
+
88
+ value = argv[index + 1]
89
+ result[key] = value
90
+ index += 1
91
+ end
92
+
93
+ return index
94
+ end
95
+
96
+ public
97
+
98
+ def self.help(options)
99
+ lines = []
100
+ options.each() { |long, option|
101
+ if option.instance_of?(::String)
102
+ lines.push(option)
103
+ next
104
+ end
105
+
106
+ arg =
107
+ if option[:shortcut].nil?()
108
+ (' ' * 7) + '--' + long.to_s()
109
+ else
110
+ (' ' * 4) + '-' + option[:shortcut] + ', ' + '--' + long.to_s()
111
+ end
112
+
113
+ lines.push([arg, option[:description]])
114
+ }
115
+
116
+ return format(lines)
117
+ end
118
+
119
+ def self.format(lines)
120
+ indent =
121
+ lines.
122
+ inject(0) { |current, line|
123
+ if line.instance_of?(::String)
124
+ next current
125
+ end
126
+
127
+ if current < line[0].length
128
+ current = line[0].length
129
+ end
130
+
131
+ current
132
+ } +
133
+ 4
134
+
135
+ result = []
136
+ lines.each() { |line|
137
+ if line.instance_of?(::String)
138
+ result.push(line)
139
+ next
140
+ end
141
+
142
+ summary = _wrap(line[1] || '', 60)
143
+ result.push(line[0] + (' ' * (indent - line[0].length)) +
144
+ (summary[0] || ''))
145
+
146
+ (summary[1..-1] || []).each() { |ln| result.push(' ' * indent + ln) }
147
+
148
+ result.push('')
149
+ }
150
+
151
+ return result.join("\n").rstrip()
152
+ end
153
+
154
+ def self._wrap(text, length)
155
+ line = ''
156
+ result = []
157
+
158
+ text.split("\n").each() { |text_line|
159
+ text_line.split(' ').each() { |word|
160
+ if (line + word + ' ').length > length
161
+ result.push(line.rstrip())
162
+ line = ''
163
+ end
164
+
165
+ line += word + ' '
166
+ }
167
+
168
+ result.push(line.rstrip())
169
+ line = ''
170
+ }
171
+
172
+ return result
173
+ end
174
+
175
+ end
@@ -0,0 +1,150 @@
1
+ require 'balmora'
2
+ require 'balmora/arguments'
3
+ require 'optparse'
4
+
5
+ class Balmora::Cli
6
+
7
+ class Error < StandardError; end
8
+
9
+ attr_accessor :out, :err, :rescue
10
+
11
+ def initialize()
12
+ _create_options()
13
+
14
+ @out = STDOUT
15
+ @err = STDERR
16
+ @rescue = true
17
+
18
+ @arguments = Balmora::Arguments
19
+ end
20
+
21
+ def _create_options()
22
+ @options = {
23
+ config: {
24
+ shortcut: 'c',
25
+ description: 'Configuration file; if not specified, balmora will ' +
26
+ 'look configuration file at ~/.config/balmora/balmora.conf and ' +
27
+ '/etc/balmora/balmora.conf in order',
28
+ },
29
+
30
+ quite: {
31
+ shortcut: 'q',
32
+ flag: true,
33
+ description: 'Quite mode; reports only errors',
34
+ },
35
+
36
+ verbose: {
37
+ shortcut: 'v',
38
+ flag: true,
39
+ description: 'Verbose mode; reports all executed commands',
40
+ },
41
+
42
+ help: {
43
+ shortcut: 'h',
44
+ flag: true,
45
+ description: 'Show help; if --config specified in arguments, shows ' +
46
+ 'config help; if TASK specified, shows task help',
47
+ },
48
+ }
49
+ end
50
+
51
+ def run(argv)
52
+ args, tail = @arguments.parse(@options, argv)
53
+ state = Balmora::State.create(args, {})
54
+ if tail.nil?()
55
+ if args[:help] == true
56
+ @out.puts(help(state))
57
+ return 1
58
+ end
59
+
60
+ @err.puts('No task specified; use --help to see help')
61
+ return 1
62
+ end
63
+
64
+ if tail[0].start_with?('-')
65
+ raise Error.new("Unknown option #{tail.first()}; use --help to see " +
66
+ "options")
67
+ end
68
+
69
+
70
+ task_name, *task_argv = tail
71
+ task = state.config.get([:tasks, task_name.to_sym()], default: nil,
72
+ variables: false)
73
+
74
+ if task.nil?()
75
+ raise Error.new("Unknown task #{task_name.to_s().inspect()}; use " +
76
+ "--help to see task list")
77
+ end
78
+
79
+ if args[:help] == true
80
+ @out.puts(task_help(state, task_name, task))
81
+ return 1
82
+ end
83
+
84
+ if task.has_key?(:arguments)
85
+ task_args, task_args_tail = @arguments.parse(task[:arguments], task_argv)
86
+ if !task_args_tail.nil?()
87
+ raise Error.new("Unknown task option #{task_args_tail.first().
88
+ inspect()}; use --help #{task_name} to see task options")
89
+ end
90
+ else
91
+ task_args = {}
92
+ end
93
+
94
+ state.arguments.merge!(task_args)
95
+
96
+ STDIN.reopen("/dev/tty") # make sure that input to tty will
97
+
98
+ return state.balmora.run(task_name, state)
99
+ rescue => error
100
+ if !@rescue || true
101
+ raise error
102
+ end
103
+
104
+ @err.puts('Error: ' + error.to_s())
105
+ if args[:debug]
106
+ @err.puts(error.backtrace)
107
+ end
108
+
109
+ return 1
110
+ end
111
+
112
+ def help(state)
113
+ tasks =
114
+ state.
115
+ config.
116
+ get([:tasks], variables: false, default: []).
117
+ collect() { |key, task|
118
+ [' ' * 4 + key.to_s(), task[:description] || '']
119
+ }
120
+
121
+ help =
122
+ "Usage: balmora [common-options] task [task-options]\n\n" +
123
+ "Options\n\n" +
124
+ @arguments.help(@options) + "\n\n" +
125
+ "Tasks:\n\n" +
126
+ @arguments.format(tasks)
127
+
128
+ return help
129
+ end
130
+
131
+ def task_help(statetask_name, task)
132
+ help =
133
+ "Usage: balmora [common-optinos] #{task_name} [task-options]\n\n" +
134
+ "Description:\n\n" +
135
+ (
136
+ task[:description] &&
137
+ @arguments.format([[' ', task[:description]]]) ||
138
+ ((' ' * 4) + 'No description provided')
139
+ ) + "\n\n" +
140
+ "Arguments:\n\n" +
141
+ (
142
+ task[:arguments] &&
143
+ @arguments.help(statetask[:arguments]) ||
144
+ ((' ' * 4) + 'Task has no arguments')
145
+ )
146
+
147
+ return help
148
+ end
149
+
150
+ end
@@ -0,0 +1,106 @@
1
+ class Balmora::Command
2
+
3
+ class Error < StandardError; end
4
+
5
+ def initialize(state, command)
6
+ @state = state
7
+
8
+ @logger = state.logger
9
+ @shell = state.shell
10
+ @config = state.config
11
+ @contexts = state.contexts
12
+ @variables = state.variables
13
+ @balmora = state.balmora
14
+
15
+ @command = command
16
+ end
17
+
18
+ def init()
19
+ if (@command.keys() - [:command] - options()).length != 0
20
+ raise Error.new("Unknown options #{(@command.keys() - [:command] -
21
+ options()).inspect()}")
22
+ end
23
+
24
+ options().each() { |key|
25
+ if self.instance_variable_defined?(:"@#{key}")
26
+ raise Error.new("Can not use #{key} as option")
27
+ end
28
+
29
+ option = @command.fetch(key, nil)
30
+ self.instance_variable_set(:"@#{key}", option)
31
+ }
32
+
33
+ verify()
34
+ end
35
+
36
+ def options()
37
+ return [:ignore_error, :fallback, :extensions, :sudo, :context, :chdir,
38
+ :require]
39
+ end
40
+
41
+ def execute()
42
+ if @command.has_key?(:chdir)
43
+ dir = @command[:chdir]
44
+ dir = @variables.inject(dir)
45
+ dir = @shell.expand(dir)
46
+ Dir.chdir(dir) {
47
+ _execute()
48
+ }
49
+ else
50
+ _execute()
51
+ end
52
+ end
53
+
54
+ def _execute()
55
+ if @command.instance_of?(::Hash) && !@contexts.check(@command[:context])
56
+ @logger.debug("Skip: #{@command.inspect()}")
57
+ return
58
+ end
59
+
60
+ init()
61
+
62
+ if !@require.nil?()
63
+ option(:require).each() { |file|
64
+ require file
65
+ }
66
+ end
67
+
68
+ @logger.debug("Run: #{@command.inspect()}")
69
+
70
+ execute = self.method(:run)
71
+
72
+ if !@sudo.nil?()
73
+ execute_sudo = execute
74
+ execute = Proc.new() {
75
+ @shell.sudo!(option(:sudo)) {
76
+ execute_sudo.call()
77
+ }
78
+ }
79
+ end
80
+
81
+ execute.call()
82
+ rescue => error
83
+ @logger.error("#{error.inspect()}; failed to run " +
84
+ "command: #{@command.inspect()}")
85
+
86
+ if @fallback
87
+ @logger.debug("Executing fallback for command #{@command.inspect()}")
88
+ @balmora.execute(@fallback)
89
+ end
90
+
91
+ if option(:ignore_error) == true
92
+ return
93
+ end
94
+
95
+ raise error
96
+ end
97
+
98
+ def run()
99
+ raise Error.new("Run should be implemented in subclass")
100
+ end
101
+
102
+ def option(option)
103
+ return @variables.inject(self.instance_variable_get(:"@#{option}"))
104
+ end
105
+
106
+ end