lino 3.2.0.pre.5 → 3.2.0.pre.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +93 -58
  3. data/lib/lino/builders/command_line.rb +59 -0
  4. data/lib/lino/builders/mixins/appliables.rb +27 -0
  5. data/lib/lino/builders/mixins/arguments.rb +38 -0
  6. data/lib/lino/builders/mixins/defaulting.rb +19 -0
  7. data/lib/lino/builders/mixins/environment_variables.rb +46 -0
  8. data/lib/lino/builders/mixins/executor.rb +24 -0
  9. data/lib/lino/builders/mixins/option_config.rb +50 -0
  10. data/lib/lino/builders/mixins/options.rb +118 -0
  11. data/lib/lino/builders/mixins/state_boundary.rb +22 -0
  12. data/lib/lino/builders/mixins/subcommands.rb +52 -0
  13. data/lib/lino/builders/mixins/validation.rb +21 -0
  14. data/lib/lino/builders/mixins/working_directory.rb +24 -0
  15. data/lib/lino/builders/subcommand.rb +47 -0
  16. data/lib/lino/builders.rb +9 -0
  17. data/lib/lino/errors/execution_error.rb +18 -0
  18. data/lib/lino/errors.rb +8 -0
  19. data/lib/lino/executors/childprocess.rb +63 -0
  20. data/lib/lino/executors/open4.rb +42 -0
  21. data/lib/lino/executors.rb +9 -0
  22. data/lib/lino/model/argument.rb +41 -0
  23. data/lib/lino/model/command_line.rb +117 -0
  24. data/lib/lino/model/environment_variable.rb +63 -0
  25. data/lib/lino/model/flag.rb +53 -0
  26. data/lib/lino/model/option.rb +73 -0
  27. data/lib/lino/model/subcommand.rb +52 -0
  28. data/lib/lino/model.rb +13 -0
  29. data/lib/lino/version.rb +1 -1
  30. data/lib/lino.rb +11 -2
  31. metadata +72 -10
  32. data/lib/lino/appliables.rb +0 -23
  33. data/lib/lino/command_line.rb +0 -28
  34. data/lib/lino/command_line_builder.rb +0 -225
  35. data/lib/lino/options.rb +0 -72
  36. data/lib/lino/subcommand_builder.rb +0 -49
  37. data/lib/lino/utilities.rb +0 -59
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'validation'
4
+ require_relative '../../model'
5
+
6
+ module Lino
7
+ module Builders
8
+ module Mixins
9
+ module Subcommands
10
+ include Validation
11
+
12
+ def initialize(state)
13
+ @subcommands = Hamster::Vector.new(state[:subcommands] || [])
14
+ super
15
+ end
16
+
17
+ def with_subcommand(subcommand, &block)
18
+ return self if nil_or_empty?(subcommand)
19
+
20
+ with(
21
+ subcommands: @subcommands.add(
22
+ (block || ->(sub) { sub }).call(
23
+ Builders::Subcommand.for_subcommand(subcommand)
24
+ )
25
+ )
26
+ )
27
+ end
28
+
29
+ def with_subcommands(subcommands, &)
30
+ return self if nil_or_empty?(subcommands)
31
+
32
+ without_block = subcommands[0...-1]
33
+ with_block = subcommands.last
34
+
35
+ without_block
36
+ .inject(self) { |s, sc| s.with_subcommand(sc) }
37
+ .with_subcommand(with_block, &)
38
+ end
39
+
40
+ private
41
+
42
+ def state
43
+ super.merge(subcommands: @subcommands)
44
+ end
45
+
46
+ def build_subcommands
47
+ @subcommands.map(&:build)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lino
4
+ module Builders
5
+ module Mixins
6
+ module Validation
7
+ def nil?(value)
8
+ value.nil?
9
+ end
10
+
11
+ def empty?(value)
12
+ value.respond_to?(:empty?) && value.empty?
13
+ end
14
+
15
+ def nil_or_empty?(value)
16
+ nil?(value) || empty?(value)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lino
4
+ module Builders
5
+ module Mixins
6
+ module WorkingDirectory
7
+ def initialize(state)
8
+ @working_directory = state[:working_directory]
9
+ super
10
+ end
11
+
12
+ def with_working_directory(working_directory)
13
+ with(working_directory:)
14
+ end
15
+
16
+ private
17
+
18
+ def state
19
+ super.merge(working_directory: @working_directory)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hamster'
4
+
5
+ require_relative '../model'
6
+ require_relative 'mixins/options'
7
+ require_relative 'mixins/appliables'
8
+
9
+ module Lino
10
+ module Builders
11
+ class Subcommand
12
+ include Mixins::Options
13
+ include Mixins::Appliables
14
+
15
+ class << self
16
+ def for_subcommand(subcommand)
17
+ Builders::Subcommand.new(subcommand:)
18
+ end
19
+ end
20
+
21
+ def initialize(state)
22
+ @subcommand = state[:subcommand]
23
+ @options = Hamster::Vector.new(state[:options] || [])
24
+ end
25
+
26
+ def build
27
+ Model::Subcommand.new(
28
+ @subcommand,
29
+ options: build_options
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ def with(replacements)
36
+ Builders::Subcommand.new(state.merge(replacements))
37
+ end
38
+
39
+ def state
40
+ {
41
+ subcommand: @subcommand,
42
+ options: @options
43
+ }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'builders/command_line'
4
+ require_relative 'builders/subcommand'
5
+
6
+ module Lino
7
+ module Builders
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lino
4
+ module Errors
5
+ class ExecutionError < StandardError
6
+ def initialize(
7
+ command_line = nil,
8
+ exit_code = nil,
9
+ cause = nil
10
+ )
11
+ @command_line = command_line
12
+ @exit_code = exit_code
13
+ @cause = cause
14
+ super('Failed while executing command line.')
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors/execution_error'
4
+
5
+ module Lino
6
+ module Errors
7
+ end
8
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'childprocess'
4
+
5
+ require_relative '../errors'
6
+
7
+ module Lino
8
+ module Executors
9
+ class Childprocess
10
+ def execute(command_line, opts = {})
11
+ process = ::ChildProcess.build(*command_line.array)
12
+
13
+ set_output_streams(process, opts)
14
+ set_working_directory(process, command_line.working_directory)
15
+ set_environment(process, command_line.environment_variables)
16
+ start_process(process, opts)
17
+
18
+ exit_code = process.wait
19
+
20
+ return unless exit_code != 0
21
+
22
+ raise Lino::Errors::ExecutionError.new(
23
+ command_line.string, exit_code
24
+ )
25
+ end
26
+
27
+ def ==(other)
28
+ self.class == other.class
29
+ end
30
+
31
+ alias eql? ==
32
+
33
+ def hash
34
+ self.class.hash
35
+ end
36
+
37
+ private
38
+
39
+ def start_process(process, opts)
40
+ process.duplex = true if opts[:stdin]
41
+ process.start
42
+ process.io.stdin.write(opts[:stdin]) if opts[:stdin]
43
+ end
44
+
45
+ def set_output_streams(process, opts)
46
+ process.io.inherit!
47
+ process.io.stdout = opts[:stdout] if opts[:stdout]
48
+ process.io.stderr = opts[:stderr] if opts[:stderr]
49
+ end
50
+
51
+ def set_working_directory(process, working_directory)
52
+ process.cwd = working_directory
53
+ end
54
+
55
+ def set_environment(process, environment_variables)
56
+ environment_variables.each do |environment_variable|
57
+ process.environment[environment_variable.name] =
58
+ environment_variable.value
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open4'
4
+
5
+ module Lino
6
+ module Executors
7
+ class Open4
8
+ def execute(command_line, opts = {})
9
+ opts = with_defaults(opts)
10
+
11
+ ::Open4.spawn(
12
+ command_line.env,
13
+ *command_line.array,
14
+ stdin: opts[:stdin],
15
+ stdout: opts[:stdout],
16
+ stderr: opts[:stderr],
17
+ cwd: command_line.working_directory
18
+ )
19
+ end
20
+
21
+ def ==(other)
22
+ self.class == other.class
23
+ end
24
+
25
+ alias eql? ==
26
+
27
+ def hash
28
+ self.class.hash
29
+ end
30
+
31
+ private
32
+
33
+ def with_defaults(opts)
34
+ {
35
+ stdin: opts[:stdin] || '',
36
+ stdout: opts[:stdout] || $stdout,
37
+ stderr: opts[:stderr] || $stderr
38
+ }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'executors/open4'
4
+ require_relative 'executors/childprocess'
5
+
6
+ module Lino
7
+ module Executors
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lino
4
+ module Model
5
+ class Argument
6
+ attr_reader :argument
7
+
8
+ def initialize(argument)
9
+ @argument = argument
10
+ end
11
+
12
+ def string
13
+ argument
14
+ end
15
+ alias to_s string
16
+
17
+ def array
18
+ [argument]
19
+ end
20
+ alias to_a string
21
+
22
+ def ==(other)
23
+ self.class == other.class &&
24
+ state == other.state
25
+ end
26
+ alias eql? ==
27
+
28
+ def hash
29
+ [self.class, state].hash
30
+ end
31
+
32
+ protected
33
+
34
+ def state
35
+ [
36
+ @argument
37
+ ]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lino
4
+ module Model
5
+ class CommandLine
6
+ COMPONENTS = [
7
+ %i[environment_variables],
8
+ %i[command],
9
+ %i[options after_command],
10
+ %i[subcommands],
11
+ %i[options after_subcommands],
12
+ %i[arguments],
13
+ %i[options after_arguments]
14
+ ].freeze
15
+
16
+ attr_reader :command,
17
+ :subcommands,
18
+ :options,
19
+ :arguments,
20
+ :environment_variables,
21
+ :executor,
22
+ :working_directory
23
+
24
+ def initialize(command, opts = {})
25
+ opts = with_defaults(opts)
26
+ @command = command
27
+ @subcommands = Hamster::Vector.new(opts[:subcommands])
28
+ @options = Hamster::Vector.new(opts[:options])
29
+ @arguments = Hamster::Vector.new(opts[:arguments])
30
+ @environment_variables =
31
+ Hamster::Vector.new(opts[:environment_variables])
32
+ @executor = opts[:executor]
33
+ @working_directory = opts[:working_directory]
34
+ end
35
+
36
+ def execute(opts = {})
37
+ @executor.execute(self, opts)
38
+ end
39
+
40
+ def env
41
+ @environment_variables.to_h(&:array)
42
+ end
43
+
44
+ def array
45
+ format_components(:array, COMPONENTS.drop(1)).flatten
46
+ end
47
+
48
+ alias to_a array
49
+
50
+ def string
51
+ format_components(:string, COMPONENTS).join(' ')
52
+ end
53
+
54
+ alias to_s string
55
+
56
+ def ==(other)
57
+ self.class == other.class && state == other.state
58
+ end
59
+
60
+ alias eql? ==
61
+
62
+ def hash
63
+ [self.class, state].hash
64
+ end
65
+
66
+ protected
67
+
68
+ def state
69
+ [
70
+ @command,
71
+ @subcommands,
72
+ @options,
73
+ @arguments,
74
+ @environment_variables,
75
+ @executor,
76
+ @working_directory
77
+ ]
78
+ end
79
+
80
+ private
81
+
82
+ def with_defaults(opts)
83
+ {
84
+ subcommands: opts.fetch(:subcommands, []),
85
+ options: opts.fetch(:options, []),
86
+ arguments: opts.fetch(:arguments, []),
87
+ environment_variables: opts.fetch(:environment_variables, []),
88
+ executor: opts.fetch(:executor, Executors::Childprocess.new),
89
+ working_directory: opts.fetch(:working_directory, nil)
90
+ }
91
+ end
92
+
93
+ def format_components(format, paths)
94
+ paths
95
+ .collect { |p| components_at_path(formatted_components(format), p) }
96
+ .compact
97
+ .reject(&:empty?)
98
+ end
99
+
100
+ def formatted_components(format)
101
+ {
102
+ environment_variables: @environment_variables.map(&format),
103
+ command: @command,
104
+ options: @options
105
+ .group_by(&:placement)
106
+ .map { |p, o| [p, o.map(&format)] },
107
+ subcommands: @subcommands.map(&format),
108
+ arguments: @arguments.map(&format)
109
+ }
110
+ end
111
+
112
+ def components_at_path(components, path)
113
+ path.inject(components) { |c, p| c && c[p] }
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+
5
+ module Lino
6
+ module Model
7
+ class EnvironmentVariable
8
+ attr_reader :name,
9
+ :value,
10
+ :quoting
11
+
12
+ def initialize(name, value, opts = {})
13
+ opts = with_defaults(opts)
14
+ @name = name
15
+ @value = value
16
+ @quoting = opts[:quoting]
17
+ end
18
+
19
+ def quoted_value
20
+ "#{quoting}#{value.to_s.gsub(quoting.to_s, "\\#{quoting}")}#{quoting}"
21
+ end
22
+
23
+ def string
24
+ "#{name}=#{quoted_value}"
25
+ end
26
+ alias to_s string
27
+
28
+ def array
29
+ [name, value]
30
+ end
31
+ alias to_a array
32
+
33
+ def ==(other)
34
+ self.class == other.class &&
35
+ state == other.state
36
+ end
37
+
38
+ alias eql? ==
39
+
40
+ def hash
41
+ [self.class, state].hash
42
+ end
43
+
44
+ protected
45
+
46
+ def state
47
+ [
48
+ @name,
49
+ @value,
50
+ @quoting
51
+ ]
52
+ end
53
+
54
+ private
55
+
56
+ def with_defaults(opts)
57
+ {
58
+ quoting: opts[:quoting] || '"'
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lino
4
+ module Model
5
+ class Flag
6
+ attr_reader :flag,
7
+ :placement
8
+
9
+ def initialize(flag, opts = {})
10
+ opts = with_defaults(opts)
11
+ @flag = flag
12
+ @placement = opts[:placement]
13
+ end
14
+
15
+ def string
16
+ flag
17
+ end
18
+ alias to_s string
19
+
20
+ def array
21
+ [flag]
22
+ end
23
+ alias to_a string
24
+
25
+ def ==(other)
26
+ self.class == other.class &&
27
+ state == other.state
28
+ end
29
+ alias eql? ==
30
+
31
+ def hash
32
+ [self.class, state].hash
33
+ end
34
+
35
+ protected
36
+
37
+ def state
38
+ [
39
+ @flag,
40
+ @placement
41
+ ]
42
+ end
43
+
44
+ private
45
+
46
+ def with_defaults(opts)
47
+ {
48
+ placement: opts[:placement] || :after_command
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lino
4
+ module Model
5
+ class Option
6
+ attr_reader :option,
7
+ :value,
8
+ :separator,
9
+ :quoting,
10
+ :placement
11
+
12
+ def initialize(option, value, opts = {})
13
+ opts = with_defaults(opts)
14
+ @option = option
15
+ @value = value
16
+ @separator = opts[:separator]
17
+ @quoting = opts[:quoting]
18
+ @placement = opts[:placement]
19
+ end
20
+
21
+ def quoted_value
22
+ "#{quoting}#{value}#{quoting}"
23
+ end
24
+
25
+ def string
26
+ "#{option}#{separator}#{quoted_value}"
27
+ end
28
+ alias to_s string
29
+
30
+ def array
31
+ if separator == ' '
32
+ [option, value]
33
+ else
34
+ ["#{option}#{separator}#{value}"]
35
+ end
36
+ end
37
+ alias to_a array
38
+
39
+ def ==(other)
40
+ self.class == other.class &&
41
+ state == other.state
42
+ end
43
+
44
+ alias eql? ==
45
+
46
+ def hash
47
+ [self.class, state].hash
48
+ end
49
+
50
+ protected
51
+
52
+ def state
53
+ [
54
+ @option,
55
+ @value,
56
+ @separator,
57
+ @quoting,
58
+ @placement
59
+ ]
60
+ end
61
+
62
+ private
63
+
64
+ def with_defaults(opts)
65
+ {
66
+ separator: opts[:separator] || ' ',
67
+ quoting: opts[:quoting],
68
+ placement: opts[:placement] || :after_command
69
+ }
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lino
4
+ module Model
5
+ class Subcommand
6
+ attr_reader :subcommand, :options
7
+
8
+ def initialize(subcommand, opts = {})
9
+ opts = with_defaults(opts)
10
+ @subcommand = subcommand
11
+ @options = Hamster::Vector.new(opts[:options])
12
+ end
13
+
14
+ def string
15
+ [@subcommand, @options.map(&:string)].reject(&:empty?).join(' ')
16
+ end
17
+ alias to_s string
18
+
19
+ def array
20
+ [@subcommand, @options.map(&:array)].flatten
21
+ end
22
+ alias to_a array
23
+
24
+ def ==(other)
25
+ self.class == other.class &&
26
+ state == other.state
27
+ end
28
+ alias eql? ==
29
+
30
+ def hash
31
+ [self.class, state].hash
32
+ end
33
+
34
+ protected
35
+
36
+ def state
37
+ [
38
+ @subcommand,
39
+ @options
40
+ ]
41
+ end
42
+
43
+ private
44
+
45
+ def with_defaults(opts)
46
+ {
47
+ options: opts.fetch(:options, [])
48
+ }
49
+ end
50
+ end
51
+ end
52
+ end