瀏覽代碼

Move most sfdoc code to separate module

Allow for re-use and more independent testing.
Alois Mahdal 6 年之前
父節點
當前提交
64b4320ba3

+ 17
- 0
packaging/debian/control 查看文件

@@ -82,6 +82,23 @@ Description: Logging Shellfu/Bash module
82 82
  .
83 83
  This sub-package contains 'pretty', universal Shellfu logging module.
84 84
 
85
+Package: shellfu-bash-sfdoc
86
+Architecture: all
87
+Depends: shellfu-bash, shellfu-bash-pretty
88
+Description: Shellfu/Bash module to export docs from Shellfu modules
89
+ Shellfu is an attempt to add modularity to your shell scripts.
90
+ .
91
+ With Shellfu you can develop your shell modules separately from your
92
+ scripts, and test, use, explore or study them without need to be aware
93
+ of details such as where the actual files are placed.
94
+ .
95
+ Shellfu is mostly intended for cases when there's need for non-trivial
96
+ amount of reliable body of shell scripts, and access to advanced modular
97
+ languages such as Python or Perl is limited.
98
+ .
99
+ This sub-package contains Shellfu/Bash module to export documentation
100
+ from other Shellfu modules that follow Shellfu coding style.
101
+
85 102
 Package: shellfu-devel
86 103
 Architecture: all
87 104
 Depends: shellfu-bash-pretty

+ 1
- 0
packaging/debian/shellfu-bash-sfdoc.install 查看文件

@@ -0,0 +1 @@
1
+/usr/share/shellfu/include-bash/sfdoc.sh

+ 12
- 0
packaging/template.spec 查看文件

@@ -56,6 +56,15 @@ Obsoletes: shellfu-bash-core < 0.10
56 56
 %description bash-pretty
57 57
 This sub-package contains 'pretty', universal Shellfu logging module.
58 58
 
59
+%package bash-sfdoc
60
+Summary: Shellfu/Bash module to export docs from Shellfu modules
61
+Requires: perl
62
+Requires: shellfu-bash
63
+Requires: shellfu-bash-pretty
64
+%description bash-sfdoc
65
+This sub-package contains Shellfu/Bash module to export documentation
66
+from other Shellfu modules that follow Shellfu coding style.
67
+
59 68
 %package devel
60 69
 Summary: Essential developer tools
61 70
 Requires: shellfu-bash-pretty
@@ -140,6 +149,9 @@ make test \
140 149
 %{_datadir}/%{name}/include-bash/_pretty_plain.sh
141 150
 %{_datadir}/%{name}/include-bash/pretty.sh
142 151
 
152
+%files bash-sfdoc
153
+%{_datadir}/%{name}/include-bash/sfdoc.sh
154
+
143 155
 %files devel
144 156
 %{_bindir}/sfembed
145 157
 

+ 22
- 565
src/bin/sfdoc 查看文件

@@ -4,6 +4,7 @@
4 4
 . "$(sfpath)" || exit 3
5 5
 
6 6
 shellfu import pretty
7
+shellfu import sfdoc
7 8
 
8 9
 
9 10
 usage() {
@@ -39,487 +40,6 @@ usage() {
39 40
         "in man(1) pager."
40 41
 }
41 42
 
42
-export_as_manpage() {
43
-    #
44
-    # Export module doc as manpage
45
-    #
46
-    local module=$1
47
-    local mfile
48
-    mfile=$(select_mfile "$module") || return 3
49
-    mver="v$(shellfu _read_directive "module-version" "$mfile" 2>/dev/null)"
50
-    test "$mver" == "v" && mver="(no version)"
51
-    export_as_pod "$module" \
52
-      | pod2man \
53
-            --name "${RealModuleName:-${module^^}}" \
54
-            --center "User Contributed Shellfu Documentation" \
55
-            --section 3x \
56
-            --date "$(date -I -r "$mfile")" \
57
-            --release "$module $mver" \
58
-            --utf8 -q \`
59
-}
60
-
61
-export_as_markdown() {
62
-    #
63
-    # Export module doc as Markdown
64
-    #
65
-    local module=$1
66
-    local module_name=${RealModuleName:-$module}
67
-    local variable
68
-    local function
69
-    local vheader_done=false
70
-    local fheader_done=false
71
-    debug -v module
72
-    echo "$module_name"
73
-    echo "$module_name" | tr -c '\n' '[=*]'
74
-    echo
75
-    get_doc "$module" | md_escapes
76
-    echo
77
-    for variable in $(sf_list v "$module" | cut -d. -f2);
78
-    do
79
-        debug -v variable
80
-        $vheader_done || {
81
-            echo
82
-            echo Variables
83
-            echo ---------
84
-            vheader_done=true
85
-        }
86
-        echo
87
-        echo "### \`\$$variable\` ###"
88
-        echo ""
89
-        get_doc "$module" "v:$variable" | md_escapes
90
-        echo
91
-    done
92
-    for function in $(sf_list f "$module" | cut -d. -f2);
93
-    do
94
-        debug -v function
95
-        $fheader_done || {
96
-            echo
97
-            echo Functions
98
-            echo ---------
99
-            fheader_done=true
100
-        }
101
-        echo
102
-        echo "### \`$function()\` ###"
103
-        echo ""
104
-        get_doc "$module" "f:$function" | md_escapes
105
-        echo
106
-    done
107
-}
108
-
109
-export_as_pod() {
110
-    #
111
-    # Export module doc as POD
112
-    #
113
-    local module=$1
114
-    local variable
115
-    local function
116
-    local vheader_done=false
117
-    local fheader_done=false
118
-    local indented=false
119
-    debug -v module
120
-    echo "true <<'=cut'"
121
-    echo "=pod"
122
-    echo
123
-    echo "=head1 NAME"
124
-    echo
125
-    echo "${RealModuleName:-$module} - $(get_doc "$module" | head -1)"
126
-    echo
127
-    echo "=head1 DESCRIPTION"
128
-    echo
129
-    get_doc "$module"
130
-    echo
131
-    for variable in $(sf_list v "$module" | cut -d. -f2);
132
-    do
133
-        debug -v variable
134
-        $vheader_done || {
135
-            echo
136
-            echo "=head1 VARIABLES"
137
-            vheader_done=true
138
-            echo
139
-            echo "=over 8"
140
-            echo
141
-            indented=true
142
-        }
143
-        echo
144
-        echo "=item I<\$$variable>"
145
-        echo
146
-        get_doc "$module" "v:$variable"
147
-        echo
148
-    done
149
-    $indented && {
150
-        echo "=back"
151
-        echo
152
-    }
153
-    for function in $(sf_list f "$module" | cut -d. -f2);
154
-    do
155
-        debug -v function
156
-        $fheader_done || {
157
-            echo
158
-            echo "=head1 FUNCTIONS"
159
-            fheader_done=true
160
-            echo
161
-            echo "=over 8"
162
-            echo
163
-            indented=true
164
-        }
165
-        echo
166
-        echo "=item I<$function()>"
167
-        echo
168
-        get_doc "$module" "f:$function"
169
-        echo
170
-    done
171
-    $indented && {
172
-        echo "=back"
173
-        echo
174
-    }
175
-    echo "=encoding $Encoding"
176
-    echo "=cut"
177
-}
178
-
179
-filter_body() {
180
-    #
181
-    # Filter part $1 of body on stdin
182
-    #
183
-    local part=$1
184
-    case "$part" in
185
-        f:*)  filter_fun "${part:2}" ;;
186
-        v:*)  filter_var "${part:2}" ;;
187
-        *) warn "bug: invalid part specification $part" ;;
188
-    esac
189
-}
190
-
191
-filter_doc() {
192
-    #
193
-    # Filter docstring for part $1 from object body on stdin
194
-    #
195
-    local part=$1
196
-    case $part in
197
-        "")  filter_mdoc ;;
198
-        v:*)  filter_vdoc ;;
199
-        f:*)  filter_fdoc ;;
200
-        *) warn "bug: invalid part specification: $part" ;;
201
-    esac
202
-}
203
-
204
-filter_fun() {
205
-    #
206
-    # From module body on stdin, filter out function named $1
207
-    #
208
-    # Function definition should look like:
209
-    #
210
-    #     foo_fun() {
211
-    #         foo=$1
212
-    #     }
213
-    #
214
-    # That is,
215
-    #
216
-    #  *  no `function` keyword,
217
-    #  *  name starts the first line,
218
-    #  *  name is followed by '() {' with no additional spaces,
219
-    #  *  function end is denoted by line with a single '}'.
220
-    #
221
-    local fn_name=$1
222
-    name=$fn_name perl -we '
223
-        undef $/;
224
-        my $name = $ENV{name};
225
-        my ($fbody) = <> =~ m/^($name\(\) \{.*?^\}$)/ms;
226
-        print "$fbody\n" if defined $fbody;
227
-    '
228
-}
229
-
230
-filter_fdoc() {
231
-    #
232
-    # Filter docstring from function body on stdin
233
-    #
234
-    # Look for:
235
-    #
236
-    #  1. line "^    #$" to open docstring - this must be
237
-    #     the first one after function name (`fun() {`)
238
-    #  2. block of consecutive docstring lines (i.e. matching
239
-    #     "^    # " or "^    #$"
240
-    #  3. last "^    #$" line to close docstring.
241
-    #  4. Next line must not match any of the patterns above.
242
-    #
243
-    # For example, stdin like this:
244
-    #
245
-    #     myfun() {
246
-    #         #
247
-    #         # Do my thing with foo $1
248
-    #         #
249
-    #         # Detailed description, possibly spanning
250
-    #         # several paragraph.
251
-    #         #
252
-    #         # The format here should be Markdown.
253
-    #         #
254
-    #         local foo=$1
255
-    #         printf %p "${foo:4}"
256
-    #     }
257
-    #
258
-    # would be read as:
259
-    #
260
-    #     Do my thing with foo $1
261
-    #
262
-    #     Detailed description, possibly spanning
263
-    #     several paragraph.
264
-    #
265
-    #     The format here should be Markdown.
266
-    #
267
-    # However if we added following line right before the `local`
268
-    # declaration
269
-    #
270
-    #         #FIXME: TODO/FIXME lines are not dropped properly
271
-    #
272
-    # it **will not** be considered part of the docstring. This is
273
-    # to allow for special comments tools like TODO/FIXME or
274
-    # data for lint-like tools that are not part of function
275
-    # description.
276
-    #
277
-    perl -we '
278
-        my $isdoc;
279
-        while (<>) {
280
-            if (m/^    #$/) {
281
-                $isdoc = 1;
282
-            } elsif (m/^    [^#]/) {
283
-                exit;
284
-            }
285
-            next unless $isdoc;
286
-            s/^    //;
287
-            print;
288
-        }' | strip_doc
289
-}
290
-
291
-filter_mdoc() {
292
-    #
293
-    # From module body on stdin, filter module doc
294
-    #
295
-    # Example:
296
-    #
297
-    #     #!/bin/bash
298
-    #     #
299
-    #     # legal stuff
300
-    #
301
-    #     shellfu import irrelevant_stuff
302
-    #
303
-    #     #
304
-    #     # Module is cool this
305
-    #     #
306
-    #     # Ok so this is my
307
-    #     # "module", heh...
308
-    #     #
309
-    #
310
-    perl -we '
311
-        sub ok_chunk {
312
-            my $chunk = shift;
313
-            my @lines = split "\n", $_;
314
-            chomp @lines;
315
-            return 0 unless (defined $lines[0] and $lines[0] eq "#");
316
-            return 0 unless $lines[$#lines] eq "#";
317
-            foreach (@lines) {
318
-                return 0 unless (m/^#$/ or m/^# /);
319
-            }
320
-            return 1
321
-        }
322
-        my @chunks;
323
-        my @lines;
324
-        $/ = "\n\n";
325
-        @chunks = <STDIN>;
326
-        foreach (@chunks) {
327
-            if (ok_chunk "$_") {
328
-                s/\n+$//;
329
-                print "$_\n";
330
-                exit 0;
331
-            }
332
-        }
333
-        exit 1
334
-    ' | strip_doc
335
-}
336
-
337
-filter_var() {
338
-    #
339
-    # From module body on stdin, filter variable definition named $1
340
-    #
341
-    # Look for:
342
-    #
343
-    #  1. Empty line, followed by line with single hash sign
344
-    #  2. Set of consecutive docstring lines (start with '# ')
345
-    #  3. Another "#" followed by one or more variable definitions (start
346
-    #     with $name or \w+, followed by equal sign), at least one of which
347
-    #     must be $1
348
-    #  4. Empty line or EOF
349
-    #
350
-    # and return what is found.
351
-    #
352
-    # For example:
353
-    #
354
-    #     #
355
-    #     # My variable
356
-    #     #
357
-    #     my_var=foo
358
-    #
359
-    # is found if $1 is 'my_var'
360
-    #
361
-    # A more advanced example:
362
-    #
363
-    #     #
364
-    #     # Bar variables
365
-    #     #
366
-    #     # These serve similar purpose; the difference is
367
-    #     # obvious
368
-    #     #
369
-    #     bar_1=one
370
-    #     bar_2=two
371
-    #     bar_3=three
372
-    #
373
-    # is found if $1 is either 'bar_1', 'bar_2' or 'bar_3'.  This allows
374
-    # for a docstring being shared among closely related variables.
375
-    #
376
-    local oname=$1    # object name
377
-    local state       # void, edge, dstr, vstr, junk
378
-    local o_end       # object ended; is complete
379
-    local cache       # buffer of hope
380
-    # parse and keep each var docstring in cache;
381
-    # then decide whether or not print the cache
382
-    cache=$(mktemp -t shellfu_doc.filter_var.XXXXXXXX)
383
-    state=junk
384
-    o_end=false
385
-    while read -r line;
386
-    do
387
-        case $state:$line in
388
-            # looks like void
389
-            void:)          state=void; o_end=false ;;
390
-            edge:)          state=void; o_end=true  ;;
391
-            dstr:)          state=void; o_end=true  ;;
392
-            vstr:)          state=void; o_end=true  ;;
393
-            junk:)          state=void; o_end=false ;;
394
-            # looks like edge
395
-            void:"#")       state=edge; o_end=false ;;
396
-            edge:"#")       state=dstr; o_end=false ;;
397
-            dstr:"#")       state=dstr; o_end=false ;;
398
-            vstr:"#")       state=dstr; o_end=false ;;
399
-            junk:"#")       state=junk; o_end=false ;;
400
-            # looks like docstring
401
-            void:"# "*)     state=junk; o_end=false ;;
402
-            edge:"# "*)     state=dstr; o_end=false ;;
403
-            dstr:"# "*)     state=dstr; o_end=false ;;
404
-            vstr:"# "*)     state=dstr; o_end=false ;;
405
-            junk:"# "*)     state=junk; o_end=false ;;
406
-            # looks like junk
407
-            void:"#"*)      state=junk; o_end=false ;;
408
-            edge:"#"*)      state=junk; o_end=false ;;
409
-            dstr:"#"*)      state=junk; o_end=false ;;
410
-            vstr:"#"*)      state=junk; o_end=false ;;
411
-            junk:"#"*)      state=junk; o_end=false ;;
412
-            # looks like variable string
413
-            void:*=*)       state=vstr; o_end=false ;;
414
-            edge:*=*)       state=junk; o_end=false ;;
415
-            dstr:*=*)       state=vstr; o_end=false ;;
416
-            vstr:*=*)       state=vstr; o_end=false ;;
417
-            junk:*=*)       state=junk; o_end=false ;;
418
-            # looks like something else
419
-            *)              state=junk; o_end=false ;;
420
-        esac
421
-        case $state in
422
-            edge|dstr|vstr) echo "$line" >> "$cache" ;;
423
-        esac
424
-        if $o_end;
425
-        then
426
-            if grep -q "^$oname=" "$cache";   # it's our wanted object
427
-            then
428
-                cat "$cache"
429
-                rm "$cache"
430
-                return 0
431
-            fi
432
-            rm "$cache"
433
-        fi
434
-    done
435
-    rm "$cache"
436
-    return 1
437
-}
438
-
439
-filter_vdoc() {
440
-    #
441
-    # From variable definition body on stdin, filter doc
442
-    #
443
-    # Example:
444
-    #
445
-    #     #
446
-    #     # Bar variables
447
-    #     #
448
-    #     # These serve similar purpose; the difference is
449
-    #     # obvious
450
-    #     #
451
-    #     bar_1=one
452
-    #     bar_2=two
453
-    #     bar_3=three
454
-    #
455
-    grep '^#' | strip_doc
456
-}
457
-
458
-get_doc() {
459
-    #
460
-    # Show doc for part $2 of module $1
461
-    #
462
-    local module=$1
463
-    local part=$2
464
-    get_part "$module" "$part" | filter_doc "$part"
465
-}
466
-
467
-get_part() {
468
-    #
469
-    # Print part $2 (f|v:name) of module $1
470
-    #
471
-    # Part has format
472
-    #
473
-    #     TYPE:NAME
474
-    #
475
-    # where TYPE is 'v' for variables and f for functions, and
476
-    # NAME is name of the part.
477
-    #
478
-    # If part is not specified, whole module file is printed.
479
-    #
480
-    local module=$1
481
-    local part=$2
482
-    local mfile
483
-    mfile=$(select_mfile "$module") || return 3
484
-    test -n "$mfile" || return 1
485
-    test -n "$part" || { cat "$mfile"; return $?; }
486
-    <"$mfile" filter_body "$part"
487
-}
488
-
489
-md_escapes() {
490
-    #
491
-    # Do additional escapes for Markdown
492
-    #
493
-    # In docstrings, references to variables are often done
494
-    # without backticks;  add these to prevent interpreting
495
-    # underscores in variable names as cursive.
496
-    #
497
-    perl -ne '
498
-        my @bigwords = ();
499
-        my @newwords = ();
500
-        if (m|^    |) {
501
-            print;                              # probably code -- leave be
502
-        } else {
503
-            @bigwords = split " ";
504
-            foreach (@bigwords) {
505
-                if (m|`\w+\(\)`|) {
506
-                    push @newwords, $_;              # function: escaped
507
-                } elsif (m|(.*)\b(\w+)\(\)(.*)|) {
508
-                    push @newwords, "$1`$2()`$3";    # function: needs escape
509
-                } elsif (m|`\$\w+`|) {
510
-                    push @newwords, $_;              # variable: escaped
511
-                } elsif (m|(.*)\$(\w+)(.*)|) {
512
-                    push @newwords, "$1`\$$2`$3";    # variable: needs escape
513
-                } else {
514
-                    push @newwords, $_;              # a normal word
515
-                }
516
-            }
517
-            print join " ", @newwords;
518
-            print "\n";
519
-        }
520
-    '
521
-}
522
-
523 43
 select_mfile() {
524 44
     #
525 45
     # Find and/or verify file that holds module $1
@@ -530,143 +50,80 @@ select_mfile() {
530 50
         */*)    mfile="$module" ;;
531 51
         *)      mfile=$(shellfu _select_mfile "$module") ;;
532 52
     esac
53
+    debug -v mfile
533 54
     test -n "$mfile" || { warn "no such module found: $module"; return 3; }
534 55
     test -f "$mfile" || { warn "no such file found: $mfile"; return 3; }
535 56
     echo "$mfile"
536 57
 }
537 58
 
538
-sf_list() {
539
-    #
540
-    # List all objects of type $1 in module $2
541
-    #
542
-    local otype=$1
543
-    local module=$2
544
-    local mfile
545
-    mfile=$(select_mfile "$module") || return 3
546
-    case $otype in
547
-            f)
548
-                grep -HE '^[[:alnum:]_]+\(\) \{' "$mfile" \
549
-                  | sed -e 's|(.*||; s|\.sh:|.|; s|^.*/||'
550
-                ;;
551
-            v)
552
-                grep -HE '^[[:alnum:]_]+=' "$mfile" \
553
-                  | sed -e 's|=.*||; s|\.sh:|.|; s|^.*/||'
554
-                ;;
555
-            *)
556
-                warn "bug: invalid object type: $otype"
557
-                ;;
558
-    esac \
559
-      | sf_flt_hidden
560
-}
561
-
562
-sf_list_modules() {
563
-    #
564
-    # List all module files
565
-    #
566
-    shellfu _list_mfiles \
567
-      | sed 's|\.sh$||; s|.*/||;' \
568
-      | sf_flt_hidden
569
-}
570
-
571
-sf_flt_hidden() {
572
-    $ShowHidden && cat && return
573
-    grep -v ^_ | grep -v \\._
574
-}
575
-
576
-strip_doc() {
577
-    #
578
-    # Strip doc out of Shellfu docstring
579
-    #
580
-    # From e.g.
581
-    #
582
-    #     #
583
-    #     # foo
584
-    #     #
585
-    #     # bar
586
-    #     # baz
587
-    #     #
588
-    #
589
-    # where first and last line are mandatory and each line
590
-    # must be either '#' or '# *', get:
591
-    #
592
-    #     foo
593
-    #
594
-    #     bar
595
-    #     baz
596
-    #
597
-    # just as when you're parsing this text by eyes.
598
-    #
599
-    head -n -1 \
600
-      | tail -n +2 \
601
-      | sed -e 's/^#$//; s/^# //;'
602
-}
603
-
604 59
 main() {
605
-    local ShowHidden        # show hidden objects?
606 60
     local action            # what to do
607 61
     local format            # export format
608 62
     local module            # module name or path/to/module.sh
609 63
     local m                 # module helper var
610 64
     local RealModuleName    # name to override eg. if accessing via filename
611
-    local Encoding          # encoding
612
-    ShowHidden=false
65
+    local encoding          # encoding
66
+    local mpath             # path to module file
613 67
     action=man
614 68
     format=markdown
615
-    Encoding=utf8
69
+    encoding=utf8
616 70
     #shellcheck disable=SC2034
617 71
     while true; do case "$1" in
618 72
         -d|--debug)     PRETTY_DEBUG=true;          shift ;;
619
-        -a|--all)       ShowHidden=true;            shift ;;
73
+        -a|--all)       SFDOC_SHOW_HIDDEN=true;     shift ;;
620 74
         -I|--include)   SHELLFU_PATH="$2:$SHELLFU_PATH"; shift 2 || usage ;;
621 75
         -l|--ls)        action=lsx;                 shift; break ;;
622 76
         -L|--lsmod)     action=lsm;                 shift; break ;;
623 77
         --lsvar)        action=lsv;                 shift; break ;;
624 78
         --lsfun)        action=lsf;                 shift; break ;;
625 79
         -e|--export)    action=exp; format="$2";    shift 2 || usage; break ;;
626
-        --encoding)     Encoding="$2";              shift 2 || usage ;;
80
+        --encoding)     encoding="$2";              shift 2 || usage ;;
627 81
         --name)         RealModuleName="$2";        shift 2 || usage ;;
628 82
         -*)                                         usage ;;
629 83
         *)                                          break ;;
630 84
     esac done
631 85
     module="$1"; shift
632
-    debug -v SHELLFU_INCLUDE SHELLFU_PATH
633
-    debug -v action ShowHidden format module RealModuleName
86
+    debug -v SHELLFU_INCLUDE SHELLFU_PATH SFDOC_SHOW_HIDDEN
87
+    debug -v action format module RealModuleName
634 88
     case $action:$module in
635 89
         lsx:*|lsm:*)    true ;;
636 90
         *:)             usage ;;
637 91
     esac
92
+    case $action in
93
+        lsm)    : ;;
94
+        *)      mpath=$(select_mfile "$module") || die ;;
95
+    esac
638 96
     case $action in
639 97
         exp)    # --export
640 98
             grep -qw "$format" <<<manpage,markdown,pod \
641 99
              || die "unknown format: $format"
642
-            "export_as_$format" "$module"
100
+            sfdoc__export "$format" "${RealModuleName:-$module}" "$mpath"
643 101
             ;;
644 102
         lsm)    # --lsmod
645
-            sf_list_modules | sort
103
+            sfdoc__ls_m | sort
646 104
             ;;
647 105
         lsv)    # --lsvar MODULE
648
-            sf_list v "$module" | sort
106
+            sfdoc__ls v "$mpath" | sort
649 107
             ;;
650 108
         lsf)    # --lsfun MODULE
651
-            sf_list f "$module" | sort
109
+            sfdoc__ls f "$mpath" | sort
652 110
             ;;
653 111
         lsx)    # --ls [MODULE]
654 112
             case $module in
655
-                "") sf_list_modules ;;
656
-                *)  echo "$module" ;;
113
+                "") shellfu _list_mfiles ;;
114
+                *)  echo "$mpath" ;;
657 115
             esac \
658 116
               | while read -r m;
659 117
                 do
660
-                    sf_list v "$m"
661
-                    sf_list f "$m"
118
+                    sfdoc__ls v "$m"
119
+                    sfdoc__ls f "$m"
662 120
                 done \
663 121
               | sort
664 122
             ;;
665 123
         man)
666 124
             which pod2man &>/dev/null \
667 125
              || die "pod2man is missing cannot use man page mode"
668
-            select_mfile "$module" >/dev/null || return 3
669
-            export_as_manpage "$module" \
126
+            sfdoc__export manpage "${RealModuleName:-$module}" "$mpath" \
670 127
               | man -l -
671 128
             ;;
672 129
     esac

+ 567
- 0
src/include-bash/sfdoc.sh 查看文件

@@ -0,0 +1,567 @@
1
+#!/bin/bash
2
+
3
+shellfu import pretty
4
+
5
+#
6
+# Show hidden objects (modules, functions, variables)?
7
+#
8
+SFDOC_SHOW_HIDDEN=${SFDOC_SHOW_HIDDEN:-false}
9
+
10
+sfdoc__export() {
11
+    #
12
+    # Export module doc as manpage
13
+    #
14
+    local format=$1             # format to export in
15
+    local MName=$2              # name to use in headers
16
+    local MPath=$3              # path to file to export
17
+    local Encoding=${4:-utf8}   # file encoding
18
+    local MVer                  # module version (if found)
19
+    local exfun                 # export function
20
+    local usage="usage: sfdoc__export FMT NAME PATH [ENCODING]"
21
+    test -n "$format"   || { warn "$usage"; return 2; }
22
+    test -n "$MPath"    || { warn "$usage"; return 2; }
23
+    test -n "$MName"    || { warn "$usage"; return 2; }
24
+    exfun="__sfdoc__export_as_$format"
25
+    type -t "$exfun" >/dev/null || {
26
+        warn "unknown format: $format"
27
+        return 2
28
+    }
29
+    MVer="v$(shellfu _read_directive "module-version" "$MPath" 2>/dev/null)"
30
+    test "$MVer" == "v" && MVer="(no version)"
31
+    debug -v format MName MPath Encoding MVer
32
+    "$exfun"
33
+}
34
+
35
+sfdoc__ls() {
36
+    #
37
+    # List all objects of type $1 in module file $2
38
+    #
39
+    local otype=$1
40
+    local mpath=$2
41
+    case $otype in
42
+            f)
43
+                grep -HE '^[[:alnum:]_]+\(\) \{' "$mpath" \
44
+                  | sed -e 's|(.*||; s|\.sh:|.|; s|^.*/||'
45
+                ;;
46
+            v)
47
+                grep -HE '^[[:alnum:]_]+=' "$mpath" \
48
+                  | sed -e 's|=.*||; s|\.sh:|.|; s|^.*/||'
49
+                ;;
50
+            *)
51
+                warn "bug: invalid object type: $otype"
52
+                ;;
53
+    esac \
54
+      | __sfdoc__filter_hidden
55
+}
56
+
57
+sfdoc__ls_m() {
58
+    #
59
+    # List all module files
60
+    #
61
+    shellfu _list_mfiles \
62
+      | sed 's|\.sh$||; s|.*/||;' \
63
+      | __sfdoc__filter_hidden
64
+}
65
+
66
+__sfdoc__export_as_manpage() {
67
+    #
68
+    # Export module $Module doc as manpage
69
+    #
70
+    __sfdoc__export_as_pod \
71
+      | pod2man \
72
+            --name "${MName^^}" \
73
+            --center "User Contributed Shellfu Documentation" \
74
+            --section 3x \
75
+            --date "$(date -I -r "$MPath")" \
76
+            --release "$MName $MVer" \
77
+            --utf8 -q \`
78
+}
79
+
80
+__sfdoc__export_as_markdown() {
81
+    #
82
+    # Export module $Module doc as Markdown
83
+    #
84
+    local variable
85
+    local function
86
+    local vheader_done=false
87
+    local fheader_done=false
88
+    debug -v module
89
+    echo "$MName"
90
+    echo "$MName" | tr -c '\n' '[=*]'
91
+    echo
92
+    __sfdoc__get_doc | __sfdoc__md_escapes
93
+    echo
94
+    for variable in $(sfdoc__ls v "$MPath" | cut -d. -f2);
95
+    do
96
+        debug -v variable
97
+        $vheader_done || {
98
+            echo
99
+            echo Variables
100
+            echo ---------
101
+            vheader_done=true
102
+        }
103
+        echo
104
+        echo "### \`\$$variable\` ###"
105
+        echo ""
106
+        __sfdoc__get_doc "v:$variable" | __sfdoc__md_escapes
107
+        echo
108
+    done
109
+    for function in $(sfdoc__ls f "$MPath" | cut -d. -f2);
110
+    do
111
+        debug -v function
112
+        $fheader_done || {
113
+            echo
114
+            echo Functions
115
+            echo ---------
116
+            fheader_done=true
117
+        }
118
+        echo
119
+        echo "### \`$function()\` ###"
120
+        echo ""
121
+        __sfdoc__get_doc "f:$function" | __sfdoc__md_escapes
122
+        echo
123
+    done
124
+}
125
+
126
+__sfdoc__export_as_pod() {
127
+    #
128
+    # Export module $Module doc as POD
129
+    #
130
+    local variable
131
+    local function
132
+    local vheader_done=false
133
+    local fheader_done=false
134
+    local indented=false
135
+    echo "true <<'=cut'"
136
+    echo "=pod"
137
+    echo
138
+    echo "=head1 NAME"
139
+    echo
140
+    echo "$MName - $(__sfdoc__get_doc | head -1)"
141
+    echo
142
+    echo "=head1 DESCRIPTION"
143
+    echo
144
+    __sfdoc__get_doc
145
+    echo
146
+    for variable in $(sfdoc__ls v "$MPath" | cut -d. -f2);
147
+    do
148
+        debug -v variable
149
+        $vheader_done || {
150
+            echo
151
+            echo "=head1 VARIABLES"
152
+            vheader_done=true
153
+            echo
154
+            echo "=over 8"
155
+            echo
156
+            indented=true
157
+        }
158
+        echo
159
+        echo "=item I<\$$variable>"
160
+        echo
161
+        __sfdoc__get_doc "v:$variable"
162
+        echo
163
+    done
164
+    $indented && {
165
+        echo "=back"
166
+        echo
167
+    }
168
+    for function in $(sfdoc__ls f "$MPath" | cut -d. -f2);
169
+    do
170
+        debug -v function
171
+        $fheader_done || {
172
+            echo
173
+            echo "=head1 FUNCTIONS"
174
+            fheader_done=true
175
+            echo
176
+            echo "=over 8"
177
+            echo
178
+            indented=true
179
+        }
180
+        echo
181
+        echo "=item I<$function()>"
182
+        echo
183
+        __sfdoc__get_doc "f:$function"
184
+        echo
185
+    done
186
+    $indented && {
187
+        echo "=back"
188
+        echo
189
+    }
190
+    echo "=encoding $Encoding"
191
+    echo "=cut"
192
+}
193
+
194
+__sfdoc__filter_body() {
195
+    #
196
+    # Filter part $1 of body on stdin
197
+    #
198
+    local part=$1
199
+    case "$part" in
200
+        f:*)  __sfdoc__filter_fun "${part:2}" ;;
201
+        v:*)  __sfdoc__filter_var "${part:2}" ;;
202
+        *) warn "bug: invalid part specification $part" ;;
203
+    esac
204
+}
205
+
206
+__sfdoc__filter_doc() {
207
+    #
208
+    # Filter docstring for part $1 from object body on stdin
209
+    #
210
+    local part=$1
211
+    case $part in
212
+        "")  __sfdoc__filter_mdoc ;;
213
+        v:*)  __sfdoc__filter_vdoc ;;
214
+        f:*)  __sfdoc__filter_fdoc ;;
215
+        *) warn "bug: invalid part specification: $part" ;;
216
+    esac
217
+}
218
+
219
+__sfdoc__filter_fun() {
220
+    #
221
+    # From module body on stdin, filter out function named $1
222
+    #
223
+    # Function definition should look like:
224
+    #
225
+    #     foo_fun() {
226
+    #         foo=$1
227
+    #     }
228
+    #
229
+    # That is,
230
+    #
231
+    #  *  no `function` keyword,
232
+    #  *  name starts the first line,
233
+    #  *  name is followed by '() {' with no additional spaces,
234
+    #  *  function end is denoted by line with a single '}'.
235
+    #
236
+    local fn_name=$1
237
+    name=$fn_name perl -we '
238
+        undef $/;
239
+        my $name = $ENV{name};
240
+        my ($fbody) = <> =~ m/^($name\(\) \{.*?^\}$)/ms;
241
+        print "$fbody\n" if defined $fbody;
242
+    '
243
+}
244
+
245
+__sfdoc__filter_fdoc() {
246
+    #
247
+    # Filter docstring from function body on stdin
248
+    #
249
+    # Look for:
250
+    #
251
+    #  1. line "^    #$" to open docstring - this must be
252
+    #     the first one after function name (`fun() {`)
253
+    #  2. block of consecutive docstring lines (i.e. matching
254
+    #     "^    # " or "^    #$"
255
+    #  3. last "^    #$" line to close docstring.
256
+    #  4. Next line must not match any of the patterns above.
257
+    #
258
+    # For example, stdin like this:
259
+    #
260
+    #     myfun() {
261
+    #         #
262
+    #         # Do my thing with foo $1
263
+    #         #
264
+    #         # Detailed description, possibly spanning
265
+    #         # several paragraph.
266
+    #         #
267
+    #         # The format here should be Markdown.
268
+    #         #
269
+    #         local foo=$1
270
+    #         printf %p "${foo:4}"
271
+    #     }
272
+    #
273
+    # would be read as:
274
+    #
275
+    #     Do my thing with foo $1
276
+    #
277
+    #     Detailed description, possibly spanning
278
+    #     several paragraph.
279
+    #
280
+    #     The format here should be Markdown.
281
+    #
282
+    # However if we added following line right before the `local`
283
+    # declaration
284
+    #
285
+    #         #FIXME: TODO/FIXME lines are not dropped properly
286
+    #
287
+    # it **will not** be considered part of the docstring. This is
288
+    # to allow for special comments tools like TODO/FIXME or
289
+    # data for lint-like tools that are not part of function
290
+    # description.
291
+    #
292
+    perl -we '
293
+        my $isdoc;
294
+        while (<>) {
295
+            if (m/^    #$/) {
296
+                $isdoc = 1;
297
+            } elsif (m/^    [^#]/) {
298
+                exit;
299
+            }
300
+            next unless $isdoc;
301
+            s/^    //;
302
+            print;
303
+        }' | __sfdoc__strip_doc
304
+}
305
+
306
+__sfdoc__filter_hidden() {
307
+    #
308
+    # From objects on stdin, print only visible unless $SFDOC_SHOW_HIDDEN
309
+    #
310
+    $SFDOC_SHOW_HIDDEN && cat && return
311
+    grep -v ^_ | grep -v \\._
312
+}
313
+
314
+__sfdoc__filter_mdoc() {
315
+    #
316
+    # From module body on stdin, filter module doc
317
+    #
318
+    # Example:
319
+    #
320
+    #     #!/bin/bash
321
+    #     #
322
+    #     # legal stuff
323
+    #
324
+    #     shellfu import irrelevant_stuff
325
+    #
326
+    #     #
327
+    #     # Module is cool this
328
+    #     #
329
+    #     # Ok so this is my
330
+    #     # "module", heh...
331
+    #     #
332
+    #
333
+    perl -we '
334
+        sub ok_chunk {
335
+            my $chunk = shift;
336
+            my @lines = split "\n", $_;
337
+            chomp @lines;
338
+            return 0 unless (defined $lines[0] and $lines[0] eq "#");
339
+            return 0 unless $lines[$#lines] eq "#";
340
+            foreach (@lines) {
341
+                return 0 unless (m/^#$/ or m/^# /);
342
+            }
343
+            return 1
344
+        }
345
+        my @chunks;
346
+        my @lines;
347
+        $/ = "\n\n";
348
+        @chunks = <STDIN>;
349
+        foreach (@chunks) {
350
+            if (ok_chunk "$_") {
351
+                s/\n+$//;
352
+                print "$_\n";
353
+                exit 0;
354
+            }
355
+        }
356
+        exit 1
357
+    ' | __sfdoc__strip_doc
358
+}
359
+
360
+__sfdoc__filter_var() {
361
+    #
362
+    # From module body on stdin, filter variable definition named $1
363
+    #
364
+    # Look for:
365
+    #
366
+    #  1. Empty line, followed by line with single hash sign
367
+    #  2. Set of consecutive docstring lines (start with '# ')
368
+    #  3. Another "#" followed by one or more variable definitions (start
369
+    #     with $name or \w+, followed by equal sign), at least one of which
370
+    #     must be $1
371
+    #  4. Empty line or EOF
372
+    #
373
+    # and return what is found.
374
+    #
375
+    # For example:
376
+    #
377
+    #     #
378
+    #     # My variable
379
+    #     #
380
+    #     my_var=foo
381
+    #
382
+    # is found if $1 is 'my_var'
383
+    #
384
+    # A more advanced example:
385
+    #
386
+    #     #
387
+    #     # Bar variables
388
+    #     #
389
+    #     # These serve similar purpose; the difference is
390
+    #     # obvious
391
+    #     #
392
+    #     bar_1=one
393
+    #     bar_2=two
394
+    #     bar_3=three
395
+    #
396
+    # is found if $1 is either 'bar_1', 'bar_2' or 'bar_3'.  This allows
397
+    # for a docstring being shared among closely related variables.
398
+    #
399
+    local oname=$1    # object name
400
+    local state       # void, edge, dstr, vstr, junk
401
+    local o_end       # object ended; is complete
402
+    local cache       # buffer of hope
403
+    # parse and keep each var docstring in cache;
404
+    # then decide whether or not print the cache
405
+    cache=$(mktemp -t shellfu_doc.__sfdoc__filter_var.XXXXXXXX)
406
+    state=junk
407
+    o_end=false
408
+    while read -r line;
409
+    do
410
+        case $state:$line in
411
+            # looks like void
412
+            void:)          state=void; o_end=false ;;
413
+            edge:)          state=void; o_end=true  ;;
414
+            dstr:)          state=void; o_end=true  ;;
415
+            vstr:)          state=void; o_end=true  ;;
416
+            junk:)          state=void; o_end=false ;;
417
+            # looks like edge
418
+            void:"#")       state=edge; o_end=false ;;
419
+            edge:"#")       state=dstr; o_end=false ;;
420
+            dstr:"#")       state=dstr; o_end=false ;;
421
+            vstr:"#")       state=dstr; o_end=false ;;
422
+            junk:"#")       state=junk; o_end=false ;;
423
+            # looks like docstring
424
+            void:"# "*)     state=junk; o_end=false ;;
425
+            edge:"# "*)     state=dstr; o_end=false ;;
426
+            dstr:"# "*)     state=dstr; o_end=false ;;
427
+            vstr:"# "*)     state=dstr; o_end=false ;;
428
+            junk:"# "*)     state=junk; o_end=false ;;
429
+            # looks like junk
430
+            void:"#"*)      state=junk; o_end=false ;;
431
+            edge:"#"*)      state=junk; o_end=false ;;
432
+            dstr:"#"*)      state=junk; o_end=false ;;
433
+            vstr:"#"*)      state=junk; o_end=false ;;
434
+            junk:"#"*)      state=junk; o_end=false ;;
435
+            # looks like variable string
436
+            void:*=*)       state=vstr; o_end=false ;;
437
+            edge:*=*)       state=junk; o_end=false ;;
438
+            dstr:*=*)       state=vstr; o_end=false ;;
439
+            vstr:*=*)       state=vstr; o_end=false ;;
440
+            junk:*=*)       state=junk; o_end=false ;;
441
+            # looks like something else
442
+            *)              state=junk; o_end=false ;;
443
+        esac
444
+        case $state in
445
+            edge|dstr|vstr) echo "$line" >> "$cache" ;;
446
+        esac
447
+        if $o_end;
448
+        then
449
+            if grep -q "^$oname=" "$cache";   # it's our wanted object
450
+            then
451
+                cat "$cache"
452
+                rm "$cache"
453
+                return 0
454
+            fi
455
+            rm "$cache"
456
+        fi
457
+    done
458
+    rm "$cache"
459
+    return 1
460
+}
461
+
462
+__sfdoc__filter_vdoc() {
463
+    #
464
+    # From variable definition body on stdin, filter doc
465
+    #
466
+    # Example:
467
+    #
468
+    #     #
469
+    #     # Bar variables
470
+    #     #
471
+    #     # These serve similar purpose; the difference is
472
+    #     # obvious
473
+    #     #
474
+    #     bar_1=one
475
+    #     bar_2=two
476
+    #     bar_3=three
477
+    #
478
+    grep '^#' | __sfdoc__strip_doc
479
+}
480
+
481
+__sfdoc__get_doc() {
482
+    #
483
+    # Show doc for part $2 of module $1
484
+    #
485
+    local part=$1
486
+    __sfdoc__get_part "$part" | __sfdoc__filter_doc "$part"
487
+}
488
+
489
+__sfdoc__get_part() {
490
+    #
491
+    # Print part $1 (f|v:name) of module $MPath
492
+    #
493
+    # Part has format
494
+    #
495
+    #     TYPE:NAME
496
+    #
497
+    # where TYPE is 'v' for variables and f for functions, and
498
+    # NAME is name of the part.
499
+    #
500
+    # If part is not specified, whole module file is printed.
501
+    #
502
+    local part=$1
503
+    test -n "$part" || { cat "$MPath"; return $?; }
504
+    <"$MPath" __sfdoc__filter_body "$part"
505
+}
506
+
507
+__sfdoc__md_escapes() {
508
+    #
509
+    # Do additional escapes for Markdown
510
+    #
511
+    # In docstrings, references to variables are often done
512
+    # without backticks;  add these to prevent interpreting
513
+    # underscores in variable names as cursive.
514
+    #
515
+    perl -ne '
516
+        my @bigwords = ();
517
+        my @newwords = ();
518
+        if (m|^    |) {
519
+            print;                              # probably code -- leave be
520
+        } else {
521
+            @bigwords = split " ";
522
+            foreach (@bigwords) {
523
+                if (m|`\w+\(\)`|) {
524
+                    push @newwords, $_;              # function: escaped
525
+                } elsif (m|(.*)\b(\w+)\(\)(.*)|) {
526
+                    push @newwords, "$1`$2()`$3";    # function: needs escape
527
+                } elsif (m|`\$\w+`|) {
528
+                    push @newwords, $_;              # variable: escaped
529
+                } elsif (m|(.*)\$(\w+)(.*)|) {
530
+                    push @newwords, "$1`\$$2`$3";    # variable: needs escape
531
+                } else {
532
+                    push @newwords, $_;              # a normal word
533
+                }
534
+            }
535
+            print join " ", @newwords;
536
+            print "\n";
537
+        }
538
+    '
539
+}
540
+
541
+__sfdoc__strip_doc() {
542
+    #
543
+    # Strip doc out of Shellfu docstring
544
+    #
545
+    # From e.g.
546
+    #
547
+    #     #
548
+    #     # foo
549
+    #     #
550
+    #     # bar
551
+    #     # baz
552
+    #     #
553
+    #
554
+    # where first and last line are mandatory and each line
555
+    # must be either '#' or '# *', get:
556
+    #
557
+    #     foo
558
+    #
559
+    #     bar
560
+    #     baz
561
+    #
562
+    # just as when you're parsing this text by eyes.
563
+    #
564
+    head -n -1 \
565
+      | tail -n +2 \
566
+      | sed -e 's/^#$//; s/^# //;'
567
+}

+ 5
- 1
tests/sfdoc/TF_RUN 查看文件

@@ -28,6 +28,10 @@ tf_do_subtest() {
28 28
     #
29 29
     local name=$1
30 30
     local cmd
31
+    local o_es=0
32
+    case $name in
33
+        nonesuch)      o_es=3 ;;
34
+    esac
31 35
     case $name in
32 36
         lsx_tricky)    cmd="sfdoc --ls test/include/tricky.sh" ;;
33 37
         lsv_tricky)    cmd="sfdoc --lsvar test/include/tricky.sh" ;;
@@ -36,7 +40,7 @@ tf_do_subtest() {
36 40
         included_path) cmd="sfdoc -I test/other -e markdown path" ;;
37 41
         *)             cmd="sfdoc -e markdown $name" ;;
38 42
     esac
39
-    tf_testflt -n "$name" -O "oracle/$name.stdout" -E "oracle/$name.stderr" "$cmd"
43
+    tf_testflt -n "$name" -O "oracle/$name.stdout" -E "oracle/$name.stderr" -S "$o_es" "$cmd"
40 44
 }
41 45
 
42 46
 tf_do_subtests

+ 1
- 2
tests/sfdoc/oracle/nonesuch.stderr 查看文件

@@ -1,3 +1,2 @@
1 1
 no such module found: nonesuch
2
-no such module found: nonesuch
3
-no such module found: nonesuch
2
+

+ 0
- 4
tests/sfdoc/oracle/nonesuch.stdout 查看文件

@@ -1,4 +0,0 @@
1
-nonesuch
2
-========
3
-
4
-

+ 1
- 0
tests/shellfu_api/TF_RUN 查看文件

@@ -19,6 +19,7 @@ flt_ours() {
19 19
         -e exit \
20 20
         -e termcolors \
21 21
         -e pretty \
22
+        -e sfdoc \
22 23
         -e mdfmt \
23 24
     #FIXME: remove the filter when TFKit learns to test in sandbox
24 25
 }

+ 18
- 0
tests/shellfu_api/oracle/functions.stdout 查看文件

@@ -73,3 +73,21 @@ pretty.mkhelp
73 73
 pretty.mkusage
74 74
 pretty.think
75 75
 pretty.warn
76
+sfdoc.__sfdoc__export_as_manpage
77
+sfdoc.__sfdoc__export_as_markdown
78
+sfdoc.__sfdoc__export_as_pod
79
+sfdoc.__sfdoc__filter_body
80
+sfdoc.__sfdoc__filter_doc
81
+sfdoc.__sfdoc__filter_fdoc
82
+sfdoc.__sfdoc__filter_fun
83
+sfdoc.__sfdoc__filter_hidden
84
+sfdoc.__sfdoc__filter_mdoc
85
+sfdoc.__sfdoc__filter_var
86
+sfdoc.__sfdoc__filter_vdoc
87
+sfdoc.__sfdoc__get_doc
88
+sfdoc.__sfdoc__get_part
89
+sfdoc.__sfdoc__md_escapes
90
+sfdoc.__sfdoc__strip_doc
91
+sfdoc.sfdoc__export
92
+sfdoc.sfdoc__ls
93
+sfdoc.sfdoc__ls_m

+ 1
- 0
tests/shellfu_api/oracle/modules.stdout 查看文件

@@ -8,4 +8,5 @@ exit
8 8
 inigrep
9 9
 mdfmt
10 10
 pretty
11
+sfdoc
11 12
 termcolors

+ 1
- 0
tests/shellfu_api/oracle/variables.stdout 查看文件

@@ -16,6 +16,7 @@ pretty.PRETTY_DEBUG
16 16
 pretty.PRETTY_DEBUG_EXCLUDE
17 17
 pretty.PRETTY_USAGE
18 18
 pretty.PRETTY_VERBOSE
19
+sfdoc.SFDOC_SHOW_HIDDEN
19 20
 termcolors.TERMCOLORS_BLACK
20 21
 termcolors.TERMCOLORS_BLUE
21 22
 termcolors.TERMCOLORS_CYAN