shell_helpers 0.1.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/.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: []
|