├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── question.yml │ ├── suggestion.yml │ └── bug.yml ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── codeql.yml │ └── ci.yml ├── dependabot.yml ├── CONTRIBUTING.md ├── images │ ├── license.svg │ ├── card.svg │ └── usage.svg └── CODE_OF_CONDUCT.md ├── .typos.toml ├── go.mod ├── templates ├── markdown.tpl └── html.tpl ├── parser ├── fuzz.go ├── parser.go └── parser_test.go ├── shdoc.go ├── go.sum ├── SECURITY.md ├── render ├── template │ └── template_render.go └── terminal │ └── terminal_render.go ├── README.md ├── script ├── script_test.go └── script.go ├── Makefile ├── cli └── cli.go └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /shdoc 2 | /vendor 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @essentialkaos/go 2 | /.github/workflows/* @essentialkaos/devops 3 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = ["go.sum"] 3 | 4 | [default.extend-identifiers] 5 | alse = "alse" 6 | O_WRONLY = "O_WRONLY" 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/essentialkaos/shdoc 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/essentialkaos/check v1.4.1 7 | github.com/essentialkaos/ek/v13 v13.26.2 8 | ) 9 | 10 | require ( 11 | github.com/essentialkaos/depsy v1.3.1 // indirect 12 | github.com/kr/pretty v0.3.1 // indirect 13 | github.com/kr/text v0.2.0 // indirect 14 | github.com/rogpeppe/go-internal v1.14.1 // indirect 15 | golang.org/x/sys v0.33.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What did you implement: 2 | 3 | Closes #XXXXX 4 | 5 | ### How did you implement it: 6 | 7 | ... 8 | 9 | ### How can we verify it: 10 | 11 | ... 12 | 13 | ### TODO's: 14 | 15 | - [ ] Write tests 16 | - [ ] Write documentation 17 | - [ ] Check that there aren't other open pull requests for the same issue/feature 18 | - [ ] Format your source code by `make fmt` 19 | - [ ] Pass the test by `make test` 20 | - [ ] Provide verification config / commands 21 | - [ ] Enable "Allow edits from maintainers" for this PR 22 | - [ ] Update the messages below 23 | 24 | **Is this ready for review?:** No 25 | **Is it a breaking change?:** No 26 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master, develop] 6 | pull_request: 7 | branches: [master] 8 | schedule: 9 | - cron: '0 3 * * */2' 10 | 11 | permissions: 12 | security-events: write 13 | actions: read 14 | contents: read 15 | 16 | jobs: 17 | analyse: 18 | name: Analyse 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 2 26 | 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v3 29 | with: 30 | languages: go 31 | 32 | - name: Perform CodeQL Analysis 33 | uses: github/codeql-action/analyze@v3 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | target-branch: "develop" 7 | schedule: 8 | interval: "daily" 9 | timezone: "Etc/UTC" 10 | time: "03:00" 11 | labels: 12 | - "PR • MAINTENANCE" 13 | assignees: 14 | - "andyone" 15 | groups: 16 | all: 17 | applies-to: version-updates 18 | update-types: 19 | - "minor" 20 | - "patch" 21 | 22 | - package-ecosystem: "github-actions" 23 | directory: "/" 24 | target-branch: "develop" 25 | schedule: 26 | interval: "daily" 27 | timezone: "Etc/UTC" 28 | time: "03:00" 29 | labels: 30 | - "PR • MAINTENANCE" 31 | assignees: 32 | - "andyone" 33 | -------------------------------------------------------------------------------- /templates/markdown.tpl: -------------------------------------------------------------------------------- 1 | # {{ .Title }} 2 | {{ if .HasAbout }} 3 | ### About 4 | 5 | {{ range .About }}{{ . }}{{ end}} 6 | {{ end }} 7 | 8 | {{ if .HasConstants }} 9 | ### Constants 10 | {{ range .Constants }} 11 | * `{{ .Name}} = {{ .Value }}` {{ .UnitedDesc }} (_{{ .TypeName 0 }}_){{ end }} 12 | {{ end }} 13 | 14 | {{ if .HasVariables }} 15 | ### Global Variables 16 | {{ range .Variables }} 17 | * `{{ .Name}} = {{ .Value }}` {{ .UnitedDesc }} (_{{ .TypeName 0 }}_){{ end }} 18 | {{ end }} 19 | 20 | {{ if .HasMethods }} 21 | ### Methods 22 | {{ range .Methods }} 23 | `{{ .Name }}` - {{ .UnitedDesc }} 24 | {{ range .Arguments }}* {{ .Index }}: {{ .Desc }} {{ if not .IsUnknown }}(_{{ .TypeName 0 }}_){{ end }}{{ if .IsOptional }} [_Optional_]{{ end }} 25 | {{ end }}{{ end }}{{ end }} 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: ❓ Question 2 | description: Question about application, configuration or code 3 | title: "[Question]: " 4 | labels: ["issue • question"] 5 | assignees: 6 | - andyone 7 | 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | > [!IMPORTANT] 13 | > Before you open an issue, search GitHub Issues for a similar question. If so, please add a 👍 reaction to the existing issue. 14 | 15 | - type: textarea 16 | attributes: 17 | label: Question 18 | description: Detailed question 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | attributes: 24 | label: Related version application info 25 | description: Output of `shdoc -vv` command 26 | render: shell 27 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | **IMPORTANT! Contribute your code only if you have an excellent understanding of project idea and all existing code base. Otherwise, a nicely formatted issue will be more helpful to us.** 4 | 5 | ### Issues 6 | 7 | 1. Provide product version where the problem was found; 8 | 2. Provide info about your environment; 9 | 3. Provide detailed info about your problem; 10 | 4. Provide steps to reproduce the problem; 11 | 5. Provide actual and expected results. 12 | 13 | ### Code 14 | 15 | 1. Check your code **before** creating pull request; 16 | 2. If tests are present in a project, add tests for your code; 17 | 3. Add inline documentation for your code; 18 | 4. Apply code style used throughout the project; 19 | 5. Create your pull request to `develop` branch (_pull requests to other branches are not allowed_). 20 | -------------------------------------------------------------------------------- /parser/fuzz.go: -------------------------------------------------------------------------------- 1 | //go:build gofuzz 2 | // +build gofuzz 3 | 4 | package parser 5 | 6 | // ////////////////////////////////////////////////////////////////////////////////// // 7 | // // 8 | // Copyright (c) 2025 ESSENTIAL KAOS // 9 | // Apache License, Version 2.0 // 10 | // // 11 | // ////////////////////////////////////////////////////////////////////////////////// // 12 | 13 | import ( 14 | "bytes" 15 | ) 16 | 17 | // ////////////////////////////////////////////////////////////////////////////////// // 18 | 19 | func Fuzz(data []byte) int { 20 | _, errs := readData("temp", bytes.NewReader(data)) 21 | 22 | if len(errs) != 0 { 23 | return 0 24 | } 25 | 26 | return 1 27 | } 28 | -------------------------------------------------------------------------------- /shdoc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // ////////////////////////////////////////////////////////////////////////////////// // 4 | // // 5 | // Copyright (c) 2025 ESSENTIAL KAOS // 6 | // Apache License, Version 2.0 // 7 | // // 8 | // ////////////////////////////////////////////////////////////////////////////////// // 9 | 10 | import ( 11 | _ "embed" 12 | 13 | CLI "github.com/essentialkaos/shdoc/cli" 14 | ) 15 | 16 | // ////////////////////////////////////////////////////////////////////////////////// // 17 | 18 | //go:embed go.mod 19 | var gomod []byte 20 | 21 | // gitrev is short hash of the latest git commit 22 | var gitrev string 23 | 24 | // ////////////////////////////////////////////////////////////////////////////////// // 25 | 26 | func main() { 27 | CLI.Run(gitrev, gomod) 28 | } 29 | -------------------------------------------------------------------------------- /.github/images/license.svg: -------------------------------------------------------------------------------- 1 | license: Apache-2.0licenseApache-2.0 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/suggestion.yml: -------------------------------------------------------------------------------- 1 | name: ➕ Suggestion 2 | description: Suggest new feature or improvement 3 | title: "[Suggestion]: " 4 | labels: ["issue • suggestion"] 5 | assignees: 6 | - andyone 7 | 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | > [!IMPORTANT] 13 | > Before you open an issue, search GitHub Issues for a similar feature requests. If so, please add a 👍 reaction to the existing issue. 14 | > 15 | > Opening a feature request kicks off a discussion. Requests may be closed if we're not actively planning to work on them. 16 | 17 | - type: textarea 18 | attributes: 19 | label: Proposal 20 | description: Description of the feature 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | attributes: 26 | label: Current behavior 27 | description: What currently happens 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | attributes: 33 | label: Desired behavior 34 | description: What you would like to happen 35 | validations: 36 | required: true 37 | 38 | - type: textarea 39 | attributes: 40 | label: Use case 41 | description: Why is this important (helps with prioritizing requests) 42 | validations: 43 | required: true 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/essentialkaos/check v1.4.1 h1:SuxXzrbokPGTPWxGRnzy0hXvtb44mtVrdNxgPa1s4c8= 3 | github.com/essentialkaos/check v1.4.1/go.mod h1:xQOYwFvnxfVZyt5Qvjoa1SxcRqu5VyP77pgALr3iu+M= 4 | github.com/essentialkaos/depsy v1.3.1 h1:00k9QcMsdPM4IzDaEFHsTHBD/zoM0oxtB5+dMUwbQa8= 5 | github.com/essentialkaos/depsy v1.3.1/go.mod h1:B5+7Jhv2a2RacOAxIKU2OeJp9QfZjwIpEEPI5X7auWM= 6 | github.com/essentialkaos/ek/v13 v13.26.2 h1:PTPaqGvmUIgoHjf2Kru4sC/Mb+Fnwna2XBlVKXhVGnI= 7 | github.com/essentialkaos/ek/v13 v13.26.2/go.mod h1:8/TJJ/5C5F1MC1iCMyepkRHoKGjPt4U6OzQvmgFN+9U= 8 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 9 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 13 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 14 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 15 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 16 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 17 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 18 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for all 4 | ESSENTIAL KAOS projects. 5 | 6 | * [Reporting a Bug](#reporting-a-bug) 7 | * [Disclosure Policy](#disclosure-policy) 8 | 9 | ## Reporting a Bug 10 | 11 | The ESSENTIAL KAOS team and community take all security bugs in our projects 12 | very seriously. Thank you for improving the security of our project. We 13 | appreciate your efforts and responsible disclosure and will make every effort 14 | to acknowledge your contributions. 15 | 16 | Report security bugs by emailing our security team at security@essentialkaos.com. 17 | 18 | The security team will acknowledge your email within 48 hours and will send a 19 | more detailed response within 48 hours, indicating the next steps in handling 20 | your report. After the initial reply to your report, the security team will 21 | endeavor to keep you informed of the progress towards a fix and full 22 | announcement, and may ask for additional information or guidance. 23 | 24 | Report security bugs in third-party dependencies to the person or team 25 | maintaining the dependencies. 26 | 27 | ## Disclosure Policy 28 | 29 | When the security team receives a security bug report, they will assign it to a 30 | primary handler. This person will coordinate the fix and release process, 31 | involving the following steps: 32 | 33 | * Confirm the problem and determine the affected versions; 34 | * Audit code to find any similar potential problems; 35 | * Prepare fixes for all releases still under maintenance. These fixes will be 36 | released as fast as possible. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: ❗ Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["issue • bug"] 5 | assignees: 6 | - andyone 7 | 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | > [!IMPORTANT] 13 | > Before you open an issue, search GitHub Issues for a similar bug reports. If so, please add a 👍 reaction to the existing issue. 14 | 15 | - type: textarea 16 | attributes: 17 | label: Verbose application info 18 | description: Output of `shdoc -vv` command 19 | render: shell 20 | validations: 21 | required: true 22 | 23 | - type: dropdown 24 | id: version 25 | attributes: 26 | label: Install tools 27 | description: How did you install this application 28 | options: 29 | - From Sources 30 | - RPM Package 31 | - Prebuilt Binary 32 | default: 0 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | attributes: 38 | label: Steps to reproduce 39 | description: Short guide on how to reproduce this problem on our site 40 | placeholder: | 41 | 1. [First Step] 42 | 2. [Second Step] 43 | 3. [and so on...] 44 | validations: 45 | required: true 46 | 47 | - type: textarea 48 | attributes: 49 | label: Expected behavior 50 | description: What you expected to happen 51 | validations: 52 | required: true 53 | 54 | - type: textarea 55 | attributes: 56 | label: Actual behavior 57 | description: What actually happened 58 | validations: 59 | required: true 60 | 61 | - type: textarea 62 | attributes: 63 | label: Additional info 64 | description: Include gist of relevant config, logs, etc. 65 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at `conduct@essentialkaos.com`. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master, develop] 6 | pull_request: 7 | branches: [master] 8 | schedule: 9 | - cron: '0 14 */15 * *' 10 | workflow_dispatch: 11 | inputs: 12 | force_run: 13 | description: 'Force workflow run' 14 | required: true 15 | type: choice 16 | options: [yes, no] 17 | 18 | permissions: 19 | actions: read 20 | contents: read 21 | statuses: write 22 | 23 | concurrency: 24 | group: ${{ github.workflow }}-${{ github.ref }} 25 | cancel-in-progress: true 26 | 27 | jobs: 28 | Go: 29 | name: Go 30 | runs-on: ubuntu-latest 31 | 32 | strategy: 33 | matrix: 34 | go: [ 'oldstable', 'stable' ] 35 | 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | 40 | - name: Set up Go 41 | uses: actions/setup-go@v5 42 | with: 43 | go-version: ${{ matrix.go }} 44 | 45 | - name: Download dependencies 46 | run: make deps 47 | 48 | - name: Build binary 49 | run: make all 50 | 51 | - name: Run tests 52 | run: go test -covermode count -coverprofile cover.out ./parser ./script 53 | 54 | - name: Send coverage data to Coveralls 55 | uses: essentialkaos/goveralls-action@v2 56 | env: 57 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | profile: cover.out 60 | parallel: true 61 | flag-name: linux-${{ matrix.go }} 62 | 63 | - name: Send coverage data to Codacy 64 | env: 65 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} 66 | run: | 67 | bash <(curl -Ls https://coverage.codacy.com/get.sh) report \ 68 | --force-coverage-parser go \ 69 | -r cover.out \ 70 | --partial 71 | 72 | SendCoverage: 73 | name: Send Coverage 74 | runs-on: ubuntu-latest 75 | if: success() || failure() 76 | 77 | needs: Go 78 | 79 | steps: 80 | - name: Finish parallel tests (Coveralls) 81 | uses: essentialkaos/goveralls-action@v2 82 | env: 83 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | parallel-finished: true 86 | 87 | - name: Finish parallel tests (Codacy) 88 | env: 89 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} 90 | run: bash <(curl -Ls https://coverage.codacy.com/get.sh) final 91 | 92 | Typos: 93 | name: Typos 94 | runs-on: ubuntu-latest 95 | 96 | needs: Go 97 | 98 | steps: 99 | - name: Checkout 100 | uses: actions/checkout@v4 101 | 102 | - name: Check spelling 103 | uses: crate-ci/typos@master 104 | continue-on-error: true 105 | -------------------------------------------------------------------------------- /.github/images/card.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /render/template/template_render.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | // ////////////////////////////////////////////////////////////////////////////////// // 4 | // // 5 | // Copyright (c) 2025 ESSENTIAL KAOS // 6 | // Apache License, Version 2.0 // 7 | // // 8 | // ////////////////////////////////////////////////////////////////////////////////// // 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "text/template" 14 | 15 | "github.com/essentialkaos/ek/v13/fmtc" 16 | "github.com/essentialkaos/ek/v13/fmtutil" 17 | "github.com/essentialkaos/ek/v13/fsutil" 18 | "github.com/essentialkaos/ek/v13/path" 19 | 20 | "github.com/essentialkaos/shdoc/script" 21 | ) 22 | 23 | // ////////////////////////////////////////////////////////////////////////////////// // 24 | 25 | // Render prints script info into terminal 26 | func Render(doc *script.Document, tmpl, output string) error { 27 | templateFile := getPathToTemplate(tmpl) 28 | 29 | if templateFile == "" { 30 | return fmt.Errorf("Can't find template %q", tmpl) 31 | } 32 | 33 | t, err := readTemplate(templateFile) 34 | 35 | if err != nil { 36 | return err 37 | } 38 | 39 | fd, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY, 0644) 40 | 41 | if err != nil { 42 | return err 43 | } 44 | 45 | defer fd.Close() 46 | 47 | err = t.Execute(fd, doc) 48 | 49 | if err != nil { 50 | return err 51 | } 52 | 53 | printDocumentStats(doc, output) 54 | 55 | return nil 56 | } 57 | 58 | // ////////////////////////////////////////////////////////////////////////////////// // 59 | 60 | // getPathToTemplate returns path to template file 61 | func getPathToTemplate(tmpl string) string { 62 | if fsutil.IsExist(tmpl) { 63 | return tmpl 64 | } 65 | 66 | templateFile := path.Join( 67 | os.Getenv("GOPATH"), 68 | "src/github.com/essentialkaos/shdoc/templates", 69 | tmpl+".tpl", 70 | ) 71 | 72 | if fsutil.IsExist(templateFile) { 73 | return templateFile 74 | } 75 | 76 | return "" 77 | } 78 | 79 | // readTemplate reads template 80 | func readTemplate(templateFile string) (*template.Template, error) { 81 | err := fsutil.ValidatePerms("FRS", templateFile) 82 | 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | templateData, err := os.ReadFile(templateFile) 88 | 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | tmpl := template.New("Template") 94 | 95 | return tmpl.Parse(string(templateData)) 96 | } 97 | 98 | // printDocumentStats prints information about document 99 | func printDocumentStats(doc *script.Document, output string) { 100 | fmtutil.Separator(false, doc.Title) 101 | 102 | fmtc.Printfn(" {*}Constants:{!} %d", len(doc.Constants)) 103 | fmtc.Printfn(" {*}Variables:{!} %d", len(doc.Variables)) 104 | fmtc.Printfn(" {*}Methods:{!} %d", len(doc.Methods)) 105 | 106 | fmtc.NewLine() 107 | 108 | fmtc.Printfn( 109 | " {*}Output:{!} %s {s-}(%s){!}", output, 110 | fmtutil.PrettySize(fsutil.GetSize(output)), 111 | ) 112 | 113 | fmtutil.Separator(false) 114 | } 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | GoReportCard 5 | Codacy badge 6 | Coverage Status 7 |
8 | GitHub Actions CI Status 9 | GitHub Actions CodeQL Status 10 | 11 |

12 | 13 |

Usage DemoInstallationUsageTest & Coverage StatusContributingLicense

14 | 15 |
16 | 17 | `shdoc` is a tool for viewing and exporting documentation for shell scripts. 18 | 19 | ### Usage Demo 20 | 21 | [![demo](https://github.com/user-attachments/assets/693ae7df-63ee-42ff-af46-e95fc652fd25)](#usage-demo) 22 | 23 | ### Installation 24 | 25 | #### From source 26 | 27 | Make sure you have a working Go [1.23+](https://github.com/essentialkaos/.github/blob/master/GO-VERSION-SUPPORT.md) workspace ([instructions](https://go.dev/doc/install)), then: 28 | 29 | ```bash 30 | go install github.com/essentialkaos/shdoc@latest 31 | ``` 32 | 33 | #### Prebuilt binaries 34 | 35 | You can download prebuilt binaries for Linux and macOS from [EK Apps Repository](https://apps.kaos.st/shdoc/latest). 36 | 37 | To install the latest prebuilt version of bibop, do: 38 | 39 | ```bash 40 | bash <(curl -fsSL https://apps.kaos.st/get) shdoc 41 | ``` 42 | 43 | ### Command-line completion 44 | 45 | You can generate completion for `bash`, `zsh` or `fish` shell. 46 | 47 | Bash: 48 | ``` 49 | sudo shdoc --completion=bash 1> /etc/bash_completion.d/shdoc 50 | ``` 51 | 52 | 53 | ZSH: 54 | ``` 55 | sudo shdoc --completion=zsh 1> /usr/share/zsh/site-functions/shdoc 56 | ``` 57 | 58 | 59 | Fish: 60 | ``` 61 | sudo shdoc --completion=fish 1> /usr/share/fish/vendor_completions.d/shdoc.fish 62 | ``` 63 | 64 | ### Usage 65 | 66 | 67 | 68 | ### Test & Coverage Status 69 | 70 | | Branch | CI | Coveralls | 71 | |--------|----------|-----------| 72 | | `master` | [![CI](https://kaos.sh/w/shdoc/ci.svg?branch=master)](https://kaos.sh/w/shdoc/ci?query=branch:master) | [![Coverage Status](https://kaos.sh/c/shdoc.svg?branch=master)](https://kaos.sh/c/shdoc?branch=master) | 73 | | `develop` | [![CI](https://kaos.sh/w/shdoc/ci.svg?branch=develop)](https://kaos.sh/w/shdoc/ci?query=branch:develop) | [![Coverage Status](https://kaos.sh/c/shdoc.svg?branch=develop)](https://kaos.sh/c/shdoc?branch=develop) | 74 | 75 | ### Contributing 76 | 77 | Before contributing to this project please read our [Contributing Guidelines](https://github.com/essentialkaos/.github/blob/master/CONTRIBUTING.md). 78 | 79 | ### License 80 | 81 | [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) 82 | 83 |

84 | -------------------------------------------------------------------------------- /.github/images/usage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SHDoc Usage 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Terminal 57 | 58 | Usage: shdoc {options} script 59 | Options 60 | --output, -o file .... Path to output file 61 | --template, -t name .. Name of template 62 | --name, -n name ...... Overwrite default name 63 | --no-pager, -np ...... Disable pager for long output 64 | --no-color, -nc ...... Disable colors in output 65 | --help, -h ........... Show this help message 66 | --version, -v ........ Show version 67 | Examples 68 | shdoc script.sh 69 | Parse shell script and show documentation in console 70 | shdoc script.sh -t markdown -o my_script.md 71 | Parse shell script and render documentation to markdown file 72 | shdoc script.sh -t /path/to/template.tpl -o my_script.ext 73 | Parse shell script and render documentation with given template 74 | shdoc script.sh myFunction 75 | Parse shell script and show documentation for some constant, variable or method 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /script/script_test.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | // ////////////////////////////////////////////////////////////////////////////////// // 4 | // // 5 | // Copyright (c) 2025 ESSENTIAL KAOS // 6 | // Apache License, Version 2.0 // 7 | // // 8 | // ////////////////////////////////////////////////////////////////////////////////// // 9 | 10 | import ( 11 | "testing" 12 | 13 | . "github.com/essentialkaos/check" 14 | ) 15 | 16 | // ////////////////////////////////////////////////////////////////////////////////// // 17 | 18 | func Test(t *testing.T) { TestingT(t) } 19 | 20 | // ////////////////////////////////////////////////////////////////////////////////// // 21 | 22 | type ScriptSuite struct{} 23 | 24 | // ////////////////////////////////////////////////////////////////////////////////// // 25 | 26 | var _ = Suite(&ScriptSuite{}) 27 | 28 | // ////////////////////////////////////////////////////////////////////////////////// // 29 | 30 | func (s *ScriptSuite) TestValidation(c *C) { 31 | sh1 := &Document{} 32 | sh2 := &Document{Constants: make([]*Variable, 0)} 33 | sh3 := &Document{Variables: make([]*Variable, 0)} 34 | sh4 := &Document{Methods: make([]*Method, 0)} 35 | sh5 := &Document{Constants: make([]*Variable, 1)} 36 | sh6 := &Document{Variables: make([]*Variable, 1)} 37 | sh7 := &Document{Methods: make([]*Method, 1)} 38 | 39 | c.Assert(sh1.IsValid(), Equals, false) 40 | c.Assert(sh2.IsValid(), Equals, false) 41 | c.Assert(sh3.IsValid(), Equals, false) 42 | c.Assert(sh4.IsValid(), Equals, false) 43 | c.Assert(sh5.IsValid(), Equals, true) 44 | c.Assert(sh6.IsValid(), Equals, true) 45 | c.Assert(sh7.IsValid(), Equals, true) 46 | } 47 | 48 | func (s *ScriptSuite) TestNil(c *C) { 49 | var d *Document 50 | var a *Argument 51 | var v *Variable 52 | var m *Method 53 | 54 | c.Assert(d.IsValid(), Equals, false) 55 | c.Assert(d.HasAbout(), Equals, false) 56 | c.Assert(d.HasConstants(), Equals, false) 57 | c.Assert(d.HasVariables(), Equals, false) 58 | c.Assert(d.HasMethods(), Equals, false) 59 | 60 | c.Assert(a.TypeName(0), Equals, "") 61 | c.Assert(a.IsString(), Equals, false) 62 | c.Assert(a.IsNumber(), Equals, false) 63 | c.Assert(a.IsBoolean(), Equals, false) 64 | c.Assert(a.IsUnknown(), Equals, false) 65 | 66 | c.Assert(v.TypeName(0), Equals, "") 67 | c.Assert(v.IsString(), Equals, false) 68 | c.Assert(v.IsNumber(), Equals, false) 69 | c.Assert(v.IsBoolean(), Equals, false) 70 | c.Assert(v.IsUnknown(), Equals, false) 71 | c.Assert(v.UnitedDesc(), Equals, "") 72 | 73 | c.Assert(m.HasArguments(), Equals, false) 74 | c.Assert(m.HasEcho(), Equals, false) 75 | c.Assert(m.HasExample(), Equals, false) 76 | c.Assert(m.UnitedDesc(), Equals, "") 77 | } 78 | 79 | func (s *ScriptSuite) TestHelpers(c *C) { 80 | d := &Document{ 81 | Title: "Test title", 82 | About: []string{"Test", "description"}, 83 | Constants: []*Variable{&Variable{}}, 84 | Variables: []*Variable{&Variable{}}, 85 | Methods: []*Method{&Method{}}, 86 | } 87 | 88 | c.Assert(d.HasAbout(), Equals, true) 89 | c.Assert(d.HasConstants(), Equals, true) 90 | c.Assert(d.HasVariables(), Equals, true) 91 | c.Assert(d.HasMethods(), Equals, true) 92 | 93 | a1 := &Argument{"1", "A1", VAR_TYPE_UNKNOWN, false, false} 94 | a2 := &Argument{"2", "A2", VAR_TYPE_STRING, false, false} 95 | a3 := &Argument{"3", "A3", VAR_TYPE_NUMBER, false, false} 96 | a4 := &Argument{"4", "A4", VAR_TYPE_BOOLEAN, false, false} 97 | a5 := &Argument{"*", "A5", VAR_TYPE_UNKNOWN, true, true} 98 | 99 | c.Assert(a1.TypeName(VAR_MOD_DEFAULT), Equals, "") 100 | c.Assert(a2.TypeName(VAR_MOD_DEFAULT), Equals, "String") 101 | c.Assert(a3.TypeName(VAR_MOD_DEFAULT), Equals, "Number") 102 | c.Assert(a4.TypeName(VAR_MOD_DEFAULT), Equals, "Boolean") 103 | c.Assert(a5.TypeName(VAR_MOD_DEFAULT), Equals, "") 104 | 105 | c.Assert(a1.IsUnknown(), Equals, true) 106 | c.Assert(a2.IsString(), Equals, true) 107 | c.Assert(a3.IsNumber(), Equals, true) 108 | c.Assert(a4.IsBoolean(), Equals, true) 109 | 110 | v1 := &Variable{"1", []string{"V1", "", "D"}, VAR_TYPE_UNKNOWN, "v1", 1} 111 | v2 := &Variable{"2", []string{"V2"}, VAR_TYPE_STRING, "v2", 2} 112 | v3 := &Variable{"3", []string{"V3"}, VAR_TYPE_NUMBER, "v3", 3} 113 | v4 := &Variable{"4", []string{"V4"}, VAR_TYPE_BOOLEAN, "v4", 4} 114 | 115 | c.Assert(v1.TypeName(VAR_MOD_DEFAULT), Equals, "") 116 | c.Assert(v2.TypeName(VAR_MOD_DEFAULT), Equals, "String") 117 | c.Assert(v3.TypeName(VAR_MOD_DEFAULT), Equals, "Number") 118 | c.Assert(v4.TypeName(VAR_MOD_DEFAULT), Equals, "Boolean") 119 | 120 | c.Assert(v1.IsUnknown(), Equals, true) 121 | c.Assert(v2.IsString(), Equals, true) 122 | c.Assert(v3.IsNumber(), Equals, true) 123 | c.Assert(v4.IsBoolean(), Equals, true) 124 | 125 | c.Assert(v1.UnitedDesc(), Equals, "V1 D") 126 | 127 | m := &Method{ 128 | Name: "m1", 129 | Desc: []string{"M1", "", "D"}, 130 | Arguments: []*Argument{ 131 | &Argument{"1", "A1", VAR_TYPE_UNKNOWN, false, false}, 132 | }, 133 | ResultCode: true, 134 | ResultEcho: &Variable{"1", []string{"V1"}, VAR_TYPE_STRING, "v1", 1}, 135 | Example: []string{"example"}, 136 | Line: 15, 137 | } 138 | 139 | c.Assert(m.HasArguments(), Equals, true) 140 | c.Assert(m.HasEcho(), Equals, true) 141 | c.Assert(m.HasExample(), Equals, true) 142 | c.Assert(m.UnitedDesc(), Equals, "M1 D") 143 | } 144 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | 3 | # This Makefile generated by GoMakeGen 3.3.2 using next command: 4 | # gomakegen --mod . 5 | # 6 | # More info: https://kaos.sh/gomakegen 7 | 8 | ################################################################################ 9 | 10 | ifdef VERBOSE ## Print verbose information (Flag) 11 | VERBOSE_FLAG = -v 12 | endif 13 | 14 | ifdef PROXY ## Force proxy usage for downloading dependencies (Flag) 15 | export GOPROXY=https://proxy.golang.org/cached-only,direct 16 | endif 17 | 18 | ifdef CGO ## Enable CGO usage (Flag) 19 | export CGO_ENABLED=1 20 | else 21 | export CGO_ENABLED=0 22 | endif 23 | 24 | MAKEDIR = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) 25 | GITREV ?= $(shell test -s $(MAKEDIR)/.git && git rev-parse --short HEAD) 26 | 27 | ################################################################################ 28 | 29 | .DEFAULT_GOAL := help 30 | .PHONY = fmt vet all install uninstall clean deps update test init vendor gen-fuzz tidy mod-init mod-update mod-download mod-vendor help 31 | 32 | ################################################################################ 33 | 34 | all: shdoc ## Build all binaries 35 | 36 | shdoc: 37 | @echo "Building shdoc…" 38 | @go build $(VERBOSE_FLAG) -ldflags="-X main.gitrev=$(GITREV)" shdoc.go 39 | 40 | install: ## Install all binaries 41 | @echo "Installing binaries…" 42 | @cp shdoc /usr/bin/shdoc 43 | 44 | uninstall: ## Uninstall all binaries 45 | @echo "Removing installed binaries…" 46 | @rm -f /usr/bin/shdoc 47 | 48 | init: mod-init ## Initialize new module 49 | 50 | deps: mod-download ## Download dependencies 51 | 52 | update: mod-update ## Update dependencies to the latest versions 53 | 54 | vendor: mod-vendor ## Make vendored copy of dependencies 55 | 56 | test: ## Run tests 57 | @echo "Starting tests…" 58 | ifdef COVERAGE_FILE ## Save coverage data into file (String) 59 | @go test $(VERBOSE_FLAG) -covermode=count -coverprofile=$(COVERAGE_FILE) ./parser ./script 60 | else 61 | @go test $(VERBOSE_FLAG) -covermode=count ./parser ./script 62 | endif 63 | 64 | gen-fuzz: ## Generate archives for fuzz testing 65 | @which go-fuzz-build &>/dev/null || go install github.com/dvyukov/go-fuzz/go-fuzz-build@latest 66 | @echo "Generating fuzzing data…" 67 | @go-fuzz-build -o parser-fuzz.zip github.com/essentialkaos/shdoc/parser 68 | 69 | tidy: ## Cleanup dependencies 70 | @echo "•• Tidying up dependencies…" 71 | ifdef COMPAT ## Compatible Go version (String) 72 | @go mod tidy $(VERBOSE_FLAG) -compat=$(COMPAT) -go=$(COMPAT) 73 | else 74 | @go mod tidy $(VERBOSE_FLAG) 75 | endif 76 | @echo "•• Updating vendored dependencies…" 77 | @test -d vendor && rm -rf vendor && go mod vendor $(VERBOSE_FLAG) || : 78 | 79 | mod-init: 80 | @echo "••• Modules initialization…" 81 | @rm -f go.mod go.sum 82 | ifdef MODULE_PATH ## Module path for initialization (String) 83 | @go mod init $(MODULE_PATH) 84 | else 85 | @go mod init 86 | endif 87 | 88 | @echo "••• Dependencies cleanup…" 89 | ifdef COMPAT ## Compatible Go version (String) 90 | @go mod tidy $(VERBOSE_FLAG) -compat=$(COMPAT) -go=$(COMPAT) 91 | else 92 | @go mod tidy $(VERBOSE_FLAG) 93 | endif 94 | @echo "••• Stripping toolchain info…" 95 | @grep -q 'toolchain ' go.mod && go mod edit -toolchain=none || : 96 | 97 | mod-update: 98 | @echo "•••• Updating dependencies…" 99 | ifdef UPDATE_ALL ## Update all dependencies (Flag) 100 | @go get -u $(VERBOSE_FLAG) all 101 | else 102 | @go get -u $(VERBOSE_FLAG) ./... 103 | endif 104 | 105 | @echo "•••• Stripping toolchain info…" 106 | @grep -q 'toolchain ' go.mod && go mod edit -toolchain=none || : 107 | 108 | @echo "•••• Dependencies cleanup…" 109 | ifdef COMPAT 110 | @go mod tidy $(VERBOSE_FLAG) -compat=$(COMPAT) 111 | else 112 | @go mod tidy $(VERBOSE_FLAG) 113 | endif 114 | 115 | @echo "•••• Updating vendored dependencies…" 116 | @test -d vendor && rm -rf vendor && go mod vendor $(VERBOSE_FLAG) || : 117 | 118 | mod-download: 119 | @echo "Downloading dependencies…" 120 | @go mod download 121 | 122 | mod-vendor: 123 | @echo "Vendoring dependencies…" 124 | @rm -rf vendor && go mod vendor $(VERBOSE_FLAG) || : 125 | 126 | fmt: ## Format source code with gofmt 127 | @echo "Formatting sources…" 128 | @find . -name "*.go" -exec gofmt -s -w {} \; 129 | 130 | vet: ## Runs 'go vet' over sources 131 | @echo "Running 'go vet' over sources…" 132 | @go vet -composites=false -printfuncs=LPrintf,TLPrintf,TPrintf,log.Debug,log.Info,log.Warn,log.Error,log.Critical,log.Print ./... 133 | 134 | clean: ## Remove generated files 135 | @echo "Removing built binaries…" 136 | @rm -f shdoc 137 | 138 | help: ## Show this info 139 | @echo -e '\n\033[1mTargets:\033[0m\n' 140 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ 141 | | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[33m%-9s\033[0m %s\n", $$1, $$2}' 142 | @echo -e '\n\033[1mVariables:\033[0m\n' 143 | @grep -E '^ifdef [A-Z_]+ .*?## .*$$' $(abspath $(lastword $(MAKEFILE_LIST))) \ 144 | | sed 's/ifdef //' \ 145 | | sort -h \ 146 | | awk 'BEGIN {FS = " .*?## "}; {printf " \033[32m%-13s\033[0m %s\n", $$1, $$2}' 147 | @echo -e '' 148 | @echo -e '\033[90mGenerated by GoMakeGen 3.3.2\033[0m\n' 149 | 150 | ################################################################################ 151 | -------------------------------------------------------------------------------- /render/terminal/terminal_render.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | // ////////////////////////////////////////////////////////////////////////////////// // 4 | // // 5 | // Copyright (c) 2025 ESSENTIAL KAOS // 6 | // Apache License, Version 2.0 // 7 | // // 8 | // ////////////////////////////////////////////////////////////////////////////////// // 9 | 10 | import ( 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/essentialkaos/ek/v13/fmtc" 15 | "github.com/essentialkaos/ek/v13/fmtutil" 16 | 17 | "github.com/essentialkaos/shdoc/script" 18 | ) 19 | 20 | // ////////////////////////////////////////////////////////////////////////////////// // 21 | 22 | var varExtractRegex = regexp.MustCompile(`\$\{*[^\}\n\r]+\}*`) 23 | 24 | // ////////////////////////////////////////////////////////////////////////////////// // 25 | 26 | // Render prints script info into terminal 27 | func Render(doc *script.Document, pattern string) error { 28 | if pattern != "" { 29 | renderPart(doc, pattern) 30 | } else { 31 | renderAll(doc) 32 | } 33 | 34 | return nil 35 | } 36 | 37 | // ////////////////////////////////////////////////////////////////////////////////// // 38 | 39 | // renderAll renders all document info 40 | func renderAll(doc *script.Document) { 41 | if doc.HasAbout() { 42 | fmtutil.Separator(false, "ABOUT") 43 | 44 | for _, l := range doc.About { 45 | fmtc.Printfn(" %s", l) 46 | } 47 | } 48 | 49 | if doc.HasConstants() { 50 | fmtutil.Separator(false, "CONSTANTS") 51 | 52 | totalConstants := len(doc.Constants) 53 | 54 | for i, c := range doc.Constants { 55 | renderConstant(c) 56 | 57 | if i < totalConstants-1 { 58 | fmtc.NewLine() 59 | } 60 | } 61 | } 62 | 63 | if doc.HasVariables() { 64 | fmtutil.Separator(false, "GLOBAL VARIABLES") 65 | 66 | totalVariables := len(doc.Variables) 67 | 68 | for i, v := range doc.Variables { 69 | renderVariable(v) 70 | 71 | if i < totalVariables-1 { 72 | fmtc.NewLine() 73 | } 74 | } 75 | } 76 | 77 | if doc.HasMethods() { 78 | fmtutil.Separator(false, "METHODS") 79 | 80 | totalMethods := len(doc.Methods) 81 | 82 | for i, m := range doc.Methods { 83 | renderMethod(m, false) 84 | 85 | if i < totalMethods-1 { 86 | fmtc.Println("\n{s-}" + strings.Repeat("-", 88) + "{!}") 87 | fmtc.NewLine() 88 | } 89 | } 90 | } 91 | 92 | fmtutil.Separator(false) 93 | } 94 | 95 | // renderPart renders only part of document (method/variable/constant) 96 | func renderPart(doc *script.Document, pattern string) { 97 | fmtc.NewLine() 98 | 99 | if doc.Constants != nil { 100 | for _, c := range doc.Constants { 101 | if strings.Contains(c.Name, pattern) { 102 | renderConstant(c) 103 | fmtc.NewLine() 104 | } 105 | } 106 | } 107 | 108 | if doc.Variables != nil { 109 | for _, v := range doc.Variables { 110 | if strings.Contains(v.Name, pattern) { 111 | renderVariable(v) 112 | fmtc.NewLine() 113 | } 114 | } 115 | } 116 | 117 | if doc.Methods != nil { 118 | for _, m := range doc.Methods { 119 | if strings.Contains(m.Name, pattern) { 120 | renderMethod(m, true) 121 | fmtc.NewLine() 122 | } 123 | } 124 | } 125 | } 126 | 127 | // renderConstant prints constant info to console 128 | func renderConstant(c *script.Variable) { 129 | fmtc.Printfn("{s}%4d:{!} {m*}%s{!} {s}={!} "+colorizeValue(c.Value)+" "+getVarTypeDesc(c.Type), c.Line, c.Name) 130 | fmtc.Printfn(" %s", c.UnitedDesc()) 131 | } 132 | 133 | // renderMethod prints variable info to console 134 | func renderVariable(v *script.Variable) { 135 | fmtc.Printfn("{s}%4d:{!} {c*}%s{!} {s}={!} "+colorizeValue(v.Value)+" "+getVarTypeDesc(v.Type), v.Line, v.Name) 136 | fmtc.Printfn(" %s", v.UnitedDesc()) 137 | } 138 | 139 | // renderMethod prints method info to console 140 | func renderMethod(m *script.Method, showExamples bool) { 141 | fmtc.Printfn("{s}%4d:{!} {b*}%s{!} {s}-{!} %s", m.Line, m.Name, m.UnitedDesc()) 142 | 143 | if len(m.Arguments) != 0 { 144 | fmtc.NewLine() 145 | 146 | for _, a := range m.Arguments { 147 | switch { 148 | case a.IsOptional: 149 | fmtc.Printfn(" {s-}%2s.{!} %s "+getVarTypeDesc(a.Type)+" {s-}[Optional]{!}", a.Index, a.Desc) 150 | case a.IsWildcard: 151 | fmtc.Printfn(" {s-}%2s.{!} %s", a.Index, a.Desc) 152 | default: 153 | fmtc.Printfn(" {s-}%2s.{!} %s "+getVarTypeDesc(a.Type), a.Index, a.Desc) 154 | } 155 | } 156 | } 157 | 158 | if m.ResultCode { 159 | fmtc.NewLine() 160 | fmtc.Printfn(" {*}Code:{!} 0 - ok, 1 - not ok") 161 | } 162 | 163 | if m.ResultEcho != nil { 164 | fmtc.NewLine() 165 | fmtc.Printfn(" {*}Echo:{!} %s "+getVarTypeDesc(m.ResultEcho.Type), strings.Join(m.ResultEcho.Desc, " ")) 166 | } 167 | 168 | if m.Example != nil && showExamples { 169 | fmtc.NewLine() 170 | fmtc.Println(" {*}Example:{!}") 171 | fmtc.NewLine() 172 | 173 | for _, l := range m.Example { 174 | fmtc.Printfn(" {s}%s{!}", l) 175 | } 176 | } 177 | } 178 | 179 | // colorizeValue adds color tags based on variable value 180 | func colorizeValue(value string) string { 181 | if !varExtractRegex.MatchString(value) { 182 | return value 183 | } 184 | 185 | return varExtractRegex.ReplaceAllStringFunc(value, func(v string) string { 186 | return "{g}" + v + "{!}" 187 | }) 188 | } 189 | 190 | // getVarTypeDesc returns type description 191 | func getVarTypeDesc(t script.VariableType) string { 192 | switch t { 193 | case script.VAR_TYPE_STRING: 194 | return "{b}({&}String{!&}){!}" 195 | case script.VAR_TYPE_NUMBER: 196 | return "{y}({&}Number{!&}){!}" 197 | case script.VAR_TYPE_BOOLEAN: 198 | return "{g}({&}Boolean{!&}){!}" 199 | default: 200 | return "" 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /templates/html.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ .Title }} 7 | 8 | 9 | 10 | 43 | 44 | 45 |
46 |

{{ .Title }}

47 | {{ if .HasAbout }} 48 |

About

49 |
50 | {{ range .About }}

{{ . }}

{{ end }} 51 |
52 | {{ end }} 53 | 54 | 55 | 56 | {{ if .HasConstants }} 57 |

Constants

58 | {{ range .Constants }} 59 | 60 | {{ end }} 61 | {{ end }} 62 | 63 | {{ if .HasVariables }} 64 |

Global Variables

65 | {{ range .Variables }} 66 | 67 | {{ end }} 68 | {{ end }} 69 | 70 | {{ if .HasMethods }} 71 |

Methods

72 | {{ range .Methods }} 73 | 74 | {{ end }} 75 | {{ end }} 76 | 77 | 78 | 79 | {{ if .HasConstants }} 80 |

Constants

81 | {{ range .Constants }} 82 |
83 |
84 | {{ .Name }} = {{ .Value }} {{ .TypeName 2 }} 85 |
86 |
87 | {{ .UnitedDesc }} 88 |
89 |
90 | {{ end }} 91 | {{ end }} 92 | 93 | 94 | 95 | {{ if .HasVariables }} 96 |

Global Variables

97 | {{ range .Variables }} 98 |
99 |
100 | {{ .Name }} = {{ .Value }} {{ .TypeName 2 }} 101 |
102 |
103 | {{ .UnitedDesc }} 104 |
105 |
106 | {{ end }} 107 | {{ end }} 108 | 109 | 110 | 111 | {{ if .HasMethods }} 112 |

Methods

113 | {{ range .Methods }} 114 |
115 |
116 | {{ .Name }} — {{ .UnitedDesc }} 117 |
118 |
119 | {{ if .HasArguments }} 120 |
121 | {{ range .Arguments }} 122 |
123 | {{ .Index }}. {{ .Desc }} {{ .TypeName 2 }} {{ if .IsOptional }}OPTIONAL{{ end }} 124 |
125 | {{ end }} 126 |
127 | {{ end }} 128 | {{ if .ResultCode }} 129 |
130 | Code: 0 - ok, 1 - not ok 131 |
132 | {{ end }} 133 | {{ if .HasEcho }} 134 |
135 | Echo: {{ .ResultEcho.UnitedDesc }} {{ .ResultEcho.TypeName 2 }} 136 |
137 | {{ end }} 138 | {{ if .HasExample }} 139 |
140 | Example: 141 |
{{ range .Example }}{{ . }}
{{ end }}
142 |
143 | {{ end }} 144 |
145 |
146 | {{ end }} 147 | {{ end }} 148 |
149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | // ////////////////////////////////////////////////////////////////////////////////// // 4 | // // 5 | // Copyright (c) 2025 ESSENTIAL KAOS // 6 | // Apache License, Version 2.0 // 7 | // // 8 | // ////////////////////////////////////////////////////////////////////////////////// // 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | 14 | "github.com/essentialkaos/ek/v13/fmtc" 15 | "github.com/essentialkaos/ek/v13/fsutil" 16 | "github.com/essentialkaos/ek/v13/options" 17 | "github.com/essentialkaos/ek/v13/pager" 18 | "github.com/essentialkaos/ek/v13/support" 19 | "github.com/essentialkaos/ek/v13/support/apps" 20 | "github.com/essentialkaos/ek/v13/support/deps" 21 | "github.com/essentialkaos/ek/v13/terminal/tty" 22 | "github.com/essentialkaos/ek/v13/usage" 23 | "github.com/essentialkaos/ek/v13/usage/completion/bash" 24 | "github.com/essentialkaos/ek/v13/usage/completion/fish" 25 | "github.com/essentialkaos/ek/v13/usage/completion/zsh" 26 | "github.com/essentialkaos/ek/v13/usage/man" 27 | "github.com/essentialkaos/ek/v13/usage/update" 28 | 29 | term "github.com/essentialkaos/ek/v13/terminal" 30 | 31 | "github.com/essentialkaos/shdoc/parser" 32 | "github.com/essentialkaos/shdoc/render/template" 33 | "github.com/essentialkaos/shdoc/render/terminal" 34 | ) 35 | 36 | // ////////////////////////////////////////////////////////////////////////////////// // 37 | 38 | const ( 39 | APP = "SHDoc" 40 | VER = "0.10.2" 41 | DESC = "Tool for viewing and exporting docs for shell scripts" 42 | ) 43 | 44 | const ( 45 | OPT_OUTPUT = "o:output" 46 | OPT_TEMPLATE = "t:template" 47 | OPT_NAME = "n:name" 48 | OPT_NO_PAGER = "np:no-pager" 49 | OPT_NO_COLOR = "nc:no-color" 50 | OPT_HELP = "h:help" 51 | OPT_VER = "v:version" 52 | 53 | OPT_VERB_VER = "vv:verbose-version" 54 | OPT_COMPLETION = "completion" 55 | OPT_GENERATE_MAN = "generate-man" 56 | ) 57 | 58 | // ////////////////////////////////////////////////////////////////////////////////// // 59 | 60 | var optMap = options.Map{ 61 | OPT_OUTPUT: {}, 62 | OPT_TEMPLATE: {Value: "html"}, 63 | OPT_NAME: {}, 64 | OPT_NO_PAGER: {Type: options.BOOL}, 65 | OPT_NO_COLOR: {Type: options.BOOL}, 66 | OPT_HELP: {Type: options.BOOL}, 67 | OPT_VER: {Type: options.MIXED}, 68 | 69 | OPT_VERB_VER: {Type: options.BOOL}, 70 | OPT_COMPLETION: {}, 71 | OPT_GENERATE_MAN: {Type: options.BOOL}, 72 | } 73 | 74 | // ////////////////////////////////////////////////////////////////////////////////// // 75 | 76 | // Run is main application function 77 | func Run(gitRev string, gomod []byte) { 78 | preConfigureUI() 79 | 80 | args, errs := options.Parse(optMap) 81 | 82 | if !errs.IsEmpty() { 83 | term.Error("Options parsing errors:") 84 | term.Error(errs.Error(" - ")) 85 | os.Exit(1) 86 | } 87 | 88 | configureUI() 89 | 90 | switch { 91 | case options.Has(OPT_COMPLETION): 92 | os.Exit(printCompletion()) 93 | case options.Has(OPT_GENERATE_MAN): 94 | printMan() 95 | os.Exit(0) 96 | case options.GetB(OPT_VER): 97 | genAbout(gitRev).Print(options.GetS(OPT_VER)) 98 | os.Exit(0) 99 | case options.GetB(OPT_VERB_VER): 100 | support.Collect(APP, VER). 101 | WithRevision(gitRev). 102 | WithDeps(deps.Extract(gomod)). 103 | WithApps(apps.Bash()). 104 | Print() 105 | os.Exit(0) 106 | case options.GetB(OPT_HELP), len(args) == 0: 107 | genUsage().Print() 108 | os.Exit(0) 109 | } 110 | 111 | err := readDocs( 112 | args.Get(0).Clean().String(), 113 | args.Get(1).String(), 114 | ) 115 | 116 | if err != nil { 117 | term.Error(err) 118 | os.Exit(1) 119 | } 120 | } 121 | 122 | // ////////////////////////////////////////////////////////////////////////////////// // 123 | 124 | // preConfigureUI preconfigures UI based on information about user terminal 125 | func preConfigureUI() { 126 | if !tty.IsTTY() { 127 | fmtc.DisableColors = true 128 | } 129 | } 130 | 131 | // configureUI configures user interface 132 | func configureUI() { 133 | if options.GetB(OPT_NO_COLOR) { 134 | fmtc.DisableColors = true 135 | } 136 | } 137 | 138 | // readDocs reads the file and prints documentation from it 139 | func readDocs(file string, pattern string) error { 140 | err := fsutil.ValidatePerms("FRS", file) 141 | 142 | if err != nil { 143 | return err 144 | } 145 | 146 | doc, errs := parser.Parse(file) 147 | 148 | if !errs.IsEmpty() { 149 | term.Error("Shell script documentation parsing errors:") 150 | term.Error(errs.Error(" - ")) 151 | fmtc.NewLine() 152 | return fmt.Errorf("Can't parse script documentation") 153 | } 154 | 155 | if !doc.IsValid() { 156 | return fmt.Errorf("File %s doesn't contains any documentation", file) 157 | } 158 | 159 | if options.GetS(OPT_NAME) != "" { 160 | doc.Title = options.GetS(OPT_NAME) 161 | } 162 | 163 | if !options.Has(OPT_OUTPUT) { 164 | if !options.GetB(OPT_NO_PAGER) { 165 | if tty.IsTTY() { 166 | if pager.Setup() == nil { 167 | defer pager.Complete() 168 | } 169 | } 170 | } 171 | 172 | err = terminal.Render(doc, pattern) 173 | } else { 174 | 175 | err = template.Render( 176 | doc, 177 | options.GetS(OPT_TEMPLATE), 178 | options.GetS(OPT_OUTPUT), 179 | ) 180 | } 181 | 182 | return err 183 | } 184 | 185 | // ////////////////////////////////////////////////////////////////////////////////// // 186 | 187 | // printCompletion prints completion for given shell 188 | func printCompletion() int { 189 | info := genUsage() 190 | 191 | switch options.GetS(OPT_COMPLETION) { 192 | case "bash": 193 | fmt.Print(bash.Generate(info, "shdoc")) 194 | case "fish": 195 | fmt.Print(fish.Generate(info, "shdoc")) 196 | case "zsh": 197 | fmt.Print(zsh.Generate(info, optMap, "shdoc")) 198 | default: 199 | return 1 200 | } 201 | 202 | return 0 203 | } 204 | 205 | // printMan prints man page 206 | func printMan() { 207 | fmt.Println(man.Generate(genUsage(), genAbout(""))) 208 | } 209 | 210 | // genUsage generates usage info 211 | func genUsage() *usage.Info { 212 | info := usage.NewInfo("", "script") 213 | 214 | info.AddOption(OPT_OUTPUT, "Path to output file", "file") 215 | info.AddOption(OPT_TEMPLATE, "Name of template", "name") 216 | info.AddOption(OPT_NAME, "Overwrite default name", "name") 217 | info.AddOption(OPT_NO_PAGER, "Disable pager for long output") 218 | info.AddOption(OPT_NO_COLOR, "Disable colors in output") 219 | info.AddOption(OPT_HELP, "Show this help message") 220 | info.AddOption(OPT_VER, "Show version") 221 | 222 | info.AddExample( 223 | "script.sh", 224 | "Parse shell script and show documentation in console", 225 | ) 226 | 227 | info.AddExample( 228 | "script.sh -t markdown -o my_script.md", 229 | "Parse shell script and render documentation to markdown file", 230 | ) 231 | 232 | info.AddExample( 233 | "script.sh -t /path/to/template.tpl -o my_script.ext", 234 | "Parse shell script and render documentation with given template", 235 | ) 236 | 237 | info.AddExample( 238 | "script.sh myFunction", 239 | "Parse shell script and show documentation for some constant, variable or method", 240 | ) 241 | 242 | return info 243 | } 244 | 245 | // genAbout generates info about version 246 | func genAbout(gitRev string) *usage.About { 247 | about := &usage.About{ 248 | App: APP, 249 | Version: VER, 250 | Desc: DESC, 251 | Year: 2009, 252 | Owner: "ESSENTIAL KAOS", 253 | License: "Apache License, Version 2.0 ", 254 | } 255 | 256 | if gitRev != "" { 257 | about.Build = "git:" + gitRev 258 | about.UpdateChecker = usage.UpdateChecker{ 259 | "essentialkaos/shdoc", 260 | update.GitHubChecker, 261 | } 262 | } 263 | 264 | return about 265 | } 266 | -------------------------------------------------------------------------------- /script/script.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | // ////////////////////////////////////////////////////////////////////////////////// // 4 | // // 5 | // Copyright (c) 2025 ESSENTIAL KAOS // 6 | // Apache License, Version 2.0 // 7 | // // 8 | // ////////////////////////////////////////////////////////////////////////////////// // 9 | 10 | import "github.com/essentialkaos/ek/v13/mathutil" 11 | 12 | // ////////////////////////////////////////////////////////////////////////////////// // 13 | 14 | // VariableType contains variable type 15 | type VariableType uint8 16 | 17 | const ( 18 | VAR_TYPE_UNKNOWN VariableType = 0 19 | VAR_TYPE_STRING VariableType = 1 20 | VAR_TYPE_NUMBER VariableType = 2 21 | VAR_TYPE_BOOLEAN VariableType = 3 22 | ) 23 | 24 | // ////////////////////////////////////////////////////////////////////////////////// // 25 | 26 | const ( 27 | VAR_MOD_DEFAULT int = 0 28 | VAR_MOD_UPPERCASE int = 1 29 | VAR_MOD_LOWERCASE int = 2 30 | VAR_MOD_UPPERCASE_SHORT int = 3 31 | VAR_MOD_LOWERCASE_SHORT int = 4 32 | ) 33 | 34 | // ////////////////////////////////////////////////////////////////////////////////// // 35 | 36 | // Method contains info about method 37 | type Method struct { 38 | Name string `json:"name"` // Name 39 | Desc []string `json:"desc"` // Description 40 | Arguments []*Argument `json:"arguments"` // Arguments 41 | ResultCode bool `json:"result_code"` // Method uses exit codes 42 | ResultEcho *Variable `json:"result_echo"` // Return argument 43 | Example []string `json:"example"` // Example 44 | Line int `json:"line"` // LOC of definition 45 | } 46 | 47 | // Argument contains info about method argument 48 | type Argument struct { 49 | Index string `json:"index"` // Index 50 | Desc string `json:"desc"` // Desc 51 | Type VariableType `json:"type"` // Type 52 | IsOptional bool `json:"optional"` // Optional 53 | IsWildcard bool `json:"wildcard"` // Wildcard 54 | } 55 | 56 | // Variable contains info about variable 57 | type Variable struct { 58 | Name string `json:"name"` // Name 59 | Desc []string `json:"desc"` // Description 60 | Type VariableType `json:"type"` // Type 61 | Value string `json:"value"` // Value 62 | Line int `json:"line"` // LOC of definition 63 | } 64 | 65 | // Document contains info about all constants, global variables and methods 66 | type Document struct { 67 | Title string `json:"title"` 68 | About []string `json:"about"` 69 | Constants []*Variable `json:"constants"` 70 | Variables []*Variable `json:"variables"` 71 | Methods []*Method `json:"methods"` 72 | } 73 | 74 | // ////////////////////////////////////////////////////////////////////////////////// // 75 | 76 | var ( 77 | typeNameString = []string{"String", "string", "STRING", "S", "s"} 78 | typeNameNumber = []string{"Number", "number", "NUMBER", "N", "n"} 79 | typeNameBoolean = []string{"Boolean", "boolean", "BOOLEAN", "B", "b"} 80 | ) 81 | 82 | // ////////////////////////////////////////////////////////////////////////////////// // 83 | 84 | // IsValid return false if document is nil or doesn't have any content 85 | func (d *Document) IsValid() bool { 86 | switch { 87 | case d == nil: 88 | return false 89 | case len(d.Constants) != 0, len(d.Variables) != 0, len(d.Methods) != 0: 90 | return true 91 | } 92 | 93 | return false 94 | } 95 | 96 | // HasAbout return true if about is present 97 | func (d *Document) HasAbout() bool { 98 | if d == nil { 99 | return false 100 | } 101 | 102 | return d.About != nil 103 | } 104 | 105 | // HasConstants return true if doc has constants info 106 | func (d *Document) HasConstants() bool { 107 | if d == nil { 108 | return false 109 | } 110 | 111 | return d.Constants != nil 112 | } 113 | 114 | // HasVariables return true if doc has global variables info 115 | func (d *Document) HasVariables() bool { 116 | if d == nil { 117 | return false 118 | } 119 | 120 | return d.Variables != nil 121 | } 122 | 123 | // HasMethods return true if doc has methods info 124 | func (d *Document) HasMethods() bool { 125 | if d == nil { 126 | return false 127 | } 128 | 129 | return d.Methods != nil 130 | } 131 | 132 | // TypeDesc return type description 133 | func (a *Argument) TypeName(mod int) string { 134 | if a == nil { 135 | return "" 136 | } 137 | 138 | return getTypeName(a.Type, mod) 139 | } 140 | 141 | // IsString return true if type is string 142 | func (a *Argument) IsString() bool { 143 | if a == nil { 144 | return false 145 | } 146 | 147 | return a.Type == VAR_TYPE_STRING 148 | } 149 | 150 | // IsNumber return true if type is number 151 | func (a *Argument) IsNumber() bool { 152 | if a == nil { 153 | return false 154 | } 155 | 156 | return a.Type == VAR_TYPE_NUMBER 157 | } 158 | 159 | // IsBoolean return true if type is boolean 160 | func (a *Argument) IsBoolean() bool { 161 | if a == nil { 162 | return false 163 | } 164 | 165 | return a.Type == VAR_TYPE_BOOLEAN 166 | } 167 | 168 | // IsUnknown return true if type is unknown 169 | func (a *Argument) IsUnknown() bool { 170 | if a == nil { 171 | return false 172 | } 173 | 174 | return a.Type == VAR_TYPE_UNKNOWN 175 | } 176 | 177 | // TypeDesc return type description 178 | func (v *Variable) TypeName(mod int) string { 179 | if v == nil { 180 | return "" 181 | } 182 | 183 | return getTypeName(v.Type, mod) 184 | } 185 | 186 | // IsString return true if type is string 187 | func (v *Variable) IsString() bool { 188 | if v == nil { 189 | return false 190 | } 191 | 192 | return v.Type == VAR_TYPE_STRING 193 | } 194 | 195 | // IsNumber return true if type is number 196 | func (v *Variable) IsNumber() bool { 197 | if v == nil { 198 | return false 199 | } 200 | 201 | return v.Type == VAR_TYPE_NUMBER 202 | } 203 | 204 | // IsBoolean return true if type is boolean 205 | func (v *Variable) IsBoolean() bool { 206 | if v == nil { 207 | return false 208 | } 209 | 210 | return v.Type == VAR_TYPE_BOOLEAN 211 | } 212 | 213 | // IsUnknown return true if type is unknown 214 | func (v *Variable) IsUnknown() bool { 215 | if v == nil { 216 | return false 217 | } 218 | 219 | return v.Type == VAR_TYPE_UNKNOWN 220 | } 221 | 222 | // UnitedDesc return united description string 223 | func (v *Variable) UnitedDesc() string { 224 | if v == nil { 225 | return "" 226 | } 227 | 228 | return mergeDesc(v.Desc) 229 | } 230 | 231 | // HasArguments return true if method has arguments 232 | func (m *Method) HasArguments() bool { 233 | if m == nil { 234 | return false 235 | } 236 | 237 | return m.Arguments != nil 238 | } 239 | 240 | // HasEcho return true if method echoed some data 241 | func (m *Method) HasEcho() bool { 242 | if m == nil { 243 | return false 244 | } 245 | 246 | return m.ResultEcho != nil 247 | } 248 | 249 | // HasExample return true if method has code usage example 250 | func (m *Method) HasExample() bool { 251 | if m == nil { 252 | return false 253 | } 254 | 255 | return m.Example != nil 256 | } 257 | 258 | // UnitedDesc return united description string 259 | func (m *Method) UnitedDesc() string { 260 | if m == nil { 261 | return "" 262 | } 263 | 264 | return mergeDesc(m.Desc) 265 | } 266 | 267 | // ////////////////////////////////////////////////////////////////////////////////// // 268 | 269 | // getTypeName returns variable type name 270 | func getTypeName(t VariableType, mod int) string { 271 | mod = mathutil.Between(mod, 0, 4) 272 | 273 | switch t { 274 | case VAR_TYPE_STRING: 275 | return typeNameString[mod] 276 | case VAR_TYPE_NUMBER: 277 | return typeNameNumber[mod] 278 | case VAR_TYPE_BOOLEAN: 279 | return typeNameBoolean[mod] 280 | default: 281 | return "" 282 | } 283 | } 284 | 285 | // mergeDesc merges description lines to one string 286 | func mergeDesc(data []string) string { 287 | var result string 288 | 289 | dataLen := len(data) 290 | 291 | for index, line := range data { 292 | if line == "" { 293 | continue 294 | } 295 | 296 | result += line 297 | 298 | if index < dataLen-1 { 299 | result += " " 300 | } 301 | } 302 | 303 | return result 304 | } 305 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // ////////////////////////////////////////////////////////////////////////////////// // 4 | // // 5 | // Copyright (c) 2025 ESSENTIAL KAOS // 6 | // Apache License, Version 2.0 // 7 | // // 8 | // ////////////////////////////////////////////////////////////////////////////////// // 9 | 10 | import ( 11 | "bufio" 12 | "fmt" 13 | "io" 14 | "os" 15 | "path/filepath" 16 | "regexp" 17 | "slices" 18 | "strings" 19 | 20 | "github.com/essentialkaos/ek/v13/errors" 21 | "github.com/essentialkaos/ek/v13/strutil" 22 | 23 | "github.com/essentialkaos/shdoc/script" 24 | ) 25 | 26 | // ////////////////////////////////////////////////////////////////////////////////// // 27 | 28 | type EntityType uint8 29 | 30 | const ( 31 | ENT_TYPE_UNKNOWN EntityType = 0 32 | ENT_TYPE_METHOD EntityType = 1 33 | ENT_TYPE_VARIABLE EntityType = 2 34 | ENT_TYPE_CONSTANT EntityType = 3 35 | ) 36 | 37 | // ////////////////////////////////////////////////////////////////////////////////// // 38 | 39 | var ( 40 | methodRegExp = regexp.MustCompile(`^([a-zA-Z0-9._]{1,})\(\)`) 41 | variableRegExp = regexp.MustCompile(`^([a-zA-Z0-9_.\[\]]{1,})=(.*)$`) 42 | constantRegExp = regexp.MustCompile(`^[A-Z0-9_]{1,}$`) 43 | numberRegExp = regexp.MustCompile(`^[0-9]{1,}$`) 44 | typeCommentRegExp = regexp.MustCompile(`^(.*) \((Boolean|String|Number)\)`) 45 | methodArgRegExp = regexp.MustCompile(`([0-9]{1,}|\*):[ ]{0,}(.*)`) 46 | negativeValRegexp = regexp.MustCompile(`^((N|n)one|(N|n)o(t|)|(F|f)alse)`) 47 | 48 | shellcheckRegexp = regexp.MustCompile(`\# +shellcheck +disable\=`) 49 | ) 50 | 51 | var ignoreTags = []string{"private", "PRIVATE", "-"} 52 | 53 | // ////////////////////////////////////////////////////////////////////////////////// // 54 | 55 | // Parse method parse given file and return document struct and slice with errors 56 | func Parse(file string) (*script.Document, errors.Errors) { 57 | fd, err := os.OpenFile(file, os.O_RDONLY, 0) 58 | 59 | if err != nil { 60 | return nil, errors.Errors{fmt.Errorf("Can't open script file: %v", err)} 61 | } 62 | 63 | defer fd.Close() 64 | 65 | return readData(file, bufio.NewReader(fd)) 66 | } 67 | 68 | // ////////////////////////////////////////////////////////////////////////////////// // 69 | 70 | // readData reads file data 71 | func readData(file string, reader io.Reader) (*script.Document, errors.Errors) { 72 | scanner := bufio.NewScanner(reader) 73 | 74 | var buffer []string 75 | var methodsSection bool 76 | var lineNum int 77 | 78 | doc := &script.Document{Title: filepath.Base(file)} 79 | 80 | for scanner.Scan() { 81 | line := scanner.Text() 82 | line = strings.TrimLeft(line, " ") 83 | 84 | lineNum++ 85 | 86 | if lineNum == 1 || shellcheckRegexp.MatchString(line) { 87 | continue 88 | } 89 | 90 | if line == "" { 91 | if buffer != nil && !doc.IsValid() { 92 | doc.About = getCleanData(buffer) 93 | } 94 | 95 | buffer = nil 96 | continue 97 | } 98 | 99 | if strings.Trim(line, "#") == "" { 100 | if buffer != nil { 101 | buffer = append(buffer, "") 102 | } 103 | 104 | continue 105 | } 106 | 107 | if line[0] == '#' { 108 | buffer = append(buffer, line[2:]) 109 | continue 110 | } 111 | 112 | t, name, value := parseEntity(line) 113 | 114 | if t == ENT_TYPE_UNKNOWN || len(buffer) == 0 { 115 | buffer = nil 116 | continue 117 | } 118 | 119 | // Ignore all var definitions after first method 120 | if t != ENT_TYPE_METHOD && methodsSection { 121 | buffer = nil 122 | continue 123 | } 124 | 125 | switch t { 126 | case ENT_TYPE_METHOD: 127 | m := parseMethodComment(name, buffer) 128 | 129 | if m == nil { 130 | buffer = nil 131 | continue 132 | } 133 | 134 | m.Line = lineNum 135 | 136 | // Methods MUST have description 137 | if len(m.Desc) != 0 { 138 | doc.Methods = append(doc.Methods, m) 139 | } 140 | 141 | if !methodsSection { 142 | methodsSection = true 143 | } 144 | 145 | buffer = nil 146 | 147 | case ENT_TYPE_VARIABLE, ENT_TYPE_CONSTANT: 148 | v := parseVariableComment(name, value, buffer) 149 | 150 | if v == nil { 151 | buffer = nil 152 | continue 153 | } 154 | 155 | // Append multiline parts to value 156 | if isMultilineValue(value) { 157 | MULTIPART: 158 | for scanner.Scan() { 159 | valuePart := scanner.Text() 160 | 161 | v.Value += valuePart 162 | 163 | if strutil.Tail(valuePart, 1) == "\"" { 164 | break MULTIPART 165 | } 166 | } 167 | } 168 | 169 | v.Line = lineNum 170 | 171 | // Variables MUST have description 172 | if len(v.Desc) != 0 { 173 | if t == ENT_TYPE_VARIABLE { 174 | doc.Variables = append(doc.Variables, v) 175 | } else { 176 | doc.Constants = append(doc.Constants, v) 177 | } 178 | } 179 | 180 | buffer = nil 181 | } 182 | } 183 | 184 | return doc, nil 185 | } 186 | 187 | // parseEntity method parse entity and return type, name and value of 188 | // entity 189 | func parseEntity(data string) (EntityType, string, string) { 190 | if methodRegExp.MatchString(data) { 191 | md := methodRegExp.FindStringSubmatch(data) 192 | return ENT_TYPE_METHOD, md[1], "" 193 | } 194 | 195 | if variableRegExp.MatchString(data) { 196 | vd := variableRegExp.FindStringSubmatch(data) 197 | 198 | if constantRegExp.MatchString(vd[1]) { 199 | return ENT_TYPE_CONSTANT, vd[1], vd[2] 200 | } 201 | 202 | return ENT_TYPE_VARIABLE, vd[1], vd[2] 203 | } 204 | 205 | return ENT_TYPE_UNKNOWN, "", "" 206 | } 207 | 208 | // parseVariableComment method parse variable comment data and return 209 | // variable struct 210 | func parseVariableComment(name, value string, data []string) *script.Variable { 211 | if len(data) == 0 || slices.Contains(ignoreTags, strings.TrimRight(data[0], " ")) { 212 | return nil 213 | } 214 | 215 | variable := &script.Variable{ 216 | Name: name, 217 | Value: value, 218 | } 219 | 220 | data, t := getVariableType(data) 221 | 222 | if t == script.VAR_TYPE_UNKNOWN { 223 | t = guessVariableType(value) 224 | } 225 | 226 | variable.Type = t 227 | variable.Desc = data 228 | 229 | return variable 230 | } 231 | 232 | // parseMethodComment method parse method comment data and return 233 | // method struct 234 | func parseMethodComment(name string, data []string) *script.Method { 235 | if len(data) == 0 || slices.Contains(ignoreTags, strings.TrimRight(data[0], " ")) { 236 | return nil 237 | } 238 | 239 | method := &script.Method{Name: name} 240 | 241 | for index, line := range data { 242 | if methodArgRegExp.MatchString(line) { 243 | if method.Desc == nil { 244 | method.Desc = extractMethodDesc(data, index) 245 | } 246 | 247 | method.Arguments = append(method.Arguments, parseArgumentComment(line)) 248 | 249 | continue 250 | } 251 | 252 | if strings.HasPrefix(line, "Code:") { 253 | if method.Desc == nil { 254 | method.Desc = extractMethodDesc(data, index) 255 | } 256 | 257 | retValue := strutil.Substr(line, 6, 99999) 258 | 259 | if negativeValRegexp.MatchString(retValue) { 260 | continue 261 | } 262 | 263 | method.ResultCode = true 264 | } 265 | 266 | if strings.HasPrefix(line, "Echo:") { 267 | if method.Desc == nil { 268 | method.Desc = extractMethodDesc(data, index) 269 | } 270 | 271 | echoValue := strutil.Substr(line, 6, 99999) 272 | 273 | if negativeValRegexp.MatchString(echoValue) { 274 | continue 275 | } 276 | 277 | method.ResultEcho = parseVariableComment("", "", []string{echoValue}) 278 | } 279 | 280 | if strings.HasPrefix(line, "Example:") { 281 | if method.Desc == nil { 282 | method.Desc = extractMethodDesc(data, index) 283 | } 284 | 285 | method.Example = getCleanData(data[index+1:]) 286 | break // Example is last part of comment 287 | } 288 | } 289 | 290 | if method.Desc == nil { 291 | method.Desc = extractMethodDesc(data, len(data)) 292 | } 293 | 294 | return method 295 | } 296 | 297 | // extractMethodDesc return description from all comment data 298 | func extractMethodDesc(data []string, index int) []string { 299 | if len(data) <= index { 300 | return getCleanData(data) 301 | } 302 | 303 | return getCleanData(data[:index]) 304 | } 305 | 306 | // parseArgumentComment method parse given comment data and return 307 | // argument struct 308 | func parseArgumentComment(data string) *script.Argument { 309 | argument := &script.Argument{} 310 | 311 | ar := methodArgRegExp.FindStringSubmatch(data) 312 | 313 | argument.Index = ar[1] 314 | 315 | if argument.Index == "*" { 316 | argument.IsWildcard = true 317 | } 318 | 319 | ds := strings.Split(ar[2], " ") 320 | 321 | var desc []string 322 | 323 | for _, word := range ds { 324 | switch word { 325 | case "(Boolean)": 326 | argument.Type = script.VAR_TYPE_BOOLEAN 327 | case "(Number)": 328 | argument.Type = script.VAR_TYPE_NUMBER 329 | case "(String)": 330 | argument.Type = script.VAR_TYPE_STRING 331 | case "[Optional]": 332 | argument.IsOptional = true 333 | default: 334 | desc = append(desc, word) 335 | } 336 | } 337 | 338 | argument.Desc = strings.Join(desc, " ") 339 | 340 | return argument 341 | } 342 | 343 | // guessVariableType try to guess variable type by value 344 | func guessVariableType(data string) script.VariableType { 345 | if data == "" { 346 | return script.VAR_TYPE_UNKNOWN 347 | } 348 | 349 | if data == "true" { 350 | return script.VAR_TYPE_BOOLEAN 351 | } 352 | 353 | if numberRegExp.MatchString(data) { 354 | return script.VAR_TYPE_NUMBER 355 | } 356 | 357 | return script.VAR_TYPE_STRING 358 | } 359 | 360 | // getVariableType search in comment info about variable type and 361 | // return data without comment and varuable type 362 | func getVariableType(data []string) ([]string, script.VariableType) { 363 | var result []string 364 | var resultType script.VariableType 365 | 366 | for _, line := range data { 367 | if resultType == script.VAR_TYPE_UNKNOWN { 368 | if typeCommentRegExp.MatchString(line) { 369 | cd := typeCommentRegExp.FindStringSubmatch(line) 370 | 371 | // Append to result first regexp group contains 372 | // description without type marker 373 | result = append(result, cd[1]) 374 | 375 | switch cd[2] { 376 | case "Boolean": 377 | resultType = script.VAR_TYPE_BOOLEAN 378 | 379 | case "Number": 380 | resultType = script.VAR_TYPE_NUMBER 381 | 382 | default: 383 | resultType = script.VAR_TYPE_STRING 384 | } 385 | 386 | continue 387 | } 388 | } 389 | 390 | result = append(result, line) 391 | } 392 | 393 | return getCleanData(result), resultType 394 | } 395 | 396 | // getCleanData return removes empty lines and whitespaces at the 397 | // end of the line 398 | func getCleanData(data []string) []string { 399 | if len(data) == 0 { 400 | return data 401 | } 402 | 403 | var result []string 404 | 405 | lastNonEmptyIndex := 0 406 | 407 | // Search index of last non empty line 408 | for index, line := range data { 409 | if line != "" { 410 | lastNonEmptyIndex = index 411 | } 412 | } 413 | 414 | // Make result slice with non empty lines without whitespaces 415 | // at end of the line 416 | for i := 0; i <= lastNonEmptyIndex; i++ { 417 | result = append(result, strings.TrimRight(data[i], " ")) 418 | } 419 | 420 | return result 421 | } 422 | 423 | // isMultilineValue return true if value is multiline string 424 | func isMultilineValue(value string) bool { 425 | if strutil.Head(value, 1) == "\"" && strutil.Tail(value, 1) != "\"" { 426 | return true 427 | } 428 | 429 | return false 430 | } 431 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 ESSENTIAL KAOS 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // ////////////////////////////////////////////////////////////////////////////////// // 4 | // // 5 | // Copyright (c) 2025 ESSENTIAL KAOS // 6 | // Apache License, Version 2.0 // 7 | // // 8 | // ////////////////////////////////////////////////////////////////////////////////// // 9 | 10 | import ( 11 | "os" 12 | "testing" 13 | 14 | "github.com/essentialkaos/shdoc/script" 15 | 16 | . "github.com/essentialkaos/check" 17 | ) 18 | 19 | // ////////////////////////////////////////////////////////////////////////////////// // 20 | 21 | const _SCRIPT = `#!/bin/bash 22 | # shellcheck disable=SC1117 23 | 24 | # This is example of shell script. 25 | # Second line of about info. 26 | # 27 | # Third line of about info. 28 | # 29 | # 30 | 31 | ############################################################################### 32 | 33 | # Constant #1 without type 34 | # Second line of description 35 | CONST_1="" 36 | 37 | # Constant #2 without type 38 | CONST_2=0 39 | 40 | # Constant #3 without type 41 | CONST_3=true 42 | 43 | # Constant #4 with type (String) 44 | CONST_4="" 45 | 46 | # Constant #5 with type (Number) 47 | CONST_5="" 48 | 49 | # Constant #6 with type (Boolean) 50 | CONST_6="" 51 | 52 | # - 53 | # Private constant 54 | CONST_7="" 55 | 56 | ############################################################################### 57 | 58 | # Variable #1 without type 59 | # Second line of description 60 | var_1="" 61 | 62 | # Variable #2 without type 63 | var_2=1 64 | 65 | # Variable #3 without type 66 | var_3=true 67 | 68 | # Variable #4 with type (String) 69 | var_4="" 70 | 71 | # Variable #5 with type (Number) 72 | var_5="" 73 | 74 | # Variable #6 with type (Boolean) 75 | var_6="" 76 | 77 | # Variable #7 without value 78 | # shellcheck disable=SC1117 79 | var_7= 80 | 81 | # Variable #8 with multiline value 82 | var_8="This is 83 | multiline 84 | value" 85 | 86 | # - 87 | # Private variable 88 | var_9="" 89 | 90 | ############################################################################### 91 | 92 | # This is desc for method #1. 93 | # Second line of description. 94 | # Third line of description. 95 | # 96 | # Fourth line of description. 97 | # 98 | # Code: none 99 | # Echo: none 100 | method1() { 101 | stub=1 102 | # Must be ignored 103 | stub=2 104 | } 105 | 106 | # This is desc for method #2. 107 | # 108 | # 1: First argument (String) 109 | # 2: Second argument (Number) 110 | # 3: Third argument (Boolean) 111 | # *: Wildcard argument 112 | # 113 | # Code: Yes 114 | # Echo: Magic value (Boolean) 115 | # 116 | # Example: 117 | # if [[ -f $file ]] ; then 118 | # method2 123 119 | # fi 120 | # 121 | method2() { 122 | stub=1 123 | } 124 | 125 | # This is desc for method #3. 126 | # 127 | # 128 | # 1: First argument (String) [Optional] 129 | # 130 | # Code: No 131 | # Echo: No 132 | method3() { 133 | stub=1 134 | } 135 | 136 | # This is desc for method #4. 137 | method4() { 138 | stub=1 139 | } 140 | 141 | # This is desc for method #5. 142 | # Echo: none 143 | method5() { 144 | stub=1 145 | } 146 | 147 | # This is desc for method #6. 148 | # Code: none 149 | method6() { 150 | stub=1 151 | } 152 | 153 | # This is desc for method #7. 154 | # 155 | # 1: First argument 156 | method7() { 157 | stub=1 158 | } 159 | 160 | # This is desc for method #8. 161 | # 162 | # Example: 163 | # method8 123 164 | method8() { 165 | stub=1 166 | } 167 | 168 | # 169 | # This is desc for method #9. 170 | method9() { 171 | stub=1 172 | } 173 | 174 | # Code: Yes 175 | method10() { 176 | stub=1 177 | } 178 | 179 | # Echo: Magic value (Number) 180 | method11() { 181 | stub=1 182 | } 183 | 184 | # - 185 | # 186 | # This is private method 187 | method12() { 188 | stub=1 189 | } 190 | 191 | # 1: () 192 | # Code: 193 | # Echo: 194 | # Example: 195 | method13(){ 196 | stub=1 197 | } 198 | ` 199 | 200 | // ////////////////////////////////////////////////////////////////////////////////// // 201 | 202 | func Test(t *testing.T) { TestingT(t) } 203 | 204 | // ////////////////////////////////////////////////////////////////////////////////// // 205 | 206 | type ParseSuite struct { 207 | TmpDir string 208 | } 209 | 210 | // ////////////////////////////////////////////////////////////////////////////////// // 211 | 212 | var _ = Suite(&ParseSuite{}) 213 | 214 | // ////////////////////////////////////////////////////////////////////////////////// // 215 | 216 | func (s *ParseSuite) SetUpSuite(c *C) { 217 | s.TmpDir = c.MkDir() 218 | 219 | err := os.WriteFile(s.TmpDir+"/script.sh", []byte(_SCRIPT), 0644) 220 | 221 | if err != nil { 222 | c.Fatal(err.Error()) 223 | } 224 | } 225 | 226 | func (s *ParseSuite) TestErrors(c *C) { 227 | doc, errs := Parse(s.TmpDir + "/script1.sh") 228 | 229 | c.Assert(doc, IsNil) 230 | c.Assert(errs, Not(HasLen), 0) 231 | } 232 | 233 | func (s *ParseSuite) TestParsing(c *C) { 234 | doc, errs := Parse(s.TmpDir + "/script.sh") 235 | 236 | c.Assert(doc, NotNil) 237 | c.Assert(errs, HasLen, 0) 238 | 239 | c.Assert(doc.IsValid(), Equals, true) 240 | c.Assert(doc.HasAbout(), Equals, true) 241 | c.Assert(doc.HasConstants(), Equals, true) 242 | c.Assert(doc.HasVariables(), Equals, true) 243 | c.Assert(doc.HasMethods(), Equals, true) 244 | 245 | c.Assert(doc.Title, Equals, "script.sh") 246 | 247 | c.Assert(doc.About, HasLen, 4) 248 | c.Assert(doc.About[0], Equals, "This is example of shell script.") 249 | c.Assert(doc.About[1], Equals, "Second line of about info.") 250 | c.Assert(doc.About[2], Equals, "") 251 | c.Assert(doc.About[3], Equals, "Third line of about info.") 252 | 253 | c.Assert(doc.Constants, HasLen, 6) 254 | c.Assert(doc.Variables, HasLen, 8) 255 | c.Assert(doc.Methods, HasLen, 9) 256 | 257 | // //////////////////////////////////////////////////////////////////////////////// // 258 | 259 | c.Assert(doc.Constants[0], NotNil) 260 | c.Assert(doc.Constants[0].Name, Equals, "CONST_1") 261 | c.Assert(doc.Constants[0].Desc, DeepEquals, []string{"Constant #1 without type", "Second line of description"}) 262 | c.Assert(doc.Constants[0].Type, Equals, script.VariableType(script.VAR_TYPE_STRING)) 263 | c.Assert(doc.Constants[0].Value, Equals, "\"\"") 264 | c.Assert(doc.Constants[0].Line, Equals, 15) 265 | c.Assert(doc.Constants[0].UnitedDesc(), Equals, "Constant #1 without type Second line of description") 266 | c.Assert(doc.Constants[0].IsString(), Equals, true) 267 | c.Assert(doc.Constants[0].IsNumber(), Equals, false) 268 | c.Assert(doc.Constants[0].IsBoolean(), Equals, false) 269 | c.Assert(doc.Constants[0].IsUnknown(), Equals, false) 270 | c.Assert(doc.Constants[0].TypeName(0), Equals, "String") 271 | c.Assert(doc.Constants[0].TypeName(1), Equals, "string") 272 | c.Assert(doc.Constants[0].TypeName(2), Equals, "STRING") 273 | c.Assert(doc.Constants[0].TypeName(3), Equals, "S") 274 | c.Assert(doc.Constants[0].TypeName(4), Equals, "s") 275 | 276 | c.Assert(doc.Constants[1], NotNil) 277 | c.Assert(doc.Constants[1].Name, Equals, "CONST_2") 278 | c.Assert(doc.Constants[1].Desc, DeepEquals, []string{"Constant #2 without type"}) 279 | c.Assert(doc.Constants[1].Type, Equals, script.VariableType(script.VAR_TYPE_NUMBER)) 280 | c.Assert(doc.Constants[1].Value, Equals, "0") 281 | c.Assert(doc.Constants[1].Line, Equals, 18) 282 | c.Assert(doc.Constants[1].UnitedDesc(), Equals, "Constant #2 without type") 283 | c.Assert(doc.Constants[1].IsString(), Equals, false) 284 | c.Assert(doc.Constants[1].IsNumber(), Equals, true) 285 | c.Assert(doc.Constants[1].IsBoolean(), Equals, false) 286 | c.Assert(doc.Constants[1].IsUnknown(), Equals, false) 287 | c.Assert(doc.Constants[1].TypeName(0), Equals, "Number") 288 | c.Assert(doc.Constants[1].TypeName(1), Equals, "number") 289 | c.Assert(doc.Constants[1].TypeName(2), Equals, "NUMBER") 290 | c.Assert(doc.Constants[1].TypeName(3), Equals, "N") 291 | c.Assert(doc.Constants[1].TypeName(4), Equals, "n") 292 | 293 | c.Assert(doc.Constants[2], NotNil) 294 | c.Assert(doc.Constants[2].Name, Equals, "CONST_3") 295 | c.Assert(doc.Constants[2].Desc, DeepEquals, []string{"Constant #3 without type"}) 296 | c.Assert(doc.Constants[2].Type, Equals, script.VariableType(script.VAR_TYPE_BOOLEAN)) 297 | c.Assert(doc.Constants[2].Value, Equals, "true") 298 | c.Assert(doc.Constants[2].Line, Equals, 21) 299 | c.Assert(doc.Constants[2].UnitedDesc(), Equals, "Constant #3 without type") 300 | c.Assert(doc.Constants[2].IsString(), Equals, false) 301 | c.Assert(doc.Constants[2].IsNumber(), Equals, false) 302 | c.Assert(doc.Constants[2].IsBoolean(), Equals, true) 303 | c.Assert(doc.Constants[2].IsUnknown(), Equals, false) 304 | c.Assert(doc.Constants[2].TypeName(0), Equals, "Boolean") 305 | c.Assert(doc.Constants[2].TypeName(1), Equals, "boolean") 306 | c.Assert(doc.Constants[2].TypeName(2), Equals, "BOOLEAN") 307 | c.Assert(doc.Constants[2].TypeName(3), Equals, "B") 308 | c.Assert(doc.Constants[2].TypeName(4), Equals, "b") 309 | 310 | c.Assert(doc.Constants[3], NotNil) 311 | c.Assert(doc.Constants[3].Name, Equals, "CONST_4") 312 | c.Assert(doc.Constants[3].Desc, DeepEquals, []string{"Constant #4 with type"}) 313 | c.Assert(doc.Constants[3].Type, Equals, script.VariableType(script.VAR_TYPE_STRING)) 314 | c.Assert(doc.Constants[3].Value, Equals, "\"\"") 315 | c.Assert(doc.Constants[3].Line, Equals, 24) 316 | 317 | c.Assert(doc.Constants[4], NotNil) 318 | c.Assert(doc.Constants[4].Name, Equals, "CONST_5") 319 | c.Assert(doc.Constants[4].Desc, DeepEquals, []string{"Constant #5 with type"}) 320 | c.Assert(doc.Constants[4].Type, Equals, script.VariableType(script.VAR_TYPE_NUMBER)) 321 | c.Assert(doc.Constants[4].Value, Equals, "\"\"") 322 | c.Assert(doc.Constants[4].Line, Equals, 27) 323 | 324 | c.Assert(doc.Constants[5], NotNil) 325 | c.Assert(doc.Constants[5].Name, Equals, "CONST_6") 326 | c.Assert(doc.Constants[5].Desc, DeepEquals, []string{"Constant #6 with type"}) 327 | c.Assert(doc.Constants[5].Type, Equals, script.VariableType(script.VAR_TYPE_BOOLEAN)) 328 | c.Assert(doc.Constants[5].Value, Equals, "\"\"") 329 | c.Assert(doc.Constants[5].Line, Equals, 30) 330 | 331 | // //////////////////////////////////////////////////////////////////////////////// // 332 | 333 | c.Assert(doc.Variables[0], NotNil) 334 | c.Assert(doc.Variables[0].Name, Equals, "var_1") 335 | c.Assert(doc.Variables[0].Desc, DeepEquals, []string{"Variable #1 without type", "Second line of description"}) 336 | c.Assert(doc.Variables[0].Type, Equals, script.VariableType(script.VAR_TYPE_STRING)) 337 | c.Assert(doc.Variables[0].Value, Equals, "\"\"") 338 | c.Assert(doc.Variables[0].Line, Equals, 40) 339 | c.Assert(doc.Variables[0].UnitedDesc(), Equals, "Variable #1 without type Second line of description") 340 | c.Assert(doc.Variables[0].IsString(), Equals, true) 341 | c.Assert(doc.Variables[0].IsNumber(), Equals, false) 342 | c.Assert(doc.Variables[0].IsBoolean(), Equals, false) 343 | c.Assert(doc.Variables[0].IsUnknown(), Equals, false) 344 | c.Assert(doc.Variables[0].TypeName(0), Equals, "String") 345 | c.Assert(doc.Variables[0].TypeName(1), Equals, "string") 346 | c.Assert(doc.Variables[0].TypeName(2), Equals, "STRING") 347 | c.Assert(doc.Variables[0].TypeName(3), Equals, "S") 348 | c.Assert(doc.Variables[0].TypeName(4), Equals, "s") 349 | 350 | c.Assert(doc.Variables[1], NotNil) 351 | c.Assert(doc.Variables[1].Name, Equals, "var_2") 352 | c.Assert(doc.Variables[1].Desc, DeepEquals, []string{"Variable #2 without type"}) 353 | c.Assert(doc.Variables[1].Type, Equals, script.VariableType(script.VAR_TYPE_NUMBER)) 354 | c.Assert(doc.Variables[1].Value, Equals, "1") 355 | c.Assert(doc.Variables[1].Line, Equals, 43) 356 | c.Assert(doc.Variables[1].UnitedDesc(), Equals, "Variable #2 without type") 357 | c.Assert(doc.Variables[1].IsString(), Equals, false) 358 | c.Assert(doc.Variables[1].IsNumber(), Equals, true) 359 | c.Assert(doc.Variables[1].IsBoolean(), Equals, false) 360 | c.Assert(doc.Variables[1].IsUnknown(), Equals, false) 361 | c.Assert(doc.Variables[1].TypeName(0), Equals, "Number") 362 | c.Assert(doc.Variables[1].TypeName(1), Equals, "number") 363 | c.Assert(doc.Variables[1].TypeName(2), Equals, "NUMBER") 364 | c.Assert(doc.Variables[1].TypeName(3), Equals, "N") 365 | c.Assert(doc.Variables[1].TypeName(4), Equals, "n") 366 | 367 | c.Assert(doc.Variables[2], NotNil) 368 | c.Assert(doc.Variables[2].Name, Equals, "var_3") 369 | c.Assert(doc.Variables[2].Desc, DeepEquals, []string{"Variable #3 without type"}) 370 | c.Assert(doc.Variables[2].Type, Equals, script.VariableType(script.VAR_TYPE_BOOLEAN)) 371 | c.Assert(doc.Variables[2].Value, Equals, "true") 372 | c.Assert(doc.Variables[2].Line, Equals, 46) 373 | c.Assert(doc.Variables[2].UnitedDesc(), Equals, "Variable #3 without type") 374 | c.Assert(doc.Variables[2].IsString(), Equals, false) 375 | c.Assert(doc.Variables[2].IsNumber(), Equals, false) 376 | c.Assert(doc.Variables[2].IsBoolean(), Equals, true) 377 | c.Assert(doc.Variables[2].IsUnknown(), Equals, false) 378 | c.Assert(doc.Variables[2].TypeName(0), Equals, "Boolean") 379 | c.Assert(doc.Variables[2].TypeName(1), Equals, "boolean") 380 | c.Assert(doc.Variables[2].TypeName(2), Equals, "BOOLEAN") 381 | c.Assert(doc.Variables[2].TypeName(3), Equals, "B") 382 | c.Assert(doc.Variables[2].TypeName(4), Equals, "b") 383 | 384 | c.Assert(doc.Variables[3], NotNil) 385 | c.Assert(doc.Variables[3].Name, Equals, "var_4") 386 | c.Assert(doc.Variables[3].Desc, DeepEquals, []string{"Variable #4 with type"}) 387 | c.Assert(doc.Variables[3].Type, Equals, script.VariableType(script.VAR_TYPE_STRING)) 388 | c.Assert(doc.Variables[3].Value, Equals, "\"\"") 389 | c.Assert(doc.Variables[3].Line, Equals, 49) 390 | 391 | c.Assert(doc.Variables[4], NotNil) 392 | c.Assert(doc.Variables[4].Name, Equals, "var_5") 393 | c.Assert(doc.Variables[4].Desc, DeepEquals, []string{"Variable #5 with type"}) 394 | c.Assert(doc.Variables[4].Type, Equals, script.VariableType(script.VAR_TYPE_NUMBER)) 395 | c.Assert(doc.Variables[4].Value, Equals, "\"\"") 396 | c.Assert(doc.Variables[4].Line, Equals, 52) 397 | 398 | c.Assert(doc.Variables[5], NotNil) 399 | c.Assert(doc.Variables[5].Name, Equals, "var_6") 400 | c.Assert(doc.Variables[5].Desc, DeepEquals, []string{"Variable #6 with type"}) 401 | c.Assert(doc.Variables[5].Type, Equals, script.VariableType(script.VAR_TYPE_BOOLEAN)) 402 | c.Assert(doc.Variables[5].Value, Equals, "\"\"") 403 | c.Assert(doc.Variables[5].Line, Equals, 55) 404 | 405 | c.Assert(doc.Variables[6], NotNil) 406 | c.Assert(doc.Variables[6].Name, Equals, "var_7") 407 | c.Assert(doc.Variables[6].Desc, DeepEquals, []string{"Variable #7 without value"}) 408 | c.Assert(doc.Variables[6].Type, Equals, script.VariableType(script.VAR_TYPE_UNKNOWN)) 409 | c.Assert(doc.Variables[6].Value, Equals, "") 410 | c.Assert(doc.Variables[6].Line, Equals, 59) 411 | c.Assert(doc.Variables[6].TypeName(0), Equals, "") 412 | c.Assert(doc.Variables[6].TypeName(1), Equals, "") 413 | c.Assert(doc.Variables[6].TypeName(2), Equals, "") 414 | c.Assert(doc.Variables[6].TypeName(3), Equals, "") 415 | c.Assert(doc.Variables[6].TypeName(4), Equals, "") 416 | 417 | c.Assert(doc.Variables[7], NotNil) 418 | c.Assert(doc.Variables[7].Name, Equals, "var_8") 419 | c.Assert(doc.Variables[7].Desc, DeepEquals, []string{"Variable #8 with multiline value"}) 420 | c.Assert(doc.Variables[7].Type, Equals, script.VariableType(script.VAR_TYPE_STRING)) 421 | c.Assert(doc.Variables[7].Value, Equals, "\"This is multiline value\"") 422 | c.Assert(doc.Variables[7].Line, Equals, 62) 423 | c.Assert(doc.Variables[7].IsString(), Equals, true) 424 | c.Assert(doc.Variables[7].IsNumber(), Equals, false) 425 | c.Assert(doc.Variables[7].IsBoolean(), Equals, false) 426 | c.Assert(doc.Variables[7].IsUnknown(), Equals, false) 427 | c.Assert(doc.Variables[7].TypeName(0), Equals, "String") 428 | c.Assert(doc.Variables[7].TypeName(1), Equals, "string") 429 | c.Assert(doc.Variables[7].TypeName(2), Equals, "STRING") 430 | c.Assert(doc.Variables[7].TypeName(3), Equals, "S") 431 | c.Assert(doc.Variables[7].TypeName(4), Equals, "s") 432 | 433 | // //////////////////////////////////////////////////////////////////////////////// // 434 | 435 | c.Assert(doc.Methods[0], NotNil) 436 | c.Assert(doc.Methods[0].Name, Equals, "method1") 437 | c.Assert(doc.Methods[0].Desc, DeepEquals, []string{ 438 | "This is desc for method #1.", 439 | "Second line of description.", 440 | "Third line of description.", 441 | "", 442 | "Fourth line of description.", 443 | }) 444 | c.Assert(doc.Methods[0].Arguments, HasLen, 0) 445 | c.Assert(doc.Methods[0].ResultCode, Equals, false) 446 | c.Assert(doc.Methods[0].ResultEcho, IsNil) 447 | c.Assert(doc.Methods[0].Example, HasLen, 0) 448 | c.Assert(doc.Methods[0].Line, Equals, 78) 449 | c.Assert(doc.Methods[0].HasArguments(), Equals, false) 450 | c.Assert(doc.Methods[0].HasEcho(), Equals, false) 451 | c.Assert(doc.Methods[0].HasEcho(), Equals, false) 452 | c.Assert(doc.Methods[0].UnitedDesc(), Equals, "This is desc for method #1. Second line of description. Third line of description. Fourth line of description.") 453 | 454 | c.Assert(doc.Methods[1], NotNil) 455 | c.Assert(doc.Methods[1].Name, Equals, "method2") 456 | c.Assert(doc.Methods[1].Desc, DeepEquals, []string{"This is desc for method #2."}) 457 | c.Assert(doc.Methods[1].Arguments, HasLen, 4) 458 | // //////////////////////////////////////////////////////////////////////////////// // 459 | c.Assert(doc.Methods[1].Arguments[0], NotNil) 460 | c.Assert(doc.Methods[1].Arguments[0].Index, Equals, "1") 461 | c.Assert(doc.Methods[1].Arguments[0].Desc, Equals, "First argument") 462 | c.Assert(doc.Methods[1].Arguments[0].Type, Equals, script.VariableType(script.VAR_TYPE_STRING)) 463 | c.Assert(doc.Methods[1].Arguments[0].IsOptional, Equals, false) 464 | c.Assert(doc.Methods[1].Arguments[0].IsWildcard, Equals, false) 465 | c.Assert(doc.Methods[1].Arguments[0].TypeName(0), Equals, "String") 466 | c.Assert(doc.Methods[1].Arguments[0].TypeName(1), Equals, "string") 467 | c.Assert(doc.Methods[1].Arguments[0].TypeName(2), Equals, "STRING") 468 | c.Assert(doc.Methods[1].Arguments[0].TypeName(3), Equals, "S") 469 | c.Assert(doc.Methods[1].Arguments[0].TypeName(4), Equals, "s") 470 | c.Assert(doc.Methods[1].Arguments[0].IsString(), Equals, true) 471 | c.Assert(doc.Methods[1].Arguments[0].IsNumber(), Equals, false) 472 | c.Assert(doc.Methods[1].Arguments[0].IsBoolean(), Equals, false) 473 | c.Assert(doc.Methods[1].Arguments[0].IsUnknown(), Equals, false) 474 | // //////////////////////////////////////////////////////////////////////////////// // 475 | c.Assert(doc.Methods[1].Arguments[1], NotNil) 476 | c.Assert(doc.Methods[1].Arguments[1].Index, Equals, "2") 477 | c.Assert(doc.Methods[1].Arguments[1].Desc, Equals, "Second argument") 478 | c.Assert(doc.Methods[1].Arguments[1].Type, Equals, script.VariableType(script.VAR_TYPE_NUMBER)) 479 | c.Assert(doc.Methods[1].Arguments[1].IsOptional, Equals, false) 480 | c.Assert(doc.Methods[1].Arguments[1].IsWildcard, Equals, false) 481 | c.Assert(doc.Methods[1].Arguments[1].TypeName(0), Equals, "Number") 482 | c.Assert(doc.Methods[1].Arguments[1].TypeName(1), Equals, "number") 483 | c.Assert(doc.Methods[1].Arguments[1].TypeName(2), Equals, "NUMBER") 484 | c.Assert(doc.Methods[1].Arguments[1].TypeName(3), Equals, "N") 485 | c.Assert(doc.Methods[1].Arguments[1].TypeName(4), Equals, "n") 486 | c.Assert(doc.Methods[1].Arguments[1].IsString(), Equals, false) 487 | c.Assert(doc.Methods[1].Arguments[1].IsNumber(), Equals, true) 488 | c.Assert(doc.Methods[1].Arguments[1].IsBoolean(), Equals, false) 489 | c.Assert(doc.Methods[1].Arguments[1].IsUnknown(), Equals, false) 490 | // //////////////////////////////////////////////////////////////////////////////// // 491 | c.Assert(doc.Methods[1].Arguments[2], NotNil) 492 | c.Assert(doc.Methods[1].Arguments[2].Index, Equals, "3") 493 | c.Assert(doc.Methods[1].Arguments[2].Desc, Equals, "Third argument") 494 | c.Assert(doc.Methods[1].Arguments[2].Type, Equals, script.VariableType(script.VAR_TYPE_BOOLEAN)) 495 | c.Assert(doc.Methods[1].Arguments[2].IsOptional, Equals, false) 496 | c.Assert(doc.Methods[1].Arguments[2].IsWildcard, Equals, false) 497 | c.Assert(doc.Methods[1].Arguments[2].TypeName(0), Equals, "Boolean") 498 | c.Assert(doc.Methods[1].Arguments[2].TypeName(1), Equals, "boolean") 499 | c.Assert(doc.Methods[1].Arguments[2].TypeName(2), Equals, "BOOLEAN") 500 | c.Assert(doc.Methods[1].Arguments[2].TypeName(3), Equals, "B") 501 | c.Assert(doc.Methods[1].Arguments[2].TypeName(4), Equals, "b") 502 | c.Assert(doc.Methods[1].Arguments[2].IsString(), Equals, false) 503 | c.Assert(doc.Methods[1].Arguments[2].IsNumber(), Equals, false) 504 | c.Assert(doc.Methods[1].Arguments[2].IsBoolean(), Equals, true) 505 | c.Assert(doc.Methods[1].Arguments[2].IsUnknown(), Equals, false) 506 | // //////////////////////////////////////////////////////////////////////////////// // 507 | c.Assert(doc.Methods[1].Arguments[3], NotNil) 508 | c.Assert(doc.Methods[1].Arguments[3].Index, Equals, "*") 509 | c.Assert(doc.Methods[1].Arguments[3].Desc, Equals, "Wildcard argument") 510 | c.Assert(doc.Methods[1].Arguments[3].Type, Equals, script.VariableType(script.VAR_TYPE_UNKNOWN)) 511 | c.Assert(doc.Methods[1].Arguments[3].IsOptional, Equals, false) 512 | c.Assert(doc.Methods[1].Arguments[3].IsWildcard, Equals, true) 513 | c.Assert(doc.Methods[1].Arguments[3].TypeName(0), Equals, "") 514 | c.Assert(doc.Methods[1].Arguments[3].TypeName(1), Equals, "") 515 | c.Assert(doc.Methods[1].Arguments[3].TypeName(2), Equals, "") 516 | c.Assert(doc.Methods[1].Arguments[3].TypeName(3), Equals, "") 517 | c.Assert(doc.Methods[1].Arguments[3].TypeName(4), Equals, "") 518 | c.Assert(doc.Methods[1].Arguments[3].IsString(), Equals, false) 519 | c.Assert(doc.Methods[1].Arguments[3].IsNumber(), Equals, false) 520 | c.Assert(doc.Methods[1].Arguments[3].IsBoolean(), Equals, false) 521 | c.Assert(doc.Methods[1].Arguments[3].IsUnknown(), Equals, true) 522 | // //////////////////////////////////////////////////////////////////////////////// /1 523 | c.Assert(doc.Methods[1].ResultCode, Equals, true) 524 | c.Assert(doc.Methods[1].ResultEcho, NotNil) 525 | c.Assert(doc.Methods[1].ResultEcho.Desc, DeepEquals, []string{"Magic value"}) 526 | c.Assert(doc.Methods[1].ResultEcho.Type, Equals, script.VariableType(script.VAR_TYPE_BOOLEAN)) 527 | c.Assert(doc.Methods[1].Example, HasLen, 3) 528 | c.Assert(doc.Methods[1].Example[0], Equals, "if [[ -f $file ]] ; then") 529 | c.Assert(doc.Methods[1].Example[1], Equals, " method2 123") 530 | c.Assert(doc.Methods[1].Example[2], Equals, "fi") 531 | c.Assert(doc.Methods[1].Line, Equals, 99) 532 | c.Assert(doc.Methods[1].HasArguments(), Equals, true) 533 | c.Assert(doc.Methods[1].HasEcho(), Equals, true) 534 | c.Assert(doc.Methods[1].HasExample(), Equals, true) 535 | c.Assert(doc.Methods[1].UnitedDesc(), Equals, "This is desc for method #2.") 536 | 537 | c.Assert(doc.Methods[2], NotNil) 538 | c.Assert(doc.Methods[2].Name, Equals, "method3") 539 | c.Assert(doc.Methods[2].Desc, DeepEquals, []string{"This is desc for method #3."}) 540 | c.Assert(doc.Methods[2].Arguments, HasLen, 1) 541 | c.Assert(doc.Methods[2].Arguments[0], NotNil) 542 | c.Assert(doc.Methods[2].Arguments[0].Index, Equals, "1") 543 | c.Assert(doc.Methods[2].Arguments[0].Desc, Equals, "First argument") 544 | c.Assert(doc.Methods[2].Arguments[0].Type, Equals, script.VariableType(script.VAR_TYPE_STRING)) 545 | c.Assert(doc.Methods[2].Arguments[0].IsOptional, Equals, true) 546 | c.Assert(doc.Methods[2].Arguments[0].IsWildcard, Equals, false) 547 | c.Assert(doc.Methods[2].ResultCode, Equals, false) 548 | c.Assert(doc.Methods[2].ResultEcho, IsNil) 549 | c.Assert(doc.Methods[2].Example, HasLen, 0) 550 | c.Assert(doc.Methods[2].Line, Equals, 110) 551 | c.Assert(doc.Methods[2].HasArguments(), Equals, true) 552 | c.Assert(doc.Methods[2].HasEcho(), Equals, false) 553 | c.Assert(doc.Methods[2].HasExample(), Equals, false) 554 | c.Assert(doc.Methods[2].UnitedDesc(), Equals, "This is desc for method #3.") 555 | 556 | c.Assert(doc.Methods[3], NotNil) 557 | c.Assert(doc.Methods[3].Name, Equals, "method4") 558 | c.Assert(doc.Methods[3].Desc, DeepEquals, []string{"This is desc for method #4."}) 559 | c.Assert(doc.Methods[3].Arguments, HasLen, 0) 560 | c.Assert(doc.Methods[3].ResultCode, Equals, false) 561 | c.Assert(doc.Methods[3].ResultEcho, IsNil) 562 | c.Assert(doc.Methods[3].Example, HasLen, 0) 563 | c.Assert(doc.Methods[3].Line, Equals, 115) 564 | c.Assert(doc.Methods[3].HasArguments(), Equals, false) 565 | c.Assert(doc.Methods[3].HasEcho(), Equals, false) 566 | c.Assert(doc.Methods[3].HasExample(), Equals, false) 567 | c.Assert(doc.Methods[3].UnitedDesc(), Equals, "This is desc for method #4.") 568 | 569 | c.Assert(doc.Methods[4], NotNil) 570 | c.Assert(doc.Methods[4].Name, Equals, "method5") 571 | c.Assert(doc.Methods[4].Desc, DeepEquals, []string{"This is desc for method #5."}) 572 | c.Assert(doc.Methods[4].Arguments, HasLen, 0) 573 | c.Assert(doc.Methods[4].ResultCode, Equals, false) 574 | c.Assert(doc.Methods[4].ResultEcho, IsNil) 575 | c.Assert(doc.Methods[4].Example, HasLen, 0) 576 | c.Assert(doc.Methods[4].Line, Equals, 121) 577 | c.Assert(doc.Methods[4].HasArguments(), Equals, false) 578 | c.Assert(doc.Methods[4].HasEcho(), Equals, false) 579 | c.Assert(doc.Methods[4].HasExample(), Equals, false) 580 | c.Assert(doc.Methods[4].UnitedDesc(), Equals, "This is desc for method #5.") 581 | 582 | c.Assert(doc.Methods[5], NotNil) 583 | c.Assert(doc.Methods[5].Name, Equals, "method6") 584 | c.Assert(doc.Methods[5].Desc, DeepEquals, []string{"This is desc for method #6."}) 585 | c.Assert(doc.Methods[5].Arguments, HasLen, 0) 586 | c.Assert(doc.Methods[5].ResultCode, Equals, false) 587 | c.Assert(doc.Methods[5].ResultEcho, IsNil) 588 | c.Assert(doc.Methods[5].Example, HasLen, 0) 589 | c.Assert(doc.Methods[5].Line, Equals, 127) 590 | c.Assert(doc.Methods[5].HasArguments(), Equals, false) 591 | c.Assert(doc.Methods[5].HasEcho(), Equals, false) 592 | c.Assert(doc.Methods[5].HasExample(), Equals, false) 593 | c.Assert(doc.Methods[5].UnitedDesc(), Equals, "This is desc for method #6.") 594 | 595 | c.Assert(doc.Methods[6], NotNil) 596 | c.Assert(doc.Methods[6].Name, Equals, "method7") 597 | c.Assert(doc.Methods[6].Desc, DeepEquals, []string{"This is desc for method #7."}) 598 | c.Assert(doc.Methods[6].Arguments, HasLen, 1) 599 | c.Assert(doc.Methods[6].Arguments[0], NotNil) 600 | c.Assert(doc.Methods[6].Arguments[0].Index, Equals, "1") 601 | c.Assert(doc.Methods[6].Arguments[0].Desc, Equals, "First argument") 602 | c.Assert(doc.Methods[6].Arguments[0].Type, Equals, script.VariableType(script.VAR_TYPE_UNKNOWN)) 603 | c.Assert(doc.Methods[6].Arguments[0].IsOptional, Equals, false) 604 | c.Assert(doc.Methods[6].Arguments[0].IsWildcard, Equals, false) 605 | c.Assert(doc.Methods[6].ResultCode, Equals, false) 606 | c.Assert(doc.Methods[6].ResultEcho, IsNil) 607 | c.Assert(doc.Methods[6].Example, HasLen, 0) 608 | c.Assert(doc.Methods[6].Line, Equals, 134) 609 | c.Assert(doc.Methods[6].HasArguments(), Equals, true) 610 | c.Assert(doc.Methods[6].HasEcho(), Equals, false) 611 | c.Assert(doc.Methods[6].HasExample(), Equals, false) 612 | c.Assert(doc.Methods[6].UnitedDesc(), Equals, "This is desc for method #7.") 613 | 614 | c.Assert(doc.Methods[7], NotNil) 615 | c.Assert(doc.Methods[7].Name, Equals, "method8") 616 | c.Assert(doc.Methods[7].Desc, DeepEquals, []string{"This is desc for method #8."}) 617 | c.Assert(doc.Methods[7].Arguments, HasLen, 0) 618 | c.Assert(doc.Methods[7].ResultCode, Equals, false) 619 | c.Assert(doc.Methods[7].ResultEcho, IsNil) 620 | c.Assert(doc.Methods[7].Example, HasLen, 1) 621 | c.Assert(doc.Methods[7].Example[0], Equals, "method8 123") 622 | c.Assert(doc.Methods[7].Line, Equals, 142) 623 | c.Assert(doc.Methods[7].HasArguments(), Equals, false) 624 | c.Assert(doc.Methods[7].HasEcho(), Equals, false) 625 | c.Assert(doc.Methods[7].HasExample(), Equals, true) 626 | c.Assert(doc.Methods[7].UnitedDesc(), Equals, "This is desc for method #8.") 627 | 628 | c.Assert(doc.Methods[8], NotNil) 629 | c.Assert(doc.Methods[8].Name, Equals, "method9") 630 | c.Assert(doc.Methods[8].Desc, DeepEquals, []string{"This is desc for method #9."}) 631 | c.Assert(doc.Methods[8].Arguments, HasLen, 0) 632 | c.Assert(doc.Methods[8].ResultCode, Equals, false) 633 | c.Assert(doc.Methods[8].ResultEcho, IsNil) 634 | c.Assert(doc.Methods[8].Example, HasLen, 0) 635 | c.Assert(doc.Methods[8].Line, Equals, 148) 636 | c.Assert(doc.Methods[8].HasArguments(), Equals, false) 637 | c.Assert(doc.Methods[8].HasEcho(), Equals, false) 638 | c.Assert(doc.Methods[8].HasExample(), Equals, false) 639 | c.Assert(doc.Methods[8].UnitedDesc(), Equals, "This is desc for method #9.") 640 | } 641 | --------------------------------------------------------------------------------