phpdocr 0.1.1

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. data/COPYING +676 -0
  2. data/NEWS +8 -0
  3. data/README +11 -0
  4. data/phpdocr +546 -0
  5. data/phpdocr.1 +102 -0
  6. metadata +66 -0
data/NEWS ADDED
@@ -0,0 +1,8 @@
1
+ Verison 0.1.1
2
+ - Minor fix to the manpage
3
+ - Better parsing of php.net pages
4
+ - No longer exlicitly requires rubygems, better handling of missing
5
+ mechanize gem
6
+
7
+ Version 0.1
8
+ - Initial release
data/README ADDED
@@ -0,0 +1,11 @@
1
+ phpdocr is a simple way to access PHP documentation from php.net from the
2
+ command-line.
3
+
4
+ It will download, prettify, and output PHP documentation for the
5
+ function/class/.. that was supplied on the command-line, much like perldoc(1)
6
+ does for perl(1) and ri does for ruby(1). Unless you explicitly tell it not to,
7
+ phpdocr will also cache the documentation locally for fast retrieval in the
8
+ future.
9
+
10
+ phpdocr will send its output to your PAGER (if it is set, otherwise it will
11
+ default to less).
data/phpdocr ADDED
@@ -0,0 +1,546 @@
1
+ #!/usr/bin/ruby
2
+ # phpdocr
3
+ # Copyright (C) Eskild Hustvedt 2009
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ # Fetch from rubygems if available
19
+ begin
20
+ require 'rubygems'
21
+ rescue LoadError
22
+ puts '(rubygems appears to be missing, will attempt to continue anyway)'
23
+ end
24
+ # Command-line parsing
25
+ require 'getoptlong'
26
+ # To run the prettifier
27
+ require 'open3'
28
+ # To create a temporary file with HTML
29
+ require 'tempfile'
30
+ # CGI is used for escaping
31
+ require 'cgi'
32
+ # www-mechanize is used for fetching from HTTP
33
+ begin
34
+ require 'mechanize'
35
+ rescue LoadError
36
+ puts
37
+ puts 'You appear to be missing the "mechanize" rubygem.'
38
+ puts 'phpdocr needs this to be able to fetch data from php.net'
39
+ puts 'Please install the mechanize rubygem using your OS package manager'
40
+ puts 'or using the "gem" utility.'
41
+ puts
42
+ puts 'Package examples:'
43
+ puts 'Fedora: yum install rubygem-mechanize'
44
+ puts 'Debian/Ubuntu: aptitude install libwww-mechanize-ruby'
45
+ puts 'Generic/all: gem install mechanize'
46
+ exit(1)
47
+ end
48
+ # For creating the cache dir if needed
49
+ require 'fileutils'
50
+ # Application version
51
+ $version = '0.1.1'
52
+ # Bool, true if we should include the user notes section
53
+ $includeUserNotes = false
54
+ # Verbose mode (true/false)
55
+ $verbose = false
56
+ # Cache enabled?
57
+ $cache = true
58
+ # php.net mirror
59
+ $mirror = 'php.net'
60
+
61
+ # Purpose: Print formatted --help output
62
+ # Usage: printHelp('-shortoption', '--longoption', 'description');
63
+ # Description will be reformatted to fit within a normal terminal
64
+ def printHelp (short,long,description)
65
+ maxlen = 80
66
+ optionlen = 20
67
+ # Check if the short/long are LONGER than optionlen, if so, we need
68
+ # to do some additional magic to take up only $maxlen.
69
+ # The +1 here is because we always add a space between them, no matter what
70
+ if (short.length + long.length + 1) > optionlen
71
+ optionlen = short.length + long.length + 1;
72
+ end
73
+
74
+ generatedDesc = ''
75
+ currdesc = ''
76
+
77
+ description.split(/ /).each do |part|
78
+ if(generatedDesc.length > 0)
79
+ if (currdesc.length + part.length + 1 + 20) > maxlen
80
+ generatedDesc.concat("\n")
81
+ currdesc = ''
82
+ else
83
+ currdesc.concat(' ')
84
+ generatedDesc.concat(' ')
85
+ end
86
+ end
87
+ currdesc.concat(part)
88
+ generatedDesc.concat(part)
89
+ end
90
+ if !(generatedDesc.length > 0)
91
+ raise("Option mismatch")
92
+ end
93
+ generatedDesc.split(/\n/).each do |descr|
94
+ printf("%-4s %-15s %s\n",short,long,descr)
95
+ short = ''; long = ''
96
+ end
97
+ end
98
+
99
+ # Purpose: Print the help output
100
+ def Help ()
101
+ puts "phpdocr "+$version.to_s
102
+ puts ""
103
+ puts "Usage: phpdocr [OPTIONS] [FUNCTION/CLASS/SEARCH STRING/..]"
104
+ puts ""
105
+ printHelp('-h','--help','Display this help text')
106
+ printHelp('-u','--comments','Include the user comments part of the documentation in output')
107
+ printHelp('-n','--no-cache','Disable the local doc cache (~/.phpdocr/)')
108
+ printHelp('-v','--verbose','Enable verbose mode')
109
+ printHelp('-m','--mirror','Use the supplied mirror of php.net')
110
+ printHelp('','--version','Output the phpdocr version and exit')
111
+ printHelp('','--man','Show the phpdocr manpage')
112
+ end
113
+
114
+ # Purpose: Output a string if in verbose mode
115
+ def vputs (str)
116
+ if $verbose
117
+ puts(str)
118
+ end
119
+ end
120
+
121
+ # Purpose: Show the manpage
122
+ def showManPage ()
123
+ if ! inPath('man')
124
+ puts
125
+ puts "You don't appear to have the 'man' program installed."
126
+ puts "Please install it, then re-run phpdocr --man"
127
+ exit(0)
128
+ end
129
+ mySelf = File.expand_path($0)
130
+ while File.symlink?(mySelf)
131
+ mySelf = File.readlink(mySelf)
132
+ end
133
+ sourceDir = '.'
134
+ if mySelf != nil
135
+ sourceDir = File.dirname(mySelf)
136
+ end
137
+ dirs = [sourceDir]
138
+ if ENV['MANPATH']
139
+ dirs.concat(ENV['MANPATH'].split(':'))
140
+ end
141
+ dirs.push('./')
142
+ dirs.each do |dir|
143
+ [ 'phpdocr.1','man1/phpdocr.1','man1/phpdocr.1.gz','man1/phpdocr.1.bz2','man1/phpdocr.1.lzma'].each do |manFile|
144
+ if File.exists?(dir+'/'+manFile)
145
+ exec('man',dir+'/'+manFile)
146
+ end
147
+ end
148
+ end
149
+ puts
150
+ puts 'phpdocr failed to locate its manpage.'
151
+ puts 'Run the following command to view it:'
152
+ puts '\curl -s "http://github.com/zerodogg/phpdocr/raw/master/phpdocr.1" | groff -T utf8 -man | \less'
153
+ end
154
+
155
+ # Purpose: Print debugging info
156
+ def debugInfo ()
157
+ puts "phpdocr "+$version.to_s
158
+ begin
159
+ if inPath('md5sum')
160
+ outStr = nil
161
+ out = IO.popen('md5sum '+File.expand_path($0))
162
+ outStr = out.readline
163
+ outStr.sub!(/\s+.*$/,'')
164
+ puts('md5sum: '+outStr)
165
+ end
166
+ rescue
167
+ puts('(exception while generating md5sum: '+$!+')')
168
+ end
169
+ prettifierCmd = prettifier('path',false)
170
+ if prettifierCmd == nil
171
+ prettifierCmd = '(missing)'
172
+ else
173
+ prettifierCmd = String.new(prettifierCmd.join(' '))
174
+ end
175
+ puts 'Prettifier: '+prettifierCmd
176
+ exit(0)
177
+ end
178
+
179
+ # Fetch a URL and return its contents
180
+ def getURL (url)
181
+ begin
182
+ www = WWW::Mechanize.new
183
+ vputs('Downloading '+url)
184
+ return www.get(url).body
185
+ rescue
186
+ return ''
187
+ end
188
+ end
189
+
190
+ # Purpose: Check for a file in path
191
+ def inPath(exec)
192
+ ENV['PATH'].split(/:/).each do |part|
193
+ if File.executable?(part+'/'+exec) and not File.directory?(part+'/'+exec)
194
+ return true
195
+ end
196
+ end
197
+ return false
198
+ end
199
+
200
+ # Purpose: Detect and run a prettifier
201
+ def prettifier (path, missingIsFatal = true)
202
+ # Links family, they use basically the same syntax, but we append
203
+ # -no-references for elinks.
204
+ if inPath('elinks')
205
+ return [ 'elinks','-force-html','-no-references','-dump',path ]
206
+ end
207
+ ['links2', 'links' ].each do |links|
208
+ if inPath(links)
209
+ return [ links,'-force-html','-dump',path ]
210
+ end
211
+ end
212
+
213
+ # w3m
214
+ if inPath('w3m')
215
+ return ['w3m','-dump','-T','text/html',path]
216
+ end
217
+
218
+ # html2text
219
+ if inPath('html2text')
220
+ return ['html2text','-style','pretty',path ]
221
+ end
222
+
223
+ # Finally, try lynx
224
+ if inPath('lynx')
225
+ return ['lynx','-dump','-force_html',path ]
226
+ end
227
+
228
+ if missingIsFatal
229
+ # If we found none, then give up
230
+ puts
231
+ puts "Failed to locate any HTML parser. Please install one of the following,"
232
+ puts "and then re-run phpdocr: elinks, links2, links, w3m, html2text, lynx"
233
+ exit(1)
234
+ else
235
+ return nil
236
+ end
237
+ end
238
+
239
+ # Purpose: Convert links
240
+ def convertLinks (list)
241
+ result = ''
242
+
243
+ rmlastA = false
244
+ first = true
245
+
246
+ list.split(/</).each do |line|
247
+ if line =~ /^a[^>]+href="\w/
248
+ currlink = String.new(line)
249
+ # Remove the href
250
+ currlink.sub!(/^a[^>]+href="/,'')
251
+ # Remove whatever is after the href
252
+ currlink.sub!(/".*$/,'')
253
+ # Remove '#' links
254
+ currlink.sub!(/#.*$/,'')
255
+ # Parse away .php and function declarations
256
+ currlink.sub!(/\.php$/,'')
257
+ currlink.sub!(/^function\./,'')
258
+ # Remove other HTML
259
+ line.sub!(/^[^>]+>/,'')
260
+ # Add new content
261
+ line = String.new('['+currlink+'] '+line)
262
+ rmlastA = true
263
+ # Remove the a> if rmlastA is true
264
+ elsif line =~ /\/a>/ and rmlastA
265
+ line.sub!(/^\/a>/,'')
266
+ rmlastA = false
267
+ elsif first
268
+ first = false
269
+ else
270
+ line = '<'+line
271
+ end
272
+ result.concat(line)
273
+ end
274
+ return result
275
+ end
276
+
277
+ # Purpose: Attempt to fetch suggestions
278
+ def fetchSuggestions (data)
279
+ final = []
280
+ hadStart = false
281
+ no = 0
282
+ data.split(/\n/).each do |line|
283
+ if line =~ /result list start/
284
+ hadStart = true
285
+ elsif hadStart == false
286
+ next
287
+ elsif line =~ /result list end/
288
+ break
289
+ else
290
+ no += 1
291
+ # Kill all HTML
292
+ line.gsub!(/<[^>]+>/,'')
293
+ if line =~ /\S/
294
+ final.push(line)
295
+ end
296
+ end
297
+ end
298
+ if hadStart && final != nil
299
+ return final
300
+ else
301
+ return nil
302
+ end
303
+ end
304
+
305
+ # Purpose: Display the contents of the supplied string inside the users PAGER
306
+ def pager(contents)
307
+ # Detect the pager
308
+ pager = ENV['PAGER']
309
+ if pager == '' || pager == nil
310
+ pager = 'less'
311
+ end
312
+
313
+ # Write data to the pager
314
+ input = IO.popen(pager,'w')
315
+ begin
316
+ input.puts(contents)
317
+ input.close
318
+ rescue
319
+ end
320
+ end
321
+
322
+ # Purpose: Look up something on the website
323
+ def lookupWeb (name, fetchPattern = true, prevSearch = nil)
324
+ # fetchPattern means we should run a serach
325
+ stringC = String.new(name)
326
+ if fetchPattern == true
327
+ stringC = CGI.escape(name)
328
+ url = 'http://'+$mirror+'/manual-lookup.php?pattern='+name
329
+ else
330
+ # Otherwise, attempt a direct page
331
+ if ! stringC =~ /\.php$/
332
+ stringC.concat('.php')
333
+ end
334
+ url = 'http://'+$mirror+'/manual/en/'+stringC+'.php'
335
+ end
336
+ # Retrieve data
337
+ data = getURL(url)
338
+ # True if this is within the normally returned page
339
+ hadFirst = false
340
+ # True if we have had the UDM statement
341
+ hadUDM = false
342
+ # The result on normal pages
343
+ result = ''
344
+ # The result on index-like pages
345
+ indexResult = ''
346
+ # Parse it
347
+ data.split(/\n/).each do |line|
348
+ if hadFirst
349
+ if ! $includeUserNotes && line =~ /(User Contributed Notes|<div id="usernotes">)/
350
+ break
351
+ end
352
+ result.concat(line)
353
+ elsif line =~ /class="refentry"/ || line =~ /<h3\s*class="title">Description/ || line =~ /<div\s*class="refnamediv">/
354
+ hadFirst = true
355
+ end
356
+ if hadUDM
357
+ if line =~ /<h1 class="title">/
358
+ indexResult = String.new(line)
359
+ else
360
+ indexResult.concat(line)
361
+ end
362
+ elsif line =~ /UdmComment/
363
+ hadUDM = true
364
+ end
365
+ end
366
+
367
+ # If we got no useful data, try again if possible, else output failure
368
+ if ! hadFirst && ! hadUDM
369
+ # If this was a fetchPattern run, try a direct one
370
+ if fetchPattern
371
+ return lookupWeb(name,false,data)
372
+ else
373
+ # This was a direct one, output errors
374
+ puts
375
+ puts 'Could not find '+name+' in PHP documentation'
376
+ # If we have a previous search value, attempt to fetch suggestions from it
377
+ if prevSearch != nil
378
+ suggest = fetchSuggestions(prevSearch)
379
+ # If we have suggestions, output them.
380
+ if suggest != nil
381
+ puts "\n"
382
+ puts "Perhaps you were looking for one of these?"
383
+ while suggest.length > 0
384
+ printf("%-20s %-20s %-20s %-20s\n",suggest[0],suggest[1],suggest[2],suggest[3])
385
+ 4.times { suggest.shift }
386
+ end
387
+ end
388
+ end
389
+ return
390
+ end
391
+ end
392
+
393
+ # If we 'hadFirst', then result contains what we want, otherwise use
394
+ # indexResult
395
+ if hadFirst
396
+ result = convertLinks(result)
397
+ else
398
+ result = convertLinks(indexResult)
399
+ end
400
+
401
+ # Write the data to a temporary file so the prettifier can
402
+ # read it there (not all of them support reading from STDIN)
403
+ tmp = Tempfile.new('phpdocr')
404
+ tmp.puts('<html><body>'+result+'</body></html>')
405
+ tmp.flush
406
+ cmd = prettifier(tmp.path)
407
+ # Get the output
408
+ Open3.popen3(*cmd) { |stdin, stdout, stderr|
409
+ result = stdout.gets(nil)
410
+ }
411
+
412
+ # Close and remove the temporary file
413
+ tmp.close(true)
414
+
415
+ # Append the url we used to the result
416
+ result.concat("\nRetrieved from "+url)
417
+
418
+ return result
419
+ end
420
+
421
+ # Purpose: Prepare the cache dir
422
+ def prepCacheDir (name)
423
+ cacheDir = ENV['HOME']+'/.phpdocr/'
424
+ if ! File.directory?(cacheDir)
425
+ if File.exists?(cacheDir)
426
+ puts cacheDir+': exists but is not a directory, caching disabled.'
427
+ $cache = false
428
+ return nil
429
+ end
430
+ FileUtils.mkpath(cacheDir)
431
+ end
432
+ name.gsub!(/\//,'')
433
+ filePrefix = 'cache.'
434
+ if $includeUserNotes
435
+ filePrefix = 'cache.withNotes.'
436
+ end
437
+ return cacheDir+filePrefix+name
438
+ end
439
+
440
+ # Purpose: Look something up in the cache
441
+ def lookupCache (name)
442
+ cacheFile = prepCacheDir(name)
443
+ if cacheFile == nil
444
+ return
445
+ end
446
+
447
+ if File.exists?(cacheFile)
448
+ file = File.open(cacheFile)
449
+ vputs('Loaded information for '+name+' from the cache')
450
+ return file.gets(nil)
451
+ end
452
+
453
+ return nil
454
+ end
455
+
456
+ # Purpose: Write something to the cache
457
+ def writeToCache (name,data)
458
+ cacheFile = prepCacheDir(name)
459
+ if cacheFile == nil
460
+ return
461
+ end
462
+ file = File.open(cacheFile,'w')
463
+ file.puts(data)
464
+ file.close
465
+ end
466
+
467
+ # Purpose: Look something up
468
+ def lookup(name)
469
+ result = nil
470
+ sourceWasWeb = false
471
+ if $cache
472
+ result = lookupCache(name)
473
+ end
474
+ if !$cache || result == nil
475
+ sourceWasWeb = true
476
+ result = lookupWeb(name)
477
+ end
478
+ if result != nil
479
+ if $cache && sourceWasWeb
480
+ writeToCache(name,result)
481
+ end
482
+ pager(result)
483
+ end
484
+ end
485
+
486
+ opts = GetoptLong.new(
487
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
488
+ [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
489
+ [ '--comments','-u', GetoptLong::NO_ARGUMENT ],
490
+ [ '--no-cache','--nocache','-n', GetoptLong::NO_ARGUMENT ],
491
+ [ '--debuginfo', GetoptLong::NO_ARGUMENT ],
492
+ [ '--version', GetoptLong::NO_ARGUMENT ],
493
+ [ '--mirror', '-m', GetoptLong::REQUIRED_ARGUMENT ],
494
+ [ '--man', GetoptLong::NO_ARGUMENT ]
495
+ )
496
+
497
+ # Handle command-line arguments
498
+ begin
499
+ opts.each do |opt, arg|
500
+ case opt
501
+ when '--help'
502
+ Help()
503
+ exit(0)
504
+ when '--verbose'
505
+ $verbose = true
506
+ when '--comments'
507
+ $includeUserNotes = true
508
+ when '--no-cache'
509
+ $cache = false
510
+ when '--debuginfo'
511
+ debugInfo()
512
+ when '--version'
513
+ puts('phpdocr version '+$version.to_s)
514
+ exit(0)
515
+ when '--mirror'
516
+ $mirror = arg
517
+ when '--man'
518
+ showManPage()
519
+ exit(0)
520
+ end
521
+ end
522
+ rescue
523
+ puts('See --help for more inforation')
524
+ exit(1)
525
+ end
526
+
527
+ if ARGV.length == 0 || ARGV.length > 1
528
+ Help()
529
+ exit(1)
530
+ end
531
+
532
+ begin
533
+ lookup(ARGV.shift)
534
+ rescue => ex
535
+ puts('---')
536
+ puts('Exception: '+ex.to_s)
537
+ puts('Backtrace: '+"\n"+ex.backtrace.join("\n"))
538
+ puts('---')
539
+ puts()
540
+ puts('An exception has occurred and phpdocr can not continue.')
541
+ puts('This almost certainly reflects a bug in phpdocr.')
542
+ puts('Please check that you have the latest version off phpdocr,')
543
+ puts('and if you do, report this issue along with the text between the "---" above');
544
+ puts('to http://random.zerodogg.org/phpdocr/bugs')
545
+ exit(1)
546
+ end