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.
- checksums.yaml +7 -0
- data/lib/mmsh.rb +209 -0
- 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: []
|