phpdocr 0.1.1

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