upm 0.0.0 → 0.1.0

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