├── .github ├── FUNDING.yml └── workflows │ ├── CD.yml │ └── CI.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── .yarnclean ├── CHANGELOG.md ├── LICENSE ├── README.md ├── image └── shell_format.gif ├── logo.png ├── logo.psd ├── package.json ├── renovate.json ├── src ├── config.ts ├── diffUtils.ts ├── downloader.ts ├── extension.ts ├── pathUtil.ts └── shFormat.ts ├── test ├── index.ts ├── runTest.ts ├── suite │ ├── downloader.test.ts │ ├── extension.test.ts │ ├── index.ts │ └── shfmt.test.ts ├── supported │ ├── .env │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── Dockerfile │ ├── application.properties │ ├── azure.azcli │ ├── bats.bats │ ├── error.sh │ ├── getacme.sh │ ├── hosts │ └── idea.vmoptions └── test.sh ├── tsconfig.json ├── typings └── node │ └── diff.d.ts ├── webpack.config.js └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # foxundermoon # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | # patreon: foxmn # Replace with a single Patreon username 5 | open_collective: vsformat # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: foxundermoon # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # custom: [] # 'https://www.paypal.me/foxmn' Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/CD.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | unit-test: 10 | name: Test 11 | runs-on: ${{ matrix.config.os }} # we run many different builds 12 | strategy: 13 | matrix: 14 | config: 15 | - os: ubuntu-latest 16 | - os: macos-latest 17 | - os: windows-latest 18 | steps: 19 | - name: Get yarn cache directory path 20 | id: yarn-cache-dir-path 21 | run: echo "::set-output name=dir::$(yarn cache dir)" 22 | 23 | - uses: actions/cache@v2 24 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 25 | with: 26 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 27 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-yarn- 30 | - uses: actions/checkout@v2 31 | - uses: actions/setup-node@v2 32 | with: 33 | node-version: '16' 34 | - run: yarn 35 | - run: yarn format-check 36 | - run: yarn test-compile 37 | - run: yarn compile 38 | # https://github.com/GabrielBB/xvfb-action 39 | - name: Run headless test 40 | uses: GabrielBB/xvfb-action@v1 41 | with: 42 | run: yarn test 43 | release: 44 | name: release 45 | runs-on: ubuntu-latest 46 | needs: unit-test 47 | steps: 48 | - name: Get yarn cache directory path 49 | id: yarn-cache-dir-path 50 | run: echo "::set-output name=dir::$(yarn cache dir)" 51 | 52 | - uses: actions/cache@v2 53 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 54 | with: 55 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 56 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 57 | restore-keys: | 58 | ${{ runner.os }}-yarn- 59 | - uses: actions/checkout@v2 60 | - uses: actions/setup-node@v2 61 | with: 62 | node-version: '16' 63 | - name: read version 64 | run: | 65 | PACKAGE_VERSION=$(node -p "require('./package.json').version") 66 | echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV 67 | echo "PACKAGE_VERSION=$PACKAGE_VERSION" 68 | PUBLISHED_VERSION=$(curl 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery' \ 69 | -H 'origin: https://marketplace.visualstudio.com' \ 70 | -H 'pragma: no-cache' \ 71 | -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36' \ 72 | -H 'content-type: application/json' \ 73 | -H 'accept: application/json;api-version=5.1-preview.1;excludeUrls=true' \ 74 | -H 'cache-control: no-cache' \-H 'authority: marketplace.visualstudio.com' \ 75 | -H 'referer: https://marketplace.visualstudio.com/items?itemName=foxundermoon.shell-format' \ 76 | --data-binary '{"assetTypes":null,"filters":[{"criteria":[{"filterType":7,"value":"foxundermoon.shell-format"}],"direction":2,"pageSize":1,"pageNumber":1,"sortBy":0,"sortOrder":0,"pagingToken":null}],"flags":71}' | 77 | jq '.results[0].extensions[0].versions[0].version') 78 | echo "PUBLISHED_VERSION=$PUBLISHED_VERSION" 79 | if [ "$PACKAGE_VERSION" = "$PUBLISHED_VERSION" ]; then 80 | echo 'niddend published' 81 | echo "NEED_RELEASE=no" >> $GITHUB_ENV 82 | else 83 | echo "need publish" 84 | echo "NEED_RELEASE=yes" >> $GITHUB_ENV 85 | fi 86 | - run: yarn 87 | if: env.NEED_RELEASE == 'yes' 88 | - name: mini changelog 89 | if: env.NEED_RELEASE == 'yes' 90 | id: minichangelog 91 | run: | 92 | VERSION_REGEX="## $(echo ${{ env.PACKAGE_VERSION }} | sed 's/\./\\./g')" 93 | sed -n "/$VERSION_REGEX/,/## /p" CHANGELOG.md | sed '$d' > minichangelog.txt 94 | cat minichangelog.txt 95 | # echo "::set-output name=minichangelog::$(cat minichangelog.txt) 96 | echo "MINI_CHANGELOG<> $GITHUB_ENV 97 | cat minichangelog.txt >> $GITHUB_ENV 98 | echo "EOF" >> $GITHUB_ENV 99 | - name: check changelog 100 | if: env.NEED_RELEASE == 'yes' 101 | run: | 102 | LINE_COUNT=$(cat minichangelog.txt | wc -l) 103 | if [ "$LINE_COUNT" -lt 3 ]; then 104 | echo Mini changelog is too short. Did you use the wrong version number in CHANGELOG.txt? 105 | exit 1 106 | fi 107 | - name: package 108 | if: env.NEED_RELEASE == 'yes' 109 | run: | 110 | yarn package 111 | echo FILE_NAME=$(node -p "require('./package.json').name")-${{ env.PACKAGE_VERSION }}.vsix >> $GITHUB_ENV 112 | - name: create github release 113 | if: env.NEED_RELEASE == 'yes' 114 | id: create_release 115 | uses: actions/create-release@master 116 | env: 117 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 118 | with: 119 | tag_name: ${{ env.PACKAGE_VERSION }} 120 | release_name: Release ${{ env.PACKAGE_VERSION }} 121 | body: | 122 | ${{ env.MINI_CHANGELOG }} 123 | draft: false 124 | prerelease: false 125 | - name: Upload Release Asset 126 | if: env.NEED_RELEASE == 'yes' 127 | id: upload-release-asset 128 | uses: actions/upload-release-asset@v1.0.1 129 | env: 130 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 131 | with: 132 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 133 | asset_path: ./${{ env.FILE_NAME }} 134 | asset_name: ${{ env.FILE_NAME }} 135 | asset_content_type: application/gzip 136 | - name: Vscode release plugin 137 | if: env.NEED_RELEASE == 'yes' 138 | run: | 139 | yarn vsce publish -p ${{ secrets.PUBLISHER_TOKEN }} 140 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | paths-ignore: 5 | - CHANGELOG.md 6 | - README.md 7 | pull_request: 8 | paths-ignore: 9 | - CHANGELOG.md 10 | - README.md 11 | 12 | jobs: 13 | unit-test: 14 | name: Test 15 | runs-on: ${{ matrix.config.os }} # we run many different builds 16 | strategy: 17 | matrix: 18 | config: 19 | - os: ubuntu-latest 20 | - os: macos-latest 21 | - os: windows-latest 22 | steps: 23 | - name: Get yarn cache directory path 24 | id: yarn-cache-dir-path 25 | run: echo "::set-output name=dir::$(yarn cache dir)" 26 | 27 | - uses: actions/cache@v2 28 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 29 | with: 30 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 31 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 32 | restore-keys: | 33 | ${{ runner.os }}-yarn- 34 | - uses: actions/checkout@v2 35 | - uses: actions/setup-node@v2 36 | with: 37 | node-version: '16' 38 | - run: yarn 39 | - run: yarn format-check 40 | - run: yarn test-compile 41 | - run: yarn compile 42 | # https://github.com/GabrielBB/xvfb-action 43 | - name: Run headless test 44 | uses: GabrielBB/xvfb-action@v1 45 | with: 46 | run: yarn test 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test 4 | .DS_Store 5 | *.vsix 6 | branch.txt 7 | minichangelog.txt 8 | version.txt 9 | dist 10 | bin -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | image 3 | dist 4 | bin 5 | .vscode-test -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["eamodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": ["${workspaceRoot}/dist/**/*.js"], 14 | "preLaunchTask": "npm: watch" 15 | }, 16 | { 17 | "name": "Launch Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": [ 22 | "--extensionDevelopmentPath=${workspaceRoot}", 23 | "--extensionTestsPath=${workspaceRoot}/out/test" 24 | ], 25 | "stopOnEntry": false, 26 | "sourceMaps": true, 27 | "outFiles": ["${workspaceRoot}/out/test/**/*.js"], 28 | "preLaunchTask": "npm: test-compile" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "files.associations": { 10 | ".azure-pipelines/*.yml": "azure-pipelines" 11 | }, 12 | "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | // A task runner that calls a custom npm script that compiles the extension. 9 | { 10 | "version": "2.0.0", 11 | "tasks": [ 12 | { 13 | "type": "npm", 14 | "script": "watch", 15 | "presentation": { 16 | "echo": true, 17 | "reveal": "never", 18 | "focus": false, 19 | "panel": "shared", 20 | "showReuseMessage": true, 21 | "clear": false 22 | }, 23 | "problemMatcher": ["$ts-webpack"], 24 | "isBackground": true 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/*.map 2 | .azure-pipelines/** 3 | .git 4 | .gitignore 5 | .pipelines/** 6 | .vscode 7 | .vscode-test/** 8 | .vscode/** 9 | .vsts-ci.yml 10 | .yarnclean 11 | CHANGELOG.md 12 | image/* 13 | logo.psd 14 | node_modules 15 | out/ 16 | out/test/** 17 | src/ 18 | src/** 19 | test/** 20 | tsconfig.json 21 | tslint.json 22 | typings 23 | vsc-extension-quickstart.md 24 | webpack.config.json 25 | yarn.lock 26 | bin 27 | logo.psd 28 | package-lock.json 29 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | assets 13 | 14 | # examples 15 | example 16 | examples 17 | 18 | # code coverage directories 19 | coverage 20 | .nyc_output 21 | 22 | # build scripts 23 | Makefile 24 | Gulpfile.js 25 | Gruntfile.js 26 | 27 | # configs 28 | .tern-project 29 | .gitattributes 30 | .editorconfig 31 | .*ignore 32 | .eslintrc 33 | .jshintrc 34 | .flowconfig 35 | .documentup.json 36 | .yarn-metadata.json 37 | .*.yml 38 | *.yml 39 | 40 | # misc 41 | *.gz 42 | *.md 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Check [Keep a Changelog](https://keepachangelog.com/) for recommendations on how to structure this file. 4 | 5 | ## 7.2.5 6 | 7 | - fix rating badge 8 | 9 | ## 7.2.4 10 | 11 | - fix badge 12 | 13 | ## 7.2.3 14 | 15 | - bump shfmt to 3.6.0 16 | 17 | ## 7.2.2 18 | 19 | - Fix: substitule "${workspaceFolder}" in need install check #243 20 | 21 | ## 7.2.1 22 | 23 | - publish by github action 24 | 25 | ## 7.2.0 26 | 27 | - bats files support 28 | 29 | ## 7.1.1 30 | 31 | - bump shfmt to 3.3.1 32 | 33 | ## 7.1.0 34 | 35 | - bump shfmt to 3.2.4 36 | - support apple m1 chip #136 37 | - support editor config 38 | 39 | ## 7.0.1 40 | 41 | - bump shfmt to 3.0.1 #65 42 | 43 | ## 7.0.0 44 | 45 | - bump shfmt to 3.0.0 46 | - add prettier and husky to project 47 | 48 | ## 6.1.3 49 | 50 | - fix Strange file permissions for binary file [#50](https://github.com/foxundermoon/vs-shell-format/issues/50) 51 | 52 | ## 6.1.2 53 | 54 | - fix check install bug [#46](https://github.com/foxundermoon/vs-shell-format/issues/46) 55 | 56 | ## 6.1.1 57 | 58 | - add effect language 59 | 60 | ## 6.1.0 61 | 62 | - Reduce output information influx [#43](https://github.com/foxundermoon/vs-shell-format/pull/43) 63 | - azure cli support 64 | 65 | ## 6.0.1 66 | 67 | - fix issue[#40](https://github.com/foxundermoon/vs-shell-format/issues/40) Cursor focus is stolen by update 68 | 69 | ## 6.0.0 70 | 71 | - Let the plugin work out of the box and automatically download the shfmt of the corresponding platform. 72 | - No longer get shfmt from PATH 73 | 74 | ## 5.0.1 75 | 76 | - support both `-i` flag and `editor.tabSize` setting 77 | 78 | ## 5.0.0 79 | 80 | - display format error on problems panel [#37](https://github.com/foxundermoon/vs-shell-format/issues/37) 81 | - remove showError configuration 82 | 83 | ## 4.0.11 84 | 85 | - resolve 1 vulnerability https://www.npmjs.com/advisories/803 86 | 87 | ## 4.0.10 88 | 89 | - remove time on change log, record by github release 90 | 91 | ## [4.0.9] 2019-04-30 92 | 93 | - add license [#35](https://github.com/foxundermoon/vs-shell-format/issues/35) 94 | - shot changelog 95 | 96 | ## [4.0.8] 2019-04-30 97 | 98 | - add github auto release 99 | 100 | ## [4.0.7] 2019-04-30 101 | 102 | - add azure pipelines for auto deploy 103 | 104 | ## [4.0.6] 2019-04-30 105 | 106 | - temporary disable linux auto download shfmt for ubuntu 107 | 108 | ## [4.0.5] 2019-04-09 109 | 110 | - fix always auto downoad 111 | 112 | ## [4.0.4] 2019-01-17 113 | 114 | - add format options supported [#24](!https://github.com/foxundermoon/vs-shell-format/pull/24) 115 | 116 | ## [4.0.3] 2019-01-17 117 | 118 | - new gif 119 | - add supported files for test 120 | 121 | ## [4.0.0] 2019-01-17 122 | 123 | - fix doc ,adapter spring properties 124 | 125 | ## [3.0.0] 2019-01-16 126 | 127 | - auto install dependencies `shfmt` for macos and linux 128 | 129 | ## [2.0.4] 2019-01-16 130 | 131 | - add hosts、properties、.gitignore、.dockerignore、 jvmoptions file surport 132 | 133 | ## [2.0.2] 2019-01-14 134 | 135 | - change logo 136 | 137 | ## [2.0.2] 2018-12-17 138 | 139 | - fix #23 bug 140 | 141 | ## [2.0.0] - 2018-11-29 142 | 143 | - replace command `shell.format.shfmt` with `editor.action.formatDocument` 144 | - replace configuration `shellformat.runOnSave` with `wditor.formatOnSave` 145 | - fix the [issue bug #18](https://github.com/foxundermoon/vs-shell-format/issues/18) 146 | - add dotenv file support 147 | 148 | ## [1.1.2] - 2018-4-17 149 | 150 | - add setting config `"shellformat.runOnSave":false` 151 | - add Dockerfile support. 152 | 153 | ## [1.1.0] - 2017-10-07 154 | 155 | - add setting config `"shellformat.showError":true` 156 | - help you location error, you can set false to off the error tips. 157 | - change format by child_process spawn . better performance when large file. 158 | - add donate link . thank for your donate. 159 | 160 | ## [1.0.0] - 2017-04-06 161 | 162 | - add command flag configuration 163 | 164 | ## [0.1.2] - 2017-01-07 165 | 166 | - add icon & gif 167 | 168 | ## [0.1.1] - 2017-01-06 169 | 170 | - fix document 171 | 172 | ## [0.1.0] - 2017-01-06 173 | 174 | - change format base on TextDocument 175 | 176 | ## [0.0.1] - 2017-01-05 177 | 178 | - add shell format base on file 179 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017 vs-shell-format 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Get it on the VS Code Marketplace!](https://marketplace.visualstudio.com/items?itemName=foxundermoon.shell-format) 2 | 3 | # supported file types or languages 4 | 5 | | language | extension | describe | 6 | | ----------- | ------------------------ | --------------------- | 7 | | shellscript | .sh .bash | shell script files | 8 | | dockerfile | Dockerfile | docker files | 9 | | ignore | .gitignore .dockerignore | ignore files | 10 | | properties | .properties | java properties files | 11 | | jvmoptions | .vmoptions , jvm.options | jvm options file | 12 | | hosts | /etc/hosts | hosts file | 13 | | bats | .bats | Bats test file | 14 | 15 | --- 16 | 17 | | | | | | 18 | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | 19 | | ![version](https://img.shields.io/visual-studio-marketplace/v/foxundermoon.shell-format?style=flat-square) | ![downloads](https://img.shields.io/visual-studio-marketplace/d/foxundermoon.shell-format?style=flat-square) | ![install](https://img.shields.io/visual-studio-marketplace/i/foxundermoon.shell-format?style=flat-square) | ![ratings](https://img.shields.io/visual-studio-marketplace/stars/foxundermoon.shell-format?style=flat-square) | 20 | | [![Financial Contributors on Open Collective](https://opencollective.com/vsformat/all/badge.svg?label=financial+contributors)](https://opencollective.com/vsformat) ![LICENSE](https://img.shields.io/badge/license-mit-blue.svg) | ![LICENSE](https://img.shields.io/badge/license-Anti%20996-blue.svg) | ![star](https://img.shields.io/github/stars/foxundermoon/vs-shell-format.svg) | ![forks](https://img.shields.io/github/forks/foxundermoon/vs-shell-format.svg) | 21 | 22 | --- 23 | 24 | | build | release | 25 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | 26 | | [![CI](https://github.com/foxundermoon/vs-shell-format/actions/workflows/CI.yml/badge.svg)](https://github.com/foxundermoon/vs-shell-format/actions/workflows/CI.yml) | [![RELEASE](https://github.com/foxundermoon/vs-shell-format/actions/workflows/CD.yml/badge.svg)](https://github.com/foxundermoon/vs-shell-format/actions/workflows/CD.yml) | 27 | 28 | [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/vs-shell-format/community) 29 | 30 | --- 31 | 32 | ![screenshot](https://github.com/foxundermoon/vs-shell-format/raw/master/image/shell_format.gif) 33 | 34 | ## usage 35 | 36 | shift+option+f 37 | 38 | shift+command+p then type `Format Document` 39 | 40 | ## dependencies 41 | 42 | - [shfmt](https://github.com/mvdan/sh#shfmt) 43 | 44 | ## custom configuration 45 | 46 | - `shellformat.path` the shfmt fullpath example [mac,linux]: `/usr/local/bin/shfmt` [windows]: `C:\\bin\\shfmt.exe` 47 | - `shellformat.flag` shfmt -h to see detailed usage. 48 | 49 | --- 50 | 51 | ## Links 52 | 53 | ### [source code](https://github.com/foxundermoon/vs-shell-format) 54 | 55 | ### [shfmt](https://github.com/mvdan/sh) 56 | 57 | **Enjoy shellscript!** 58 | 59 | ## Contributors 60 | 61 | ### Code Contributors 62 | 63 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 64 | 65 | 66 | ### Financial Contributors 67 | 68 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/vsformat/contribute)] 69 | 70 | #### Individuals 71 | 72 | 73 | 74 | #### Organizations 75 | 76 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/vsformat/contribute)] 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /image/shell_format.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxundermoon/vs-shell-format/7099e276f4da38a334c2aa7682347c9a44d42fc0/image/shell_format.gif -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxundermoon/vs-shell-format/7099e276f4da38a334c2aa7682347c9a44d42fc0/logo.png -------------------------------------------------------------------------------- /logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxundermoon/vs-shell-format/7099e276f4da38a334c2aa7682347c9a44d42fc0/logo.psd -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shell-format", 3 | "displayName": "shell-format", 4 | "description": "A formatter for shell scripts, Dockerfile, gitignore, dotenv, /etc/hosts, jvmoptions, and other file types", 5 | "version": "7.2.5", 6 | "publisher": "foxundermoon", 7 | "engines": { 8 | "vscode": "^1.36.0" 9 | }, 10 | "categories": [ 11 | "Formatters" 12 | ], 13 | "activationEvents": [ 14 | "onLanguage:shellscript", 15 | "onLanguage:dotenv", 16 | "onLanguage:dockerfile", 17 | "onLanguage:ignore", 18 | "onLanguage:hosts", 19 | "onLanguage:jvmoptions", 20 | "onLanguage:properties", 21 | "onLanguage:spring-boot-properties", 22 | "onLanguage:azcli", 23 | "onLanguage:bats" 24 | ], 25 | "main": "./dist/extension", 26 | "capabilities": { 27 | "documentFormattingProvider": "true" 28 | }, 29 | "icon": "logo.png", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/foxundermoon/vs-shell-format.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/foxundermoon/vs-shell-format/issues" 36 | }, 37 | "keywords": [ 38 | "shell", 39 | "docker", 40 | "shellscript", 41 | "format", 42 | "bash", 43 | "dockerfile", 44 | "properties", 45 | "formatter", 46 | "beautify", 47 | "dotenv", 48 | "hosts", 49 | "jvmoptions", 50 | "vmoptions", 51 | "bashrc", 52 | "zshrc", 53 | "azcli", 54 | "azure cli tool", 55 | "bats" 56 | ], 57 | "contributes": { 58 | "languages": [ 59 | { 60 | "id": "shellscript" 61 | }, 62 | { 63 | "id": "dockerfile", 64 | "aliases": [ 65 | "docker", 66 | "Dockerfile" 67 | ], 68 | "filenamePatterns": [ 69 | "Dockerfile", 70 | "Dockerfile.*", 71 | "*.dockerfile" 72 | ] 73 | }, 74 | { 75 | "id": "ignore", 76 | "aliases": [ 77 | "gitignore", 78 | "dockerignore" 79 | ], 80 | "filenames": [ 81 | ".dockerignore", 82 | ".gitignore" 83 | ] 84 | }, 85 | { 86 | "id": "dotenv", 87 | "aliases": [ 88 | "env" 89 | ], 90 | "filenamePatterns": [ 91 | ".env.*" 92 | ], 93 | "filenames": [ 94 | "env", 95 | ".env" 96 | ] 97 | }, 98 | { 99 | "id": "jvmoptions", 100 | "aliases": [ 101 | "vmoptions" 102 | ], 103 | "extensions": [ 104 | ".vmoptions" 105 | ], 106 | "filenames": [ 107 | "jvm.options" 108 | ] 109 | }, 110 | { 111 | "id": "hosts", 112 | "filenames": [ 113 | "hosts" 114 | ] 115 | }, 116 | { 117 | "id": "properties", 118 | "aliases": [ 119 | "Properties", 120 | "spring-boot-properties" 121 | ], 122 | "extensions": [ 123 | ".properties" 124 | ] 125 | }, 126 | { 127 | "id": "azcli", 128 | "extensions": [ 129 | ".azcli" 130 | ] 131 | }, 132 | { 133 | "id": "bats", 134 | "extensions": [ 135 | ".bats" 136 | ] 137 | } 138 | ], 139 | "configuration": { 140 | "type": "object", 141 | "title": "shell-format configuration", 142 | "properties": { 143 | "shellformat.path": { 144 | "type": [ 145 | "string", 146 | "null" 147 | ], 148 | "default": null, 149 | "description": "the shfmt fullpath example[mac,linux] /usr/local/bin/shfmt [windows] C:/bin/shfmt.exe download from https://github.com/mvdan/sh/releases" 150 | }, 151 | "shellformat.flag": { 152 | "type": [ 153 | "string", 154 | "null" 155 | ], 156 | "default": null, 157 | "description": "shfmt -h to see detail usage, example: -p -bn -ci" 158 | }, 159 | "shellformat.effectLanguages": { 160 | "type": "array", 161 | "default": [ 162 | "shellscript", 163 | "dockerfile", 164 | "dotenv", 165 | "hosts", 166 | "jvmoptions", 167 | "ignore", 168 | "gitignore", 169 | "properties", 170 | "spring-boot-properties", 171 | "azcli", 172 | "bats" 173 | ], 174 | "description": "the trigger effect on the language" 175 | }, 176 | "shellformat.useEditorConfig": { 177 | "type": "boolean", 178 | "default": false, 179 | "description": "Use EditorConfig for shfmt configuration" 180 | } 181 | } 182 | } 183 | }, 184 | "scripts": { 185 | "vscode:prepublish": "webpack --mode production", 186 | "package": "vsce package", 187 | "compile": "webpack --mode none", 188 | "watch": "webpack --mode none --watch", 189 | "test-compile": "tsc -p ./", 190 | "test": "node ./out/test/runTest.js", 191 | "format": "pretty-quick", 192 | "format-check": "pretty-quick --check" 193 | }, 194 | "devDependencies": { 195 | "@types/mocha": "10.0.1", 196 | "@types/node": "18.16.19", 197 | "husky": "8.0.3", 198 | "lint-staged": "13.3.0", 199 | "mocha": "10.2.0", 200 | "prettier": "3.0.0", 201 | "pretty-quick": "3.1.3", 202 | "ts-loader": "9.4.4", 203 | "typescript": "5.1.6", 204 | "vsce": "2.15.0", 205 | "vscode": "^1.1.37", 206 | "vscode-test": "^1.6.1", 207 | "webpack": "5.88.2", 208 | "webpack-cli": "5.1.4" 209 | }, 210 | "dependencies": { 211 | "diff": "~5.1.0", 212 | "editorconfig": "^2.0.0" 213 | }, 214 | "licenses": [ 215 | { 216 | "type": "MIT", 217 | "url": "https://www.opensource.org/licenses/mit-license.php" 218 | } 219 | ] 220 | } 221 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "lockFileMaintenance": { 4 | "enabled": true, 5 | "automerge": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | shfmtVersion: 'v3.8.0', 3 | needCheckInstall: true, 4 | }; 5 | -------------------------------------------------------------------------------- /src/diffUtils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TextDocument, 3 | Position, 4 | Range, 5 | TextEdit, 6 | Uri, 7 | WorkspaceEdit, 8 | TextEditorEdit, 9 | } from 'vscode'; 10 | 11 | import { getExecutableFileUnderPath } from './pathUtil'; 12 | import jsDiff = require('diff'); 13 | 14 | let diffToolAvailable: boolean = null; 15 | 16 | export function isDiffToolAvailable(): boolean { 17 | if (diffToolAvailable == null) { 18 | diffToolAvailable = getExecutableFileUnderPath('diff') != null; 19 | } 20 | return diffToolAvailable; 21 | } 22 | 23 | export enum EditTypes { 24 | EDIT_DELETE, 25 | EDIT_INSERT, 26 | EDIT_REPLACE, 27 | } 28 | 29 | export class Edit { 30 | action: number; 31 | start: Position; 32 | end: Position; 33 | text: string; 34 | 35 | constructor(action: number, start: Position) { 36 | this.action = action; 37 | this.start = start; 38 | this.text = ''; 39 | } 40 | 41 | // Creates TextEdit for current Edit 42 | apply(): TextEdit { 43 | switch (this.action) { 44 | case EditTypes.EDIT_INSERT: 45 | return TextEdit.insert(this.start, this.text); 46 | 47 | case EditTypes.EDIT_DELETE: 48 | return TextEdit.delete(new Range(this.start, this.end)); 49 | 50 | case EditTypes.EDIT_REPLACE: 51 | return TextEdit.replace(new Range(this.start, this.end), this.text); 52 | } 53 | } 54 | 55 | // Applies Edit using given TextEditorEdit 56 | applyUsingTextEditorEdit(editBuilder: TextEditorEdit): void { 57 | switch (this.action) { 58 | case EditTypes.EDIT_INSERT: 59 | editBuilder.insert(this.start, this.text); 60 | break; 61 | 62 | case EditTypes.EDIT_DELETE: 63 | editBuilder.delete(new Range(this.start, this.end)); 64 | break; 65 | 66 | case EditTypes.EDIT_REPLACE: 67 | editBuilder.replace(new Range(this.start, this.end), this.text); 68 | break; 69 | } 70 | } 71 | 72 | // Applies Edits to given WorkspaceEdit 73 | applyUsingWorkspaceEdit(workspaceEdit: WorkspaceEdit, fileUri: Uri): void { 74 | switch (this.action) { 75 | case EditTypes.EDIT_INSERT: 76 | workspaceEdit.insert(fileUri, this.start, this.text); 77 | break; 78 | 79 | case EditTypes.EDIT_DELETE: 80 | workspaceEdit.delete(fileUri, new Range(this.start, this.end)); 81 | break; 82 | 83 | case EditTypes.EDIT_REPLACE: 84 | workspaceEdit.replace(fileUri, new Range(this.start, this.end), this.text); 85 | break; 86 | } 87 | } 88 | } 89 | 90 | export interface FilePatch { 91 | fileName: string; 92 | edits: Edit[]; 93 | } 94 | 95 | /** 96 | * Uses diff module to parse given array of IUniDiff objects and returns edits for files 97 | * 98 | * @param diffOutput jsDiff.IUniDiff[] 99 | * 100 | * @returns Array of FilePatch objects, one for each file 101 | */ 102 | function parseUniDiffs(diffOutput: jsDiff.IUniDiff[]): FilePatch[] { 103 | let filePatches: FilePatch[] = []; 104 | diffOutput.forEach((uniDiff: jsDiff.IUniDiff) => { 105 | let edit: Edit = null; 106 | let edits: Edit[] = []; 107 | uniDiff.hunks.forEach((hunk: jsDiff.IHunk) => { 108 | let startLine = hunk.oldStart; 109 | hunk.lines.forEach((line) => { 110 | switch (line.substr(0, 1)) { 111 | case '-': 112 | if (edit == null) { 113 | edit = new Edit(EditTypes.EDIT_DELETE, new Position(startLine - 1, 0)); 114 | } 115 | edit.end = new Position(startLine, 0); 116 | startLine++; 117 | break; 118 | case '+': 119 | if (edit == null) { 120 | edit = new Edit(EditTypes.EDIT_INSERT, new Position(startLine - 1, 0)); 121 | } else if (edit.action === EditTypes.EDIT_DELETE) { 122 | edit.action = EditTypes.EDIT_REPLACE; 123 | } 124 | edit.text += line.substr(1) + '\n'; 125 | break; 126 | case ' ': 127 | startLine++; 128 | if (edit != null) { 129 | edits.push(edit); 130 | } 131 | edit = null; 132 | break; 133 | } 134 | }); 135 | if (edit != null) { 136 | edits.push(edit); 137 | } 138 | }); 139 | filePatches.push({ fileName: uniDiff.oldFileName, edits: edits }); 140 | }); 141 | 142 | return filePatches; 143 | } 144 | 145 | ('use strict'); 146 | /** 147 | * Returns a FilePatch object by generating diffs between given oldStr and newStr using the diff module 148 | * 149 | * @param fileName string: Name of the file to which edits should be applied 150 | * @param oldStr string 151 | * @param newStr string 152 | * 153 | * @returns A single FilePatch object 154 | */ 155 | export function getEdits(fileName: string, oldStr: string, newStr: string): FilePatch { 156 | if (process.platform === 'win32') { 157 | oldStr = oldStr.split('\r\n').join('\n'); 158 | newStr = newStr.split('\r\n').join('\n'); 159 | } 160 | let unifiedDiffs: jsDiff.IUniDiff = jsDiff.structuredPatch( 161 | fileName, 162 | fileName, 163 | oldStr, 164 | newStr, 165 | '', 166 | '' 167 | ); 168 | let filePatches: FilePatch[] = parseUniDiffs([unifiedDiffs]); 169 | return filePatches[0]; 170 | } 171 | 172 | /** 173 | * Uses diff module to parse given diff string and returns edits for files 174 | * 175 | * @param diffStr : Diff string in unified format. http://www.gnu.org/software/diffutils/manual/diffutils.html#Unified-Format 176 | * 177 | * @returns Array of FilePatch objects, one for each file 178 | */ 179 | export function getEditsFromUnifiedDiffStr(diffstr: string): FilePatch[] { 180 | // Workaround for the bug https://github.com/kpdecker/jsdiff/issues/135 181 | if (diffstr.startsWith('---')) { 182 | diffstr = diffstr.split('---').join('Index\n---'); 183 | } 184 | let unifiedDiffs: jsDiff.IUniDiff[] = jsDiff.parsePatch(diffstr); 185 | let filePatches: FilePatch[] = parseUniDiffs(unifiedDiffs); 186 | return filePatches; 187 | } 188 | -------------------------------------------------------------------------------- /src/downloader.ts: -------------------------------------------------------------------------------- 1 | import * as https from 'https'; 2 | import * as fs from 'fs'; 3 | import { IncomingMessage } from 'http'; 4 | import { config } from './config'; 5 | import * as vscode from 'vscode'; 6 | import * as path from 'path'; 7 | import * as child_process from 'child_process'; 8 | import { getSettings } from './shFormat'; 9 | import { shellformatPath } from './extension'; 10 | const MaxRedirects = 10; 11 | export interface DownloadProgress { 12 | (progress: number): void; 13 | } 14 | /** 15 | * https://repl.it/@lordproud/Downloading-file-in-nodejs 16 | * @param url 17 | * @param path 18 | * @param progress 19 | */ 20 | export async function download( 21 | url: string, 22 | path: string, 23 | progress?: DownloadProgress 24 | ): Promise { 25 | // deprecated 26 | } 27 | 28 | export async function download2( 29 | srcUrl: string, 30 | destPath: string, 31 | progress?: (downloaded: number, contentLength?: number, prev_downloaded?: number) => void 32 | ) { 33 | return new Promise(async (resolve, reject) => { 34 | let response; 35 | for (let i = 0; i < MaxRedirects; ++i) { 36 | response = await new Promise((resolve) => https.get(srcUrl, resolve)); 37 | if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { 38 | srcUrl = response.headers.location; 39 | } else { 40 | break; 41 | } 42 | } 43 | if (response.statusCode < 200 || response.statusCode >= 300) { 44 | reject(new Error(`HTTP status ${response.statusCode} : ${response.statusMessage}`)); 45 | } 46 | if (response.headers['content-type'] != 'application/octet-stream') { 47 | reject(new Error('HTTP response does not contain an octet stream')); 48 | } else { 49 | let stm = fs.createWriteStream(destPath, { mode: 0o755 }); 50 | let pipeStm = response.pipe(stm); 51 | if (progress) { 52 | let contentLength = response.headers['content-length'] 53 | ? Number.parseInt(response.headers['content-length']) 54 | : null; 55 | let downloaded = 0; 56 | let old_downloaded = 0; 57 | response.on('data', (chunk) => { 58 | old_downloaded = downloaded; 59 | downloaded += chunk.length; 60 | progress(downloaded, contentLength, old_downloaded); 61 | }); 62 | } 63 | pipeStm.on('finish', resolve); 64 | pipeStm.on('error', reject); 65 | response.on('error', reject); 66 | } 67 | }); 68 | } 69 | 70 | // const fileExtensionMap = { 71 | // // 'arm', 'arm64', 'ia32', 'ppc', 'ppc64', 's390', 's390x', 'x32', and 'x64' 72 | // arm: "arm", 73 | // arm64: "arm", 74 | // ia32: "386", 75 | // mips: "mips", 76 | // x32: "386", 77 | // x64: "amd64" 78 | // }; 79 | 80 | enum Arch { 81 | arm = 'arm', 82 | arm64 = 'arm64', 83 | i386 = '386', 84 | mips = 'mips', 85 | x64 = 'amd64', 86 | unknown = 'unknown', 87 | } 88 | 89 | enum Platform { 90 | darwin = 'darwin', 91 | freebsd = 'freebsd', 92 | linux = 'linux', 93 | netbsd = 'netbsd', 94 | openbsd = 'openbsd', 95 | windows = 'windows', 96 | unknown = 'unknown', 97 | } 98 | 99 | export function getArchExtension(): Arch { 100 | switch (process.arch) { 101 | case 'arm': 102 | return Arch.arm; 103 | case 'arm64': 104 | return Arch.arm64; 105 | case 'ia32': 106 | return Arch.i386; 107 | case 'x64': 108 | return Arch.x64; 109 | case 'mips': 110 | return Arch.mips; 111 | default: 112 | return Arch.unknown; 113 | } 114 | } 115 | 116 | function getExecuteableFileExt() { 117 | if (process.platform === 'win32') { 118 | return '.exe'; 119 | } else { 120 | return ''; 121 | } 122 | } 123 | 124 | export function getPlatform(): Platform { 125 | switch (process.platform) { 126 | case 'win32': 127 | return Platform.windows; 128 | case 'freebsd': 129 | return Platform.freebsd; 130 | case 'openbsd': 131 | return Platform.openbsd; 132 | case 'darwin': 133 | return Platform.darwin; 134 | case 'linux': 135 | return Platform.linux; 136 | default: 137 | return Platform.unknown; 138 | } 139 | } 140 | 141 | export function getPlatFormFilename() { 142 | const arch = getArchExtension(); 143 | const platform = getPlatform(); 144 | if (arch === Arch.unknown || platform == Platform.unknown) { 145 | throw new Error('do not find release shfmt for your platform'); 146 | } 147 | return `shfmt_${config.shfmtVersion}_${platform}_${arch}${getExecuteableFileExt()}`; 148 | } 149 | 150 | export function getReleaseDownloadUrl() { 151 | // https://github.com/mvdan/sh/releases/download/v2.6.4/shfmt_v2.6.4_darwin_amd64 152 | return `https://github.com/mvdan/sh/releases/download/${ 153 | config.shfmtVersion 154 | }/${getPlatFormFilename()}`; 155 | } 156 | 157 | export function getDestPath(context: vscode.ExtensionContext): string { 158 | let shfmtPath: string = getSettings('path'); 159 | return shfmtPath || path.join(context.extensionPath, 'bin', getPlatFormFilename()); 160 | } 161 | 162 | async function ensureDirectory(dir: string) { 163 | let exists = await new Promise((resolve) => fs.exists(dir, (exists) => resolve(exists))); 164 | if (!exists) { 165 | await ensureDirectory(path.dirname(dir)); 166 | await new Promise((resolve, reject) => 167 | fs.mkdir(dir, (err) => { 168 | if (err) { 169 | reject(err); 170 | } else { 171 | resolve(true); 172 | } 173 | }) 174 | ); 175 | } 176 | } 177 | 178 | export async function checkInstall(context: vscode.ExtensionContext, output: vscode.OutputChannel) { 179 | if (!config.needCheckInstall) { 180 | return; 181 | } 182 | const destPath = getDestPath(context); 183 | await ensureDirectory(path.dirname(destPath)); 184 | const needDownload = await checkNeedInstall(destPath, output); 185 | if (needDownload) { 186 | output.show(); 187 | try { 188 | await cleanFile(destPath); 189 | } catch (err) { 190 | output.appendLine(`clean old file failed:[ ${destPath} ] ,please delete it mutual`); 191 | output.show(); 192 | return; 193 | } 194 | const url = getReleaseDownloadUrl(); 195 | try { 196 | output.appendLine('Shfmt will be downloaded automatically!'); 197 | output.appendLine(`download url: ${url}`); 198 | output.appendLine(`download to: ${destPath}`); 199 | output.appendLine( 200 | `If the download fails, you can manually download it to the dest directory.` 201 | ); 202 | output.appendLine( 203 | 'Or download to another directory, and then set the "shellformat.path" as the path' 204 | ); 205 | output.appendLine(`download shfmt page: https://github.com/mvdan/sh/releases`); 206 | output.appendLine(`You can't use this plugin until the download is successful.`); 207 | output.show(); 208 | await download2(url, destPath, (d, t, p) => { 209 | if (Math.floor(p / 5) < Math.floor(d / 5)) { 210 | output.appendLine(`downloaded:[${((100.0 * d) / t).toFixed(2)}%]`); 211 | } else { 212 | output.append('.'); 213 | } 214 | }); 215 | // await fs.promises.chmod(destPath, 755); 216 | output.appendLine(`download success, You can use it successfully!`); 217 | output.appendLine('Start or issues can be submitted here https://git.io/shfmt'); 218 | } catch (err) { 219 | output.appendLine(`download failed: ${err}`); 220 | } 221 | output.show(); 222 | } 223 | } 224 | 225 | async function cleanFile(file: string) { 226 | try { 227 | await fs.promises.access(file); 228 | } catch (err) { 229 | // ignore 230 | return; 231 | } 232 | await fs.promises.unlink(file); 233 | } 234 | 235 | async function checkNeedInstall(dest: string, output: vscode.OutputChannel): Promise { 236 | try { 237 | const configPath = getSettings('path'); 238 | if (configPath) { 239 | try { 240 | await fs.promises.access(configPath, fs.constants.X_OK); 241 | config.needCheckInstall = false; 242 | return false; 243 | } catch (err) { 244 | output.appendLine( 245 | `"${shellformatPath}": "${configPath}" find config shellformat path ,but the file cannot execute or not exists, so will auto download shfmt` 246 | ); 247 | } 248 | } 249 | 250 | const version = await getInstalledVersion(dest); 251 | 252 | const needInstall = version !== config.shfmtVersion; 253 | if (!needInstall) { 254 | config.needCheckInstall = false; 255 | } else { 256 | output.appendLine( 257 | `current shfmt version : ${version} ,is outdate to new version : ${config.shfmtVersion}` 258 | ); 259 | } 260 | return needInstall; 261 | } catch (err) { 262 | output.appendLine(`shfmt hasn't downloaded yet!` + err); 263 | output.show(); 264 | return true; 265 | } 266 | } 267 | 268 | async function getInstalledVersion(dest: string): Promise { 269 | const stat = await fs.promises.stat(dest); 270 | if (stat.isFile()) { 271 | const v = child_process.execFileSync(dest, ['--version'], { 272 | encoding: 'utf8', 273 | }); 274 | return v.replace('\n', ''); 275 | } else { 276 | throw new Error(`[${dest}] is not file`); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | ShellDocumentFormattingEditProvider, 4 | Formatter, 5 | checkEnv, 6 | configurationPrefix, 7 | ConfigItemName, 8 | } from './shFormat'; 9 | 10 | import { checkInstall } from './downloader'; 11 | 12 | export enum DocumentFilterScheme { 13 | File = 'file', 14 | Untitled = 'untitled', 15 | } 16 | 17 | const formatOnSaveConfig = 'editor.formatOnSave'; 18 | const formatDocumentCommand = 'editor.action.formatDocument'; 19 | 20 | export const shellformatPath = 'shellformat.path'; 21 | 22 | export const output = vscode.window.createOutputChannel('shellformat'); 23 | export function activate(context: vscode.ExtensionContext) { 24 | const settings = vscode.workspace.getConfiguration(configurationPrefix); 25 | const shfmter = new Formatter(context, output); 26 | const shFmtProvider = new ShellDocumentFormattingEditProvider(shfmter, settings); 27 | // checkEnv(); 28 | checkInstall(context, output); 29 | const effectLanguages = settings.get(ConfigItemName.EffectLanguages); 30 | if (effectLanguages) { 31 | for (const lang of effectLanguages) { 32 | for (const schemae of Object.values(DocumentFilterScheme)) { 33 | context.subscriptions.push( 34 | vscode.languages.registerDocumentFormattingEditProvider( 35 | { language: lang, scheme: schemae /*pattern: '*.sh'*/ }, 36 | shFmtProvider 37 | ) 38 | ); 39 | } 40 | } 41 | } 42 | 43 | const formatOnSave = vscode.workspace.getConfiguration().get(formatOnSaveConfig); 44 | if (formatOnSave) { 45 | vscode.workspace.onWillSaveTextDocument((event: vscode.TextDocumentWillSaveEvent) => { 46 | // Only on explicit save 47 | if (event.reason === 1 && isAllowedTextDocument(event.document)) { 48 | vscode.commands.executeCommand(formatDocumentCommand); 49 | } 50 | }); 51 | } 52 | } 53 | 54 | function isAllowedTextDocument(textDocument: vscode.TextDocument): boolean { 55 | const settings = vscode.workspace.getConfiguration(configurationPrefix); 56 | const effectLanguages = settings.get(ConfigItemName.EffectLanguages); 57 | const { scheme } = textDocument.uri; 58 | if (effectLanguages) { 59 | const checked = effectLanguages.find((e) => e === textDocument.languageId); 60 | if (checked) { 61 | return scheme === DocumentFilterScheme.File || scheme === DocumentFilterScheme.Untitled; 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | export function deactivate() {} 68 | -------------------------------------------------------------------------------- /src/pathUtil.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as vscode from 'vscode'; 4 | 5 | let binPathCache: { [bin: string]: string } = {}; 6 | export function getExecutableFileUnderPath(toolName: string) { 7 | let cachePath = binPathCache[toolName]; 8 | if (cachePath) { 9 | return cachePath; 10 | } 11 | toolName = correctBinname(toolName); 12 | if (path.isAbsolute(toolName)) { 13 | return toolName; 14 | } 15 | let paths = process.env['PATH'].split(path.delimiter); 16 | for (let i = 0; i < paths.length; i++) { 17 | let binpath = path.join(paths[i], toolName); 18 | if (fileExists(binpath)) { 19 | binPathCache[toolName] = binpath; 20 | return binpath; 21 | } 22 | } 23 | return null; 24 | } 25 | 26 | function correctBinname(binname: string) { 27 | if (process.platform === 'win32' && path.extname(binname) !== '.exe') { 28 | return binname + '.exe'; 29 | } else { 30 | return binname; 31 | } 32 | } 33 | 34 | export function fileExists(filePath: string): boolean { 35 | try { 36 | return fs.statSync(filePath).isFile(); 37 | } catch (e) { 38 | return false; 39 | } 40 | } 41 | 42 | export function substitutePath(filePath: string): string { 43 | let workspaceFolder = 44 | vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0].uri.fsPath; 45 | return filePath 46 | .replace(/\${workspaceRoot}/g, workspaceFolder || '') 47 | .replace(/\${workspaceFolder}/g, workspaceFolder || ''); 48 | } 49 | -------------------------------------------------------------------------------- /src/shFormat.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as child_process from 'child_process'; 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | import { fileExists, getExecutableFileUnderPath, substitutePath } from './pathUtil'; 6 | import { output } from './extension'; 7 | 8 | import { isDiffToolAvailable, getEdits, getEditsFromUnifiedDiffStr } from '../src/diffUtils'; 9 | 10 | import { 11 | Diagnostic, 12 | DiagnosticSeverity, 13 | Range, 14 | DiagnosticCollection, 15 | TextDocument, 16 | Position, 17 | FormattingOptions, 18 | TextEdit, 19 | } from 'vscode'; 20 | import * as editorconfig from 'editorconfig'; 21 | 22 | import { config } from './config'; 23 | import { getPlatFormFilename, getDestPath } from './downloader'; 24 | export const configurationPrefix = 'shellformat'; 25 | 26 | export enum ConfigItemName { 27 | Flag = 'flag', 28 | Path = 'path', 29 | EffectLanguages = 'effectLanguages', 30 | ShowError = 'showError', 31 | UseEditorConfig = 'useEditorConfig', 32 | } 33 | 34 | const defaultDownloadDirParrent = '/usr/local'; 35 | const defaultDownloadDir = '/usr/local/bin'; 36 | const defaultDownloadShfmtPath = `${defaultDownloadDir}/shfmt`; 37 | const fileExtensionMap = { 38 | arm: 'arm', 39 | arm64: 'arm64', 40 | ia32: '386', 41 | mips: 'mips', 42 | x32: '386', 43 | x64: 'amd64', 44 | }; 45 | export class Formatter { 46 | static formatCommand = 'shfmt'; 47 | diagnosticCollection: DiagnosticCollection; 48 | 49 | constructor(public context: vscode.ExtensionContext, public output: vscode.OutputChannel) { 50 | this.diagnosticCollection = vscode.languages.createDiagnosticCollection('shell-format'); 51 | } 52 | 53 | getShfmtPath() { 54 | return getDestPath(this.context); 55 | } 56 | 57 | public formatDocument(document: TextDocument, options?: FormattingOptions): Thenable { 58 | const start = new Position(0, 0); 59 | const end = new vscode.Position( 60 | document.lineCount - 1, 61 | document.lineAt(document.lineCount - 1).text.length 62 | ); 63 | const range = new vscode.Range(start, end); 64 | const content = document.getText(range); 65 | return this.formatDocumentWithContent(content, document, range, options); 66 | } 67 | 68 | public formatDocumentWithContent( 69 | content: string, 70 | document: TextDocument, 71 | range: Range, 72 | options?: vscode.FormattingOptions 73 | ): Thenable { 74 | return new Promise((resolve, reject) => { 75 | try { 76 | let settings = vscode.workspace.getConfiguration(configurationPrefix); 77 | let binPath: string = getSettings('path'); 78 | let flag: string = getSettings('flag'); 79 | 80 | let shfmtFlags = []; // TODO: Add user configuration 81 | let shfmtIndent = false; 82 | if (/\.bats$/.test(document.fileName)) { 83 | shfmtFlags.push('--ln=bats'); 84 | } 85 | 86 | if (binPath) { 87 | if (fileExists(binPath)) { 88 | Formatter.formatCommand = binPath; 89 | } else { 90 | let errMsg = `Invalid shfmt path in extension configuration: ${binPath}`; 91 | vscode.window.showErrorMessage(errMsg); 92 | reject(errMsg); 93 | } 94 | } else { 95 | Formatter.formatCommand = this.getShfmtPath(); 96 | } 97 | 98 | if (settings.useEditorConfig) { 99 | if (flag) { 100 | flag = ''; 101 | output.appendLine('shfmt flags will be ignored as EditorConfig mode is enabled.'); 102 | } 103 | 104 | let edcfgOptions = editorconfig.parseSync(document.fileName); 105 | output.appendLine( 106 | `EditorConfig for file "${document.fileName}": ${JSON.stringify(edcfgOptions)}` 107 | ); 108 | 109 | if (edcfgOptions.indent_style === 'tab') { 110 | shfmtFlags.push('-i=0'); 111 | shfmtIndent = true; 112 | } else if (edcfgOptions.indent_style === 'space') { 113 | if (typeof edcfgOptions.indent_size === 'number') { 114 | shfmtFlags.push(`-i=${edcfgOptions.indent_size}`); 115 | shfmtIndent = true; 116 | } 117 | } 118 | 119 | if (edcfgOptions['shell_variant']) { 120 | shfmtFlags.push(`-ln=${edcfgOptions['shell_variant']}`); 121 | } 122 | 123 | if (edcfgOptions['binary_next_line']) { 124 | shfmtFlags.push('-bn'); 125 | } 126 | 127 | if (edcfgOptions['switch_case_indent']) { 128 | shfmtFlags.push('-ci'); 129 | } 130 | 131 | if (edcfgOptions['space_redirects']) { 132 | shfmtFlags.push('-sr'); 133 | } 134 | 135 | if (edcfgOptions['keep_padding']) { 136 | shfmtFlags.push('-kp'); 137 | } 138 | 139 | if (edcfgOptions['function_next_line']) { 140 | shfmtFlags.push('-fn'); 141 | } 142 | } 143 | 144 | if (flag) { 145 | if (flag.includes('-w')) { 146 | let errMsg = 'Incompatible flag specified in shellformat.flag: -w'; 147 | vscode.window.showWarningMessage(errMsg); 148 | reject(errMsg); 149 | } 150 | 151 | if (flag.includes('-i')) { 152 | shfmtIndent = true; 153 | } 154 | 155 | let flags = flag.split(' '); 156 | shfmtFlags.push(...flags); 157 | } 158 | 159 | if (options?.insertSpaces && !shfmtIndent) { 160 | shfmtFlags.push(`-i=${options.tabSize}`); 161 | } 162 | 163 | if (shfmtFlags) { 164 | output.appendLine(`Effective shfmt flags: ${shfmtFlags}`); 165 | } 166 | 167 | let shfmt = child_process.spawn(Formatter.formatCommand, shfmtFlags); 168 | 169 | let shfmtOut: Buffer[] = []; 170 | shfmt.stdout.on('data', (chunk) => { 171 | let bc: Buffer; 172 | if (chunk instanceof Buffer) { 173 | bc = chunk; 174 | } else { 175 | bc = new Buffer(chunk); 176 | } 177 | shfmtOut.push(bc); 178 | }); 179 | let shfmtErr: Buffer[] = []; 180 | shfmt.stderr.on('data', (chunk) => { 181 | let bc: Buffer; 182 | if (chunk instanceof Buffer) { 183 | bc = chunk; 184 | } else { 185 | bc = new Buffer(chunk); 186 | } 187 | shfmtErr.push(bc); 188 | }); 189 | 190 | let textEdits: TextEdit[] = []; 191 | shfmt.on('close', (code, signal) => { 192 | if (code == 0) { 193 | this.diagnosticCollection.delete(document.uri); 194 | 195 | if (shfmtOut.length != 0) { 196 | let result = Buffer.concat(shfmtOut).toString(); 197 | let filePatch = getEdits(document.fileName, content, result); 198 | 199 | filePatch.edits.forEach((edit) => { 200 | textEdits.push(edit.apply()); 201 | }); 202 | 203 | resolve(textEdits); 204 | } else { 205 | resolve(null); 206 | } 207 | } else { 208 | let errMsg = ''; 209 | 210 | if (shfmtErr.length != 0) { 211 | errMsg = Buffer.concat(shfmtErr).toString(); 212 | 213 | // https://regex101.com/r/uPoLKg/2/ 214 | let errLoc = /^:(\d+):(\d+):/.exec(errMsg); 215 | 216 | if (errLoc !== null && errLoc.length > 2) { 217 | let line = parseInt(errLoc[1]); 218 | let column = parseInt(errLoc[2]); 219 | 220 | const diag: Diagnostic = { 221 | range: new vscode.Range( 222 | new vscode.Position(line, column), 223 | new vscode.Position(line, column) 224 | ), 225 | message: errMsg.slice(':'.length, errMsg.length), 226 | severity: DiagnosticSeverity.Error, 227 | }; 228 | 229 | this.diagnosticCollection.delete(document.uri); 230 | this.diagnosticCollection.set(document.uri, [diag]); 231 | } 232 | } 233 | 234 | reject(errMsg); 235 | } 236 | }); 237 | 238 | shfmt.stdin.write(content); 239 | shfmt.stdin.end(); 240 | } catch (e) { 241 | reject(`Fatal error calling shfmt: ${e}`); 242 | } 243 | }); 244 | } 245 | } 246 | 247 | export class ShellDocumentFormattingEditProvider implements vscode.DocumentFormattingEditProvider { 248 | private settings: vscode.WorkspaceConfiguration; 249 | 250 | constructor(public formatter: Formatter, settings?: vscode.WorkspaceConfiguration) { 251 | if (settings === undefined) { 252 | this.settings = vscode.workspace.getConfiguration(configurationPrefix); 253 | } else { 254 | this.settings = settings; 255 | } 256 | } 257 | 258 | public provideDocumentFormattingEdits( 259 | document: vscode.TextDocument, 260 | options: vscode.FormattingOptions, 261 | token: vscode.CancellationToken 262 | ): Thenable { 263 | // const onSave = this.settings["onsave"]; 264 | // if (!onSave) { 265 | // console.log(onSave); 266 | // } 267 | return this.formatter.formatDocument(document, options); 268 | } 269 | } 270 | 271 | /** 272 | * deprecated 273 | * will clean 274 | * */ 275 | 276 | export function checkEnv() { 277 | const settings = vscode.workspace.getConfiguration(configurationPrefix); 278 | let configBinPath = false; 279 | if (settings) { 280 | let flag: string = settings.get(ConfigItemName.Flag); 281 | if (flag) { 282 | if (flag.includes('-w')) { 283 | vscode.window.showWarningMessage('can not set -w flag please fix config'); 284 | } 285 | } 286 | let binPath: string = settings.get(ConfigItemName.Path); 287 | if (binPath) { 288 | configBinPath = true; 289 | if (fileExists(binPath)) { 290 | this.formatCommand = binPath; 291 | } else { 292 | vscode.window.showErrorMessage( 293 | `the config [${configurationPrefix}.${ConfigItemName.Path}] file not exists please fix it` 294 | ); 295 | } 296 | } 297 | } 298 | if (!configBinPath && !isExecutedFmtCommand() && !fileExists(defaultDownloadShfmtPath)) { 299 | if (process.platform == 'darwin') { 300 | installFmtForMaxos(); 301 | } else if ( 302 | [ 303 | // "android", 304 | // "darwin", 305 | 'freebsd', 306 | 'linux', 307 | 'openbsd', 308 | // "sunos", 309 | // "win32", 310 | // "cygwin" 311 | ].includes(process.platform) 312 | ) { 313 | // installForLinux(); 314 | showMamualInstallMessage(); 315 | } else { 316 | showMamualInstallMessage(); 317 | } 318 | } 319 | } 320 | 321 | function showMamualInstallMessage() { 322 | vscode.window.showErrorMessage( 323 | `[${configurationPrefix}.${ConfigItemName.Path}]not found! please install manually https://mvdan.cc/sh/cmd/shfmt ` 324 | ); 325 | } 326 | function installFmtForMaxos() { 327 | if (getExecutableFileUnderPath('brew')) { 328 | vscode.window.showInformationMessage('will install shfmt by brew'); 329 | const terminal = vscode.window.createTerminal(); 330 | terminal.show(); 331 | terminal.sendText('brew install shfmt', true); 332 | terminal.sendText("echo '**Enjoy shellscript!**'", true); 333 | terminal.sendText("echo 'fork or star https://github.com/foxundermoon/vs-shell-format'", true); 334 | } else { 335 | installForLinux(); 336 | } 337 | } 338 | 339 | /** will clean */ 340 | function installForLinux() { 341 | //todo fix the ubuntu permission issue 342 | return; 343 | try { 344 | const url = getDownloadUrl(); 345 | vscode.window.showInformationMessage('will install shfmt by curl'); 346 | const terminal = vscode.window.createTerminal(); 347 | terminal.show(); 348 | if (!fs.existsSync(defaultDownloadDir)) { 349 | try { 350 | fs.accessSync(defaultDownloadDirParrent, fs.constants.W_OK); 351 | terminal.sendText(`mkdir -p ${defaultDownloadDir}`, true); 352 | } catch (err) { 353 | terminal.sendText(`sudo mkdir -p ${defaultDownloadDir}`, true); 354 | } 355 | } 356 | 357 | try { 358 | fs.accessSync(defaultDownloadDir, fs.constants.W_OK); 359 | terminal.sendText(`curl -L '${url}' --output /usr/local/bin/shfmt`, true); 360 | terminal.sendText(`chmod a+x /usr/local/bin/shfmt`, true); 361 | } catch (err) { 362 | terminal.sendText(`sudo curl -L '${url}' --output /usr/local/bin/shfmt`, true); 363 | terminal.sendText(`sudo chmod a+x /usr/local/bin/shfmt`, true); 364 | } 365 | terminal.sendText("echo '**Enjoy shellscript!**'", true); 366 | terminal.sendText("echo 'fork or star https://github.com/foxundermoon/vs-shell-format'", true); 367 | } catch (error) { 368 | vscode.window.showWarningMessage( 369 | 'install shfmt failed , please install manually https://mvdan.cc/sh/cmd/shfmt' 370 | ); 371 | } 372 | } 373 | 374 | function getDownloadUrl(): String { 375 | try { 376 | const extension = fileExtensionMap[process.arch]; 377 | const url = `https://github.com/mvdan/sh/releases/download/${config.shfmtVersion}/shfmt_${config.shfmtVersion}_${process.platform}_${extension}`; 378 | return url; 379 | } catch (error) { 380 | throw new Error('nor sourport'); 381 | } 382 | } 383 | 384 | /** 385 | * will clean 386 | */ 387 | function isExecutedFmtCommand(): Boolean { 388 | return getExecutableFileUnderPath(Formatter.formatCommand) != null; 389 | } 390 | 391 | export function getSettings(key: string) { 392 | let settings = vscode.workspace.getConfiguration(configurationPrefix); 393 | if (key === 'path' && settings[key]) { 394 | return substitutePath(settings[key]); 395 | } 396 | return key !== undefined ? settings[key] : null; 397 | } 398 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | var testRunner = require('vscode/lib/testrunner'); 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true, // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; 23 | -------------------------------------------------------------------------------- /test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from 'vscode-test'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /test/suite/downloader.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | import * as myExtension from '../../src/extension'; 7 | import { 8 | DownloadProgress, 9 | download, 10 | download2, 11 | getReleaseDownloadUrl, 12 | getPlatFormFilename, 13 | } from '../../src/downloader'; 14 | import * as fs from 'fs'; 15 | import * as child_process from 'child_process'; 16 | import { config } from '../../src/config'; 17 | 18 | // Defines a Mocha test suite to group tests of similar kind together 19 | suite('Downloader Tests', () => { 20 | // Defines a Mocha unit test 21 | test('download', async () => { 22 | const url = getReleaseDownloadUrl(); 23 | const dest = `${__dirname}/../${getPlatFormFilename()}`; 24 | 25 | try { 26 | if ((await fs.promises.stat(dest)).isFile) { 27 | await fs.promises.unlink(dest); 28 | } 29 | } catch (err) { 30 | console.log(err); 31 | } 32 | 33 | const success = await download2(url, dest, (p, t) => console.log(`${(100.0 * p) / t}%`)); 34 | 35 | await fs.promises.chmod(dest, 755); 36 | 37 | let version = await child_process.execFileSync(dest, ['--version'], { 38 | encoding: 'utf8', 39 | }); 40 | 41 | version = version.replace('\n', ''); 42 | 43 | assert.equal(version, config.shfmtVersion); 44 | }).timeout('60s'); 45 | }); 46 | -------------------------------------------------------------------------------- /test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | import * as vscode from 'vscode'; 12 | import * as myExtension from '../../src/extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite('Extension Tests', () => { 16 | // Defines a Mocha unit test 17 | test('Something 1', () => { 18 | assert.equal(-1, [1, 2, 3].indexOf(5)); 19 | assert.equal(-1, [1, 2, 3].indexOf(0)); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | }); 10 | // mocha.useColors(true); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run((failures) => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | e(err); 34 | } 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/suite/shfmt.test.ts: -------------------------------------------------------------------------------- 1 | import { getSettings } from '../../src/shFormat'; 2 | import * as assert from 'assert'; 3 | import { fileExists } from '../../src/pathUtil'; 4 | 5 | suite('shfmt path Tests', () => { 6 | let shfmtPath = getSettings('path'); 7 | 8 | // Defines a Mocha unit test 9 | if (shfmtPath) { 10 | test('shfmt exists', () => { 11 | assert.equal(fileExists(shfmtPath), true); 12 | }); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /test/supported/.env: -------------------------------------------------------------------------------- 1 | FOO=bar 2 | BAR=baz 3 | SPACED=with spaces 4 | 5 | NULL= 6 | KEY=xxxxxxxxxxxxxxxxxxxx.xxx.xxxxxxxxxxxxxxxxxxxx 7 | AK=dqweasd 8 | SK=qweasdzxc 9 | -------------------------------------------------------------------------------- /test/supported/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .CFUserTextEncoding 3 | .DS_Store 4 | .adobe/ 5 | 6 | .android/ 7 | .bash_history 8 | .bash_profile 9 | .bash_sessions/ 10 | 11 | .bashrc 12 | .cache/ 13 | .gem/ 14 | !.gitconfig 15 | !.gitignore 16 | .gradle/ 17 | .gvm/ 18 | .npm/ 19 | .dotnet/ 20 | .electron/ 21 | 22 | .atom/ 23 | .bin/ssh_3d_199.sh 24 | .black-screen/ 25 | 26 | tmp/ 27 | .sdkman 28 | .vim 29 | .tmux 30 | 31 | .helm 32 | 33 | -------------------------------------------------------------------------------- /test/supported/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | // "*.js": "javascriptreact" 4 | // "hosts": "shellscript", 5 | // "*.vmoptions":"shellscript" 6 | }, 7 | "editor.lineNumbers": "off" 8 | } 9 | -------------------------------------------------------------------------------- /test/supported/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | 3 | LABEL name="shell-format for Dockerfile test" 4 | 5 | ENV AUTHOR=foxundermoon website=https://fox.mn 6 | 7 | WORKDIR /app 8 | 9 | RUN mkdr test && \ 10 | cd test && \ 11 | echo "hello world" > helloworld && \ 12 | cat helloworld && \ 13 | wget -qO- ip.sb | tee buildip && \ 14 | date -u | tee builddate && \ 15 | echo "build success" && \ 16 | echo "enjoy shellscript and docker by vs-shell-format" && \ 17 | cd .. 18 | 19 | VOLUME [ "/data" ] 20 | 21 | CMD [ "sh","-c", "hello world" ] 22 | -------------------------------------------------------------------------------- /test/supported/application.properties: -------------------------------------------------------------------------------- 1 | # ---------------------------------------- 2 | # CORE PROPERTIES 3 | # ---------------------------------------- 4 | debug=false # Enable debug logs. 5 | trace=false # Enable trace logs. 6 | 7 | # LOGGING 8 | logging.config= # Location of the logging configuration file. For instance, `classpath:logback.xml` for Logback. 9 | logging.exception-conversion-word=%wEx # Conversion word used when logging exceptions. 10 | logging.file= # Log file name (for instance, `myapp.log`). Names can be an exact location or relative to the current directory. 11 | logging.file.max-history=0 # Maximum of archive log files to keep. Only supported with the default logback setup. 12 | logging.file.max-size=10MB # Maximum log file size. Only supported with the default logback setup. 13 | logging.group.*= # Log groups to quickly change multiple loggers at the same time. For instance, `logging.level.db=org.hibernate,org.springframework.jdbc`. 14 | logging.level.*= # Log levels severity mapping. For instance, `logging.level.org.springframework=DEBUG`. 15 | logging.path= # Location of the log file. For instance, `/var/log`. 16 | logging.pattern.console= # Appender pattern for output to the console. Supported only with the default Logback setup. 17 | logging.pattern.dateformat=yyyy-MM-dd HH:mm:ss.SSS # Appender pattern for log date format. Supported only with the default Logback setup. 18 | logging.pattern.file= # Appender pattern for output to a file. Supported only with the default Logback setup. 19 | logging.pattern.level=%5p # Appender pattern for log level. Supported only with the default Logback setup. 20 | logging.register-shutdown-hook=false # Register a shutdown hook for the logging system when it is initialized. 21 | -------------------------------------------------------------------------------- /test/supported/azure.azcli: -------------------------------------------------------------------------------- 1 | az network public-ip list -o tsv -------------------------------------------------------------------------------- /test/supported/bats.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | setup() { 4 | load test_helper 5 | fixtures bats 6 | } 7 | 8 | @test "no arguments prints message and usage instructions" { 9 | run bats 10 | [ $status -eq 1 ] 11 | [ "${lines[0]}" == 'Error: Must specify at least one ' ] 12 | [ "${lines[1]%% *}" == 'Usage:' ] 13 | } 14 | 15 | @test "invalid option prints message and usage instructions" { 16 | run bats --invalid-option 17 | [ $status -eq 1 ] 18 | [ "${lines[0]}" == "Error: Bad command line option '--invalid-option'" ] 19 | [ "${lines[1]%% *}" == 'Usage:' ] 20 | } 21 | 22 | @test "-v and --version print version number" { 23 | run bats -v 24 | [ $status -eq 0 ] 25 | [ "$(expr "$output" : "Bats [0-9][0-9.]*")" -ne 0 ] 26 | } 27 | 28 | @test "-h and --help print help" { 29 | run bats -h 30 | [ $status -eq 0 ] 31 | [ "${#lines[@]}" -gt 3 ] 32 | } 33 | 34 | @test "invalid filename prints an error" { 35 | run bats nonexistent 36 | [ $status -eq 1 ] 37 | [ "$(expr "$output" : ".*does not exist")" -ne 0 ] 38 | } 39 | 40 | @test "empty test file runs zero tests" { 41 | run bats "$FIXTURE_ROOT/empty.bats" 42 | [ $status -eq 0 ] 43 | [ "$output" = "1..0" ] 44 | } 45 | 46 | @test "one passing test" { 47 | run bats "$FIXTURE_ROOT/passing.bats" 48 | [ $status -eq 0 ] 49 | [ "${lines[0]}" = "1..1" ] 50 | [ "${lines[1]}" = "ok 1 a passing test" ] 51 | } 52 | 53 | @test "summary passing tests" { 54 | run filter_control_sequences bats -p "$FIXTURE_ROOT/passing.bats" 55 | echo "$output" 56 | [ $status -eq 0 ] 57 | [ "${lines[1]}" = "1 test, 0 failures" ] 58 | } 59 | 60 | @test "summary passing and skipping tests" { 61 | run filter_control_sequences bats -p "$FIXTURE_ROOT/passing_and_skipping.bats" 62 | [ $status -eq 0 ] 63 | [ "${lines[3]}" = "3 tests, 0 failures, 2 skipped" ] 64 | } 65 | 66 | @test "tap passing and skipping tests" { 67 | run filter_control_sequences bats --formatter tap "$FIXTURE_ROOT/passing_and_skipping.bats" 68 | [ $status -eq 0 ] 69 | [ "${lines[0]}" = "1..3" ] 70 | [ "${lines[1]}" = "ok 1 a passing test" ] 71 | [ "${lines[2]}" = "ok 2 a skipped test with no reason # skip" ] 72 | [ "${lines[3]}" = "ok 3 a skipped test with a reason # skip for a really good reason" ] 73 | } 74 | 75 | @test "summary passing and failing tests" { 76 | run filter_control_sequences bats -p "$FIXTURE_ROOT/failing_and_passing.bats" 77 | [ $status -eq 0 ] 78 | [ "${lines[4]}" = "2 tests, 1 failure" ] 79 | } 80 | 81 | @test "summary passing, failing and skipping tests" { 82 | run filter_control_sequences bats -p "$FIXTURE_ROOT/passing_failing_and_skipping.bats" 83 | [ $status -eq 0 ] 84 | [ "${lines[5]}" = "3 tests, 1 failure, 1 skipped" ] 85 | } 86 | 87 | @test "tap passing, failing and skipping tests" { 88 | run filter_control_sequences bats --formatter tap "$FIXTURE_ROOT/passing_failing_and_skipping.bats" 89 | [ $status -eq 0 ] 90 | [ "${lines[0]}" = "1..3" ] 91 | [ "${lines[1]}" = "ok 1 a passing test" ] 92 | [ "${lines[2]}" = "ok 2 a skipping test # skip" ] 93 | [ "${lines[3]}" = "not ok 3 a failing test" ] 94 | } 95 | 96 | @test "BATS_CWD is correctly set to PWD as validated by bats_trim_filename" { 97 | local trimmed 98 | bats_trim_filename "$PWD/foo/bar" 'trimmed' 99 | printf 'ACTUAL: %s\n' "$trimmed" >&2 100 | [ "$trimmed" = 'foo/bar' ] 101 | } 102 | 103 | @test "one failing test" { 104 | run bats "$FIXTURE_ROOT/failing.bats" 105 | [ $status -eq 1 ] 106 | [ "${lines[0]}" = '1..1' ] 107 | [ "${lines[1]}" = 'not ok 1 a failing test' ] 108 | [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing.bats, line 4)" ] 109 | [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed" ] 110 | } 111 | 112 | @test "one failing and one passing test" { 113 | run bats "$FIXTURE_ROOT/failing_and_passing.bats" 114 | [ $status -eq 1 ] 115 | [ "${lines[0]}" = '1..2' ] 116 | [ "${lines[1]}" = 'not ok 1 a failing test' ] 117 | [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing_and_passing.bats, line 2)" ] 118 | [ "${lines[3]}" = "# \`false' failed" ] 119 | [ "${lines[4]}" = 'ok 2 a passing test' ] 120 | } 121 | 122 | @test "failing test with significant status" { 123 | STATUS=2 run bats "$FIXTURE_ROOT/failing.bats" 124 | [ $status -eq 1 ] 125 | [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed with status 2" ] 126 | } 127 | 128 | @test "failing helper function logs the test case's line number" { 129 | run bats "$FIXTURE_ROOT/failing_helper.bats" 130 | [ $status -eq 1 ] 131 | [ "${lines[1]}" = 'not ok 1 failing helper function' ] 132 | [ "${lines[2]}" = "# (from function \`failing_helper' in file $RELATIVE_FIXTURE_ROOT/test_helper.bash, line 6," ] 133 | [ "${lines[3]}" = "# in test file $RELATIVE_FIXTURE_ROOT/failing_helper.bats, line 5)" ] 134 | [ "${lines[4]}" = "# \`failing_helper' failed" ] 135 | } 136 | 137 | @test "failing bash condition logs correct line number" { 138 | run bats "$FIXTURE_ROOT/failing_with_bash_cond.bats" 139 | [ "$status" -eq 1 ] 140 | [ "${#lines[@]}" -eq 4 ] 141 | [ "${lines[1]}" = 'not ok 1 a failing test' ] 142 | [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing_with_bash_cond.bats, line 4)" ] 143 | [ "${lines[3]}" = "# \`[[ 1 == 2 ]]' failed" ] 144 | } 145 | 146 | @test "failing bash expression logs correct line number" { 147 | run bats "$FIXTURE_ROOT/failing_with_bash_expression.bats" 148 | [ "$status" -eq 1 ] 149 | [ "${#lines[@]}" -eq 4 ] 150 | [ "${lines[1]}" = 'not ok 1 a failing test' ] 151 | [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing_with_bash_expression.bats, line 3)" ] 152 | [ "${lines[3]}" = "# \`(( 1 == 2 ))' failed" ] 153 | } 154 | 155 | @test "failing negated command logs correct line number" { 156 | run bats "$FIXTURE_ROOT/failing_with_negated_command.bats" 157 | [ "$status" -eq 1 ] 158 | [ "${#lines[@]}" -eq 4 ] 159 | [ "${lines[1]}" = 'not ok 1 a failing test' ] 160 | [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing_with_negated_command.bats, line 3)" ] 161 | [ "${lines[3]}" = "# \`! true' failed" ] 162 | } 163 | 164 | @test "test environments are isolated" { 165 | run bats "$FIXTURE_ROOT/environment.bats" 166 | [ $status -eq 0 ] 167 | } 168 | 169 | @test "setup is run once before each test" { 170 | # shellcheck disable=SC2031,SC2030 171 | export BATS_TEST_SUITE_TMPDIR="${BATS_TEST_TMPDIR}" 172 | run bats "$FIXTURE_ROOT/setup.bats" 173 | [ $status -eq 0 ] 174 | run cat "$BATS_TEST_SUITE_TMPDIR/setup.log" 175 | [ ${#lines[@]} -eq 3 ] 176 | } 177 | 178 | @test "teardown is run once after each test, even if it fails" { 179 | # shellcheck disable=SC2031,SC2030 180 | export BATS_TEST_SUITE_TMPDIR="${BATS_TEST_TMPDIR}" 181 | run bats "$FIXTURE_ROOT/teardown.bats" 182 | [ $status -eq 1 ] 183 | run cat "$BATS_TEST_SUITE_TMPDIR/teardown.log" 184 | [ ${#lines[@]} -eq 3 ] 185 | } 186 | 187 | @test "setup failure" { 188 | run bats "$FIXTURE_ROOT/failing_setup.bats" 189 | [ $status -eq 1 ] 190 | [ "${lines[1]}" = 'not ok 1 truth' ] 191 | [ "${lines[2]}" = "# (from function \`setup' in test file $RELATIVE_FIXTURE_ROOT/failing_setup.bats, line 2)" ] 192 | [ "${lines[3]}" = "# \`false' failed" ] 193 | } 194 | 195 | @test "passing test with teardown failure" { 196 | PASS=1 run bats "$FIXTURE_ROOT/failing_teardown.bats" 197 | [ $status -eq 1 ] 198 | echo "$output" 199 | [ "${lines[1]}" = 'not ok 1 truth' ] 200 | [ "${lines[2]}" = "# (from function \`teardown' in test file $RELATIVE_FIXTURE_ROOT/failing_teardown.bats, line 2)" ] 201 | [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed" ] 202 | } 203 | 204 | @test "failing test with teardown failure" { 205 | PASS=0 run bats "$FIXTURE_ROOT/failing_teardown.bats" 206 | [ $status -eq 1 ] 207 | [ "${lines[1]}" = 'not ok 1 truth' ] 208 | [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing_teardown.bats, line 6)" ] 209 | [ "${lines[3]}" = $'# `[ "$PASS" = 1 ]\' failed' ] 210 | } 211 | 212 | @test "teardown failure with significant status" { 213 | PASS=1 STATUS=2 run bats "$FIXTURE_ROOT/failing_teardown.bats" 214 | [ $status -eq 1 ] 215 | [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed with status 2" ] 216 | } 217 | 218 | @test "failing test file outside of BATS_CWD" { 219 | cd "${BATS_TEST_TMPDIR}" 220 | run bats "$FIXTURE_ROOT/failing.bats" 221 | [ $status -eq 1 ] 222 | [ "${lines[2]}" = "# (in test file $FIXTURE_ROOT/failing.bats, line 4)" ] 223 | } 224 | 225 | @test "load sources scripts relative to the current test file" { 226 | run bats "$FIXTURE_ROOT/load.bats" 227 | [ $status -eq 0 ] 228 | } 229 | 230 | @test "load sources relative scripts with filename extension" { 231 | HELPER_NAME="test_helper.bash" run bats "$FIXTURE_ROOT/load.bats" 232 | [ $status -eq 0 ] 233 | } 234 | 235 | @test "load aborts if the specified script does not exist" { 236 | HELPER_NAME="nonexistent" run bats "$FIXTURE_ROOT/load.bats" 237 | [ $status -eq 1 ] 238 | } 239 | 240 | @test "load sources scripts by absolute path" { 241 | HELPER_NAME="${FIXTURE_ROOT}/test_helper.bash" run bats "$FIXTURE_ROOT/load.bats" 242 | [ $status -eq 0 ] 243 | } 244 | 245 | @test "load aborts if the script, specified by an absolute path, does not exist" { 246 | HELPER_NAME="${FIXTURE_ROOT}/nonexistent" run bats "$FIXTURE_ROOT/load.bats" 247 | [ $status -eq 1 ] 248 | } 249 | 250 | @test "load relative script with ambiguous name" { 251 | HELPER_NAME="ambiguous" run bats "$FIXTURE_ROOT/load.bats" 252 | [ $status -eq 0 ] 253 | } 254 | 255 | @test "load supports scripts on the PATH" { 256 | path_dir="$BATS_TMPNAME/path" 257 | mkdir -p "$path_dir" 258 | cp "${FIXTURE_ROOT}/test_helper.bash" "${path_dir}/on_path" 259 | PATH="${path_dir}:$PATH" HELPER_NAME="on_path" run bats "$FIXTURE_ROOT/load.bats" 260 | [ $status -eq 0 ] 261 | } 262 | 263 | @test "load supports plain symbols" { 264 | local -r helper="${BATS_TEST_TMPDIR}/load_helper_plain" 265 | { 266 | echo "plain_variable='value of plain variable'" 267 | echo "plain_array=(test me hard)" 268 | } >"${helper}" 269 | 270 | load "${helper}" 271 | # shellcheck disable=SC2154 272 | [ "${plain_variable}" = 'value of plain variable' ] 273 | # shellcheck disable=SC2154 274 | [ "${plain_array[2]}" = 'hard' ] 275 | 276 | rm "${helper}" 277 | } 278 | 279 | @test "load doesn't support _declare_d symbols" { 280 | local -r helper="${BATS_TEST_TMPDIR}/load_helper_declared" 281 | { 282 | echo "declare declared_variable='value of declared variable'" 283 | echo "declare -r a_constant='constant value'" 284 | echo "declare -i an_integer=0x7e4" 285 | echo "declare -a an_array=(test me hard)" 286 | echo "declare -x exported_variable='value of exported variable'" 287 | } >"${helper}" 288 | 289 | load "${helper}" 290 | 291 | [ "${declared_variable:-}" != 'value of declared variable' ] 292 | [ "${a_constant:-}" != 'constant value' ] 293 | (("${an_integer:-2019}" != 2020)) 294 | [ "${an_array[2]:-}" != 'hard' ] 295 | [ "${exported_variable:-}" != 'value of exported variable' ] 296 | 297 | rm "${helper}" 298 | } 299 | 300 | @test "output is discarded for passing tests and printed for failing tests" { 301 | run bats "$FIXTURE_ROOT/output.bats" 302 | [ $status -eq 1 ] 303 | [ "${lines[6]}" = '# failure stdout 1' ] 304 | [ "${lines[7]}" = '# failure stdout 2' ] 305 | [ "${lines[11]}" = '# failure stderr' ] 306 | } 307 | 308 | @test "-c prints the number of tests" { 309 | run bats -c "$FIXTURE_ROOT/empty.bats" 310 | [ $status -eq 0 ] 311 | [ "$output" = 0 ] 312 | 313 | run bats -c "$FIXTURE_ROOT/output.bats" 314 | [ $status -eq 0 ] 315 | [ "$output" = 4 ] 316 | } 317 | 318 | @test "dash-e is not mangled on beginning of line" { 319 | run bats "$FIXTURE_ROOT/intact.bats" 320 | [ $status -eq 0 ] 321 | [ "${lines[1]}" = "ok 1 dash-e on beginning of line" ] 322 | } 323 | 324 | @test "dos line endings are stripped before testing" { 325 | run bats "$FIXTURE_ROOT/dos_line_no_shellcheck.bats" 326 | [ $status -eq 0 ] 327 | } 328 | 329 | @test "test file without trailing newline" { 330 | run bats "$FIXTURE_ROOT/without_trailing_newline.bats" 331 | [ $status -eq 0 ] 332 | [ "${lines[1]}" = "ok 1 truth" ] 333 | } 334 | 335 | @test "skipped tests" { 336 | run bats "$FIXTURE_ROOT/skipped.bats" 337 | [ $status -eq 0 ] 338 | [ "${lines[1]}" = "ok 1 a skipped test # skip" ] 339 | [ "${lines[2]}" = "ok 2 a skipped test with a reason # skip a reason" ] 340 | } 341 | 342 | @test "skipped test with parens (pretty formatter)" { 343 | run bats --pretty "$FIXTURE_ROOT/skipped_with_parens.bats" 344 | [ $status -eq 0 ] 345 | 346 | # Some systems (Alpine, for example) seem to emit an extra whitespace into 347 | # entries in the 'lines' array when a carriage return is present from the 348 | # pretty formatter. This is why a '+' is used after the 'skipped' note. 349 | [[ "${lines[*]}" =~ "- a skipped test with parentheses in the reason (skipped: "+"a reason (with parentheses))" ]] 350 | } 351 | 352 | @test "extended syntax" { 353 | emulate_bats_env 354 | run bats-exec-suite -x "$FIXTURE_ROOT/failing_and_passing.bats" 355 | echo "$output" 356 | [ $status -eq 1 ] 357 | [ "${lines[1]}" = "suite $FIXTURE_ROOT/failing_and_passing.bats" ] 358 | [ "${lines[2]}" = 'begin 1 a failing test' ] 359 | [ "${lines[3]}" = 'not ok 1 a failing test' ] 360 | [ "${lines[6]}" = 'begin 2 a passing test' ] 361 | [ "${lines[7]}" = 'ok 2 a passing test' ] 362 | } 363 | 364 | @test "timing syntax" { 365 | run bats -T "$FIXTURE_ROOT/failing_and_passing.bats" 366 | echo "$output" 367 | [ $status -eq 1 ] 368 | regex='not ok 1 a failing test in [0-9]+ms' 369 | [[ "${lines[1]}" =~ $regex ]] 370 | regex='ok 2 a passing test in [0-9]+ms' 371 | [[ "${lines[4]}" =~ $regex ]] 372 | } 373 | 374 | @test "extended timing syntax" { 375 | emulate_bats_env 376 | run bats-exec-suite -x -T "$FIXTURE_ROOT/failing_and_passing.bats" 377 | echo "$output" 378 | [ $status -eq 1 ] 379 | regex="not ok 1 a failing test in [0-9]+ms" 380 | [ "${lines[2]}" = 'begin 1 a failing test' ] 381 | [[ "${lines[3]}" =~ $regex ]] 382 | [ "${lines[6]}" = 'begin 2 a passing test' ] 383 | regex="ok 2 a passing test in [0-9]+ms" 384 | [[ "${lines[7]}" =~ $regex ]] 385 | } 386 | 387 | @test "time is greater than 0ms for long test" { 388 | emulate_bats_env 389 | run bats-exec-suite -x -T "$FIXTURE_ROOT/run_long_command.bats" 390 | echo "$output" 391 | [ $status -eq 0 ] 392 | regex="ok 1 run long command in [1-9][0-9]*ms" 393 | [[ "${lines[3]}" =~ $regex ]] 394 | } 395 | 396 | @test "pretty and tap formats" { 397 | run bats --formatter tap "$FIXTURE_ROOT/passing.bats" 398 | tap_output="$output" 399 | [ $status -eq 0 ] 400 | 401 | run bats --pretty "$FIXTURE_ROOT/passing.bats" 402 | pretty_output="$output" 403 | [ $status -eq 0 ] 404 | 405 | [ "$tap_output" != "$pretty_output" ] 406 | } 407 | 408 | @test "pretty formatter bails on invalid tap" { 409 | run bats-format-pretty < <(printf "This isn't TAP!\nGood day to you\n") 410 | [ $status -eq 0 ] 411 | [ "${lines[0]}" = "This isn't TAP!" ] 412 | [ "${lines[1]}" = "Good day to you" ] 413 | } 414 | 415 | @test "single-line tests" { 416 | run bats "$FIXTURE_ROOT/single_line_no_shellcheck.bats" 417 | [ $status -eq 1 ] 418 | [ "${lines[1]}" = 'ok 1 empty' ] 419 | [ "${lines[2]}" = 'ok 2 passing' ] 420 | [ "${lines[3]}" = 'ok 3 input redirection' ] 421 | [ "${lines[4]}" = 'not ok 4 failing' ] 422 | [ "${lines[5]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/single_line_no_shellcheck.bats, line 9)" ] 423 | [ "${lines[6]}" = $'# `@test "failing" { false; }\' failed' ] 424 | } 425 | 426 | @test "testing IFS not modified by run" { 427 | run bats "$FIXTURE_ROOT/loop_keep_IFS.bats" 428 | [ $status -eq 0 ] 429 | [ "${lines[1]}" = "ok 1 loop_func" ] 430 | } 431 | 432 | @test "expand variables in test name" { 433 | SUITE='test/suite' run bats "$FIXTURE_ROOT/expand_var_in_test_name.bats" 434 | [ $status -eq 0 ] 435 | [ "${lines[1]}" = "ok 1 test/suite: test with variable in name" ] 436 | } 437 | 438 | @test "handle quoted and unquoted test names" { 439 | run bats "$FIXTURE_ROOT/quoted_and_unquoted_test_names_no_shellcheck.bats" 440 | [ $status -eq 0 ] 441 | [ "${lines[1]}" = "ok 1 single-quoted name" ] 442 | [ "${lines[2]}" = "ok 2 double-quoted name" ] 443 | [ "${lines[3]}" = "ok 3 unquoted name" ] 444 | } 445 | 446 | @test 'ensure compatibility with unofficial Bash strict mode' { 447 | local expected='ok 1 unofficial Bash strict mode conditions met' 448 | 449 | # Run Bats under SHELLOPTS=nounset (recursive `set -u`) to catch 450 | # as many unset variable accesses as possible. 451 | run run_under_clean_bats_env env SHELLOPTS=nounset \ 452 | "${BATS_ROOT}/bin/bats" "$FIXTURE_ROOT/unofficial_bash_strict_mode.bats" 453 | if [[ "$status" -ne 0 || "${lines[1]}" != "$expected" ]]; then 454 | cat <&2 511 | printf 'actual: "%s"\n' "${lines[0]}" >&2 512 | [ "${lines[0]}" = "$expected" ] 513 | 514 | printf 'num lines: %d\n' "${#lines[*]}" >&2 515 | [ "${#lines[*]}" = "1" ] 516 | } 517 | 518 | @test "sourcing a nonexistent file in setup produces error output" { 519 | run bats "$FIXTURE_ROOT/source_nonexistent_file_in_setup.bats" 520 | [ $status -eq 1 ] 521 | [ "${lines[1]}" = 'not ok 1 sourcing nonexistent file fails in setup' ] 522 | [ "${lines[2]}" = "# (from function \`setup' in test file $RELATIVE_FIXTURE_ROOT/source_nonexistent_file_in_setup.bats, line 3)" ] 523 | [ "${lines[3]}" = "# \`source \"nonexistent file\"' failed" ] 524 | } 525 | 526 | @test "referencing unset parameter in setup produces error output" { 527 | run bats "$FIXTURE_ROOT/reference_unset_parameter_in_setup.bats" 528 | [ $status -eq 1 ] 529 | [ "${lines[1]}" = 'not ok 1 referencing unset parameter fails in setup' ] 530 | [ "${lines[2]}" = "# (from function \`setup' in test file $RELATIVE_FIXTURE_ROOT/reference_unset_parameter_in_setup.bats, line 4)" ] 531 | [ "${lines[3]}" = "# \`echo \"\$unset_parameter\"' failed" ] 532 | } 533 | 534 | @test "sourcing a nonexistent file in test produces error output" { 535 | run bats "$FIXTURE_ROOT/source_nonexistent_file.bats" 536 | [ $status -eq 1 ] 537 | [ "${lines[1]}" = 'not ok 1 sourcing nonexistent file fails' ] 538 | [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/source_nonexistent_file.bats, line 3)" ] 539 | [ "${lines[3]}" = "# \`source \"nonexistent file\"' failed" ] 540 | } 541 | 542 | @test "referencing unset parameter in test produces error output" { 543 | run bats "$FIXTURE_ROOT/reference_unset_parameter.bats" 544 | [ $status -eq 1 ] 545 | [ "${lines[1]}" = 'not ok 1 referencing unset parameter fails' ] 546 | [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/reference_unset_parameter.bats, line 4)" ] 547 | [ "${lines[3]}" = "# \`echo \"\$unset_parameter\"' failed" ] 548 | } 549 | 550 | @test "sourcing a nonexistent file in teardown produces error output" { 551 | run bats "$FIXTURE_ROOT/source_nonexistent_file_in_teardown.bats" 552 | [ $status -eq 1 ] 553 | [ "${lines[1]}" = 'not ok 1 sourcing nonexistent file fails in teardown' ] 554 | [ "${lines[2]}" = "# (from function \`teardown' in test file $RELATIVE_FIXTURE_ROOT/source_nonexistent_file_in_teardown.bats, line 3)" ] 555 | [ "${lines[3]}" = "# \`source \"nonexistent file\"' failed" ] 556 | } 557 | 558 | @test "referencing unset parameter in teardown produces error output" { 559 | run bats "$FIXTURE_ROOT/reference_unset_parameter_in_teardown.bats" 560 | [ $status -eq 1 ] 561 | [ "${lines[1]}" = 'not ok 1 referencing unset parameter fails in teardown' ] 562 | [ "${lines[2]}" = "# (from function \`teardown' in test file $RELATIVE_FIXTURE_ROOT/reference_unset_parameter_in_teardown.bats, line 4)" ] 563 | [ "${lines[3]}" = "# \`echo \"\$unset_parameter\"' failed" ] 564 | } 565 | 566 | @test "execute exported function without breaking failing test output" { 567 | exported_function() { return 0; } 568 | export -f exported_function 569 | run bats "$FIXTURE_ROOT/exported_function.bats" 570 | [ $status -eq 1 ] 571 | [ "${lines[0]}" = "1..1" ] 572 | [ "${lines[1]}" = "not ok 1 failing test" ] 573 | [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/exported_function.bats, line 7)" ] 574 | [ "${lines[3]}" = "# \`false' failed" ] 575 | [ "${lines[4]}" = "# a='exported_function'" ] 576 | } 577 | 578 | @test "output printed even when no final newline" { 579 | run bats "$FIXTURE_ROOT/no-final-newline.bats" 580 | printf 'num lines: %d\n' "${#lines[@]}" >&2 581 | printf 'LINE: %s\n' "${lines[@]}" >&2 582 | [ "$status" -eq 1 ] 583 | [ "${#lines[@]}" -eq 11 ] 584 | [ "${lines[1]}" = 'not ok 1 error in test' ] 585 | [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/no-final-newline.bats, line 3)" ] 586 | [ "${lines[3]}" = "# \`false' failed" ] 587 | [ "${lines[4]}" = '# foo' ] 588 | [ "${lines[5]}" = '# bar' ] 589 | [ "${lines[6]}" = 'not ok 2 test function returns nonzero' ] 590 | [ "${lines[7]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/no-final-newline.bats, line 8)" ] 591 | [ "${lines[8]}" = "# \`return 1' failed" ] 592 | [ "${lines[9]}" = '# foo' ] 593 | [ "${lines[10]}" = '# bar' ] 594 | } 595 | 596 | @test "run tests which consume stdin (see #197)" { 597 | run bats "$FIXTURE_ROOT/read_from_stdin.bats" 598 | [ "$status" -eq 0 ] 599 | [[ "${lines[0]}" == "1..3" ]] 600 | [[ "${lines[1]}" == "ok 1 test 1" ]] 601 | [[ "${lines[2]}" == "ok 2 test 2 with TAB in name" ]] 602 | [[ "${lines[3]}" == "ok 3 test 3" ]] 603 | } 604 | 605 | @test "report correct line on unset variables" { 606 | LANG=C run bats "$FIXTURE_ROOT/unbound_variable.bats" 607 | [ "$status" -eq 1 ] 608 | [ "${#lines[@]}" -eq 9 ] 609 | [ "${lines[1]}" = 'not ok 1 access unbound variable' ] 610 | [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/unbound_variable.bats, line 9)" ] 611 | [ "${lines[3]}" = "# \`foo=\$unset_variable' failed" ] 612 | # shellcheck disable=SC2076 613 | [[ "${lines[4]}" =~ ".src: line 9:" ]] 614 | [ "${lines[5]}" = 'not ok 2 access second unbound variable' ] 615 | [ "${lines[6]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/unbound_variable.bats, line 15)" ] 616 | [ "${lines[7]}" = "# \`foo=\$second_unset_variable' failed" ] 617 | # shellcheck disable=SC2076 618 | [[ "${lines[8]}" =~ ".src: line 15:" ]] 619 | } 620 | 621 | @test "report correct line on external function calls" { 622 | run bats "$FIXTURE_ROOT/external_function_calls.bats" 623 | [ "$status" -eq 1 ] 624 | 625 | expectedNumberOfTests=12 626 | linesPerTest=5 627 | 628 | outputOffset=1 629 | currentErrorLine=9 630 | 631 | for t in $(seq $expectedNumberOfTests); do 632 | # shellcheck disable=SC2076 633 | [[ "${lines[$outputOffset]}" =~ "not ok $t " ]] 634 | 635 | [[ "${lines[$outputOffset]}" =~ stackdepth=([0-9]+) ]] 636 | stackdepth="${BASH_REMATCH[1]}" 637 | case "${stackdepth}" in 638 | 1) 639 | [ "${lines[$((outputOffset + 1))]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/external_function_calls.bats, line $currentErrorLine)" ] 640 | outputOffset=$((outputOffset + 3)) 641 | ;; 642 | 2) 643 | [[ "${lines[$((outputOffset + 1))]}" =~ ^'# (from function `'.*\'' in file '.*'/test_helper.bash, line '[0-9]+,$ ]] 644 | [ "${lines[$((outputOffset + 2))]}" = "# in test file $RELATIVE_FIXTURE_ROOT/external_function_calls.bats, line $currentErrorLine)" ] 645 | outputOffset=$((outputOffset + 4)) 646 | ;; 647 | *) 648 | printf 'error: stackdepth=%s not implemented\n' "${stackdepth}" >&2 649 | return 1 650 | ;; 651 | esac 652 | currentErrorLine=$((currentErrorLine + linesPerTest)) 653 | done 654 | } 655 | 656 | @test "test count validator catches mismatch and returns non zero" { 657 | # shellcheck source=lib/bats-core/validator.bash 658 | source "$BATS_ROOT/lib/bats-core/validator.bash" 659 | export -f bats_test_count_validator 660 | run bash -c "echo $'1..1\n' | bats_test_count_validator" 661 | [[ $status -ne 0 ]] 662 | 663 | run bash -c "echo $'1..1\nok 1\nok 2' | bats_test_count_validator" 664 | [[ $status -ne 0 ]] 665 | 666 | run bash -c "echo $'1..1\nok 1' | bats_test_count_validator" 667 | [[ $status -eq 0 ]] 668 | } 669 | 670 | @test "running the same file twice runs its tests twice without errors" { 671 | run bats "$FIXTURE_ROOT/passing.bats" "$FIXTURE_ROOT/passing.bats" 672 | echo "$output" 673 | [[ $status -eq 0 ]] 674 | [[ "${lines[0]}" == "1..2" ]] # got 2x1 tests 675 | } 676 | 677 | @test "Don't use unbound variables inside bats (issue #340)" { 678 | run bats "$FIXTURE_ROOT/set_-eu_in_setup_and_teardown.bats" 679 | echo "$output" 680 | [[ "${lines[0]}" == "1..4" ]] 681 | [[ "${lines[1]}" == "ok 1 skipped test # skip" ]] 682 | [[ "${lines[2]}" == "ok 2 skipped test with reason # skip reason" ]] 683 | [[ "${lines[3]}" == "ok 3 passing test" ]] 684 | [[ "${lines[4]}" == "not ok 4 failing test" ]] 685 | [[ "${lines[5]}" == "# (in test file $RELATIVE_FIXTURE_ROOT/set_-eu_in_setup_and_teardown.bats, line 22)" ]] 686 | [[ "${lines[6]}" == "# \`false' failed" ]] 687 | [[ "${#lines[@]}" -eq 7 ]] 688 | } 689 | 690 | @test "filenames with tab can be used" { 691 | [[ "$OSTYPE" == "linux"* ]] || skip "FS cannot deal with tabs in filenames" 692 | 693 | cp "${FIXTURE_ROOT}/tab in filename.bats" "${BATS_TEST_TMPDIR}/tab"$'\t'"in filename.bats" 694 | bats "${BATS_TEST_TMPDIR}/tab"$'\t'"in filename.bats" 695 | } 696 | 697 | @test "each file is evaluated n+1 times" { 698 | # shellcheck disable=SC2031,SC2030 699 | export TEMPFILE="$BATS_TEST_TMPDIR/$BATS_TEST_NAME.log" 700 | run bats "$FIXTURE_ROOT/evaluation_count/" 701 | 702 | cat "$TEMPFILE" 703 | 704 | run grep "file1" "$TEMPFILE" 705 | [[ ${#lines[@]} -eq 2 ]] 706 | 707 | run grep "file2" "$TEMPFILE" 708 | [[ ${#lines[@]} -eq 3 ]] 709 | } 710 | 711 | @test "Don't hang on CTRL-C (issue #353)" { 712 | load 'concurrent-coordination' 713 | # shellcheck disable=SC2031,SC2030 714 | export SINGLE_USE_LATCH_DIR="${BATS_TEST_TMPDIR}" 715 | 716 | # guarantee that background processes get their own process group -> pid=pgid 717 | set -m 718 | bats "$FIXTURE_ROOT/hang_in_test.bats" & # don't block execution, or we cannot send signals 719 | SUBPROCESS_PID=$! 720 | 721 | single-use-latch::wait hang_in_test 1 722 | 723 | # emulate CTRL-C by sending SIGINT to the whole process group 724 | kill -SIGINT -- -$SUBPROCESS_PID 725 | 726 | sleep 1 # wait for the signal to be acted upon 727 | 728 | # when the process is gone, we cannot deliver a signal anymore, getting non-zero from kill 729 | run kill -0 -- -$SUBPROCESS_PID 730 | [[ $status -ne 0 ]] || 731 | ( 732 | kill -9 -- -$SUBPROCESS_PID 733 | false 734 | ) 735 | # ^ kill the process for good when SIGINT failed, 736 | # to avoid waiting endlessly for stuck children to finish 737 | } 738 | 739 | @test "test comment style" { 740 | run bats "$FIXTURE_ROOT/comment_style.bats" 741 | [ $status -eq 0 ] 742 | [ "${lines[0]}" = '1..6' ] 743 | [ "${lines[1]}" = 'ok 1 should_be_found' ] 744 | [ "${lines[2]}" = 'ok 2 should_be_found_with_trailing_whitespace' ] 745 | [ "${lines[3]}" = 'ok 3 should_be_found_with_parens' ] 746 | [ "${lines[4]}" = 'ok 4 should_be_found_with_parens_and_whitespace' ] 747 | [ "${lines[5]}" = 'ok 5 should_be_found_with_function_and_parens' ] 748 | [ "${lines[6]}" = 'ok 6 should_be_found_with_function_parens_and_whitespace' ] 749 | } 750 | 751 | @test "test works even if PATH is reset" { 752 | run bats "$FIXTURE_ROOT/update_path_env.bats" 753 | [ "$status" -eq 1 ] 754 | [ "${lines[4]}" = "# /usr/local/bin:/usr/bin:/bin" ] 755 | } 756 | 757 | @test "Test nounset does not trip up bats' internals (see #385)" { 758 | # don't export nounset within this file or we might trip up the testsuite itself, 759 | # getting bad diagnostics 760 | run bash -c "set -o nounset; export SHELLOPTS; bats --tap '$FIXTURE_ROOT/passing.bats'" 761 | echo "$output" 762 | [ "${lines[0]}" = "1..1" ] 763 | [ "${lines[1]}" = "ok 1 a passing test" ] 764 | [ ${#lines[@]} = 2 ] 765 | } 766 | 767 | @test "run tmpdir is cleaned up by default" { 768 | TEST_TMPDIR="${BATS_TEST_TMPDIR}/$BATS_TEST_NAME" 769 | bats --tempdir "$TEST_TMPDIR" "$FIXTURE_ROOT/passing.bats" 770 | 771 | [ ! -d "$TEST_TMPDIR" ] 772 | } 773 | 774 | @test "run tmpdir is not cleanup up with --no-cleanup-tempdir" { 775 | TEST_TMPDIR="${BATS_TEST_TMPDIR}/$BATS_TEST_NAME" 776 | bats --tempdir "$TEST_TMPDIR" --no-tempdir-cleanup "$FIXTURE_ROOT/passing.bats" 777 | 778 | [ -d "$TEST_TMPDIR" ] 779 | 780 | # should also find preprocessed files! 781 | [ "$(find "$TEST_TMPDIR" -name '*.src' | wc -l)" -eq 1 ] 782 | } 783 | 784 | @test "All formatters (except cat) implement the callback interface" { 785 | cd "$BATS_ROOT/libexec/bats-core/" 786 | for formatter in bats-format-*; do 787 | # the cat formatter is not expected to implement this interface 788 | if [[ "$formatter" == *"bats-format-cat" ]]; then 789 | continue 790 | fi 791 | tested_at_least_one_formatter=1 792 | echo "Formatter: ${formatter}" 793 | # the replay should be possible without errors 794 | "$formatter" >/dev/null </dev/null || skip "--jobs requires GNU parallel" 848 | (type -p flock &>/dev/null || type -p shlock &>/dev/null) || skip "--jobs requires flock/shlock" 849 | run bats -j 2 "$FIXTURE_ROOT/issue-433" 850 | 851 | [ "$status" -eq 0 ] 852 | [[ "$output" != *"No such file or directory"* ]] || exit 1 # ensure failures are detected with old bash 853 | } 854 | 855 | @test "Failure in free code (see #399)" { 856 | run bats --tap "$FIXTURE_ROOT/failure_in_free_code.bats" 857 | echo "$output" 858 | [ "$status" -ne 0 ] 859 | [ "${lines[0]}" == 1..1 ] 860 | [ "${lines[1]}" == 'not ok 1 setup_file failed' ] 861 | [ "${lines[2]}" == "# (from function \`helper' in file $RELATIVE_FIXTURE_ROOT/failure_in_free_code.bats, line 4," ] 862 | [ "${lines[3]}" == "# in test file $RELATIVE_FIXTURE_ROOT/failure_in_free_code.bats, line 7)" ] 863 | [ "${lines[4]}" == "# \`helper' failed" ] 864 | } 865 | 866 | @test "CTRL-C aborts and fails the current test" { 867 | if [[ "$BATS_NUMBER_OF_PARALLEL_JOBS" -gt 1 ]]; then 868 | skip "Aborts don't work in parallel mode" 869 | fi 870 | 871 | # shellcheck disable=SC2031,SC2030 872 | export TEMPFILE="$BATS_TEST_TMPDIR/$BATS_TEST_NAME.log" 873 | 874 | # guarantee that background processes get their own process group -> pid=pgid 875 | set -m 876 | 877 | load 'concurrent-coordination' 878 | # shellcheck disable=SC2031,SC2030 879 | export SINGLE_USE_LATCH_DIR="${BATS_SUITE_TMPDIR}" 880 | # we cannot use run for a background task, so we have to store the output for later 881 | bats "$FIXTURE_ROOT/hang_in_test.bats" --tap >"$TEMPFILE" 2>&1 & # don't block execution, or we cannot send signals 882 | 883 | SUBPROCESS_PID=$! 884 | 885 | single-use-latch::wait hang_in_test 1 10 || ( 886 | cat "$TEMPFILE" 887 | false 888 | ) # still forward output on timeout 889 | 890 | # emulate CTRL-C by sending SIGINT to the whole process group 891 | kill -SIGINT -- -$SUBPROCESS_PID 892 | 893 | # the test suite must be marked as failed! 894 | wait $SUBPROCESS_PID && return 1 895 | 896 | run cat "$TEMPFILE" 897 | echo "$output" 898 | 899 | [[ "${lines[1]}" == "not ok 1 test" ]] 900 | [[ "${lines[2]}" == "# (in test file ${RELATIVE_FIXTURE_ROOT}/hang_in_test.bats, line 7)" ]] 901 | [[ "${lines[3]}" == "# \`sleep 10' failed with status 130" ]] 902 | [[ "${lines[4]}" == "# Received SIGINT, aborting ..." ]] 903 | } 904 | 905 | @test "CTRL-C aborts and fails the current run" { 906 | if [[ "$BATS_NUMBER_OF_PARALLEL_JOBS" -gt 1 ]]; then 907 | skip "Aborts don't work in parallel mode" 908 | fi 909 | 910 | # shellcheck disable=SC2031,2030 911 | export TEMPFILE="$BATS_TEST_TMPDIR/$BATS_TEST_NAME.log" 912 | 913 | # guarantee that background processes get their own process group -> pid=pgid 914 | set -m 915 | 916 | load 'concurrent-coordination' 917 | # shellcheck disable=SC2031,SC2030 918 | export SINGLE_USE_LATCH_DIR="${BATS_SUITE_TMPDIR}" 919 | # we cannot use run for a background task, so we have to store the output for later 920 | bats "$FIXTURE_ROOT/hang_in_run.bats" --tap >"$TEMPFILE" 2>&1 & # don't block execution, or we cannot send signals 921 | 922 | SUBPROCESS_PID=$! 923 | 924 | single-use-latch::wait hang_in_run 1 10 925 | 926 | # emulate CTRL-C by sending SIGINT to the whole process group 927 | kill -SIGINT -- -$SUBPROCESS_PID 928 | 929 | # the test suite must be marked as failed! 930 | wait $SUBPROCESS_PID && return 1 931 | 932 | run cat "$TEMPFILE" 933 | 934 | [ "${lines[1]}" == "not ok 1 test" ] 935 | [ "${lines[2]}" == "# (in test file ${RELATIVE_FIXTURE_ROOT}/hang_in_run.bats, line 7)" ] 936 | [ "${lines[3]}" == "# \`run sleep 10' failed with status 130" ] 937 | [ "${lines[4]}" == "# Received SIGINT, aborting ..." ] 938 | } 939 | 940 | @test "CTRL-C aborts and fails after run" { 941 | if [[ "$BATS_NUMBER_OF_PARALLEL_JOBS" -gt 1 ]]; then 942 | skip "Aborts don't work in parallel mode" 943 | fi 944 | 945 | # shellcheck disable=SC2031,2030 946 | export TEMPFILE="$BATS_TEST_TMPDIR/$BATS_TEST_NAME.log" 947 | 948 | # guarantee that background processes get their own process group -> pid=pgid 949 | set -m 950 | 951 | load 'concurrent-coordination' 952 | # shellcheck disable=SC2031,SC2030 953 | export SINGLE_USE_LATCH_DIR="${BATS_SUITE_TMPDIR}" 954 | # we cannot use run for a background task, so we have to store the output for later 955 | bats "$FIXTURE_ROOT/hang_after_run.bats" --tap >"$TEMPFILE" 2>&1 & # don't block execution, or we cannot send signals 956 | 957 | SUBPROCESS_PID=$! 958 | 959 | single-use-latch::wait hang_after_run 1 10 960 | 961 | # emulate CTRL-C by sending SIGINT to the whole process group 962 | kill -SIGINT -- -$SUBPROCESS_PID 963 | 964 | # the test suite must be marked as failed! 965 | wait $SUBPROCESS_PID && return 1 966 | 967 | run cat "$TEMPFILE" 968 | 969 | [ "${lines[1]}" == "not ok 1 test" ] 970 | [ "${lines[2]}" == "# (in test file ${RELATIVE_FIXTURE_ROOT}/hang_after_run.bats, line 8)" ] 971 | [ "${lines[3]}" == "# \`sleep 10' failed with status 130" ] 972 | [ "${lines[4]}" == "# Received SIGINT, aborting ..." ] 973 | } 974 | 975 | @test "CTRL-C aborts and fails the current teardown" { 976 | if [[ "$BATS_NUMBER_OF_PARALLEL_JOBS" -gt 1 ]]; then 977 | skip "Aborts don't work in parallel mode" 978 | fi 979 | 980 | # shellcheck disable=SC2031,SC2030 981 | export TEMPFILE="$BATS_TEST_TMPDIR/$BATS_TEST_NAME.log" 982 | 983 | # guarantee that background processes get their own process group -> pid=pgid 984 | set -m 985 | 986 | load 'concurrent-coordination' 987 | # shellcheck disable=SC2031,SC2030 988 | export SINGLE_USE_LATCH_DIR="${BATS_SUITE_TMPDIR}" 989 | # we cannot use run for a background task, so we have to store the output for later 990 | bats "$FIXTURE_ROOT/hang_in_teardown.bats" --tap >"$TEMPFILE" 2>&1 & # don't block execution, or we cannot send signals 991 | 992 | SUBPROCESS_PID=$! 993 | 994 | single-use-latch::wait hang_in_teardown 1 10 995 | 996 | # emulate CTRL-C by sending SIGINT to the whole process group 997 | kill -SIGINT -- -$SUBPROCESS_PID 998 | 999 | # the test suite must be marked as failed! 1000 | wait $SUBPROCESS_PID && return 1 1001 | 1002 | run cat "$TEMPFILE" 1003 | echo "$output" 1004 | 1005 | [[ "${lines[1]}" == "not ok 1 empty" ]] 1006 | [[ "${lines[2]}" == "# (from function \`teardown' in test file ${RELATIVE_FIXTURE_ROOT}/hang_in_teardown.bats, line 4)" ]] 1007 | [[ "${lines[3]}" == "# \`sleep 10' failed with status 130" ]] 1008 | [[ "${lines[4]}" == "# Received SIGINT, aborting ..." ]] 1009 | } 1010 | 1011 | @test "CTRL-C aborts and fails the current setup_file" { 1012 | if [[ "$BATS_NUMBER_OF_PARALLEL_JOBS" -gt 1 ]]; then 1013 | skip "Aborts don't work in parallel mode" 1014 | fi 1015 | 1016 | # shellcheck disable=SC2031,SC2030 1017 | export TEMPFILE="$BATS_TEST_TMPDIR/$BATS_TEST_NAME.log" 1018 | 1019 | # guarantee that background processes get their own process group -> pid=pgid 1020 | set -m 1021 | 1022 | load 'concurrent-coordination' 1023 | # shellcheck disable=SC2031,SC2030 1024 | export SINGLE_USE_LATCH_DIR="${BATS_SUITE_TMPDIR}" 1025 | # we cannot use run for a background task, so we have to store the output for later 1026 | bats "$FIXTURE_ROOT/hang_in_setup_file.bats" --tap >"$TEMPFILE" 2>&1 & # don't block execution, or we cannot send signals 1027 | 1028 | SUBPROCESS_PID=$! 1029 | 1030 | single-use-latch::wait hang_in_setup_file 1 10 1031 | 1032 | # emulate CTRL-C by sending SIGINT to the whole process group 1033 | kill -SIGINT -- -$SUBPROCESS_PID 1034 | 1035 | # the test suite must be marked as failed! 1036 | wait $SUBPROCESS_PID && return 1 1037 | 1038 | run cat "$TEMPFILE" 1039 | echo "$output" 1040 | 1041 | [[ "${lines[1]}" == "not ok 1 setup_file failed" ]] 1042 | [[ "${lines[2]}" == "# (from function \`setup_file' in test file ${RELATIVE_FIXTURE_ROOT}/hang_in_setup_file.bats, line 4)" ]] 1043 | [[ "${lines[3]}" == "# \`sleep 10' failed with status 130" ]] 1044 | [[ "${lines[4]}" == "# Received SIGINT, aborting ..." ]] 1045 | } 1046 | 1047 | @test "CTRL-C aborts and fails the current teardown_file" { 1048 | if [[ "$BATS_NUMBER_OF_PARALLEL_JOBS" -gt 1 ]]; then 1049 | skip "Aborts don't work in parallel mode" 1050 | fi 1051 | # shellcheck disable=SC2031 1052 | export TEMPFILE="${BATS_TEST_TMPDIR}/$BATS_TEST_NAME.log" 1053 | 1054 | # guarantee that background processes get their own process group -> pid=pgid 1055 | set -m 1056 | 1057 | load 'concurrent-coordination' 1058 | # shellcheck disable=SC2031 1059 | export SINGLE_USE_LATCH_DIR="${BATS_SUITE_TMPDIR}" 1060 | # we cannot use run for a background task, so we have to store the output for later 1061 | bats "$FIXTURE_ROOT/hang_in_teardown_file.bats" --tap >"$TEMPFILE" 2>&1 & # don't block execution, or we cannot send signals 1062 | 1063 | SUBPROCESS_PID=$! 1064 | 1065 | single-use-latch::wait hang_in_teardown_file 1 10 1066 | 1067 | # emulate CTRL-C by sending SIGINT to the whole process group 1068 | kill -SIGINT -- -$SUBPROCESS_PID 1069 | 1070 | # the test suite must be marked as failed! 1071 | wait $SUBPROCESS_PID && return 1 1072 | 1073 | run cat "$TEMPFILE" 1074 | echo "$output" 1075 | 1076 | [[ "${lines[0]}" == "1..1" ]] 1077 | [[ "${lines[1]}" == "ok 1 empty" ]] 1078 | [[ "${lines[2]}" == "not ok 2 teardown_file failed" ]] 1079 | [[ "${lines[3]}" == "# (from function \`teardown_file' in test file ${RELATIVE_FIXTURE_ROOT}/hang_in_teardown_file.bats, line 4)" ]] 1080 | [[ "${lines[4]}" == "# \`sleep 10' failed with status 130" ]] 1081 | [[ "${lines[5]}" == "# Received SIGINT, aborting ..." ]] 1082 | [[ "${lines[6]}" == "# bats warning: Executed 2 instead of expected 1 tests" ]] 1083 | } 1084 | 1085 | @test "single star in output is not treated as a glob" { 1086 | star() { echo '*'; } 1087 | 1088 | run star 1089 | [ "${lines[0]}" = '*' ] 1090 | } 1091 | 1092 | @test "multiple stars in output are not treated as a glob" { 1093 | stars() { echo '**'; } 1094 | 1095 | run stars 1096 | [ "${lines[0]}" = '**' ] 1097 | } 1098 | 1099 | @test "ensure all folders are shellchecked" { 1100 | if [[ ! -f "$BATS_ROOT/shellcheck.sh" ]]; then 1101 | skip "\$BATS_ROOT/shellcheck.sh is required for this test" 1102 | fi 1103 | cd "$BATS_ROOT" 1104 | run "./shellcheck.sh" --list 1105 | echo "$output" 1106 | 1107 | grep bin/bats <<<"$output" 1108 | grep contrib/ <<<"$output" 1109 | grep docker/ <<<"$output" 1110 | grep lib/bats-core/ <<<"$output" 1111 | grep libexec/bats-core/ <<<"$output" 1112 | grep test/fixtures <<<"$output" 1113 | grep install.sh <<<"$output" 1114 | } 1115 | 1116 | @test "BATS_RUN_COMMAND: test content of variable" { 1117 | run bats -v 1118 | [[ "${BATS_RUN_COMMAND}" == "bats -v" ]] 1119 | run bats "${BATS_TEST_DESCRIPTION}" 1120 | echo "$BATS_RUN_COMMAND" 1121 | [[ "$BATS_RUN_COMMAND" == "bats BATS_RUN_COMMAND: test content of variable" ]] 1122 | } 1123 | 1124 | @test "pretty formatter summary is colorized red on failure" { 1125 | run -1 bats --pretty "$FIXTURE_ROOT/failing.bats" 1126 | 1127 | [ "${lines[3]}" == $'\033[0m\033[31;1m' ] # TODO: avoid checking for the leading reset too 1128 | [ "${lines[4]}" == '1 test, 1 failure' ] 1129 | [ "${lines[5]}" == $'\033[0m' ] 1130 | } 1131 | 1132 | @test "pretty formatter summary is colorized green on success" { 1133 | run -0 bats --pretty "$FIXTURE_ROOT/passing.bats" 1134 | 1135 | [ "${lines[1]}" == $'\033[0m\033[32;1m' ] # TODO: avoid checking for the leading reset too 1136 | [ "${lines[2]}" == '1 test, 0 failures' ] 1137 | [ "${lines[3]}" == $'\033[0m' ] 1138 | } 1139 | 1140 | @test "--print-output-on-failure works as expected" { 1141 | run bats --print-output-on-failure --show-output-of-passing-tests "$FIXTURE_ROOT/print_output_on_failure.bats" 1142 | [ "${lines[0]}" == '1..3' ] 1143 | [ "${lines[1]}" == 'ok 1 no failure prints no output' ] 1144 | # ^ no output despite --show-output-of-passing-tests, because there is no failure 1145 | [ "${lines[2]}" == 'not ok 2 failure prints output' ] 1146 | [ "${lines[3]}" == "# (in test file $RELATIVE_FIXTURE_ROOT/print_output_on_failure.bats, line 6)" ] 1147 | [ "${lines[4]}" == "# \`run -1 echo \"fail hard\"' failed, expected exit code 1, got 0" ] 1148 | [ "${lines[5]}" == '# Last output:' ] 1149 | [ "${lines[6]}" == '# fail hard' ] 1150 | [ "${lines[7]}" == 'not ok 3 empty output on failure' ] 1151 | [ "${lines[8]}" == "# (in test file $RELATIVE_FIXTURE_ROOT/print_output_on_failure.bats, line 10)" ] 1152 | [ "${lines[9]}" == "# \`false' failed" ] 1153 | [ ${#lines[@]} -eq 10 ] 1154 | } 1155 | 1156 | @test "--show-output-of-passing-tests works as expected" { 1157 | run -0 bats --show-output-of-passing-tests "$FIXTURE_ROOT/show-output-of-passing-tests.bats" 1158 | [ "${lines[0]}" == '1..1' ] 1159 | [ "${lines[1]}" == 'ok 1 test' ] 1160 | [ "${lines[2]}" == '# output' ] 1161 | [ ${#lines[@]} -eq 3 ] 1162 | } 1163 | 1164 | @test "--verbose-run prints output" { 1165 | run -1 bats --verbose-run "$FIXTURE_ROOT/verbose-run.bats" 1166 | [ "${lines[0]}" == '1..1' ] 1167 | [ "${lines[1]}" == 'not ok 1 test' ] 1168 | [ "${lines[2]}" == "# (in test file $RELATIVE_FIXTURE_ROOT/verbose-run.bats, line 2)" ] 1169 | [ "${lines[3]}" == "# \`run ! echo test' failed, expected nonzero exit code!" ] 1170 | [ "${lines[4]}" == '# test' ] 1171 | [ ${#lines[@]} -eq 5 ] 1172 | } 1173 | 1174 | @test "BATS_VERBOSE_RUN=1 also prints output" { 1175 | run -1 env BATS_VERBOSE_RUN=1 bats "$FIXTURE_ROOT/verbose-run.bats" 1176 | [ "${lines[0]}" == '1..1' ] 1177 | [ "${lines[1]}" == 'not ok 1 test' ] 1178 | [ "${lines[2]}" == "# (in test file $RELATIVE_FIXTURE_ROOT/verbose-run.bats, line 2)" ] 1179 | [ "${lines[3]}" == "# \`run ! echo test' failed, expected nonzero exit code!" ] 1180 | [ "${lines[4]}" == '# test' ] 1181 | [ ${#lines[@]} -eq 5 ] 1182 | } 1183 | 1184 | @test "--gather-test-outputs-in gathers outputs of all tests (even succeeding!)" { 1185 | local OUTPUT_DIR="$BATS_TEST_TMPDIR/logs" 1186 | run bats --verbose-run --gather-test-outputs-in "$OUTPUT_DIR" "$FIXTURE_ROOT/print_output_on_failure.bats" 1187 | 1188 | [ -d "$OUTPUT_DIR" ] # will be generated! 1189 | 1190 | # even outputs of successful tests are generated 1191 | OUTPUT=$(<"$OUTPUT_DIR/1-no failure prints no output.log") # own line to trigger failure if file does not exist 1192 | [ "$OUTPUT" == "success" ] 1193 | 1194 | OUTPUT=$(<"$OUTPUT_DIR/2-failure prints output.log") 1195 | [ "$OUTPUT" == "fail hard" ] 1196 | 1197 | # even empty outputs are generated 1198 | OUTPUT=$(<"$OUTPUT_DIR/3-empty output on failure.log") 1199 | [ "$OUTPUT" == "" ] 1200 | 1201 | [ "$(find "$OUTPUT_DIR" -type f | wc -l)" -eq 3 ] 1202 | } 1203 | 1204 | @test "Tell about missing flock and shlock" { 1205 | if ! command -v parallel; then 1206 | skip "this test requires GNU parallel to be installed" 1207 | fi 1208 | if command -v flock; then 1209 | skip "this test requires flock not to be installed" 1210 | fi 1211 | if command -v shlock; then 1212 | skip "this test requires flock not to be installed" 1213 | fi 1214 | 1215 | run ! bats --jobs 2 "$FIXTURE_ROOT/parallel.bats" 1216 | [ "${lines[0]}" == "ERROR: flock/shlock is required for parallelization within files!" ] 1217 | [ "${#lines[@]}" -eq 1 ] 1218 | } 1219 | 1220 | @test "Test with a name that is waaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaay too long" { 1221 | skip "This test should only check if the long name chokes bats' internals during execution" 1222 | } 1223 | -------------------------------------------------------------------------------- /test/supported/error.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxundermoon/vs-shell-format/7099e276f4da38a334c2aa7682347c9a44d42fc0/test/supported/error.sh -------------------------------------------------------------------------------- /test/supported/getacme.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | #https://github.com/Neilpang/get.acme.sh 4 | 5 | _exists() { 6 | cmd="$1" 7 | if [ -z "$cmd" ]; 8 | then 9 | echo "Usage: _exists cmd" 10 | return 1 11 | fi 12 | if type command >/dev/null 2>&1 ; then 13 | command -v $cmd >/dev/null 2>&1 14 | else 15 | type $cmd >/dev/null 2>&1 16 | fi 17 | ret="$?" 18 | return $ret 19 | } 20 | 21 | if _exists curl && [ "${ACME_USE_WGET:-0}" = "0" ]; then 22 | curl https://raw.githubusercontent.com/Neilpang/acme.sh/master/acme.sh | \ 23 | INSTALLONLINE=1 sh 24 | elif _exists wget ; then 25 | wget -O - https://raw.githubusercontent.com/Neilpang/acme.sh/master/acme.sh |\ 26 | INSTALLONLINE=1 sh 27 | else 28 | echo "Sorry, you must have curl or wget installed first." 29 | echo "Please install either of them and try again." 30 | fi -------------------------------------------------------------------------------- /test/supported/hosts: -------------------------------------------------------------------------------- 1 | ## 2 | # Host Database 3 | # 4 | # localhost is used to configure the loopback interface 5 | # when the system is booting. Do not change this entry. 6 | ## 7 | 127.0.0.1 localhost 8 | 255.255.255.255 broadcasthost 9 | ::1 localhost 10 | 11 | 127.0.0.1 cas.example.org 12 | 13 | 8.8.8.8.8 dnsserver 14 | 114.114.114.114 dnsserver2 15 | 16 | -------------------------------------------------------------------------------- /test/supported/idea.vmoptions: -------------------------------------------------------------------------------- 1 | # custom IntelliJ IDEA VM options 2 | 3 | -ea 4 | -server 5 | -XX:+UseG1GC 6 | -XX:+UseNUMA 7 | -Xms4g 8 | -Xmx20g 9 | -XX:MaxGCPauseMillis=200 10 | -XX:MaxMetaspaceSize=2g 11 | -XX:+DisableExplicitGC 12 | # -XX:+UnlockExperimentalVMOptions 13 | # -XX:+UseZGC 14 | -XX:ParallelGCThreads=8 15 | -XX:ConcGCThreads=2 16 | -XX:MaxPermSize=4g 17 | -XX:ReservedCodeCacheSize=2g 18 | -XX:+UseCompressedOops 19 | -Dfile.encoding=UTF-8 20 | # default gc 21 | # -XX:+UseConcMarkSweepGC 22 | -XX:SoftRefLRUPolicyMSPerMB=50 23 | -Dsun.io.useCanonCaches=false 24 | -Djava.net.preferIPv4Stack=true 25 | -Djdk.http.auth.tunneling.disabledSchemes="" 26 | -XX:+HeapDumpOnOutOfMemoryError 27 | -XX:-OmitStackTraceInFastThrow 28 | -Xverify:none 29 | 30 | -XX:ErrorFile=$USER_HOME/java_error_in_idea_%p.log 31 | -XX:HeapDumpPath=$USER_HOME/java_error_in_idea.hprof 32 | -javaagent:/Users/fox/workspace/tool/jetbrains-agent/jetbrains-agent.jar 33 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | COUNTER=0 3 | while [ $COUNTER -lt 5 ]; do 4 | COUNTER=$(expr $COUNTER + 1) 5 | echo $COUNTER 6 | done 7 | 8 | echo 'type to terminate' 9 | echo -n 'enter your most liked film: ' 10 | while read FILM; do 11 | echo "Yeah! great film the $FILM" 12 | done 13 | 14 | a=0 15 | until [ ! $a -lt 10 ]; do 16 | echo $a 17 | a=$(expr $a + 1) 18 | if [ $a -eq 3 ]; then 19 | echo "yeah i am three" 20 | fi 21 | done 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": ["esnext"], 7 | "sourceMap": true, 8 | "rootDir": "." 9 | }, 10 | "exclude": ["node_modules", ".vscode-test", ".vscode"] 11 | } 12 | -------------------------------------------------------------------------------- /typings/node/diff.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for diff 2 | // Project: https://github.com/kpdecker/jsdiff 3 | // Definitions by: vvakame 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | 6 | declare namespace JsDiff { 7 | interface IDiffResult { 8 | value: string; 9 | count?: number; 10 | added?: boolean; 11 | removed?: boolean; 12 | } 13 | 14 | interface IBestPath { 15 | newPos: number; 16 | componenets: IDiffResult[]; 17 | } 18 | 19 | interface IHunk { 20 | oldStart: number; 21 | oldLines: number; 22 | newStart: number; 23 | newLines: number; 24 | lines: string[]; 25 | } 26 | 27 | interface IUniDiff { 28 | oldFileName: string; 29 | newFileName: string; 30 | oldHeader: string; 31 | newHeader: string; 32 | hunks: IHunk[]; 33 | } 34 | 35 | class Diff { 36 | ignoreWhitespace: boolean; 37 | 38 | constructor(ignoreWhitespace?: boolean); 39 | 40 | diff(oldString: string, newString: string): IDiffResult[]; 41 | 42 | pushComponent(components: IDiffResult[], value: string, added: boolean, removed: boolean): void; 43 | 44 | extractCommon( 45 | basePath: IBestPath, 46 | newString: string, 47 | oldString: string, 48 | diagonalPath: number 49 | ): number; 50 | 51 | equals(left: string, right: string): boolean; 52 | 53 | join(left: string, right: string): string; 54 | 55 | tokenize(value: string): any; // return types are string or string[] 56 | } 57 | 58 | function diffChars(oldStr: string, newStr: string): IDiffResult[]; 59 | 60 | function diffWords(oldStr: string, newStr: string): IDiffResult[]; 61 | 62 | function diffWordsWithSpace(oldStr: string, newStr: string): IDiffResult[]; 63 | 64 | function diffJson(oldObj: Object, newObj: Object): IDiffResult[]; 65 | 66 | function diffLines(oldStr: string, newStr: string): IDiffResult[]; 67 | 68 | function diffCss(oldStr: string, newStr: string): IDiffResult[]; 69 | 70 | function createPatch( 71 | fileName: string, 72 | oldStr: string, 73 | newStr: string, 74 | oldHeader: string, 75 | newHeader: string, 76 | options?: { context: number } 77 | ): string; 78 | 79 | function createTwoFilesPatch( 80 | oldFileName: string, 81 | newFileName: string, 82 | oldStr: string, 83 | newStr: string, 84 | oldHeader: string, 85 | newHeader: string, 86 | options?: { context: number } 87 | ): string; 88 | 89 | function structuredPatch( 90 | oldFileName: string, 91 | newFileName: string, 92 | oldStr: string, 93 | newStr: string, 94 | oldHeader: string, 95 | newHeader: string, 96 | options?: { context: number } 97 | ): IUniDiff; 98 | 99 | function applyPatch(oldStr: string, uniDiff: string | IUniDiff | IUniDiff[]): string; 100 | 101 | function applyPatches( 102 | uniDiff: IUniDiff[], 103 | options: { 104 | loadFile: (index: number, callback: (err: Error, data: string) => void) => void; 105 | patched: (index: number, content: string) => void; 106 | complete: (err?: Error) => void; 107 | } 108 | ): void; 109 | 110 | function parsePatch(diffStr: string, options?: { strict: boolean }): IUniDiff[]; 111 | 112 | function convertChangesToXML(changes: IDiffResult[]): string; 113 | 114 | function convertChangesToDMP(changes: IDiffResult[]): { 0: number; 1: string }[]; 115 | } 116 | 117 | declare module 'diff' { 118 | export = JsDiff; 119 | } 120 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | //@ts-check 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | 12 | /**@type {import('webpack').Configuration}*/ 13 | const config = { 14 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 15 | 16 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 17 | output: { 18 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 19 | path: path.resolve(__dirname, 'dist'), 20 | filename: 'extension.js', 21 | libraryTarget: 'commonjs2', 22 | devtoolModuleFilenameTemplate: '../[resource-path]', 23 | }, 24 | devtool: 'source-map', 25 | externals: { 26 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 27 | }, 28 | resolve: { 29 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 30 | extensions: ['.ts', '.js'], 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.ts$/, 36 | exclude: /node_modules/, 37 | use: [ 38 | { 39 | loader: 'ts-loader', 40 | }, 41 | ], 42 | }, 43 | ], 44 | }, 45 | }; 46 | 47 | module.exports = config; 48 | --------------------------------------------------------------------------------