mmsh 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []