├── .gitignore ├── CONTRIBUTING.md ├── settings └── language-shellscript.cson ├── package.json ├── README.md ├── .github ├── workflows │ └── main.yml └── no-response.yml ├── coffeelint.json ├── grammars ├── shell-session.cson ├── tree-sitter-bash.cson └── shell-unix-bash.cson ├── PULL_REQUEST_TEMPLATE.md ├── snippets └── language-shellscript.cson ├── LICENSE.md ├── ISSUE_TEMPLATE.md └── spec ├── shell-session-spec.coffee └── shell-unix-bash-spec.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See the [Atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md) 2 | -------------------------------------------------------------------------------- /settings/language-shellscript.cson: -------------------------------------------------------------------------------- 1 | '.source.shell': 2 | 'editor': 3 | 'commentStart': '# ' 4 | 'foldEndPattern': '^\\s*(\\}|(done|fi|esac)\\b)' 5 | 'increaseIndentPattern': '^\\s*(else|case)\\b|^.*(\\{|\\b(then|do)\\b)$' 6 | 'decreaseIndentPattern': '^\\s*(\\}|(elif|else|fi|esac|done)\\b)' 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "language-shellscript", 3 | "version": "0.28.2", 4 | "description": "ShellScript language support in Atom", 5 | "keywords": [ 6 | "tree-sitter" 7 | ], 8 | "engines": { 9 | "atom": "*", 10 | "node": "*" 11 | }, 12 | "homepage": "http://atom.github.io/language-shellscript", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/atom/language-shellscript.git" 16 | }, 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/atom/language-shellscript/issues" 20 | }, 21 | "dependencies": { 22 | "tree-sitter-bash": "^0.16.1" 23 | }, 24 | "devDependencies": { 25 | "coffeelint": "^1.10.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | # ShellScript language support in Atom 3 | ![CI Status](https://github.com/atom/language-shellscript/actions/workflows/main.yml/badge.svg) 4 | 5 | Adds syntax highlighting and snippets to shell scripts in Atom. 6 | 7 | Originally [converted](http://flight-manual.atom.io/hacking-atom/sections/converting-from-textmate) from the [ShellScript TextMate bundle](https://github.com/textmate/shellscript.tmbundle). 8 | 9 | Contributions are greatly appreciated. Please fork this repository and open a pull request to add snippets, make grammar tweaks, etc. 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | env: 6 | CI: true 7 | 8 | jobs: 9 | Test: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | channel: [stable, beta] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: UziTech/action-setup-atom@v2 18 | with: 19 | version: ${{ matrix.channel }} 20 | - name: Install windows-build-tools 21 | if: ${{ matrix.os == 'windows-latest' }} 22 | run: | 23 | npm i windows-build-tools@4.0.0 24 | npm config set msvs_version 2019 25 | - name: Install dependencies 26 | run: apm install 27 | - name: Run tests 28 | run: atom --test spec 29 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an issue is closed for lack of response 4 | daysUntilClose: 28 5 | 6 | # Label requiring a response 7 | responseRequiredLabel: more-information-needed 8 | 9 | # Comment to post when closing an issue for lack of response. Set to `false` to disable. 10 | closeComment: > 11 | This issue has been automatically closed because there has been no response 12 | to our request for more information from the original author. With only the 13 | information that is currently in the issue, we don't have enough information 14 | to take action. Please reach out if you have or find the answers we need so 15 | that we can investigate further. 16 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_line_length": { 3 | "level": "ignore" 4 | }, 5 | "no_empty_param_list": { 6 | "level": "error" 7 | }, 8 | "arrow_spacing": { 9 | "level": "error" 10 | }, 11 | "no_interpolation_in_single_quotes": { 12 | "level": "error" 13 | }, 14 | "no_debugger": { 15 | "level": "error" 16 | }, 17 | "prefer_english_operator": { 18 | "level": "error" 19 | }, 20 | "colon_assignment_spacing": { 21 | "spacing": { 22 | "left": 0, 23 | "right": 1 24 | }, 25 | "level": "error" 26 | }, 27 | "braces_spacing": { 28 | "spaces": 0, 29 | "level": "error" 30 | }, 31 | "spacing_after_comma": { 32 | "level": "error" 33 | }, 34 | "no_stand_alone_at": { 35 | "level": "error" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /grammars/shell-session.cson: -------------------------------------------------------------------------------- 1 | 'scopeName': 'text.shell-session' 2 | 'name': 'Shell Session' 3 | 'fileTypes': [ 4 | 'sh-session' 5 | ] 6 | 'patterns': [ 7 | { 8 | 'match': '(?x) 9 | ^ 10 | (?: 11 | ( 12 | (?:\\(\\S+\\)\\s*)? 13 | (?: 14 | sh\\S*? | 15 | \\w+\\S+[@:]\\S+(?:\\s+\\S+)? | 16 | \\[\\S+?[@:][^\\n]+?\\].*? 17 | ) 18 | ) 19 | \\s* 20 | )? 21 | ( 22 | [>$#%❯➜] | 23 | \\p{Greek} 24 | ) 25 | \\s+ 26 | (.*) 27 | $ 28 | ' 29 | 'captures': 30 | '1': 31 | 'name': 'entity.other.prompt-prefix.shell-session' 32 | '2': 33 | 'name': 'punctuation.separator.prompt.shell-session' 34 | '3': 35 | 'name': 'source.shell' 36 | 'patterns': [ 37 | 'include': 'source.shell' 38 | ] 39 | } 40 | { 41 | 'match': '^.+$' 42 | 'name': 'meta.output.shell-session' 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Requirements 2 | 3 | * Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. 4 | * All new code requires tests to ensure against regressions 5 | 6 | ### Description of the Change 7 | 8 | 13 | 14 | ### Alternate Designs 15 | 16 | 17 | 18 | ### Benefits 19 | 20 | 21 | 22 | ### Possible Drawbacks 23 | 24 | 25 | 26 | ### Applicable Issues 27 | 28 | 29 | -------------------------------------------------------------------------------- /snippets/language-shellscript.cson: -------------------------------------------------------------------------------- 1 | '.source.shell': 2 | '#!/usr/bin/env': 3 | 'prefix': 'env' 4 | 'body': '#!/usr/bin/env $1\n' 5 | '#!/usr/bin/env bash': 6 | 'prefix': 'bash' 7 | 'body': '#!/usr/bin/env bash\n' 8 | '#!/bin/sh': 9 | 'prefix': 'sh' 10 | 'body': '#!/bin/sh\n' 11 | '#!/usr/bin/env zsh': 12 | 'prefix': 'zsh' 13 | 'body': '#!/usr/bin/env zsh\n' 14 | 'alias …': 15 | 'prefix': 'alias' 16 | 'body': 'alias ${1:name}="${2:#statement}"' 17 | 'case … esac': 18 | 'prefix': 'case' 19 | 'body': 'case ${1:word} in\n\t${2:pattern} )\n\t\t$0;;\nesac' 20 | 'elif …': 21 | 'prefix': 'elif' 22 | 'body': 'elif ${2:[[ ${1:condition} ]]}; then\n\t${0:#statements}' 23 | 'fi': 24 | 'prefix': 'fi' 25 | 'body': 'fi' 26 | 'for … done': 27 | 'prefix': 'for' 28 | 'body': 'for (( i = 0; i < ${1:10}; i++ )); do\n\t${0:#statements}\ndone' 29 | 'function …': 30 | 'prefix': 'function' 31 | 'body': 'function ${1:name}(${2:parameter}) {\n\t${3:#statements}\n}' 32 | 'if … fi': 33 | 'prefix': 'if' 34 | 'body': 'if ${2:[[ ${1:condition} ]]}; then\n\t${0:#statements}\nfi' 35 | 'until … done': 36 | 'prefix': 'until' 37 | 'body': 'until ${2:[[ ${1:condition} ]]}; do\n\t${0:#statements}\ndone' 38 | 'while … done': 39 | 'prefix': 'while' 40 | 'body': 'while ${2:[[ ${1:condition} ]]}; do\n\t${0:#statements}\ndone' 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------- 23 | 24 | This package was derived from a TextMate bundle located at 25 | https://github.com/textmate/shellscript.tmbundle and distributed under the 26 | following license, located in `README.mdown`: 27 | 28 | Permission to copy, use, modify, sell and distribute this 29 | software is granted. This software is provided "as is" without 30 | express or implied warranty, and with no claim as to its 31 | suitability for any purpose. 32 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Prerequisites 10 | 11 | * [ ] Put an X between the brackets on this line if you have done all of the following: 12 | * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode 13 | * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/ 14 | * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq 15 | * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom 16 | * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages 17 | 18 | ### Description 19 | 20 | [Description of the issue] 21 | 22 | ### Steps to Reproduce 23 | 24 | 1. [First Step] 25 | 2. [Second Step] 26 | 3. [and so on...] 27 | 28 | **Expected behavior:** [What you expect to happen] 29 | 30 | **Actual behavior:** [What actually happens] 31 | 32 | **Reproduces how often:** [What percentage of the time does it reproduce?] 33 | 34 | ### Versions 35 | 36 | You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running. 37 | 38 | ### Additional Information 39 | 40 | Any additional information, configuration or data that might be necessary to reproduce the issue. 41 | -------------------------------------------------------------------------------- /grammars/tree-sitter-bash.cson: -------------------------------------------------------------------------------- 1 | name: 'Shell Script' 2 | scopeName: 'source.shell' 3 | type: 'tree-sitter' 4 | parser: 'tree-sitter-bash' 5 | 6 | fileTypes: [ 7 | 'sh' 8 | 'bash' 9 | ] 10 | 11 | injectionRegex: '^(bash|sh|BASH|SH)$' 12 | 13 | firstLineRegex: [ 14 | # shebang line 15 | '^#!.*\\b(bash|sh)\\r?\\n' 16 | 17 | # vim modeline 18 | 'vim\\b.*\\bset\\b.*\\b(filetype|ft|syntax)=(sh|bash)' 19 | ] 20 | 21 | folds: [ 22 | { 23 | type: 'heredoc_body' 24 | } 25 | { 26 | type: 'if_statement', 27 | start: {type: '"then"'} 28 | end: {type: ['elif_clause', 'else_clause']} 29 | } 30 | { 31 | type: 'elif_clause', 32 | start: {type: '"then"'} 33 | } 34 | { 35 | type: 'if_statement' 36 | start: {type: '"then"'} 37 | end: {index: -1} 38 | } 39 | { 40 | type: 'case_statement' 41 | start: {type: '"in"'} 42 | end: {index: -1} 43 | } 44 | { 45 | type: 'elif_clause' 46 | start: {type: 'then'} 47 | } 48 | { 49 | type: 'else_clause' 50 | start: {index: 0} 51 | } 52 | { 53 | type: 'case_item' 54 | start: {type: ')'} 55 | } 56 | { 57 | type: [ 58 | 'array' 59 | 'do_group' 60 | 'subshell' 61 | 'expansion' 62 | 'test_command' 63 | 'compound_statement' 64 | 'process_substitution' 65 | 'command_substitution' 66 | ] 67 | start: {index: 0} 68 | end: {index: -1} 69 | } 70 | ] 71 | 72 | comments: 73 | start: '# ' 74 | 75 | scopes: 76 | 'program': 'source.shell' 77 | 78 | 'comment': 'comment.block' 79 | 80 | 'string': 'string' 81 | 'raw_string': 'string' 82 | 'ansii_c_string': 'string' 83 | 'heredoc_body': 'string' 84 | 'heredoc_start': 'string' 85 | 'regex': 'string.regexp' 86 | 87 | ' 88 | "$", 89 | expansion > "${", 90 | expansion > "}" 91 | ': 'punctuation.section.embedded' 92 | 93 | 'string > command_substitution': 'embedded.source' 94 | 95 | 'function_definition > word': 'entity.name.function' 96 | 'command_name': 'entity.name.function' 97 | 98 | 'file_descriptor': 'constant.numeric' 99 | 100 | 'command_name > word': [ 101 | {match: '^(cd|echo|eval|exit|false|getopts|pushd|popd|return|set|shift|true)$', scopes: 'support.function'} 102 | ] 103 | 104 | 'test_operator': 'entity.other.attribute-name' 105 | 'word': [{match: '^-', scopes: 'entity.other.attribute-name'}] 106 | 107 | 'special_variable_name': 'variable.other.member' 108 | 'variable_name': 'variable.other.member' 109 | 110 | '"if"': 'keyword.control' 111 | '"fi"': 'keyword.control' 112 | '"then"': 'keyword.control' 113 | '"else"': 'keyword.control' 114 | '"elif"': 'keyword.control' 115 | '"for"': 'keyword.control' 116 | '"do"': 'keyword.control' 117 | '"done"': 'keyword.control' 118 | '"case"': 'keyword.control' 119 | '"esac"': 'keyword.control' 120 | '"in"': 'keyword.control' 121 | '"while"': 'keyword.control' 122 | '"until"': 'keyword.control' 123 | '"function"': 'keyword.control' 124 | '"local"': 'keyword.control' 125 | '"declare"': 'keyword.control' 126 | '"export"': 'keyword.control' 127 | '"readonly"': 'keyword.control' 128 | '"typeset"': 'keyword.control' 129 | '"unset"': 'keyword.control' 130 | '"unsetenv"': 'keyword.control' 131 | 132 | '"&"': 'keyword.operator' 133 | '"&&"': 'keyword.operator' 134 | '"|"': 'keyword.operator' 135 | '"||"': 'keyword.operator' 136 | '"<"': 'keyword.operator' 137 | '">"': 'keyword.operator' 138 | '">>"': 'keyword.operator' 139 | '"&>"': 'keyword.operator' 140 | '"&>>"': 'keyword.operator' 141 | '"<&"': 'keyword.operator' 142 | '">&"': 'keyword.operator' 143 | '"<<-"': 'keyword.operator' 144 | '"<<<"': 'keyword.operator' 145 | -------------------------------------------------------------------------------- /spec/shell-session-spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Shell session grammar", -> 2 | grammar = null 3 | 4 | beforeEach -> 5 | atom.config.set('core.useTreeSitterParsers', false) 6 | 7 | waitsForPromise -> 8 | atom.packages.activatePackage("language-shellscript") 9 | 10 | runs -> 11 | grammar = atom.grammars.grammarForScopeName("text.shell-session") 12 | 13 | it "parses the grammar", -> 14 | expect(grammar).toBeDefined() 15 | expect(grammar.scopeName).toBe "text.shell-session" 16 | 17 | prompts = [">", "$", "#", "%", "❯", "➜"] 18 | it "tokenizes prompts", -> 19 | for delim in prompts 20 | {tokens} = grammar.tokenizeLine(delim + ' echo $FOO') 21 | 22 | expect(tokens[0]).toEqual value: delim, scopes: ['text.shell-session', 'punctuation.separator.prompt.shell-session'] 23 | expect(tokens[1]).toEqual value: ' ', scopes: ['text.shell-session'] 24 | expect(tokens[2]).toEqual value: 'echo', scopes: ['text.shell-session', 'source.shell', 'support.function.builtin.shell'] 25 | 26 | it "tokenises prompts with Greek characters", -> 27 | sigils = ["λ", "Λ", "Δ", "Σ", "Ω"] 28 | for sigil in sigils 29 | lines = grammar.tokenizeLines """ 30 | #{sigil} echo #{sigil}μμ 31 | O#{sigil}tput Ω 32 | """ 33 | expect(lines[0][0]).toEqual value: sigil, scopes: ['text.shell-session', 'punctuation.separator.prompt.shell-session'] 34 | expect(lines[0][2]).toEqual value: 'echo', scopes: ['text.shell-session', 'source.shell', 'support.function.builtin.shell'] 35 | expect(lines[0][3]).toEqual value: " #{sigil}μμ", scopes: ['text.shell-session', 'source.shell'] 36 | expect(lines[1][0]).toEqual value: "O#{sigil}tput Ω", scopes: ['text.shell-session', 'meta.output.shell-session'] 37 | 38 | it "does not tokenize prompts with indents", -> 39 | for delim in prompts 40 | {tokens} = grammar.tokenizeLine(' ' + delim + ' echo $FOO') 41 | 42 | expect(tokens[0]).toEqual value: ' ' + delim + ' echo $FOO', scopes: ['text.shell-session', 'meta.output.shell-session'] 43 | 44 | it "tokenizes prompts with prefixes", -> 45 | {tokens} = grammar.tokenizeLine('user@machine $ echo $FOO') 46 | 47 | expect(tokens[0]).toEqual value: 'user@machine', scopes: ['text.shell-session', 'entity.other.prompt-prefix.shell-session'] 48 | expect(tokens[1]).toEqual value: ' ', scopes: ['text.shell-session'] 49 | expect(tokens[2]).toEqual value: '$', scopes: ['text.shell-session', 'punctuation.separator.prompt.shell-session'] 50 | expect(tokens[3]).toEqual value: ' ', scopes: ['text.shell-session'] 51 | expect(tokens[4]).toEqual value: 'echo', scopes: ['text.shell-session', 'source.shell', 'support.function.builtin.shell'] 52 | 53 | it "tokenizes prompts with prefixes and a leading parenthetical", -> 54 | {tokens} = grammar.tokenizeLine('(venv) machine:pwd user$ echo $FOO') 55 | 56 | expect(tokens[0]).toEqual value: '(venv) machine:pwd user', scopes: ['text.shell-session', 'entity.other.prompt-prefix.shell-session'] 57 | expect(tokens[1]).toEqual value: '$', scopes: ['text.shell-session', 'punctuation.separator.prompt.shell-session'] 58 | expect(tokens[2]).toEqual value: ' ', scopes: ['text.shell-session'] 59 | expect(tokens[3]).toEqual value: 'echo', scopes: ['text.shell-session', 'source.shell', 'support.function.builtin.shell'] 60 | 61 | it "tokenizes prompts with prefixes with brackets", -> 62 | {tokens} = grammar.tokenizeLine('[user@machine pwd]$ echo $FOO') 63 | 64 | expect(tokens[0]).toEqual value: '[user@machine pwd]', scopes: ['text.shell-session', 'entity.other.prompt-prefix.shell-session'] 65 | expect(tokens[1]).toEqual value: '$', scopes: ['text.shell-session', 'punctuation.separator.prompt.shell-session'] 66 | expect(tokens[2]).toEqual value: ' ', scopes: ['text.shell-session'] 67 | expect(tokens[3]).toEqual value: 'echo', scopes: ['text.shell-session', 'source.shell', 'support.function.builtin.shell'] 68 | 69 | it "tokenizes shell output", -> 70 | tokens = grammar.tokenizeLines """ 71 | $ echo $FOO 72 | foo 73 | """ 74 | 75 | expect(tokens[1][0]).toEqual value: 'foo', scopes: ['text.shell-session', 'meta.output.shell-session'] 76 | -------------------------------------------------------------------------------- /spec/shell-unix-bash-spec.coffee: -------------------------------------------------------------------------------- 1 | TextEditor = null 2 | buildTextEditor = (params) -> 3 | if atom.workspace.buildTextEditor? 4 | atom.workspace.buildTextEditor(params) 5 | else 6 | TextEditor ?= require('atom').TextEditor 7 | new TextEditor(params) 8 | 9 | describe "Shell script grammar", -> 10 | grammar = null 11 | 12 | beforeEach -> 13 | atom.config.set('core.useTreeSitterParsers', false) 14 | 15 | waitsForPromise -> 16 | atom.packages.activatePackage("language-shellscript") 17 | 18 | runs -> 19 | grammar = atom.grammars.grammarForScopeName("source.shell") 20 | 21 | it "parses the grammar", -> 22 | expect(grammar).toBeDefined() 23 | expect(grammar.scopeName).toBe "source.shell" 24 | 25 | it "tokenizes strings inside variable constructs", -> 26 | {tokens} = grammar.tokenizeLine("${'root'}") 27 | expect(tokens[0]).toEqual value: '${', scopes: ['source.shell', 'variable.other.bracket.shell', 'punctuation.definition.variable.shell'] 28 | expect(tokens[1]).toEqual value: "'", scopes: ['source.shell', 'variable.other.bracket.shell', 'string.quoted.single.shell', 'punctuation.definition.string.begin.shell'] 29 | expect(tokens[2]).toEqual value: "root", scopes: ['source.shell', 'variable.other.bracket.shell', 'string.quoted.single.shell'] 30 | expect(tokens[3]).toEqual value: "'", scopes: ['source.shell', 'variable.other.bracket.shell', 'string.quoted.single.shell', 'punctuation.definition.string.end.shell'] 31 | expect(tokens[4]).toEqual value: '}', scopes: ['source.shell', 'variable.other.bracket.shell', 'punctuation.definition.variable.shell'] 32 | 33 | it "tokenizes if correctly when it's a parameter", -> 34 | {tokens} = grammar.tokenizeLine('dd if=/dev/random of=/dev/null') 35 | expect(tokens[0]).toEqual value: 'dd if=/dev/random of=/dev/null', scopes: ['source.shell'] 36 | 37 | it "tokenizes if as a keyword", -> 38 | brackets = 39 | "[": "]" 40 | "[[": "]]" 41 | 42 | for openingBracket, closingBracket of brackets 43 | {tokens} = grammar.tokenizeLine('if ' + openingBracket + ' -f /var/log/messages ' + closingBracket) 44 | expect(tokens[0]).toEqual value: 'if', scopes: ['source.shell', 'meta.scope.if-block.shell', 'keyword.control.shell'] 45 | expect(tokens[2]).toEqual value: openingBracket, scopes: ['source.shell', 'meta.scope.if-block.shell', 'meta.scope.logical-expression.shell', 'punctuation.definition.logical-expression.shell'] 46 | expect(tokens[4]).toEqual value: '-f', scopes: ['source.shell', 'meta.scope.if-block.shell', 'meta.scope.logical-expression.shell', 'keyword.operator.logical.shell'] 47 | expect(tokens[5]).toEqual value: ' /var/log/messages ', scopes: ['source.shell', 'meta.scope.if-block.shell', 'meta.scope.logical-expression.shell'] 48 | expect(tokens[6]).toEqual value: closingBracket, scopes: ['source.shell', 'meta.scope.if-block.shell', 'meta.scope.logical-expression.shell', 'punctuation.definition.logical-expression.shell'] 49 | 50 | it "tokenizes for...in loops", -> 51 | {tokens} = grammar.tokenizeLine('for variable in file do do-something-done done') 52 | expect(tokens[0]).toEqual value: 'for', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'keyword.control.shell'] 53 | expect(tokens[2]).toEqual value: 'variable', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'variable.other.loop.shell'] 54 | expect(tokens[4]).toEqual value: 'in', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'keyword.control.shell'] 55 | expect(tokens[5]).toEqual value: ' file ', scopes: ['source.shell', 'meta.scope.for-in-loop.shell'] 56 | expect(tokens[6]).toEqual value: 'do', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'keyword.control.shell'] 57 | expect(tokens[7]).toEqual value: ' do-something-done ', scopes: ['source.shell', 'meta.scope.for-in-loop.shell'] 58 | expect(tokens[8]).toEqual value: 'done', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'keyword.control.shell'] 59 | 60 | {tokens} = grammar.tokenizeLine('for "variable" in "${list[@]}" do something done') 61 | expect(tokens[0]).toEqual value: 'for', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'keyword.control.shell'] 62 | expect(tokens[2]).toEqual value: '"', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'variable.other.loop.shell', 'string.quoted.double.shell', 'punctuation.definition.string.begin.shell'] 63 | expect(tokens[3]).toEqual value: 'variable', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'variable.other.loop.shell', 'string.quoted.double.shell'] 64 | expect(tokens[4]).toEqual value: '"', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'variable.other.loop.shell', 'string.quoted.double.shell', 'punctuation.definition.string.end.shell'] 65 | expect(tokens[6]).toEqual value: 'in', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'keyword.control.shell'] 66 | expect(tokens[8]).toEqual value: '"', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'string.quoted.double.shell', 'punctuation.definition.string.begin.shell'] 67 | expect(tokens[9]).toEqual value: '${', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'string.quoted.double.shell', 'variable.other.bracket.shell', 'punctuation.definition.variable.shell'] 68 | expect(tokens[10]).toEqual value: 'list', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'string.quoted.double.shell', 'variable.other.bracket.shell'] 69 | expect(tokens[11]).toEqual value: '[', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'string.quoted.double.shell', 'variable.other.bracket.shell', 'punctuation.section.array.shell'] 70 | expect(tokens[12]).toEqual value: '@', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'string.quoted.double.shell', 'variable.other.bracket.shell'] 71 | expect(tokens[13]).toEqual value: ']', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'string.quoted.double.shell', 'variable.other.bracket.shell', 'punctuation.section.array.shell'] 72 | expect(tokens[14]).toEqual value: '}', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'string.quoted.double.shell', 'variable.other.bracket.shell', 'punctuation.definition.variable.shell'] 73 | expect(tokens[15]).toEqual value: '"', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'string.quoted.double.shell', 'punctuation.definition.string.end.shell'] 74 | expect(tokens[17]).toEqual value: 'do', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'keyword.control.shell'] 75 | expect(tokens[18]).toEqual value: ' something ', scopes: ['source.shell', 'meta.scope.for-in-loop.shell'] 76 | expect(tokens[19]).toEqual value: 'done', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'keyword.control.shell'] 77 | 78 | {tokens} = grammar.tokenizeLine('for variable in something do # in') 79 | expect(tokens[4]).toEqual value: 'in', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'keyword.control.shell'] 80 | expect(tokens[8]).toEqual value: '#', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'comment.line.number-sign.shell', 'punctuation.definition.comment.shell'] 81 | expect(tokens[9]).toEqual value: ' in', scopes: ['source.shell', 'meta.scope.for-in-loop.shell', 'comment.line.number-sign.shell'] 82 | 83 | it "doesn't tokenize keywords when they're part of a phrase", -> 84 | {tokens} = grammar.tokenizeLine('grep --ignore-case "something"') 85 | expect(tokens[0]).toEqual value: 'grep --ignore-case ', scopes: ['source.shell'] 86 | expect(tokens[1]).toEqual value: '"', scopes: ['source.shell', 'string.quoted.double.shell', 'punctuation.definition.string.begin.shell'] 87 | 88 | strings = [ 89 | 'iffy' 90 | 'enable-something' 91 | 'there.for' 92 | 'be+done' 93 | 'little,while' 94 | 'rest@until' 95 | 'lets:select words' 96 | 'in🚀case of stuff' 97 | 'the#fi%nal countdown' 98 | 'time⏰out' 99 | ] 100 | 101 | for string in strings 102 | {tokens} = grammar.tokenizeLine(string) 103 | expect(tokens[0]).toEqual value: string, scopes: ['source.shell'] 104 | 105 | {tokens} = grammar.tokenizeLine('this/function ()') 106 | expect(tokens[0]).toEqual value: 'this/function', scopes: ['source.shell', 'meta.function.shell', 'entity.name.function.shell'] 107 | expect(tokens[2]).toEqual value: '()', scopes: ['source.shell', 'meta.function.shell', 'punctuation.definition.arguments.shell'] 108 | 109 | {tokens} = grammar.tokenizeLine('and,for (( this ))') 110 | expect(tokens[0]).toEqual value: 'and,for ', scopes: ['source.shell'] 111 | expect(tokens[1]).toEqual value: '((', scopes: ['source.shell', 'string.other.math.shell', 'punctuation.definition.string.begin.shell'] 112 | 113 | it "tokenizes herestrings", -> 114 | delimsByScope = 115 | "string.quoted.double.shell": '"' 116 | "string.quoted.single.shell": "'" 117 | 118 | for scope, delim of delimsByScope 119 | tokens = grammar.tokenizeLines """ 120 | $cmd <<<#{delim} 121 | lorem ipsum#{delim} 122 | """ 123 | expect(tokens[0][0]).toEqual value: '$', scopes: ['source.shell', 'variable.other.normal.shell', 'punctuation.definition.variable.shell'] 124 | expect(tokens[0][1]).toEqual value: 'cmd', scopes: ['source.shell', 'variable.other.normal.shell'] 125 | expect(tokens[0][3]).toEqual value: '<<<', scopes: ['source.shell', 'meta.herestring.shell', 'keyword.operator.herestring.shell'] 126 | expect(tokens[0][4]).toEqual value: delim, scopes: ['source.shell', 'meta.herestring.shell', scope, 'punctuation.definition.string.begin.shell'] 127 | expect(tokens[1][0]).toEqual value: 'lorem ipsum', scopes: ['source.shell', 'meta.herestring.shell', scope] 128 | expect(tokens[1][1]).toEqual value: delim, scopes: ['source.shell', 'meta.herestring.shell', scope, 'punctuation.definition.string.end.shell'] 129 | 130 | for scope, delim of delimsByScope 131 | tokens = grammar.tokenizeLines """ 132 | $cmd <<< #{delim} 133 | lorem ipsum#{delim} 134 | """ 135 | expect(tokens[0][0]).toEqual value: '$', scopes: ['source.shell', 'variable.other.normal.shell', 'punctuation.definition.variable.shell'] 136 | expect(tokens[0][1]).toEqual value: 'cmd', scopes: ['source.shell', 'variable.other.normal.shell'] 137 | expect(tokens[0][3]).toEqual value: '<<<', scopes: ['source.shell', 'meta.herestring.shell', 'keyword.operator.herestring.shell'] 138 | expect(tokens[0][4]).toEqual value: ' ', scopes: ['source.shell', 'meta.herestring.shell'] 139 | expect(tokens[0][5]).toEqual value: delim, scopes: ['source.shell', 'meta.herestring.shell', scope, 'punctuation.definition.string.begin.shell'] 140 | expect(tokens[1][0]).toEqual value: 'lorem ipsum', scopes: ['source.shell', 'meta.herestring.shell', scope] 141 | expect(tokens[1][1]).toEqual value: delim, scopes: ['source.shell', 'meta.herestring.shell', scope, 'punctuation.definition.string.end.shell'] 142 | 143 | {tokens} = grammar.tokenizeLine '$cmd = something <<< $COUNTRIES' 144 | expect(tokens[3]).toEqual value: '<<<', scopes: ['source.shell', 'meta.herestring.shell', 'keyword.operator.herestring.shell'] 145 | expect(tokens[4]).toEqual value: ' ', scopes: ['source.shell', 'meta.herestring.shell'] 146 | expect(tokens[5]).toEqual value: '$', scopes: ['source.shell', 'meta.herestring.shell', 'string.unquoted.herestring.shell', 'variable.other.normal.shell', 'punctuation.definition.variable.shell'] 147 | expect(tokens[6]).toEqual value: 'COUNTRIES', scopes: ['source.shell', 'meta.herestring.shell', 'string.unquoted.herestring.shell', 'variable.other.normal.shell'] 148 | 149 | {tokens} = grammar.tokenizeLine '$cmd = something <<< TEST 1 2' 150 | expect(tokens[3]).toEqual value: '<<<', scopes: ['source.shell', 'meta.herestring.shell', 'keyword.operator.herestring.shell'] 151 | expect(tokens[4]).toEqual value: ' ', scopes: ['source.shell', 'meta.herestring.shell'] 152 | expect(tokens[5]).toEqual value: 'TEST', scopes: ['source.shell', 'meta.herestring.shell', 'string.unquoted.herestring.shell'] 153 | expect(tokens[6]).toEqual value: ' 1 2', scopes: ['source.shell'] 154 | 155 | {tokens} = grammar.tokenizeLine '$cmd = "$(3 / x <<< $WORD)"' 156 | expect(tokens[6]).toEqual value: '<<<', scopes: ['source.shell', 'string.quoted.double.shell', 'string.interpolated.dollar.shell', 'meta.herestring.shell', 'keyword.operator.herestring.shell'] 157 | expect(tokens[8]).toEqual value: '$', scopes: ['source.shell', 'string.quoted.double.shell', 'string.interpolated.dollar.shell', 'meta.herestring.shell', 'string.unquoted.herestring.shell', 'variable.other.normal.shell', 'punctuation.definition.variable.shell'] 158 | expect(tokens[9]).toEqual value: 'WORD', scopes: ['source.shell', 'string.quoted.double.shell', 'string.interpolated.dollar.shell', 'meta.herestring.shell', 'string.unquoted.herestring.shell', 'variable.other.normal.shell'] 159 | expect(tokens[10]).toEqual value: ')', scopes: ['source.shell', 'string.quoted.double.shell', 'string.interpolated.dollar.shell', 'punctuation.definition.string.end.shell'] 160 | 161 | it "tokenizes heredocs", -> 162 | delimsByScope = 163 | "ruby": "RUBY" 164 | "python": "PYTHON" 165 | "applescript": "APPLESCRIPT" 166 | "shell": "SHELL" 167 | 168 | for scope, delim of delimsByScope 169 | tokens = grammar.tokenizeLines """ 170 | <<#{delim} 171 | stuff 172 | #{delim} 173 | """ 174 | expect(tokens[0][0]).toEqual value: '<<', scopes: ['source.shell', 'string.unquoted.heredoc.' + scope + '.shell', 'keyword.operator.heredoc.shell'] 175 | expect(tokens[0][1]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.' + scope + '.shell', 'keyword.control.heredoc-token.shell'] 176 | expect(tokens[1][0]).toEqual value: 'stuff', scopes: ['source.shell', 'string.unquoted.heredoc.' + scope + '.shell', 'source.' + scope + '.embedded.shell'] 177 | expect(tokens[2][0]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.' + scope + '.shell', 'keyword.control.heredoc-token.shell'] 178 | 179 | tokens = grammar.tokenizeLines """ 180 | << #{delim} 181 | stuff 182 | #{delim} 183 | """ 184 | expect(tokens[0][0]).toEqual value: '<<', scopes: ['source.shell', 'string.unquoted.heredoc.' + scope + '.shell', 'keyword.operator.heredoc.shell'] 185 | expect(tokens[0][1]).toEqual value: ' ', scopes: ['source.shell', 'string.unquoted.heredoc.' + scope + '.shell'] 186 | expect(tokens[0][2]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.' + scope + '.shell', 'keyword.control.heredoc-token.shell'] 187 | expect(tokens[1][0]).toEqual value: 'stuff', scopes: ['source.shell', 'string.unquoted.heredoc.' + scope + '.shell', 'source.' + scope + '.embedded.shell'] 188 | expect(tokens[2][0]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.' + scope + '.shell', 'keyword.control.heredoc-token.shell'] 189 | 190 | tokens = grammar.tokenizeLines """ 191 | <<-#{delim} 192 | stuff 193 | #{delim} 194 | """ 195 | expect(tokens[0][0]).toEqual value: '<<', scopes: ['source.shell', 'string.unquoted.heredoc.no-indent.' + scope + '.shell', 'keyword.operator.heredoc.shell'] 196 | expect(tokens[0][2]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.no-indent.' + scope + '.shell', 'keyword.control.heredoc-token.shell'] 197 | expect(tokens[1][0]).toEqual value: 'stuff', scopes: ['source.shell', 'string.unquoted.heredoc.no-indent.' + scope + '.shell', 'source.' + scope + '.embedded.shell'] 198 | expect(tokens[2][0]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.no-indent.' + scope + '.shell', 'keyword.control.heredoc-token.shell'] 199 | 200 | tokens = grammar.tokenizeLines """ 201 | <<- #{delim} 202 | stuff 203 | #{delim} 204 | """ 205 | expect(tokens[0][0]).toEqual value: '<<', scopes: ['source.shell', 'string.unquoted.heredoc.no-indent.' + scope + '.shell', 'keyword.operator.heredoc.shell'] 206 | expect(tokens[0][1]).toEqual value: '- ', scopes: ['source.shell', 'string.unquoted.heredoc.no-indent.' + scope + '.shell'] 207 | expect(tokens[0][2]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.no-indent.' + scope + '.shell', 'keyword.control.heredoc-token.shell'] 208 | expect(tokens[1][0]).toEqual value: 'stuff', scopes: ['source.shell', 'string.unquoted.heredoc.no-indent.' + scope + '.shell', 'source.' + scope + '.embedded.shell'] 209 | expect(tokens[2][0]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.no-indent.' + scope + '.shell', 'keyword.control.heredoc-token.shell'] 210 | 211 | delims = [ 212 | "RANDOMTHING" 213 | "RUBY@1.8" 214 | "END-INPUT" 215 | ] 216 | 217 | for delim in delims 218 | tokens = grammar.tokenizeLines """ 219 | <<#{delim} 220 | stuff 221 | #{delim} 222 | """ 223 | expect(tokens[0][0]).toEqual value: '<<', scopes: ['source.shell', 'string.unquoted.heredoc.expanded.shell', 'keyword.operator.heredoc.shell'] 224 | expect(tokens[0][1]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.expanded.shell', 'keyword.control.heredoc-token.shell'] 225 | expect(tokens[1][0]).toEqual value: 'stuff', scopes: ['source.shell', 'string.unquoted.heredoc.expanded.shell'] 226 | expect(tokens[2][0]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.expanded.shell', 'keyword.control.heredoc-token.shell'] 227 | 228 | for delim in delims 229 | tokens = grammar.tokenizeLines """ 230 | << '#{delim}' 231 | stuff 232 | #{delim} 233 | """ 234 | expect(tokens[0][0]).toEqual value: '<<', scopes: ['source.shell', 'string.unquoted.heredoc.shell', 'keyword.operator.heredoc.shell'] 235 | expect(tokens[0][2]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.shell', 'keyword.control.heredoc-token.shell'] 236 | expect(tokens[1][0]).toEqual value: 'stuff', scopes: ['source.shell', 'string.unquoted.heredoc.shell'] 237 | expect(tokens[2][0]).toEqual value: delim, scopes: ['source.shell', 'string.unquoted.heredoc.shell', 'keyword.control.heredoc-token.shell'] 238 | 239 | it "tokenizes shebangs", -> 240 | {tokens} = grammar.tokenizeLine('#!/bin/sh') 241 | expect(tokens[0]).toEqual value: '#!', scopes: ['source.shell', 'comment.line.number-sign.shebang.shell', 'punctuation.definition.comment.shebang.shell'] 242 | expect(tokens[1]).toEqual value: '/bin/sh', scopes: ['source.shell', 'comment.line.number-sign.shebang.shell'] 243 | 244 | it "tokenizes comments", -> 245 | {tokens} = grammar.tokenizeLine('#comment') 246 | expect(tokens[0]).toEqual value: '#', scopes: ['source.shell', 'comment.line.number-sign.shell', 'punctuation.definition.comment.shell'] 247 | expect(tokens[1]).toEqual value: 'comment', scopes: ['source.shell', 'comment.line.number-sign.shell'] 248 | 249 | it "tokenizes comments in interpolated strings", -> 250 | {tokens} = grammar.tokenizeLine('`#comment`') 251 | expect(tokens[1]).toEqual value: '#', scopes: ['source.shell', 'string.interpolated.backtick.shell', 'comment.line.number-sign.shell', 'punctuation.definition.comment.shell'] 252 | expect(tokens[3]).toEqual value: '`', scopes: ['source.shell', 'string.interpolated.backtick.shell', 'punctuation.definition.string.end.shell'] 253 | 254 | it "does not tokenize -# in argument lists as a comment", -> 255 | {tokens} = grammar.tokenizeLine('curl -#') 256 | expect(tokens[0]).toEqual value: 'curl -#', scopes: ['source.shell'] 257 | 258 | it "tokenizes nested variable expansions", -> 259 | {tokens} = grammar.tokenizeLine('${${C}}') 260 | expect(tokens[0]).toEqual value: '${', scopes: ['source.shell', 'variable.other.bracket.shell', 'punctuation.definition.variable.shell'] 261 | expect(tokens[1]).toEqual value: '${', scopes: ['source.shell', 'variable.other.bracket.shell', 'variable.other.bracket.shell', 'punctuation.definition.variable.shell'] 262 | expect(tokens[2]).toEqual value: 'C', scopes: ['source.shell', 'variable.other.bracket.shell', 'variable.other.bracket.shell'] 263 | expect(tokens[3]).toEqual value: '}', scopes: ['source.shell', 'variable.other.bracket.shell', 'variable.other.bracket.shell', 'punctuation.definition.variable.shell'] 264 | expect(tokens[4]).toEqual value: '}', scopes: ['source.shell', 'variable.other.bracket.shell', 'punctuation.definition.variable.shell'] 265 | 266 | it "tokenizes case blocks", -> 267 | {tokens} = grammar.tokenizeLine('case word in esac);; esac') 268 | expect(tokens[0]).toEqual value: 'case', scopes: ['source.shell', 'meta.scope.case-block.shell', 'keyword.control.shell'] 269 | expect(tokens[1]).toEqual value: ' word ', scopes: ['source.shell', 'meta.scope.case-block.shell'] 270 | expect(tokens[2]).toEqual value: 'in', scopes: ['source.shell', 'meta.scope.case-block.shell', 'meta.scope.case-body.shell', 'keyword.control.shell'] 271 | expect(tokens[3]).toEqual value: ' ', scopes: ['source.shell', 'meta.scope.case-block.shell', 'meta.scope.case-body.shell'] 272 | expect(tokens[4]).toEqual value: 'esac', scopes: ['source.shell', 'meta.scope.case-block.shell', 'meta.scope.case-body.shell', 'meta.scope.case-clause.shell', 'meta.scope.case-pattern.shell'] 273 | expect(tokens[5]).toEqual value: ')', scopes: ['source.shell', 'meta.scope.case-block.shell', 'meta.scope.case-body.shell', 'meta.scope.case-clause.shell', 'meta.scope.case-pattern.shell', 'punctuation.definition.case-pattern.shell'] 274 | expect(tokens[6]).toEqual value: ';;', scopes: ['source.shell', 'meta.scope.case-block.shell', 'meta.scope.case-body.shell', 'meta.scope.case-clause.shell', 'punctuation.terminator.case-clause.shell'] 275 | expect(tokens[7]).toEqual value: ' ', scopes: ['source.shell', 'meta.scope.case-block.shell', 'meta.scope.case-body.shell'] 276 | expect(tokens[8]).toEqual value: 'esac', scopes: ['source.shell', 'meta.scope.case-block.shell', 'keyword.control.shell'] 277 | 278 | it "does not confuse strings and functions", -> 279 | {tokens} = grammar.tokenizeLine('echo "()"') 280 | expect(tokens[0]).toEqual value: 'echo', scopes: ['source.shell', 'support.function.builtin.shell'] 281 | expect(tokens[2]).toEqual value: '"', scopes: ['source.shell', 'string.quoted.double.shell', 'punctuation.definition.string.begin.shell'] 282 | expect(tokens[3]).toEqual value: '()', scopes: ['source.shell', 'string.quoted.double.shell'] 283 | expect(tokens[4]).toEqual value: '"', scopes: ['source.shell', 'string.quoted.double.shell', 'punctuation.definition.string.end.shell'] 284 | 285 | describe "indentation", -> 286 | editor = null 287 | 288 | beforeEach -> 289 | editor = buildTextEditor() 290 | editor.setGrammar(grammar) 291 | 292 | expectPreservedIndentation = (text) -> 293 | editor.setText(text) 294 | editor.autoIndentBufferRows(0, editor.getLineCount() - 1) 295 | 296 | expectedLines = text.split("\n") 297 | actualLines = editor.getText().split("\n") 298 | for actualLine, i in actualLines 299 | expect([ 300 | actualLine, 301 | editor.indentLevelForLine(actualLine) 302 | ]).toEqual([ 303 | expectedLines[i], 304 | editor.indentLevelForLine(expectedLines[i]) 305 | ]) 306 | 307 | it "indents semicolon-style conditional", -> 308 | expectPreservedIndentation """ 309 | if [ $? -eq 0 ]; then 310 | echo "0" 311 | elif [ $? -eq 1 ]; then 312 | echo "1" 313 | else 314 | echo "other" 315 | fi 316 | """ 317 | 318 | it "indents newline-style conditional", -> 319 | expectPreservedIndentation """ 320 | if [ $? -eq 0 ] 321 | then 322 | echo "0" 323 | elif [ $? -eq 1 ] 324 | then 325 | echo "1" 326 | else 327 | echo "other" 328 | fi 329 | """ 330 | 331 | it "indents semicolon-style while loop", -> 332 | expectPreservedIndentation """ 333 | while [ $x -gt 0 ]; do 334 | x=$(($x-1)) 335 | done 336 | """ 337 | 338 | it "indents newline-style while loop", -> 339 | expectPreservedIndentation """ 340 | while [ $x -gt 0 ] 341 | do 342 | x=$(($x-1)) 343 | done 344 | """ 345 | 346 | describe "firstLineMatch", -> 347 | it "recognises interpreter directives", -> 348 | valid = """ 349 | #!/bin/sh 350 | #!/usr/sbin/env bash 351 | #!/usr/bin/bash foo=bar/ 352 | #!/usr/sbin/ksh foo bar baz 353 | #!/usr/bin/dash perl 354 | #!/usr/bin/env bin/sh 355 | #!/usr/bin/rc 356 | #!/bin/env rc 357 | #!/usr/bin/bash --script=usr/bin 358 | #! /usr/bin/env A=003 B=149 C=150 D=xzd E=base64 F=tar G=gz H=head I=tail bash 359 | #!\t/usr/bin/env --foo=bar bash --quu=quux 360 | #! /usr/bin/bash 361 | #!/usr/bin/env bash 362 | """ 363 | for line in valid.split /\n/ 364 | expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull() 365 | 366 | invalid = """ 367 | \x20#!/usr/sbin/bash 368 | \t#!/usr/sbin/bash 369 | #!/usr/bin/env-bash/node-env/ 370 | #!/usr/bin/env-bash 371 | #! /usr/binbash 372 | #! /usr/arc 373 | #!\t/usr/bin/env --bash=bar 374 | """ 375 | for line in invalid.split /\n/ 376 | expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull() 377 | 378 | it "recognises Emacs modelines", -> 379 | valid = """ 380 | #-*-shell-script-*- 381 | #-*-mode:shell-script-*- 382 | /* -*-sh-*- */ 383 | // -*- SHELL-SCRIPT -*- 384 | /* -*- mode:shell-script -*- */ 385 | // -*- font:bar;mode:sh -*- 386 | // -*- font:bar;mode:shell-script;foo:bar; -*- 387 | // -*-font:mode;mode:SH-*- 388 | // -*- foo:bar mode: sh bar:baz -*- 389 | " -*-foo:bar;mode:sh;bar:foo-*- "; 390 | " -*-font-mode:foo;mode:shell-script;foo-bar:quux-*-" 391 | "-*-font:x;foo:bar; mode : sh;bar:foo;foooooo:baaaaar;fo:ba;-*-"; 392 | "-*- font:x;foo : bar ; mode : sH ; bar : foo ; foooooo:baaaaar;fo:ba-*-"; 393 | """ 394 | for line in valid.split /\n/ 395 | expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull() 396 | 397 | invalid = """ 398 | /* --*sh-*- */ 399 | /* -*-- sh -*- 400 | /* -*- -- sh -*- 401 | /* -*- shell-scripts -;- -*- 402 | // -*- SSSSSSSSSH -*- 403 | // -*- SH; -*- 404 | // -*- sh-stuff -*- 405 | /* -*- model:sh -*- 406 | /* -*- indent-mode:sh -*- 407 | // -*- font:mode;SH -*- 408 | // -*- mode: -*- SH 409 | // -*- mode: secret-sh -*- 410 | // -*-font:mode;mode:sh--*- 411 | """ 412 | for line in invalid.split /\n/ 413 | expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull() 414 | 415 | it "recognises Vim modelines", -> 416 | valid = """ 417 | vim: se filetype=sh: 418 | # vim: se ft=sh: 419 | # vim: set ft=sh: 420 | # vim: set filetype=sh: 421 | # vim: ft=sh 422 | # vim: syntax=sH 423 | # vim: se syntax=SH: 424 | # ex: syntax=SH 425 | # vim:ft=sh 426 | # vim600: ft=sh 427 | # vim>600: set ft=sh: 428 | # vi:noai:sw=3 ts=6 ft=sh 429 | # vi::::::::::noai:::::::::::: ft=sh 430 | # vim:ts=4:sts=4:sw=4:noexpandtab:ft=sh 431 | # vi:: noai : : : : sw =3 ts =6 ft =sh 432 | # vim: ts=4: pi sts=4: ft=sh: noexpandtab: sw=4: 433 | # vim: ts=4 sts=4: ft=sh noexpandtab: 434 | # vim:noexpandtab sts=4 ft=sh ts=4 435 | # vim:noexpandtab:ft=sh 436 | # vim:ts=4:sts=4 ft=sh:noexpandtab:\x20 437 | # vim:noexpandtab titlestring=hi\|there\\\\ ft=sh ts=4 438 | """ 439 | for line in valid.split /\n/ 440 | expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull() 441 | 442 | invalid = """ 443 | ex: se filetype=sh: 444 | _vi: se filetype=sh: 445 | vi: se filetype=sh 446 | # vim set ft=ssh 447 | # vim: soft=sh 448 | # vim: hairy-syntax=sh: 449 | # vim set ft=sh: 450 | # vim: setft=sh: 451 | # vim: se ft=sh backupdir=tmp 452 | # vim: set ft=sh set cmdheight=1 453 | # vim:noexpandtab sts:4 ft:sh ts:4 454 | # vim:noexpandtab titlestring=hi\\|there\\ ft=sh ts=4 455 | # vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=sh ts=4 456 | """ 457 | for line in invalid.split /\n/ 458 | expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull() 459 | -------------------------------------------------------------------------------- /grammars/shell-unix-bash.cson: -------------------------------------------------------------------------------- 1 | 'scopeName': 'source.shell' 2 | 'name': 'Shell Script' 3 | 'fileTypes': [ 4 | 'sh' 5 | 'bash' 6 | 'ksh' 7 | 'zsh' 8 | 'zsh-theme' 9 | 'zshenv' 10 | 'zlogin' 11 | 'zlogout' 12 | 'zprofile' 13 | 'zshrc' 14 | 'bashrc' 15 | 'bash_aliases' 16 | 'bash_profile' 17 | 'bash_login' 18 | 'profile' 19 | 'bash_logout' 20 | '.textmate_init' 21 | 'npmrc' 22 | 'PKGBUILD' 23 | 'install' 24 | 'cygport' 25 | 'bats' 26 | 'ebuild' 27 | ] 28 | 'firstLineMatch': '''(?x) 29 | # Hashbang 30 | ^\\#!.*(?:\\s|\\/) 31 | (?:bash|zsh|sh|tcsh|ksh|dash|ash|csh|rc) 32 | (?:$|\\s) 33 | | 34 | # Modeline 35 | (?i: 36 | # Emacs 37 | -\\*-(?:\\s*(?=[^:;\\s]+\\s*-\\*-)|(?:.*?[;\\s]|(?<=-\\*-))mode\\s*:\\s*) 38 | (?:shell-script|sh) 39 | (?=[\\s;]|(?]?\\d+|m)?|\\sex)(?=:(?=\\s*set?\\s[^\\n:]+:)|:(?!\\s* set?\\s))(?:(?:\\s|\\s*:\\s*)\\w*(?:\\s*=(?:[^\\n\\\\\\s]|\\\\.)*)?)*[\\s:](?:filetype|ft|syntax)\\s*= 43 | sh 44 | (?=\\s|:|$) 45 | ) 46 | ''' 47 | 'patterns': [ 48 | { 49 | 'include': '#comment' 50 | } 51 | { 52 | 'include': '#pipeline' 53 | } 54 | { 55 | 'include': '#list' 56 | } 57 | { 58 | 'include': '#compound-command' 59 | } 60 | { 61 | 'include': '#loop' 62 | } 63 | { 64 | 'include': '#string' 65 | } 66 | { 67 | 'include': '#function-definition' 68 | } 69 | { 70 | 'include': '#variable' 71 | } 72 | { 73 | 'include': '#interpolation' 74 | } 75 | { 76 | 'include': '#heredoc' 77 | } 78 | { 79 | 'include': '#herestring' 80 | } 81 | { 82 | 'include': '#redirection' 83 | } 84 | { 85 | 'include': '#pathname' 86 | } 87 | { 88 | 'include': '#keyword' 89 | } 90 | { 91 | 'include': '#support' 92 | } 93 | ] 94 | 'repository': 95 | 'case-clause': 96 | 'patterns': [ 97 | { 98 | 'begin': '(?=\\S)' 99 | 'end': ';;' 100 | 'endCaptures': 101 | '0': 102 | 'name': 'punctuation.terminator.case-clause.shell' 103 | 'name': 'meta.scope.case-clause.shell' 104 | 'patterns': [ 105 | { 106 | 'begin': '\\(|(?=\\S)' 107 | 'beginCaptures': 108 | '0': 109 | 'name': 'punctuation.definition.case-pattern.shell' 110 | 'end': '\\)' 111 | 'endCaptures': 112 | '0': 113 | 'name': 'punctuation.definition.case-pattern.shell' 114 | 'name': 'meta.scope.case-pattern.shell' 115 | 'patterns': [ 116 | { 117 | 'match': '\\|' 118 | 'name': 'punctuation.separator.pipe-sign.shell' 119 | } 120 | { 121 | 'include': '#string' 122 | } 123 | { 124 | 'include': '#variable' 125 | } 126 | { 127 | 'include': '#interpolation' 128 | } 129 | { 130 | 'include': '#pathname' 131 | } 132 | ] 133 | } 134 | { 135 | 'begin': '(?<=\\))' 136 | 'end': '(?=;;)' 137 | 'name': 'meta.scope.case-clause-body.shell' 138 | 'patterns': [ 139 | { 140 | 'include': '$self' 141 | } 142 | ] 143 | } 144 | ] 145 | } 146 | ] 147 | 'comment': 148 | 'begin': '(^\\s+)?(?<=^|\\W)(?|&&|\\|\\|' 776 | 'name': 'keyword.operator.logical.shell' 777 | } 778 | { 779 | 'match': '(?[>=]?|==|!=|^|\\|{1,2}|&{1,2}|\\?|\\:|,|=|[*/%+\\-&^|]=|<<=|>>=' 919 | 'name': 'keyword.operator.arithmetic.shell' 920 | } 921 | { 922 | 'match': '0[xX][0-9A-Fa-f]+' 923 | 'name': 'constant.numeric.hex.shell' 924 | } 925 | { 926 | 'match': '0\\d+' 927 | 'name': 'constant.numeric.octal.shell' 928 | } 929 | { 930 | 'match': '\\d{1,2}#[0-9a-zA-Z@_]+' 931 | 'name': 'constant.numeric.other.shell' 932 | } 933 | { 934 | 'match': '\\d+' 935 | 'name': 'constant.numeric.integer.shell' 936 | } 937 | ] 938 | 'pathname': 939 | 'patterns': [ 940 | { 941 | 'match': '(?<=\\s|:|=|^)~' 942 | 'name': 'keyword.operator.tilde.shell' 943 | } 944 | { 945 | 'match': '\\*|\\?' 946 | 'name': 'keyword.operator.glob.shell' 947 | } 948 | { 949 | 'begin': '([?*+@!])(\\()' 950 | 'beginCaptures': 951 | '1': 952 | 'name': 'keyword.operator.extglob.shell' 953 | '2': 954 | 'name': 'punctuation.definition.extglob.shell' 955 | 'end': '\\)' 956 | 'endCaptures': 957 | '0': 958 | 'name': 'punctuation.definition.extglob.shell' 959 | 'name': 'meta.structure.extglob.shell' 960 | 'patterns': [ 961 | { 962 | 'include': '$self' 963 | } 964 | ] 965 | } 966 | ] 967 | 'pipeline': 968 | 'patterns': [ 969 | { 970 | 'match': '(?<=^|;|&|\\s)(time)(?=\\s|;|&|$)' 971 | 'name': 'keyword.other.shell' 972 | } 973 | { 974 | 'match': '[|!]' 975 | 'name': 'keyword.operator.pipe.shell' 976 | } 977 | ] 978 | 'redirection': 979 | 'patterns': [ 980 | { 981 | 'begin': '[><]\\(' 982 | 'beginCaptures': 983 | '0': 984 | 'name': 'punctuation.definition.string.begin.shell' 985 | 'end': '\\)' 986 | 'endCaptures': 987 | '0': 988 | 'name': 'punctuation.definition.string.end.shell' 989 | 'name': 'string.interpolated.process-substitution.shell' 990 | 'patterns': [ 991 | { 992 | 'include': '$self' 993 | } 994 | ] 995 | } 996 | { 997 | # valid: &>word >&word >word [n]>&[n] [n]word [n]>>word [n]<&word (last one is duplicate) 998 | 'match': '(?])(&>|\\d*>&\\d*|\\d*(>>|>|<)|\\d*<&|\\d*<>)(?![<>])' 999 | 'name': 'keyword.operator.redirect.shell' 1000 | } 1001 | ] 1002 | 'string': 1003 | 'patterns': [ 1004 | { 1005 | 'match': '\\\\.' 1006 | 'name': 'constant.character.escape.shell' 1007 | } 1008 | { 1009 | 'begin': '\'' 1010 | 'beginCaptures': 1011 | '0': 1012 | 'name': 'punctuation.definition.string.begin.shell' 1013 | 'end': '\'' 1014 | 'endCaptures': 1015 | '0': 1016 | 'name': 'punctuation.definition.string.end.shell' 1017 | 'name': 'string.quoted.single.shell' 1018 | } 1019 | { 1020 | 'begin': '\\$?"' 1021 | 'beginCaptures': 1022 | '0': 1023 | 'name': 'punctuation.definition.string.begin.shell' 1024 | 'end': '"' 1025 | 'endCaptures': 1026 | '0': 1027 | 'name': 'punctuation.definition.string.end.shell' 1028 | 'name': 'string.quoted.double.shell' 1029 | 'patterns': [ 1030 | { 1031 | 'match': '\\\\[\\$`"\\\\\\n]' 1032 | 'name': 'constant.character.escape.shell' 1033 | } 1034 | { 1035 | 'include': '#variable' 1036 | } 1037 | { 1038 | 'include': '#interpolation' 1039 | } 1040 | ] 1041 | } 1042 | { 1043 | 'begin': '\\$\'' 1044 | 'beginCaptures': 1045 | '0': 1046 | 'name': 'punctuation.definition.string.begin.shell' 1047 | 'end': '\'' 1048 | 'endCaptures': 1049 | '0': 1050 | 'name': 'punctuation.definition.string.end.shell' 1051 | 'name': 'string.quoted.single.dollar.shell' 1052 | 'patterns': [ 1053 | { 1054 | 'match': '\\\\(a|b|e|f|n|r|t|v|\\\\|\')' 1055 | 'name': 'constant.character.escape.ansi-c.shell' 1056 | } 1057 | { 1058 | 'match': '\\\\[0-9]{3}' 1059 | 'name': 'constant.character.escape.octal.shell' 1060 | } 1061 | { 1062 | 'match': '\\\\x[0-9a-fA-F]{2}' 1063 | 'name': 'constant.character.escape.hex.shell' 1064 | } 1065 | { 1066 | 'match': '\\\\c.' 1067 | 'name': 'constant.character.escape.control-char.shell' 1068 | } 1069 | ] 1070 | } 1071 | ] 1072 | 'support': 1073 | 'patterns': [ 1074 | { 1075 | 'match': '(?<=^|;|&|\\s)(?::|\\.)(?=\\s|;|&|$)' 1076 | 'name': 'support.function.builtin.shell' 1077 | } 1078 | { 1079 | 'match': '(?<=^|;|&|\\s)(?:alias|bg|bind|break|builtin|caller|cd|command|compgen|complete|dirs|disown|echo|enable|eval|exec|exit|false|fc|fg|getopts|hash|help|history|jobs|kill|let|logout|popd|printf|pushd|pwd|read|readonly|set|shift|shopt|source|suspend|test|times|trap|true|type|ulimit|umask|unalias|unset|wait)(?=\\s|;|&|$)' 1080 | 'name': 'support.function.builtin.shell' 1081 | } 1082 | ] 1083 | 'variable': 1084 | 'patterns': [ 1085 | { 1086 | 'captures': 1087 | '1': 1088 | 'name': 'punctuation.definition.variable.shell' 1089 | 'match': '(\\$)[a-zA-Z_][a-zA-Z0-9_]*' 1090 | 'name': 'variable.other.normal.shell' 1091 | } 1092 | { 1093 | 'captures': 1094 | '1': 1095 | 'name': 'punctuation.definition.variable.shell' 1096 | 'match': '(\\$)[-*@#?$!0_]' 1097 | 'name': 'variable.other.special.shell' 1098 | } 1099 | { 1100 | 'captures': 1101 | '1': 1102 | 'name': 'punctuation.definition.variable.shell' 1103 | 'match': '(\\$)[1-9]' 1104 | 'name': 'variable.other.positional.shell' 1105 | } 1106 | { 1107 | 'begin': '\\${' 1108 | 'beginCaptures': 1109 | '0': 1110 | 'name': 'punctuation.definition.variable.shell' 1111 | 'end': '}' 1112 | 'endCaptures': 1113 | '0': 1114 | 'name': 'punctuation.definition.variable.shell' 1115 | 'name': 'variable.other.bracket.shell' 1116 | 'patterns': [ 1117 | { 1118 | 'match': '!|:[-=?]?|\\*|@|\#\{1,2}|%{1,2}|/' # #{ is escaped to prevent coffeelint complaining about interpolation 1119 | 'name': 'keyword.operator.expansion.shell' 1120 | } 1121 | { 1122 | 'captures': 1123 | '1': 1124 | 'name': 'punctuation.section.array.shell' 1125 | '3': 1126 | 'name': 'punctuation.section.array.shell' 1127 | 'match': '(\\[)([^\\]]+)(\\])' 1128 | } 1129 | { 1130 | 'include': '#variable' 1131 | } 1132 | { 1133 | 'include': '#string' 1134 | } 1135 | ] 1136 | } 1137 | ] 1138 | --------------------------------------------------------------------------------