easysh 0.1.1 → 0.1.2
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.
- data/lib/easysh.rb +209 -80
- data/test/test_easysh.rb +25 -0
- metadata +1 -1
data/lib/easysh.rb
CHANGED
@@ -1,67 +1,177 @@
|
|
1
|
-
# EasySH
|
1
|
+
# EasySH Examples
|
2
2
|
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# sh.cat('/dev/urandom').bytes.first(10)
|
20
|
-
# sh.ps._e._o('euser,comm').map(&:split).group_by(&:first)
|
21
|
-
#
|
22
|
-
# # chaining commands
|
23
|
-
# sudo = sh.sudo
|
24
|
-
# tail = sh.tail
|
25
|
-
# sudo[tail._f '/var/log/everything.log'].lines { |l| puts l.upcase }
|
26
|
-
# sudo.tail._f '/var/log/everything.log' do |l| puts l.upcase end
|
27
|
-
#
|
28
|
-
# lab = sh.ssh['lab'] # or, sh.ssh.lab
|
29
|
-
# puts lab.ls._l '/bin', color: :always
|
30
|
-
#
|
31
|
-
# # redirects
|
32
|
-
# puts sh.echo('hello') > '/tmp/test'
|
33
|
-
# puts sh.echo 'hello', 1 => '/tmp/stdout', 2 => '/tmp/stderr'
|
34
|
-
# puts sh.cat < '/tmp/test'
|
35
|
-
# puts sh.cat 0 => '/tmp/fffff'
|
36
|
-
#
|
37
|
-
# # pipes
|
38
|
-
# puts sh.man('ls') | sh.tail(n: 30) | sh.head(:n, 4)
|
39
|
-
#
|
40
|
-
# grep = sh['grep']
|
41
|
-
# filter = grep['problem'] | grep._v['bugs']
|
42
|
-
# puts sh.man.ls | filter
|
43
|
-
#
|
44
|
-
# kat = sh.cat
|
45
|
-
# puts kat['/tmp/foo'] | (kat | kat | kat.|(kat) | (kat | kat) | (kat | kat))
|
46
|
-
#
|
47
|
-
# # exit status
|
48
|
-
# p = sh.which('bash')
|
49
|
-
# puts p
|
50
|
-
# p.status # => #<Process::Status: pid 5931 exit 0>
|
51
|
-
# p = sh.which.nonexists
|
52
|
-
# puts p
|
53
|
-
# p.status # => #<Process::Status: pid 6156 exit 1>
|
54
|
-
#
|
3
|
+
# Basic usage:
|
4
|
+
#
|
5
|
+
# require 'easysh'
|
6
|
+
# sh = EasySH.instant;
|
7
|
+
#
|
8
|
+
# sh.ls # ls
|
9
|
+
# sh['/bin/ls'] # /bin/ls
|
10
|
+
#
|
11
|
+
# EasySH automatically convert method names, symbols, hashes to meaningful parameters:
|
12
|
+
#
|
13
|
+
# * `_method` will be converted to `-method`
|
14
|
+
# * `__method` will be converted to `--method`
|
15
|
+
# * `:symbol` will be converted to `--symbol`
|
16
|
+
# * `:s` will be converted to `-s`
|
17
|
+
# * `{:a => '1', :long => 2}` will be converted to `-a 1`, `--long=2`
|
18
|
+
# * strings will be left untouched.
|
55
19
|
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
20
|
+
# Examples:
|
21
|
+
#
|
22
|
+
# sh.ls('/bin')._l # ls /bin -l
|
23
|
+
# sh.ls._l '/bin' # ls -l /bin
|
24
|
+
# sh.ls._l '/bin', color: 'always' # ls /bin -l --color=always
|
25
|
+
#
|
26
|
+
# EasySH supports method chaining and `[params]`, `method(params)`, just write in any form as you like:
|
27
|
+
#
|
28
|
+
# sh['ls', '-l', :color => :always]
|
29
|
+
# sh.ls '/bin', :l, :color => :always
|
30
|
+
# sh.ls('/bin')['-l', :color => :always]
|
31
|
+
# sh.ls('/bin')._l(:color => :always)
|
32
|
+
# sh.ls._l(:color => :always)['/bin']
|
33
|
+
# sh.ls('/bin', :color => :always)._l
|
34
|
+
#
|
35
|
+
# You can save command with parameters to variables for later use:
|
36
|
+
#
|
37
|
+
# myls = sh.ls._l :color => :always;
|
38
|
+
# myls['/bin'] # note: myls '/bin' will not work since myls is an object, not a method
|
39
|
+
#
|
40
|
+
# Commands can also be chained freely:
|
41
|
+
#
|
42
|
+
# sudo = sh.sudo;
|
43
|
+
# sudo.whoami
|
44
|
+
#
|
45
|
+
# lab = sh.ssh.lab; # or: sh.ssh 'lab', sh.ssh['lab'], sh.ssh('lab')
|
46
|
+
# lab.ls._l '/bin' # ssh lab ls -l /bin
|
47
|
+
#
|
48
|
+
# You can pass arrays or EasySH objects(without pipes) as arguments to another EasySH object:
|
49
|
+
#
|
50
|
+
# cmd = sh.ifconfig.eth0;
|
51
|
+
# opt = ['mtu', 1440]
|
52
|
+
# sudo[cmd].up # sudo ifconfig eth0 up
|
53
|
+
# sudo[cmd, opt].up # sudo ifconfig eth0 up mtu 1440
|
54
|
+
# # sudo[cmd | sh.cat] # Error: EasySH objects with pipes are not allowed here.
|
55
|
+
#
|
56
|
+
# EasySH makes full use of Ruby's Enumerable. `each_line` (`lines`), `each_char` (`chars`), `each_byte` (`bytes`) are available like string. For convenience, `each` is an alias of `each_line`.
|
57
|
+
#
|
58
|
+
# Use Enumerable for simple or complex tasks:
|
59
|
+
#
|
60
|
+
# sh.ls.max
|
61
|
+
# sh.ls.sort
|
62
|
+
# sh.ls.chars.to_a.sample(5) # pick 5 chars randomly from `ls` output
|
63
|
+
# sh.ps._e._o('euser,comm').map(&:split).group_by(&:first) # group process names by user name
|
64
|
+
#
|
65
|
+
# EasySH handles endless stream correctly:
|
66
|
+
#
|
67
|
+
# sh.cat('/dev/urandom').bytes.first(10)
|
68
|
+
# sudo[sh.tail._f '/var/log/everything.log'].lines { |l| puts l.upcase }
|
69
|
+
#
|
70
|
+
# You can even omit `lines` or `each` sometimes:
|
71
|
+
#
|
72
|
+
# sh.cat { |l| puts l.upcase }
|
73
|
+
# sudo.tail._f '/var/log/everything.log' do |l| puts l.upcase end
|
74
|
+
#
|
75
|
+
# By not passing a block, you can use external iterator: (Note: in this case, make sure that the iteration does reach the end, otherwise background processes do not exit)
|
76
|
+
#
|
77
|
+
# iter = sh.ls('/sys/fs').lines
|
78
|
+
# iter.next # 'btrfs'
|
79
|
+
# iter.next # 'cgroup'
|
80
|
+
# iter.next # 'ext4'
|
81
|
+
# iter.next # 'fuse'
|
82
|
+
# iter.next # StopIteration
|
83
|
+
#
|
84
|
+
# Redirects
|
85
|
+
# ---------
|
86
|
+
#
|
87
|
+
# Use `<` or `>` (Note: only one input redirect and one output redirect is supported currently):
|
64
88
|
#
|
89
|
+
# sh.echo('hello') > '/tmp/test'
|
90
|
+
# sh.cat < '/tmp/test'
|
91
|
+
# sh.cat < '/tmp/abc' > '/tmp/def'
|
92
|
+
#
|
93
|
+
# You can also associate file descriptor to file directly by using fd numbers => filename Hash (Note: for more information, see Process.spawn. EasySH will distinct Hash parameters from Hash redirects by
|
94
|
+
# checking if the Hash has any numeric key):
|
95
|
+
#
|
96
|
+
# sh.echo 'hello', 1 => '/tmp/stdout', 2 => '/tmp/stderr'
|
97
|
+
# sh.cat 0 => '/tmp/test'
|
98
|
+
#
|
99
|
+
# Pipes
|
100
|
+
# -----
|
101
|
+
#
|
102
|
+
# Use `|` (Note: redirects except the rightmost output and leftmost input will be ignored):
|
103
|
+
#
|
104
|
+
# (sh.cat | sh.head(n: 5)).each { |l| puts l.upcase }
|
105
|
+
# sh.man('ls') | sh.tail(n: 30) | sh.head(:n, 4) # man ls | tail -n 30 | head -n 4
|
106
|
+
# (sh.cat < '/tmp/abc') | sh.cat | sh.cat > '/tmp/def' # cat < /tmp/abc | cat | cat > /tmp/def
|
107
|
+
#
|
108
|
+
# EasySH objects connected with pipes can be saved for later use:
|
109
|
+
#
|
110
|
+
# grep = sh['grep']; # sh.grep does not work because grep is provided by Enumerable
|
111
|
+
# filter = grep['problem'] | grep._v['bugs'];
|
112
|
+
# sh.man.ls | filter
|
113
|
+
#
|
114
|
+
# Since EasySH does some lazy evaluation. You can add parentheses in anywhere in any order:
|
115
|
+
#
|
116
|
+
# kat = sh.cat;
|
117
|
+
# kat['/tmp/foo'] | (kat | (kat | (kat | kat)) | (kat | kat) | (kat | kat))
|
118
|
+
#
|
119
|
+
# Exit status
|
120
|
+
# -----------
|
121
|
+
#
|
122
|
+
# Use `exitcode` or `to_i` to get exitcode directly:
|
123
|
+
#
|
124
|
+
# sh.true.exitcode # => 0
|
125
|
+
# sh.false.to_i # => 1
|
126
|
+
#
|
127
|
+
# `successful?` is `exitcode == 0` and `failed?` is `exitcode != 0`:
|
128
|
+
#
|
129
|
+
# grep = sh['grep', :q];
|
130
|
+
# (sh.echo.hello | grep['world']).failed? # => true
|
131
|
+
# (sh.echo.world | grep['world']).successful? # => true
|
132
|
+
#
|
133
|
+
# Use `status` method to get a Process::Status object about last run status:
|
134
|
+
#
|
135
|
+
# p = sh.which('bash')
|
136
|
+
# p.status # => #<Process::Status: pid 5931 exit 0>
|
137
|
+
# p = sh.which.nonexists
|
138
|
+
# p.status # => #<Process::Status: pid 6156 exit 1>
|
139
|
+
#
|
140
|
+
# More sugars
|
141
|
+
# -----------
|
142
|
+
#
|
143
|
+
# An EasySH object behaves like an Array or a String sometimes.
|
144
|
+
#
|
145
|
+
# If you pass arguments like: `[int]`, `[int, int]`, `[range]`; `[regex]`, `[regex, int]`, then `to_a` or `to_s` will be automatically called:
|
146
|
+
#
|
147
|
+
# # like Array
|
148
|
+
# sh.echo("Line 1\nLine 2\nLine 3")[1] # => "Line 2"
|
149
|
+
# sh.echo("Line 1\nLine 2\nLine 3")[-1] # => "Line 3"
|
150
|
+
# sh.echo("Line 1\nLine 2\nLine 3")[0, 2] # => ["Line 1", "Line 2"]
|
151
|
+
# sh.echo("Line 1\nLine 2\nLine 3")[1..2] # => ["Line 2", "Line 3"]
|
152
|
+
#
|
153
|
+
# # like String
|
154
|
+
# sh.echo("Hello world\nThis is a test")[/T.*$/] # => "This is a test"
|
155
|
+
# sh.echo("Hello world\nThis is a test")[/T.* ([^ ]*)$/, 1] # => "test"
|
156
|
+
#
|
157
|
+
# Instant mode
|
158
|
+
# ------------
|
159
|
+
# EasySH object with `instant = true` will execute command when `inspect` is called, which is useful in REPL environment like pry or irb.
|
160
|
+
#
|
161
|
+
# If you like traditional `inspect` behavior, you can create the `sh` object using:
|
162
|
+
#
|
163
|
+
# sh = EasySH.new
|
164
|
+
#
|
165
|
+
# or set `instant` to false:
|
166
|
+
#
|
167
|
+
# sh.instant = false
|
168
|
+
#
|
169
|
+
# With `instant = false`, you need additional `to_s` or `to_a` or `to_i` etc. to get command executed:
|
170
|
+
#
|
171
|
+
# [1] pry(main)> sh = EasySH.new; sh.uname
|
172
|
+
# => #<EasySH: uname>
|
173
|
+
# [2] pry(main)> sh.uname.to_s
|
174
|
+
# => "Linux"
|
65
175
|
#
|
66
176
|
class EasySH < Struct.new(:cmd, :opt, :chain, :instant) # :no-doc:
|
67
177
|
include Enumerable
|
@@ -75,23 +185,34 @@ class EasySH < Struct.new(:cmd, :opt, :chain, :instant) # :no-doc:
|
|
75
185
|
# continue
|
76
186
|
end
|
77
187
|
|
78
|
-
|
79
|
-
|
188
|
+
args = [name, *args]
|
189
|
+
*args, opt = *args if args.last.is_a?(Hash) && args.last.keys.find{|k| k.is_a? Integer}
|
190
|
+
opt ||= {}
|
191
|
+
args = args.map.with_index do |a, i|
|
192
|
+
case a
|
193
|
+
when Symbol
|
194
|
+
if i == 0
|
195
|
+
a.to_s.gsub(/^_+/) {|s| '-' * s.size}
|
80
196
|
else
|
81
|
-
|
82
|
-
*args, opt = *args if args.last.is_a?(Hash) && args.last.keys.find{|k| k.is_a? Integer}
|
83
|
-
args = args.map do |a|
|
84
|
-
case a
|
85
|
-
when Symbol
|
86
|
-
"-#{a.length > 1 ? '-' : ''}#{a}"
|
87
|
-
when Hash
|
88
|
-
a.map { |k,v| k.length > 1 ? "--#{k}=#{v}" : ["-#{k}", v.to_s] }
|
89
|
-
else
|
90
|
-
a.to_s
|
91
|
-
end
|
92
|
-
end.flatten
|
93
|
-
self.class.new [*cmd, *args], Hash[[*self.opt, *opt]], chain, instant
|
197
|
+
"-#{a.length > 1 ? '-' : ''}#{a}"
|
94
198
|
end
|
199
|
+
when Hash
|
200
|
+
a.map { |k,v| k.length > 1 ? "--#{k}=#{v}" : ["-#{k}", v.to_s] }
|
201
|
+
when EasySH
|
202
|
+
# no Pipe allowed
|
203
|
+
raise ArgumentError.new("#{self.class} argument can not be #{self.class} with pipes") if a.chain && !a.chain.empty?
|
204
|
+
opt = Hash[[*opt, *a.opt]]
|
205
|
+
a.cmd
|
206
|
+
when NilClass
|
207
|
+
nil
|
208
|
+
when Array
|
209
|
+
a
|
210
|
+
else
|
211
|
+
a.to_s
|
212
|
+
end
|
213
|
+
end.compact.flatten
|
214
|
+
|
215
|
+
r = self.class.new [*cmd, *args], Hash[[*self.opt, *opt]], chain, instant
|
95
216
|
block ? r.each(&block) : r
|
96
217
|
end
|
97
218
|
|
@@ -99,7 +220,12 @@ class EasySH < Struct.new(:cmd, :opt, :chain, :instant) # :no-doc:
|
|
99
220
|
def to_s(n = "\n"); cmd ? to_a.join(n) : ''; end
|
100
221
|
|
101
222
|
def inspect
|
102
|
-
instant
|
223
|
+
if instant
|
224
|
+
s = to_s
|
225
|
+
s.empty? ? nil : s
|
226
|
+
else
|
227
|
+
"#<#{self.class}: #{([*chain, [cmd]]).map(&:first).map {|c| c && c.join(' ')}.compact.join(' | ')}>"
|
228
|
+
end
|
103
229
|
end
|
104
230
|
|
105
231
|
def |(sh, &block)
|
@@ -107,8 +233,8 @@ class EasySH < Struct.new(:cmd, :opt, :chain, :instant) # :no-doc:
|
|
107
233
|
self.class.new sh.cmd, sh.opt, [*chain, cmd && [cmd, opt || {}], *sh.chain].compact, instant
|
108
234
|
end
|
109
235
|
|
110
|
-
def < path; self.opt[0] = path; self; end
|
111
|
-
def > path; self.opt[1] = path; self; end
|
236
|
+
def < path; self.opt ||= {}; self.opt[0] = path; self; end
|
237
|
+
def > path; self.opt ||= {}; self.opt[1] = path; self; end
|
112
238
|
|
113
239
|
def to_io
|
114
240
|
return unless cmd
|
@@ -188,8 +314,11 @@ class EasySH < Struct.new(:cmd, :opt, :chain, :instant) # :no-doc:
|
|
188
314
|
! successful?
|
189
315
|
end
|
190
316
|
|
317
|
+
def to_ary
|
318
|
+
[*cmd]
|
319
|
+
end
|
320
|
+
|
191
321
|
alias :call :method_missing
|
192
|
-
alias :to_ary :to_a
|
193
322
|
alias :lines :each_line
|
194
323
|
alias :each :each_line
|
195
324
|
alias :lines :each_line
|
data/test/test_easysh.rb
CHANGED
@@ -156,4 +156,29 @@ class EasySHTest < Test::Unit::TestCase
|
|
156
156
|
assert_not_equal sh1.status, nil
|
157
157
|
assert_equal sh1.exitcode, 0
|
158
158
|
end
|
159
|
+
|
160
|
+
def test_sh_in_sh
|
161
|
+
sudo = sh.sudo
|
162
|
+
cmd = sh.ifconfig.eth0
|
163
|
+
|
164
|
+
assert_equal sudo[cmd].cmd, sh.sudo.ifconfig.eth0.cmd
|
165
|
+
assert_equal sh.sudo(cmd).cmd, sh.sudo.ifconfig.eth0.cmd
|
166
|
+
assert_equal sudo[cmd, 'up'].cmd, sh.sudo.ifconfig.eth0.up.cmd
|
167
|
+
assert_equal sh.sudo(cmd).up.cmd, sh.sudo.ifconfig.eth0.up.cmd
|
168
|
+
|
169
|
+
assert_raise(ArgumentError) { sudo[cmd | cmd] }
|
170
|
+
|
171
|
+
opt = (sh > '/tmp/output')
|
172
|
+
assert_equal sudo.cmd, sudo[opt].cmd
|
173
|
+
assert_not_equal sudo.opt, sudo[opt].opt
|
174
|
+
assert_equal sudo[opt].opt[1], '/tmp/output'
|
175
|
+
|
176
|
+
mtu = sh.mtu['1440']
|
177
|
+
ref_cmd = sh.sudo.ifconfig.eth0.up.mtu['1440'].cmd
|
178
|
+
assert_equal ref_cmd, sh.sudo[cmd].up[mtu].cmd
|
179
|
+
assert_equal ref_cmd, sh.sudo[cmd, 'up', mtu].cmd
|
180
|
+
assert_equal ref_cmd, sh.sudo[cmd, ['up', mtu]].cmd
|
181
|
+
assert_equal ref_cmd, sh.sudo(cmd, 'up', mtu).cmd
|
182
|
+
assert_equal ref_cmd, sh.sudo(cmd, ['up', 'mtu', '1440']).cmd
|
183
|
+
end
|
159
184
|
end
|