%YAML 1.2
# [Sublime]: https://www.sublimetext.com/docs/3/syntax.html
# [Bash]:    https://www.gnu.org/software/bash/manual/bash.html
--- #---------------------------------------------------------------------------

name: console

scope: source.shell.console

#-------------------------------------------------------------------------------
variables:
  call_token: \./
  cmd_boundary: (?=\s|;|$|>|<)
  extension: \.sh
  identifier: '[[:alpha:]_][[:alnum:]_]*'
  identifier_non_posix: '[^{{metachar}}\d][^{{metachar}}=]*'
  is_command: (?=\S)
  is_end_of_interpolation: \)
  is_end_of_option: '[^\w$-]|$'
  is_function: \s*\b(function)\s+|(?=\s*{{identifier_non_posix}}\s*\(\s*\))
  is_path_component: (?=[^\s/]*/)
  is_start_of_arguments: '[`=|&;()<>\s]'
  is_variable: (?=\s*{{nbc}}(?:[({]{{nbc}}[)}])?{{nbc}}=)
  keyword_break: (?![-=\w])

  # A character that, when unquoted, separates words. A metacharacter is a
  # space, tab, newline, or one of the following characters: ‘|’, ‘&’, ‘;’,
  # ‘(’, ‘)’, ‘<’, or ‘>’.
  metachar: '[\s\t\n|&;()<>]'

  nbc: '[^{}()=\s]*' # non bracket characters (and also non-whitespace, parens)
  start_of_option: (?:\s+|^)--?(?=[\w$])
  varassign: '[+\-?]?='
#-------------------------------------------------------------------------------
contexts:

  comment:
    - match: (?:^\s*|\s+)(\#)
      captures:
        1:
          comment.line.number-sign.shell
          punctuation.definition.comment.begin.shell
      push:
        - meta_content_scope: comment.line.number-sign.shell
        # NOTE: The reason for consuming the newline character is as follows.
        # When triggering a snippet, its scope is tested to the *right* of the
        # cursor. So, if you don't want your snippet to trigger in a comment,
        # you have to use something like <scope>source.shell - comment</scope>.
        # If the newline character is not scoped as a comment too, then that
        # scope will never work, because the scope to the right of the cursor
        # will never be a comment scope. That is, unless we consume the newline
        # character (or we are editing something in the middle of an existing
        # comment).
        - match: '(?=\n)'
          scope: comment.line.number-sign.shell
          pop: true

  line-continuation-or-pop-at-end:
    - include: pop-at-end
    - include: line-continuation

  pop-at-end:
    - match: $
      pop: true

  any-escape:
    - match: \\.
      scope: constant.character.escape.shell

  line-continuation:
    - match: \\\n
      scope: punctuation.separator.continuation.line.shell
      push:
        - match: ^
          pop: true
    - match: \\(\s+)\n
      captures:
        1: invalid.illegal.extraneous-spaces-after-line-continuation.shell

  prototype:
    - include: comment
    - include: line-continuation
    - include: any-escape

  main:
    - meta_include_prototype: false
    - match: ^(\$|\#|❯)(?:\s)
      captures:
          1: keyword.operator.assignment.redirection.process.shell
      push: console-line

  console-line:
    - match: $
      pop: true
    - include: prototype
    - include: funcdef
    - include: vardef
    - include: redirection
    - include: operator-exclamation
    - match: '{{is_command}}'
      push: cmd

  # NOTE: Contexts with a "-bt" suffix are the "backtick" contexts. They mirror
  # the ordinary contexts, except that when a backtick is encountered while in
  # a "-bt" context, we pop.
  # Normally, we are in a non-bt context. When we encounter a backtick character
  # (the ` character), we enter the main-bt context. Popping when encountering
  # another ` character then ensures that we don't enter yet another backtick
  # context.
  # The "expansion" context is the **only** place where this main-bt context is
  # used. If you, the reader, knows of a more elegant way to handle backticks,
  # don't hesitate to change it.
  main-bt:
    - include: funcdef-bt
    - include: vardef
    - include: redirection
    - match: '{{is_command}}'
      push: cmd-bt

  control:
    - match: \bif{{keyword_break}}
      scope: keyword.control.conditional.if.shell
      pop: true
    - match: \bthen{{keyword_break}}
      scope: keyword.control.conditional.then.shell
      pop: true
    - match: \belif{{keyword_break}}
      scope: keyword.control.conditional.elseif.shell
      pop: true
    - match: \bfi{{keyword_break}}
      scope: keyword.control.conditional.end.shell
      set: [cmd-post, cmd-args]
    - match: \belse{{keyword_break}}
      scope: keyword.control.conditional.else.shell
      pop: true
    - match: \bfor{{keyword_break}}
      scope: keyword.control.loop.for.shell
      set: [cmd-post, for-args]
    - match: \bdo{{keyword_break}}
      scope: keyword.control.loop.do.shell
      pop: true
    - match: \bdone{{keyword_break}}
      scope: keyword.control.loop.end.shell
      set: [cmd-post, cmd-args]
    - match: \bwhile{{keyword_break}}
      scope: keyword.control.loop.while.shell
    - match: \buntil{{keyword_break}}
      scope:  keyword.control.loop.until.shell
    - match: \bcase{{keyword_break}}
      scope: keyword.control.conditional.case.shell
      set: [case-body, case-word]
    - match: \bcontinue{{keyword_break}}
      scope: keyword.control.flow.continue.shell
    - match: \bbreak{{keyword_break}}
      scope: keyword.control.flow.break.shell
      set: [cmd-post, cmd-args]
    - match: \besac{{keyword_break}}
      scope: keyword.control.conditional.end.shell
      pop: true

  case-word:
    - match: \bin{{keyword_break}}
      scope: keyword.control.in.shell
      pop: true
    - include: case-end-ahead
    - include: expansion-and-string

  case-body:
    - meta_scope: meta.conditional.case.shell
    - match: \besac{{keyword_break}}
      scope: keyword.control.conditional.end.shell
      pop: true
    - match: (?=\()
      push:
        - clear_scopes: 1  # remove meta.conditional.case.shell
        - match: \(
          scope: keyword.control.conditional.patterns.begin.shell
          set: case-clause-patterns
    - match: (?=\S)
      push: case-clause-patterns

  case-clause-patterns:
    - clear_scopes: 1  # remove meta.conditional.case.shell
    - meta_scope: meta.conditional.case.clause.patterns.shell
    - match: \)
      scope: keyword.control.conditional.patterns.end.shell
      set: case-clause-commands
    # emergency bail outs if ')' is missing
    - match: (?=;;&?|;&)
      set: case-clause-commands
    - include: case-end-ahead
    - include: case-clause-patterns-body

  case-clause-patterns-body:
    # [Bash] 3.2.4.2: Each pattern undergoes tilde expansion, parameter
    # expansion, command substitution, and arithmetic expansion.
    - include: expansion-pattern
    - include: expansion-tilde
    - include: expansion-parameter
    - include: expansion-command
    - include: expansion-arithmetic
    - include: string
    - match: \|
      scope: keyword.operator.logical.shell
    - match: \(
      scope: punctuation.section.parens.begin.shell
      push:
        - match: \)
          scope: punctuation.section.parens.end.shell
          pop: true
        - include: case-clause-patterns-body

  case-clause-commands:
    - clear_scopes: 1  # remove meta.conditional.case.shell
    - meta_content_scope: meta.conditional.case.clause.commands.shell
    - match: ;;&?|;&
      scope:
        meta.conditional.case.clause.commands.shell
        punctuation.terminator.case.clause.shell
      pop: true
    - include: case-end-ahead
    - include: main

  case-end-ahead:
    - match: (?=\besac{{keyword_break}})
      pop: true

  # I don't think anybody will write a for-loop inside backticks. Hence no
  # for-args-bt context.
  for-args:
    - match: ""
      set:
        - meta_scope: meta.group.for.shell
        - include: cmd-args-boilerplate
        - include: arithmetic
        - match: \bin{{keyword_break}}
          scope: keyword.control.in.shell

  expansion-and-string:
    - include: string
    - include: expansion

  funcdef:
    - match: '{{is_function}}'
      captures:
        1: storage.type.function.shell
      push: [funcdef-body, funcdef-parens, funcdef-name]
    - match: \bcoproc{{keyword_break}}
      scope: keyword.other.coproc.shell
      push: [cmd-post, cmd-args, coproc-body]

  funcdef-bt:
    - match: '{{is_function}}'
      captures:
        1: storage.type.function.shell
      push: [funcdef-body-bt, funcdef-parens, funcdef-name]
    - match: \bcoproc{{keyword_break}}
      scope: keyword.other.coproc.shell
      push: [cmd-post, cmd-args-bt, coproc-body]

  coproc-body:
    - match: \s*(?=\S+\s*\{)
      set:
        - meta_content_scope: entity.name.function.coproc.shell
        - match: (?=\s*\{)
          set:
            - match: \{
              scope: punctuation.section.braces.begin.shell
              set:
                - meta_scope: meta.function.coproc.shell
                - match: \}
                  scope: punctuation.section.braces.end.shell
                  pop: true
                - include: main
    - match: ""
      set: main-with-pop-at-end

  funcdef-name:
    - match: \s*
      set:
        - meta_content_scope: entity.name.function.shell
        - match: (?=\s*[({]|$)
          pop: true

  funcdef-parens:
    - match: (\()\s*(\))
      captures:
        1: punctuation.section.parens.begin.shell
        2: punctuation.section.parens.end.shell
    - match: \{
      scope: punctuation.section.braces.begin.shell
      pop: true
    - match: \(
      scope: punctuation.definition.compound.begin.shell
      pop: true

  funcdef-body:
    - meta_scope: meta.function.shell
    - match: \}
      scope: punctuation.section.braces.end.shell
      pop: true
    - match: \)
      scope: punctuation.definition.compound.end.shell
      pop: true
    - include: main

  funcdef-body-bt:
    - meta_scope: meta.function.shell
    - match: \}
      scope: punctuation.section.braces.end.shell
      pop: true
    - match: \)
      scope: punctuation.definition.compound.end.shell
      pop: true
    - include: main-bt

  vardef:
    - match: \s*\b(alias){{keyword_break}}
      captures:
        1: support.function.alias.shell
      push:
      - vardef-ensure-function-call-scope
      - vardef-maybe-more
      - vardef-value
      - vardef-assign
      - vardef-alias-name
      - vardef-alias-options
    - match: \s*\b(typeset|declare|local){{keyword_break}}
      captures:
        1: storage.modifier.shell
      push:
      - vardef-ensure-function-call-scope
      - vardef-maybe-more
      - vardef-value
      - vardef-assign
      - vardef-name
      - vardef-declare-options
    - match: \s*\b(export){{keyword_break}}
      captures:
        1: storage.modifier.shell
      push:
      - vardef-ensure-function-call-scope
      - vardef-maybe-more
      - vardef-value
      - vardef-assign
      - vardef-name
      - vardef-export-options
    - match: \s*\b(readonly){{keyword_break}}
      captures:
        1: storage.modifier.shell
      push:
      - vardef-ensure-function-call-scope
      - vardef-maybe-more
      - vardef-value
      - vardef-assign
      - vardef-name
      - vardef-readonly-options
    - match: '{{is_variable}}'
      push:
      - vardef-value
      - vardef-assign
      - vardef-name

  vardef-readonly-options:
    - match: \s*((-)(?:[aAf]+|p))
      captures:
        2: punctuation.definition.parameter.shell
        1: variable.parameter.option.shell
    - match: \s*
      pop: true

  vardef-export-options:
    - match: \s*((-)(?:[fn]+|p))
      captures:
        2: punctuation.definition.parameter.shell
        1: variable.parameter.option.shell
    - match: \s*
      pop: true

  vardef-ensure-function-call-scope:
    - meta_include_prototype: false
    - meta_scope: meta.function-call.shell
    - match: ""
      pop: true

  vardef-maybe-more:
    - meta_include_prototype: false
    - match: (?=`)
      pop: true
    - match: (?=\s*#)
      pop: true
    - include: cmd-args-boilerplate
    - match: (?=\S)
      push: [vardef-value, vardef-assign, vardef-name]

  vardef-alias-options:
    - match: \s*((-)p)
      captures:
        2: punctuation.definition.parameter.shell
        1: variable.parameter.option.shell
    - match: \s*
      pop: true

  vardef-alias-name:
    - match: \s*
      set:
        - meta_include_prototype: false
        - meta_content_scope: entity.name.function.alias.shell
        - include: line-continuation-or-pop-at-end
        - include: any-escape
        - match: (?={{varassign}}|\s)|$
          pop: true
        - include: array
        - match: \s*$
          pop: true
        - include: string

  vardef-declare-options:
    - match: \s*((-)(?:[aAfFgilnrtux]+|p))
      captures:
        2: punctuation.definition.parameter.shell
        1: variable.parameter.option.shell
    - match: \s*
      pop: true

  vardef-name:
    - match: \s*
      set:
        - meta_include_prototype: false
        - meta_content_scope: variable.other.readwrite.assignment.shell
        - include: line-continuation-or-pop-at-end
        - include: any-escape
        - match: (?={{varassign}}|\s)|$|(?=[;&`]|{{metachar}})
          pop: true
        - include: array
        - match: \s*$
          pop: true
        - include: string

  vardef-assign:
    - meta_include_prototype: false
    - include: line-continuation-or-pop-at-end
    - include: any-escape
    - match: '{{varassign}}'
      scope: keyword.operator.assignment.shell
      pop: true
    - match: ""
      pop: true

  vardef-value:
    - meta_include_prototype: false
    - match: \(
      scope: punctuation.section.parens.begin.shell
      set:
        - match: \)
          scope: punctuation.section.parens.end.shell
          pop: true
        - match: \[
          scope: punctuation.section.brackets.begin.shell
          push:
            - match: \]
              scope: punctuation.section.brackets.end.shell
              set:
                - match: =
                  scope: keyword.operator.assignment.shell
                  pop: true
                - match: ""
                  pop: true
            - include: expansion-and-string
        - include: expansion-and-string
    - match: (?=[&`])
      pop: true
    - match: ""
      set:
        - meta_include_prototype: false
        - meta_scope: string.unquoted.shell
        - match: (?=`)
          pop: true
        - include: expansion-and-string
        - include: line-continuation-or-pop-at-end
        - include: any-escape
        - match: (?={{metachar}})
          pop: true

  redirection:
    - include: redirection-here-string
    - include: redirection-here-document
    - include: redirection-process
    - include: redirection-input
    - include: redirection-output
    - include: redirection-inout

  redirection-process:
    - match: (\d*)([<>])(\()
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.process.shell
        3: punctuation.section.parens.begin.shell
      push:
        - match: \)
          scope: punctuation.section.parens.end.shell
          pop: true
        - include: main

  redirection-output:
    - match: (\d*)(>>!?|>&?|&>|&?>(?:\||>))
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.shell
      push: redirection-post

  redirection-input:
    - match: (\d*)(<&?)
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.shell
      push: redirection-post

  redirection-post:
    - match: \s*(?:(\d+)|(-))
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: punctuation.terminator.file-descriptor.shell
      pop: true
    - match: \s*(?=\S)
      set:
        - match: (?={{metachar}}|`)
          pop: true
        - include: expansion-and-string
    - match: \s*
      pop: true

  redirection-inout:
    - match: (\d*)(<>)
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.shell

  redirection-here-string:
    - match: (\d*)(<<<)\s
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.herestring.shell

  redirection-here-document:
    # These are the variants that allow tabs before the end token
    - match: (\d*)(<<-)\s*(')({{identifier}})(')
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.shell
        3: punctuation.definition.string.begin.shell
        4: keyword.control.heredoc-token.shell
        5: punctuation.definition.string.end.shell
      push: [heredocs-body-allow-tabs-no-expansion, heredocs-preamble]
    - match: (\d*)(<<-)\s*(")({{identifier}})(")
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.shell
        3: punctuation.definition.string.begin.shell
        4: keyword.control.heredoc-token.shell
        5: punctuation.definition.string.end.shell
      push: [heredocs-body-allow-tabs-no-expansion, heredocs-preamble]
    - match: (\d*)(<<-)\s*(\\)({{identifier}})
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.shell
        3: punctuation.definition.string.shell
        4: keyword.control.heredoc-token.shell
      push: [heredocs-body-allow-tabs-no-expansion, heredocs-preamble]
    - match: (\d*)(<<-)\s*({{identifier}})
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.shell
        3: keyword.control.heredoc-token.shell
      push: [heredocs-body-allow-tabs, heredocs-preamble]
    # These are the variants that DON'T allow tabs before the end token
    - match: (\d*)(<<)\s*(')({{identifier}})(')
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.shell
        3: punctuation.definition.string.begin.shell
        4: keyword.control.heredoc-token.shell
        5: punctuation.definition.string.end.shell
      push: [heredocs-body-no-expansion, heredocs-preamble]
    - match: (\d*)(<<)\s*(")({{identifier}})(")
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.shell
        3: punctuation.definition.string.begin.shell
        4: keyword.control.heredoc-token.shell
        5: punctuation.definition.string.end.shell
      push: [heredocs-body-no-expansion, heredocs-preamble]
    - match: (\d*)(<<)\s*(\\)({{identifier}})
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.shell
        3: punctuation.definition.string.shell
        4: keyword.control.heredoc-token.shell
      push: [heredocs-body-no-expansion, heredocs-preamble]
    - match: (\d*)(<<)\s*({{identifier}})
      captures:
        1: constant.numeric.integer.decimal.file-descriptor.shell
        2: keyword.operator.assignment.redirection.shell
        3: keyword.control.heredoc-token.shell
      push: [heredocs-body, heredocs-preamble]

  heredocs-body:
    - meta_include_prototype: false
    - meta_scope: string.unquoted.heredoc.shell
    - include: heredocs-body-common-with-expansion
    - match: ^\3(\s+)\n # the third capture from redirection-here-document
      captures:
        1: invalid.illegal.no-spaces-allowed-after-heredoc-token.shell
      # rather not pop, but sublime throws an error otherwise.
      pop: true
    - match: ^\3$ # the third capture from redirection-here-document
      scope: keyword.control.heredoc-token.shell
      pop: true

  heredocs-body-allow-tabs:
    - meta_include_prototype: false
    - meta_scope: string.unquoted.heredoc.shell
    - include: heredocs-body-common-with-expansion
    - match: ^\s*\3(\s+)\n # the third capture from redirection-here-document
      captures:
        1: invalid.illegal.no-spaces-allowed-after-heredoc-token.shell
      # rather not pop, but sublime throws an error otherwise.
      pop: true
    - match: ^\s*(\3)$ # the third capture from redirection-here-document
      captures:
        1: keyword.control.heredoc-token.shell
      pop: true

  heredocs-body-common-with-expansion:
    # [Bash] 3.6.6: all lines of the here-document are subjected to parameter
    # expansion, command substitution, and arithmetic expansion, the character
    # sequence \newline is ignored, and ‘\’ must be used to quote the
    # characters ‘\’, ‘$’, and ‘`’.
    - match: \\[`$"\\]
      scope: constant.character.escape.backtick.shell
    - include: expansion-parameter
    - include: expansion-arithmetic
    - include: expansion-command

  heredocs-body-no-expansion:
    - meta_include_prototype: false
    - meta_scope: string.unquoted.heredoc.shell
    - match: ^\4(\s+)\n # the fourth capture from redirection-here-document
      captures:
        1: invalid.illegal.no-spaces-allowed-after-heredoc-token.shell
      # rather not pop, but sublime throws an error otherwise.
      pop: true
    - match: ^\4$ # the fourth capture from redirection-here-document
      scope: keyword.control.heredoc-token.shell
      pop: true

  heredocs-body-allow-tabs-no-expansion:
    - meta_include_prototype: false
    - meta_scope: string.unquoted.heredoc.shell
    - match: ^\s*\4(\s+)\n # the fourth capture from redirection-here-document
      captures:
        1: invalid.illegal.no-spaces-allowed-after-heredoc-token.shell
      # rather not pop, but sublime throws an error otherwise.
      pop: true
    - match: ^\s*(\4)$ # the fourth capture from redirection-here-document
      captures:
        1: keyword.control.heredoc-token.shell
      pop: true

  heredocs-preamble:
    - match: ""
      set:
        # This enables us to keep parsing on the line where the start token of
        # the heredoc is. Once the first line has ended, we enter the body of
        # the heredoc, where everything is just an unquoted string.
        # One clear_scope for the string.unquoted.
        # The problem with this is that when we also end a function definition
        # on the same line (with the "}" token), we cannot do that.
        - clear_scopes: 1
        - match: $
          pop: true
        - match: \s*(?=\S)
          push: [main-with-pop-at-end, cmd-post, cmd-args]

  main-with-pop-at-end:
    - include: line-continuation-or-pop-at-end
    - include: main

  cmd-name-common:
    - match: (?=}|\s+#|\s*(?:[|;]|&(?!>)))
      pop: true
    - include: string
    - include: expansion-parameter
    - include: expansion-arithmetic
    - include: expansion-command
    - include: expansion-tilde
    - include: expansion-job
    - include: line-continuation-or-pop-at-end

  cmd-args-common:
    - match: (?=}|\s+#)
      pop: true
    - include: redirection
    - match: (?=\s*([|;]|&(?!>)))
      pop: true
    - include: expansion-and-string
    - include: line-continuation-or-pop-at-end

  cmd-post: # looks like [main, cmd-post] at this point
    - match: ;(?![;&])
      scope: keyword.operator.logical.continue.shell
      pop: true
    - match: \|\|
      scope: keyword.operator.logical.or.shell
      pop: true
    - match: \|
      scope: keyword.operator.logical.pipe.shell
      pop: true
    - match: \&\&
      scope: keyword.operator.logical.and.shell
      pop: true
    - match: \&
      scope: keyword.operator.logical.job.shell
      pop: true
    - match: $|(?=\S)
      pop: true

  cmd-args-boilerplate:
    - match: (?={{is_end_of_interpolation}})
      pop: true
    - include: cmd-args-common
    - match: (?:\s+|^)--(?=\s|$)
      scope: keyword.operator.end-of-options.shell
      set:
        - meta_content_scope: meta.function-call.arguments.shell
        - include: end-of-options-common

  cmd-args-boilerplate-bt:
    - match: (?={{is_end_of_interpolation}}|`) # <-------------- extra backtick
      pop: true
    - include: cmd-args-common
    - match: (?:\s+|^)--(?=\s|$)
      scope: keyword.operator.end-of-options.shell
      set:
        - meta_content_scope: meta.function-call.arguments.shell
        - match: (?=`) # <-------------------------------------- extra backtick
          pop: true
        - include: end-of-options-common

  end-of-options-common:
    - include: redirection
    - match: (?=[)};&|])
      pop: true
    - include: expansion-and-string
    - include: line-continuation-or-pop-at-end

  cmd-args:
    - match: ""
      set:
        - meta_scope: meta.function-call.arguments.shell
        - include: cmd-args-boilerplate
        - match: '{{start_of_option}}'
          scope: punctuation.definition.parameter.shell
          push:
            - meta_scope: variable.parameter.option.shell
            - match: (?==)
              set:
                - match: =
                  scope: keyword.operator.assignment.option.shell
                  pop: true
            - match: (?={{is_end_of_option}})
              pop: true
            - include: expansion-and-string

  cmd-args-bt:
    - match: ""
      set:
        - meta_scope: meta.function-call.arguments.shell
        - include: cmd-args-boilerplate-bt
        - match: '{{start_of_option}}'
          scope: punctuation.definition.parameter.shell
          push:
            - meta_scope: variable.parameter.option.shell
            - match: (?==)
              set:
                - match: =
                  scope: keyword.operator.assignment.option.shell
                  pop: true
            - match: (?={{is_end_of_option}}|`) # <------------- extra backtick
              pop: true
            - include: expansion-and-string

  cmd:
    - include: cmd-common
    - match: \(
      scope: punctuation.definition.compound.begin.shell
      push:
        - match: \)
          scope: punctuation.definition.compound.end.shell
          set: [cmd-post, cmd-args]
        - include: main
    - include: scope:commands.builtin.shell.bash#main
    - match: \blet\b
      scope: support.function.let.bash
      push:
        - meta_scope: meta.function-call.shell
        - match: $
          pop: true
        - include: expression
    - match: (\[\[)(?=\s)
      captures:
        1: support.function.double-brace.begin.shell
      set: [cmd-post, cmd-test-double-brace-args]
    - match: (\[)(?=\s)
      captures:
        1: support.function.test.begin.shell
      set: [cmd-post, cmd-test-brace-args]
    - match: (\{)(?=\s)
      captures:
        1: punctuation.definition.compound.braces.begin.shell
      push:
        - match: \}
          scope: punctuation.definition.compound.braces.end.shell
          set: [cmd-post, cmd-args]
        - include: main
    - match: (?=\S)
      set: [cmd-post, cmd-args, cmd-name]

  cmd-bt:
    - include: cmd-common
    - match: \(
      scope: punctuation.definition.compound.begin.shell
      push:
        - match: \)
          scope: punctuation.definition.compound.end.shell
          set: [cmd-post, cmd-args-bt]
        - include: main
    - include: scope:commands.builtin.shell.bash#main-bt
    - match: \blet\b
      scope: support.function.let.bash
      push:
        - meta_scope: meta.function-call.shell
        - match: $|(?=\`)
          pop: true
        - include: expression
    - match: (\[\[)(?=\s)
      captures:
        1: support.function.double-brace.begin.shell
      set: [cmd-post, cmd-test-double-brace-args-bt]
    - match: (\[)(?=\s)
      captures:
        1: support.function.test.begin.shell
      set: [cmd-post, cmd-test-brace-args-bt]
    - match: (\{)(?=\s)
      captures:
        1: punctuation.definition.compound.braces.begin.shell
      push:
        - match: \}
          scope: punctuation.definition.compound.braces.end.shell
          set: [cmd-post, cmd-args-bt]
        - include: main-bt
    - match: (?=\S)
      set: [cmd-post, cmd-args-bt, cmd-name-bt]

  cmd-test-brace-args:
    - match: ""
      set:
        - meta_scope: meta.function-call.arguments.shell
        - include: cmd-args-boilerplate
        - match: \s+(\])
          captures:
            1: support.function.test.end.shell
          pop: true
        - include: expression-test

  cmd-test-brace-args-bt:
    - match: ""
      set:
        - meta_scope: meta.function-call.arguments.shell
        - include: cmd-args-boilerplate-bt
        - match: \s+(\])
          captures:
            1: support.function.test.end.shell
          pop: true
        - include: expression-test

  cmd-test-double-brace-args:
    - meta_scope: meta.function-call.arguments.shell
    - match: \s+(\]\])
      captures:
        1: support.function.double-brace.end.shell
      pop: true
    - include: expression-test
    # - include: cmd-args-boilerplate

  cmd-test-double-brace-args-bt:
    - meta_scope: meta.function-call.arguments.shell
    - match: \s+(\]\])
      captures:
        1: support.function.double-brace.end.shell
      pop: true
    - include: expression-test
    # - include: cmd-args-boilerplate-bt

  cmd-name:
    - match: ""
      set:
        - meta_scope: meta.function-call.shell variable.function.shell
        - match: (?={{is_start_of_arguments}}|{{is_end_of_interpolation}})
          pop: true
        - include: cmd-name-common

  cmd-name-bt:
    - match: ""
      set:
        - meta_scope: meta.function-call.shell variable.function.shell
        # extra backtick
        - match: (?={{is_start_of_arguments}}|{{is_end_of_interpolation}}|`)
          pop: true
        - include: cmd-name-common

  cmd-common:
    - include: control
    - include: arithmetic
    - match: (?=\)|})
      pop: true
    - include: line-continuation-or-pop-at-end

  arithmetic:
    - match: \(\((?=.+\)\))
      scope: punctuation.section.arithmetic.begin.shell
      push:
        - meta_scope: meta.group.arithmetic.shell
        - match: \)\)
          scope: punctuation.section.arithmetic.end.shell
          pop: true
        - include: expression

  expansion-tilde:
    - match: '~'
      scope: meta.group.expansion.tilde variable.language.tilde.shell

  expansion-brace:
    - match: \{
      scope: punctuation.section.expansion.brace.begin.shell
      push:
        - meta_scope: meta.group.expansion.brace.shell
        - match: \}
          scope: punctuation.section.expansion.brace.end.shell
          pop: true
        - match: \,
          scope: punctuation.separator.shell
        - include: expansion-and-string

  expansion-parameter:
    - match: (\$)(\{)
      captures:
        0: meta.group.expansion.parameter.shell
        1: punctuation.definition.variable.shell
        2: punctuation.section.expansion.parameter.begin.shell
      push:
        - meta_content_scope: meta.group.expansion.parameter.shell
        - meta_include_prototype: false
        - match: \!
          scope: keyword.operator.indirection.shell
          set: expansion-parameter-post-first-character
        - match: \#
          scope: keyword.operator.arithmetic.shell
          set: expansion-parameter-post-first-character
        - match: ""
          set: expansion-parameter-post-first-character
    - match: (\$)(\d)
      captures:
        0: meta.group.expansion.parameter.shell
        1: punctuation.definition.variable.shell
        2: variable.other.readwrite.shell
    - match: (\$)([$#@!~*?_-])(?!\w)
      captures:
        0: meta.group.expansion.parameter.shell
        1: punctuation.definition.variable.shell
        2: variable.language.shell
    - match: (\$)({{identifier}})
      captures:
        0: meta.group.expansion.parameter.shell
        1: punctuation.definition.variable.shell
        2: variable.other.readwrite.shell

  expansion-pattern:
    - match: ([?*+@!])(\()
      captures:
        1: keyword.operator.regexp.quantifier.shell
        2: punctuation.section.parens.begin.shell
      push:
        - match: \)
          scope: punctuation.section.parens.end.shell
          pop: true
        - match: \|
          scope: keyword.operator.logical.or.shell
        - include: expansion-and-string
    - match: '[*?]'
      scope: keyword.operator.regexp.quantifier.shell
    - match: \[(?=.*])
      scope: keyword.control.regexp.set.begin.shell
      push:
        - match: (?=])
          set: expansion-pattern-post-first-char
        - match: '[!^]'
          scope: keyword.operator.logical.not.shell
          set: expansion-pattern-post-first-char
        - match: \-
          set: expansion-pattern-post-first-char
        - match: ""
          set: expansion-pattern-post-first-char

  expansion-pattern-post-first-char:
    - match: (?:-)?(\])
      captures:
        1: keyword.control.regexp.set.end.shell
      pop: true
    - match: \-
      scope: keyword.operator.word.shell
    - match: (\.)[[:word:]](\.)
      captures:
        1: punctuation.separator.collate.begin.shell
        2: punctuation.separator.collate.end.shell
    - match: (=)[[:word:]](=)
      captures:
        1: punctuation.separator.equivalence-class.begin.shell
        2: punctuation.separator.equivalence-class.end.shell
    - match: (:)[[:lower:]]+(:)
      captures:
        1: punctuation.separator.character-class.begin.shell
        2: punctuation.separator.character-class.end.shell
    # You cannot have a regex set inside a regex set, so just consume this
    # character in order to not push into another regex set.
    # Except when writing a character class like [:lower:], so negative look
    # ahead for that possibility.
    - match: \[(?![\.=:])
    - include: expansion-and-string

  expansion-arithmetic:
    - match: (\$)(\(\()(?=.+\)\))
      captures:
        1: punctuation.definition.variable.shell
        2: punctuation.section.parens.begin.shell
      push:
        - meta_scope: meta.group.expansion.arithmetic.shell
        - match: \)\)
          scope: punctuation.section.parens.end.shell
          pop: true
        - include: expression

  expansion-command:
    - match: (\$)(\()
      captures:
        1: punctuation.definition.variable.shell
        2: punctuation.section.parens.begin.shell
      push:
        - meta_scope: meta.group.expansion.command.parens.shell
        - match: \s*(\))
          captures:
            1: punctuation.section.parens.end.shell
          pop: true
        - include: main
    - match: \`
      scope: punctuation.section.group.begin.shell
      push:
        - meta_scope: meta.group.expansion.command.backticks.shell
        - match: \`
          scope: punctuation.section.group.end.shell
          pop: true
        - include: main-bt # all those *-bt contexts just for this!!!!

  expansion:
    - include: expansion-pattern
    - include: expansion-parameter
    - include: expansion-brace
    - include: expansion-arithmetic
    - include: expansion-command
    - include: expansion-tilde
    - include: expansion-job

  expansion-parameter-common:
    - meta_content_scope: meta.group.expansion.parameter.shell
    - match: \}
      scope:
        meta.group.expansion.parameter.shell
        punctuation.section.expansion.parameter.end.shell
      pop: true
    - include: string
    - include: expansion-parameter
    # no brace expansion
    - include: expansion-arithmetic
    - include: expansion-command
    - include: expansion-tilde
    # no pattern expansion
    - include: any-escape

  array:
    - match: \[
      scope: punctuation.section.braces.begin.shell
      push:
        - match: \]
          scope: punctuation.section.braces.end.shell
          pop: true
        - match: '[*@]'
          scope: variable.language.array.shell
        - include: expression

  expansion-parameter-post-first-character:
    - meta_content_scope:
        meta.group.expansion.parameter.shell
        variable.other.readwrite.shell
    - include: expansion-parameter-common
    - match: (?=[@*]?/)
      set:
        - meta_content_scope: meta.group.expansion.parameter.shell
        - match: ([@*])?(/)
          captures:
            1: variable.language.shell
            2: keyword.operator.substitution.shell
          set:
            - meta_include_prototype: false
            - meta_content_scope: meta.group.expansion.parameter.shell
            - match: '[/#%]'
              scope: variable.parameter.switch.shell
              set: expansion-parameter-pattern
            - match: ""
              set: expansion-parameter-pattern
    - match: (?=\:?[-+=?])
      set:
        - meta_content_scope: meta.group.expansion.parameter.shell
        - match: \:?[-+=?]
          scope: keyword.operator.assignment.shell
          set: expansion-parameter-common
    - match: (?=@?:)
      set:
        - meta_content_scope: meta.group.expansion.parameter.shell
        - match: '(@)?(:)'
          captures:
            1: variable.language.shell
            2: keyword.operator.substring.begin.shell
          set:
            - meta_content_scope: meta.group.expansion.parameter.shell
            - match: (?=:)
              set:
                - meta_content_scope: meta.group.expansion.parameter.shell
                - match: ":"
                  scope: keyword.operator.substring.end.shell
                  set:
                    - meta_content_scope: meta.group.expansion.parameter.shell
                    - include: expression
                    - include: expansion-parameter-common
            - include: expression
            - include: expansion-parameter-common
    - match: \#(?=})
    - match: ([@*])?(\#\#?|%%?|\^\^?|,,?)
      captures:
        1: variable.language.shell
        2: keyword.operator.expansion.shell
      set:
        - meta_include_prototype: false
        - meta_content_scope: meta.group.expansion.parameter.shell
        - include: expansion-parameter-common
        - include: expansion-pattern
    - match: ([@*]?)(@)([QEPAa])(?=})
      captures:
        1: variable.language.shell
        2: keyword.operator.expansion.shell
        3: variable.parameter.switch.shell
    - include: array
    - match: '[*@](?=})'
      scope: variable.language.shell

  expansion-parameter-pattern:
    - meta_content_scope: meta.group.expansion.parameter.shell
    - match: /
      scope: keyword.operator.substitution.shell
      set: expansion-parameter-common
    - include: expansion-parameter-common
    - include: expansion-pattern

  expansion-job:
    # There are a number of ways to refer to a job in the shell.
    # The symbols ‘%%’ and ‘%+’ refer to the shell’s notion of the current job,
    # which is the last job stopped while it was in the foreground or started in
    # the background. The previous job may be referenced using ‘%-’.
    - match: (%)([%+-])
      captures:
        0: meta.group.expansion.job.shell
        1: punctuation.definition.variable.job.shell
        2: variable.language.job.shell
    # The character ‘%’ introduces a job specification (jobspec). Job number n
    # may be referred to as ‘%n’.
    - match: (%)(\d+)
      captures:
        0: meta.group.expansion.job.shell
        1: punctuation.definition.variable.job.shell
        2: constant.numeric.integer.decimal.job.shell
    # A job may also be referred to using a prefix of the name used to start it,
    # or using a substring that appears in its command line. For example, ‘%ce’
    # refers to a stopped ce job. Using ‘%?ce’, on the other hand, refers to any
    # job containing the string ‘ce’ in its command line. If the prefix or
    # substring matches more than one job, Bash reports an error.
    - match: (%)(\??)(\w+)
      captures:
        0: meta.group.expansion.job.shell
        1: punctuation.definition.variable.job.shell
        2: keyword.operator.regexp.quantifier.shell
        3: variable.other.readwrite.shell
    # A single ‘%’ (with no accompanying job specification) also refers to the
    # current job.
    - match: '%'
      scope:
        meta.group.expansion.job.shell
        punctuation.definition.variable.job.shell


  expression:
    # A leading ‘0x’ or ‘0X’ denotes hexadecimal.
    - match: \b0[xX]
      scope: punctuation.definition.numeric.base.shell
      push:
        - meta_scope: constant.numeric.integer.hexadecimal.shell
        - match: '[g-zG-Z]'
          scope: invalid.illegal.not-a-hex-character.shell
          pop: true
        - match: (?=\H)
          pop: true
    # Constants with a leading 0 are interpreted as octal numbers.
    - match: \b0(?=[0-7])
      scope: punctuation.definition.numeric.base.shell
      push:
        - meta_scope: constant.numeric.integer.octal.shell
        - match: '[89]'
          scope: invalid.illegal.not-an-octal-character.shell
          pop: true
        - match: (?=[^0-7])
          pop: true
    # Otherwise, numbers take the form [base#]n, where the optional base is a
    # decimal number between 2 and 64 representing the arithmetic base, and n is
    # a number in that base. When specifying n, the digits greater than 9 are
    # represented by the lowercase letters, the uppercase letters, ‘@’, and ‘_’,
    # in that order.
    - match: \b(\d+#)[a-zA-Z0-9@_]+
      scope: constant.numeric.integer.other.shell
      captures:
        1: punctuation.definition.numeric.base.shell
    # If base# is omitted, then base 10 is used.
    - match: \b\d+
      scope: constant.numeric.integer.decimal.shell
    - match: '[*/%+\-&^|]?=|<<=|>>='
      scope: keyword.operator.assignment.shell
    - match: \+\+?|\-\-?|\*\*?|%|/
      scope: keyword.operator.arithmetic.shell
    - match: <[=<]?|>[=>]?|[=!]=|&&|\:|\|\||!
      scope: keyword.operator.logical.shell
    - match: '[&|^~]'
      scope: keyword.operator.bitwise.shell
    - match: '[,;]'
      scope: punctuation.separator.shell
    - match: \?
      scope: keyword.operator.ternary.shell
    - match: \(
      scope: punctuation.section.parens.begin.shell
      push:
        - meta_scope: meta.group.parens.shell
        - match: \)
          scope: punctuation.section.parens.end.shell
          pop: true
        - include: expression
    # Shell variables are allowed as operands; parameter expansion is performed
    # before the expression is evaluated. Within an expression, shell variables
    # may also be referenced by name without using the parameter expansion
    # syntax.
    - include: string
    - include: expansion-parameter
    - include: expansion-arithmetic
    - include: expansion-command

  expression-test:
    - include: expansion-and-string
    - match: ((-)[aobcdefghknoprstuvwxzGLNORS])(?=\s)
      captures:
        2: punctuation.definition.parameter.shell
        1: variable.parameter.option.shell
    - match: ((-)(?:ef|nt|ot|eq|ne|lt|le|gt|ge))(?=\s)
      captures:
        2: punctuation.definition.parameter.shell
        1: variable.parameter.option.shell
    - match: (=~)\s*
      captures:
        1: keyword.operator.logical.shell
      push:
        - meta_content_scope: meta.regexp.shell
        - match: (?=\s)
          pop: true
        - include: expansion-and-string
    - match: ==?|!=?|<|>|\|\||&&
      scope: keyword.operator.logical.shell

  operator-exclamation:
    - match: \!(?!\S)
      scope: keyword.operator.logical.shell
    - match: (\!)(-?\d+|!)
      scope: variable.language.history.shell
      captures:
        1: punctuation.definition.history.shell
    - match: \!
      scope: punctuation.definition.history.shell

  string:
    - include: string-quoted-double
    - include: string-quoted-single
    - include: string-ansi-c
    - include: string-locale

  # nothing is escaped in a singly-quoted string!
  string-quoted-single:
    - match: \'
      scope: punctuation.definition.string.begin.shell
      push:
        - meta_include_prototype: false
        - meta_scope: string.quoted.single.shell
        - match: \'
          scope: punctuation.definition.string.end.shell
          pop: true

  string-quoted-double:
    - match: \"
      scope: punctuation.definition.string.begin.shell
      push:
        - meta_include_prototype: false
        - meta_scope: string.quoted.double.shell
        - include: string-quoted-double-common

  string-quoted-double-escape-character:
    - match: \\[$`"\\]
      scope: constant.character.escape.shell
    - match: \\\n
      scope: constant.character.escape.shell
      push:
        - meta_include_prototype: false
        - match: (?=\S)
          pop: true

  # [Bash] 3.1.2.4
  string-ansi-c:
    - match: \$'
      scope: punctuation.definition.string.begin.shell
      push:
        - meta_include_prototype: false
        - meta_scope: string.quoted.single.ansi-c.shell
        - match: "'"
          scope: punctuation.definition.string.end.shell
          pop: true
        - include: string-quoted-double-escape-character
        - match: \\([abfnrtv'"?]|[0-8]{1,3}|x\h{1,8}|c[a-z])
          scope: constant.character.escape.shell

  # [Bash] 3.1.2.5
  # If the string is translated and replaced, the replacement is double-quoted.
  string-locale:
    - match: \$"
      scope: punctuation.definition.string.begin.shell
      push:
        - meta_include_prototype: false
        - meta_scope: string.quoted.double.locale.shell
        - include: string-quoted-double-common

  string-quoted-double-common:
    - match: \"
      scope: punctuation.definition.string.end.shell
      pop: true
    - include: string-quoted-double-escape-character
    - include: expansion-parameter
    - include: expansion-arithmetic
    - include: expansion-command

