upm 0.0.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 60027059fa9203a42da7e7fddd3fafc34f889d49
4
- data.tar.gz: 4b37928fe24f4eeb3336fbd70eb74ec02c58e110
2
+ SHA256:
3
+ metadata.gz: 4da92ce63688e75043c91e8d40bf9a1829687b974a4bc21bc40e23c06ce108f9
4
+ data.tar.gz: 0eb4815df308f4dfaffdcc9fee5a877c65dda0d65f85ed3149fdb338bc34517b
5
5
  SHA512:
6
- metadata.gz: 875dfba6634cd5121ea32b22342b7a8b659d56668c0bb0c23b5e08d128ef09b6bc44e7df1eff73dbaffcdd5cc4db739501364d5471382606367da09dbe3c144d
7
- data.tar.gz: 4c56742fd0d34c5ab7201a17691f1f02f98aa7f48f1a4e5ec35ae11ae27c765a7a3350ea2a9b3225137e32a4309fc46ba38c6d43391048aae50896af821fab44
6
+ metadata.gz: d8ca05eff0cd5037aa9a30b9206d79933fdd7fe6bc8a7d225185278c0696965416ba5d0315fec9cc25a1ecb1ed566ed80704b213c9fee1e37b515596a1d50dc0
7
+ data.tar.gz: 7a0341ee388207404b5a785d7d27fd5a563f886a8638165539f1a790520e9c097b8d33bbbd2f79d046183a6515e73d5d1cb08ab44f0e3a0d33fb7d4e58f6df94
data/.gemspec CHANGED
@@ -2,26 +2,17 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  Gem::Specification.new do |s|
5
- s.name = "upm"
6
-
7
- s.version = File.read "VERSION"
8
- s.date = File.mtime("VERSION").strftime("%Y-%m-%d")
9
-
10
- s.summary = "Universal Package Manager"
5
+ s.name = "upm"
6
+ s.version = File.read "VERSION"
7
+ s.date = File.mtime("VERSION").strftime("%Y-%m-%d")
8
+ s.summary = "Universal Package Manager"
11
9
  s.description = "Wrap all known command-line package tools with a consistent and pretty interface."
12
-
13
- s.homepage = "http://github.com/epitron/upm/"
14
- s.licenses = ["WTFPL"]
15
-
16
- s.email = "chris@ill-logic.com"
17
- s.authors = ["epitron"]
18
-
19
- # s.executables = ["upm", "up", "u"]
10
+ s.homepage = "http://github.com/epitron/upm/"
11
+ s.licenses = ["WTFPL"]
12
+ s.email = "chris@ill-logic.com"
13
+ s.authors = ["epitron"]
14
+ s.executables = ["upm"]
20
15
 
21
16
  s.files = `git ls`.lines.map(&:strip)
22
17
  s.extra_rdoc_files = ["README.md", "LICENSE"]
23
-
24
- # s.require_paths = %w[lib]
25
-
26
- # s.add_dependency "slop", "~> 3.0"
27
18
  end
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.gem
data/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # upm: Universal Package Manager
2
2
 
3
+ ## Concept:
4
+
5
+ Wraps all known package managers to provide a consistent and pretty interface, along with advanced features not supported by all tools, such as rollback and pinning.
6
+
7
+ All tools will give you modern, pretty, colourful, piped-to-less output, and you'll only have to remember one consistent set of commands. It'll also prompt you with a text UI whenever faced with ambiguity.
8
+
3
9
  ## Usage:
4
10
 
5
11
  ```
@@ -8,35 +14,36 @@ up <command> <pkg>
8
14
  u <command> <pkg>
9
15
  ```
10
16
 
11
- ## Commands
12
-
13
- * list
14
- * search
15
- * install
16
- * sync/update
17
- * upgrade
18
- * log - show history of package installs
19
- * pin
20
- * rollback
21
- * packagers - detect package systems, and enable/disable them
22
- * sources/mirrors - select remote repositories and mirrors
23
- * verfiy - verifies integrity of installed packages
24
- * remove
25
- * clean
26
- * build
27
- * monitor - ad-hoc package manager for custom installations (like instmon)
28
- * keys - keyrings and package authentication
29
- * default - configurable default action (defaults to update only the OS)
30
-
31
- ### All searches and package names can be prefixed with their namespace:
17
+ ## Commands:
18
+
19
+ * `install`
20
+ * `remove`
21
+ * `build` - compile a package from source and install it
22
+ * `search` - using the fastest known API or service
23
+ * `list` - show all packages, or the contents of a specific package
24
+ * `info` - show metadata about a package
25
+ * `sync`/`update` - retrieve the latest package list or manifest
26
+ * `upgrade` - install new versions of all packages
27
+ * `pin` - pinning a package means it won't be automatically upgraded
28
+ * `rollback` - revert to an earlier version of a package (including its dependencies)
29
+ * `log` - show history of package installs
30
+ * `packagers` - detect installed package managers, and pick which ones upm should wrap
31
+ * `sources`/`mirrors` - select remote repositories and mirrors
32
+ * `verfiy` - verifies the integrity of installed files
33
+ * `clean` - clear out the local package cache
34
+ * `monitor` - ad-hoc package manager for custom installations (like instmon)
35
+ * `keys` - keyrings and package authentication
36
+ * `default` - configure the action to take when no arguments are passed to "upm" (defaults to "os:update")
37
+
38
+ ### Any command that takes a package name can be prefixed with the package tool's namespace:
32
39
 
33
40
  ```
34
- ruby:<pkg> (or r:)
35
- deb:<pkg> (or d:)
36
- rpm:<pkg>
37
- python:<pkg>
38
- py:<pkg>
39
- pip:<pkg>
41
+ os:<pkg> -- automatically select the package manager for the current unix distribution
42
+ deb:<pkg> (or d: u:)
43
+ rpm:<pkg> (or yum: y:)
44
+ bsd:<pkg> (or b:)
45
+ ruby:<pkg> (or r: gem:)
46
+ python:<pkg>,<pkg> (or py: p: pip:)
40
47
  ```
41
48
 
42
49
  ### ...or suffixed with its file extension:
@@ -48,25 +55,52 @@ pip:<pkg>
48
55
  <pkg>.pip
49
56
  ```
50
57
 
51
- ## Supported packagers:
58
+ ## Package tools to wrap:
59
+
60
+ * Arch: `pacman`/`aur`/`abs` (svn mirror)
61
+ * Debian/Ubuntu: `apt-get`/`dpkg` (+ curated list of ppa's)
62
+ * RedHat/Fedora/Centos: `yum`/`rpm`
63
+ * Mac OSX: `brew`/`fink`/`ports`
64
+ * FreeBSD: `pkg`/`ports`
65
+ * OpenBSD: `pkg_add`/`ports`
66
+ * NetBSD: `pkgin`/`ports`
67
+ * SmartOS/Illumos: `pkgin`
68
+ * Windows: `apt-cyg`/`mingw-get`/`nuget`/`Windows Update`/(as-yet-not-created package manager, "winget")
69
+ * Wine: `winetricks`
70
+ * Ruby: `rubygems`
71
+ * Python: `pip`/`easy_install`
72
+ * Javascript: `npm`
73
+ * Clojure: `leiningen`
74
+ * Java: `gradle`
75
+ * Erlang: `rebar`
76
+ * Scala: `sbt`
77
+ * Rust: `cargo`
78
+ * R: `cran`
79
+ * Lua: `rocks`
80
+ * Julia: `Pkg`
81
+ * Haskell: `cabal`
82
+ * Perl: `cpan`
83
+ * go: `go-get`
84
+
85
+ ...[and many more!](https://en.wikipedia.org/wiki/List_of_software_package_management_systems)
86
+
87
+
88
+ ## What it might look like:
89
+
90
+ Info:
91
+
92
+ ![acs](https://raw.githubusercontent.com/epitron/scripts/master/screenshots/acs.png)
93
+
94
+ Log:
95
+
96
+ ![paclog](https://raw.githubusercontent.com/epitron/scripts/master/screenshots/paclog.png)
52
97
 
53
- * OpenBSD: pkg_add
54
- * FreeBSD: pkg
55
- * RedHat/Fedora/Centos: yum/rpm
56
- * Debian/Ubuntu: apt-get/dpkg (+ curated list of ppa's)
57
- * Windows: apt-cyg/nuget/"winget" (new package manager)
58
- * Arch: pacman/aur/abs (svn mirror)
59
- * Mac OSX: brew/fink
60
- * Python: pip/easy_install
61
- * Ruby: rubygems
62
- * Haskell: cabal
63
- * Perl: cpan
64
- * go: go-get
65
- * Java: ?
98
+ Rollback:
66
99
 
100
+ ![pacman-rollback](https://raw.githubusercontent.com/epitron/scripts/master/screenshots/pacman-rollback.png)
67
101
 
68
102
  ## TODOs:
69
103
 
70
- * Build using the pretty text-mode UI that passenger-install uses
104
+ * Use the pretty text-mode UI that passenger-install uses
71
105
  * Context-dependent operation
72
- * if you're in a ruby project, prioritize the 'ruby' namespace
106
+ * eg: if you're in a ruby project's directory, set the 'ruby' namespace to highest priority
data/Rakefile CHANGED
@@ -9,7 +9,7 @@ task :release => :build do
9
9
  system "gem push #{pkgname}-#{gem_version}.gem"
10
10
  end
11
11
 
12
- task :gem => :release
12
+ task :gem => :build
13
13
 
14
14
  task :install => :build do
15
15
  system "gem install #{pkgname}-#{gem_version}.gem"
data/TODO.md ADDED
@@ -0,0 +1,38 @@
1
+ # TODO
2
+
3
+ ## More package managers
4
+
5
+ Currently missing:
6
+ * RedHat/Fedora/CentOS
7
+ * OSX
8
+ * FreeBSD
9
+ * OpenBSD
10
+ * SuSE
11
+
12
+ ## fzf
13
+
14
+ Use fzf for "list" output (or other commands that require selecting, like "remove")
15
+
16
+ ## Proper ARGV parser
17
+
18
+ * Something better than "command, *args = ARGV" (with "--help" available, at the very least.)
19
+
20
+ ## Streaming pipes
21
+
22
+ * Make the `run` command able to grep the output while streaming the results to the screen.
23
+ * Make run pretend to be a tty, so I don't need `--color=always`.
24
+
25
+ ## Figure out how to integrate language package managers
26
+
27
+ * The packages that you can get through gem/pip/luarocks/etc. are often duplicated in the OS-level package managers. Should there be a preference?
28
+ * Should the search command show matches from all available package tools? (There could be a configure step where the user says which package managers should be included, and which have preference)
29
+ * Possibilites:
30
+ * upm install --ruby <pkg>
31
+ * upm install ruby:<pkg>,<pkg>
32
+ * Add detectors for language-specific package-managers
33
+ * Help screen needs to display language-specific package managers
34
+ * `upm help --ruby` should show available ruby commands
35
+
36
+ ## Mirror Selector
37
+
38
+ * Do a ping test on available mirrors, and use fzf to select
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.1.0
data/bin/upm ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ bin_dir = File.dirname(File.realpath(__FILE__))
3
+ $LOAD_PATH.unshift(File.expand_path(File.join('..', 'lib'), bin_dir))
4
+
5
+ require 'upm'
6
+
7
+ tool = UPM::Tool.for_os
8
+ command, *args = ARGV
9
+
10
+ if command.nil?
11
+ tool.help
12
+ else
13
+ tool.call_command command, args
14
+ end
@@ -0,0 +1,320 @@
1
+ #
2
+ # ANSI Colour-coding (for terminals that support it.)
3
+ #
4
+ # Originally by defunkt (Chris Wanstrath)
5
+ # Enhanced by epitron (Chris Gahan)
6
+ #
7
+ # It adds methods to String to allow easy coloring.
8
+ #
9
+ # (Note: Colors are automatically disabled if your program is piped to another program,
10
+ # ie: if STDOUT is not a TTY)
11
+ #
12
+ # Basic examples:
13
+ #
14
+ # >> "this is red".red
15
+ # >> "this is red with a blue background (read: ugly)".red_on_blue
16
+ # >> "this is light blue".light_blue
17
+ # >> "this is red with an underline".red.underline
18
+ # >> "this is really bold and really blue".bold.blue
19
+ #
20
+ # Color tags:
21
+ # (Note: You don't *need* to close color tags, but you can!)
22
+ #
23
+ # >> "<yellow>This is using <green>color tags</green> to colorize.".colorize
24
+ # >> "<1>N<9>u<11>m<15>eric tags!".colorize
25
+ # (For those who still remember the DOS color palette and want more terse tagged-colors.)
26
+ #
27
+ # Highlight search results:
28
+ #
29
+ # >> string.gsub(pattern) { |match| "<yellow>#{match}</yellow>" }.colorize
30
+ #
31
+ # Forcing colors:
32
+ #
33
+ # Since the presence of a terminal is detected automatically, the colors will be
34
+ # disabled when you pipe your program to another program. However, if you want to
35
+ # show colors when piped (eg: when you pipe to `less -R`), you can force it:
36
+ #
37
+ # >> Colored.enable!
38
+ # >> Colored.disable!
39
+ # >> Colored.enable_temporarily { puts "whee!".red }
40
+ #
41
+
42
+ require 'set'
43
+ require 'rbconfig'
44
+ require 'Win32/Console/ANSI' if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
45
+ #require 'Win32/Console/ANSI' if RUBY_PLATFORM =~ /win32/
46
+
47
+ module Colored
48
+ extend self
49
+
50
+ @@is_tty = STDOUT.isatty
51
+
52
+ COLORS = {
53
+ 'black' => 30,
54
+ 'red' => 31,
55
+ 'green' => 32,
56
+ 'yellow' => 33,
57
+ 'blue' => 34,
58
+ 'magenta' => 35,
59
+ 'purple' => 35,
60
+ 'cyan' => 36,
61
+ 'white' => 37
62
+ }
63
+
64
+ EXTRAS = {
65
+ 'clear' => 0,
66
+ 'bold' => 1,
67
+ 'light' => 1,
68
+ 'underline' => 4,
69
+ 'reversed' => 7
70
+ }
71
+
72
+ #
73
+ # BBS-style numeric color codes.
74
+ #
75
+ BBS_COLOR_TABLE = {
76
+ 0 => :black,
77
+ 1 => :blue,
78
+ 2 => :green,
79
+ 3 => :cyan,
80
+ 4 => :red,
81
+ 5 => :magenta,
82
+ 6 => :yellow,
83
+ 7 => :white,
84
+ 8 => :light_black,
85
+ 9 => :light_blue,
86
+ 10 => :light_green,
87
+ 11 => :light_cyan,
88
+ 12 => :light_red,
89
+ 13 => :light_magenta,
90
+ 14 => :light_yellow,
91
+ 15 => :light_white,
92
+ }
93
+
94
+ VALID_COLORS = begin
95
+ normal = COLORS.keys
96
+ lights = normal.map { |fore| "light_#{fore}" }
97
+ brights = normal.map { |fore| "bright_#{fore}" }
98
+ on_backgrounds = normal.map { |fore| normal.map { |back| "#{fore}_on_#{back}" } }.flatten
99
+
100
+ Set.new(normal + lights + brights + on_backgrounds + ["grey", "gray"])
101
+ end
102
+
103
+ COLORS.each do |color, value|
104
+ define_method(color) do
105
+ colorize(self, :foreground => color)
106
+ end
107
+
108
+ define_method("on_#{color}") do
109
+ colorize(self, :background => color)
110
+ end
111
+
112
+ define_method("light_#{color}") do
113
+ colorize(self, :foreground => color, :extra => 'bold')
114
+ end
115
+
116
+ define_method("bright_#{color}") do
117
+ colorize(self, :foreground => color, :extra => 'bold')
118
+ end
119
+
120
+ COLORS.each do |highlight, value|
121
+ next if color == highlight
122
+
123
+ define_method("#{color}_on_#{highlight}") do
124
+ colorize(self, :foreground => color, :background => highlight)
125
+ end
126
+
127
+ define_method("light_#{color}_on_#{highlight}") do
128
+ colorize(self, :foreground => color, :background => highlight, :extra => 'bold')
129
+ end
130
+
131
+ end
132
+ end
133
+
134
+ alias_method :gray, :light_black
135
+ alias_method :grey, :light_black
136
+
137
+ EXTRAS.each do |extra, value|
138
+ next if extra == 'clear'
139
+ define_method(extra) do
140
+ colorize(self, :extra => extra)
141
+ end
142
+ end
143
+
144
+ define_method(:to_eol) do
145
+ tmp = sub(/^(\e\[[\[\e0-9;m]+m)/, "\\1\e[2K")
146
+ if tmp == self
147
+ return "\e[2K" << self
148
+ end
149
+ tmp
150
+ end
151
+
152
+ #
153
+ # Colorize a string (this method is called by #red, #blue, #red_on_green, etc.)
154
+ #
155
+ # Accepts options:
156
+ # :foreground
157
+ # The name of the foreground color as a string.
158
+ # :background
159
+ # The name of the background color as a string.
160
+ # :extra
161
+ # Extra styling, like 'bold', 'light', 'underline', 'reversed', or 'clear'.
162
+ #
163
+ #
164
+ # With no options, it uses tagged colors:
165
+ #
166
+ # puts "<light_green><magenta>*</magenta> Hey mom! I am <light_blue>SO</light_blue> colored right now.</light_green>".colorize
167
+ #
168
+ # Or numeric ANSI tagged colors (from the BBS days):
169
+ # puts "<10><5>*</5> Hey mom! I am <9>SO</9> colored right now.</10>".colorize
170
+ #
171
+ #
172
+ def colorize(string=nil, options = {})
173
+ if string == nil
174
+ return self.tagged_colors
175
+ end
176
+
177
+ if @@is_tty
178
+ colored = [color(options[:foreground]), color("on_#{options[:background]}"), extra(options[:extra])].compact * ''
179
+ colored << string
180
+ colored << extra(:clear)
181
+ else
182
+ string
183
+ end
184
+ end
185
+
186
+ #
187
+ # Find all occurrences of "pattern" in the string and highlight them
188
+ # with the specified color. (defaults to light_yellow)
189
+ #
190
+ # The pattern can be a string or a regular expression.
191
+ #
192
+ def highlight(pattern, color=:light_yellow, &block)
193
+ pattern = Regexp.new(Regexp.escape(pattern)) if pattern.is_a? String
194
+
195
+ if block_given?
196
+ gsub(pattern, &block)
197
+ else
198
+ gsub(pattern) { |match| match.send(color) }
199
+ end
200
+ end
201
+
202
+ #
203
+ # An array of all possible colors.
204
+ #
205
+ def colors
206
+ @@colors ||= COLORS.keys.sort
207
+ end
208
+
209
+ #
210
+ # Returns the terminal code for one of the extra styling options.
211
+ #
212
+ def extra(extra_name)
213
+ extra_name = extra_name.to_s
214
+ "\e[#{EXTRAS[extra_name]}m" if EXTRAS[extra_name]
215
+ end
216
+
217
+ #
218
+ # Returns the terminal code for a specified color.
219
+ #
220
+ def color(color_name)
221
+ background = color_name.to_s =~ /on_/
222
+ color_name = color_name.to_s.sub('on_', '')
223
+ return unless color_name && COLORS[color_name]
224
+ "\e[#{COLORS[color_name] + (background ? 10 : 0)}m"
225
+ end
226
+
227
+ #
228
+ # Will color commands actually modify the strings?
229
+ #
230
+ def enabled?
231
+ @@is_tty
232
+ end
233
+
234
+ alias_method :is_tty?, :enabled?
235
+
236
+ #
237
+ # Color commands will always produce colored strings, regardless
238
+ # of whether the script is being run in a terminal.
239
+ #
240
+ def enable!
241
+ @@is_tty = true
242
+ end
243
+
244
+ alias_method :force!, :enable!
245
+
246
+ #
247
+ # Enable Colored just for this block.
248
+ #
249
+ def enable_temporarily(&block)
250
+ last_state = @@is_tty
251
+
252
+ @@is_tty = true
253
+ block.call
254
+ @@is_tty = last_state
255
+ end
256
+
257
+ #
258
+ # Color commands will do nothing.
259
+ #
260
+ def disable!
261
+ @@is_tty = false
262
+ end
263
+
264
+ #
265
+ # Is this string legal?
266
+ #
267
+ def valid_tag?(tag)
268
+ VALID_COLORS.include?(tag) or
269
+ (tag =~ /^\d+$/ and BBS_COLOR_TABLE.include?(tag.to_i) )
270
+ end
271
+
272
+ #
273
+ # Colorize a string that has "color tags".
274
+ #
275
+ def tagged_colors
276
+ stack = []
277
+
278
+ # split the string into tags and literal strings
279
+ tokens = self.split(/(<\/?[\w\d_]+>)/)
280
+ tokens.delete_if { |token| token.size == 0 }
281
+
282
+ result = ""
283
+
284
+ tokens.each do |token|
285
+
286
+ # token is an opening tag!
287
+
288
+ if /<([\w\d_]+)>/ =~ token and valid_tag?($1)
289
+ stack.push $1
290
+
291
+ # token is a closing tag!
292
+
293
+ elsif /<\/([\w\d_]+)>/ =~ token and valid_tag?($1)
294
+
295
+ # if this color is on the stack somwehere...
296
+ if pos = stack.rindex($1)
297
+ # close the tag by removing it from the stack
298
+ stack.delete_at pos
299
+ else
300
+ raise "Error: tried to close an unopened color tag -- #{token}"
301
+ end
302
+
303
+ # token is a literal string!
304
+
305
+ else
306
+
307
+ color = (stack.last || "white")
308
+ color = BBS_COLOR_TABLE[color.to_i] if color =~ /^\d+$/
309
+ result << token.send(color)
310
+
311
+ end
312
+
313
+ end
314
+
315
+ result
316
+ end
317
+
318
+ end unless Object.const_defined? :Colored
319
+
320
+ String.send(:include, Colored)
@@ -0,0 +1,131 @@
1
+ require 'date'
2
+
3
+ class DateTime
4
+ def to_i; to_time.to_i; end
5
+ end
6
+
7
+ module Enumerable
8
+ #
9
+ # Split this enumerable into chunks, given some boundary condition. (Returns an array of arrays.)
10
+ #
11
+ # Options:
12
+ # :include_boundary => true #=> include the element that you're splitting at in the results
13
+ # (default: false)
14
+ # :after => true #=> split after the matched element (only has an effect when used with :include_boundary)
15
+ # (default: false)
16
+ # :once => flase #=> only perform one split (default: false)
17
+ #
18
+ # Examples:
19
+ # [1,2,3,4,5].split{ |e| e == 3 }
20
+ # #=> [ [1,2], [4,5] ]
21
+ #
22
+ # "hello\n\nthere\n".each_line.split_at("\n").to_a
23
+ # #=> [ ["hello\n"], ["there\n"] ]
24
+ #
25
+ # [1,2,3,4,5].split(:include_boundary=>true) { |e| e == 3 }
26
+ # #=> [ [1,2], [3,4,5] ]
27
+ #
28
+ # chapters = File.read("ebook.txt").split(/Chapter \d+/, :include_boundary=>true)
29
+ # #=> [ ["Chapter 1", ...], ["Chapter 2", ...], etc. ]
30
+ #
31
+ def split_at(matcher=nil, options={}, &block)
32
+ include_boundary = options[:include_boundary] || false
33
+
34
+ if matcher.nil?
35
+ boundary_test_proc = block
36
+ else
37
+ if matcher.is_a? Regexp
38
+ boundary_test_proc = proc { |element| element =~ matcher }
39
+ else
40
+ boundary_test_proc = proc { |element| element == matcher }
41
+ end
42
+ end
43
+
44
+ Enumerator.new do |yielder|
45
+ current_chunk = []
46
+ splits = 0
47
+ max_splits = options[:once] == true ? 1 : options[:max_splits]
48
+
49
+ each do |e|
50
+
51
+ if boundary_test_proc.call(e) and (max_splits == nil or splits < max_splits)
52
+
53
+ if current_chunk.empty? and not include_boundary
54
+ next # hit 2 boundaries in a row... just keep moving, people!
55
+ end
56
+
57
+ if options[:after]
58
+ # split after boundary
59
+ current_chunk << e if include_boundary # include the boundary, if necessary
60
+ yielder << current_chunk # shift everything after the boundary into the resultset
61
+ current_chunk = [] # start a new result
62
+ else
63
+ # split before boundary
64
+ yielder << current_chunk # shift before the boundary into the resultset
65
+ current_chunk = [] # start a new result
66
+ current_chunk << e if include_boundary # include the boundary, if necessary
67
+ end
68
+
69
+ splits += 1
70
+
71
+ else
72
+ current_chunk << e
73
+ end
74
+
75
+ end
76
+
77
+ yielder << current_chunk if current_chunk.any?
78
+
79
+ end
80
+ end
81
+
82
+ #
83
+ # Split the array into chunks, cutting between the matched element and the next element.
84
+ #
85
+ # Example:
86
+ # [1,2,3,4].split_after{|e| e == 3 } #=> [ [1,2,3], [4] ]
87
+ #
88
+ def split_after(matcher=nil, options={}, &block)
89
+ options[:after] ||= true
90
+ options[:include_boundary] ||= true
91
+ split_at(matcher, options, &block)
92
+ end
93
+
94
+ #
95
+ # Split the array into chunks, cutting before each matched element.
96
+ #
97
+ # Example:
98
+ # [1,2,3,4].split_before{|e| e == 3 } #=> [ [1,2], [3,4] ]
99
+ #
100
+ def split_before(matcher=nil, options={}, &block)
101
+ options[:include_boundary] ||= true
102
+ split_at(matcher, options, &block)
103
+ end
104
+
105
+ #
106
+ # Split the array into chunks, cutting between two elements.
107
+ #
108
+ # Example:
109
+ # [1,1,2,2].split_between{|a,b| a != b } #=> [ [1,1], [2,2] ]
110
+ #
111
+ def split_between(&block)
112
+ Enumerator.new do |yielder|
113
+ current = []
114
+ last = nil
115
+
116
+ each_cons(2) do |a,b|
117
+ current << a
118
+ if yield(a,b)
119
+ yielder << current
120
+ current = []
121
+ end
122
+ last = b
123
+ end
124
+
125
+ current << last unless last.nil?
126
+ yielder << current
127
+ end
128
+ end
129
+
130
+ alias_method :cut_between, :split_between
131
+ end
@@ -0,0 +1,57 @@
1
+ #
2
+ # Create scrollable output via less!
3
+ #
4
+ # This command runs `less` in a subprocess, and gives you the IO to its STDIN pipe
5
+ # so that you can communicate with it.
6
+ #
7
+ # Example:
8
+ #
9
+ # lesspipe do |less|
10
+ # 50.times { less.puts "Hi mom!" }
11
+ # end
12
+ #
13
+ # The default less parameters are:
14
+ # * Allow colour
15
+ # * Don't wrap lines longer than the screen
16
+ # * Quit immediately (without paging) if there's less than one screen of text.
17
+ #
18
+ # You can change these options by passing a hash to `lesspipe`, like so:
19
+ #
20
+ # lesspipe(:wrap=>false) { |less| less.puts essay.to_s }
21
+ #
22
+ # It accepts the following boolean options:
23
+ # :color => Allow ANSI colour codes?
24
+ # :wrap => Wrap long lines?
25
+ # :always => Always page, even if there's less than one page of text?
26
+ #
27
+ def lesspipe(*args)
28
+ if args.any? and args.last.is_a?(Hash)
29
+ options = args.pop
30
+ else
31
+ options = {}
32
+ end
33
+
34
+ output = args.first if args.any?
35
+
36
+ params = []
37
+ params << "-R" unless options[:color] == false
38
+ params << "-S" unless options[:wrap] == true
39
+ params << "-F" unless options[:always] == true
40
+ if options[:tail] == true
41
+ params << "+\\>"
42
+ $stderr.puts "Seeking to end of stream..."
43
+ end
44
+ params << "-X"
45
+
46
+ IO.popen("less #{params * ' '}", "w") do |less|
47
+ if output
48
+ less.puts output
49
+ else
50
+ yield less
51
+ end
52
+ end
53
+
54
+ rescue Errno::EPIPE, Interrupt
55
+ # less just quit -- eat the exception.
56
+ end
57
+
@@ -0,0 +1,56 @@
1
+ module UPM
2
+ class LogParser
3
+
4
+ def initialize(klass, log_glob)
5
+ @klass = klass
6
+ @log_glob = log_glob
7
+ end
8
+
9
+ def log_events
10
+ return to_enum(:log_events) unless block_given?
11
+
12
+ yielder = proc do |io|
13
+ io.each_line do |line|
14
+ if e = @klass.from_line(line.strip)
15
+ yield e
16
+ end
17
+ end
18
+ end
19
+
20
+ logs = Dir[@log_glob].sort_by { |path| File.mtime(path) }
21
+
22
+ logs.each do |log|
23
+ if log =~ /\.gz$/
24
+ IO.popen(["zcat", log], &yielder)
25
+ else
26
+ open(log, &yielder)
27
+ end
28
+ end
29
+ end
30
+
31
+ def display
32
+ lesspipe(tail: true) do |less|
33
+ groups = log_events.split_between { |a,b| (b.date.to_i - a.date.to_i) > 60 }
34
+
35
+ groups.each do |group|
36
+ first, last = group.first.date, group.last.date
37
+ elapsed = (last.to_i - first.to_i) / 60
38
+
39
+ empty_group = true
40
+
41
+ group.each do |ev|
42
+ # Print the header only if the query matched something in this group
43
+ if empty_group
44
+ empty_group = false
45
+ less.puts
46
+ less.puts "<8>== <11>#{first.strftime("<10>%Y-%m-%d <7>at <2>%l:%M %p")} <7>(<9>#{elapsed} <7>minute session) <8>========".colorize
47
+ end
48
+
49
+ less.puts ev
50
+ end
51
+ end
52
+ end # lesspipe
53
+ end
54
+
55
+ end # LogParser
56
+ end
data/lib/upm/tool.rb ADDED
@@ -0,0 +1,180 @@
1
+
2
+ # os:<pkg> -- automatically select the package manager for the current unix distribution
3
+ # deb:<pkg> (or d: u:)
4
+ # rpm:<pkg> (or yum: y:)
5
+ # bsd:<pkg> (or b:)
6
+ # ruby:<pkg> (or r: gem:)
7
+ # python:<pkg>,<pkg> (or py: p: pip:)
8
+
9
+ module UPM
10
+
11
+ class Tool
12
+
13
+ COMMAND_HELP = {
14
+ "install" => "install a package",
15
+ "remove/uninstall" => "remove a package",
16
+ "build" => "compile a package from source and install it",
17
+ "search" => "using the fastest known API or service",
18
+ "list" => "list installed packages (or search their names if extra arguments are supplied)",
19
+ "info" => "show metadata about a package",
20
+ "sync/update" => "retrieve the latest package list or manifest",
21
+ "upgrade" => "install new versions of all packages",
22
+ "pin" => "pinning a package means it won't be automatically upgraded",
23
+ "rollback" => "revert to an earlier version of a package (including its dependencies)",
24
+ "log" => "show history of package installs",
25
+ "packagers" => "detect installed package managers, and pick which ones upm should wrap",
26
+ "mirrors/sources" => "manage remote repositories and mirrors",
27
+ "verfiy" => "verify the integrity of installed files",
28
+ "clean" => "clear out the local package cache",
29
+ "monitor" => "ad-hoc package manager for custom installations (like instmon)",
30
+ "keys" => "keyrings and package authentication",
31
+ "default" => "configure the action to take when no arguments are passed to 'upm' (defaults to 'os:update')",
32
+ }
33
+
34
+ ALIASES = {
35
+ "file" => "files",
36
+ "sync" => "update",
37
+ "sources" => "mirrors",
38
+ "show" => "info",
39
+ }
40
+
41
+ @@tools = {}
42
+
43
+ def self.register_tools!
44
+ Dir["#{__dir__}/tools/*.rb"].each { |lib| require_relative(lib) }
45
+ end
46
+
47
+ def self.os_release
48
+ @os_release ||= open("/etc/os-release") do |io|
49
+ io.read.scan(/^(\w+)="?(.+?)"?$/)
50
+ end.to_h
51
+ end
52
+
53
+ def self.current_os_names
54
+ # ID=ubuntu
55
+ # ID_LIKE=debian
56
+ os_release.values_at("ID", "ID_LIKE")
57
+ end
58
+
59
+ def self.nice_os_name
60
+ os_release.values_at("PRETTY_NAME", "NAME", "ID", "ID_LIKE").first
61
+ end
62
+
63
+ def self.for_os(os_names=nil)
64
+ os_names = os_names ? [os_names].flatten : current_os_names
65
+ @@tools.find { |name, tool| os_names.any? { |name| tool.os.include? name } }.last
66
+ end
67
+
68
+ def self.tools
69
+ @@tools
70
+ end
71
+
72
+ def initialize(name, &block)
73
+ @name = name
74
+ instance_eval(&block)
75
+
76
+ @@tools[name] = self
77
+ end
78
+
79
+ def call_command(name, args)
80
+ if block = (@cmds[name] || @cmds[ALIASES[name]])
81
+ block.call args
82
+ else
83
+ puts "Command #{name} not supported in #{@name}"
84
+ end
85
+ end
86
+
87
+ def help
88
+ puts " Detected OS: #{Tool.nice_os_name}"
89
+ puts "Package manager: #{@name}"
90
+ puts
91
+ puts "Available commands:"
92
+ available = COMMAND_HELP.select do |name, desc|
93
+ names = name.split("/")
94
+ names.any? { |name| @cmds[name] }
95
+ end
96
+
97
+ max_width = available.map(&:first).map(&:size).max
98
+ available.each do |name, desc|
99
+ puts " #{name.rjust(max_width)} | #{desc}"
100
+ end
101
+ end
102
+
103
+ def print_files(*paths, include: nil, exclude: nil)
104
+ lesspipe do |less|
105
+ paths.each do |path|
106
+ less.puts "<8>=== <11>#{path} <8>========".colorize
107
+ open(path) do |io|
108
+ enum = io.each_line
109
+ enum = enum.grep(include) if include
110
+ enum = enum.reject { |line| line[exclude] } if exclude
111
+ enum.each { |line| less.puts line }
112
+ end
113
+ less.puts
114
+ end
115
+ end
116
+ end
117
+
118
+ ## DSL methods
119
+
120
+ def prefix(name)
121
+ @prefix = name
122
+ end
123
+
124
+ def command(name, shell_command=nil, root: false, paged: false, &block)
125
+ @cmds ||= {}
126
+
127
+ if block_given?
128
+ @cmds[name] = block
129
+ elsif shell_command
130
+ if shell_command.is_a? String
131
+ shell_command = shell_command.split
132
+ elsif not shell_command.is_a? Array
133
+ raise "Error: command argument must be a String or an Array; it was a #{cmd.class}"
134
+ end
135
+
136
+ shell_command.unshift "sudo" if root
137
+
138
+ @cmds[name] = proc { |args| run(*shell_command, *args, paged: paged) }
139
+ end
140
+ end
141
+
142
+ def os(*names)
143
+ names.any? ? @os = names : @os
144
+ end
145
+
146
+ ## Helpers
147
+
148
+ def run(*args, paged: false, grep: nil)
149
+ if !paged and !grep
150
+ system(*args)
151
+ else
152
+
153
+ IO.popen(args, err: [:child, :out]) do |command_io|
154
+
155
+ if grep
156
+ pattern = grep.is_a?(Regexp) ? grep.source : grep.to_s
157
+ grep_io = IO.popen(["grep", "--color=always", "-Ei", pattern], "w+")
158
+ IO.copy_stream(command_io, grep_io)
159
+ grep_io.close_write
160
+ command_io = grep_io
161
+ end
162
+
163
+ if paged
164
+ lesspipe do |less|
165
+ IO.copy_stream(command_io, less)
166
+ end
167
+ else
168
+ IO.copy_stream(command_io, STDOUT)
169
+ end
170
+
171
+ end
172
+
173
+ $?.to_i == 0
174
+ end
175
+ end
176
+
177
+ end # class Tool
178
+
179
+ end # module UPM
180
+
@@ -0,0 +1,68 @@
1
+ UPM::Tool.new "apt" do
2
+
3
+ os "debian", "ubuntu"
4
+
5
+ command "install", "apt install", root: true
6
+ command "update", "apt update", root: true
7
+ command "upgrade", "apt upgrade", root: true
8
+ command "remove", "apt remove", root: true
9
+
10
+ command "files", "dpkg-query -L", paged: true
11
+ command "search", "apt search", paged: true
12
+ command "info", "apt show", paged: true
13
+
14
+ command "list" do |args|
15
+ if args.any?
16
+ query = args.join
17
+ run("dpkg", "-l", grep: query, paged: true)
18
+ else
19
+ run("dpkg", "-l", paged: true)
20
+ end
21
+ end
22
+
23
+ command "mirrors" do
24
+ print_files("/etc/apt/sources.list", *Dir["/etc/apt/sources.list.d/*"], exclude: /^(#|$)/)
25
+ end
26
+
27
+ command "log" do
28
+ UPM::LogParser.new(DpkgEvent, "/var/log/dpkg*").display
29
+ end
30
+
31
+ class DpkgEvent < Struct.new(:datestr, :date, :cmd, :name, :v1, :v2)
32
+
33
+ # 2010-12-03 01:36:56 remove gir1.0-mutter-2.31 2.31.5-0ubuntu9 2.31.5-0ubuntu9
34
+ # 2010-12-03 01:36:58 install gir1.0-mutter-2.91 <none> 2.91.2+git20101114.982a10ac-0ubuntu1~11.04~ricotz0
35
+ #LINE_RE = /^(.+ .+) (status \w+|\w+) (.+) (.+)$/
36
+ LINE_RE = /^(.+ .+) (remove|install|upgrade) (.+) (.+) (.+)$/
37
+
38
+ CMD_COLORS = {
39
+ 'remove' => :light_red,
40
+ 'install' => :light_yellow,
41
+ 'upgrade' => :light_green,
42
+ nil => :white,
43
+ }
44
+
45
+ def self.parse_date(date)
46
+ DateTime.strptime(date, "%Y-%m-%d %H:%M:%S")
47
+ end
48
+
49
+ def self.from_line(line)
50
+ if line =~ LINE_RE
51
+ new($1, parse_date($1), $2, $3, $4, $5)
52
+ else
53
+ nil
54
+ end
55
+ end
56
+
57
+ def cmd_color
58
+ CMD_COLORS[cmd]
59
+ end
60
+
61
+ def to_s
62
+ date, time = datestr.split
63
+ "<grey>[<white>#{date} #{time}<grey>] <#{cmd_color}>#{cmd} <light_cyan>#{name} <light_white>#{v2} <white>(#{v1})".colorize
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,105 @@
1
+ UPM::Tool.new "pacman" do
2
+
3
+ os "arch"
4
+
5
+ bin = ["pacman", "--color=always"]
6
+
7
+ command "install", [*bin, "-S"], root: true
8
+ command "update", [*bin, "-Sy"], root: true
9
+ command "upgrade", [*bin, "-Syu"], root: true
10
+ command "remove", [*bin, "-R"], root: true
11
+
12
+ command "files", [*bin, "-Ql"], paged: true
13
+ command "search", [*bin, "-Ss"], paged: true
14
+
15
+ command "info" do |args|
16
+ run(*bin, "-Qi", *args) || run(*bin, "-Si", *args)
17
+ end
18
+
19
+ command "list" do |args|
20
+ if args.any?
21
+ query = args.join
22
+ run(*bin, "-Q", grep: query, paged: true)
23
+ else
24
+ run(*bin, "-Q", paged: true)
25
+ end
26
+ end
27
+
28
+ command "mirrors" do
29
+ print_files("/etc/pacman.d/mirrorlist", exclude: /^(#|$)/)
30
+ end
31
+
32
+ command "depends" do |args|
33
+ packages_that_depend_on = proc do |package|
34
+ result = []
35
+
36
+ [`pacman -Sii #{package}`, `pacman -Qi #{package}`].each do |output|
37
+ output.each_line do |l|
38
+ if l =~ /Required By\s+: (.+)/
39
+ result += $1.strip.split unless $1["None"]
40
+ break
41
+ end
42
+ end
43
+ end
44
+
45
+ result
46
+ end
47
+
48
+ args.each do |arg|
49
+ puts "=== Packages which depend on: #{arg} ============"
50
+ packages = packages_that_depend_on.call(arg)
51
+ puts
52
+ run *bin, "-Ss", "^(#{packages.join '|'})$" # upstream packages
53
+ run *bin, "-Qs", "^(#{packages.join '|'})$" # packages that are only installed locally
54
+ puts
55
+ end
56
+ end
57
+
58
+ command "log" do
59
+ UPM::LogParser.new(PacmanEvent, "/var/log/pacman.log*").display
60
+ end
61
+
62
+ class PacmanEvent < Struct.new(:datestr, :date, :cmd, :name, :v1, :v2)
63
+
64
+ # [2015-01-04 04:21] [PACMAN] installed lib32-libidn (1.29-1)
65
+ # [2015-01-04 04:21] [PACMAN] upgraded lib32-curl (7.38.0-1 -> 7.39.0-1)
66
+ # [2015-01-07 04:39] [ALPM] upgraded intel-tbb (4.3_20141023-1 -> 4.3_20141204-1)
67
+ # [2015-01-07 04:39] [ALPM] upgraded iso-codes (3.54-1 -> 3.57-1)
68
+
69
+ DATE_RE = /[\d:-]+/
70
+ LINE_RE = /^\[(#{DATE_RE} #{DATE_RE})\](?: \[(?:PACMAN|ALPM)\])? (removed|installed|upgraded) (.+) \((.+)(?: -> (.+))?\)$/
71
+
72
+ CMD_COLORS = {
73
+ 'removed' => :light_red,
74
+ 'installed' => :light_yellow,
75
+ 'upgraded' => :light_green,
76
+ nil => :white,
77
+ }
78
+
79
+ def self.parse_date(date)
80
+ DateTime.strptime(date, "%Y-%m-%d %H:%M")
81
+ end
82
+
83
+ def self.from_line(line)
84
+ if line =~ LINE_RE
85
+ new($1, parse_date($1), $2, $3, $4, $5)
86
+ else
87
+ nil
88
+ end
89
+ end
90
+
91
+ def cmd_color
92
+ CMD_COLORS[cmd]
93
+ end
94
+
95
+ def to_s
96
+ date, time = datestr.split
97
+ "<grey>[<white>#{date} #{time}<grey>] <#{cmd_color}>#{cmd} <light_cyan>#{name} #{"<light_white>#{v2} " if v2}<white>(#{v1})".colorize
98
+ end
99
+
100
+ end
101
+
102
+
103
+ end
104
+
105
+
@@ -0,0 +1,20 @@
1
+ UPM::Tool.new "xbps" do
2
+
3
+ os "void"
4
+
5
+ command "install", "xbps-install", root: true
6
+ command "update", "xbps-install -S", root: true
7
+ command "upgrade", "xbps-install -Su", root: true
8
+ command "files", "xbps-query -f"
9
+ command "search", "xbps-query --regex -Rs"
10
+ # command "info", ""
11
+
12
+ command "list" do |args|
13
+ if args.any?
14
+ run "xbps-query", "-f", *args
15
+ else
16
+ run "xbps-query", "-l"
17
+ end
18
+ end
19
+
20
+ end
data/lib/upm.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'upm/colored'
2
+ require 'upm/lesspipe'
3
+ require 'upm/core_ext'
4
+ require 'upm/log_parser'
5
+
6
+ module UPM
7
+
8
+ require 'upm/tool'
9
+
10
+ Tool.register_tools!
11
+
12
+ end
metadata CHANGED
@@ -1,29 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: upm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - epitron
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-12 00:00:00.000000000 Z
11
+ date: 2018-05-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Wrap all known command-line package tools with a consistent and pretty
14
14
  interface.
15
15
  email: chris@ill-logic.com
16
- executables: []
16
+ executables:
17
+ - upm
17
18
  extensions: []
18
19
  extra_rdoc_files:
19
20
  - README.md
20
21
  - LICENSE
21
22
  files:
22
23
  - ".gemspec"
24
+ - ".gitignore"
23
25
  - LICENSE
24
26
  - README.md
25
27
  - Rakefile
28
+ - TODO.md
26
29
  - VERSION
30
+ - bin/upm
31
+ - lib/upm.rb
32
+ - lib/upm/colored.rb
33
+ - lib/upm/core_ext.rb
34
+ - lib/upm/lesspipe.rb
35
+ - lib/upm/log_parser.rb
36
+ - lib/upm/tool.rb
37
+ - lib/upm/tools/apt.rb
38
+ - lib/upm/tools/pacman.rb
39
+ - lib/upm/tools/xbps.rb
27
40
  homepage: http://github.com/epitron/upm/
28
41
  licenses:
29
42
  - WTFPL
@@ -44,9 +57,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
44
57
  version: '0'
45
58
  requirements: []
46
59
  rubyforge_project:
47
- rubygems_version: 2.4.5
60
+ rubygems_version: 2.7.6
48
61
  signing_key:
49
62
  specification_version: 4
50
63
  summary: Universal Package Manager
51
64
  test_files: []
52
- has_rdoc: