lino 3.2.0.pre.5 → 3.2.0.pre.7

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.
Files changed (38) 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/mock.rb +29 -0
  21. data/lib/lino/executors/open4.rb +42 -0
  22. data/lib/lino/executors.rb +10 -0
  23. data/lib/lino/model/argument.rb +41 -0
  24. data/lib/lino/model/command_line.rb +117 -0
  25. data/lib/lino/model/environment_variable.rb +63 -0
  26. data/lib/lino/model/flag.rb +53 -0
  27. data/lib/lino/model/option.rb +73 -0
  28. data/lib/lino/model/subcommand.rb +52 -0
  29. data/lib/lino/model.rb +13 -0
  30. data/lib/lino/version.rb +1 -1
  31. data/lib/lino.rb +35 -2
  32. metadata +73 -10
  33. data/lib/lino/appliables.rb +0 -23
  34. data/lib/lino/command_line.rb +0 -28
  35. data/lib/lino/command_line_builder.rb +0 -225
  36. data/lib/lino/options.rb +0 -72
  37. data/lib/lino/subcommand_builder.rb +0 -49
  38. 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 if exit_code.zero?
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,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lino
4
+ module Executors
5
+ class Mock
6
+ attr_reader :calls
7
+ attr_accessor :exit_code
8
+
9
+ def initialize
10
+ reset
11
+ end
12
+
13
+ def execute(command_line, opts = {})
14
+ @calls << { command_line:, opts:, exit_code: @exit_code }
15
+
16
+ return if @exit_code.zero?
17
+
18
+ raise Lino::Errors::ExecutionError.new(
19
+ command_line.string, @exit_code
20
+ )
21
+ end
22
+
23
+ def reset
24
+ @calls = []
25
+ @exit_code = 0
26
+ end
27
+ end
28
+ end
29
+ 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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'executors/open4'
4
+ require_relative 'executors/childprocess'
5
+ require_relative 'executors/mock'
6
+
7
+ module Lino
8
+ module Executors
9
+ end
10
+ 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, Lino.configuration.executor),
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