mmsh 1.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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/mmsh.rb +209 -0
  3. metadata +44 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0453453a4f67dcd038111d1d3bf17cde45901358eb40ec3faa4ebc936fa08bb4
4
+ data.tar.gz: aedef1b82710c8c83d9728b2ab9df80995eb7fd2dffdc954fea1822f48801793
5
+ SHA512:
6
+ metadata.gz: c2cd0214c6391a9c94dd82c5038fd3617382a7cc7d0168e94684d9dfd5c26d2f5c43b270d7f1b62edaae4b6f918a6ab32bf242e07a3fe894c0cc6c41f314cb29
7
+ data.tar.gz: b8aa1584caf3de917f7e019eb73bc095aff777e39e4772bead19eed255983dd907c707f754df197208c3ab11054cb66182ceb8640f08ea753313a34115371d87
data/lib/mmsh.rb ADDED
@@ -0,0 +1,209 @@
1
+ require 'readline'
2
+ require 'securerandom'
3
+
4
+ module Mmsh
5
+ Cmd = Struct.new(:id, :name, :args, :input, :output)
6
+
7
+ def self.read(prompt)
8
+ cmd_lines = []
9
+
10
+ loop do
11
+ cmd = Readline.readline("#{prompt} ", false).rstrip
12
+ Readline::HISTORY.push(cmd) unless cmd.empty?
13
+ cmd_lines << minimize(cmd)
14
+
15
+ if cmd.end_with?('\\')
16
+ next
17
+ else
18
+ break
19
+ end
20
+ end
21
+
22
+ cmd_lines.join
23
+ end
24
+
25
+ # Takes a command string that may contain leading and/or trailing whitespace,
26
+ # as well as the line continuation character ('\'), and produces a single
27
+ # string with all of that stripped out. Multiple lines get combined, with
28
+ # continuation removed.
29
+ #
30
+ # This:
31
+ # > foo \
32
+ # > bar
33
+ #
34
+ # Becomes:
35
+ # 'foo bar'
36
+ #
37
+ # This:
38
+ # > foo\
39
+ # >bar
40
+ #
41
+ # Becomes:
42
+ # 'foobar'
43
+ def self.minimize(cmd)
44
+ min_strip(
45
+ strip_continuation(cmd)
46
+ )
47
+ end
48
+
49
+ # Takes a command that may or may not contain line continuation, and removes
50
+ # it if it's present.
51
+ def self.strip_continuation(cmd)
52
+ cmd.rstrip.end_with?('\\') ? cmd.rstrip[0..-2] : cmd.rstrip
53
+ end
54
+
55
+ def self.min_strip(cmd)
56
+ rpad = cmd.end_with?(' ') ? ' ' : ''
57
+
58
+ "#{cmd.strip}#{rpad}"
59
+ end
60
+
61
+ ## Parsing methods
62
+
63
+ ##
64
+ # Takes a string captured from a CLI containing one or more commands, and
65
+ # returns an array of Cmd structs, fully connected and ready to be executed.
66
+ #
67
+ # Ex:
68
+ # > Parser.parse('foo | bar; baz < fizz.txt')
69
+ # => [
70
+ # <Cmd name='foo', ... >,
71
+ # <Cmd name='|', ... >,
72
+ # <Cmd name='bar' ... >,
73
+ # <Cmd name=';' ... >,
74
+ # <Cmd name='baz', ... >
75
+ # ]
76
+ def self.parse(multi_cmd_str)
77
+ io_connect(
78
+ subcmds(multi_cmd_str).map{|c| cmd_from(c) }
79
+ )
80
+ end
81
+
82
+ ##
83
+ # Takes a string captured from a CLI containing one or more commands, and
84
+ # returns an array of the commands within that string. Basically this splits
85
+ # on tokens that appear between commands.
86
+ #
87
+ # Ex:
88
+ # > Parser.subcmds('foo | bar; baz < fizz.txt')
89
+ # => ["foo", "|", "bar", ";", "baz < fizz.txt"]
90
+ def self.subcmds(multi_cmd_str)
91
+ multi_cmd_str
92
+ .split(/(&&|\*|\||;)/)
93
+ .map{|cmd| cmd.strip }
94
+ end
95
+
96
+ ##
97
+ # Takes a command string containing a single command, and returns a Cmd struct
98
+ # containing the parts in the command.
99
+ #
100
+ # Ex:
101
+ # > Parser.cmd_from('baz < fizz.txt')
102
+ # => #<struct Cmd
103
+ # id="6ac8aa1c-fdc8-4a63-9b4b-8cd185bd0f40",
104
+ # name="baz",
105
+ # args="",
106
+ # input="fizz.txt",
107
+ # output=nil
108
+ # >
109
+ def self.cmd_from(single_cmd_str)
110
+ parts = parts_from(single_cmd_str)
111
+
112
+ Cmd.new(
113
+ SecureRandom.uuid,
114
+ name(parts),
115
+ args(parts),
116
+ input(parts),
117
+ output(parts)
118
+ )
119
+ end
120
+
121
+ ##
122
+ # Takes an array of Cmd structs and does two things with them:
123
+ #
124
+ # For every Cmd:
125
+ #
126
+ # - Sets the Cmd's #output attribute
127
+ # - Looks for uses of the pipe ('|') command, and where found, connect the
128
+ # output of the Cmd preceding the pipe to the input of the Cmd following the
129
+ # pipe.
130
+ #
131
+ # The command string:
132
+ # 'foo | bar'
133
+ #
134
+ # ... will be split into Cmd structs that represent 'foo', '|', and 'bar'.
135
+ # This method notices that a pipe ('|') Cmd is being used, and connects the
136
+ # output of 'foo' to the input of 'bar'.
137
+ def self.io_connect(cmd_list)
138
+ cmd_list[0][:output] ||= cmd_list[0][:id]
139
+ cmd_list[1..-1].each.with_index do |c, i|
140
+ c[:output] ||= c[:id]
141
+ c[:input] ||= cmd_list[i - 1][:id] if cmd_list[i][:name].eql?('|')
142
+ end
143
+
144
+ cmd_list
145
+ end
146
+
147
+ ##
148
+ # Takes a single command string and returns the component parts as an array of
149
+ # strings.
150
+ #
151
+ # Ex:
152
+ # > Parser.parts_from('foo bar1 bar2 bar3 < baz > fizz')
153
+ # => ["foo", "bar1", "bar2", "bar3", "<", "baz", ">", "fizz"]
154
+ # name |----- arguments -----| |- input -| |- output -|
155
+ #
156
+ # From this list, other supporting methods can extract the name, arguments,
157
+ # input redirection, and output redirection.
158
+ def self.parts_from(single_cmd_str)
159
+ single_cmd_str
160
+ .split(/(<|>)/)
161
+ .map{|p| p.strip }
162
+ .map{|p| p.split }
163
+ .flatten
164
+ end
165
+
166
+ ##
167
+ # Takes an array of the parts of a single command string and returns the name
168
+ # portion.
169
+ #
170
+ # See ::parts_from.
171
+ def self.name(parts)
172
+ parts[0]
173
+ end
174
+
175
+ ##
176
+ # Takes an array of the parts of a single command string and returns the
177
+ # arguments portion.
178
+ #
179
+ # See ::parts_from.
180
+ def self.args(parts)
181
+ end_index = [
182
+ (parts.index('<') || parts.length + 1),
183
+ (parts.index('>') || parts.length + 1),
184
+ parts.length + 1
185
+ ].compact.min - 1
186
+
187
+ parts[1..end_index].join(' ')
188
+ end
189
+
190
+ ##
191
+ # Takes an array of the parts of a single command string and returns the input
192
+ # redirection portion.
193
+ #
194
+ # See ::parts_from.
195
+ def self.input(parts)
196
+ return nil unless parts.index('<')
197
+ parts[parts.index('<') + 1]
198
+ end
199
+
200
+ ##
201
+ # Takes an array of the parts of a single command string and returns the
202
+ # output redirection portion.
203
+ #
204
+ # See ::parts_from.
205
+ def self.output(parts)
206
+ return nil unless parts.index('>')
207
+ parts[parts.index('>') + 1]
208
+ end
209
+ end
metadata ADDED
@@ -0,0 +1,44 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mmsh
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeff Lunt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-01-29 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: jefflunt@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/mmsh.rb
20
+ homepage: https://github.com/jefflunt/mmsh
21
+ licenses:
22
+ - MIT
23
+ metadata:
24
+ source_code_uri: https://github.com/jefflunt/mmsh
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubygems_version: 3.4.1
41
+ signing_key:
42
+ specification_version: 4
43
+ summary: mmsh parses mmsh commands from strings
44
+ test_files: []