├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── clossing-issues.yml │ ├── comments.yml │ ├── main.yml │ ├── move-closed-issues.yml │ ├── pr-review-hack.yml │ ├── pr-reviews-requested.yml │ ├── pr-reviews.yml │ ├── reasign.yml │ ├── stale.yml │ └── triage.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── SECURITY.md ├── cmd.go ├── cmd_test.go ├── go.mod ├── go.sum ├── ini.go ├── main.go ├── main_test.go └── vars.mk /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug 2 | description: Create a report to help us improve 3 | labels: ["tech-issues"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for reporting an issue. Before you open the bug report please review the README file present in the root of this repository. 9 | 10 | Please fill in as much of the following form as you're able to. 11 | - type: textarea 12 | attributes: 13 | label: What steps will reproduce the bug? 14 | description: Enter details about your bug. 15 | placeholder: | 16 | 1. In this environment... 17 | 2. With this config... 18 | 3. Run '...' 19 | 4. See error... 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: What is the expected behavior? 25 | description: If possible please provide textual output instead of screenshots. 26 | - type: textarea 27 | attributes: 28 | label: What do you see instead? 29 | description: If possible please provide textual output instead of screenshots. 30 | validations: 31 | required: true 32 | - type: textarea 33 | attributes: 34 | label: Additional information 35 | description: Tell us anything else you think we should know. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature request" 2 | description: Suggest an idea for this project 3 | labels: ["feature-request"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for suggesting an idea to improve this tool. 9 | Please fill in as much of the following form as you're able to. 10 | - type: textarea 11 | attributes: 12 | label: What is the problem this feature will solve? 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: What is the feature you are proposing to solve the problem? 18 | description: Describe the requests. If you already have something in mind... PRs are welcome! 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: What alternatives have you considered? 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | ### Description of the change 14 | 15 | 16 | 17 | ### Benefits 18 | 19 | 20 | 21 | ### Possible drawbacks 22 | 23 | 24 | 25 | ### Applicable issues 26 | 27 | 28 | - fixes # 29 | 30 | ### Additional information 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | version: 2 5 | # Check for updates to GitHub Actions every week 6 | updates: 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/clossing-issues.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Close Solved issues' 6 | on: 7 | schedule: 8 | # Hourly 9 | - cron: '0 * * * *' 10 | # Remove all permissions by default. Actions are performed by Bitnami Bot 11 | permissions: {} 12 | jobs: 13 | stale: 14 | runs-on: ubuntu-latest 15 | if: ${{ github.repository_owner == 'bitnami' }} 16 | steps: 17 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 18 | with: 19 | any-of-labels: 'solved' 20 | stale-issue-label: 'solved' 21 | days-before-stale: 0 22 | days-before-close: 0 23 | repo-token: ${{ secrets.BITNAMI_SUPPORT_BOARD_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/comments.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Comments based card movements' 6 | on: 7 | issue_comment: 8 | types: 9 | - created 10 | permissions: 11 | contents: read 12 | pull-requests: write 13 | issues: write 14 | # Avoid concurrency over the same issue 15 | concurrency: 16 | group: card-movement-${{ github.event.issue.number }} 17 | jobs: 18 | call-comments-workflow: 19 | if: ${{ github.repository_owner == 'bitnami' }} 20 | uses: bitnami/support/.github/workflows/comment-created.yml@main 21 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: 8 | - main 9 | 10 | release: 11 | types: [published] 12 | 13 | pull_request: 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | # Remove all permissions by default 18 | permissions: {} 19 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 20 | jobs: 21 | # This workflow contains a single job called "build" 22 | build-and-test: 23 | runs-on: ubuntu-latest 24 | name: Build and Test 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 29 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 30 | with: 31 | go-version: '^1.22.4' # The Go version to download (if necessary) and use. 32 | - name: Install Build Dependencies 33 | run: make get-build-deps 34 | - name: Download required modules 35 | run: make download 36 | - name: Vet 37 | run: make vet 38 | - name: Lint 39 | run: make lint 40 | - name: Cover 41 | run: make cover 42 | - name: Build 43 | run: | 44 | set -ex 45 | for dist in amd64 arm64; do 46 | target=out/ini-file-linux-$dist 47 | rm -rf "$target" 48 | make build/$dist TOOL_PATH="$target" 49 | file $target 50 | tar -C "$(dirname "$target")" -czf "$target.tar.gz" "$(basename "$target")" 51 | done 52 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 53 | with: 54 | name: built-binaries 55 | path: | 56 | out/*.tar.gz 57 | release: 58 | needs: [ 'build-and-test' ] 59 | if: github.repository == 'bitnami/render-template' && startsWith(github.ref, 'refs/tags/') 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 63 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 64 | with: 65 | path: ./artifacts 66 | - name: Set tag name 67 | id: vars 68 | run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} 69 | - name: Release 70 | run: | 71 | set -e 72 | create_digest_file() { 73 | local digest_file=${1:?You must provide the digest file path} 74 | shift 75 | for file in "$@"; do 76 | ( 77 | cd "$(dirname "$file")" 78 | sha256sum "$(basename "$file")" 79 | ) >> "$digest_file" 80 | done 81 | } 82 | assets=( ./artifacts/built-binaries/*.gz ) 83 | 84 | tag_name="${{ steps.vars.outputs.tag }}" 85 | checksums_file="${tag_name}_checksums.txt" 86 | create_digest_file "$checksums_file" "${assets[@]}" 87 | assets+=( "$checksums_file" ) 88 | if gh release view "$tag_name" >/dev/null 2>/dev/null; then 89 | echo "Release $tag_name already exists. Updating" 90 | gh release upload "$tag_name" "${assets[@]}" 91 | else 92 | echo "Creating new release $tag_name" 93 | # Format checksums for the release text 94 | printf '```\n%s\n```' "$(<"$checksums_file")" > release.txt 95 | gh release create -t "$tag_name" "$tag_name" -F release.txt "${assets[@]}" 96 | fi 97 | env: 98 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | notify: 100 | name: Send notification 101 | needs: 102 | - release 103 | if: ${{ always() && needs.release.result == 'failure' }} 104 | uses: bitnami/support/.github/workflows/gchat-notification.yml@main 105 | with: 106 | workflow: ${{ github.workflow }} 107 | job-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 108 | secrets: 109 | webhook-url: ${{ secrets.GCHAT_CONTENT_ALERTS_WEBHOOK_URL }} 110 | -------------------------------------------------------------------------------- /.github/workflows/move-closed-issues.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Move closed issues' 6 | on: 7 | issues: 8 | types: 9 | - closed 10 | pull_request_target: 11 | types: 12 | - closed 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | # Avoid concurrency over the same issue 17 | concurrency: 18 | group: card-movement-${{ github.event.issue != null && github.event.issue.number || github.event.number }} 19 | jobs: 20 | call-move-closed-workflow: 21 | if: ${{ github.repository_owner == 'bitnami' }} 22 | uses: bitnami/support/.github/workflows/item-closed.yml@main 23 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/pr-review-hack.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # This is a hack to run reusable workflows in the main repo context and not from the forked repository. 5 | # We this hack we can use secrets configured in the organization. 6 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 7 | name: '[Support] PR review comment trigger' 8 | on: 9 | workflow_run: 10 | workflows: 11 | - '\[Support\] PR review comment card movements' 12 | types: 13 | - completed 14 | permissions: {} 15 | jobs: 16 | pr-info: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: read 20 | actions: read 21 | outputs: 22 | author: ${{ steps.get-info.outputs.author }} 23 | actor: ${{ steps.get-info.outputs.actor }} 24 | review_state: ${{ steps.get-info.outputs.review_state }} 25 | labels: ${{ steps.get-info.outputs.labels }} 26 | resource_url: ${{ steps.get-info.outputs.resource_url }} 27 | if: ${{ github.repository_owner == 'bitnami' && github.event.workflow_run.conclusion == 'success' }} 28 | steps: 29 | - id: get-info 30 | env: 31 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 32 | run: | 33 | actor="${{ github.event.workflow_run.actor.login }}" 34 | download_url="$(gh api "${{ github.event.workflow_run.artifacts_url }}" | jq -cr '.artifacts[] | select(.name == "pull_request_info.json") | .archive_download_url')" 35 | curl -sSL -o pull_request_info.zip -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" -H "Authorization: Bearer $GITHUB_TOKEN" $download_url 36 | unzip pull_request_info.zip 37 | pull_request_number="$(jq -cr '.issue.number' pull_request_info.json)" 38 | issue_review_state="$(jq -cr '.review.state' pull_request_info.json)" 39 | pull_request="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pull_request_number}")" 40 | author="$(echo $pull_request | jq -cr '.user.login')" 41 | author_association="$(echo $pull_request | jq -cr '.author_association')" 42 | labels="$(echo $pull_request | jq -cr '[.labels[].name]')" 43 | resource_url="$(echo $pull_request | jq -cr '.html_url')" 44 | 45 | echo "::notice:: Managing PR #${pull_request_number}" 46 | echo "actor=${actor}" >> $GITHUB_OUTPUT 47 | echo "author=${author}" >> $GITHUB_OUTPUT 48 | echo "author_association=${author_association}" >> $GITHUB_OUTPUT 49 | echo "review_state=${issue_review_state}" >> $GITHUB_OUTPUT 50 | echo "labels=${labels}" >> $GITHUB_OUTPUT 51 | echo "resource_url=${resource_url}" >> $GITHUB_OUTPUT 52 | call-pr-review-comment: 53 | uses: bitnami/support/.github/workflows/pr-review-comment.yml@main 54 | needs: pr-info 55 | permissions: 56 | contents: read 57 | secrets: inherit 58 | with: 59 | author: ${{ needs.pr-info.outputs.author }} 60 | actor: ${{ needs.pr-info.outputs.actor }} 61 | labels: ${{ needs.pr-info.outputs.labels }} 62 | review_state: ${{ needs.pr-info.outputs.review_state }} 63 | resource_url: ${{ needs.pr-info.outputs.resource_url }} 64 | -------------------------------------------------------------------------------- /.github/workflows/pr-reviews-requested.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Review based card movements' 6 | on: 7 | pull_request_target: 8 | types: 9 | - review_requested 10 | - synchronize 11 | permissions: 12 | contents: read 13 | # Avoid concurrency over the same issue 14 | concurrency: 15 | group: card-movement-${{ github.event.number }} 16 | jobs: 17 | call-pr-review-workflow: 18 | if: ${{ github.repository_owner == 'bitnami' }} 19 | uses: bitnami/support/.github/workflows/pr-review-requested-sync.yml@main 20 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/pr-reviews.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] PR review comment card movements' 6 | on: 7 | pull_request_review_comment: 8 | types: 9 | - created 10 | pull_request_review: 11 | types: 12 | - submitted 13 | - dismissed 14 | permissions: {} 15 | # Avoid concurrency over the same issue 16 | concurrency: 17 | group: card-movement-${{ github.event.pull_request.number }} 18 | jobs: 19 | just-notice: 20 | # This is a dummy workflow that triggers a workflow_run 21 | runs-on: ubuntu-latest 22 | if: ${{ github.repository_owner == 'bitnami' }} 23 | steps: 24 | - run: | 25 | echo "::notice:: Comment on PR #${{ github.event.pull_request.number }}" 26 | jq -n --arg issue '${{ github.event.pull_request.number }}' --arg state '${{ github.event.review != null && github.event.review.state || '' }}' '{"issue": {"number": $issue }, "review": { "state": $state }}' > pull_request_info.json 27 | - name: Upload the PR info 28 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 29 | with: 30 | name: pull_request_info.json 31 | path: ./pull_request_info.json -------------------------------------------------------------------------------- /.github/workflows/reasign.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Review based card movements' 6 | on: 7 | pull_request_target: 8 | types: 9 | - labeled 10 | issues: 11 | types: 12 | - labeled 13 | permissions: 14 | contents: read 15 | pull-requests: write 16 | issues: write 17 | # Avoid concurrency over the same issue 18 | concurrency: 19 | group: card-movement-${{ github.event.issue != null && github.event.issue.number || github.event.number }} 20 | jobs: 21 | call-reasign-workflow: 22 | if: ${{ github.repository_owner == 'bitnami' }} 23 | uses: bitnami/support/.github/workflows/item-labeled.yml@main 24 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 5 | name: '[Support] Close stale issues and PRs' 6 | on: 7 | workflow_dispatch: 8 | schedule: 9 | - cron: '0 1 * * *' 10 | # Remove all permissions by default 11 | permissions: {} 12 | # This job won't trigger any additional event. All actions are performed with GITHUB_TOKEN 13 | jobs: 14 | stale: 15 | runs-on: ubuntu-latest 16 | if: ${{ github.repository_owner == 'bitnami' }} 17 | permissions: 18 | issues: write 19 | pull-requests: write 20 | steps: 21 | # This step will add the stale comment and label for the first 15 days without activity. It won't close any task 22 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 23 | with: 24 | repo-token: ${{ secrets.GITHUB_TOKEN }} 25 | stale-issue-message: 'This Issue has been automatically marked as "stale" because it has not had recent activity (for 15 days). It will be closed if no further activity occurs. Thanks for the feedback.' 26 | stale-pr-message: 'This Pull Request has been automatically marked as "stale" because it has not had recent activity (for 15 days). It will be closed if no further activity occurs. Thank you for your contribution.' 27 | days-before-stale: 15 28 | days-before-close: -1 29 | exempt-issue-labels: 'on-hold' 30 | exempt-pr-labels: 'on-hold' 31 | operations-per-run: 500 32 | # This step will add the 'solved' label and the last comment before closing the issue or PR. Note that it won't close any issue or PR, they will be closed by the clossing-issues workflow 33 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 34 | with: 35 | repo-token: ${{ secrets.GITHUB_TOKEN }} 36 | stale-issue-message: 'Due to the lack of activity in the last 5 days since it was marked as "stale", we proceed to close this Issue. Do not hesitate to reopen it later if necessary.' 37 | stale-pr-message: 'Due to the lack of activity in the last 5 days since it was marked as "stale", we proceed to close this Pull Request. Do not hesitate to reopen it later if necessary.' 38 | any-of-labels: 'stale' 39 | stale-issue-label: 'solved' 40 | stale-pr-label: 'solved' 41 | days-before-stale: 5 42 | days-before-close: -1 43 | exempt-issue-labels: 'on-hold' 44 | exempt-pr-labels: 'on-hold' 45 | operations-per-run: 200 -------------------------------------------------------------------------------- /.github/workflows/triage.yml: -------------------------------------------------------------------------------- 1 | # Copyright Broadcom, Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: APACHE-2.0 3 | 4 | # This workflow is built to manage the triage support by using GH issues. 5 | # NOTE: This workflow is maintained in the https://github.com/bitnami/support repository 6 | name: '[Support] Organize triage' 7 | on: 8 | issues: 9 | types: 10 | - reopened 11 | - opened 12 | pull_request_target: 13 | types: 14 | - reopened 15 | - opened 16 | permissions: 17 | contents: read 18 | pull-requests: write 19 | issues: write 20 | # Avoid concurrency over the same issue 21 | concurrency: 22 | group: card-movement-${{ github.event.issue != null && github.event.issue.number || github.event.number }} 23 | jobs: 24 | call-triage-workflow: 25 | if: ${{ github.repository_owner == 'bitnami' }} 26 | uses: bitnami/support/.github/workflows/item-opened.yml@main 27 | secrets: inherit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | out/ 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. 4 | 5 | Communication through any of Bitnami's channels (GitHub, mailing lists, Twitter, and so on) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 6 | 7 | We promise to extend courtesy and respect to everyone involved in this project, regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to this project to do the same. 8 | 9 | If any member of the community violates this code of conduct, the maintainers of this project may take action, including removing issues, comments, and PRs or blocking accounts, as deemed appropriate. 10 | 11 | If you are subjected to or witness unacceptable behavior, or have any other concerns, please communicate with us. 12 | 13 | If you have suggestions to improve this Code of Conduct, please submit an issue or PR. 14 | 15 | **Attribution** 16 | 17 | This Code of Conduct is adapted from the Angular project available at this page: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Contributions are welcome via GitHub Pull Requests. This document outlines the process to help get your contribution accepted. 4 | 5 | Any type of contribution is welcome: new features, bug fixes, documentation improvements, etc. 6 | 7 | ## How to Contribute 8 | 9 | 1. Fork this repository, develop, and test your changes. 10 | 2. Submit a pull request. 11 | 12 | ### Requirements 13 | 14 | When submitting a PR make sure that: 15 | - It must pass CI jobs for linting and test the changes (if any). 16 | - The title of the PR is clear enough. 17 | - If necessary, add information to the repository's `README.md`. 18 | 19 | #### Sign Your Work 20 | 21 | The sign-off is a simple line at the end of the explanation for a commit. All commits needs to be signed. Your signature certifies that you wrote the patch or otherwise have the right to contribute the material. The rules are pretty simple, you only need to certify the guidelines from [developercertificate.org](https://developercertificate.org/). 22 | 23 | Then you just add a line to every git commit message: 24 | 25 | Signed-off-by: Joe Smith 26 | 27 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 28 | 29 | If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`. 30 | 31 | Note: If your git config information is set properly then viewing the `git log` information for your commit will look something like this: 32 | 33 | ``` 34 | Author: Joe Smith 35 | Date: Thu Feb 2 11:41:15 2018 -0800 36 | 37 | Update README 38 | 39 | Signed-off-by: Joe Smith 40 | ``` 41 | 42 | Notice the `Author` and `Signed-off-by` lines match. If they don't your PR will be rejected by the automated DCO check. 43 | 44 | ### PR Approval and Release Process 45 | 46 | 1. Changes are manually reviewed by Bitnami team members usually within a business day. 47 | 2. Once the changes are accepted, the PR is tested (if needed) into the Bitnami CI pipeline. 48 | 3. The PR is merged by the reviewer(s) in the GitHub `main` branch. 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ini-file set/get/del in a container 2 | # 3 | # docker run --rm -it -v /tmp:/tmp bitnami/ini-file set -k "title" -v "A wonderful book" -s "My book" /tmp/my.ini 4 | # docker run --rm -it -v /tmp:/tmp bitnami/ini-file get -k "title" -s "My book" /tmp/my.ini 5 | # docker run --rm -it -v /tmp:/tmp bitnami/ini-file del -k "title" -s "My book" /tmp/my.ini 6 | # 7 | 8 | FROM golang:1.22-bullseye as build 9 | 10 | RUN apt-get update && apt-get install -y --no-install-recommends \ 11 | git make upx \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | WORKDIR /go/src/app 15 | COPY . . 16 | 17 | RUN rm -rf out 18 | 19 | RUN make 20 | 21 | RUN upx --ultra-brute out/ini-file 22 | 23 | FROM bitnami/minideb:bullseye 24 | 25 | COPY --from=build /go/src/app/out/ini-file /usr/local/bin/ 26 | 27 | ENTRYPOINT ["/usr/local/bin/ini-file"] 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build clean download get-build-deps vet lint test cover 2 | 3 | TOOL_NAME := ini-file 4 | 5 | # Load relative to the common.mk file 6 | include $(dir $(lastword $(MAKEFILE_LIST)))/vars.mk 7 | 8 | include ./vars.mk 9 | 10 | all: 11 | @$(MAKE) get-build-deps 12 | @$(MAKE) download 13 | @$(MAKE) vet 14 | @$(MAKE) lint 15 | @$(MAKE) cover 16 | @$(MAKE) build 17 | 18 | build/%: 19 | @echo "Building GOARCH=$(*F)" 20 | @GOARCH=$(*F) go build -ldflags=$(LDFLAGS) -o $(TOOL_PATH) 21 | @echo "*** Binary created under $(TOOL_PATH) ***" 22 | 23 | build: build/amd64 24 | 25 | clean: 26 | @rm -rf $(BUILD_DIR) 27 | 28 | download: 29 | $(GO_MOD) download 30 | 31 | get-build-deps: 32 | @echo "+ Downloading build dependencies" 33 | @go install golang.org/x/tools/cmd/goimports@latest 34 | @go install honnef.co/go/tools/cmd/staticcheck@latest 35 | 36 | vet: 37 | @echo "+ Vet" 38 | @go vet ./... 39 | 40 | lint: 41 | @echo "+ Linting package" 42 | @staticcheck ./... 43 | $(call fmtcheck, .) 44 | 45 | test: 46 | @echo "+ Testing package" 47 | $(GO_TEST) . 48 | 49 | cover: test 50 | @echo "+ Tests Coverage" 51 | @mkdir -p $(BUILD_DIR) 52 | @touch $(BUILD_DIR)/cover.out 53 | @go test -coverprofile=$(BUILD_DIR)/cover.out 54 | @go tool cover -html=$(BUILD_DIR)/cover.out -o=$(BUILD_DIR)/coverage.html 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/bitnami/ini-file)](https://goreportcard.com/report/github.com/bitnami/ini-file) 2 | [![CI](https://github.com/bitnami/ini-file/actions/workflows/main.yml/badge.svg)](https://github.com/bitnami/ini-file/actions/workflows/main.yml) 3 | 4 | # ini-file 5 | 6 | This tool allows manipulating INI files. 7 | 8 | # Basic usage 9 | 10 | ```console 11 | $ ini-file --help 12 | 13 | Usage: 14 | ini-file [OPTIONS] 15 | 16 | Help Options: 17 | -h, --help Show this help message 18 | 19 | Available commands: 20 | del INI FILE Delete 21 | get INI FILE Get 22 | set INI File Set 23 | ``` 24 | 25 | # Examples 26 | 27 | ## Set values in INI file 28 | 29 | ```console 30 | $ ini-file set --section "My book" --key "title" --value "A wonderful book" ./my.ini 31 | $ ini-file set -s "My book" -k "author" -v "Bitnami" ./my.ini 32 | $ ini-file set -s "My book" -k "rate" -v "very good" ./my.ini 33 | $ cat ./my.ini 34 | [My book] 35 | title=A wonderful book 36 | author=Bitnami 37 | rate=very good 38 | ``` 39 | 40 | ## Set boolean value in INI file 41 | 42 | ```console 43 | $ ini-file set -s "My book" -k "already_read" --boolean ./my.ini 44 | $ cat ./my.ini 45 | [My book] 46 | ... 47 | already_read 48 | ``` 49 | 50 | ## Get values from INI file 51 | 52 | ```console 53 | $ cat > ./my.ini <<"EOF" 54 | [My book] 55 | title=A wonderful book 56 | author=Bitnami 57 | rate=very good 58 | already_read 59 | EOF 60 | $ ini-file get --section "My book" --key title ./my.ini 61 | A wonderful book 62 | $ ini-file get --section "My book" --key missing_key ./my.ini 63 | 64 | $ ini-file get --section "My book" --key author ./my.ini 65 | Bitnami 66 | $ ini-file get --section "My book" --key already_read ./my.ini 67 | true 68 | ``` 69 | 70 | ## Deletes values from INI file 71 | 72 | ```console 73 | $ cat > ./my.ini <<"EOF" 74 | [My book] 75 | title=A wonderful book 76 | author=Bitnami 77 | rate=very good 78 | already_read 79 | EOF 80 | $ ini-file del --section "My book" --key title ./my.ini 81 | $ ini-file del --section "My book" --key missing_key ./my.ini 82 | $ ini-file del --section "My book" --key author ./my.ini 83 | $ cat ./my.ini 84 | [My book] 85 | rate=very good 86 | already_read 87 | ``` 88 | 89 | ## Working with identical keys in the file 90 | 91 | ```console 92 | $ cat > ./my.ini <<"EOF" 93 | [My book] 94 | title=A wonderful book 95 | author[]=Bitnami 96 | author[]=Contributors 97 | EOF 98 | 99 | # get retrieves the first mention of the key 100 | $ ini-file get --section "My book" --key "author[]" ./my.ini 101 | Bitnami 102 | 103 | # If the original file contains the key more than once, set adds the new value at the end 104 | $ ini-file set --section "My book" --key "author[]" --value "Other" ./my.ini 105 | $ cat ./my.ini 106 | title=A wonderful book 107 | author[]=Bitnami 108 | author[]=Contributors 109 | author[]=Other 110 | 111 | # del removes all keys with the given name 112 | $ ini-file del --section "My book" --key "author[]" ./my.ini 113 | $ cat ./my.ini 114 | title=A wonderful book 115 | ``` 116 | 117 | ## License 118 | 119 | Copyright © 2025 Broadcom. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. 120 | 121 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 122 | 123 | You may obtain a copy of the License at 124 | 125 | http://www.apache.org/licenses/LICENSE-2.0 126 | 127 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 128 | See the License for the specific language governing permissions and limitations under the License. 129 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Release Process 2 | 3 | The community has adopted this security disclosure and response policy to ensure we responsibly handle critical issues. 4 | 5 | 6 | ## Supported Versions 7 | 8 | For a list of support versions that this project will potentially create security fixes for, please refer to the Releases page on this project's GitHub and/or project related documentation on release cadence and support. 9 | 10 | 11 | ## Reporting a Vulnerability - Private Disclosure Process 12 | 13 | Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to this project privately, to minimize attacks against current users before they are fixed. Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible. This information could be kept entirely internal to the project. 14 | 15 | If you know of a publicly disclosed security vulnerability for this project, please **IMMEDIATELY** contact the maintainers of this project privately. The use of encrypted email is encouraged. 16 | 17 | 18 | **IMPORTANT: Do not file public issues on GitHub for security vulnerabilities** 19 | 20 | To report a vulnerability or a security-related issue, please contact the maintainers with enough details through one of the following channels: 21 | * Directly via their individual email addresses 22 | * Open a [GitHub Security Advisory](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability). This allows for anyone to report security vulnerabilities directly and privately to the maintainers via GitHub. Note that this option may not be present for every repository. 23 | 24 | The report will be fielded by the maintainers who have committer and release permissions. Feedback will be sent within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime. 25 | 26 | Do not report non-security-impacting bugs through this channel. Use GitHub issues for all non-security-impacting bugs. 27 | 28 | 29 | ## Proposed Report Content 30 | 31 | Provide a descriptive title and in the description of the report include the following information: 32 | 33 | * Basic identity information, such as your name and your affiliation or company. 34 | * Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and logs are all helpful to us). 35 | * Description of the effects of the vulnerability on this project and the related hardware and software configurations, so that the maintainers can reproduce it. 36 | * How the vulnerability affects this project's usage and an estimation of the attack surface, if there is one. 37 | * List other projects or dependencies that were used in conjunction with this project to produce the vulnerability. 38 | 39 | 40 | ## When to report a vulnerability 41 | 42 | * When you think this project has a potential security vulnerability. 43 | * When you suspect a potential vulnerability but you are unsure that it impacts this project. 44 | * When you know of or suspect a potential vulnerability on another project that is used by this project. 45 | 46 | 47 | ## Patch, Release, and Disclosure 48 | 49 | The maintainers will respond to vulnerability reports as follows: 50 | 51 | 1. The maintainers will investigate the vulnerability and determine its effects and criticality. 52 | 2. If the issue is not deemed to be a vulnerability, the maintainers will follow up with a detailed reason for rejection. 53 | 3. The maintainers will initiate a conversation with the reporter within 3 business days. 54 | 4. If a vulnerability is acknowledged and the timeline for a fix is determined, the maintainers will work on a plan to communicate with the appropriate community, including identifying mitigating steps that affected users can take to protect themselves until the fix is rolled out. 55 | 5. The maintainers will also create a [Security Advisory](https://docs.github.com/en/code-security/repository-security-advisories/publishing-a-repository-security-advisory) using the [CVSS Calculator](https://www.first.org/cvss/calculator/3.0), if it is not created yet. The maintainers make the final call on the calculated CVSS; it is better to move quickly than making the CVSS perfect. Issues may also be reported to [Mitre](https://cve.mitre.org/) using this [scoring calculator](https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator). The draft advisory will initially be set to private. 56 | 6. The maintainers will work on fixing the vulnerability and perform internal testing before preparing to roll out the fix. 57 | 7. Once the fix is confirmed, the maintainers will patch the vulnerability in the next patch or minor release, and backport a patch release into all earlier supported releases. 58 | 59 | 60 | ## Public Disclosure Process 61 | 62 | The maintainers publish the public advisory to this project's community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog, and other channels will assist in educating the project's users and rolling out the patched release to affected users. 63 | 64 | The maintainers will also publish any mitigating steps users can take until the fix can be applied to their instances. This project's distributors will handle creating and publishing their own security advisories. 65 | 66 | 67 | ## Confidentiality, integrity and availability 68 | 69 | We consider vulnerabilities leading to the compromise of data confidentiality, elevation of privilege, or integrity to be our highest priority concerns. Availability, in particular in areas relating to DoS and resource exhaustion, is also a serious security concern. The maintainer team takes all vulnerabilities, potential vulnerabilities, and suspected vulnerabilities seriously and will investigate them in an urgent and expeditious manner. 70 | 71 | Note that we do not currently consider the default settings for this project to be secure-by-default. It is necessary for operators to explicitly configure settings, role based access control, and other resource related features in this project to provide a hardened environment. We will not act on any security disclosure that relates to a lack of safe defaults. Over time, we will work towards improved safe-by-default configuration, taking into account backwards compatibility. 72 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // INIFileSetCmd defines a ini set command operation 10 | type INIFileSetCmd struct { 11 | Section string `short:"s" long:"section" description:"INI Section" value-name:"SECTION" required:"yes"` 12 | Key string `short:"k" long:"key" description:"INI Key to set" value-name:"KEY" required:"yes"` 13 | Value string `short:"v" long:"value" description:"Value to store" value-name:"VALUE"` 14 | Boolean bool `short:"b" long:"boolean" description:"Create a boolean key"` 15 | Args struct { 16 | File string `positional-arg-name:"file"` 17 | } `positional-args:"yes" required:"yes"` 18 | } 19 | 20 | // NewINIFileSetCmd returns a new INIFileSetCmd 21 | func NewINIFileSetCmd() *INIFileSetCmd { 22 | return &INIFileSetCmd{} 23 | } 24 | 25 | // Execute runs the ini set command 26 | func (c *INIFileSetCmd) Execute(args []string) error { 27 | var v interface{} 28 | if c.Boolean { 29 | v = true 30 | } else { 31 | v = c.Value 32 | } 33 | return iniFileSet(c.Args.File, c.Section, c.Key, v) 34 | } 35 | 36 | // INIFileGetCmd defines a ini get command operation 37 | type INIFileGetCmd struct { 38 | Section string `short:"s" long:"section" description:"INI Section" value-name:"SECTION" required:"yes"` 39 | Key string `short:"k" long:"key" description:"INI Key to get" value-name:"KEY" required:"yes"` 40 | Args struct { 41 | File string `positional-arg-name:"file"` 42 | } `positional-args:"yes" required:"yes"` 43 | OutWriter io.Writer 44 | } 45 | 46 | // NewINIFileGetCmd returns a new INIFileGetCmd 47 | func NewINIFileGetCmd() *INIFileGetCmd { 48 | return &INIFileGetCmd{OutWriter: os.Stdout} 49 | } 50 | 51 | // Execute runs the ini get command 52 | func (c *INIFileGetCmd) Execute(args []string) error { 53 | v, err := iniFileGet(c.Args.File, c.Section, c.Key) 54 | if err != nil { 55 | return err 56 | } 57 | fmt.Fprint(c.OutWriter, v) 58 | return nil 59 | } 60 | 61 | // INIFileDelCmd defines a ini del command operation 62 | type INIFileDelCmd struct { 63 | Section string `short:"s" long:"section" description:"INI Section" value-name:"SECTION" required:"yes"` 64 | Key string `short:"k" long:"key" description:"INI Key to delete" value-name:"KEY" required:"yes"` 65 | Args struct { 66 | File string `positional-arg-name:"file"` 67 | } `positional-args:"yes" required:"yes"` 68 | } 69 | 70 | // NewINIFileDelCmd returns a new INIFileDelCmd 71 | func NewINIFileDelCmd() *INIFileDelCmd { 72 | return &INIFileDelCmd{} 73 | } 74 | 75 | // Execute runs the ini del command 76 | func (c *INIFileDelCmd) Execute(args []string) error { 77 | return iniFileDel(c.Args.File, c.Section, c.Key) 78 | } 79 | -------------------------------------------------------------------------------- /cmd_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | tu "github.com/bitnami/gonit/testutils" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | type iniTestValue struct { 15 | globalOpts *Options 16 | section string 17 | key string 18 | value string 19 | isBoolean bool 20 | } 21 | type iniSetTest struct { 22 | name string 23 | values []iniTestValue 24 | initialText string 25 | expectedText string 26 | createIniFile bool 27 | expectedErr interface{} 28 | } 29 | 30 | type iniGetTest struct { 31 | name string 32 | iniTestValue 33 | initialText string 34 | createIniFile bool 35 | expectedErr interface{} 36 | } 37 | 38 | type iniDelTest struct { 39 | name string 40 | values []iniTestValue 41 | initialText string 42 | expectedText string 43 | createIniFile bool 44 | expectedErr interface{} 45 | } 46 | 47 | var delTests = []iniDelTest{ 48 | { 49 | name: "Deletes key non existent", 50 | values: []iniTestValue{ 51 | { 52 | section: "general", key: "mykey", 53 | }, 54 | }, 55 | createIniFile: true, 56 | expectedText: ``, 57 | }, 58 | { 59 | name: "Deletes boolean value", 60 | initialText: "[general]\nboolkey\nkey1=val1\n", 61 | values: []iniTestValue{ 62 | { 63 | section: "general", key: "boolkey", 64 | }, 65 | }, 66 | expectedText: `\[general\]\nkey1=val1\n\s*$`, 67 | }, 68 | { 69 | name: "Deletes key in file containing semicolon values in regular mode", 70 | values: []iniTestValue{ 71 | { 72 | section: "general", key: "key2", 73 | }, 74 | }, 75 | initialText: "[general]\nkey1=`my ; value`\nkey2=val2", 76 | expectedText: "\\[general\\]\nkey1=`my ; value`\n\\s*$", 77 | }, 78 | { 79 | name: "Deletes key in file containing semicolon values in regular mode (2)", 80 | values: []iniTestValue{ 81 | { 82 | section: "general", key: "key2", 83 | }, 84 | }, 85 | initialText: "[general]\nkey1=my ; my comment\nkey2=val2", 86 | expectedText: "\\[general\\]\n; my comment\nkey1=my\n\\s*$", 87 | }, 88 | { 89 | name: "Deletes key in file containing semicolon values in ignore-inline-comments mode", 90 | values: []iniTestValue{ 91 | { 92 | section: "general", key: "key2", globalOpts: &Options{IgnoreInlineComments: true}, 93 | }, 94 | }, 95 | initialText: "[general]\nkey1=`my ; value`\nkey2=val2", 96 | expectedText: "\\[general\\]\nkey1=my ; value\n\\s*$", 97 | }, 98 | { 99 | name: "Deletes key in file containing semicolon values in ignore-inline-comments mode (2)", 100 | values: []iniTestValue{ 101 | { 102 | section: "general", key: "key2", globalOpts: &Options{IgnoreInlineComments: true}, 103 | }, 104 | }, 105 | initialText: "[general]\nkey1=my ; my comment\nkey2=val2", 106 | expectedText: "\\[general\\]\nkey1=my ; my comment\n\\s*$", 107 | }, 108 | { 109 | name: "Deletes regular value", 110 | initialText: "[general]\nkey1=val1\nkey2=val2\n", 111 | values: []iniTestValue{ 112 | { 113 | section: "general", key: "key1", 114 | }, 115 | }, 116 | expectedText: `\[general\]\nkey2=val2\n\s*$`, 117 | }, 118 | { 119 | name: "Fails if ini file does not exists", 120 | values: []iniTestValue{{section: "general", key: "key1"}}, 121 | expectedErr: "no such file or directory", 122 | }, 123 | { 124 | name: "Preserve comments", 125 | createIniFile: true, 126 | initialText: "# this is a comment\n[general]\n# key 1 sample\nkey1=value1\n# mykey comment\nmykey=myvalue", 127 | values: []iniTestValue{ 128 | {section: "general", key: "key1"}, 129 | }, 130 | expectedText: `^# this is a comment\n\[general\]\n# mykey comment\nmykey=myvalue\n\s*$`, 131 | }, 132 | } 133 | var getTests = []iniGetTest{ 134 | { 135 | name: "Get non-existent", 136 | createIniFile: true, 137 | iniTestValue: iniTestValue{ 138 | section: "general", key: "mykey", value: "", 139 | }, 140 | }, 141 | { 142 | name: "Get regular key", 143 | initialText: "[general]\nmykey=myvalue\n", 144 | iniTestValue: iniTestValue{ 145 | section: "general", key: "mykey", value: "myvalue", 146 | }, 147 | }, 148 | { 149 | name: "Get value with semicolon in regular mode", 150 | initialText: "[general]\nmykey=`my ; value`\n", 151 | iniTestValue: iniTestValue{ 152 | section: "general", key: "mykey", value: "my ; value", 153 | }, 154 | }, 155 | { 156 | name: "Get value with semicolon in regular mode (2)", 157 | initialText: "[general]\nmykey=my ; this is a comment\n", 158 | iniTestValue: iniTestValue{ 159 | section: "general", key: "mykey", value: "my", 160 | }, 161 | }, 162 | { 163 | name: "Get value with semicolon in ignore-inline-comments mode", 164 | initialText: "[general]\nmykey=`my ; value`\n", 165 | iniTestValue: iniTestValue{ 166 | globalOpts: &Options{IgnoreInlineComments: true}, 167 | section: "general", key: "mykey", value: "my ; value", 168 | }, 169 | }, 170 | { 171 | name: "Get value with semicolon in ignore-inline-comments mode (2)", 172 | initialText: "[general]\nmykey=my ; this is a comment\n", 173 | iniTestValue: iniTestValue{ 174 | globalOpts: &Options{IgnoreInlineComments: true}, 175 | section: "general", key: "mykey", value: "my ; this is a comment", 176 | }, 177 | }, 178 | { 179 | name: "Get present boolean key", 180 | initialText: "[general]\nmykey\n", 181 | iniTestValue: iniTestValue{ 182 | section: "general", key: "mykey", value: "true", 183 | }, 184 | }, 185 | { 186 | name: "Fails if ini file does not exists", 187 | iniTestValue: iniTestValue{section: "general", key: "key1"}, 188 | expectedErr: "no such file or directory", 189 | }, 190 | { 191 | name: "Get from malformed file", 192 | createIniFile: true, 193 | initialText: "this is not a\nINI\nfile\nmykey\n[general]\nmykey=myvalue", 194 | iniTestValue: iniTestValue{ 195 | section: "general", key: "mykey", value: "myvalue", 196 | }, 197 | }, 198 | } 199 | var setTests = []iniSetTest{ 200 | { 201 | name: "Sets regular key non existent", 202 | values: []iniTestValue{ 203 | { 204 | section: "general", key: "mykey", value: "myvalue", 205 | }, 206 | }, 207 | expectedText: `mykey=myvalue\n`, 208 | }, 209 | { 210 | name: "Sets value with semicolon in regular mode", 211 | values: []iniTestValue{ 212 | { 213 | section: "general", key: "mykey", value: "my ; value", 214 | }, 215 | }, 216 | expectedText: "mykey=`my ; value`\n", 217 | }, 218 | { 219 | name: "Sets value with semicolon in ignore-inline-comments mode", 220 | values: []iniTestValue{ 221 | { 222 | globalOpts: &Options{IgnoreInlineComments: true}, 223 | section: "general", key: "mykey", value: "my ; value", 224 | }, 225 | }, 226 | expectedText: "mykey=my ; value\n", 227 | }, 228 | { 229 | name: "Sets boolean value", 230 | values: []iniTestValue{ 231 | { 232 | section: "testbool", key: "mykey", isBoolean: true, 233 | }, 234 | }, 235 | expectedText: `\[testbool\]\nmykey\n\s*$`, 236 | }, 237 | { 238 | name: "Override with boolean value", 239 | initialText: `\[testbool\]\nmykey=value1\n\s*$`, 240 | values: []iniTestValue{ 241 | { 242 | section: "testbool", key: "mykey", isBoolean: true, 243 | }, 244 | }, 245 | expectedText: `\[testbool\]\nmykey\n\s*$`, 246 | }, 247 | { 248 | name: "Override boolean value with regular one", 249 | initialText: `\[testbool\]\nmykey\n\s*$`, 250 | values: []iniTestValue{ 251 | { 252 | section: "testbool", key: "mykey", value: "myvalue", 253 | }, 254 | }, 255 | expectedText: `\[testbool\]\nmykey=myvalue\n\s*$`, 256 | }, 257 | { 258 | name: "Set value with duplicate key", 259 | initialText: ` 260 | [testduplicate] 261 | mykey=value1 262 | mykey=value2 263 | `, 264 | values: []iniTestValue{ 265 | { 266 | section: "testduplicate", key: "mykey", value: "value3", 267 | }, 268 | }, 269 | expectedText: `^\[testduplicate\]\nmykey=value1\nmykey=value2\nmykey=value3\n\s*$`, 270 | }, 271 | { 272 | name: "Set multiple keys", 273 | values: []iniTestValue{ 274 | {section: "general", key: "key1", value: "value1"}, 275 | {section: "general", key: "key2", value: "value2"}, 276 | {section: "general", key: "key3", value: "value3"}, 277 | {section: "general", key: "key4", isBoolean: true}, 278 | }, 279 | expectedText: `^\[general\]\nkey1=value1\nkey2=value2\nkey3=value3\nkey4\n\s*$`, 280 | }, 281 | { 282 | name: "Sets regular keys in existing file", 283 | values: []iniTestValue{ 284 | {section: "general", key: "mykey", value: "myvalue"}, 285 | {section: "general", key: "key2", value: "newvalue2"}, 286 | {section: "newsection", key: "key5", value: "value5"}, 287 | }, 288 | initialText: ` 289 | [general] 290 | key1=value1 291 | key2=value2 292 | key3=value3 293 | [newsection] 294 | key4=value4 295 | `, 296 | expectedText: `^\[general\]\nkey1=value1\nkey2=newvalue2\nkey3=value3\nmykey=myvalue\n\s+` + 297 | `\[newsection\]\nkey4=value4\nkey5=value5\n.*`, 298 | }, 299 | { 300 | name: "Preserve comments", 301 | createIniFile: true, 302 | initialText: "# this is a comment\n[general]\n# key 1 sample\nkey1=value1", 303 | values: []iniTestValue{ 304 | {section: "general", key: "mykey", value: "myvalue"}, 305 | }, 306 | expectedText: `^# this is a comment\n\[general\]\n# key 1 sample\nkey1=value1\nmykey=myvalue\n\s*$`, 307 | }, 308 | { 309 | name: "Preserve arrays", 310 | createIniFile: true, 311 | initialText: "[general]\narray[]=value1\narray[]=value2", 312 | values: []iniTestValue{ 313 | {section: "general", key: "mykey", value: "myvalue"}, 314 | }, 315 | expectedText: `^\[general\]\narray\[\]=value1\narray\[\]=value2\nmykey=myvalue\n\s*$`, 316 | }, 317 | } 318 | 319 | func testFile(t *testing.T, path string, fn func(t *testing.T, contents string) bool, msgAndArgs ...interface{}) bool { 320 | if !assert.FileExists(t, path) { 321 | return false 322 | } 323 | data, err := os.ReadFile(path) 324 | require.NoError(t, err) 325 | return fn(t, string(data)) 326 | } 327 | func AssertFileContains(t *testing.T, path string, expected interface{}, msgAndArgs ...interface{}) bool { 328 | return testFile(t, path, func(t *testing.T, contents string) bool { 329 | return assert.Regexp(t, expected, contents, msgAndArgs...) 330 | }) 331 | } 332 | func AssertFileDoesNotContain(t *testing.T, path string, expected interface{}, msgAndArgs ...interface{}) bool { 333 | return testFile(t, path, func(t *testing.T, contents string) bool { 334 | return assert.NotRegexp(t, expected, contents, msgAndArgs...) 335 | }) 336 | } 337 | func TestINIFileSetCmd_Execute(t *testing.T) { 338 | type testFn func(file, section, key, value string, isBoolean bool, opts *Options) error 339 | var testViaCommand = func(file, section, key, value string, isBoolean bool, opts *Options) error { 340 | cmd := NewINIFileSetCmd() 341 | cmd.Section = section 342 | cmd.Key = key 343 | cmd.Value = value 344 | cmd.Boolean = isBoolean 345 | cmd.Args.File = file 346 | if opts != nil { 347 | globalOpts = opts 348 | } else { 349 | globalOpts = &Options{} 350 | } 351 | return cmd.Execute([]string{}) 352 | } 353 | var testViaCli = func(file, section, key, value string, isBoolean bool, opts *Options) error { 354 | args := []string{"set", "-k", key, "-s", section, "-v", value} 355 | if isBoolean { 356 | args = append(args, "-b") 357 | } 358 | args = append(args, file) 359 | if opts != nil && opts.IgnoreInlineComments { 360 | args = append([]string{"--ignore-inline-comments"}, args...) 361 | } 362 | res := RunTool(args...) 363 | if !res.Success() { 364 | return fmt.Errorf("%s", res.Stderr()) 365 | } 366 | return nil 367 | } 368 | 369 | for _, tt := range setTests { 370 | for id, fn := range map[string]testFn{ 371 | "command": testViaCommand, 372 | "cli": testViaCli, 373 | } { 374 | var err error 375 | 376 | file := "" 377 | sb := tu.NewSandbox() 378 | defer sb.Cleanup() 379 | if tt.initialText != "" || tt.createIniFile { 380 | file, err = sb.Write("my.ini", tt.initialText) 381 | require.NoError(t, err) 382 | } else { 383 | file = sb.Normalize("my.ini") 384 | } 385 | testTitle := fmt.Sprintf("%s (via %s)", tt.name, id) 386 | t.Run(testTitle, func(t *testing.T) { 387 | for _, v := range tt.values { 388 | err = fn(file, v.section, v.key, v.value, v.isBoolean, v.globalOpts) 389 | if err != nil { 390 | break 391 | } 392 | } 393 | if tt.expectedErr != nil { 394 | if err == nil { 395 | t.Errorf("the command was expected to fail but succeeded") 396 | } else { 397 | assert.Regexp(t, tt.expectedErr, err) 398 | } 399 | } else { 400 | require.NoError(t, err) 401 | AssertFileContains(t, file, tt.expectedText) 402 | } 403 | }) 404 | } 405 | } 406 | } 407 | 408 | func TestINIFileGetCmd_Execute(t *testing.T) { 409 | type testFn func(file, section, key string, opts *Options) (string, error) 410 | var testViaCommand = func(file, section, key string, opts *Options) (string, error) { 411 | b := &bytes.Buffer{} 412 | cmd := NewINIFileGetCmd() 413 | cmd.Section = section 414 | cmd.Key = key 415 | cmd.Args.File = file 416 | cmd.OutWriter = b 417 | if opts != nil { 418 | globalOpts = opts 419 | } else { 420 | globalOpts = &Options{} 421 | } 422 | err := cmd.Execute([]string{}) 423 | stdout := b.String() 424 | return stdout, err 425 | } 426 | var testViaCli = func(file, section, key string, opts *Options) (string, error) { 427 | args := []string{"get", "-k", key, "-s", section, file} 428 | if opts != nil && opts.IgnoreInlineComments { 429 | args = append([]string{"--ignore-inline-comments"}, args...) 430 | } 431 | res := RunTool(args...) 432 | stdout := res.Stdout() 433 | var err error 434 | if !res.Success() { 435 | err = fmt.Errorf("%s", res.Stderr()) 436 | } 437 | return stdout, err 438 | } 439 | 440 | for _, tt := range getTests { 441 | for id, fn := range map[string]testFn{ 442 | "command": testViaCommand, 443 | "cli": testViaCli, 444 | } { 445 | var err error 446 | file := "" 447 | sb := tu.NewSandbox() 448 | defer sb.Cleanup() 449 | if tt.initialText != "" || tt.createIniFile { 450 | file, err = sb.Write("my.ini", tt.initialText) 451 | require.NoError(t, err) 452 | } else { 453 | file = sb.Normalize("my.ini") 454 | } 455 | testTitle := fmt.Sprintf("%s (via %s)", tt.name, id) 456 | 457 | t.Run(testTitle, func(t *testing.T) { 458 | 459 | stdout, err := fn(file, tt.section, tt.key, tt.globalOpts) 460 | 461 | if tt.expectedErr != nil { 462 | if err == nil { 463 | t.Errorf("the command was expected to fail but succeeded") 464 | } else { 465 | assert.Regexp(t, tt.expectedErr, err) 466 | } 467 | } else { 468 | require.NoError(t, err) 469 | assert.Equal(t, tt.value, stdout) 470 | } 471 | }) 472 | } 473 | } 474 | } 475 | 476 | func TestINIFileDelCmd_Execute(t *testing.T) { 477 | type testFn func(file, section, key string, opts *Options) error 478 | var testViaCommand = func(file, section, key string, opts *Options) error { 479 | cmd := NewINIFileDelCmd() 480 | cmd.Section = section 481 | cmd.Key = key 482 | cmd.Args.File = file 483 | if opts != nil { 484 | globalOpts = opts 485 | } else { 486 | globalOpts = &Options{} 487 | } 488 | return cmd.Execute([]string{}) 489 | } 490 | var testViaCli = func(file, section, key string, opts *Options) error { 491 | args := []string{"del", "-k", key, "-s", section, file} 492 | if opts != nil && opts.IgnoreInlineComments { 493 | args = append([]string{"--ignore-inline-comments"}, args...) 494 | } 495 | res := RunTool(args...) 496 | if !res.Success() { 497 | return fmt.Errorf("%s", res.Stderr()) 498 | } 499 | return nil 500 | } 501 | for _, tt := range delTests { 502 | for id, fn := range map[string]testFn{ 503 | "command": testViaCommand, 504 | "cli": testViaCli, 505 | } { 506 | var err error 507 | file := "" 508 | sb := tu.NewSandbox() 509 | defer sb.Cleanup() 510 | if tt.initialText != "" || tt.createIniFile { 511 | file, err = sb.Write("my.ini", tt.initialText) 512 | require.NoError(t, err) 513 | } else { 514 | file = sb.Normalize("my.ini") 515 | } 516 | testTitle := fmt.Sprintf("%s (via %s)", tt.name, id) 517 | 518 | t.Run(testTitle, func(t *testing.T) { 519 | for _, v := range tt.values { 520 | 521 | err = fn(file, v.section, v.key, v.globalOpts) 522 | if err != nil { 523 | break 524 | } 525 | } 526 | if tt.expectedErr != nil { 527 | if err == nil { 528 | t.Errorf("the command was expected to fail but succeeded") 529 | } else { 530 | assert.Regexp(t, tt.expectedErr, err) 531 | } 532 | } else { 533 | require.NoError(t, err) 534 | AssertFileContains(t, file, tt.expectedText) 535 | } 536 | }) 537 | } 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bitnami/ini-file 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/bitnami/gonit v0.2.0 7 | github.com/go-ini/ini v1.67.0 8 | github.com/jessevdk/go-flags v1.6.1 9 | github.com/juamedgod/cliassert v0.0.0-20180320011200-425256f2bb0b 10 | github.com/stretchr/testify v1.2.2 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | golang.org/x/sys v0.21.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bitnami/gonit v0.2.0 h1:qrza7cn2/YmX47SCIBKShqtupSNX4KSwaQJL698s2WU= 2 | github.com/bitnami/gonit v0.2.0/go.mod h1:rWUClL7qwHeUIIP7SkokjfvE3RonglnANqJtzUpyHyU= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= 6 | github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 7 | github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= 8 | github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= 9 | github.com/juamedgod/cliassert v0.0.0-20180320011200-425256f2bb0b h1:j2GwaFh0vduPA3PilmBcsdWv0reobJHyIxGJnh9gIAA= 10 | github.com/juamedgod/cliassert v0.0.0-20180320011200-425256f2bb0b/go.mod h1:+N11eVKRhj1RNqjc9l+QMib0/XYixFOywXFH1zoGucg= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 14 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 15 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 16 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 17 | -------------------------------------------------------------------------------- /ini.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/go-ini/ini" 8 | ) 9 | 10 | func init() { 11 | // Maintain the original file format 12 | ini.PrettyFormat = false 13 | } 14 | 15 | func loadOptions() ini.LoadOptions { 16 | return ini.LoadOptions{ 17 | // Support mysql-style "boolean" values - a key wth no value. 18 | AllowBooleanKeys: true, 19 | // Support preserving arrays in original document 20 | AllowShadows: true, 21 | IgnoreInlineComment: globalOpts.IgnoreInlineComments, 22 | } 23 | } 24 | 25 | // iniLoad attempts to load the ini file. 26 | func iniLoad(filename string) (*ini.File, error) { 27 | return ini.LoadSources( 28 | loadOptions(), 29 | filename, 30 | ) 31 | } 32 | 33 | // iniLoadOrEmpty attempts to load the ini file. If it does not exists, 34 | // it will return an empty one 35 | func iniLoadOrEmpty(filename string) (*ini.File, error) { 36 | f, err := iniLoad(filename) 37 | if err == nil { 38 | return f, nil 39 | } 40 | if os.IsNotExist(err) { 41 | return ini.Empty(loadOptions()), nil 42 | } 43 | return nil, err 44 | } 45 | 46 | // iniSave writes the ini file to the named file. 47 | func iniSave(filename string, iniFile *ini.File) error { 48 | // The third argument, perm, is ignored when the file doesn't exist 49 | // So we can safely set it to '0644', it won't modify the existing permissions 50 | // if the file exists. 51 | f, err := os.OpenFile(filename, os.O_SYNC|os.O_RDWR|os.O_CREATE, os.FileMode(0644)) 52 | if err != nil { 53 | return err 54 | } 55 | // Clear file content 56 | err = f.Truncate(0) 57 | if err != nil { 58 | return err 59 | } 60 | _, err = iniFile.WriteTo(f) 61 | if err != nil { 62 | return err 63 | } 64 | return f.Close() 65 | } 66 | 67 | func iniFileGet(file string, s string, key string) (string, error) { 68 | iniFile, err := iniLoad(file) 69 | if err != nil { 70 | return "", err 71 | } 72 | section := iniFile.Section(s) 73 | if !section.HasKey(key) { 74 | return "", nil 75 | } 76 | k, err := section.GetKey(key) 77 | if err != nil { 78 | return "", err 79 | } 80 | return k.String(), nil 81 | } 82 | 83 | func iniFileSet(file string, s string, key string, value interface{}) error { 84 | iniFile, err := iniLoadOrEmpty(file) 85 | if err != nil { 86 | return err 87 | } 88 | section := iniFile.Section(s) 89 | switch v := value.(type) { 90 | case string: 91 | if section.HasKey(key) && len(section.Key(key).ValueWithShadows()) == 1 { 92 | section.Key(key).SetValue(v) 93 | } else { 94 | section.NewKey(key, v) 95 | } 96 | case bool: 97 | section.NewBooleanKey(key) 98 | default: 99 | return fmt.Errorf("invalid key type %T", v) 100 | } 101 | return iniSave(file, iniFile) 102 | } 103 | 104 | func iniFileDel(file string, s string, key string) error { 105 | iniFile, err := iniLoad(file) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | section := iniFile.Section(s) 111 | if !section.HasKey(key) { 112 | return nil 113 | } 114 | section.DeleteKey(key) 115 | return iniSave(file, iniFile) 116 | } 117 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | flags "github.com/jessevdk/go-flags" 8 | ) 9 | 10 | // Options defines options supported by all subcommands 11 | type Options struct { 12 | IgnoreInlineComments bool `long:"ignore-inline-comments" description:"Ignore inline comments"` 13 | } 14 | 15 | var globalOpts = &Options{} 16 | 17 | var ( 18 | version = "1.4.7" 19 | buildDate = "" 20 | commit = "" 21 | ) 22 | 23 | func versionText() string { 24 | msg := fmt.Sprintf("%-12s %s", "Version:", version) 25 | if buildDate != "" { 26 | msg += fmt.Sprintf("\n%-12s %s", "Built on:", buildDate) 27 | } 28 | if commit != "" { 29 | msg += fmt.Sprintf("\n%-12s %s", "Git Commit:", commit) 30 | } 31 | return msg 32 | } 33 | 34 | func main() { 35 | setCmd := NewINIFileSetCmd() 36 | getCmd := NewINIFileGetCmd() 37 | delCmd := NewINIFileDelCmd() 38 | 39 | parser := flags.NewParser(globalOpts, flags.HelpFlag|flags.PassDoubleDash) 40 | 41 | parser.LongDescription = versionText() 42 | 43 | parser.AddCommand("set", "INI File Set", "Sets values in a INI file", setCmd) 44 | parser.AddCommand("get", "INI FILE Get", "Gets values from a INI file", getCmd) 45 | parser.AddCommand("del", "INI FILE Delete", "Deletes values from a INI file", delCmd) 46 | 47 | _, err := parser.Parse() 48 | if err != nil { 49 | fmt.Fprintf(os.Stderr, "%v\n", err) 50 | os.Exit(1) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "testing" 7 | 8 | ca "github.com/juamedgod/cliassert" 9 | ) 10 | 11 | func Test_main(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | wantErr bool 15 | stdin string 16 | expectedErr interface{} 17 | expectedResult string 18 | }{} 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | args := []string{} 22 | res := runTool(os.Args[0], args, "") 23 | if tt.wantErr { 24 | if res.Success() { 25 | t.Errorf("the command was expected to fail but succeeded") 26 | } else if tt.expectedErr != nil { 27 | res.AssertErrorMatch(t, tt.expectedErr) 28 | } 29 | } else { 30 | res.AssertSuccessMatch(t, tt.expectedResult) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func TestMain(m *testing.M) { 37 | if os.Getenv("BE_TOOL") == "1" { 38 | main() 39 | os.Exit(0) 40 | return 41 | } 42 | flag.Parse() 43 | c := m.Run() 44 | os.Exit(c) 45 | } 46 | 47 | func runTool(bin string, args []string, stdin string) ca.CmdResult { 48 | cmd := ca.NewCommand() 49 | if stdin != "" { 50 | cmd.SetStdin(stdin) 51 | } 52 | os.Setenv("BE_TOOL", "1") 53 | defer os.Unsetenv("BE_TOOL") 54 | return cmd.Exec(bin, args...) 55 | } 56 | 57 | func RunTool(args ...string) ca.CmdResult { 58 | return runTool(os.Args[0], args, "") 59 | } 60 | -------------------------------------------------------------------------------- /vars.mk: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | GOPATH ?= $(shell go env GOPATH) 3 | PATH := $(GOPATH)/bin:$(PATH) 4 | 5 | BUILD_DIR := $(abspath ./out) 6 | TOOL_NAME ?= $(shell basename $(CURDIR)) 7 | TOOL_PATH ?= $(BUILD_DIR)/$(TOOL_NAME) 8 | 9 | BUILD_DATE := $(shell date -u '+%Y-%m-%d %I:%M:%S UTC' 2> /dev/null) 10 | GIT_HASH := $(shell git rev-parse HEAD 2> /dev/null) 11 | LDFLAGS="-X main.commit=$(GIT_HASH) -X 'main.buildDate=$(BUILD_DATE)' -s -w" 12 | 13 | DEBUG ?= 0 14 | 15 | ifeq ($(DEBUG),1) 16 | GO_TEST := @go test -v 17 | else 18 | GO_TEST := @go test 19 | endif 20 | 21 | GO_MOD := @go mod 22 | # Do not do goimport of the vendor dir 23 | go_files=$$(find $(1) -type f -name '*.go' -not -path "./vendor/*") 24 | fmtcheck = @if goimports -l $(go_files) | read var; then echo "goimports check failed for $(1):\n `goimports -d $(go_files)`"; exit 1; fi 25 | --------------------------------------------------------------------------------