easysh 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|