purl 1.3.1 → 1.4.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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/README.md +170 -1
  4. data/exe/purl +435 -0
  5. data/lib/purl/version.rb +1 -1
  6. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c6c3fd2d17a4601a4c296b18ce67d41c423a5893aeb2b79cd484c0028863abd
4
- data.tar.gz: 2c5e92ca69e3bb8442df158f34944fad645319cdbac5fb45875c7c8521be5d0c
3
+ metadata.gz: 4a3bf4cd374d69577598e05fa94658b7768c94b3522314a3382d1f9b872b203b
4
+ data.tar.gz: e1ddc0ec9b9ec7d675c9eeff8527af612610bcce7bc344313fe5c79a4975bd7d
5
5
  SHA512:
6
- metadata.gz: 34e7c52f43f92146d605e06338f25b5d12f80f495e320901aed32206dc3e743029be9b51eb5bcd1f84ef46ca450a76ac45fa7c74da5b2506eb3d53b82db6c840
7
- data.tar.gz: 7941f3f3cb65695448599c590e75ef06198fdafe4739e7587e0f7518f6e58fc948896452c38136a3c1a1e6e0d52acac4b766c2a5f2dd22d0a33f8b2e1b61feb1
6
+ metadata.gz: 6a4e84903907467f13205f4f9c31c29e67945f83c87d68f577008598d50da3f702d98b1c0e115a5c4a441a2babfdae6b6463da5e125f5f1630476d983a116849
7
+ data.tar.gz: b6e45ef48f2fb2d52eea945b658a24a3e3cc4236c7656ba21b8287bb5eb7b662d7adf9d46148771b48cfcb884f5f3fcce0bbbaffb54f3d7126a3d4f1f5f297a8
data/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.0] - 2025-01-06
11
+
12
+ ### Added
13
+ - Command-line interface with parse, validate, convert, url, generate, and info commands plus JSON output support
14
+
10
15
  ## [1.3.1] - 2025-08-04
11
16
 
12
17
  ### Fixed
data/README.md CHANGED
@@ -16,6 +16,7 @@ This library features comprehensive error handling with namespaced error types,
16
16
 
17
17
  ## Features
18
18
 
19
+ - **Command-line interface** with parse, validate, convert, generate, and info commands plus JSON output
19
20
  - **Comprehensive PURL parsing and validation** with 37 package types (32 official + 5 additional ecosystems)
20
21
  - **Better error handling** with namespaced error classes and contextual information
21
22
  - **Bidirectional registry URL conversion** - generate registry URLs from PURLs and parse PURLs from registry URLs
@@ -46,7 +47,175 @@ Or install it yourself as:
46
47
  gem install purl
47
48
  ```
48
49
 
49
- ## Usage
50
+ ## Command Line Interface
51
+
52
+ The purl gem includes a command-line interface that provides convenient access to all parsing, validation, conversion, and generation functionality.
53
+
54
+ ### Installation
55
+
56
+ The CLI is automatically available after installing the gem:
57
+
58
+ ```bash
59
+ gem install purl
60
+ purl --help
61
+ ```
62
+
63
+ ### Available Commands
64
+
65
+ ```bash
66
+ purl parse <purl-string> # Parse and display PURL components
67
+ purl validate <purl-string> # Validate a PURL (exit code indicates success)
68
+ purl convert <registry-url> # Convert registry URL to PURL
69
+ purl url <purl-string> # Convert PURL to registry URL
70
+ purl generate [options] # Generate PURL from components
71
+ purl info [type] # Show information about PURL types
72
+ ```
73
+
74
+ ### JSON Output
75
+
76
+ All commands support JSON output with the `--json` flag:
77
+
78
+ ```bash
79
+ purl --json parse "pkg:gem/rails@7.0.0"
80
+ purl --json info gem
81
+ ```
82
+
83
+ ### Command Examples
84
+
85
+ #### Parse a PURL
86
+ ```bash
87
+ $ purl parse "pkg:gem/rails@7.0.0"
88
+ Valid PURL: pkg:gem/rails@7.0.0
89
+ Components:
90
+ Type: gem
91
+ Namespace: (none)
92
+ Name: rails
93
+ Version: 7.0.0
94
+ Qualifiers: (none)
95
+ Subpath: (none)
96
+
97
+ $ purl --json parse "pkg:npm/@babel/core@7.0.0"
98
+ {
99
+ "success": true,
100
+ "purl": "pkg:npm/%40babel/core@7.0.0",
101
+ "components": {
102
+ "type": "npm",
103
+ "namespace": "@babel",
104
+ "name": "core",
105
+ "version": "7.0.0",
106
+ "qualifiers": {},
107
+ "subpath": null
108
+ }
109
+ }
110
+ ```
111
+
112
+ #### Validate a PURL
113
+ ```bash
114
+ $ purl validate "pkg:gem/rails@7.0.0"
115
+ Valid PURL
116
+
117
+ $ purl validate "invalid-purl"
118
+ Invalid PURL: PURL must start with 'pkg:'
119
+ ```
120
+
121
+ #### Convert Registry URL to PURL
122
+ ```bash
123
+ $ purl convert "https://rubygems.org/gems/rails"
124
+ pkg:gem/rails
125
+
126
+ $ purl convert "https://www.npmjs.com/package/@babel/core"
127
+ pkg:npm/@babel/core
128
+ ```
129
+
130
+ #### Convert PURL to Registry URL
131
+ ```bash
132
+ $ purl url "pkg:gem/rails@7.0.0"
133
+ https://rubygems.org/gems/rails
134
+
135
+ $ purl url "pkg:npm/@babel/core@7.0.0"
136
+ https://www.npmjs.com/package/@babel/core
137
+
138
+ $ purl --json url "pkg:gem/rails@7.0.0"
139
+ {
140
+ "success": true,
141
+ "purl": "pkg:gem/rails@7.0.0",
142
+ "registry_url": "https://rubygems.org/gems/rails",
143
+ "type": "gem"
144
+ }
145
+ ```
146
+
147
+ #### Generate a PURL
148
+ ```bash
149
+ $ purl generate --type gem --name rails --version 7.0.0
150
+ pkg:gem/rails@7.0.0
151
+
152
+ $ purl generate --type npm --namespace @babel --name core --version 7.0.0 --qualifiers "arch=x64,os=linux"
153
+ pkg:npm/%40babel/core@7.0.0?arch=x64&os=linux
154
+ ```
155
+
156
+ #### Show Type Information
157
+ ```bash
158
+ $ purl info gem
159
+ Type: gem
160
+ Known: Yes
161
+ Description: RubyGems
162
+ Default registry: https://rubygems.org
163
+ Registry URL generation: Yes
164
+ Reverse parsing: Yes
165
+ Examples:
166
+ pkg:gem/ruby-advisory-db-check@0.12.4
167
+ pkg:gem/rails@7.0.4
168
+ pkg:gem/bundler@2.3.26
169
+ Registry URL patterns:
170
+ https://rubygems.org/gems/:name
171
+ https://rubygems.org/gems/:name/versions/:version
172
+
173
+ $ purl info # Shows all types
174
+ Known PURL types:
175
+
176
+ alpm
177
+ Description: Arch Linux packages
178
+ Registry support: No
179
+ Reverse parsing: No
180
+ ...
181
+ Total types: 37
182
+ Registry supported: 20
183
+ ```
184
+
185
+ ### Generate Options
186
+
187
+ The `generate` command supports all PURL components:
188
+
189
+ ```bash
190
+ purl generate --help
191
+ Usage: purl generate [options]
192
+ --type TYPE Package type (required)
193
+ --name NAME Package name (required)
194
+ --namespace NAMESPACE Package namespace
195
+ --version VERSION Package version
196
+ --qualifiers QUALIFIERS Qualifiers as key=value,key2=value2
197
+ --subpath SUBPATH Package subpath
198
+ -h, --help Show this help
199
+ ```
200
+
201
+ ### Exit Codes
202
+
203
+ The CLI uses standard exit codes:
204
+ - `0` - Success
205
+ - `1` - Error (invalid PURL, unsupported operation, etc.)
206
+
207
+ This makes the CLI suitable for use in scripts and CI/CD pipelines:
208
+
209
+ ```bash
210
+ if purl validate "pkg:gem/rails@7.0.0"; then
211
+ echo "Valid PURL"
212
+ else
213
+ echo "Invalid PURL"
214
+ exit 1
215
+ fi
216
+ ```
217
+
218
+ ## Library Usage
50
219
 
51
220
  ### Basic PURL Parsing
52
221
 
data/exe/purl ADDED
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require "json"
6
+ require_relative "../lib/purl"
7
+
8
+ class PurlCLI
9
+ def self.run(args = ARGV)
10
+ new.run(args)
11
+ end
12
+
13
+ def initialize
14
+ @json_output = false
15
+ end
16
+
17
+ def run(args)
18
+ if args.empty?
19
+ puts usage
20
+ exit 1
21
+ end
22
+
23
+ # Check for global --json flag
24
+ if args.include?("--json")
25
+ @json_output = true
26
+ args.delete("--json")
27
+ end
28
+
29
+ command = args.shift
30
+ case command
31
+ when "parse"
32
+ parse_command(args)
33
+ when "validate"
34
+ validate_command(args)
35
+ when "convert"
36
+ convert_command(args)
37
+ when "generate"
38
+ generate_command(args)
39
+ when "url"
40
+ url_command(args)
41
+ when "info"
42
+ info_command(args)
43
+ when "--help", "-h", "help"
44
+ puts usage
45
+ exit 0
46
+ when "--version", "-v"
47
+ puts Purl::VERSION
48
+ exit 0
49
+ else
50
+ puts "Unknown command: #{command}"
51
+ puts usage
52
+ exit 1
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def usage
59
+ <<~USAGE
60
+ purl - Parse, validate, convert and generate Package URLs (PURLs)
61
+
62
+ Usage:
63
+ purl [--json] parse <purl-string> Parse and display PURL components
64
+ purl [--json] validate <purl-string> Validate a PURL (exit code indicates success)
65
+ purl [--json] convert <registry-url> Convert registry URL to PURL
66
+ purl [--json] url <purl-string> Convert PURL to registry URL
67
+ purl [--json] generate [options] Generate PURL from components
68
+ purl [--json] info [type] Show information about PURL types
69
+ purl --version Show version
70
+ purl --help Show this help
71
+
72
+ Global Options:
73
+ --json Output results in JSON format
74
+
75
+ Examples:
76
+ purl parse "pkg:gem/rails@7.0.0"
77
+ purl --json parse "pkg:gem/rails@7.0.0"
78
+ purl validate "pkg:npm/@babel/core@7.0.0"
79
+ purl convert "https://rubygems.org/gems/rails"
80
+ purl url "pkg:gem/rails@7.0.0"
81
+ purl generate --type gem --name rails --version 7.0.0
82
+ purl --json info gem
83
+ USAGE
84
+ end
85
+
86
+ def parse_command(args)
87
+ if args.empty?
88
+ output_error("PURL string required")
89
+ exit 1
90
+ end
91
+
92
+ purl_string = args[0]
93
+
94
+ begin
95
+ purl = Purl.parse(purl_string)
96
+
97
+ if @json_output
98
+ result = {
99
+ success: true,
100
+ purl: purl.to_s,
101
+ components: {
102
+ type: purl.type,
103
+ namespace: purl.namespace,
104
+ name: purl.name,
105
+ version: purl.version,
106
+ qualifiers: purl.qualifiers || {},
107
+ subpath: purl.subpath
108
+ }
109
+ }
110
+ puts JSON.pretty_generate(result)
111
+ else
112
+ puts "Valid PURL: #{purl.to_s}"
113
+ puts "Components:"
114
+ puts " Type: #{purl.type}"
115
+ puts " Namespace: #{purl.namespace || '(none)'}"
116
+ puts " Name: #{purl.name}"
117
+ puts " Version: #{purl.version || '(none)'}"
118
+
119
+ if purl.qualifiers && !purl.qualifiers.empty?
120
+ puts " Qualifiers:"
121
+ purl.qualifiers.each do |key, value|
122
+ puts " #{key}: #{value}"
123
+ end
124
+ else
125
+ puts " Qualifiers: (none)"
126
+ end
127
+
128
+ puts " Subpath: #{purl.subpath || '(none)'}"
129
+ end
130
+ rescue Purl::Error => e
131
+ output_error("Error parsing PURL: #{e.message}")
132
+ exit 1
133
+ end
134
+ end
135
+
136
+ def validate_command(args)
137
+ if args.empty?
138
+ output_error("PURL string required")
139
+ exit 1
140
+ end
141
+
142
+ purl_string = args[0]
143
+
144
+ begin
145
+ purl = Purl.parse(purl_string)
146
+ if @json_output
147
+ result = {
148
+ success: true,
149
+ valid: true,
150
+ purl: purl.to_s,
151
+ message: "Valid PURL"
152
+ }
153
+ puts JSON.pretty_generate(result)
154
+ else
155
+ puts "Valid PURL"
156
+ end
157
+ exit 0
158
+ rescue Purl::Error => e
159
+ if @json_output
160
+ result = {
161
+ success: false,
162
+ valid: false,
163
+ purl: purl_string,
164
+ error: e.message
165
+ }
166
+ puts JSON.pretty_generate(result)
167
+ else
168
+ puts "Invalid PURL: #{e.message}"
169
+ end
170
+ exit 1
171
+ end
172
+ end
173
+
174
+ def convert_command(args)
175
+ if args.empty?
176
+ output_error("Registry URL required")
177
+ exit 1
178
+ end
179
+
180
+ registry_url = args[0]
181
+
182
+ begin
183
+ purl = Purl.from_registry_url(registry_url)
184
+ if @json_output
185
+ result = {
186
+ success: true,
187
+ registry_url: registry_url,
188
+ purl: purl.to_s,
189
+ components: {
190
+ type: purl.type,
191
+ namespace: purl.namespace,
192
+ name: purl.name,
193
+ version: purl.version,
194
+ qualifiers: purl.qualifiers || {},
195
+ subpath: purl.subpath
196
+ }
197
+ }
198
+ puts JSON.pretty_generate(result)
199
+ else
200
+ puts purl.to_s
201
+ end
202
+ rescue Purl::Error => e
203
+ if @json_output
204
+ result = {
205
+ success: false,
206
+ registry_url: registry_url,
207
+ error: e.message
208
+ }
209
+ puts JSON.pretty_generate(result)
210
+ else
211
+ puts "Error converting URL: #{e.message}"
212
+ end
213
+ exit 1
214
+ end
215
+ end
216
+
217
+ def url_command(args)
218
+ if args.empty?
219
+ output_error("PURL string required")
220
+ exit 1
221
+ end
222
+
223
+ purl_string = args[0]
224
+
225
+ begin
226
+ purl = Purl.parse(purl_string)
227
+
228
+ unless purl.supports_registry_url?
229
+ if @json_output
230
+ result = {
231
+ success: false,
232
+ purl: purl_string,
233
+ error: "Registry URL generation not supported for type '#{purl.type}'"
234
+ }
235
+ puts JSON.pretty_generate(result)
236
+ else
237
+ puts "Error: Registry URL generation not supported for type '#{purl.type}'"
238
+ end
239
+ exit 1
240
+ end
241
+
242
+ registry_url = purl.registry_url
243
+
244
+ if @json_output
245
+ result = {
246
+ success: true,
247
+ purl: purl.to_s,
248
+ registry_url: registry_url,
249
+ type: purl.type
250
+ }
251
+ puts JSON.pretty_generate(result)
252
+ else
253
+ puts registry_url
254
+ end
255
+ rescue Purl::Error => e
256
+ if @json_output
257
+ result = {
258
+ success: false,
259
+ purl: purl_string,
260
+ error: e.message
261
+ }
262
+ puts JSON.pretty_generate(result)
263
+ else
264
+ puts "Error: #{e.message}"
265
+ end
266
+ exit 1
267
+ end
268
+ end
269
+
270
+ def generate_command(args)
271
+ options = {}
272
+ OptionParser.new do |opts|
273
+ opts.banner = "Usage: purl generate [options]"
274
+
275
+ opts.on("--type TYPE", "Package type (required)") do |v|
276
+ options[:type] = v
277
+ end
278
+
279
+ opts.on("--name NAME", "Package name (required)") do |v|
280
+ options[:name] = v
281
+ end
282
+
283
+ opts.on("--namespace NAMESPACE", "Package namespace") do |v|
284
+ options[:namespace] = v
285
+ end
286
+
287
+ opts.on("--version VERSION", "Package version") do |v|
288
+ options[:version] = v
289
+ end
290
+
291
+ opts.on("--qualifiers QUALIFIERS", "Qualifiers as key=value,key2=value2") do |v|
292
+ qualifiers = {}
293
+ v.split(",").each do |pair|
294
+ key, value = pair.split("=", 2)
295
+ if key && value
296
+ qualifiers[key.strip] = value.strip
297
+ end
298
+ end
299
+ options[:qualifiers] = qualifiers unless qualifiers.empty?
300
+ end
301
+
302
+ opts.on("--subpath SUBPATH", "Package subpath") do |v|
303
+ options[:subpath] = v
304
+ end
305
+
306
+ opts.on("-h", "--help", "Show this help") do
307
+ puts opts
308
+ exit 0
309
+ end
310
+ end.parse!(args)
311
+
312
+ unless options[:type] && options[:name]
313
+ output_error("--type and --name are required")
314
+ puts "Use 'purl generate --help' for more information" unless @json_output
315
+ exit 1
316
+ end
317
+
318
+ begin
319
+ purl = Purl::PackageURL.new(**options)
320
+ if @json_output
321
+ result = {
322
+ success: true,
323
+ purl: purl.to_s,
324
+ components: {
325
+ type: purl.type,
326
+ namespace: purl.namespace,
327
+ name: purl.name,
328
+ version: purl.version,
329
+ qualifiers: purl.qualifiers || {},
330
+ subpath: purl.subpath
331
+ }
332
+ }
333
+ puts JSON.pretty_generate(result)
334
+ else
335
+ puts purl.to_s
336
+ end
337
+ rescue Purl::Error => e
338
+ if @json_output
339
+ result = {
340
+ success: false,
341
+ error: e.message,
342
+ options: options
343
+ }
344
+ puts JSON.pretty_generate(result)
345
+ else
346
+ puts "Error generating PURL: #{e.message}"
347
+ end
348
+ exit 1
349
+ end
350
+ end
351
+
352
+ def info_command(args)
353
+ if args.empty?
354
+ # Show all types
355
+ all_info = Purl.all_type_info
356
+ metadata = Purl.types_config_metadata
357
+
358
+ if @json_output
359
+ result = {
360
+ success: true,
361
+ types: all_info,
362
+ metadata: metadata
363
+ }
364
+ puts JSON.pretty_generate(result)
365
+ else
366
+ puts "Known PURL types:"
367
+ puts
368
+
369
+ all_info.each do |type, info|
370
+ puts " #{type}"
371
+ puts " Description: #{info[:description] || 'No description available'}"
372
+ puts " Registry support: #{info[:registry_url_generation] ? 'Yes' : 'No'}"
373
+ puts " Reverse parsing: #{info[:reverse_parsing] ? 'Yes' : 'No'}"
374
+ puts
375
+ end
376
+
377
+ puts "Total types: #{metadata[:total_types]}"
378
+ puts "Registry supported: #{metadata[:registry_supported_types]}"
379
+ end
380
+ else
381
+ # Show specific type info
382
+ type = args[0]
383
+ info = Purl.type_info(type)
384
+
385
+ if @json_output
386
+ result = {
387
+ success: true,
388
+ type: info
389
+ }
390
+ puts JSON.pretty_generate(result)
391
+ else
392
+ puts "Type: #{info[:type]}"
393
+ puts "Known: #{info[:known] ? 'Yes' : 'No'}"
394
+ puts "Description: #{info[:description] || 'No description available'}"
395
+
396
+ if info[:default_registry]
397
+ puts "Default registry: #{info[:default_registry]}"
398
+ end
399
+
400
+ puts "Registry URL generation: #{info[:registry_url_generation] ? 'Yes' : 'No'}"
401
+ puts "Reverse parsing: #{info[:reverse_parsing] ? 'Yes' : 'No'}"
402
+
403
+ if info[:examples] && !info[:examples].empty?
404
+ puts "Examples:"
405
+ info[:examples].each do |example|
406
+ puts " #{example}"
407
+ end
408
+ end
409
+
410
+ if info[:route_patterns] && !info[:route_patterns].empty?
411
+ puts "Registry URL patterns:"
412
+ info[:route_patterns].each do |pattern|
413
+ puts " #{pattern}"
414
+ end
415
+ end
416
+ end
417
+ end
418
+ end
419
+
420
+ def output_error(message)
421
+ if @json_output
422
+ result = {
423
+ success: false,
424
+ error: message
425
+ }
426
+ puts JSON.pretty_generate(result)
427
+ else
428
+ puts "Error: #{message}"
429
+ end
430
+ end
431
+ end
432
+
433
+ if __FILE__ == $0
434
+ PurlCLI.run
435
+ end
data/lib/purl/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Purl
4
- VERSION = "1.3.1"
4
+ VERSION = "1.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: purl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
@@ -28,7 +28,8 @@ description: |-
28
28
  It supports 37 package types (32 official + 5 additional ecosystems) and is fully compliant with the official PURL specification test suite.
29
29
  email:
30
30
  - andrewnez@gmail.com
31
- executables: []
31
+ executables:
32
+ - purl
32
33
  extensions: []
33
34
  extra_rdoc_files: []
34
35
  files:
@@ -40,6 +41,7 @@ files:
40
41
  - README.md
41
42
  - Rakefile
42
43
  - SECURITY.md
44
+ - exe/purl
43
45
  - lib/purl.rb
44
46
  - lib/purl/errors.rb
45
47
  - lib/purl/package_url.rb