1#
2# buildhelp.tcl --
3#
4# Program to extract help files from TCL manual pages or TCL script files.
5# The help directories are built as a hierarchical tree of subjects and help
6# files.
7#
8#------------------------------------------------------------------------------
9# Copyright 1992-1999 Karl Lehenbauer and Mark Diekhans.
10#
11# Permission to use, copy, modify, and distribute this software and its
12# documentation for any purpose and without fee is hereby granted, provided
13# that the above copyright notice appear in all copies.  Karl Lehenbauer and
14# Mark Diekhans make no representations about the suitability of this
15# software for any purpose.  It is provided "as is" without express or
16# implied warranty.
17#------------------------------------------------------------------------------
18# $Id: buildhelp.tcl,v 1.3 2005/03/25 19:32:48 hobbs Exp $
19#------------------------------------------------------------------------------
20#
21# For nroff man pages, the areas of text to extract are delimited with:
22#
23#     '\"@help: subjectdir/helpfile
24#     '\"@endhelp
25#
26# start in column one. The text between these markers is extracted and stored
27# in help/subjectdir/help.  The file must not exists, this is done to enforced
28# cleaning out the directories before help file generation is started, thus
29# removing any stale files.  The extracted text is run through:
30#
31#     nroff -man|col -xb   {col -b on BSD derived systems}
32#
33# If there is other text to include in the helpfile, but not in the manual
34# page, the text, along with nroff formatting commands, may be included using:
35#
36#     '\"@:Other text to include in the help page.
37#
38# A entry in the brief file, used by apropos my be included by:
39#
40#     '\"@brief: Short, one line description
41#
42# These brief request must occur with in the bounds of a help section.
43#
44# If some header text, such as nroff macros, need to be preappended to the
45# text streem before it is run through nroff, then that text can be bracketed
46# with:
47#
48#     '\"@header
49#     '\"@endheader
50#
51# If multiple header blocks are encountered, they will all be preappended.
52#
53# For TCL script files, which are indentified because they end in ".tcl",
54# the text to be extracted is delimited by:
55#
56#    #@help: subjectdir/helpfile
57#    #@endhelp
58#
59# And brief lines are in the form:
60#
61#     #@brief: Short, one line description
62#
63# The only processing done on text extracted from .tcl files it to replace
64# the # in column one with a space.
65#
66#
67#-----------------------------------------------------------------------------
68#
69# To generate help:
70#
71#   buildhelp helpDir brief.brf filelist
72#
73# o helpDir is the help tree root directory.  helpDir should  exists, but any
74#   subdirectories that don't exists will be created.  helpDir should be
75#   cleaned up before the start of manual page generation, as this program
76#   will not overwrite existing files.
77# o brief.brf  is the name of the brief file to create form the @brief entries.
78#   It must have an extension of ".brf".  It will be created in helpDir.
79# o filelist are the nroff manual pages, or .tcl, .tlib files to extract
80#   the help files from. If the suffix is not .tcl or .tlib, a nroff manual
81#   page is assumed.
82#
83#-----------------------------------------------------------------------------
84
85#@package: TclX-buildhelp buildhelp
86
87#-----------------------------------------------------------------------------
88# Truncate a file name of a help file if the system does not support long
89# file names.  If the name starts with `Tcl_', then this prefix is removed.
90# If the name is then over 14 characters, it is truncated to 14 charactes
91#
92proc TruncFileName {pathName} {
93    global truncFileNames
94
95    if {!$truncFileNames} {
96        return $pathName}
97    set fileName [file tail $pathName]
98    if {"[crange $fileName 0 3]" == "Tcl_"} {
99        set fileName [crange $fileName 4 end]}
100    set fileName [crange $fileName 0 13]
101    return "[file dirname $pathName]/$fileName"
102}
103
104#-----------------------------------------------------------------------------
105# Proc to ensure that all directories for the specified file path exists,
106# and if they don't create them.  Don't use -path so we can set the
107# permissions.
108
109proc EnsureDirs {filePath} {
110    set dirPath [file dirname $filePath]
111    if [file exists $dirPath] return
112    foreach dir [split $dirPath /] {
113        lappend dirList $dir
114        set partPath [join $dirList /]
115        if [file exists $partPath] continue
116
117        mkdir $partPath
118        chmod u=rwx,go=rx $partPath
119    }
120}
121
122#-----------------------------------------------------------------------------
123# Proc to set up scan context for use by FilterNroffManPage.
124# This keeps the a two line cache of the previous two lines encountered
125# and the blank lines that followed them.
126#
127
128proc CreateFilterNroffManPageContext {} {
129    global filterNroffManPageContext
130
131    set filterNroffManPageContext [scancontext create]
132
133    # On finding a page header, drop the previous line (which is
134    # the page footer). Also deleting the blank lines followin
135    # the last line on the previous page.
136
137    scanmatch $filterNroffManPageContext {@@@BUILDHELP@@@} {
138        catch {unset prev2Blanks}
139        catch {unset prev1Line}
140        catch {unset prev1Blanks}
141        set nukeBlanks {}
142    }
143
144    # Save blank lines
145
146    scanmatch $filterNroffManPageContext {$^} {
147        if ![info exists nukeBlanks] {
148            append prev1Blanks \n
149        }
150    }
151
152    # Non-blank line, save it.  Output the 2nd previous line if necessary.
153
154    scanmatch $filterNroffManPageContext {
155        catch {unset nukeBlanks}
156        if [info exists prev2Line] {
157            puts $outFH $prev2Line
158            unset prev2Line
159        }
160        if [info exists prev2Blanks] {
161            puts $outFH $prev2Blanks nonewline
162            unset prev2Blanks
163        }
164        if [info exists prev1Line] {
165            set prev2Line $prev1Line
166        }
167        set prev1Line $matchInfo(line)
168        if [info exists prev1Blanks] {
169            set prev2Blanks $prev1Blanks
170            unset prev1Blanks
171        }
172    }
173}
174
175#-----------------------------------------------------------------------------
176# Proc to filter a formatted manual page, removing the page headers and
177# footers.  This relies on each manual page having a .TH macro in the form:
178#   .TH @@@BUILDHELP@@@ n
179
180proc FilterNroffManPage {inFH outFH} {
181    global filterNroffManPageContext
182
183    if ![info exists filterNroffManPageContext] {
184        CreateFilterNroffManPageContext
185    }
186
187    scanfile $filterNroffManPageContext $inFH
188
189    if [info exists prev2Line] {
190        puts $outFH $prev2Line
191    }
192}
193
194#-----------------------------------------------------------------------------
195# Proc to set up scan context for use by ExtractNroffHeader
196#
197
198proc CreateExtractNroffHeaderContext {} {
199    global extractNroffHeaderContext
200
201    set extractNroffHeaderContext [scancontext create]
202
203    scanmatch $extractNroffHeaderContext {'\\"@endheader[ 	]*$} {
204        break
205    }
206    scanmatch $extractNroffHeaderContext {'\\"@:} {
207        append nroffHeader "[crange $matchInfo(line) 5 end]\n"
208    }
209    scanmatch $extractNroffHeaderContext {
210        append nroffHeader "$matchInfo(line)\n"
211    }
212}
213
214#-----------------------------------------------------------------------------
215# Proc to extract nroff text to use as a header to all pass to nroff when
216# processing a help file.
217#    manPageFH - The file handle of the manual page.
218#
219
220proc ExtractNroffHeader {manPageFH} {
221    global extractNroffHeaderContext nroffHeader
222
223    if ![info exists extractNroffHeaderContext] {
224        CreateExtractNroffHeaderContext
225    }
226    scanfile $extractNroffHeaderContext $manPageFH
227}
228
229
230#-----------------------------------------------------------------------------
231# Proc to set up scan context for use by ExtractNroffHelp
232#
233
234proc CreateExtractNroffHelpContext {} {
235    global extractNroffHelpContext
236
237    set extractNroffHelpContext [scancontext create]
238
239    scanmatch $extractNroffHelpContext {^'\\"@endhelp[ 	]*$} {
240        break
241    }
242
243    scanmatch $extractNroffHelpContext {^'\\"@brief:} {
244        if $foundBrief {
245            error {Duplicate "@brief:" entry}
246        }
247        set foundBrief 1
248        puts $briefHelpFH "$helpName\t[csubstr $matchInfo(line) 11 end]"
249        continue
250    }
251
252    scanmatch $extractNroffHelpContext {^'\\"@:} {
253        puts $nroffFH  [csubstr $matchInfo(line) 5 end]
254        continue
255    }
256    scanmatch $extractNroffHelpContext {^'\\"@help:} {
257        error {"@help" found within another help section"}
258    }
259    scanmatch $extractNroffHelpContext {
260        puts $nroffFH $matchInfo(line)
261    }
262}
263
264#-----------------------------------------------------------------------------
265# Proc to extract a nroff help file when it is located in the text.
266#    manPageFH - The file handle of the manual page.
267#    manLine - The '\"@help: line starting the data to extract.
268#
269
270proc ExtractNroffHelp {manPageFH manLine} {
271    global helpDir nroffHeader briefHelpFH colArgs
272    global extractNroffHelpContext
273
274    if ![info exists extractNroffHelpContext] {
275        CreateExtractNroffHelpContext
276    }
277
278    set helpName [string trim [csubstr $manLine 9 end]]
279    set helpFile [TruncFileName "$helpDir/$helpName"]
280    if [file exists $helpFile] {
281        error "Help file already exists: $helpFile"
282    }
283    EnsureDirs $helpFile
284
285    set tmpFile "[file dirname $helpFile]/tmp.[id process]"
286
287    echo "    creating help file $helpName"
288
289    set nroffFH [open "| nroff -man | col $colArgs > $tmpFile" w]
290
291    puts $nroffFH {.TH @@@BUILDHELP@@@ 1}
292
293    set foundBrief 0
294    scanfile $extractNroffHelpContext $manPageFH
295
296    # Close returns an error on if anything comes back on stderr, even if
297    # its a warning.  Output errors and continue.
298
299    set stat [catch {
300        close $nroffFH
301    } msg]
302    if $stat {
303        puts stderr "nroff: $msg"
304    }
305
306    set tmpFH [open $tmpFile r]
307    set helpFH [open $helpFile w]
308
309    FilterNroffManPage $tmpFH $helpFH
310
311    close $tmpFH
312    close $helpFH
313
314    unlink $tmpFile
315    chmod a-w,a+r $helpFile
316}
317
318#-----------------------------------------------------------------------------
319# Proc to set up scan context for use by ExtractScriptHelp
320#
321
322proc CreateExtractScriptHelpContext {} {
323    global extractScriptHelpContext
324
325    set extractScriptHelpContext [scancontext create]
326
327    scanmatch $extractScriptHelpContext {^#@endhelp[ 	]*$} {
328        break
329    }
330
331    scanmatch $extractScriptHelpContext {^#@brief:} {
332        if $foundBrief {
333            error {Duplicate "@brief" entry}
334        }
335        set foundBrief 1
336        puts $briefHelpFH "$helpName\t[csubstr $matchInfo(line) 9 end]"
337        continue
338    }
339
340    scanmatch $extractScriptHelpContext {^#@help:} {
341        error {"@help" found within another help section"}
342    }
343
344    scanmatch $extractScriptHelpContext {^#$} {
345        puts $helpFH ""
346    }
347
348    scanmatch $extractScriptHelpContext {
349        if {[clength $matchInfo(line)] > 1} {
350            puts $helpFH " [csubstr $matchInfo(line) 1 end]"
351        } else {
352            puts $helpFH $matchInfo(line)
353        }
354    }
355}
356
357#-----------------------------------------------------------------------------
358# Proc to extract a tcl script help file when it is located in the text.
359#    ScriptPageFH - The file handle of the .tcl file.
360#    ScriptLine - The #@help: line starting the data to extract.
361#
362
363proc ExtractScriptHelp {scriptPageFH scriptLine} {
364    global helpDir briefHelpFH
365    global extractScriptHelpContext
366
367    if ![info exists extractScriptHelpContext] {
368        CreateExtractScriptHelpContext
369    }
370
371    set helpName [string trim [csubstr $scriptLine 7 end]]
372    set helpFile "$helpDir/$helpName"
373    if {[file exists $helpFile]} {
374        error "Help file already exists: $helpFile"
375    }
376    EnsureDirs $helpFile
377
378    echo "    creating help file $helpName"
379
380    set helpFH [open $helpFile w]
381
382    set foundBrief 0
383    scanfile $extractScriptHelpContext $scriptPageFH
384
385    close $helpFH
386    chmod a-w,a+r $helpFile
387}
388
389#-----------------------------------------------------------------------------
390# Proc to scan a nroff manual file looking for the start of a help text
391# sections and extracting those sections.
392#    pathName - Full path name of file to extract documentation from.
393#
394
395proc ProcessNroffFile {pathName} {
396   global nroffScanCT scriptScanCT nroffHeader
397
398   set fileName [file tail $pathName]
399
400   set nroffHeader {}
401   set manPageFH [open $pathName r]
402   set matchInfo(fileName) [file tail $pathName]
403
404   echo "    scanning $pathName"
405
406   scanfile $nroffScanCT $manPageFH
407
408   close $manPageFH
409}
410
411#-----------------------------------------------------------------------------
412# Proc to scan a Tcl script file looking for the start of a
413# help text sections and extracting those sections.
414#    pathName - Full path name of file to extract documentation from.
415#
416
417proc ProcessTclScript {pathName} {
418   global scriptScanCT nroffHeader
419
420   set scriptFH [open "$pathName" r]
421   set matchInfo(fileName) [file tail $pathName]
422
423   echo "    scanning $pathName"
424   scanfile $scriptScanCT $scriptFH
425
426   close $scriptFH
427}
428
429#-----------------------------------------------------------------------------
430# build: main procedure.  Generates help from specified files.
431#    helpDirPath - Directory were the help files go.
432#    briefFile - The name of the brief file to create.
433#    sourceFiles - List of files to extract help files from.
434
435proc buildhelp {helpDirPath briefFile sourceFiles} {
436    global helpDir truncFileNames nroffScanCT
437    global scriptScanCT briefHelpFH colArgs
438
439    echo ""
440    echo "Begin building help tree"
441
442    # Determine version of col command to use (no -x on BSD)
443    if {[catch {exec col -bx </dev/null >/dev/null 2>/dev/null}]} {
444        set colArgs {-b}
445    } else {
446        set colArgs {-bx}
447    }
448    set helpDir $helpDirPath
449    if {![file exists $helpDir]} {
450        mkdir $helpDir
451    }
452
453    if {![file isdirectory $helpDir]} {
454        error "$helpDir is not a directory or does not exist.\n \
455                      This should be the help root directory"
456    }
457
458    set status [catch {set tmpFH [open $helpDir/AVeryVeryBigFileName w]}]
459    if {$status != 0} {
460        set truncFileNames 1
461    } else {
462        close $tmpFH
463        unlink $helpDir/AVeryVeryBigFileName
464        set truncFileNames 0
465    }
466
467    set nroffScanCT [scancontext create]
468
469    scanmatch $nroffScanCT {'\\"@help:} {
470        ExtractNroffHelp $matchInfo(handle) $matchInfo(line)
471        continue
472    }
473
474    scanmatch $nroffScanCT {^'\\"@header} {
475        ExtractNroffHeader $matchInfo(handle)
476        continue
477    }
478    scanmatch $nroffScanCT {^'\\"@endhelp} {
479        error [concat {@endhelp" without corresponding "@help:"} \
480                 ", offset = $matchInfo(offset)"]
481    }
482    scanmatch $nroffScanCT {^'\\"@brief} {
483        error [concat {"@brief" without corresponding "@help:"} \
484                 ", offset = $matchInfo(offset)"]
485    }
486
487    set scriptScanCT [scancontext create]
488    scanmatch $scriptScanCT {^#@help:} {
489        ExtractScriptHelp $matchInfo(handle) $matchInfo(line)
490    }
491
492    if {[file extension $briefFile] != ".brf"} {
493        error "Brief file \"$briefFile\" must have an extension \".brf\""
494    }
495    if [file exists $helpDir/$briefFile] {
496        error "Brief file \"$helpDir/$briefFile\" already exists"
497    }
498    set briefHelpFH [open "|sort > $helpDir/$briefFile" w]
499
500    foreach manFile [glob $sourceFiles] {
501        set ext [file extension $manFile]
502        if {$ext == ".tcl" || $ext == ".tlib"} {
503            set status [catch {ProcessTclScript $manFile} msg]
504        } else {
505            set status [catch {ProcessNroffFile $manFile} msg]
506        }
507        if {$status != 0} {
508            global errorInfo errorCode
509            error "Error extracting help from: $manFile" $errorInfo $errorCode
510        }
511    }
512
513    close $briefHelpFH
514    chmod a-w,a+r $helpDir/$briefFile
515    echo "Completed extraction of help files"
516}
517
518
519
520