shell_helpers 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +2 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +4 -0
- data/LICENSE.txt +20 -0
- data/README.md +37 -0
- data/Rakefile +34 -0
- data/gemspec.yml +15 -0
- data/lib/shell_helpers.rb +32 -0
- data/lib/shell_helpers/logger.rb +219 -0
- data/lib/shell_helpers/pathname.rb +159 -0
- data/lib/shell_helpers/run.rb +139 -0
- data/lib/shell_helpers/sh.rb +244 -0
- data/lib/shell_helpers/utils.rb +226 -0
- data/lib/shell_helpers/version.rb +4 -0
- data/shell_helpers.gemspec +59 -0
- data/test/helper.rb +2 -0
- data/test/test_shell_helpers.rb +12 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e423354674b7162de244672ab417617837d4667f
|
4
|
+
data.tar.gz: 36527735c314d40be7389de683688e4446c24961
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 743a7bbaec3fa0511cbba321b847db43233a3c964bc4a03cbd07577c5ef2ae4e5357d98a9906532c5536842f92f10ecddf815904f2e776035533763a3a9b8fb4
|
7
|
+
data.tar.gz: 023a3e4ce0a45b3cde0c94906ae05ab1d15687531f9425eb93b60fd59d1fd17c3fd6939b629617d92f0520d6083f06b729a3e54e1ae2f3a317ce58c6b80bf03f
|
data/.document
ADDED
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown -M kramdown --title "shell_helpers Documentation" --protected
|
data/ChangeLog.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2015 Damien Robert
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# shell_helpers
|
2
|
+
|
3
|
+
* [Homepage](https://github.com/DamienRobert/shell_helpers#readme)
|
4
|
+
* [Gems]("https://rubygems.org/gems/shell_helpers)
|
5
|
+
* [Issues](https://github.com/DamienRobert/shell_helpers/issues)
|
6
|
+
* [Documentation](http://rubydoc.info/gems/shell_helpers/frames)
|
7
|
+
* [Email](mailto:Damien.Olivier.Robert+gems at gmail.com)
|
8
|
+
|
9
|
+
## Description
|
10
|
+
|
11
|
+
This gem contains a collection of libraries to ease working with the
|
12
|
+
shell with ruby.
|
13
|
+
|
14
|
+
A lot of the ideas here are inspired by the utilities in
|
15
|
+
[methadone](https://github.com/davetron5000/methadone). In particular
|
16
|
+
`logger.rb` and `sh.rb` which were based on `cli_logger.rb`,
|
17
|
+
`cli_logging.rb`, `error.rb`, `exit_now.rb`, `process_status.rb`,
|
18
|
+
`run.rb` from methadone.
|
19
|
+
|
20
|
+
The reason to incorporate them in this gem is that I wanted to be able to
|
21
|
+
add some functionalities (such as `log_and_do` for `logger.rb`, and on
|
22
|
+
succes and on error callbacks for `sh.rb`), and also to be able to use
|
23
|
+
this functionality from other command parsers than methadone
|
24
|
+
(like [gli](https://github.com/davetron5000/gli)).
|
25
|
+
|
26
|
+
## Install
|
27
|
+
|
28
|
+
$ gem install shell_helpers
|
29
|
+
|
30
|
+
## Copyright
|
31
|
+
|
32
|
+
Copyright (c) 2015 Damien Robert
|
33
|
+
|
34
|
+
MIT License. See {file:LICENSE.txt} for details.
|
35
|
+
|
36
|
+
See above for the copyright: for the files `logger.rb` and `sh.rb` the
|
37
|
+
copyright applies only to the diff from the original import from methadone.
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
|
6
|
+
begin
|
7
|
+
gem 'rubygems-tasks', '~> 0.2'
|
8
|
+
require 'rubygems/tasks'
|
9
|
+
|
10
|
+
Gem::Tasks.new
|
11
|
+
rescue LoadError => e
|
12
|
+
warn e.message
|
13
|
+
warn "Run `gem install rubygems-tasks` to install Gem::Tasks."
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'rake/testtask'
|
17
|
+
|
18
|
+
Rake::TestTask.new do |test|
|
19
|
+
test.libs << 'test'
|
20
|
+
test.pattern = 'test/**/test_*.rb'
|
21
|
+
test.verbose = true
|
22
|
+
end
|
23
|
+
|
24
|
+
begin
|
25
|
+
gem 'yard', '~> 0.8'
|
26
|
+
require 'yard'
|
27
|
+
|
28
|
+
YARD::Rake::YardocTask.new
|
29
|
+
rescue LoadError => e
|
30
|
+
task :yard do
|
31
|
+
abort "Please run `gem install yard` to install YARD."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
task :doc => :yard
|
data/gemspec.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
name: shell_helpers
|
2
|
+
summary: "Shell Helpers"
|
3
|
+
description: |
|
4
|
+
Collection of script to ease working with the shell with ruby.
|
5
|
+
license: MIT
|
6
|
+
authors: Damien Robert
|
7
|
+
email: Damien.Olivier.Robert+gems@gmail.com
|
8
|
+
homepage: https://github.com/DamienRobert/shell_helpers#readme
|
9
|
+
|
10
|
+
dependencies:
|
11
|
+
drain: ~> 0.1
|
12
|
+
development_dependencies:
|
13
|
+
minitest: ~> 5.0
|
14
|
+
rubygems-tasks: ~> 0.2
|
15
|
+
yard: ~> 0.8
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'shell_helpers/version'
|
2
|
+
|
3
|
+
require 'shellwords'; require 'pathname'; require 'fileutils'
|
4
|
+
require 'dr/ruby_ext/core_ext'; require 'dr/ruby_ext/pathname_ext'
|
5
|
+
#load everything in shell_helpers/*.rb
|
6
|
+
dir=File.expand_path(File.basename(__FILE__).chomp('.rb'), File.dirname(__FILE__))
|
7
|
+
Dir.glob(File.expand_path('*.rb',dir)) do |file|
|
8
|
+
require file
|
9
|
+
end
|
10
|
+
|
11
|
+
module SH
|
12
|
+
include Run #run_command, run_output, run_status, run
|
13
|
+
include CLILogging #logger.{debug info warn error fatal}, log_and_do
|
14
|
+
include ExitNow #exit_now!
|
15
|
+
include Sh #sh, sh!
|
16
|
+
include ShellExport #export
|
17
|
+
include ShellUtils #find, run_pager
|
18
|
+
extend self
|
19
|
+
#activates debug mode
|
20
|
+
def self.debug(level=Logger::DEBUG)
|
21
|
+
#activates logging on Pathname
|
22
|
+
Pathname.send(:include, CLILogging)
|
23
|
+
logger.level=(level)
|
24
|
+
end
|
25
|
+
#including SH::FU to add FileUtils
|
26
|
+
module FU
|
27
|
+
include ::FileUtils
|
28
|
+
include ::SH
|
29
|
+
extend self
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# vim: foldmethod=marker
|
2
|
+
#From methadone (cli_logger.rb, cli_logging.rb, last import: v1.3.1-2-g9be3b5a)
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module SH
|
6
|
+
# CLILogger {{{
|
7
|
+
# A Logger instance that gives better control of messaging the user and
|
8
|
+
# logging app activity. At it's most basic, you would use <tt>info</tt>
|
9
|
+
# as a replacement for +puts+ and <tt>error</tt> as a replacement for
|
10
|
+
# <tt>STDERR.puts</tt>. Since this is a logger, however, you can also
|
11
|
+
# use #debug, #warn, and #fatal, and you can control the format and
|
12
|
+
# "logging level" as such.
|
13
|
+
#
|
14
|
+
# So, by default:
|
15
|
+
# * debug messages do not appear anywhere
|
16
|
+
# * info messages appear on the standard output
|
17
|
+
# * warn, error, and fatal message appear on the standard error
|
18
|
+
# * The default format of messages is simply the message, no logging
|
19
|
+
# cruft, however if your output is redirected to a file, a better
|
20
|
+
# timestamped logging format is used
|
21
|
+
#
|
22
|
+
# You can customize this in several ways:
|
23
|
+
# * You can override the devices used by passing different devices to the constructor
|
24
|
+
# * You can adjust the level of message that goes to the error logger via error_level=
|
25
|
+
# * You can adjust the format for messages to the error logger separately via error_formatter=
|
26
|
+
#
|
27
|
+
# === Example
|
28
|
+
#
|
29
|
+
# logger = CLILogger.new
|
30
|
+
# logger.debug("Starting up") # => only the standard output gets this
|
31
|
+
# logger.warn("careful!") # => only the standard error gets this
|
32
|
+
# logger.error("Something went wrong!") # => only the standard error gets this
|
33
|
+
#
|
34
|
+
# logger = CLILogger.new
|
35
|
+
# logger.error_level = Logger::ERROR
|
36
|
+
# logger.debug("Starting up") # => only the standard output gets this
|
37
|
+
# logger.warn("careful!") # => only the standard OUTPUT gets this
|
38
|
+
# logger.error("Something went wrong!") # => only the standard error gets this
|
39
|
+
#
|
40
|
+
# logger = CLILogger.new('logfile.txt')
|
41
|
+
# logger.debug("Starting up") #=> logfile.txt gets this
|
42
|
+
# logger.error("Something went wrong!") # => BOTH logfile.txt AND the standard error get this
|
43
|
+
class CLILogger < Logger
|
44
|
+
BLANK_FORMAT = lambda { |severity,datetime,progname,msg|
|
45
|
+
msg + "\n"
|
46
|
+
}
|
47
|
+
|
48
|
+
# Helper to proxy methods to the super class AND to the internal error logger
|
49
|
+
# +symbol+:: Symbol for name of the method to proxy
|
50
|
+
def self.proxy_method(symbol) #:nodoc:
|
51
|
+
old_name = "old_#{symbol}".to_sym
|
52
|
+
alias_method old_name,symbol
|
53
|
+
define_method symbol do |*args,&block|
|
54
|
+
send(old_name,*args,&block)
|
55
|
+
@stderr_logger.send(symbol,*args,&block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
proxy_method :'formatter='
|
60
|
+
proxy_method :'datetime_format='
|
61
|
+
|
62
|
+
def add(severity, message = nil, progname = nil, &block) #:nodoc:
|
63
|
+
if @split_logs
|
64
|
+
unless severity >= @stderr_logger.level
|
65
|
+
super(severity,message,progname,&block)
|
66
|
+
end
|
67
|
+
else
|
68
|
+
super(severity,message,progname,&block)
|
69
|
+
end
|
70
|
+
@stderr_logger.add(severity,message,progname,&block)
|
71
|
+
end
|
72
|
+
|
73
|
+
DEFAULT_ERROR_LEVEL = Logger::Severity::WARN
|
74
|
+
|
75
|
+
# A logger that logs error-type messages to a second device; useful for
|
76
|
+
# ensuring that error messages go to standard error. This should be
|
77
|
+
# pretty smart about doing the right thing. If both log devices are
|
78
|
+
# ttys, e.g. one is going to standard error and the other to the
|
79
|
+
# standard output, messages only appear once in the overall output
|
80
|
+
# stream. In other words, an ERROR logged will show up *only* in the
|
81
|
+
# standard error. If either log device is NOT a tty, then all messages
|
82
|
+
# go to +log_device+ and only errors go to +error_device+
|
83
|
+
#
|
84
|
+
# +log_device+:: device where all log messages should go, based on level
|
85
|
+
# By default, this is Logger::Severity::WARN
|
86
|
+
# +error_device+:: device where all error messages should go.
|
87
|
+
def initialize(log_device=$stdout,error_device=$stderr,
|
88
|
+
split_log:log_device.tty? && error_device.tty?)
|
89
|
+
super(log_device)
|
90
|
+
@stderr_logger = Logger.new(error_device)
|
91
|
+
|
92
|
+
@split_logs = split_log
|
93
|
+
self.level = Logger::Severity::INFO
|
94
|
+
@stderr_logger.level = DEFAULT_ERROR_LEVEL
|
95
|
+
|
96
|
+
self.formatter = BLANK_FORMAT if log_device.tty?
|
97
|
+
@stderr_logger.formatter = BLANK_FORMAT if error_device.tty?
|
98
|
+
end
|
99
|
+
|
100
|
+
def level=(level)
|
101
|
+
super(level)
|
102
|
+
#current_error_level = @stderr_logger.level
|
103
|
+
if (level > DEFAULT_ERROR_LEVEL) && @split_logs
|
104
|
+
@stderr_logger.level = level
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Set the threshold for what messages go to the error device. Note
|
109
|
+
# that calling #level= will *not* affect the error logger *unless* both
|
110
|
+
# devices are TTYs.
|
111
|
+
# +level+:: a constant from Logger::Severity for the level of messages that should go to the error logger
|
112
|
+
def error_level=(level)
|
113
|
+
@stderr_logger.level = level
|
114
|
+
end
|
115
|
+
|
116
|
+
# Overrides the formatter for the error logger. A future call to
|
117
|
+
# #formatter= will affect both, so the order of the calls matters.
|
118
|
+
# +formatter+:: Proc that handles the formatting, the same as for #formatter=
|
119
|
+
def error_formatter=(formatter)
|
120
|
+
@stderr_logger.formatter=formatter
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
#}}}
|
125
|
+
# CLILogging {{{
|
126
|
+
# Provides easier access to a shared DR::CLILogger instance.
|
127
|
+
# Include this module into your class, and #logger provides access to a
|
128
|
+
# shared logger. This is handy if you want all of your clases to have
|
129
|
+
# access to the same logger, but don't want to (or aren't able to) pass
|
130
|
+
# it around to each class.
|
131
|
+
# This also provides methods for direct logging without going through the
|
132
|
+
# #logger
|
133
|
+
#
|
134
|
+
# === Example
|
135
|
+
#
|
136
|
+
# class MyClass
|
137
|
+
# include DR::CLILogging
|
138
|
+
#
|
139
|
+
# def doit
|
140
|
+
# debug("About to doit!")
|
141
|
+
# if results
|
142
|
+
# info("We did it!")
|
143
|
+
# else
|
144
|
+
# error("Something went wrong")
|
145
|
+
# end
|
146
|
+
# debug("Done doing it")
|
147
|
+
# end
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# Note that every class that mixes this in shares the *same logger
|
151
|
+
# instance*, so if you call #change_logger, this will change the logger
|
152
|
+
# for all classes that mix this in. This is likely what you want.
|
153
|
+
module CLILogging
|
154
|
+
extend self
|
155
|
+
|
156
|
+
# Access the shared logger. All classes that include this module
|
157
|
+
# will get the same logger via this method.
|
158
|
+
def logger
|
159
|
+
unless Module.class_variable_defined?(:@@logger)
|
160
|
+
@@logger = CLILogger.new
|
161
|
+
@@logger.progname=$0
|
162
|
+
end
|
163
|
+
@@logger
|
164
|
+
end
|
165
|
+
|
166
|
+
# Change the global logger that includers will use. Useful if you
|
167
|
+
# don't want the default configured logger. Note that the
|
168
|
+
# +change_logger+ version is preferred because Ruby will often parse
|
169
|
+
# <tt>logger = Logger.new</tt> as the declaration of, and assignment
|
170
|
+
# to, of a local variable. You'd need to do
|
171
|
+
# <tt>self.logger=Logger.new</tt> to be sure. This method is a bit
|
172
|
+
# easier.
|
173
|
+
#
|
174
|
+
# +new_logger+:: the new logger. May not be nil and should be a logger of some kind
|
175
|
+
def change_logger(new_logger)
|
176
|
+
raise ArgumentError,"Logger may not be nil" if new_logger.nil?
|
177
|
+
@@logger = new_logger
|
178
|
+
@@logger.level = @log_level if @log_level
|
179
|
+
end
|
180
|
+
|
181
|
+
alias logger= change_logger
|
182
|
+
|
183
|
+
LOG_LEVELS = {
|
184
|
+
'debug' => Logger::DEBUG,
|
185
|
+
'info' => Logger::INFO,
|
186
|
+
'warn' => Logger::WARN,
|
187
|
+
'error' => Logger::ERROR,
|
188
|
+
'fatal' => Logger::FATAL,
|
189
|
+
}
|
190
|
+
|
191
|
+
#log the action and execute it
|
192
|
+
#Severity is Logger:: DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN
|
193
|
+
def log_and_do(*args, severity: Logger::INFO, definee: self, **opts, &block)
|
194
|
+
msg="log_and_do #{args} on #{self}"
|
195
|
+
msg+=" with options #{opts}" unless opts.empty?
|
196
|
+
msg+=" with block #{block}" if block
|
197
|
+
logger.add(severity,msg)
|
198
|
+
if opts.empty?
|
199
|
+
definee.send(*args, &block)
|
200
|
+
else
|
201
|
+
definee.send(*args, **opts, &block)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
#Include this in place of CLILogging if you prefer to use
|
206
|
+
#info directly rather than logger.info
|
207
|
+
module Shortcuts #{{{
|
208
|
+
extend self
|
209
|
+
include CLILogging
|
210
|
+
|
211
|
+
def debug(progname = nil, &block); logger.debug(progname,&block); end
|
212
|
+
def info(progname = nil, &block); logger.info(progname,&block); end
|
213
|
+
def warns(progname = nil, &block); logger.warn(progname,&block); end
|
214
|
+
def error(progname = nil, &block); logger.error(progname,&block); end
|
215
|
+
def fatal(progname = nil, &block); logger.fatal(progname,&block); end
|
216
|
+
end
|
217
|
+
#}}}
|
218
|
+
end #}}}
|
219
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
#backports from ruby 2.1
|
4
|
+
class Pathname
|
5
|
+
unless method_defined?(:write)
|
6
|
+
def write(*args,&b)
|
7
|
+
IO.write(self,*args,&b)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
unless method_defined?(:to_path)
|
11
|
+
alias to_path to_str
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
autoload :FileUtils, "fileutils"
|
16
|
+
|
17
|
+
module SH
|
18
|
+
#SH::Pathname is Pathname with extra features
|
19
|
+
#and methods from FileUtils rather than File when possible
|
20
|
+
#to use this module rather than ::Pathname in a module or class,
|
21
|
+
#simply define Pathname=SH::Pathname in an appropriate nesting level
|
22
|
+
class Pathname < ::Pathname
|
23
|
+
def hidden?
|
24
|
+
return self.basename.to_s[0]=="."
|
25
|
+
end
|
26
|
+
|
27
|
+
def filewrite(*args,mode:"w",perm: nil,mkdir: false)
|
28
|
+
logger.debug("Write to #{self}"+ (perm ? " (#{perm})" : "")) if respond_to?(:logger)
|
29
|
+
self.dirname.mkpath if mkdir
|
30
|
+
self.open(mode: mode) do |fh|
|
31
|
+
fh.chmod(perm) if perm
|
32
|
+
#hack to pass an array to write and do the right thing
|
33
|
+
if args.length == 1 && Array === args.first
|
34
|
+
fh.puts(args.first)
|
35
|
+
else
|
36
|
+
fh.write(*args)
|
37
|
+
end
|
38
|
+
yield fh if block_given?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
#write is not the same as filewrite
|
42
|
+
#but if given only one argument we should be ok
|
43
|
+
unless Pathname.method_defined?(:write)
|
44
|
+
alias :write :filewrite
|
45
|
+
end
|
46
|
+
|
47
|
+
#Pathname.new("foo")+"bar" return "foo/bar"
|
48
|
+
#Pathname.new("foo").append_name("bar") return "foobar"
|
49
|
+
def append_name(*args,join:'')
|
50
|
+
Pathname.new(self.to_s+args.join(join))
|
51
|
+
end
|
52
|
+
|
53
|
+
def backup(suffix: '.old', overwrite: true)
|
54
|
+
if self.exist?
|
55
|
+
filebk=self.append_name(suffix)
|
56
|
+
if filebk.exist? and !overwrite
|
57
|
+
num=0
|
58
|
+
begin
|
59
|
+
filebknum=filebk.append_name("%02d" % num)
|
60
|
+
num+=1
|
61
|
+
end while filebknum.exist?
|
62
|
+
filebk=filebknum
|
63
|
+
end
|
64
|
+
logger.debug "Backup #{self} -> #{filebk}" if respond_to?(:logger)
|
65
|
+
FileUtils.mv(self,filebk)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def abs_path(base: Pathname.pwd, mode: :clean)
|
70
|
+
f=base+self
|
71
|
+
case clean
|
72
|
+
when :clean
|
73
|
+
f.cleanpath
|
74
|
+
when :clean_sym
|
75
|
+
f.cleanpath(consider_symlink: true)
|
76
|
+
when :real
|
77
|
+
f.realpath
|
78
|
+
when :realdir
|
79
|
+
f.realdirpath
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
#wrapper around FileUtils
|
84
|
+
#Pathname#rmdir uses Dir.rmdir, but the rmdir from FileUtils is a wrapper
|
85
|
+
#around Dir.rmdir that accepts extra options
|
86
|
+
#The same for mkdir
|
87
|
+
[:chdir, :rmdir, :mkdir].each do |method|
|
88
|
+
define_method method do |*args,&b|
|
89
|
+
FileUtils.send(method,*args,&b)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
#mkpath is already defined (use FileUtils), but not mkdir_p
|
93
|
+
#alias mkdir_p mkpath
|
94
|
+
|
95
|
+
#Options: verbose, noop, force
|
96
|
+
def rm(recursive: false, mode: :file, verbose: true, noop: false, force: false)
|
97
|
+
mode.to_s.match(/^recursive-(.*)$/) do |m|
|
98
|
+
recursive=true
|
99
|
+
mode=m[1].to_sym
|
100
|
+
end
|
101
|
+
case mode
|
102
|
+
when :symlink
|
103
|
+
return unless self.symlink?
|
104
|
+
when :dangling_symlink
|
105
|
+
return unless self.symlink? && ! self.exist?
|
106
|
+
end
|
107
|
+
if recursive
|
108
|
+
FileUtils.rm_r(self, verbose: verbose, noop: noop, force: force)
|
109
|
+
else
|
110
|
+
FileUtils.rm(self, verbose: verbose, noop: noop, force: force)
|
111
|
+
end
|
112
|
+
rescue => e
|
113
|
+
warn "Error in #{self}.clobber: #{e}"
|
114
|
+
end
|
115
|
+
|
116
|
+
#Options: preserve noop verbose
|
117
|
+
def cp(*files,**opts)
|
118
|
+
FileUtils.cp(files,self,**opts)
|
119
|
+
end
|
120
|
+
#Options: force noop verbose
|
121
|
+
def mv(*files,**opts)
|
122
|
+
FileUtils.mv(files,self,**opts)
|
123
|
+
end
|
124
|
+
|
125
|
+
#find already exists
|
126
|
+
def dr_find(*args,&b)
|
127
|
+
require 'dr/sh/utils'
|
128
|
+
SH::ShellUtils.find(self,*args,&b)
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.home
|
132
|
+
return Pathname.new(Dir.home)
|
133
|
+
end
|
134
|
+
def self.hometilde
|
135
|
+
return Pathname.new('~')
|
136
|
+
end
|
137
|
+
def self.slash
|
138
|
+
return Pathname.new("/")
|
139
|
+
end
|
140
|
+
#differ from Pathname.pwd in that this returns a relative path
|
141
|
+
def self.current
|
142
|
+
return Pathname.new(".")
|
143
|
+
end
|
144
|
+
def self.null
|
145
|
+
return Pathname.new('/dev/null')
|
146
|
+
end
|
147
|
+
|
148
|
+
#Pathname / 'usr'
|
149
|
+
def self./(path)
|
150
|
+
new(path)
|
151
|
+
end
|
152
|
+
#Pathname['/usr']
|
153
|
+
def self.[](path)
|
154
|
+
new(path)
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# vim: foldmethod=marker
|
2
|
+
require 'open3'
|
3
|
+
require 'shellwords'
|
4
|
+
|
5
|
+
module SH
|
6
|
+
module Run #{{{
|
7
|
+
extend(self)
|
8
|
+
#the run_* commands here capture all the output
|
9
|
+
def run_command(*command)
|
10
|
+
return Open3.capture3(*command)
|
11
|
+
end
|
12
|
+
|
13
|
+
def run_output(*command)
|
14
|
+
stdout,_stderr,_status = run_command(*command)
|
15
|
+
return stdout
|
16
|
+
end
|
17
|
+
|
18
|
+
def run_status(*command)
|
19
|
+
_stdout,_stderr,status = run_command(*command)
|
20
|
+
return status.success?
|
21
|
+
end
|
22
|
+
|
23
|
+
#a simple wrapper for %x//
|
24
|
+
def run_simple(*command, quiet: false, fail_mode: :error, chomp: false)
|
25
|
+
if command.length > 1
|
26
|
+
launch=command.shelljoin
|
27
|
+
else
|
28
|
+
launch=command.first
|
29
|
+
end
|
30
|
+
launch+=" 2>/dev/null" if quiet
|
31
|
+
begin
|
32
|
+
out = %x/#{launch}/
|
33
|
+
status=$?.success?
|
34
|
+
rescue => e
|
35
|
+
status=false
|
36
|
+
case fail_mode
|
37
|
+
when :error
|
38
|
+
raise e
|
39
|
+
when :empty
|
40
|
+
out=""
|
41
|
+
end
|
42
|
+
end
|
43
|
+
out.chomp! if chomp
|
44
|
+
return out, status
|
45
|
+
end
|
46
|
+
|
47
|
+
#capture stdout and status, silence stderr
|
48
|
+
def run(*args)
|
49
|
+
begin
|
50
|
+
if Open3.respond_to?(:capture3) then
|
51
|
+
out, _error, status=Open3.capture3(*args)
|
52
|
+
return out, status.success?
|
53
|
+
else
|
54
|
+
out = `#{args.shelljoin} 2>/dev/null`
|
55
|
+
status=$?
|
56
|
+
return out, status.success?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
#same as Run, but if we get interrupted once, we don't want to launch any more commands
|
62
|
+
module Interrupt #{{{
|
63
|
+
extend(self)
|
64
|
+
@interrupted=false
|
65
|
+
def run_command(*args)
|
66
|
+
if !@interrupted
|
67
|
+
begin
|
68
|
+
DR::Run.run_command(*args)
|
69
|
+
rescue Interrupt #interruption
|
70
|
+
@interrupted=true
|
71
|
+
return "", "", false
|
72
|
+
end
|
73
|
+
else
|
74
|
+
return "", "", false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def run(*command)
|
79
|
+
if !@interrupted
|
80
|
+
begin
|
81
|
+
return DR::Run.run(*args)
|
82
|
+
rescue Interrupt #interruption
|
83
|
+
@interrupted=true
|
84
|
+
return "", false
|
85
|
+
end
|
86
|
+
else
|
87
|
+
return "", false
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end #}}}
|
91
|
+
end #}}}
|
92
|
+
|
93
|
+
# ProcessStatus {{{
|
94
|
+
# from methadone (process_status.rb; last import v1.3.1-2-g9be3b5a)
|
95
|
+
#
|
96
|
+
# A wrapper/enhancement of Process::Status that handles coercion and expected
|
97
|
+
# nonzero statuses
|
98
|
+
class ProcessStatus
|
99
|
+
|
100
|
+
# The exit status, either directly from a Process::Status, from the exit code, or derived from a non-Int value.
|
101
|
+
attr_reader :exitstatus, :status
|
102
|
+
|
103
|
+
# Create the ProcessStatus with the given status.
|
104
|
+
# respond to success?,exitstatus,status
|
105
|
+
#
|
106
|
+
# status:: if this responds to #exitstatus, that method is used to extract the exit code. If it's an Int, that is used as the exit code. Otherwise, it's truthiness is used: 0 for truthy, 1 for falsey.
|
107
|
+
# expected:: an Int or Array of Int representing the expected exit status, other than zero, that represent "success".
|
108
|
+
#Ex usage: stdout,stderr,status = DR::Run.run_command(*command,**opts)
|
109
|
+
#process_status = DR::ProcessStatus.new(status,expected)
|
110
|
+
def initialize(status,expected=nil)
|
111
|
+
@status=status
|
112
|
+
@exitstatus = derive_exitstatus(status)
|
113
|
+
@success = ([0] + Array(expected)).include?(@exitstatus)
|
114
|
+
end
|
115
|
+
|
116
|
+
# True if the exit status was a successul (i.e. expected) one.
|
117
|
+
def success?
|
118
|
+
@success
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def derive_exitstatus(status)
|
124
|
+
status = if status.respond_to? :exitstatus
|
125
|
+
status.exitstatus
|
126
|
+
else
|
127
|
+
status
|
128
|
+
end
|
129
|
+
if status.kind_of? Fixnum
|
130
|
+
status
|
131
|
+
elsif status
|
132
|
+
0
|
133
|
+
else
|
134
|
+
1
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
# }}}
|
139
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
# vim: foldmethod=marker
|
2
|
+
#from methadone (error.rb, exit_now.rb, process_status.rb, run.rb; last
|
3
|
+
#import v1.3.1-2-g9be3b5a)
|
4
|
+
require_relative 'logger'
|
5
|
+
require_relative 'run'
|
6
|
+
require 'simplecolor'
|
7
|
+
|
8
|
+
module SH
|
9
|
+
# ExitNow {{{
|
10
|
+
# Standard exception you can throw to exit with a given status code.
|
11
|
+
# Generally, you should prefer DR::ExitNow.exit_now! over using this
|
12
|
+
# directly, however you may wish to create a rich hierarchy of exceptions
|
13
|
+
# that extend from this in your app, so this is provided if you wish to
|
14
|
+
# do so.
|
15
|
+
class ExitError < StandardError
|
16
|
+
attr_reader :exit_code
|
17
|
+
# Create an Error with the given status code and message
|
18
|
+
def initialize(exit_code,message=nil)
|
19
|
+
super(message)
|
20
|
+
@exit_code = exit_code
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Provides #exit_now! You might mix this into your business logic classes
|
25
|
+
# if they will need to exit the program with a human-readable error
|
26
|
+
# message.
|
27
|
+
module ExitNow
|
28
|
+
# Call this to exit the program immediately
|
29
|
+
# with the given error code and message.
|
30
|
+
# +exit_code+:: exit status you'd like to exit with
|
31
|
+
# +message+:: message to display to the user explaining the problem
|
32
|
+
# If +exit_code+ is a String and +message+ is omitted, +exit_code+ will
|
33
|
+
# === Examples
|
34
|
+
# exit_now!(4,"Oh noes!")
|
35
|
+
# # => exit app with status 4 and show the user "Oh noes!" on stderr
|
36
|
+
# exit_now!("Oh noes!")
|
37
|
+
# # => exit app with status 1 and show the user "Oh noes!" on stderr
|
38
|
+
# exit_now!(4)
|
39
|
+
# # => exit app with status 4 and dont' give the user a message (how rude of you)
|
40
|
+
def exit_now!(exit_code,message=nil)
|
41
|
+
if exit_code.kind_of?(String) && message.nil?
|
42
|
+
raise ExitError.new(1,exit_code)
|
43
|
+
else
|
44
|
+
raise ExitError.new(exit_code,message)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
# }}}
|
49
|
+
# Sh {{{
|
50
|
+
# Module with various helper methods for executing external commands.
|
51
|
+
# In most cases, you can use #sh to run commands and have decent logging
|
52
|
+
# done.
|
53
|
+
# == Examples
|
54
|
+
#
|
55
|
+
# extend SH::Sh
|
56
|
+
#
|
57
|
+
# sh 'cp foo.txt /tmp'
|
58
|
+
# # => logs the command to DEBUG, executes the command, logs its output to DEBUG and its error output to WARN, returns 0
|
59
|
+
#
|
60
|
+
# sh 'cp non_existent_file.txt /nowhere_good'
|
61
|
+
# # => logs the command to DEBUG, executes the command, logs its output to INFO and its error output to WARN, returns the nonzero exit status of the underlying command
|
62
|
+
#
|
63
|
+
# sh! 'cp non_existent_file.txt /nowhere_good'
|
64
|
+
# # => same as above, EXCEPT, raises a Methadone::FailedCommandError
|
65
|
+
#
|
66
|
+
# sh 'cp foo.txt /tmp' do
|
67
|
+
# # Behaves exactly as before, but this block is called after
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# sh 'cp non_existent_file.txt /nowhere_good' do
|
71
|
+
# # This block isn't called, since the command failed
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# sh 'ls -l /tmp/' do |stdout|
|
75
|
+
# # stdout contains the output of the command
|
76
|
+
# end
|
77
|
+
# sh 'ls -l /tmp/ /non_existent_dir' do |stdout,stderr|
|
78
|
+
# # stdout contains the output of the command,
|
79
|
+
# # stderr contains the standard error output.
|
80
|
+
# end
|
81
|
+
|
82
|
+
# FailedCommandError {{{
|
83
|
+
# Thrown by certain methods when an externally-called command exits nonzero
|
84
|
+
class FailedCommandError < StandardError
|
85
|
+
# The command that caused the failure
|
86
|
+
attr_reader :command
|
87
|
+
# exit_code:: exit code of the command that caused this
|
88
|
+
# command:: the entire command-line that caused this
|
89
|
+
# custom_error_message:: an error message to show the user instead of
|
90
|
+
# the boilerplate one. Useful for allowing this exception to bubble up
|
91
|
+
# and exit the program, but to give the user something actionable.
|
92
|
+
def initialize(exit_code,command,failure_msg: nil)
|
93
|
+
error_message = String(failure_msg).empty? ? "Command '#{command}' exited #{exit_code}" : failure_msg
|
94
|
+
super(error_message)
|
95
|
+
@command = command
|
96
|
+
end
|
97
|
+
end
|
98
|
+
# }}}
|
99
|
+
|
100
|
+
module Sh
|
101
|
+
include CLILogging
|
102
|
+
extend self
|
103
|
+
attr_writer :default_sh_options
|
104
|
+
def default_sh_options
|
105
|
+
@default_sh_options||={log: false, capture: false, on_success: nil, on_failure: nil, expected:0, dryrun: false, escape: false,
|
106
|
+
log_level_execute: :debug, log_level_error: :error,
|
107
|
+
log_level_stderr: :error, log_level_stdout_success: :info,
|
108
|
+
log_level_stdout_fail: :warn}
|
109
|
+
end
|
110
|
+
|
111
|
+
# Run a shell command, capturing and logging its output.
|
112
|
+
# keywords:: log+capture
|
113
|
+
# If the command completed successfully, it's output is logged at DEBUG.
|
114
|
+
# If not, its output is logged at INFO. In either case, its
|
115
|
+
# error output is logged at WARN.
|
116
|
+
# +:expected+:: an Int or Array of Int representing error codes, <b>in addition to 0</b>, that are expected and therefore constitute success. Useful for commands that don't use exit codes the way you'd like
|
117
|
+
# name: pretty name of command
|
118
|
+
# on_success,on_failure: blocks to call on success/failure
|
119
|
+
# block:: if provided, will be called if the command exited nonzero. The block may take 0, 1, 2, or 3 arguments.
|
120
|
+
# The arguments provided are the standard output as a string, standard error as a string, and the processstatus as DR::ProcessStatus
|
121
|
+
# You should be safe to pass in a lambda instead of a block, as long as your lambda doesn't take more than three arguments
|
122
|
+
#
|
123
|
+
# Example
|
124
|
+
# sh "cp foo /tmp"
|
125
|
+
# sh "ls /tmp" do |stdout|
|
126
|
+
# # stdout contains the output of ls /tmp
|
127
|
+
# end
|
128
|
+
# sh "ls -l /tmp foobar" do |stdout,stderr|
|
129
|
+
# # ...
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# Returns the exit status of the command. Note that if the command doesn't exist, this returns 127.
|
133
|
+
def sh(*command, **opts, &block)
|
134
|
+
defaults=default_sh_options
|
135
|
+
curopts=defaults.dup
|
136
|
+
defaults.keys.each do |k|
|
137
|
+
v=opts.delete(k)
|
138
|
+
curopts[k]=v if v
|
139
|
+
end
|
140
|
+
log=curopts[:log]
|
141
|
+
command=command.first if command.length==1 and command.first.kind_of?(Array)
|
142
|
+
command_name = curopts[:name] || command_name(command)
|
143
|
+
command=command.shelljoin if curopts[:escape]
|
144
|
+
sh_logger.send(curopts[:log_level_execute], SimpleColor.color("Executing '#{command_name}'",:bold)) if log
|
145
|
+
|
146
|
+
if !curopts[:dryrun]
|
147
|
+
if curopts[:capture]
|
148
|
+
case command
|
149
|
+
when Array
|
150
|
+
stdout,stderr,status = DR::Run.run_command(*command,**opts)
|
151
|
+
else
|
152
|
+
stdout,stderr,status = DR::Run.run_command(command.to_s,**opts)
|
153
|
+
end
|
154
|
+
else
|
155
|
+
case command
|
156
|
+
when Array
|
157
|
+
system(*command,**opts)
|
158
|
+
else
|
159
|
+
system(command.to_s,**opts)
|
160
|
+
end
|
161
|
+
status=$?
|
162
|
+
stdout=nil; stderr=nil
|
163
|
+
end
|
164
|
+
else
|
165
|
+
puts command.to_s
|
166
|
+
status=0
|
167
|
+
stdout=nil; stderr=nil
|
168
|
+
end
|
169
|
+
process_status = ProcessStatus.new(status,curopts[:expected])
|
170
|
+
|
171
|
+
sh_logger.send(curopts[:log_level_stderr], SimpleColor.color("stderr output of '#{command_name}':\n",:bold,:red)+stderr) unless stderr.nil? or stderr.strip.length == 0 or !log
|
172
|
+
if process_status.success?
|
173
|
+
sh_logger.send(curopts[:log_level_stdout_success], SimpleColor.color("stdout output of '#{command_name}':\n",:bold,:green)+stdout) unless stdout.nil? or stdout.strip.length == 0 or !log
|
174
|
+
curopts[:on_success].call(stdout,stderr,process_status) unless curopts[:on_success].nil?
|
175
|
+
block.call(stdout,stderr,process_status) unless block.nil?
|
176
|
+
else
|
177
|
+
sh_logger.send(curopts[:log_level_stdout_fail], SimpleColor.color("stdout output of '#{command_name}':\n",:bold,:yellow)+stdout) unless stdout.nil? or stdout.strip.length == 0 or !log
|
178
|
+
sh_logger.send(curopts[:log_level_error], SimpleColor.color("Error running '#{command_name}': #{process_status.status}",:red,:bold)) if log
|
179
|
+
curopts[:on_failure].call(stdout,stderr,process_status) unless curopts[:on_failure].nil?
|
180
|
+
end
|
181
|
+
return process_status.success?,stdout,stderr,process_status
|
182
|
+
|
183
|
+
rescue SystemCallError => ex
|
184
|
+
sh_logger.send(curopts[:log_level_error], SimpleColor.color("Error running '#{command_name}': #{ex.message}",:red,:bold)) if log
|
185
|
+
return 127
|
186
|
+
end
|
187
|
+
|
188
|
+
# Run a command, throwing an exception if the command exited nonzero.
|
189
|
+
# Otherwise, behaves exactly like #sh.
|
190
|
+
# Raises DR::FailedCommandError if the command exited nonzero.
|
191
|
+
# Examples:
|
192
|
+
#
|
193
|
+
# sh!("rsync foo bar")
|
194
|
+
# # => if command fails, app exits and user sees: "error: Command 'rsync foo bar' exited 12"
|
195
|
+
# sh!("rsync foo bar", :failure_msg => "Couldn't rsync, check log for details")
|
196
|
+
# # => if command fails, app exits and user sees: "error: Couldn't rsync, check log for details
|
197
|
+
def sh!(*args,failure_msg: nil,**opts, &block)
|
198
|
+
on_failure=Proc.new do |*blockargs|
|
199
|
+
process_status=blockargs.last
|
200
|
+
raise DR::FailedCommandError.new(process_status.exitstatus,command_name(args),failure_msg: failure_msg)
|
201
|
+
end
|
202
|
+
sh(*args,**opts,on_failure: on_failure,&block)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Override the default logger (which is the one provided by CLILogging).
|
206
|
+
# You would do this if you want a custom logger or you aren't mixing-in
|
207
|
+
# CLILogging.
|
208
|
+
#
|
209
|
+
# Note that this method is *not* called <tt>sh_logger=</tt> to avoid annoying situations
|
210
|
+
# where Ruby thinks you are setting a local variable
|
211
|
+
def change_sh_logger(logger)
|
212
|
+
@sh_logger = logger
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
def command_name(command)
|
217
|
+
if command.size == 1
|
218
|
+
return command.first.to_s
|
219
|
+
else
|
220
|
+
return command.to_s
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def sh_logger
|
225
|
+
@sh_logger ||= begin
|
226
|
+
raise StandardError, "No logger set! Please include DR::CLILogging
|
227
|
+
ng or provide your own via #change_sh_logger." unless self.respond_to?(:logger)
|
228
|
+
self.logger
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
#SH::ShLog.sh is like SH::Sh.sh but with login enabled even when
|
235
|
+
#command succeed
|
236
|
+
module ShLog
|
237
|
+
include Sh
|
238
|
+
extend self
|
239
|
+
@default_sh_options=default_sh_options
|
240
|
+
@default_sh_options[:log]=true
|
241
|
+
@default_sh_options[:log_level_execute]=:info
|
242
|
+
end
|
243
|
+
# }}}
|
244
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'dr/ruby_ext/core_ext'
|
3
|
+
require_relative 'pathname'
|
4
|
+
require_relative 'parser'
|
5
|
+
|
6
|
+
module SH
|
7
|
+
module ShellExport
|
8
|
+
extend self
|
9
|
+
|
10
|
+
#export a value for SHELL consumption
|
11
|
+
def export_value(v)
|
12
|
+
case v
|
13
|
+
when String
|
14
|
+
return v.shellescape
|
15
|
+
when Array
|
16
|
+
return "(#{v.map {|i| i.to_s.shellescape}.join(' ')})"
|
17
|
+
when Hash
|
18
|
+
return "(#{v.map {|k,v| k.to_s.shellescape+" "+v.to_s.shellescape}.join(' ')})"
|
19
|
+
when nil
|
20
|
+
return ""
|
21
|
+
else
|
22
|
+
return v.to_s.shellescape
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#from {ploum: plim} return something like
|
27
|
+
#ploum=plim
|
28
|
+
#that can be evaluated by the shell
|
29
|
+
def export_hash(hash, local: false, export: false, prefix:"")
|
30
|
+
r=""
|
31
|
+
r+="local #{hash.keys.map {|s| s.to_s.upcase}.join(" ")}\n" if local
|
32
|
+
hash.each do |k,v|
|
33
|
+
name=prefix+k.to_s.upcase
|
34
|
+
r+="typeset -A #{name};\n" if Hash === v
|
35
|
+
r+=name+"="+export_value(v)+";\n"
|
36
|
+
end
|
37
|
+
r+="export #{hash.keys.map {|k| prefix+k.to_s.upcase}.join(" ")}\n" if export
|
38
|
+
return r
|
39
|
+
end
|
40
|
+
|
41
|
+
#export_variable("ploum","plam") yields ploum="plam"
|
42
|
+
def export_variable(name, value, local: false, export: false)
|
43
|
+
r=""
|
44
|
+
#name=name.upcase
|
45
|
+
r+="local #{name}\n" if local
|
46
|
+
r+="typeset -A #{name};\n" if Hash === value
|
47
|
+
r+=name+"="+export_value(value)+";\n"
|
48
|
+
r+="export #{name}\n" if export
|
49
|
+
return r
|
50
|
+
end
|
51
|
+
|
52
|
+
#export_parse(hash,"name:value")
|
53
|
+
#will output name=$(hash[value])
|
54
|
+
#special cases: when value = '/' we return the full hash
|
55
|
+
# when value ends by /, we return the splitted hash (and name serves
|
56
|
+
# as a prefix)
|
57
|
+
#Ex: Numenor ~ $ ./mine/00COMPUTERS.rb --export=//
|
58
|
+
# HOSTNAME=Numenor;
|
59
|
+
# HOSTTYPE=perso;
|
60
|
+
# HOMEPATH=/home/dams;...
|
61
|
+
# Numenor ~ $ ./mine/00COMPUTERS.rb --export=syst/
|
62
|
+
# LAPTOP=true;
|
63
|
+
# ARCH=i686;...
|
64
|
+
#Remark: in name:value, we don't put name in uppercase
|
65
|
+
#But in split hash mode, we put the keys in uppercase (to prevent collisions)
|
66
|
+
def export_parse(hash,s)
|
67
|
+
r=""
|
68
|
+
args=Parser.parse_string(s)
|
69
|
+
args[:values].each do |k,v|
|
70
|
+
name=k
|
71
|
+
if !v
|
72
|
+
v=name
|
73
|
+
#in split mode, don't use prefix if name gave the value
|
74
|
+
name= name.size==1? "all" : "" if name[name.size-1]=="/"
|
75
|
+
end
|
76
|
+
if v.size>1 && v[v.size-1]=='/'
|
77
|
+
all=true
|
78
|
+
v=v[0...v.size-1]
|
79
|
+
end
|
80
|
+
value=hash.keyed_value(v)
|
81
|
+
if all
|
82
|
+
r+=export_hash(value, local: args[:opts][k]["local"], export: args[:opts][k]["export"], prefix: name)
|
83
|
+
else
|
84
|
+
r+=export_variable(name,value, local: args[:opts][k]["local"], export: args[:opts][k]["export"])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
return r
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
module ShellUtils
|
92
|
+
extend self
|
93
|
+
|
94
|
+
class << self
|
95
|
+
attr_accessor :orig_stdin, :orig_stdout, :orig_stderr
|
96
|
+
end
|
97
|
+
@orig_stdin=$stdin
|
98
|
+
@orig_stdout=$stdout
|
99
|
+
@orig_stderr=$stderr
|
100
|
+
|
101
|
+
#An improved find from Find::find that takes in the block the absolute and relative name of the files (+the directory where the relative file is from), and has filter options
|
102
|
+
def find(*paths, filter: nil, follow_symlink: false, depth: nil)
|
103
|
+
block_given? or return enum_for(__method__, *paths, filter: filter, follow_symlink: follow_symlink, depth: depth)
|
104
|
+
|
105
|
+
paths.collect!{|d| raise Errno::ENOENT unless File.exist?(d); Pathname.new(d.dup)}
|
106
|
+
paths.collect!{|d| [ d, Pathname.new('.'), d ]}
|
107
|
+
while filedata = paths.shift
|
108
|
+
file, filerel, fileabs = *filedata
|
109
|
+
catch(:prune) do #use throw(:prune) to skip a path
|
110
|
+
Dir.chdir(fileabs) do
|
111
|
+
#if the filter is true, we don't yield the file
|
112
|
+
case filter
|
113
|
+
when Proc
|
114
|
+
filter.call(file,filerel,fileabs)
|
115
|
+
when Array
|
116
|
+
filter.all? do |test|
|
117
|
+
case test
|
118
|
+
when :directory? #special case
|
119
|
+
file.directory? && !file.symlink?
|
120
|
+
else
|
121
|
+
file.send(test)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end or yield file.dup.taint, filerel.dup.taint, fileabs.dup.taint
|
125
|
+
if file.directory? and (depth.nil? or filerel.each_filename.size <= depth) then
|
126
|
+
next if !follow_symlink && file.symlink?
|
127
|
+
file.children(false).sort.reverse_each do |f|
|
128
|
+
fj = file + f
|
129
|
+
f = filerel + f
|
130
|
+
paths.unshift [fj.untaint,f.untaint,fileabs]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
#all output is sent to the pager
|
139
|
+
def run_pager(opt=nil)
|
140
|
+
return unless $stdout.tty? and opt != :never
|
141
|
+
read, write = IO.pipe
|
142
|
+
|
143
|
+
unless Kernel.fork # Child process
|
144
|
+
$stdout.reopen(write)
|
145
|
+
$stderr.reopen(write) if $stderr.tty?
|
146
|
+
read.close
|
147
|
+
write.close
|
148
|
+
return
|
149
|
+
end
|
150
|
+
|
151
|
+
# Parent process, become pager
|
152
|
+
$stdin.reopen(read)
|
153
|
+
read.close
|
154
|
+
write.close
|
155
|
+
|
156
|
+
#ENV['LESS'] = 'FSRX' # Don't page if the input is short enough
|
157
|
+
lessenv=ENV['LESS']
|
158
|
+
lessenv="-FRX" if lessenv.empty?
|
159
|
+
lessenv+="F" unless lessenv.match(/F/) or opt == :always
|
160
|
+
lessenv+="R" unless lessenv.match(/R/)
|
161
|
+
lessenv+="X" unless lessenv.match(/X/)
|
162
|
+
ENV['LESS']=lessenv
|
163
|
+
|
164
|
+
Kernel.select [$stdin] # Wait until we have input before we start the pager
|
165
|
+
pager = ENV['PAGER'] || 'less'
|
166
|
+
exec pager rescue exec "/bin/sh", "-c", pager
|
167
|
+
end
|
168
|
+
|
169
|
+
#inside run_pager, escape from the pager
|
170
|
+
#does not work :-(
|
171
|
+
def escape_pager(mode=nil)
|
172
|
+
case mode
|
173
|
+
when :orig
|
174
|
+
stdout=ShellUtils.orig_stdout
|
175
|
+
stderr=ShellUtils.orig_stderr
|
176
|
+
stdin=ShellUtils.orig_stdin
|
177
|
+
else
|
178
|
+
stdout=STDOUT
|
179
|
+
stderr=STDERR
|
180
|
+
stdin=STDIN
|
181
|
+
end
|
182
|
+
$stdout.reopen(stdout)
|
183
|
+
$stderr.reopen(stderr)
|
184
|
+
$stdin.reopen(stdin)
|
185
|
+
end
|
186
|
+
|
187
|
+
def output_list(s, split: "\n")
|
188
|
+
s=s.shelljoin if s.kind_of?(Array)
|
189
|
+
return open("| #{s}").read.split(split)
|
190
|
+
end
|
191
|
+
|
192
|
+
#Stolen from mkmf:
|
193
|
+
# Searches for the executable +bin+ on +path+. The default path is your
|
194
|
+
# +PATH+ environment variable. If that isn't defined, it will resort to
|
195
|
+
# searching /usr/local/bin, /usr/ucb, /usr/bin and /bin.
|
196
|
+
# If found, it will return the full path, including the executable name, of
|
197
|
+
# where it was found.
|
198
|
+
# exts: an array of extensions to add
|
199
|
+
def find_executable(bin, path = nil, exts: nil)
|
200
|
+
executable_file = lambda do |name|
|
201
|
+
name=Pathname.new(name)
|
202
|
+
return name if name.file? and name.executable?
|
203
|
+
end
|
204
|
+
#we use Proc so that 'return' escapes the block
|
205
|
+
try_executable = Proc.new do |file|
|
206
|
+
return file if executable_file.call(file)
|
207
|
+
exts && exts.each {|ext| executable_file.call(ext = file.append_name(ext)) and return ext}
|
208
|
+
nil
|
209
|
+
end
|
210
|
+
|
211
|
+
bin=Pathname.new(bin)
|
212
|
+
if bin.absolute?
|
213
|
+
try_executable.call(bin)
|
214
|
+
else
|
215
|
+
path ||= ENV['PATH'] || %w[/usr/local/bin /usr/bin /bin]
|
216
|
+
path = path.split(File::PATH_SEPARATOR) unless path.kind_of?(Array)
|
217
|
+
path.each do |dir|
|
218
|
+
dir=Pathname.new(dir)
|
219
|
+
try_executable.call(dir+bin)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
nil
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gemspec = YAML.load_file('gemspec.yml')
|
7
|
+
|
8
|
+
gem.name = gemspec.fetch('name')
|
9
|
+
gem.version = gemspec.fetch('version') do
|
10
|
+
lib_dir = File.join(File.dirname(__FILE__),'lib')
|
11
|
+
$LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
|
12
|
+
|
13
|
+
require 'shell_helpers/version'
|
14
|
+
ShellHelpers::VERSION
|
15
|
+
end
|
16
|
+
|
17
|
+
gem.summary = gemspec['summary']
|
18
|
+
gem.description = gemspec['description']
|
19
|
+
gem.licenses = Array(gemspec['license'])
|
20
|
+
gem.authors = Array(gemspec['authors'])
|
21
|
+
gem.email = gemspec['email']
|
22
|
+
gem.homepage = gemspec['homepage']
|
23
|
+
|
24
|
+
glob = lambda { |patterns| gem.files & Dir[*patterns] }
|
25
|
+
|
26
|
+
gem.files = `git ls-files`.split($/)
|
27
|
+
gem.files = glob[gemspec['files']] if gemspec['files']
|
28
|
+
|
29
|
+
gem.executables = gemspec.fetch('executables') do
|
30
|
+
glob['bin/*'].map { |path| File.basename(path) }
|
31
|
+
end
|
32
|
+
gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.'
|
33
|
+
|
34
|
+
gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb']
|
35
|
+
gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}']
|
36
|
+
|
37
|
+
gem.require_paths = Array(gemspec.fetch('require_paths') {
|
38
|
+
%w[ext lib].select { |dir| File.directory?(dir) }
|
39
|
+
})
|
40
|
+
|
41
|
+
gem.requirements = gemspec['requirements']
|
42
|
+
gem.required_ruby_version = gemspec['required_ruby_version']
|
43
|
+
gem.required_rubygems_version = gemspec['required_rubygems_version']
|
44
|
+
gem.post_install_message = gemspec['post_install_message']
|
45
|
+
|
46
|
+
split = lambda { |string| string.split(/,\s*/) }
|
47
|
+
|
48
|
+
if gemspec['dependencies']
|
49
|
+
gemspec['dependencies'].each do |name,versions|
|
50
|
+
gem.add_dependency(name,split[versions])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
if gemspec['development_dependencies']
|
55
|
+
gemspec['development_dependencies'].each do |name,versions|
|
56
|
+
gem.add_development_dependency(name,split[versions])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/test/helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shell_helpers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Damien Robert
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: drain
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubygems-tasks
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.2'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.8'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.8'
|
69
|
+
description: 'Collection of script to ease working with the shell with ruby.
|
70
|
+
|
71
|
+
'
|
72
|
+
email: Damien.Olivier.Robert+gems@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files:
|
76
|
+
- ChangeLog.md
|
77
|
+
- LICENSE.txt
|
78
|
+
- README.md
|
79
|
+
files:
|
80
|
+
- ".document"
|
81
|
+
- ".gitignore"
|
82
|
+
- ".yardopts"
|
83
|
+
- ChangeLog.md
|
84
|
+
- LICENSE.txt
|
85
|
+
- README.md
|
86
|
+
- Rakefile
|
87
|
+
- gemspec.yml
|
88
|
+
- lib/shell_helpers.rb
|
89
|
+
- lib/shell_helpers/logger.rb
|
90
|
+
- lib/shell_helpers/pathname.rb
|
91
|
+
- lib/shell_helpers/run.rb
|
92
|
+
- lib/shell_helpers/sh.rb
|
93
|
+
- lib/shell_helpers/utils.rb
|
94
|
+
- lib/shell_helpers/version.rb
|
95
|
+
- shell_helpers.gemspec
|
96
|
+
- test/helper.rb
|
97
|
+
- test/test_shell_helpers.rb
|
98
|
+
homepage: https://github.com/DamienRobert/shell_helpers#readme
|
99
|
+
licenses:
|
100
|
+
- MIT
|
101
|
+
metadata: {}
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 2.4.5
|
119
|
+
signing_key:
|
120
|
+
specification_version: 4
|
121
|
+
summary: Shell Helpers
|
122
|
+
test_files: []
|