mirror of
https://git.savannah.gnu.org/git/emacs.git
synced 2024-11-28 07:45:00 +00:00
598505c4fa
* lisp/cmuscheme.el (run-scheme, switch-to-scheme): * lisp/shell.el (shell): * lisp/eshell/eshell.el (eshell): * lisp/progmodes/inf-lisp.el (inferior-lisp): * lisp/progmodes/sh-script.el (sh-show-shell): * lisp/textmodes/tex-mode.el (tex-display-shell) (tex-recenter-output-buffer): Add 'with-suppressed-warnings' to suppress warnings for obsolete options 'display-comint-buffer-action' and 'display-tex-shell-buffer-action'.
3396 lines
120 KiB
EmacsLisp
3396 lines
120 KiB
EmacsLisp
;;; sh-script.el --- shell-script editing commands for Emacs -*- lexical-binding:t -*-
|
||
|
||
;; Copyright (C) 1993-1997, 1999, 2001-2024 Free Software Foundation,
|
||
;; Inc.
|
||
|
||
;; Author: Daniel Pfeiffer <occitan@esperanto.org>
|
||
;; Old-Version: 2.0f
|
||
;; Maintainer: emacs-devel@gnu.org
|
||
;; Keywords: languages, unix
|
||
|
||
;; This file is part of GNU Emacs.
|
||
|
||
;; GNU Emacs is free software: you can redistribute it and/or modify
|
||
;; it under the terms of the GNU General Public License as published by
|
||
;; the Free Software Foundation, either version 3 of the License, or
|
||
;; (at your option) any later version.
|
||
|
||
;; GNU Emacs is distributed in the hope that it will be useful,
|
||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
;; GNU General Public License for more details.
|
||
|
||
;; You should have received a copy of the GNU General Public License
|
||
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
;;; Commentary:
|
||
|
||
;; Major mode for editing shell scripts. Bourne, C and rc shells as well
|
||
;; as various derivatives are supported and easily derived from. Structured
|
||
;; statements can be inserted with one command or abbrev. Completion is
|
||
;; available for filenames, variables known from the script, the shell and
|
||
;; the environment as well as commands.
|
||
|
||
;; A Flymake backend using the "shellcheck" program is provided. See
|
||
;; https://www.shellcheck.net/ for installation instructions.
|
||
|
||
;;; Known Bugs:
|
||
|
||
;; - In Bourne the keyword `in' is not anchored to case, for, select ...
|
||
;; - Variables in `"' strings aren't fontified because there's no way of
|
||
;; syntactically distinguishing those from `'' strings.
|
||
|
||
;; Indentation
|
||
;; ===========
|
||
;; Indentation for rc and es modes is very limited, but for Bourne shells
|
||
;; and its derivatives it is quite customizable.
|
||
;;
|
||
;; The following description applies to sh and derived shells (bash,
|
||
;; zsh, ...).
|
||
;;
|
||
;; There are various customization variables which allow tailoring to
|
||
;; a wide variety of styles. Most of these variables are named
|
||
;; sh-indent-for-XXX and sh-indent-after-XXX. For example.
|
||
;; sh-indent-after-if controls the indenting of a line following
|
||
;; an if statement, and sh-indent-for-fi controls the indentation
|
||
;; of the line containing the fi.
|
||
;;
|
||
;; You can set each to a numeric value, but it is often more convenient
|
||
;; to a symbol such as `+' which uses the value of variable `sh-basic-offset'.
|
||
;; By changing this one variable you can increase or decrease how much
|
||
;; indentation there is. Valid symbols:
|
||
;;
|
||
;; + Indent right by sh-basic-offset
|
||
;; - Indent left by sh-basic-offset
|
||
;; ++ Indent right twice sh-basic-offset
|
||
;; -- Indent left twice sh-basic-offset
|
||
;; * Indent right half sh-basic-offset
|
||
;; / Indent left half sh-basic-offset.
|
||
;;
|
||
;; Saving indentation values
|
||
;; -------------------------
|
||
;; After you've learned the values in a buffer, how to you remember them?
|
||
;; There is a minimal way of being able to save indentation values and
|
||
;; to reload them in another buffer or at another point in time.
|
||
;;
|
||
;; Use `sh-name-style' to give a name to the indentation settings of
|
||
;; the current buffer.
|
||
;; Use `sh-load-style' to load indentation settings for the current
|
||
;; buffer from a specific style.
|
||
;; Use `sh-save-styles-to-buffer' to write all the styles to a buffer
|
||
;; in lisp code. You can then store it in a file and later use
|
||
;; `load-file' to load it.
|
||
;;
|
||
;; Indentation variables - buffer local or global?
|
||
;; ----------------------------------------------
|
||
;; I think that often having them buffer-local makes sense,
|
||
;; especially if one is using `smie-config-guess'. However, if
|
||
;; a user sets values using customization, these changes won't appear
|
||
;; to work if the variables are already local!
|
||
;;
|
||
;; To get round this, there is a variable `sh-make-vars-local' and 2
|
||
;; functions: `sh-make-vars-local' and `sh-reset-indent-vars-to-global-values'.
|
||
;;
|
||
;; If `sh-make-vars-local' is non-nil, then these variables become
|
||
;; buffer local when the mode is established.
|
||
;; If this is nil, then the variables are global. At any time you
|
||
;; can make them local with the command `sh-make-vars-local'.
|
||
;; Conversely, to update with the global values you can use the
|
||
;; command `sh-reset-indent-vars-to-global-values'.
|
||
;;
|
||
;; This may be awkward, but the intent is to cover all cases.
|
||
;;
|
||
;; Awkward things, pitfalls
|
||
;; ------------------------
|
||
;; Indentation for a sh script is complicated for a number of reasons:
|
||
;;
|
||
;; 1. You can't format by simply looking at symbols, you need to look
|
||
;; at keywords. [This is not the case for rc and es shells.]
|
||
;; 2. The character ")" is used both as a matched pair "(" ... ")" and
|
||
;; as a stand-alone symbol (in a case alternative). This makes
|
||
;; things quite tricky!
|
||
;; 3. Here-documents in a script should be treated "as is", and when
|
||
;; they terminate we want to revert to the indentation of the line
|
||
;; containing the "<<" symbol.
|
||
;; 4. A line may be continued using the "\".
|
||
;; 5. The character "#" (outside a string) normally starts a comment,
|
||
;; but it doesn't in the sequence "$#"!
|
||
;;
|
||
;; To try and address points 2 3 and 5 I used a feature that cperl mode
|
||
;; uses, that of a text's syntax property. This, however, has 2
|
||
;; disadvantages:
|
||
;; 1. We need to scan the buffer to find which ")" symbols belong to a
|
||
;; case alternative, to find any here documents, and handle "$#".
|
||
;;
|
||
;; Bugs
|
||
;; ----
|
||
;; - Indenting many lines is slow. It currently does each line
|
||
;; independently, rather than saving state information.
|
||
;;
|
||
;; - "echo $z in ps | head)" the last ) is mis-identified as being part of
|
||
;; a case-pattern. You need to put the "in" between quotes to coerce
|
||
;; sh-script into doing the right thing.
|
||
;;
|
||
;; Richard Sharman <rsharman@pobox.com> June 1999.
|
||
|
||
;;; Code:
|
||
|
||
;; page 1: variables and settings
|
||
;; page 2: indentation stuff
|
||
;; page 3: mode-command and utility functions
|
||
;; page 4: statement syntax-commands for various shells
|
||
;; page 5: various other commands
|
||
|
||
(eval-when-compile
|
||
(require 'skeleton)
|
||
(require 'cl-lib)
|
||
(require 'comint)
|
||
(require 'let-alist)
|
||
(require 'subr-x))
|
||
(require 'executable)
|
||
(require 'treesit)
|
||
|
||
(declare-function treesit-parser-create "treesit.c")
|
||
|
||
(autoload 'comint-completion-at-point "comint")
|
||
(autoload 'comint-filename-completion "comint")
|
||
(autoload 'comint-send-string "comint")
|
||
(autoload 'shell-command-completion "shell")
|
||
(autoload 'shell-environment-variable-completion "shell")
|
||
|
||
|
||
(defgroup sh nil
|
||
"Shell programming utilities."
|
||
:group 'languages)
|
||
|
||
(defgroup sh-script nil
|
||
"Shell script mode."
|
||
:link '(custom-group-link :tag "Font Lock Faces group" font-lock-faces)
|
||
:group 'sh
|
||
:prefix "sh-")
|
||
|
||
|
||
(defcustom sh-ancestor-alist
|
||
'((ash . sh)
|
||
(bash . jsh)
|
||
(bash2 . jsh)
|
||
(dash . ash)
|
||
(dtksh . ksh)
|
||
(es . rc)
|
||
(itcsh . tcsh)
|
||
(jcsh . csh)
|
||
(jsh . sh)
|
||
(ksh . ksh88)
|
||
(ksh88 . jsh)
|
||
(oash . sh)
|
||
(pdksh . ksh88)
|
||
(mksh . pdksh)
|
||
(posix . sh)
|
||
(tcsh . csh)
|
||
(wksh . ksh88)
|
||
(wsh . sh)
|
||
(zsh . ksh88)
|
||
(rpm . sh))
|
||
"Alist showing the direct ancestor of various shells.
|
||
This is the basis for `sh-feature'. See also `sh-alias-alist'.
|
||
By default we have the following three hierarchies:
|
||
|
||
csh C Shell
|
||
jcsh C Shell with Job Control
|
||
tcsh TENEX C Shell
|
||
itcsh Ian's TENEX C Shell
|
||
rc Plan 9 Shell
|
||
es Extensible Shell
|
||
sh Bourne Shell
|
||
ash Almquist Shell
|
||
dash Debian Almquist Shell
|
||
jsh Bourne Shell with Job Control
|
||
bash GNU Bourne Again Shell
|
||
ksh88 Korn Shell '88
|
||
ksh Korn Shell '93
|
||
dtksh CDE Desktop Korn Shell
|
||
pdksh Public Domain Korn Shell
|
||
mksh MirOS BSD Korn Shell
|
||
wksh Window Korn Shell
|
||
zsh Z Shell
|
||
oash SCO OA (curses) Shell
|
||
posix IEEE 1003.2 Shell Standard
|
||
wsh ? Shell"
|
||
:type '(repeat (cons symbol symbol))
|
||
:version "24.4" ; added dash
|
||
:group 'sh-script)
|
||
|
||
(defcustom sh-alias-alist
|
||
(append (if (eq system-type 'gnu/linux)
|
||
'((csh . tcsh)
|
||
(ksh . pdksh)))
|
||
;; for the time being
|
||
'((ksh . ksh88)
|
||
(bash2 . bash)
|
||
(sh5 . sh)
|
||
;; Android's system shell
|
||
("^/system/bin/sh$" . mksh)))
|
||
"Alist for transforming shell names to what they really are.
|
||
Use this where the name of the executable doesn't correspond to
|
||
the type of shell it really is. Keys are regular expressions
|
||
matched against the full path of the interpreter. (For backward
|
||
compatibility, keys may also be symbols, which are matched
|
||
against the interpreter's basename. The values are symbols
|
||
naming the shell."
|
||
:type '(repeat (cons (radio
|
||
(regexp :tag "Regular expression")
|
||
(symbol :tag "Basename"))
|
||
(symbol :tag "Shell")))
|
||
:group 'sh-script)
|
||
|
||
|
||
(defcustom sh-shell-file
|
||
(or
|
||
;; On MSDOS and Windows, collapse $SHELL to lower-case and remove
|
||
;; the executable extension, so comparisons with the list of
|
||
;; known shells work.
|
||
(and (memq system-type '(ms-dos windows-nt))
|
||
(let* ((shell (getenv "SHELL"))
|
||
(shell-base
|
||
(and shell (file-name-nondirectory shell))))
|
||
;; shell-script mode doesn't support DOS/Windows shells,
|
||
;; so use the default instead.
|
||
(if (or (null shell)
|
||
(member (downcase shell-base)
|
||
'("command.com" "cmd.exe" "4dos.com" "ndos.com"
|
||
"cmdproxy.exe")))
|
||
"/bin/sh"
|
||
(file-name-sans-extension (downcase shell)))))
|
||
(getenv "SHELL")
|
||
"/bin/sh")
|
||
"The executable file name for the shell being programmed."
|
||
:type 'string
|
||
:group 'sh-script)
|
||
|
||
|
||
(defcustom sh-shell-arg
|
||
;; bash does not need any options when run in a shell script,
|
||
'((bash)
|
||
(csh . "-f")
|
||
(pdksh)
|
||
;; Bill_Mann@praxisint.com says -p with ksh can do harm.
|
||
(ksh88)
|
||
;; -p means don't initialize functions from the environment.
|
||
(rc . "-p")
|
||
;; Someone proposed -motif, but we don't want to encourage
|
||
;; use of a non-free widget set.
|
||
(wksh)
|
||
;; -f means don't run .zshrc.
|
||
(zsh . "-f"))
|
||
"Single argument string for the magic number. See `sh-feature'."
|
||
:type '(repeat (cons (symbol :tag "Shell")
|
||
(choice (const :tag "No Arguments" nil)
|
||
(string :tag "Arguments")
|
||
(sexp :format "Evaluate: %v"))))
|
||
:group 'sh-script)
|
||
|
||
(defcustom sh-imenu-generic-expression
|
||
`((sh
|
||
. ((nil
|
||
;; function FOO
|
||
;; function FOO()
|
||
"^\\s-*function\\s-+\\([[:alpha:]_][[:alnum:]_]*\\)\\s-*\\(?:()\\)?"
|
||
1)
|
||
;; FOO()
|
||
(nil
|
||
"^\\s-*\\([[:alpha:]_][[:alnum:]_]*\\)\\s-*()"
|
||
1)))
|
||
(mksh
|
||
. ((nil
|
||
;; function FOO
|
||
;; function FOO()
|
||
,(rx bol (* (syntax whitespace)) "function" (+ (syntax whitespace))
|
||
(group (1+ (not (any "\0\t\n \"$&'();<=>\\`|#*?[]/"))))
|
||
(* (syntax whitespace)) (? "()"))
|
||
1)
|
||
(nil
|
||
;; FOO()
|
||
,(rx bol (* (syntax whitespace))
|
||
(group (1+ (not (any "\0\t\n \"$&'();<=>\\`|#*?[]/"))))
|
||
(* (syntax whitespace)) "()")
|
||
1))))
|
||
"Alist of regular expressions for recognizing shell function definitions.
|
||
See `sh-feature' and `imenu-generic-expression'."
|
||
:type '(alist :key-type (symbol :tag "Shell")
|
||
:value-type (alist :key-type (choice :tag "Title"
|
||
string
|
||
(const :tag "None" nil))
|
||
:value-type
|
||
(repeat :tag "Regexp, index..." sexp)))
|
||
:group 'sh-script
|
||
:version "29.1")
|
||
|
||
(defun sh-current-defun-name ()
|
||
"Find the name of function or variable at point.
|
||
For use in `add-log-current-defun-function'."
|
||
(save-excursion
|
||
(end-of-line)
|
||
(when (re-search-backward
|
||
(concat "\\(?:"
|
||
;; function FOO
|
||
;; function FOO()
|
||
"^\\s-*function\\s-+\\([[:alpha:]_][[:alnum:]_]*\\)\\s-*\\(?:()\\)?"
|
||
"\\)\\|\\(?:"
|
||
;; FOO()
|
||
"^\\s-*\\([[:alpha:]_][[:alnum:]_]*\\)\\s-*()"
|
||
"\\)\\|\\(?:"
|
||
;; FOO=
|
||
"^\\([[:alpha:]_][[:alnum:]_]*\\)="
|
||
"\\)")
|
||
nil t)
|
||
(or (match-string-no-properties 1)
|
||
(match-string-no-properties 2)
|
||
(match-string-no-properties 3)))))
|
||
|
||
(defvar sh-shell-variables nil
|
||
"Alist of shell variable names that should be included in completion.
|
||
These are used for completion in addition to all the variables named
|
||
in `process-environment'. Each element looks like (VAR . VAR), where
|
||
the car and cdr are the same symbol.")
|
||
|
||
(defvar sh-shell-variables-initialized nil
|
||
"Non-nil if `sh-shell-variables' is initialized.")
|
||
|
||
(defun sh-canonicalize-shell (shell)
|
||
"Convert a shell name SHELL to the one we should handle it as.
|
||
SHELL is a full path to the shell interpreter; return a shell
|
||
name symbol."
|
||
(cl-loop
|
||
with shell = (cond ((string-match "\\.exe\\'" shell)
|
||
(substring shell 0 (match-beginning 0)))
|
||
(t shell))
|
||
with shell-base = (intern (file-name-nondirectory shell))
|
||
for (key . value) in sh-alias-alist
|
||
if (and (stringp key) (string-match key shell)) return value
|
||
if (eq key shell-base) return value
|
||
finally return shell-base))
|
||
|
||
(defvar sh-shell (sh-canonicalize-shell sh-shell-file)
|
||
"The shell being programmed. This is set by \\[sh-set-shell].")
|
||
;;;###autoload(put 'sh-shell 'safe-local-variable 'symbolp)
|
||
|
||
(define-abbrev-table 'sh-mode-abbrev-table ())
|
||
|
||
|
||
(defun sh-mode-syntax-table (table &rest list)
|
||
"Copy TABLE and set syntax for successive CHARs according to strings S."
|
||
(setq table (copy-syntax-table table))
|
||
(while list
|
||
(modify-syntax-entry (pop list) (pop list) table))
|
||
table)
|
||
|
||
(defvar sh-mode-syntax-table
|
||
(sh-mode-syntax-table ()
|
||
?\# "<"
|
||
?\n ">#"
|
||
?\" "\"\""
|
||
?\' "\"'"
|
||
?\` "\"`"
|
||
;; ?$ might also have a ". p" syntax. Both "'" and ". p" seem
|
||
;; to work fine. This is needed so that dabbrev-expand
|
||
;; $VARNAME works.
|
||
?$ "'"
|
||
?! "_"
|
||
?% "_"
|
||
?: "_"
|
||
?. "_"
|
||
?^ "_"
|
||
?~ "_"
|
||
?, "_"
|
||
?= "."
|
||
?/ "."
|
||
?\; "."
|
||
?| "."
|
||
?& "."
|
||
?< "."
|
||
?> ".")
|
||
"The syntax table to use for Shell-Script mode.
|
||
This is buffer-local in every such buffer.")
|
||
|
||
(defvar sh-mode-syntax-table-input
|
||
`((sh . nil)
|
||
;; Treat ' as punctuation rather than a string delimiter, as RPM
|
||
;; files often contain prose with apostrophes.
|
||
(rpm . (,sh-mode-syntax-table ?\' ".")))
|
||
"Syntax-table used in Shell-Script mode. See `sh-feature'.")
|
||
|
||
(defvar-keymap sh-mode-map
|
||
:doc "Keymap used in Shell-Script mode."
|
||
"C-c (" #'sh-function
|
||
"C-c C-w" #'sh-while
|
||
"C-c C-u" #'sh-until
|
||
"C-c C-t" #'sh-tmp-file
|
||
"C-c C-s" #'sh-select
|
||
"C-c C-r" #'sh-repeat
|
||
"C-c C-o" #'sh-while-getopts
|
||
"C-c C-l" #'sh-indexed-loop
|
||
"C-c C-i" #'sh-if
|
||
"C-c C-f" #'sh-for
|
||
"C-c C-c" #'sh-case
|
||
"C-c ?" #'smie-config-show-indent
|
||
"C-c =" #'smie-config-set-indent
|
||
"C-c <" #'smie-config-set-indent
|
||
"C-c >" #'smie-config-guess
|
||
"C-c C-\\" #'sh-backslash-region
|
||
|
||
"C-c +" #'sh-add
|
||
"C-M-x" #'sh-execute-region
|
||
"C-c C-x" #'executable-interpret
|
||
"C-c C-n" #'sh-send-line-or-region-and-step
|
||
"C-c C-d" #'sh-cd-here
|
||
"C-c C-z" #'sh-show-shell
|
||
"C-c :" #'sh-set-shell
|
||
|
||
"<remap> <delete-backward-char>" #'backward-delete-char-untabify
|
||
"<remap> <backward-sentence>" #'sh-beginning-of-command
|
||
"<remap> <forward-sentence>" #'sh-end-of-command)
|
||
|
||
(easy-menu-define sh-mode-menu sh-mode-map
|
||
"Menu for Shell-Script mode."
|
||
'("Sh-Script"
|
||
["Backslash region" sh-backslash-region
|
||
:help "Insert, align, or delete end-of-line backslashes on the lines in the region"]
|
||
["Set shell type..." sh-set-shell
|
||
:help "Set this buffer's shell to SHELL (a string)"]
|
||
["Execute script..." executable-interpret
|
||
:help "Run script with user-specified args, and collect output in a buffer"]
|
||
["Execute region" sh-execute-region
|
||
:help "Pass optional header and region to a subshell for noninteractive execution"]
|
||
"---"
|
||
;; Insert
|
||
["Case Statement" sh-case
|
||
:help "Insert a case/switch statement"]
|
||
["For Loop" sh-for
|
||
:help "Insert a for loop"]
|
||
["If Statement" sh-if
|
||
:help "Insert an if statement"]
|
||
["Select Statement" sh-select
|
||
:help "Insert a select statement "]
|
||
["Indexed Loop" sh-indexed-loop
|
||
:help "Insert an indexed loop from 1 to n"]
|
||
["Options Loop" sh-while-getopts
|
||
:help "Insert a while getopts loop."]
|
||
["While Loop" sh-while
|
||
:help "Insert a while loop"]
|
||
["Repeat Loop" sh-repeat
|
||
:help "Insert a repeat loop definition"]
|
||
["Until Loop" sh-until
|
||
:help "Insert an until loop"]
|
||
["Addition..." sh-add
|
||
:help "Insert an addition of VAR and prefix DELTA for Bourne (type) shell"]
|
||
["Function..." sh-function
|
||
:help "Insert a function definition"]
|
||
"---"
|
||
;; Other
|
||
["Insert braces and quotes in pairs" electric-pair-mode
|
||
:style toggle
|
||
:selected (bound-and-true-p electric-pair-mode)
|
||
:help "Inserting a brace or quote automatically inserts the matching pair"]
|
||
["Set indentation" smie-config-set-indent
|
||
:help "Set the indentation for the current line"]
|
||
["Show indentation" smie-config-show-indent
|
||
:help "Show the how the current line would be indented"]
|
||
["Learn buffer indentation" smie-config-guess
|
||
:help "Learn how to indent the buffer the way it currently is"]))
|
||
|
||
(defvar sh-skeleton-pair-default-alist '((?\( _ ?\)) (?\))
|
||
(?\[ ?\s _ ?\s ?\]) (?\])
|
||
(?{ _ ?}) (?\}))
|
||
"Value to use for `skeleton-pair-default-alist' in Shell-Script mode.")
|
||
|
||
(defcustom sh-dynamic-complete-functions
|
||
'(shell-environment-variable-completion
|
||
shell-command-completion
|
||
comint-filename-completion)
|
||
"Functions for doing TAB dynamic completion."
|
||
:type '(repeat function)
|
||
:group 'sh-script)
|
||
|
||
(defcustom sh-assignment-regexp
|
||
;; The "\\[.+\\]" matches the "[index]" in "arrayvar[index]=value".
|
||
`((csh . "\\<\\([[:alnum:]_]+\\)\\(\\[.+\\]\\)?[ \t]*[-+*/%^]?=")
|
||
;; actually spaces are only supported in let/(( ... ))
|
||
(ksh88 . ,(concat "\\<\\([[:alnum:]_]+\\)\\(\\[.+\\]\\)?"
|
||
"[ \t]*\\(?:[-+*/%&|~^]\\|<<\\|>>\\)?="))
|
||
(bash . "\\<\\([[:alnum:]_]+\\)\\(\\[.+\\]\\)?\\+?=")
|
||
(rc . "\\<\\([[:alnum:]_*]+\\)[ \t]*=")
|
||
(sh . "\\<\\([[:alnum:]_]+\\)="))
|
||
"Regexp for the variable name and what may follow in an assignment.
|
||
First grouping matches the variable name. This is up to and including the `='
|
||
sign. See `sh-feature'."
|
||
:type '(repeat (cons (symbol :tag "Shell")
|
||
(choice regexp
|
||
(sexp :format "Evaluate: %v"))))
|
||
:group 'sh-script)
|
||
|
||
(define-obsolete-variable-alias 'sh-indentation 'sh-basic-offset "26.1")
|
||
(put 'sh-indentation 'safe-local-variable 'integerp)
|
||
|
||
(defcustom sh-remember-variable-min 3
|
||
"Don't remember variables less than this length for completing reads."
|
||
:type 'integer
|
||
:group 'sh-script)
|
||
|
||
|
||
(defvar-local sh-header-marker nil
|
||
"When non-nil is the end of header for prepending by \\[sh-execute-region].
|
||
That command is also used for setting this variable.")
|
||
|
||
(defcustom sh-beginning-of-command
|
||
"\\([;({`|&]\\|\\`\\|[^\\]\n\\)[ \t]*\\([/~[:alnum:]:]\\)"
|
||
"Regexp to determine the beginning of a shell command.
|
||
The actual command starts at the beginning of the second \\(grouping\\)."
|
||
:type 'regexp
|
||
:group 'sh-script)
|
||
|
||
|
||
(defcustom sh-end-of-command
|
||
"\\([/~[:alnum:]:]\\)[ \t]*\\([;#)}`|&]\\|$\\)"
|
||
"Regexp to determine the end of a shell command.
|
||
The actual command ends at the end of the first \\(grouping\\)."
|
||
:type 'regexp
|
||
:group 'sh-script)
|
||
|
||
|
||
|
||
(defcustom sh-here-document-word "EOF"
|
||
"Word to delimit here documents.
|
||
If the first character of this string is \"-\", this is taken as
|
||
part of the redirection operator, rather than part of the
|
||
word (that is, \"<<-\" instead of \"<<\"). This is a feature
|
||
used by some shells (for example Bash) to indicate that leading
|
||
tabs inside the here document should be ignored. In this case,
|
||
Emacs indents the initial body and end of the here document with
|
||
tabs, to the same level as the start (note that apart from this
|
||
there is no support for indentation of here documents). This
|
||
will only work correctly if `sh-basic-offset' is a multiple of
|
||
`tab-width'.
|
||
|
||
Any quote characters or leading whitespace in the word are
|
||
removed when closing the here document."
|
||
:type 'string
|
||
:group 'sh-script)
|
||
|
||
|
||
(defvar sh-test
|
||
'((sh "[ ]" . 3)
|
||
(ksh88 "[[ ]]" . 4))
|
||
"Initial input in Bourne if, while and until skeletons. See `sh-feature'.")
|
||
|
||
|
||
;; customized this out of sheer bravado. not for the faint of heart.
|
||
;; but it *did* have an asterisk in the docstring!
|
||
(defcustom sh-builtins
|
||
'((bash sh-append posix
|
||
"." "alias" "bg" "bind" "builtin" "caller" "compgen" "complete"
|
||
"declare" "dirs" "disown" "enable" "fc" "fg" "help" "history"
|
||
"jobs" "kill" "let" "local" "popd" "printf" "pushd" "shopt"
|
||
"source" "suspend" "typeset" "unalias"
|
||
;; bash4
|
||
"mapfile" "readarray" "coproc")
|
||
|
||
;; The next entry is only used for defining the others
|
||
(bourne sh-append shell
|
||
"eval" "export" "getopts" "newgrp" "pwd" "read" "readonly"
|
||
"times" "ulimit")
|
||
|
||
(csh sh-append shell
|
||
"alias" "chdir" "glob" "history" "limit" "nice" "nohup" "rehash"
|
||
"setenv" "source" "time" "unalias" "unhash")
|
||
|
||
(dtksh sh-append wksh)
|
||
|
||
(es "access" "apids" "cd" "echo" "eval" "false" "let" "limit" "local"
|
||
"newpgrp" "result" "time" "umask" "var" "vars" "wait" "whatis")
|
||
|
||
(jsh sh-append sh
|
||
"bg" "fg" "jobs" "kill" "stop" "suspend")
|
||
|
||
(jcsh sh-append csh
|
||
"bg" "fg" "jobs" "kill" "notify" "stop" "suspend")
|
||
|
||
(ksh88 sh-append bourne
|
||
"alias" "bg" "false" "fc" "fg" "jobs" "kill" "let" "print" "time"
|
||
"typeset" "unalias" "whence")
|
||
|
||
(oash sh-append sh
|
||
"checkwin" "dateline" "error" "form" "menu" "newwin" "oadeinit"
|
||
"oaed" "oahelp" "oainit" "pp" "ppfile" "scan" "scrollok" "wattr"
|
||
"wclear" "werase" "win" "wmclose" "wmmessage" "wmopen" "wmove"
|
||
"wmtitle" "wrefresh")
|
||
|
||
(pdksh sh-append ksh88
|
||
"bind")
|
||
|
||
(posix sh-append sh
|
||
"command")
|
||
|
||
(rc "builtin" "cd" "echo" "eval" "limit" "newpgrp" "shift" "umask" "wait"
|
||
"whatis")
|
||
|
||
(sh sh-append bourne
|
||
"hash" "test" "type")
|
||
|
||
;; The next entry is only used for defining the others
|
||
(shell "cd" "echo" "eval" "set" "shift" "umask" "unset" "wait")
|
||
|
||
(wksh sh-append ksh88)
|
||
|
||
(zsh sh-append ksh88
|
||
"autoload" "always"
|
||
"bindkey" "builtin" "chdir" "compctl" "declare" "dirs"
|
||
"disable" "disown" "echotc" "enable" "functions" "getln" "hash"
|
||
"history" "integer" "limit" "local" "log" "popd" "pushd" "r"
|
||
"readonly" "rehash" "sched" "setopt" "source" "suspend" "true"
|
||
"ttyctl" "type" "unfunction" "unhash" "unlimit" "unsetopt" "vared"
|
||
"which"))
|
||
"List of all shell builtins for completing read and fontification.
|
||
Note that on some systems not all builtins are available or some are
|
||
implemented as aliases. See `sh-feature'."
|
||
:type '(repeat (cons (symbol :tag "Shell")
|
||
(choice (repeat string)
|
||
(sexp :format "Evaluate: %v"))))
|
||
:version "24.4" ; bash4 additions
|
||
:group 'sh-script)
|
||
|
||
(defcustom sh-indent-statement-after-and t
|
||
"How to indent statements following && in Shell-Script mode.
|
||
If t, indent to align with &&.
|
||
If nil, indent to align with the previous line's indentation."
|
||
:type 'boolean
|
||
:version "29.1")
|
||
|
||
(defcustom sh-leading-keywords
|
||
'((bash sh-append sh
|
||
"time")
|
||
|
||
(csh "else")
|
||
|
||
(es "true" "unwind-protect" "whatis")
|
||
|
||
(rc "else")
|
||
|
||
(sh "!" "do" "elif" "else" "if" "then" "trap" "type" "until" "while"))
|
||
"List of keywords that may be immediately followed by a builtin or keyword.
|
||
Given some confusion between keywords and builtins depending on shell and
|
||
system, the distinction here has been based on whether they influence the
|
||
flow of control or syntax. See `sh-feature'."
|
||
:type '(repeat (cons (symbol :tag "Shell")
|
||
(choice (repeat string)
|
||
(sexp :format "Evaluate: %v"))))
|
||
:group 'sh-script)
|
||
|
||
|
||
(defcustom sh-other-keywords
|
||
'((bash sh-append bourne
|
||
"bye" "logout" "select")
|
||
|
||
;; The next entry is only used for defining the others
|
||
(bourne sh-append sh
|
||
"function")
|
||
|
||
(csh sh-append shell
|
||
"breaksw" "default" "end" "endif" "endsw" "foreach" "goto"
|
||
"if" "logout" "onintr" "repeat" "switch" "then" "while")
|
||
|
||
(es "break" "catch" "exec" "exit" "fn" "for" "forever" "fork" "if"
|
||
"return" "throw" "while")
|
||
|
||
(ksh88 sh-append bourne
|
||
"select")
|
||
|
||
(rc "break" "case" "exec" "exit" "fn" "for" "if" "in" "return" "switch"
|
||
"while")
|
||
|
||
(sh sh-append shell
|
||
"done" "esac" "fi" "for" "in" "return")
|
||
|
||
;; The next entry is only used for defining the others
|
||
(shell "break" "case" "continue" "exec" "exit")
|
||
|
||
(zsh sh-append bash
|
||
"select" "foreach"))
|
||
"List of keywords not in `sh-leading-keywords'.
|
||
See `sh-feature'."
|
||
:type '(repeat (cons (symbol :tag "Shell")
|
||
(choice (repeat string)
|
||
(sexp :format "Evaluate: %v"))))
|
||
:group 'sh-script)
|
||
|
||
|
||
|
||
(defvar sh-variables
|
||
'((bash sh-append sh
|
||
"allow_null_glob_expansion" "auto_resume" "BASH" "BASH_ENV"
|
||
"BASH_VERSINFO" "BASH_VERSION" "cdable_vars" "COMP_CWORD"
|
||
"COMP_LINE" "COMP_POINT" "COMP_WORDS" "COMPREPLY" "DIRSTACK"
|
||
"ENV" "EUID" "FCEDIT" "FIGNORE" "FUNCNAME"
|
||
"glob_dot_filenames" "GLOBIGNORE" "GROUPS" "histchars"
|
||
"HISTCMD" "HISTCONTROL" "HISTFILE" "HISTFILESIZE"
|
||
"HISTIGNORE" "history_control" "HISTSIZE"
|
||
"hostname_completion_file" "HOSTFILE" "HOSTTYPE" "IGNOREEOF"
|
||
"ignoreeof" "INPUTRC" "LINENO" "MACHTYPE" "MAIL_WARNING"
|
||
"noclobber" "nolinks" "notify" "no_exit_on_failed_exec"
|
||
"NO_PROMPT_VARS" "OLDPWD" "OPTERR" "OSTYPE" "PIPESTATUS"
|
||
"PPID" "POSIXLY_CORRECT" "PROMPT_COMMAND" "PS3" "PS4"
|
||
"pushd_silent" "PWD" "RANDOM" "REPLY" "SECONDS" "SHELLOPTS"
|
||
"SHLVL" "TIMEFORMAT" "TMOUT" "UID")
|
||
|
||
(csh sh-append shell
|
||
"argv" "cdpath" "child" "echo" "histchars" "history" "home"
|
||
"ignoreeof" "mail" "noclobber" "noglob" "nonomatch" "path" "prompt"
|
||
"shell" "status" "time" "verbose")
|
||
|
||
(es sh-append shell
|
||
"apid" "cdpath" "CDPATH" "history" "home" "ifs" "noexport" "path"
|
||
"pid" "prompt" "signals")
|
||
|
||
(jcsh sh-append csh
|
||
"notify")
|
||
|
||
(ksh88 sh-append sh
|
||
"ENV" "ERRNO" "FCEDIT" "FPATH" "HISTFILE" "HISTSIZE" "LINENO"
|
||
"OLDPWD" "PPID" "PS3" "PS4" "PWD" "RANDOM" "REPLY" "SECONDS"
|
||
"TMOUT")
|
||
|
||
(oash sh-append sh
|
||
"FIELD" "FIELD_MAX" "LAST_KEY" "OALIB" "PP_ITEM" "PP_NUM")
|
||
|
||
(rc sh-append shell
|
||
"apid" "apids" "cdpath" "CDPATH" "history" "home" "ifs" "path" "pid"
|
||
"prompt" "status")
|
||
|
||
(sh sh-append shell
|
||
"CDPATH" "IFS" "OPTARG" "OPTIND" "PS1" "PS2")
|
||
|
||
;; The next entry is only used for defining the others
|
||
(shell "COLUMNS" "EDITOR" "HOME" "HUSHLOGIN" "LANG" "LC_COLLATE"
|
||
"LC_CTYPE" "LC_MESSAGES" "LC_MONETARY" "LC_NUMERIC" "LC_TIME"
|
||
"LINES" "LOGNAME" "MAIL" "MAILCHECK" "MAILPATH" "PAGER" "PATH"
|
||
"SHELL" "TERM" "TERMCAP" "TERMINFO" "VISUAL")
|
||
|
||
(tcsh sh-append csh
|
||
"addsuffix" "ampm" "autocorrect" "autoexpand" "autolist"
|
||
"autologout" "chase_symlinks" "correct" "dextract" "edit" "el"
|
||
"fignore" "gid" "histlit" "HOST" "HOSTTYPE" "HPATH"
|
||
"ignore_symlinks" "listjobs" "listlinks" "listmax" "matchbeep"
|
||
"nobeep" "NOREBIND" "oid" "printexitvalue" "prompt2" "prompt3"
|
||
"pushdsilent" "pushdtohome" "recexact" "recognize_only_executables"
|
||
"rmstar" "savehist" "SHLVL" "showdots" "sl" "SYSTYPE" "tcsh" "term"
|
||
"tperiod" "tty" "uid" "version" "visiblebell" "watch" "who"
|
||
"wordchars")
|
||
|
||
(zsh sh-append ksh88
|
||
"BAUD" "bindcmds" "cdpath" "DIRSTACKSIZE" "fignore" "FIGNORE" "fpath"
|
||
"HISTCHARS" "hostcmds" "hosts" "HOSTS" "LISTMAX" "LITHISTSIZE"
|
||
"LOGCHECK" "mailpath" "manpath" "NULLCMD" "optcmds" "path" "POSTEDIT"
|
||
"prompt" "PROMPT" "PROMPT2" "PROMPT3" "PROMPT4" "psvar" "PSVAR"
|
||
"READNULLCMD" "REPORTTIME" "RPROMPT" "RPS1" "SAVEHIST" "SPROMPT"
|
||
"STTY" "TIMEFMT" "TMOUT" "TMPPREFIX" "varcmds" "watch" "WATCH"
|
||
"WATCHFMT" "WORDCHARS" "ZDOTDIR"))
|
||
"List of all shell variables available for completing read.
|
||
See `sh-feature'.")
|
||
|
||
|
||
;; Font-Lock support
|
||
|
||
(defface sh-heredoc
|
||
'((((min-colors 88) (class color)
|
||
(background dark))
|
||
(:foreground "yellow1" :weight bold))
|
||
(((class color)
|
||
(background dark))
|
||
(:foreground "yellow" :weight bold))
|
||
(((class color)
|
||
(background light))
|
||
(:foreground "tan1" ))
|
||
(t
|
||
(:weight bold)))
|
||
"Face to show a here-document."
|
||
:group 'sh-indentation)
|
||
|
||
;; These colors are probably icky. It's just a placeholder though.
|
||
(defface sh-quoted-exec
|
||
'((((class color) (background dark))
|
||
(:foreground "salmon"))
|
||
(((class color) (background light))
|
||
(:foreground "magenta"))
|
||
(t
|
||
(:weight bold)))
|
||
"Face to show quoted execs like \\=`blabla\\=`."
|
||
:group 'sh-indentation)
|
||
|
||
(defface sh-escaped-newline '((t :inherit font-lock-string-face))
|
||
"Face used for (non-escaped) backslash at end of a line in Shell-script mode."
|
||
:group 'sh-script
|
||
:version "22.1")
|
||
|
||
(defvar sh-font-lock-keywords-var
|
||
'((csh sh-append shell
|
||
("\\${?[#?]?\\([[:alpha:]_][[:alnum:]_]*\\|0\\)" 1
|
||
font-lock-variable-name-face))
|
||
|
||
(es sh-append executable-font-lock-keywords
|
||
("\\$#?\\([[:alpha:]_][[:alnum:]_]*\\|[0-9]+\\)" 1
|
||
font-lock-variable-name-face))
|
||
|
||
(rc sh-append es)
|
||
(bash sh-append sh ("\\$(\\([^)\n]+\\)" (1 'sh-quoted-exec t) ))
|
||
(sh sh-append shell
|
||
;; Variable names.
|
||
("\\$\\({#?\\)?\\([[:alpha:]_][[:alnum:]_]*\\|[-#?@!]\\)" 2
|
||
font-lock-variable-name-face)
|
||
;; Function names.
|
||
("^\\(\\sw+\\)[ \t]*(" 1 font-lock-function-name-face)
|
||
("\\<\\(function\\)\\>[ \t]*\\(\\sw+\\)?"
|
||
(1 font-lock-keyword-face) (2 font-lock-function-name-face nil t))
|
||
("\\(?:^\\s *\\|[[();&|]\\s *\\|\\(?:\\s +-[ao]\\|if\\|else\\|then\\|while\\|do\\)\\s +\\)\\(!\\)"
|
||
1 font-lock-negation-char-face))
|
||
|
||
;; The next entry is only used for defining the others
|
||
(shell
|
||
;; Using font-lock-string-face here confuses sh-get-indent-info.
|
||
("\\(^\\|[^\\]\\)\\(\\\\\\\\\\)*\\(\\\\\\)$" 3 'sh-escaped-newline)
|
||
("\\\\[^[:alnum:]]" 0 font-lock-string-face)
|
||
("\\${?\\([[:alpha:]_][[:alnum:]_]*\\|[0-9]+\\|[$*_]\\)" 1
|
||
font-lock-variable-name-face))
|
||
(rpm sh-append rpm2
|
||
("^\\s-*%\\(\\sw+\\)" 1 font-lock-keyword-face)
|
||
("%{?\\([!?]*[[:alpha:]_][[:alnum:]_]*\\|[0-9]+\\|[%*#]\\*?\\|!?-[[:alpha:]]\\*?\\)"
|
||
1 font-lock-variable-name-face))
|
||
(rpm2 sh-append shell
|
||
("^Summary:\\(.*\\)$" (1 font-lock-doc-face t))
|
||
("^\\(\\sw+\\)\\((\\(\\sw+\\))\\)?:" (1 font-lock-variable-name-face)
|
||
(3 font-lock-string-face nil t))))
|
||
"Default expressions to highlight in Shell Script modes. See `sh-feature'.")
|
||
|
||
(defvar sh-font-lock-keywords-var-1
|
||
'((sh "[ \t]\\(in\\|do\\)\\>"))
|
||
"Subdued level highlighting for Shell Script modes.")
|
||
|
||
(defvar sh-font-lock-keywords-var-2 ()
|
||
"Gaudy level highlighting for Shell Script modes.")
|
||
|
||
;; These are used for the syntax table stuff (derived from cperl-mode).
|
||
;; Note: parse-sexp-lookup-properties must be set to t for it to work.
|
||
(defconst sh-st-punc (string-to-syntax "."))
|
||
(defconst sh-here-doc-syntax (string-to-syntax "|")) ;; generic string
|
||
|
||
(eval-and-compile
|
||
(defconst sh-escaped-line-re
|
||
;; Should match until the real end-of-continued-line, but if that is not
|
||
;; possible (because we bump into EOB or the search bound), then we should
|
||
;; match until the search bound.
|
||
"\\(?:\\(?:.*[^\\\n]\\)?\\(?:\\\\\\\\\\)*\\\\\n\\)*.*")
|
||
|
||
(defconst sh-here-doc-open-re
|
||
(concat "[^<]<<-?\\s-*\\\\?\\(\\(?:['\"][^'\"]+['\"]\\|\\sw\\|[-/~._@]\\)+\\)"
|
||
sh-escaped-line-re "\\(\n\\)")))
|
||
|
||
(defun sh--inside-noncommand-expression (pos)
|
||
(save-excursion
|
||
(let ((ppss (syntax-ppss pos)))
|
||
(when (nth 1 ppss)
|
||
(goto-char (nth 1 ppss))
|
||
(or
|
||
(pcase (char-after)
|
||
;; ((...)) or $((...)) or $[...] or ${...}. Nested
|
||
;; parenthesis can occur inside the first of these forms, so
|
||
;; parse backward recursively.
|
||
(?\( (eq ?\( (char-before)))
|
||
((or ?\{ ?\[) (eq ?\$ (char-before))))
|
||
(sh--inside-noncommand-expression (1- (point))))))))
|
||
|
||
(defun sh-font-lock-open-heredoc (start string eol)
|
||
"Determine the syntax of the \\n after a <<EOF.
|
||
START is the position of <<.
|
||
STRING is the actual word used as delimiter (e.g. \"EOF\").
|
||
INDENTED is non-nil if the here document's content (and the EOF mark) can
|
||
be indented (i.e. a <<- was used rather than just <<).
|
||
Point is at the beginning of the next line."
|
||
(unless (or (memq (char-before start) '(?< ?>))
|
||
(sh-in-comment-or-string start)
|
||
(sh--inside-noncommand-expression start))
|
||
;; We're looking at <<STRING, so we add "^STRING$" to the syntactic
|
||
;; font-lock keywords to detect the end of this here document.
|
||
(let ((str (replace-regexp-in-string "['\"]" "" string))
|
||
(ppss (save-excursion (syntax-ppss eol))))
|
||
(if (nth 4 ppss)
|
||
;; The \n not only starts the heredoc but also closes a comment.
|
||
;; Let's close the comment just before the \n.
|
||
(put-text-property (1- eol) eol 'syntax-table '(12))) ;">"
|
||
(if (or (nth 5 ppss) (> (count-lines start eol) 1))
|
||
;; If the sh-escaped-line-re part of sh-here-doc-open-re has matched
|
||
;; several lines, make sure we refontify them together.
|
||
;; Furthermore, if (nth 5 ppss) is non-nil (i.e. the \n is
|
||
;; escaped), it means the right \n is actually further down.
|
||
;; Don't bother fixing it now, but place a multiline property so
|
||
;; that when jit-lock-context-* refontifies the rest of the
|
||
;; buffer, it also refontifies the current line with it.
|
||
(put-text-property start (1+ eol) 'syntax-multiline t))
|
||
(put-text-property eol (1+ eol) 'sh-here-doc-marker str)
|
||
(prog1 sh-here-doc-syntax
|
||
(goto-char (+ 2 start))))))
|
||
|
||
(defun sh-syntax-propertize-here-doc (end)
|
||
(let ((ppss (syntax-ppss)))
|
||
(when (eq t (nth 3 ppss))
|
||
(let ((key (get-text-property (nth 8 ppss) 'sh-here-doc-marker))
|
||
(case-fold-search nil))
|
||
(when (re-search-forward
|
||
(concat "^\\([ \t]*\\)" (regexp-quote key) "\\(\n\\)")
|
||
end 'move)
|
||
(let ((eol (match-beginning 2)))
|
||
(put-text-property eol (1+ eol)
|
||
'syntax-table sh-here-doc-syntax)))))))
|
||
|
||
(defun sh-font-lock-quoted-subshell (limit)
|
||
"Search for a subshell embedded in a string.
|
||
Find all the unescaped \" characters within said subshell, remembering that
|
||
subshells can nest."
|
||
(when (eq ?\" (nth 3 (syntax-ppss))) ; Check we matched an opening quote.
|
||
;; bingo we have a $( or a ` inside a ""
|
||
(let (;; `state' can be: double-quote, backquote, code.
|
||
(state (if (eq (char-before) ?`) 'backquote 'code))
|
||
(startpos (point))
|
||
;; Stacked states in the context.
|
||
(states '(double-quote)))
|
||
(while (and state (progn (skip-chars-forward "^'\\\\\"`$()" limit)
|
||
(< (point) limit)))
|
||
;; unescape " inside a $( ... ) construct.
|
||
(pcase (char-after)
|
||
(?\' (pcase state
|
||
('double-quote nil)
|
||
(_ (forward-char 1)
|
||
;; FIXME: mark skipped double quotes as punctuation syntax.
|
||
(let ((spos (point)))
|
||
(skip-chars-forward "^'" limit)
|
||
(save-excursion
|
||
(let ((epos (point)))
|
||
(goto-char spos)
|
||
(while (search-forward "\"" epos t)
|
||
(put-text-property (point) (1- (point))
|
||
'syntax-table '(1)))))))))
|
||
(?\\ (forward-char 1))
|
||
(?\" (pcase state
|
||
('double-quote (setq state (pop states)))
|
||
(_ (push state states) (setq state 'double-quote)))
|
||
(if state (put-text-property (point) (1+ (point))
|
||
'syntax-table '(1))))
|
||
(?\` (pcase state
|
||
('backquote (setq state (pop states)))
|
||
(_ (push state states) (setq state 'backquote))))
|
||
(?\$ (if (not (eq (char-after (1+ (point))) ?\())
|
||
nil
|
||
(forward-char 1)
|
||
(pcase state
|
||
(_ (push state states) (setq state 'code)))))
|
||
(?\( (pcase state
|
||
('double-quote nil)
|
||
(_ (push state states) (setq state 'code))))
|
||
(?\) (pcase state
|
||
('double-quote nil)
|
||
(_ (setq state (pop states)))))
|
||
(_ (error "Internal error in sh-font-lock-quoted-subshell")))
|
||
(forward-char 1))
|
||
(when (< startpos (line-beginning-position))
|
||
(put-text-property startpos (point) 'syntax-multiline t)
|
||
(add-hook 'syntax-propertize-extend-region-functions
|
||
#'syntax-propertize-multiline nil t))
|
||
)))
|
||
|
||
|
||
(defun sh-is-quoted-p (pos)
|
||
(and (eq (char-before pos) ?\\)
|
||
(not (sh-is-quoted-p (1- pos)))))
|
||
|
||
(defun sh-font-lock-paren (start)
|
||
(unless (nth 8 (syntax-ppss))
|
||
(save-excursion
|
||
(let ((open nil))
|
||
(goto-char start)
|
||
;; Skip through all patterns
|
||
(while
|
||
(progn
|
||
(while
|
||
(progn
|
||
(forward-comment (- (point-max)))
|
||
(when (and (eolp) (sh-is-quoted-p (point)))
|
||
(forward-char -1)
|
||
t)))
|
||
;; Skip through one pattern
|
||
(while
|
||
(or (/= 0 (skip-syntax-backward "w_"))
|
||
(/= 0 (skip-chars-backward "-$=?[]*@/\\\\"))
|
||
(and (sh-is-quoted-p (1- (point)))
|
||
(goto-char (- (point) 2)))
|
||
(when (memq (char-before) '(?\" ?\' ?\}))
|
||
(condition-case nil (progn (backward-sexp 1) t)
|
||
(error nil)))))
|
||
;; Patterns can be preceded by an open-paren (bug#1320).
|
||
(when (eq (char-before (point)) ?\()
|
||
(backward-char 1)
|
||
(setq open (point)))
|
||
(while (progn
|
||
(forward-comment (- (point-max)))
|
||
;; Maybe we've bumped into an escaped newline.
|
||
(sh-is-quoted-p (point)))
|
||
(backward-char 1))
|
||
(when (and
|
||
(eq (char-before) ?|)
|
||
(not (eq (char-before (1- (point))) ?\;)))
|
||
(backward-char 1) t)))
|
||
(and (> (point) (1+ (point-min)))
|
||
(progn (backward-char 2)
|
||
(if (> start (line-end-position))
|
||
(put-text-property (point) (1+ start)
|
||
'syntax-multiline t))
|
||
;; FIXME: The `in' may just be a random argument to
|
||
;; a normal command rather than the real `in' keyword.
|
||
;; I.e. we should look back to try and find the
|
||
;; corresponding `case'.
|
||
;; Also recognize OpenBSD's case X { ... } (bug#55764).
|
||
(and (looking-at ";\\(?:;&?\\|[&|]\\)\\|\\_<in\\|.{")
|
||
;; ";; esac )" is a case that looks
|
||
;; like a case-pattern but it's really just a close
|
||
;; paren after a case statement. I.e. if we skipped
|
||
;; over `esac' just now, we're not looking
|
||
;; at a case-pattern.
|
||
(not (looking-at "..[ \t\n]+esac[^[:word:]_]"))))
|
||
(progn
|
||
(when open
|
||
(put-text-property open (1+ open) 'syntax-table sh-st-punc))
|
||
sh-st-punc))))))
|
||
|
||
(defun sh-font-lock-backslash-quote ()
|
||
(if (eq (save-excursion (nth 3 (syntax-ppss (match-beginning 0)))) ?\')
|
||
;; In a '...' the backslash is not escaping.
|
||
sh-st-punc
|
||
nil))
|
||
|
||
(defun sh-syntax-propertize-function (start end)
|
||
(goto-char start)
|
||
(sh-syntax-propertize-here-doc end)
|
||
(funcall
|
||
(syntax-propertize-rules
|
||
(sh-here-doc-open-re
|
||
(2 (sh-font-lock-open-heredoc
|
||
(1+ (match-beginning 0)) (match-string 1) (match-beginning 2))))
|
||
("\\s|" (0 (prog1 nil (sh-syntax-propertize-here-doc end))))
|
||
;; A `#' begins a comment when it is unquoted and at the
|
||
;; beginning of a word. In the shell, words are separated by
|
||
;; metacharacters. The list of special chars is taken from
|
||
;; the single-unix spec of the shell command language (under
|
||
;; `quoting') but with `$' removed. Also -- if there's something like
|
||
;; \ #foo, then that's not a comment, unless the backslash itself
|
||
;; is backslashed.
|
||
("\\(?:[^|&;<>(`\\\"' \t\n]\\|\\${\\|\\(?:[^\\]\\|^\\)\\\\\\(?:\\\\\\\\\\)*.\\)\\(#+\\)" (1 "_"))
|
||
;; In addition, `#' at the beginning of closed parentheses
|
||
;; does not start a comment if the parentheses are not isolated
|
||
;; by metacharacters, excluding [()].
|
||
;; (e.g. `foo(#q/)' and `(#b)foo' in zsh)
|
||
("[^|&;<>(`\\\"' \t\n](\\(#+\\)" (1 "_"))
|
||
("(\\(#\\)[^)]+?)[^|&;<>)`\\\"' \t\n]" (1 "_"))
|
||
;; In a '...' the backslash is not escaping.
|
||
("\\(\\\\\\)'" (1 (sh-font-lock-backslash-quote)))
|
||
;; Make sure $@ and $? are correctly recognized as sexps.
|
||
("\\$\\([?@]\\)" (1 "_"))
|
||
;; Distinguish the special close-paren in `case'.
|
||
(")" (0 (sh-font-lock-paren (match-beginning 0))))
|
||
;; Highlight (possibly nested) subshells inside "" quoted
|
||
;; regions correctly.
|
||
("\"\\(?:[^\\\"]\\|\\\\.\\)*?\\(\\$(\\|`\\)"
|
||
(1 (ignore
|
||
(if (nth 8 (save-excursion (syntax-ppss (match-beginning 0))))
|
||
(goto-char (1+ (match-beginning 0)))
|
||
;; Save excursion because we want to also apply other
|
||
;; syntax-propertize rules within the affected region.
|
||
(save-excursion
|
||
(sh-font-lock-quoted-subshell end)))))))
|
||
(point) end))
|
||
(defun sh-font-lock-syntactic-face-function (state)
|
||
(let ((q (nth 3 state)))
|
||
(if q
|
||
(if (characterp q)
|
||
(if (eq q ?\`) 'sh-quoted-exec font-lock-string-face)
|
||
'sh-heredoc)
|
||
font-lock-comment-face)))
|
||
|
||
(defgroup sh-indentation nil
|
||
"Variables controlling indentation in shell scripts.
|
||
|
||
Note: customizing these variables will not affect existing buffers if
|
||
`sh-make-vars-local' is non-nil. See the documentation for
|
||
variable `sh-make-vars-local', command `sh-make-vars-local'
|
||
and command `sh-reset-indent-vars-to-global-values'."
|
||
:group 'sh-script)
|
||
|
||
|
||
(defcustom sh-set-shell-hook nil
|
||
"Hook run by `sh-set-shell'."
|
||
:type 'hook
|
||
:group 'sh-script)
|
||
|
||
(defcustom sh-mode-hook '(sh-electric-here-document-mode)
|
||
"Hook run by `sh-mode'."
|
||
:type 'hook
|
||
:options '(sh-electric-here-document-mode)
|
||
:group 'sh-script)
|
||
|
||
(defcustom sh-popup-occur-buffer nil
|
||
"Controls when `smie-config-guess' pops the `*indent*' buffer.
|
||
If t it is always shown. If nil, it is shown only when there
|
||
are conflicts."
|
||
:type '(choice
|
||
(const :tag "Only when there are conflicts." nil)
|
||
(const :tag "Always" t))
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-first-lines-indent 0
|
||
"The indentation of the first non-blank non-comment line.
|
||
Usually 0 meaning first column.
|
||
Can be set to a number, or to nil which means leave it as is."
|
||
:type '(choice
|
||
(const :tag "Leave as is" nil)
|
||
(integer :tag "Column number"
|
||
:menu-tag "Indent to this col (0 means first col)" ))
|
||
:group 'sh-indentation)
|
||
|
||
|
||
(defcustom sh-basic-offset 4
|
||
"The default indentation increment.
|
||
This value is used for the `+' and `-' symbols in an indentation variable."
|
||
:type 'integer
|
||
:safe #'integerp
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-comment t
|
||
"How a comment line is to be indented.
|
||
nil means leave it as it is;
|
||
t means indent it as a normal line, aligning it to previous non-blank
|
||
non-comment line;
|
||
a number means align to that column, e.g. 0 means first column."
|
||
:type '(choice
|
||
(const :tag "Leave as is." nil)
|
||
(const :tag "Indent as a normal line." t)
|
||
(integer :menu-tag "Indent to this col (0 means first col)."
|
||
:tag "Indent to column number.") )
|
||
:version "24.3"
|
||
:group 'sh-indentation)
|
||
|
||
|
||
(defvar sh-debug nil
|
||
"Enable lots of debug messages - if function `sh-debug' is enabled.")
|
||
|
||
|
||
;; Uncomment this defun and comment the defmacro for debugging.
|
||
;; (defun sh-debug (&rest args)
|
||
;; "For debugging: display message ARGS if variable SH-DEBUG is non-nil."
|
||
;; (if sh-debug
|
||
;; (apply 'message args)))
|
||
(defmacro sh-debug (&rest _args))
|
||
|
||
(defconst sh-symbol-list
|
||
'((const :tag "+ " :value +
|
||
:menu-tag "+ Indent right by sh-basic-offset")
|
||
(const :tag "- " :value -
|
||
:menu-tag "- Indent left by sh-basic-offset")
|
||
(const :tag "++" :value ++
|
||
:menu-tag "++ Indent right twice sh-basic-offset")
|
||
(const :tag "--" :value --
|
||
:menu-tag "-- Indent left twice sh-basic-offset")
|
||
(const :tag "* " :value *
|
||
:menu-tag "* Indent right half sh-basic-offset")
|
||
(const :tag "/ " :value /
|
||
:menu-tag "/ Indent left half sh-basic-offset")))
|
||
|
||
(defcustom sh-indent-for-else 0
|
||
"How much to indent an `else' relative to its `if'. Usually 0."
|
||
:type `(choice
|
||
(integer :menu-tag "A number (positive=>indent right)"
|
||
:tag "A number")
|
||
(const :tag "--") ;; separator!
|
||
,@ sh-symbol-list
|
||
)
|
||
:group 'sh-indentation)
|
||
|
||
(defconst sh-number-or-symbol-list
|
||
(append '((integer :menu-tag "A number (positive=>indent right)"
|
||
:tag "A number")
|
||
(const :tag "--")) ; separator
|
||
sh-symbol-list))
|
||
|
||
(defcustom sh-indent-for-fi 0
|
||
"How much to indent a `fi' relative to its `if'. Usually 0."
|
||
:type `(choice ,@ sh-number-or-symbol-list )
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-for-done 0
|
||
"How much to indent a `done' relative to its matching stmt. Usually 0."
|
||
:type `(choice ,@ sh-number-or-symbol-list )
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-after-else '+
|
||
"How much to indent a statement after an `else' statement."
|
||
:type `(choice ,@ sh-number-or-symbol-list )
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-after-if '+
|
||
"How much to indent a statement after an `if' statement.
|
||
This includes lines after `else' and `elif' statements, too, but
|
||
does not affect the `else', `elif' or `fi' statements themselves."
|
||
:type `(choice ,@ sh-number-or-symbol-list )
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-for-then 0
|
||
"How much to indent a `then' relative to its `if'."
|
||
:type `(choice ,@ sh-number-or-symbol-list )
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-for-do 0
|
||
"How much to indent a `do' statement.
|
||
This is relative to the statement before the `do', typically a
|
||
`while', `until', `for', `repeat' or `select' statement."
|
||
:type `(choice ,@ sh-number-or-symbol-list)
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-after-do '+
|
||
"How much to indent a line after a `do' statement.
|
||
This is used when the `do' is the first word of the line.
|
||
This is relative to the statement before the `do', typically a
|
||
`while', `until', `for', `repeat' or `select' statement."
|
||
:type `(choice ,@ sh-number-or-symbol-list)
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-after-loop-construct '+
|
||
"How much to indent a statement after a loop construct.
|
||
|
||
This variable is used when the keyword `do' is on the same line as the
|
||
loop statement (e.g., `until', `while' or `for').
|
||
If the `do' is on a line by itself, then `sh-indent-after-do' is used instead."
|
||
:type `(choice ,@ sh-number-or-symbol-list)
|
||
:group 'sh-indentation)
|
||
|
||
|
||
(defcustom sh-indent-after-done 0
|
||
"How much to indent a statement after a `done' keyword.
|
||
Normally this is 0, which aligns the `done' to the matching
|
||
looping construct line.
|
||
Setting it non-zero allows you to have the `do' statement on a line
|
||
by itself and align the done under to do."
|
||
:type `(choice ,@ sh-number-or-symbol-list)
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-for-case-label '+
|
||
"How much to indent a case label statement.
|
||
This is relative to the line containing the `case' statement."
|
||
:type `(choice ,@ sh-number-or-symbol-list)
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-for-case-alt '++
|
||
"How much to indent statements after the case label.
|
||
This is relative to the line containing the `case' statement."
|
||
:type `(choice ,@ sh-number-or-symbol-list)
|
||
:group 'sh-indentation)
|
||
|
||
|
||
(defcustom sh-indent-for-continuation '+
|
||
"How much to indent for a continuation statement."
|
||
:type `(choice ,@ sh-number-or-symbol-list)
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-after-open '+
|
||
"How much to indent after a line with an opening parenthesis or brace.
|
||
For an open paren after a function, `sh-indent-after-function' is used."
|
||
:type `(choice ,@ sh-number-or-symbol-list)
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-after-function '+
|
||
"How much to indent after a function line."
|
||
:type `(choice ,@ sh-number-or-symbol-list)
|
||
:group 'sh-indentation)
|
||
|
||
;; These 2 are for the rc shell:
|
||
|
||
(defcustom sh-indent-after-switch '+
|
||
"How much to indent a `case' statement relative to the `switch' statement.
|
||
This is for the rc shell."
|
||
:type `(choice ,@ sh-number-or-symbol-list)
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-indent-after-case '+
|
||
"How much to indent a statement relative to the `case' statement.
|
||
This is for the rc shell."
|
||
:type `(choice ,@ sh-number-or-symbol-list)
|
||
:group 'sh-indentation)
|
||
|
||
(defcustom sh-backslash-column 48
|
||
"Column in which `sh-backslash-region' inserts backslashes."
|
||
:type 'integer
|
||
:group 'sh)
|
||
|
||
(defcustom sh-backslash-align t
|
||
"If non-nil, `sh-backslash-region' will align backslashes."
|
||
:type 'boolean
|
||
:group 'sh)
|
||
|
||
;; Internal use - not designed to be changed by the user:
|
||
|
||
(defun sh-mkword-regexpr (word)
|
||
"Make a regexp which matches WORD as a word.
|
||
This specifically excludes an occurrence of WORD followed by
|
||
punctuation characters like `-'."
|
||
(concat word "\\([^-[:alnum:]_]\\|$\\)"))
|
||
|
||
(defconst sh-re-done (sh-mkword-regexpr "done"))
|
||
|
||
|
||
(defconst sh-kws-for-done
|
||
'((sh . ( "while" "until" "for" ) )
|
||
(bash . ( "while" "until" "for" "select" ) )
|
||
(ksh88 . ( "while" "until" "for" "select" ) )
|
||
(zsh . ( "while" "until" "for" "repeat" "select" ) ) )
|
||
"Which keywords can match the word `done' in this shell.")
|
||
|
||
|
||
(defconst sh-indent-supported
|
||
'((sh . sh)
|
||
(csh . nil)
|
||
(rc . rc))
|
||
"Indentation rule set to use for each shell type.")
|
||
|
||
(defvar sh-indent-supported-here nil
|
||
"Non-nil if we support indentation for the current buffer's shell type.")
|
||
|
||
(defconst sh-var-list
|
||
'(
|
||
sh-basic-offset sh-first-lines-indent sh-indent-after-case
|
||
sh-indent-after-do sh-indent-after-done
|
||
sh-indent-after-else
|
||
sh-indent-after-if
|
||
sh-indent-after-loop-construct
|
||
sh-indent-after-open
|
||
sh-indent-comment
|
||
sh-indent-for-case-alt
|
||
sh-indent-for-case-label
|
||
sh-indent-for-continuation
|
||
sh-indent-for-do
|
||
sh-indent-for-done
|
||
sh-indent-for-else
|
||
sh-indent-for-fi
|
||
sh-indent-for-then
|
||
)
|
||
"A list of variables used by script mode to control indentation.
|
||
This list is used when switching between buffer-local and global
|
||
values of variables, and for the commands using indentation styles.")
|
||
|
||
(defvar sh-make-vars-local t
|
||
"Controls whether indentation variables are local to the buffer.
|
||
If non-nil, indentation variables are made local initially.
|
||
If nil, you can later make the variables local by invoking
|
||
command `sh-make-vars-local'.
|
||
The default is t because I assume that in one Emacs session one is
|
||
frequently editing existing scripts with different styles.")
|
||
|
||
|
||
;; inferior shell interaction
|
||
;; TODO: support multiple interactive shells
|
||
(defvar-local sh-shell-process nil
|
||
"The inferior shell process for interaction.")
|
||
|
||
(defvar explicit-shell-file-name)
|
||
|
||
(defun sh-shell-process (force)
|
||
"Get a shell process for interaction.
|
||
If FORCE is non-nil and no process found, create one."
|
||
(if (process-live-p sh-shell-process)
|
||
sh-shell-process
|
||
(setq sh-shell-process
|
||
(let ((found nil) proc
|
||
(procs (process-list)))
|
||
(while (and (not found) procs
|
||
(process-live-p (setq proc (pop procs)))
|
||
(process-command proc))
|
||
(when (string-equal sh-shell (file-name-nondirectory
|
||
(car (process-command proc))))
|
||
(setq found proc)))
|
||
(or found
|
||
(and force
|
||
(get-buffer-process
|
||
(let ((explicit-shell-file-name sh-shell-file)
|
||
(display-buffer-overriding-action
|
||
'(nil . ((inhibit-same-window . t)))))
|
||
;; We must prevent this `(shell)' call from
|
||
;; switching buffers, so that the variable
|
||
;; `sh-shell-process' is set locally in the
|
||
;; correct buffer.
|
||
(save-current-buffer
|
||
(shell))))))))))
|
||
|
||
(defun sh-show-shell ()
|
||
"Pop the shell interaction buffer."
|
||
(interactive)
|
||
(with-suppressed-warnings ((obsolete display-comint-buffer-action))
|
||
(pop-to-buffer (process-buffer (sh-shell-process t)) display-comint-buffer-action)))
|
||
|
||
(defun sh-send-text (text)
|
||
"Send TEXT to `sh-shell-process'."
|
||
(comint-send-string (sh-shell-process t) (concat text "\n")))
|
||
|
||
(defun sh-cd-here ()
|
||
"Change directory in the current interaction shell to the current one."
|
||
(interactive)
|
||
(sh-send-text (concat "cd " default-directory)))
|
||
|
||
(defun sh-send-line-or-region-and-step ()
|
||
"Send the current line to the inferior shell and step to the next line.
|
||
When the region is active, send the region instead."
|
||
(interactive)
|
||
(let (from to end)
|
||
(if (use-region-p)
|
||
(setq from (region-beginning)
|
||
to (region-end)
|
||
end to)
|
||
(setq from (line-beginning-position)
|
||
to (line-end-position)
|
||
end (1+ to)))
|
||
(sh-send-text (buffer-substring-no-properties from to))
|
||
(goto-char end)))
|
||
|
||
|
||
;; mode-command and utility functions
|
||
|
||
(defun sh-after-hack-local-variables ()
|
||
(when (assq 'sh-shell file-local-variables-alist)
|
||
(sh-set-shell (if (symbolp sh-shell)
|
||
(symbol-name sh-shell)
|
||
sh-shell))))
|
||
|
||
(defvar sh-mode--treesit-settings)
|
||
|
||
(defun sh--guess-shell ()
|
||
"Guess the shell used in the current buffer.
|
||
Return the name of the shell suitable for `sh-set-shell'."
|
||
(cond ((save-excursion
|
||
(goto-char (point-min))
|
||
(looking-at auto-mode-interpreter-regexp))
|
||
(match-string 2))
|
||
((not buffer-file-name) sh-shell-file)
|
||
;; Checks that use `buffer-file-name' follow.
|
||
((string-match "\\.m?spec\\'" buffer-file-name) "rpm")
|
||
((string-match "[.]sh\\>" buffer-file-name) "sh")
|
||
((string-match "[.]bash\\(rc\\)?\\>" buffer-file-name) "bash")
|
||
((string-match "[.]ksh\\>" buffer-file-name) "ksh")
|
||
((string-match "[.]mkshrc\\>" buffer-file-name) "mksh")
|
||
((string-match "[.]t?csh\\(rc\\)?\\>" buffer-file-name) "csh")
|
||
((string-match "[.]zsh\\(rc\\|env\\)?\\>" buffer-file-name) "zsh")
|
||
((equal (file-name-nondirectory buffer-file-name) ".profile") "sh")
|
||
((equal (file-name-nondirectory buffer-file-name) "PKGBUILD") "bash")
|
||
(t sh-shell-file)))
|
||
|
||
;;;###autoload
|
||
(define-derived-mode sh-base-mode prog-mode "Shell-script"
|
||
"Generic major mode for editing shell scripts.
|
||
|
||
This is a generic major mode intended to be inherited by concrete
|
||
implementations. Currently there are two: `sh-mode' and
|
||
`bash-ts-mode'."
|
||
(make-local-variable 'sh-shell-file)
|
||
(make-local-variable 'sh-shell)
|
||
|
||
(setq-local skeleton-pair-default-alist
|
||
sh-skeleton-pair-default-alist)
|
||
|
||
(setq-local paragraph-start (concat page-delimiter "\\|$"))
|
||
(setq-local paragraph-separate (concat paragraph-start "\\|#!/"))
|
||
(setq-local comment-start "# ")
|
||
(setq-local comment-start-skip "#+[\t ]*")
|
||
(setq-local local-abbrev-table sh-mode-abbrev-table)
|
||
(setq-local comint-dynamic-complete-functions
|
||
sh-dynamic-complete-functions)
|
||
(add-hook 'completion-at-point-functions #'comint-completion-at-point nil t)
|
||
;; we can't look if previous line ended with `\'
|
||
(setq-local comint-prompt-regexp "^[ \t]*")
|
||
(setq-local imenu-case-fold-search nil)
|
||
(setq-local syntax-propertize-function #'sh-syntax-propertize-function)
|
||
(add-hook 'syntax-propertize-extend-region-functions
|
||
#'syntax-propertize-multiline 'append 'local)
|
||
(setq-local skeleton-pair-alist '((?` _ ?`)))
|
||
(setq-local skeleton-pair-filter-function #'sh-quoted-p)
|
||
(setq-local skeleton-further-elements
|
||
'((< '(- (min sh-basic-offset (current-column))))))
|
||
(setq-local skeleton-filter-function #'sh-feature)
|
||
(setq-local skeleton-newline-indent-rigidly t)
|
||
(setq-local defun-prompt-regexp
|
||
(concat
|
||
"^\\("
|
||
"\\(function[ \t]\\)?[ \t]*[[:alnum:]_]+[ \t]*([ \t]*)"
|
||
"\\|"
|
||
"function[ \t]+[[:alnum:]_]+[ \t]*\\(([ \t]*)\\)?"
|
||
"\\)[ \t]*"))
|
||
(setq-local add-log-current-defun-function #'sh-current-defun-name)
|
||
(add-hook 'completion-at-point-functions
|
||
#'sh-completion-at-point-function nil t)
|
||
(setq-local outline-regexp "###")
|
||
(setq-local escaped-string-quote
|
||
(lambda (terminator)
|
||
(if (eq terminator ?')
|
||
"'\\'"
|
||
"\\"))))
|
||
|
||
;;;###autoload
|
||
(define-derived-mode sh-mode sh-base-mode "Shell-script"
|
||
"Major mode for editing shell scripts.
|
||
This mode works for many shells, since they all have roughly the same syntax,
|
||
as far as commands, arguments, variables, pipes, comments etc. are concerned.
|
||
Unless the file's magic number indicates the shell, your usual shell is
|
||
assumed. Since filenames rarely give a clue, they are not further analyzed.
|
||
|
||
This mode adapts to the variations between shells (see `sh-set-shell') by
|
||
means of an inheritance based feature lookup (see `sh-feature'). This
|
||
mechanism applies to all variables (including skeletons) that pertain to
|
||
shell-specific features. Shell script files can use the `sh-shell' local
|
||
variable to indicate the shell variant to be used for the file.
|
||
|
||
The default style of this mode is that of Rosenblatt's Korn shell book.
|
||
The syntax of the statements varies with the shell being used. The
|
||
following commands are available, based on the current shell's syntax:
|
||
\\<sh-mode-map>
|
||
\\[sh-case] case statement
|
||
\\[sh-for] for loop
|
||
\\[sh-function] function definition
|
||
\\[sh-if] if statement
|
||
\\[sh-indexed-loop] indexed loop from 1 to n
|
||
\\[sh-while-getopts] while getopts loop
|
||
\\[sh-repeat] repeat loop
|
||
\\[sh-select] select loop
|
||
\\[sh-until] until loop
|
||
\\[sh-while] while loop
|
||
|
||
For sh and rc shells indentation commands are:
|
||
\\[smie-config-show-indent] Show the rules controlling this line's indentation.
|
||
\\[smie-config-set-indent] Change the rules controlling this line's indentation.
|
||
\\[smie-config-guess] Try to tweak the indentation rules so the
|
||
buffer indents as it currently is indented.
|
||
|
||
|
||
\\[backward-delete-char-untabify] Delete backward one position, even if it was a tab.
|
||
\\[sh-end-of-command] Go to end of successive commands.
|
||
\\[sh-beginning-of-command] Go to beginning of successive commands.
|
||
\\[sh-set-shell] Set this buffer's shell, and maybe its magic number.
|
||
\\[sh-execute-region] Have optional header and region be executed in a subshell.
|
||
|
||
`sh-electric-here-document-mode' controls whether insertion of two
|
||
unquoted < insert a here document. You can control this behavior by
|
||
modifying `sh-mode-hook'.
|
||
|
||
If you generally program a shell different from your login shell you can
|
||
set `sh-shell-file' accordingly. If your shell's file name doesn't correctly
|
||
indicate what shell it is use `sh-alias-alist' to translate.
|
||
|
||
If your shell gives error messages with line numbers, you can use \\[executable-interpret]
|
||
with your script for an edit-interpret-debug cycle."
|
||
(setq font-lock-defaults
|
||
`((sh-font-lock-keywords
|
||
sh-font-lock-keywords-1 sh-font-lock-keywords-2)
|
||
nil nil
|
||
((?/ . "w") (?~ . "w") (?. . "w") (?- . "w") (?_ . "w")) nil
|
||
(font-lock-syntactic-face-function
|
||
. ,#'sh-font-lock-syntactic-face-function)))
|
||
;; Parse or insert magic number for exec, and set all variables depending
|
||
;; on the shell thus determined.
|
||
(sh-set-shell (sh--guess-shell) nil nil)
|
||
(add-hook 'flymake-diagnostic-functions #'sh-shellcheck-flymake nil t)
|
||
(add-hook 'hack-local-variables-hook
|
||
#'sh-after-hack-local-variables nil t))
|
||
|
||
;;;###autoload
|
||
(defalias 'shell-script-mode 'sh-mode)
|
||
|
||
;;;###autoload
|
||
(define-derived-mode bash-ts-mode sh-base-mode "Bash"
|
||
"Major mode for editing Bash shell scripts.
|
||
This mode automatically falls back to `sh-mode' if the buffer is
|
||
not written in Bash or sh."
|
||
:syntax-table sh-mode-syntax-table
|
||
(when (treesit-ready-p 'bash)
|
||
(sh-set-shell "bash" nil nil)
|
||
(add-hook 'flymake-diagnostic-functions #'sh-shellcheck-flymake nil t)
|
||
(add-hook 'hack-local-variables-hook
|
||
#'sh-after-hack-local-variables nil t)
|
||
(treesit-parser-create 'bash)
|
||
(setq-local treesit-font-lock-feature-list
|
||
'(( comment function)
|
||
( command declaration-command keyword string)
|
||
( builtin-variable constant heredoc number
|
||
string-interpolation variable)
|
||
( bracket delimiter misc-punctuation operator)))
|
||
(setq-local treesit-font-lock-settings
|
||
sh-mode--treesit-settings)
|
||
(setq-local treesit-thing-settings
|
||
`((bash
|
||
(sentence ,(regexp-opt '("comment"
|
||
"heredoc_start"
|
||
"heredoc_body"))))))
|
||
(setq-local treesit-defun-type-regexp "function_definition")
|
||
(treesit-major-mode-setup)))
|
||
|
||
(derived-mode-add-parents 'bash-ts-mode '(sh-mode))
|
||
|
||
(advice-add 'bash-ts-mode :around #'sh--redirect-bash-ts-mode
|
||
;; Give it lower precedence than normal advice, so other
|
||
;; advices take precedence over it.
|
||
'((depth . 50)))
|
||
|
||
(defvar sh--redirect-recursing nil)
|
||
(defun sh--redirect-bash-ts-mode (oldfn)
|
||
"Redirect to `sh-mode' if the current file is not written in Bash or sh.
|
||
OLDFN should be `bash-ts-mode'."
|
||
(let ((sh--redirect-recursing sh--redirect-recursing))
|
||
(funcall (if (or delay-mode-hooks sh--redirect-recursing)
|
||
oldfn
|
||
(setq sh--redirect-recursing t)
|
||
(if (member (file-name-base (sh--guess-shell)) '("bash" "sh"))
|
||
oldfn
|
||
#'sh-mode)))))
|
||
|
||
(defun sh-font-lock-keywords (&optional keywords)
|
||
"Function to get simple fontification based on `sh-font-lock-keywords'.
|
||
This adds rules for comments and assignments."
|
||
(sh-feature sh-font-lock-keywords-var
|
||
(when (stringp (sh-feature sh-assignment-regexp))
|
||
(lambda (list)
|
||
`((,(sh-feature sh-assignment-regexp)
|
||
1 font-lock-variable-name-face)
|
||
,@keywords
|
||
,@list
|
||
,@executable-font-lock-keywords)))))
|
||
|
||
(defun sh-font-lock-keywords-1 (&optional builtins)
|
||
"Function to get better fontification including keywords."
|
||
(let ((keywords (concat "\\([;(){}`|&]\\|^\\)[ \t]*\\(\\("
|
||
(regexp-opt (sh-feature sh-leading-keywords) t)
|
||
"[ \t]+\\)?"
|
||
(regexp-opt (append (sh-feature sh-leading-keywords)
|
||
(sh-feature sh-other-keywords))
|
||
t))))
|
||
(sh-font-lock-keywords
|
||
`(,@(if builtins
|
||
`((,(concat keywords "[ \t]+\\)?"
|
||
(regexp-opt (sh-feature sh-builtins) t)
|
||
"\\>")
|
||
(2 font-lock-keyword-face nil t)
|
||
(6 font-lock-builtin-face))
|
||
,@(sh-feature sh-font-lock-keywords-var-2)))
|
||
(,(concat keywords "\\)\\>")
|
||
2 font-lock-keyword-face)
|
||
,@(sh-feature sh-font-lock-keywords-var-1)))))
|
||
|
||
(defun sh-font-lock-keywords-2 ()
|
||
"Function to get better fontification including keywords and builtins."
|
||
(sh-font-lock-keywords-1 t))
|
||
|
||
;;; Completion
|
||
|
||
(defvar sh--completion-keywords '("if" "while" "until" "for" "then"))
|
||
|
||
(defun sh--vars-before-point ()
|
||
(save-excursion
|
||
(let ((vars ()))
|
||
(while (re-search-backward "^[ \t]*\\([[:alnum:]_]+\\)=" nil t)
|
||
(push (match-string 1) vars))
|
||
vars)))
|
||
|
||
;; (defun sh--var-completion-table (string pred action)
|
||
;; (complete-with-action action (sh--vars-before-point) string pred))
|
||
|
||
(defun sh--cmd-completion-table-gen (string)
|
||
(append (when (fboundp 'imenu--make-index-alist)
|
||
(mapcar #'car
|
||
(condition-case nil
|
||
(imenu--make-index-alist)
|
||
(imenu-unavailable nil))))
|
||
(mapcar (lambda (v) (concat v "="))
|
||
(sh--vars-before-point))
|
||
(locate-file-completion-table
|
||
exec-path exec-suffixes string nil t)
|
||
sh--completion-keywords))
|
||
|
||
(defun sh-completion-at-point-function ()
|
||
(save-excursion
|
||
(skip-chars-forward "[:alnum:]_")
|
||
(let ((end (point))
|
||
(_ (skip-chars-backward "[:alnum:]_"))
|
||
(start (point)))
|
||
(cond
|
||
((eq (char-before) ?$)
|
||
(list start end (sh--vars-before-point)
|
||
:company-kind (lambda (_) 'variable)))
|
||
((sh-smie--keyword-p)
|
||
(list start end
|
||
(completion-table-with-cache #'sh--cmd-completion-table-gen)
|
||
:company-kind
|
||
(lambda (s)
|
||
(cond
|
||
((member s sh--completion-keywords) 'keyword)
|
||
((string-suffix-p "=" s) 'variable)
|
||
(t 'function)))))))))
|
||
|
||
;;; Indentation and navigation with SMIE.
|
||
|
||
(require 'smie)
|
||
|
||
(defun sh-smie--keyword-p ()
|
||
"Non-nil if we're at a keyword position.
|
||
A keyword position is one where if we're looking at something that looks
|
||
like a keyword, then it is a keyword."
|
||
(let ((prev (funcall smie-backward-token-function)))
|
||
(if (zerop (length prev))
|
||
(looking-back "\\`\\|\\s(" (1- (point)))
|
||
(assoc prev smie-grammar))))
|
||
|
||
(defun sh-smie--newline-semi-p (&optional tok)
|
||
"Return non-nil if a newline should be treated as a semi-colon.
|
||
Here we assume that a newline should be treated as a semi-colon unless it
|
||
comes right after a special keyword.
|
||
This function does not pay attention to line-continuations.
|
||
If TOK is nil, point should be before the newline; otherwise, TOK is the token
|
||
before the newline and in that case point should be just before the token."
|
||
(save-excursion
|
||
(unless tok
|
||
(setq tok (funcall smie-backward-token-function)))
|
||
(if (and (zerop (length tok))
|
||
(looking-back "\\s(" (1- (point))))
|
||
nil
|
||
(not (numberp (nth 2 (assoc tok smie-grammar)))))))
|
||
|
||
;;;; SMIE support for `sh'.
|
||
|
||
(defconst sh-smie-sh-grammar
|
||
(smie-prec2->grammar
|
||
(smie-bnf->prec2
|
||
'((exp) ;A constant, or a $var, or a sequence of them...
|
||
(cmd ("case" exp "in" branches "esac")
|
||
("if" cmd "then" cmd "fi")
|
||
("if" cmd "then" cmd "else" cmd "fi")
|
||
("if" cmd "then" cmd "elif" cmd "then" cmd "fi")
|
||
("if" cmd "then" cmd "elif" cmd "then" cmd "else" cmd "fi")
|
||
("if" cmd "then" cmd "elif" cmd "then" cmd
|
||
"elif" cmd "then" cmd "else" cmd "fi")
|
||
("while" cmd "do" cmd "done")
|
||
("until" cmd "do" cmd "done")
|
||
("for" exp "in" cmd "do" cmd "done")
|
||
("for" exp "do" cmd "done")
|
||
("select" exp "in" cmd "do" cmd "done") ;bash&zsh&ksh88.
|
||
("repeat" exp "do" cmd "done") ;zsh.
|
||
(exp "always" exp) ;zsh.
|
||
(cmd "|" cmd) (cmd "|&" cmd)
|
||
(cmd "&&" cmd) (cmd "||" cmd)
|
||
(cmd ";" cmd) (cmd "&" cmd))
|
||
(rpattern (rpattern "|" rpattern))
|
||
(pattern (rpattern) ("case-(" rpattern))
|
||
(branches (branches ";;" branches)
|
||
(branches ";&" branches) (branches ";;&" branches) ;bash.
|
||
(branches ";|" branches) ;zsh.
|
||
(pattern "case-)" cmd)))
|
||
'((assoc ";;" ";&" ";;&" ";|"))
|
||
'((assoc ";" "&") (assoc "&&" "||") (assoc "|" "|&")))))
|
||
|
||
(defconst sh-smie--sh-operators
|
||
(delq nil (mapcar (lambda (x)
|
||
(setq x (car x))
|
||
(and (stringp x)
|
||
(not (string-match "\\`[a-z]" x))
|
||
x))
|
||
sh-smie-sh-grammar)))
|
||
|
||
(defconst sh-smie--sh-operators-re (regexp-opt sh-smie--sh-operators))
|
||
(defconst sh-smie--sh-operators-back-re
|
||
(concat "\\(?:^\\|[^\\]\\)\\(?:\\\\\\\\\\)*"
|
||
"\\(" sh-smie--sh-operators-re "\\)"))
|
||
|
||
(defun sh-smie--sh-keyword-in/do-p (tok)
|
||
"When looking at TOK (either \"in\" or \"do\"), non-nil if TOK is a keyword.
|
||
Does not preserve point."
|
||
(let ((forward-sexp-function nil)
|
||
(words nil) ;We've seen words.
|
||
(newline nil) ;We've seen newlines after the words.
|
||
(res nil)
|
||
prev)
|
||
(while (not res)
|
||
(setq prev (funcall smie-backward-token-function))
|
||
(cond
|
||
((zerop (length prev))
|
||
(cond
|
||
(newline (cl-assert words) (setq res 'word))
|
||
((bobp) (setq res 'word))
|
||
(t
|
||
(setq words t)
|
||
(condition-case nil
|
||
(forward-sexp -1)
|
||
(scan-error (setq res 'unknown))))))
|
||
((equal prev ";")
|
||
(if words (setq newline t)
|
||
(setq res 'keyword)))
|
||
((member prev (if (string= tok "in")
|
||
'("case" "for" "select")
|
||
'("for" "select")))
|
||
(setq res 'keyword))
|
||
((assoc prev smie-grammar) (setq res 'word))
|
||
(t
|
||
(if newline
|
||
(progn (cl-assert words) (setq res 'word))
|
||
(setq words t)))))
|
||
(eq res 'keyword)))
|
||
|
||
(defun sh-smie--sh-keyword-p (tok)
|
||
"Non-nil if TOK (at which we're looking) really is a keyword."
|
||
(cond
|
||
((looking-at "[[:alnum:]_]+=") nil)
|
||
((member tok '("in" "do")) (sh-smie--sh-keyword-in/do-p tok))
|
||
(t (sh-smie--keyword-p))))
|
||
|
||
(defun sh-smie--default-forward-token ()
|
||
(forward-comment (point-max))
|
||
(buffer-substring-no-properties
|
||
(point)
|
||
(progn (if (zerop (skip-syntax-forward "."))
|
||
(while (progn (skip-syntax-forward "w_'")
|
||
(looking-at "\\\\"))
|
||
(forward-char 2)))
|
||
(point))))
|
||
|
||
(defun sh-smie--default-backward-token ()
|
||
(forward-comment (- (point)))
|
||
(let ((pos (point))
|
||
(n (skip-syntax-backward ".")))
|
||
(if (or (zerop n)
|
||
(and (eq n -1)
|
||
;; Skip past quoted white space.
|
||
(let ((p (point)))
|
||
(if (eq -1 (% (skip-syntax-backward "\\") 2))
|
||
t
|
||
(goto-char p)
|
||
nil))))
|
||
(while
|
||
(progn
|
||
;; Skip past words, but stop at semicolons.
|
||
(while (and (not (zerop (skip-syntax-backward "w_'")))
|
||
(not (eq (char-before (point)) ?\;))
|
||
(skip-syntax-backward ".")))
|
||
(or (not (zerop (skip-syntax-backward "\\")))
|
||
;; Skip past quoted white space.
|
||
(when (eq ?\\ (char-before (1- (point))))
|
||
(let ((p (point)))
|
||
(forward-char -1)
|
||
(if (eq -1 (% (skip-syntax-backward "\\") 2))
|
||
t
|
||
(goto-char p)
|
||
nil))))))
|
||
(goto-char (- (point) (% (skip-syntax-backward "\\") 2))))
|
||
(buffer-substring-no-properties (point) pos)))
|
||
|
||
(defun sh-smie-sh-forward-token ()
|
||
(if (and (looking-at "[ \t]*\\(?:#\\|\\(\\s|\\)\\|$\\)")
|
||
(save-excursion
|
||
(skip-chars-backward " \t")
|
||
(not (bolp))))
|
||
(if (and (match-end 1) (not (nth 3 (syntax-ppss))))
|
||
;; Right before a here-doc.
|
||
(let ((forward-sexp-function nil))
|
||
(forward-sexp 1)
|
||
;; Pretend the here-document is a "newline representing a
|
||
;; semi-colon", since the here-doc otherwise covers the newline(s).
|
||
";")
|
||
(unless (eobp)
|
||
(let ((semi (sh-smie--newline-semi-p)))
|
||
(forward-line 1)
|
||
(if (or semi (eobp)) ";"
|
||
(sh-smie-sh-forward-token)))))
|
||
(forward-comment (point-max))
|
||
(cond
|
||
((looking-at "\\\\\n") (forward-line 1) (sh-smie-sh-forward-token))
|
||
((looking-at sh-smie--sh-operators-re)
|
||
(goto-char (match-end 0))
|
||
(let ((tok (match-string-no-properties 0)))
|
||
(if (and (memq (aref tok (1- (length tok))) '(?\; ?\& ?\|))
|
||
(looking-at "[ \t]*\\(?:#\\|$\\)"))
|
||
(forward-line 1))
|
||
tok))
|
||
(t
|
||
(let* ((pos (point))
|
||
(tok (sh-smie--default-forward-token)))
|
||
(cond
|
||
((equal tok ")") "case-)")
|
||
((equal tok "(") "case-(")
|
||
((and tok (string-match "\\`[a-z]" tok)
|
||
(assoc tok smie-grammar)
|
||
(not
|
||
(save-excursion
|
||
(goto-char pos)
|
||
(sh-smie--sh-keyword-p tok))))
|
||
" word ")
|
||
(t tok)))))))
|
||
|
||
(defun sh-smie--looking-back-at-continuation-p ()
|
||
(save-excursion
|
||
(and (if (eq (char-before) ?\n) (progn (forward-char -1) t) (eolp))
|
||
(looking-back "\\(?:^\\|[^\\]\\)\\(?:\\\\\\\\\\)*\\\\"
|
||
(line-beginning-position)))))
|
||
|
||
(defun sh-smie-sh-backward-token ()
|
||
(let ((bol (line-beginning-position)))
|
||
(forward-comment (- (point)))
|
||
(cond
|
||
((and (bolp) (not (bobp))
|
||
(equal (syntax-after (1- (point))) (string-to-syntax "|"))
|
||
(not (nth 3 (syntax-ppss))))
|
||
;; Right after a here-document.
|
||
(let ((forward-sexp-function nil))
|
||
(forward-sexp -1)
|
||
;; Pretend the here-document is a "newline representing a
|
||
;; semi-colon", since the here-doc otherwise covers the newline(s).
|
||
";"))
|
||
((< (point) bol)
|
||
(cond
|
||
((sh-smie--looking-back-at-continuation-p)
|
||
(forward-char -1)
|
||
(funcall smie-backward-token-function))
|
||
((sh-smie--newline-semi-p) ";")
|
||
(t (funcall smie-backward-token-function))))
|
||
((looking-back sh-smie--sh-operators-back-re
|
||
(line-beginning-position) 'greedy)
|
||
(goto-char (match-beginning 1))
|
||
(match-string-no-properties 1))
|
||
(t
|
||
(let ((tok (sh-smie--default-backward-token)))
|
||
(cond
|
||
((equal tok ")") "case-)")
|
||
((equal tok "(") "case-(")
|
||
((and tok (string-match "\\`[a-z]" tok)
|
||
(assoc tok smie-grammar)
|
||
(not (save-excursion (sh-smie--sh-keyword-p tok))))
|
||
" word ")
|
||
(t tok)))))))
|
||
|
||
(defcustom sh-indent-after-continuation t
|
||
"If non-nil, indent relative to the continued line's beginning.
|
||
Continued lines can either be indented as \"one long wrapped line\" without
|
||
paying attention to the actual syntactic structure, as in:
|
||
|
||
for f \\
|
||
in a; do \\
|
||
toto; \\
|
||
done
|
||
|
||
or as lines that just don't have implicit semi-colons between them, as in:
|
||
|
||
for f \\
|
||
in a; do \\
|
||
toto; \\
|
||
done
|
||
|
||
With `always' you get the former behavior whereas with nil you get the latter.
|
||
With t, you get the latter as long as that would indent the continuation line
|
||
deeper than the initial line."
|
||
:version "25.1"
|
||
:type '(choice
|
||
(const :value nil :tag "Never")
|
||
(const :value t :tag "Only if needed to make it deeper")
|
||
(const :value always :tag "Always"))
|
||
:group 'sh-indentation)
|
||
|
||
(defun sh-smie--continuation-start-indent ()
|
||
"Return the initial indentation of a continued line.
|
||
May return nil if the line should not be treated as continued."
|
||
(save-excursion
|
||
(forward-line -1)
|
||
(unless (sh-smie--looking-back-at-continuation-p)
|
||
(current-indentation))))
|
||
|
||
(defun sh-smie--indent-continuation ()
|
||
(cond
|
||
((not (and sh-indent-after-continuation
|
||
(save-excursion
|
||
(ignore-errors
|
||
(skip-chars-backward " \t")
|
||
(sh-smie--looking-back-at-continuation-p)))))
|
||
nil)
|
||
((eq sh-indent-after-continuation 'always)
|
||
(save-excursion
|
||
(forward-line -1)
|
||
(if (sh-smie--looking-back-at-continuation-p)
|
||
(current-indentation)
|
||
(+ (current-indentation) (sh-var-value 'sh-indent-for-continuation)))))
|
||
(t
|
||
;; Just make sure a line-continuation is indented deeper.
|
||
(save-excursion
|
||
(let ((indent (let ((sh-indent-after-continuation nil))
|
||
(smie-indent-calculate)))
|
||
(max most-positive-fixnum))
|
||
(if (not (numberp indent)) indent
|
||
(while (progn
|
||
(forward-line -1)
|
||
(let ((ci (current-indentation)))
|
||
(cond
|
||
;; Previous line is less indented, we're good.
|
||
((< ci indent) nil)
|
||
((sh-smie--looking-back-at-continuation-p)
|
||
(setq max (min max ci))
|
||
;; Previous line is itself a continuation.
|
||
;; If it's indented like us, we're good, otherwise
|
||
;; check the line before that one.
|
||
(> ci indent))
|
||
(t ;Previous line is the beginning of the continued line.
|
||
(setq
|
||
indent
|
||
(min
|
||
(+ ci (sh-var-value 'sh-indent-for-continuation)) max))
|
||
nil)))))
|
||
indent))))))
|
||
|
||
(defun sh-smie-sh-rules (kind token)
|
||
(pcase (cons kind token)
|
||
('(:elem . basic) sh-basic-offset)
|
||
('(:after . "case-)") (- (sh-var-value 'sh-indent-for-case-alt)
|
||
(sh-var-value 'sh-indent-for-case-label)))
|
||
(`(:before . ,(or "(" "{" "[" "while" "if" "for" "case"))
|
||
(cond
|
||
((and (equal token "{") (smie-rule-parent-p "for" "case"))
|
||
(let ((data (smie-backward-sexp "in")))
|
||
(when (member (nth 2 data) '("for" "case"))
|
||
`(column . ,(smie-indent-virtual)))))
|
||
((not (smie-rule-prev-p "&&" "||" "|"))
|
||
(when (smie-rule-hanging-p)
|
||
(smie-rule-parent)))
|
||
(t
|
||
(unless (smie-rule-bolp)
|
||
(while (equal "|" (nth 2 (smie-backward-sexp 'halfexp))))
|
||
`(column . ,(smie-indent-virtual))))))
|
||
;; FIXME: Maybe this handling of ;; should be made into
|
||
;; a smie-rule-terminator function that takes the substitute ";" as arg.
|
||
(`(:before . ,(or ";;" ";&" ";;&" ";|"))
|
||
(if (and (smie-rule-bolp) (looking-at ";\\(?:;&?\\|[&|]\\)?[ \t]*\\(#\\|$\\)"))
|
||
(cons 'column (smie-indent-keyword ";"))
|
||
(smie-rule-separator kind)))
|
||
(`(:after . ,(or ";;" ";&" ";;&" ";|"))
|
||
(with-demoted-errors "SMIE rule error: %S"
|
||
(smie-backward-sexp token)
|
||
(cons 'column
|
||
(if (or (smie-rule-bolp)
|
||
(save-excursion
|
||
(and (member (funcall smie-backward-token-function)
|
||
'("in" ";;"))
|
||
(smie-rule-bolp))))
|
||
(current-column)
|
||
(smie-indent-calculate)))))
|
||
(`(:before . ,(or "|" "&&" "||"))
|
||
(when (and (not (smie-rule-parent-p token))
|
||
(or (not (equal token "&&"))
|
||
sh-indent-statement-after-and))
|
||
(smie-backward-sexp token)
|
||
`(column . ,(+ (funcall smie-rules-function :elem 'basic)
|
||
(smie-indent-virtual)))))
|
||
|
||
;; Attempt at backward compatibility with the old config variables.
|
||
('(:before . "fi") (sh-var-value 'sh-indent-for-fi))
|
||
('(:before . "done") (sh-var-value 'sh-indent-for-done))
|
||
('(:after . "else") (sh-var-value 'sh-indent-after-else))
|
||
('(:after . "if") (sh-var-value 'sh-indent-after-if))
|
||
('(:before . "then") (sh-var-value 'sh-indent-for-then))
|
||
('(:before . "do") (sh-var-value 'sh-indent-for-do))
|
||
('(:after . "do")
|
||
(sh-var-value (if (smie-rule-hanging-p)
|
||
'sh-indent-after-loop-construct 'sh-indent-after-do)))
|
||
;; sh-indent-after-done: aligned completely differently.
|
||
('(:after . "in") (sh-var-value 'sh-indent-for-case-label))
|
||
;; sh-indent-for-continuation: Line continuations are handled differently.
|
||
(`(:after . ,(or "(" "{" "["))
|
||
(if (not (looking-at ".[ \t]*[^\n \t#]"))
|
||
(sh-var-value 'sh-indent-after-open)
|
||
(goto-char (1- (match-end 0)))
|
||
`(column . ,(current-column))))
|
||
;; sh-indent-after-function: we don't handle it differently.
|
||
))
|
||
|
||
;; (defconst sh-smie-csh-grammar
|
||
;; (smie-prec2->grammar
|
||
;; (smie-bnf->prec2
|
||
;; '((exp) ;A constant, or a $var, or a sequence of them...
|
||
;; (elseifcmd (cmd)
|
||
;; (cmd "else" "else-if" exp "then" elseifcmd))
|
||
;; (cmd ("switch" branches "endsw")
|
||
;; ("if" exp)
|
||
;; ("if" exp "then" cmd "endif")
|
||
;; ("if" exp "then" cmd "else" cmd "endif")
|
||
;; ("if" exp "then" elseifcmd "endif")
|
||
;; ;; ("if" exp "then" cmd "else" cmd "endif")
|
||
;; ;; ("if" exp "then" cmd "else" "if" exp "then" cmd "endif")
|
||
;; ;; ("if" exp "then" cmd "else" "if" exp "then" cmd
|
||
;; ;; "else" cmd "endif")
|
||
;; ;; ("if" exp "then" cmd "else" "if" exp "then" cmd
|
||
;; ;; "else" "if" exp "then" cmd "endif")
|
||
;; ("while" cmd "end")
|
||
;; ("foreach" cmd "end")
|
||
;; (cmd "|" cmd) (cmd "|&" cmd)
|
||
;; (cmd "&&" cmd) (cmd "||" cmd)
|
||
;; (cmd ";" cmd) (cmd "&" cmd))
|
||
;; ;; This is a lie, but (combined with the corresponding disambiguation
|
||
;; ;; rule) it makes it more clear that `case' and `default' are the key
|
||
;; ;; separators and the `:' is a secondary tokens.
|
||
;; (branches (branches "case" branches)
|
||
;; (branches "default" branches)
|
||
;; (exp ":" branches)))
|
||
;; '((assoc "else" "then" "endif"))
|
||
;; '((assoc "case" "default") (nonassoc ":"))
|
||
;; '((assoc ";;" ";&" ";;&"))
|
||
;; '((assoc ";" "&") (assoc "&&" "||") (assoc "|" "|&")))))
|
||
|
||
;;;; SMIE support for `rc'.
|
||
|
||
(defconst sh-smie-rc-grammar
|
||
(smie-prec2->grammar
|
||
(smie-bnf->prec2
|
||
'((exp) ;A constant, or a $var, or a sequence of them...
|
||
(cmd (cmd "case" cmd)
|
||
("if" exp)
|
||
("switch" exp)
|
||
("for" exp) ("while" exp)
|
||
(cmd "|" cmd) (cmd "|&" cmd)
|
||
(cmd "&&" cmd) (cmd "||" cmd)
|
||
(cmd ";" cmd) (cmd "&" cmd))
|
||
(pattern (pattern "|" pattern))
|
||
(branches (branches ";;" branches)
|
||
(branches ";&" branches) (branches ";;&" branches) ;bash.
|
||
(branches ";|" branches) ;zsh.
|
||
(pattern "case-)" cmd)))
|
||
'((assoc ";;" ";&" ";;&" ";|"))
|
||
'((assoc "case") (assoc ";" "&") (assoc "&&" "||") (assoc "|" "|&")))))
|
||
|
||
(defun sh-smie--rc-after-special-arg-p ()
|
||
"Check if we're after the first arg of an if/while/for/... construct.
|
||
Returns the construct's token and moves point before it, if so."
|
||
(forward-comment (- (point)))
|
||
(when (looking-back ")\\|\\_<not" (- (point) 3))
|
||
(ignore-errors
|
||
(let ((forward-sexp-function nil))
|
||
(forward-sexp -1)
|
||
(car (member (funcall smie-backward-token-function)
|
||
'("if" "for" "switch" "while")))))))
|
||
|
||
(defun sh-smie--rc-newline-semi-p ()
|
||
"Return non-nil if a newline should be treated as a semi-colon.
|
||
Point should be before the newline."
|
||
(save-excursion
|
||
(let ((tok (funcall smie-backward-token-function)))
|
||
(if (or (when (equal tok "not") (forward-word-strictly 1) t)
|
||
(and (zerop (length tok)) (eq (char-before) ?\))))
|
||
(not (sh-smie--rc-after-special-arg-p))
|
||
(sh-smie--newline-semi-p tok)))))
|
||
|
||
(defun sh-smie-rc-forward-token ()
|
||
;; FIXME: Code duplication with sh-smie-sh-forward-token.
|
||
(if (and (looking-at "[ \t]*\\(?:#\\|\\(\\s|\\)\\|$\\)")
|
||
(save-excursion
|
||
(skip-chars-backward " \t")
|
||
(not (bolp))))
|
||
(if (and (match-end 1) (not (nth 3 (syntax-ppss))))
|
||
;; Right before a here-doc.
|
||
(let ((forward-sexp-function nil))
|
||
(forward-sexp 1)
|
||
;; Pretend the here-document is a "newline representing a
|
||
;; semi-colon", since the here-doc otherwise covers the newline(s).
|
||
";")
|
||
(let ((semi (sh-smie--rc-newline-semi-p)))
|
||
(forward-line 1)
|
||
(if (or semi (eobp)) ";"
|
||
(sh-smie-rc-forward-token))))
|
||
(forward-comment (point-max))
|
||
(cond
|
||
((looking-at "\\\\\n") (forward-line 1) (sh-smie-rc-forward-token))
|
||
;; ((looking-at sh-smie--rc-operators-re)
|
||
;; (goto-char (match-end 0))
|
||
;; (let ((tok (match-string-no-properties 0)))
|
||
;; (if (and (memq (aref tok (1- (length tok))) '(?\; ?\& ?\|))
|
||
;; (looking-at "[ \t]*\\(?:#\\|$\\)"))
|
||
;; (forward-line 1))
|
||
;; tok))
|
||
(t
|
||
(let* ((pos (point))
|
||
(tok (sh-smie--default-forward-token)))
|
||
(cond
|
||
;; ((equal tok ")") "case-)")
|
||
((and tok (string-match "\\`[a-z]" tok)
|
||
(assoc tok smie-grammar)
|
||
(not
|
||
(save-excursion
|
||
(goto-char pos)
|
||
(sh-smie--keyword-p))))
|
||
" word ")
|
||
(t tok)))))))
|
||
|
||
(defun sh-smie-rc-backward-token ()
|
||
;; FIXME: Code duplication with sh-smie-sh-backward-token.
|
||
(let ((bol (line-beginning-position)))
|
||
(forward-comment (- (point)))
|
||
(cond
|
||
((and (bolp) (not (bobp))
|
||
(equal (syntax-after (1- (point))) (string-to-syntax "|"))
|
||
(not (nth 3 (syntax-ppss))))
|
||
;; Right after a here-document.
|
||
(let ((forward-sexp-function nil))
|
||
(forward-sexp -1)
|
||
;; Pretend the here-document is a "newline representing a
|
||
;; semi-colon", since the here-doc otherwise covers the newline(s).
|
||
";"))
|
||
((< (point) bol) ;We skipped over a newline.
|
||
(cond
|
||
;; A continued line.
|
||
((and (eolp)
|
||
(looking-back "\\(?:^\\|[^\\]\\)\\(?:\\\\\\\\\\)*\\\\"
|
||
(line-beginning-position)))
|
||
(forward-char -1)
|
||
(funcall smie-backward-token-function))
|
||
((sh-smie--rc-newline-semi-p) ";")
|
||
(t (funcall smie-backward-token-function))))
|
||
;; ((looking-back sh-smie--sh-operators-back-re
|
||
;; (line-beginning-position) 'greedy)
|
||
;; (goto-char (match-beginning 1))
|
||
;; (match-string-no-properties 1))
|
||
(t
|
||
(let ((tok (sh-smie--default-backward-token)))
|
||
(cond
|
||
;; ((equal tok ")") "case-)")
|
||
((and tok (string-match "\\`[a-z]" tok)
|
||
(assoc tok smie-grammar)
|
||
(not (save-excursion (sh-smie--keyword-p))))
|
||
" word ")
|
||
(t tok)))))))
|
||
|
||
(defun sh-smie-rc-rules (kind token)
|
||
(pcase (cons kind token)
|
||
('(:elem . basic) sh-basic-offset)
|
||
;; (`(:after . "case") (or sh-basic-offset smie-indent-basic))
|
||
('(:after . ";")
|
||
(if (smie-rule-parent-p "case")
|
||
(smie-rule-parent (sh-var-value 'sh-indent-after-case))))
|
||
('(:before . "{")
|
||
(save-excursion
|
||
(when (sh-smie--rc-after-special-arg-p)
|
||
`(column . ,(current-column)))))
|
||
(`(:before . ,(or "(" "{" "["))
|
||
(if (smie-rule-hanging-p) (smie-rule-parent)))
|
||
;; FIXME: SMIE parses "if (exp) cmd" as "(if ((exp) cmd))" so "cmd" is
|
||
;; treated as an arg to (exp) by default, which indents it all wrong.
|
||
;; To handle it right, we should extend smie-indent-exps so that the
|
||
;; preceding keyword can give special rules. Currently the only special
|
||
;; rule we have is the :list-intro hack, which we use here to align "cmd"
|
||
;; with "(exp)", which is rarely the right thing to do, but is better
|
||
;; than nothing.
|
||
(`(:list-intro . ,(or "for" "if" "while")) t)
|
||
;; sh-indent-after-switch: handled implicitly by the default { rule.
|
||
))
|
||
|
||
;;; End of SMIE code.
|
||
|
||
(defvar sh-regexp-for-done nil
|
||
"A buffer-local regexp to match opening keyword for done.")
|
||
|
||
|
||
(defun sh-set-shell (shell &optional no-query-flag insert-flag)
|
||
"Set this buffer's shell to SHELL (a string).
|
||
When used interactively, insert the proper starting #!-line,
|
||
and make the visited file executable via `executable-set-magic',
|
||
perhaps querying depending on the value of `executable-query'.
|
||
(If given a prefix (i.e., \\[universal-argument]) don't insert any starting #!
|
||
line.)
|
||
|
||
When this function is called noninteractively, INSERT-FLAG (the third
|
||
argument) controls whether to insert a #!-line and think about making
|
||
the visited file executable, and NO-QUERY-FLAG (the second argument)
|
||
controls whether to query about making the visited file executable.
|
||
|
||
Calls the value of `sh-set-shell-hook' if set.
|
||
|
||
Shell script files can cause this function be called automatically
|
||
when the file is visited by having a `sh-shell' file-local variable
|
||
whose value is the shell name (don't quote it)."
|
||
(interactive (list (completing-read
|
||
(format-prompt "Shell" sh-shell-file)
|
||
;; This used to use interpreter-mode-alist, but that is
|
||
;; no longer appropriate now that uses regexps.
|
||
;; Maybe there could be a separate variable that lists
|
||
;; the shells, used here and to construct i-mode-alist.
|
||
;; But the following is probably good enough:
|
||
(append (mapcar (lambda (e) (symbol-name (car e)))
|
||
sh-ancestor-alist)
|
||
'("csh" "rc" "sh"))
|
||
nil nil nil nil sh-shell-file)
|
||
(eq executable-query 'function)
|
||
(not current-prefix-arg)))
|
||
(if (string-match "\\.exe\\'" shell)
|
||
(setq shell (substring shell 0 (match-beginning 0))))
|
||
(setq sh-shell (sh-canonicalize-shell shell))
|
||
(if insert-flag
|
||
(setq sh-shell-file
|
||
(executable-set-magic shell (sh-feature sh-shell-arg)
|
||
no-query-flag insert-flag)))
|
||
(setq mode-line-process (format "[%s]" sh-shell))
|
||
(setq-local sh-shell-variables nil)
|
||
(setq-local sh-shell-variables-initialized nil)
|
||
(setq-local imenu-generic-expression
|
||
(sh-feature sh-imenu-generic-expression))
|
||
(let ((tem (sh-feature sh-mode-syntax-table-input)))
|
||
(when tem
|
||
(setq-local sh-mode-syntax-table
|
||
(apply 'sh-mode-syntax-table tem))
|
||
(set-syntax-table sh-mode-syntax-table)))
|
||
(dolist (var (sh-feature sh-variables))
|
||
(sh-remember-variable var))
|
||
(if (setq-local sh-indent-supported-here
|
||
(sh-feature sh-indent-supported))
|
||
(progn
|
||
(message "Setting up indent for shell type %s" sh-shell)
|
||
(let ((mksym (lambda (name)
|
||
(intern (format "sh-smie-%s-%s"
|
||
sh-indent-supported-here name)))))
|
||
(add-function :around (local 'smie--hanging-eolp-function)
|
||
(lambda (orig)
|
||
(if (looking-at "[ \t]*\\\\\n")
|
||
(goto-char (match-end 0))
|
||
(funcall orig))))
|
||
(add-hook 'smie-indent-functions #'sh-smie--indent-continuation nil t)
|
||
(smie-setup (symbol-value (funcall mksym "grammar"))
|
||
(funcall mksym "rules")
|
||
:forward-token (funcall mksym "forward-token")
|
||
:backward-token (funcall mksym "backward-token")))
|
||
(if sh-make-vars-local
|
||
(sh-make-vars-local))
|
||
(message "Indentation setup for shell type %s" sh-shell))
|
||
(message "No indentation for this shell type.")
|
||
(setq-local indent-line-function #'sh-basic-indent-line))
|
||
(when font-lock-mode
|
||
(setq font-lock-set-defaults nil)
|
||
(font-lock-set-defaults)
|
||
(font-lock-flush))
|
||
(setq sh-shell-process nil)
|
||
(run-hooks 'sh-set-shell-hook))
|
||
|
||
|
||
(defun sh-feature (alist &optional function)
|
||
"Index ALIST by the current shell.
|
||
If ALIST isn't a list where every element is a cons, it is returned as is.
|
||
Else indexing follows an inheritance logic which works in two ways:
|
||
|
||
- Fall back on successive ancestors (see `sh-ancestor-alist') as long as
|
||
the alist contains no value for the current shell.
|
||
The ultimate default is always `sh'.
|
||
|
||
- If the value thus looked up is a list starting with `sh-append',
|
||
we call the function `sh-append' with the rest of the list as
|
||
arguments, and use the value. However, the next element of the
|
||
list is not used as-is; instead, we look it up recursively
|
||
in ALIST to allow the function called to define the value for
|
||
one shell to be derived from another shell.
|
||
The value thus determined is physically replaced into the alist.
|
||
|
||
If FUNCTION is non-nil, it is called with one argument,
|
||
the value thus obtained, and the result is used instead."
|
||
(or (if (consp alist)
|
||
;; Check for something that isn't a valid alist.
|
||
(let ((l alist))
|
||
(while (and l (consp (car l)))
|
||
(setq l (cdr l)))
|
||
(if l alist)))
|
||
|
||
(let ((orig-sh-shell sh-shell))
|
||
(let ((sh-shell sh-shell)
|
||
elt val)
|
||
(while (and sh-shell
|
||
(not (setq elt (assq sh-shell alist))))
|
||
(setq sh-shell (cdr (assq sh-shell sh-ancestor-alist))))
|
||
;; If the shell is not known, treat it as sh.
|
||
(unless elt
|
||
(setq elt (assq 'sh alist)))
|
||
(setq val (cdr elt))
|
||
(if (and (consp val)
|
||
(memq (car val) '(sh-append sh-modify)))
|
||
(setq val
|
||
(apply (car val)
|
||
;; Refer to the value for a different shell,
|
||
;; as a kind of inheritance.
|
||
(let ((sh-shell (car (cdr val))))
|
||
(sh-feature alist))
|
||
(cddr val))))
|
||
(if function
|
||
(setq sh-shell orig-sh-shell
|
||
val (funcall function val)))
|
||
val))))
|
||
|
||
|
||
|
||
(defun sh-append (ancestor &rest list)
|
||
"Return list composed of first argument (a list) physically appended to rest."
|
||
(nconc list ancestor))
|
||
|
||
|
||
(defun sh-modify (skeleton &rest list)
|
||
"Modify a copy of SKELETON by replacing I1 with REPL1, I2 with REPL2 ..."
|
||
(setq skeleton (copy-sequence skeleton))
|
||
(while list
|
||
(setcar (or (nthcdr (car list) skeleton)
|
||
(error "Index %d out of bounds" (car list)))
|
||
(car (cdr list)))
|
||
(setq list (nthcdr 2 list)))
|
||
skeleton)
|
||
|
||
|
||
(defun sh-basic-indent-line ()
|
||
"Indent a line for Sh mode (shell script mode).
|
||
Indent as far as preceding non-empty line, then by steps of `sh-basic-offset'.
|
||
Lines containing only comments are considered empty."
|
||
(interactive)
|
||
(let ((previous (save-excursion
|
||
(while (and (progn (beginning-of-line)
|
||
(not (bobp)))
|
||
(progn
|
||
(forward-line -1)
|
||
(back-to-indentation)
|
||
(or (eolp)
|
||
(eq (following-char) ?#)))))
|
||
(current-column)))
|
||
current)
|
||
(save-excursion
|
||
(indent-to (if (or (eq this-command 'newline-and-indent)
|
||
(and electric-indent-mode (eq this-command 'newline)))
|
||
previous
|
||
(if (< (current-column)
|
||
(setq current (progn (back-to-indentation)
|
||
(current-column))))
|
||
(if (eolp) previous 0)
|
||
(delete-region (point)
|
||
(progn (beginning-of-line) (point)))
|
||
(if (eolp)
|
||
(max previous (* (1+ (/ current sh-basic-offset))
|
||
sh-basic-offset))
|
||
(* (1+ (/ current sh-basic-offset)) sh-basic-offset))))))
|
||
(if (< (current-column) (current-indentation))
|
||
(skip-chars-forward " \t"))))
|
||
|
||
|
||
(defun sh-execute-region (start end &optional flag)
|
||
"Pass optional header and region to a subshell for noninteractive execution.
|
||
The working directory is that of the buffer, and only environment variables
|
||
are already set which is why you can mark a header within the script.
|
||
|
||
The executed subshell is `sh-shell-file'.
|
||
|
||
With a positive prefix ARG, instead of sending region, define header from
|
||
beginning of buffer to point. With a negative prefix ARG, instead of sending
|
||
region, clear header."
|
||
(interactive "r\nP")
|
||
(if flag
|
||
(setq sh-header-marker (if (> (prefix-numeric-value flag) 0)
|
||
(point-marker)))
|
||
(let ((shell-file-name sh-shell-file))
|
||
(if sh-header-marker
|
||
(save-excursion
|
||
(let (buffer-undo-list)
|
||
(goto-char sh-header-marker)
|
||
(append-to-buffer (current-buffer) start end)
|
||
(shell-command-on-region (point-min)
|
||
(setq end (+ sh-header-marker
|
||
(- end start)))
|
||
sh-shell-file)
|
||
(delete-region sh-header-marker end)))
|
||
(shell-command-on-region start end (concat sh-shell-file " -"))))))
|
||
|
||
|
||
(defun sh-remember-variable (var)
|
||
"Make VARIABLE available for future completing reads in this buffer."
|
||
(or (< (length var) sh-remember-variable-min)
|
||
(getenv var)
|
||
(assoc var sh-shell-variables)
|
||
(push (cons var var) sh-shell-variables))
|
||
var)
|
||
|
||
|
||
|
||
(defun sh-quoted-p ()
|
||
"Is point preceded by an odd number of backslashes?"
|
||
(eq -1 (% (save-excursion (skip-chars-backward "\\\\")) 2)))
|
||
|
||
;; Indentation stuff.
|
||
|
||
(defun sh-make-vars-local ()
|
||
"Make the indentation variables local to this buffer.
|
||
Normally they already are local. This command is provided in case
|
||
variable `sh-make-vars-local' has been set to nil.
|
||
|
||
To revert all these variables to the global values, use
|
||
command `sh-reset-indent-vars-to-global-values'."
|
||
(interactive)
|
||
(mapc 'make-local-variable sh-var-list)
|
||
(message "Indentation variables are now local."))
|
||
|
||
(defun sh-reset-indent-vars-to-global-values ()
|
||
"Reset local indentation variables to the global values.
|
||
Then, if variable `sh-make-vars-local' is non-nil, make them local."
|
||
(interactive)
|
||
(mapc 'kill-local-variable sh-var-list)
|
||
(if sh-make-vars-local
|
||
(mapcar 'make-local-variable sh-var-list)))
|
||
|
||
(defun sh-in-comment-or-string (start)
|
||
"Return non-nil if START is in a comment or string."
|
||
(save-excursion
|
||
(let ((state (syntax-ppss start)))
|
||
(or (nth 3 state) (nth 4 state)))))
|
||
|
||
|
||
(defun sh-var-value (var &optional ignore-error)
|
||
"Return the value of variable VAR, interpreting symbols.
|
||
It can also return t or nil.
|
||
If an invalid value is found, throw an error unless Optional argument
|
||
IGNORE-ERROR is non-nil."
|
||
(let ((val (symbol-value var)))
|
||
(cond
|
||
((numberp val)
|
||
val)
|
||
((eq val t)
|
||
val)
|
||
((null val)
|
||
val)
|
||
((eq val '+)
|
||
sh-basic-offset)
|
||
((eq val '-)
|
||
(- sh-basic-offset))
|
||
((eq val '++)
|
||
(* 2 sh-basic-offset))
|
||
((eq val '--)
|
||
(* 2 (- sh-basic-offset)))
|
||
((eq val '*)
|
||
(/ sh-basic-offset 2))
|
||
((eq val '/)
|
||
(/ (- sh-basic-offset) 2))
|
||
(t
|
||
(funcall (if ignore-error #'message #'error)
|
||
"Don't know how to handle %s's value of %s" var val)
|
||
0))))
|
||
|
||
(define-obsolete-function-alias 'sh-show-indent
|
||
#'smie-config-show-indent "28.1")
|
||
|
||
(define-obsolete-function-alias 'sh-set-indent #'smie-config-set-indent "28.1")
|
||
|
||
(define-obsolete-function-alias 'sh-learn-line-indent
|
||
#'smie-config-set-indent "28.1")
|
||
|
||
(define-obsolete-function-alias 'sh-learn-buffer-indent
|
||
#'smie-config-guess "28.1")
|
||
|
||
;; ========================================================================
|
||
|
||
;; Styles -- a quick and dirty way of saving the indentation settings.
|
||
|
||
(defvar sh-styles-alist nil
|
||
"A list of all known shell indentation styles.")
|
||
|
||
(defun sh-name-style (name &optional confirm-overwrite)
|
||
"Name the current indentation settings as a style called NAME.
|
||
If this name exists, the command will prompt whether it should be
|
||
overwritten if
|
||
- - it was called interactively with a prefix argument, or
|
||
- - called non-interactively with optional CONFIRM-OVERWRITE non-nil."
|
||
;; (interactive "sName for this style: ")
|
||
(interactive
|
||
(list
|
||
(read-from-minibuffer "Name for this style? " )
|
||
(not current-prefix-arg)))
|
||
(let ((slist (cons name
|
||
(mapcar (lambda (var) (cons var (symbol-value var)))
|
||
sh-var-list)))
|
||
(style (assoc name sh-styles-alist)))
|
||
(if style
|
||
(if (and confirm-overwrite
|
||
(not (y-or-n-p "This style exists. Overwrite it? ")))
|
||
(message "Not changing style %s" name)
|
||
(message "Updating style %s" name)
|
||
(setcdr style (cdr slist)))
|
||
(message "Creating new style %s" name)
|
||
(push slist sh-styles-alist))))
|
||
|
||
(defun sh-load-style (name)
|
||
"Set shell indentation values for this buffer from those in style NAME."
|
||
(interactive (list (completing-read
|
||
"Which style to use for this buffer? "
|
||
sh-styles-alist nil t)))
|
||
(let ((sl (assoc name sh-styles-alist)))
|
||
(if (null sl)
|
||
(error "sh-load-style: Style %s not known" name)
|
||
(dolist (var (cdr sl))
|
||
(set (car var) (cdr var))))))
|
||
|
||
(defun sh-save-styles-to-buffer (buff)
|
||
"Save all current styles in elisp to buffer BUFF.
|
||
This is always added to the end of the buffer."
|
||
(interactive
|
||
(list
|
||
(read-from-minibuffer "Buffer to save styles in? " "*scratch*")))
|
||
(with-current-buffer (get-buffer-create buff)
|
||
(goto-char (point-max))
|
||
(insert "\n")
|
||
(pp `(setq sh-styles-alist ',sh-styles-alist) (current-buffer))))
|
||
|
||
|
||
|
||
;; statement syntax-commands for various shells
|
||
|
||
;; You are welcome to add the syntax or even completely new statements as
|
||
;; appropriate for your favorite shell.
|
||
|
||
(defconst sh-non-closing-paren
|
||
;; If we leave it rear-sticky, calling `newline' ends up inserting a \n
|
||
;; that inherits this property, which then confuses the indentation.
|
||
(propertize ")" 'syntax-table sh-st-punc 'rear-nonsticky t))
|
||
|
||
(define-skeleton sh-case
|
||
"Insert a case/switch statement. See `sh-feature'."
|
||
(csh "expression: "
|
||
"switch( " str " )" \n
|
||
> "case " (read-string "pattern: ") ?: \n
|
||
> _ \n
|
||
"breaksw" \n
|
||
( "other pattern, %s: "
|
||
< "case " str ?: \n
|
||
> _ \n
|
||
"breaksw" \n)
|
||
< "default:" \n
|
||
> _ \n
|
||
resume:
|
||
< < "endsw" \n)
|
||
(es)
|
||
(rc "expression: "
|
||
> "switch( " str " ) {" \n
|
||
> "case " (read-string "pattern: ") \n
|
||
> _ \n
|
||
( "other pattern, %s: "
|
||
"case " str > \n
|
||
> _ \n)
|
||
"case *" > \n
|
||
> _ \n
|
||
resume:
|
||
?\} > \n)
|
||
(sh "expression: "
|
||
> "case " str " in" \n
|
||
( "pattern, %s: "
|
||
> str sh-non-closing-paren \n
|
||
> _ \n
|
||
";;" \n)
|
||
> "*" sh-non-closing-paren \n
|
||
> _ \n
|
||
resume:
|
||
"esac" > \n))
|
||
|
||
(define-skeleton sh-for
|
||
"Insert a for loop. See `sh-feature'."
|
||
(csh sh-modify sh
|
||
1 ""
|
||
2 "foreach "
|
||
4 " ( "
|
||
6 " )"
|
||
15 '<
|
||
16 "end")
|
||
(es sh-modify rc
|
||
4 " = ")
|
||
(rc sh-modify sh
|
||
2 "for( "
|
||
6 " ) {"
|
||
15 ?\} )
|
||
(sh "Index variable: "
|
||
> "for " str " in " _ "; do" \n
|
||
> _ | ?$ & (sh-remember-variable str) \n
|
||
"done" > \n))
|
||
|
||
|
||
|
||
(define-skeleton sh-indexed-loop
|
||
"Insert an indexed loop from 1 to n. See `sh-feature'."
|
||
(bash sh-modify posix)
|
||
(csh "Index variable: "
|
||
"@ " str " = 1" \n
|
||
"while( $" str " <= " (read-string "upper limit: ") " )" \n
|
||
> _ ?$ str \n
|
||
"@ " str "++" \n
|
||
< "end" \n)
|
||
(es sh-modify rc
|
||
4 " =")
|
||
(ksh88 "Index variable: "
|
||
> "integer " str "=0" \n
|
||
> "while (( ( " str " += 1 ) <= "
|
||
(read-string "upper limit: ")
|
||
" )); do" \n
|
||
> _ ?$ (sh-remember-variable str) > \n
|
||
"done" > \n)
|
||
(posix "Index variable: "
|
||
> str "=1" \n
|
||
"while [ $" str " -le "
|
||
(read-string "upper limit: ")
|
||
" ]; do" \n
|
||
> _ ?$ str \n
|
||
str ?= (sh-add (sh-remember-variable str) 1) \n
|
||
"done" > \n)
|
||
(rc "Index variable: "
|
||
> "for( " str " in" " `{awk 'BEGIN { for( i=1; i<="
|
||
(read-string "upper limit: ")
|
||
"; i++ ) print i }'`}) {" \n
|
||
> _ ?$ (sh-remember-variable str) \n
|
||
?\} > \n)
|
||
(sh "Index variable: "
|
||
> "for " str " in `awk 'BEGIN { for( i=1; i<="
|
||
(read-string "upper limit: ")
|
||
"; i++ ) print i }'`; do" \n
|
||
> _ ?$ (sh-remember-variable str) \n
|
||
"done" > \n))
|
||
|
||
|
||
(defun sh-shell-initialize-variables ()
|
||
"Scan the buffer for variable assignments.
|
||
Add these variables to `sh-shell-variables'."
|
||
(message "Scanning buffer `%s' for variable assignments..." (buffer-name))
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(setq sh-shell-variables-initialized t)
|
||
(while (search-forward "=" nil t)
|
||
(sh--assignment-collect)))
|
||
(add-hook 'post-self-insert-hook #'sh--assignment-collect nil t)
|
||
(message "Scanning buffer `%s' for variable assignments...done"
|
||
(buffer-name)))
|
||
|
||
(defvar sh-add-buffer)
|
||
|
||
(defun sh-add-completer (string predicate code)
|
||
"Do completion using `sh-shell-variables', but initialize it first.
|
||
This function is designed for use as the \"completion table\",
|
||
so it takes three arguments:
|
||
STRING, the current buffer contents;
|
||
PREDICATE, the predicate for filtering possible matches;
|
||
CODE, which says what kind of things to do.
|
||
CODE can be nil, t or `lambda'.
|
||
nil means to return the best completion of STRING, or nil if there is none.
|
||
t means to return a list of all possible completions of STRING.
|
||
`lambda' means to return t if STRING is a valid completion as it stands."
|
||
(let ((vars
|
||
(with-current-buffer sh-add-buffer
|
||
(or sh-shell-variables-initialized
|
||
(sh-shell-initialize-variables))
|
||
(nconc (mapcar (lambda (var)
|
||
(substring var 0 (string-search "=" var)))
|
||
process-environment)
|
||
sh-shell-variables))))
|
||
(complete-with-action code vars string predicate)))
|
||
|
||
(defun sh-add (var delta)
|
||
"Insert an addition of VAR and prefix DELTA for Bourne (type) shell."
|
||
(interactive
|
||
(let ((sh-add-buffer (current-buffer)))
|
||
(list (completing-read "Variable: " 'sh-add-completer)
|
||
(prefix-numeric-value current-prefix-arg))))
|
||
(insert (sh-feature '((bash . "$(( ")
|
||
(ksh88 . "$(( ")
|
||
(posix . "$(( ")
|
||
(rc . "`{expr $")
|
||
(sh . "`expr $")
|
||
(zsh . "$[ ")))
|
||
(sh-remember-variable var)
|
||
(if (< delta 0) " - " " + ")
|
||
(number-to-string (abs delta))
|
||
(sh-feature '((bash . " ))")
|
||
(ksh88 . " ))")
|
||
(posix . " ))")
|
||
(rc . "}")
|
||
(sh . "`")
|
||
(zsh . " ]")))))
|
||
|
||
|
||
|
||
(define-skeleton sh-function
|
||
"Insert a function definition. See `sh-feature'."
|
||
(bash sh-modify ksh88
|
||
3 "() {")
|
||
(ksh88 "name: "
|
||
"function " str " {" \n
|
||
> _ \n
|
||
< "}" \n)
|
||
(rc sh-modify ksh88
|
||
1 "fn ")
|
||
(sh ()
|
||
"() {" \n
|
||
> _ \n
|
||
< "}" \n))
|
||
|
||
|
||
|
||
(define-skeleton sh-if
|
||
"Insert an if statement. See `sh-feature'."
|
||
(csh "condition: "
|
||
"if( " str " ) then" \n
|
||
> _ \n
|
||
( "other condition, %s: "
|
||
< "else if( " str " ) then" \n
|
||
> _ \n)
|
||
< "else" \n
|
||
> _ \n
|
||
resume:
|
||
< "endif" \n)
|
||
(es "condition: "
|
||
> "if { " str " } {" \n
|
||
> _ \n
|
||
( "other condition, %s: "
|
||
"} { " str " } {" > \n
|
||
> _ \n)
|
||
"} {" > \n
|
||
> _ \n
|
||
resume:
|
||
?\} > \n)
|
||
(rc "condition: "
|
||
> "if( " str " ) {" \n
|
||
> _ \n
|
||
( "other condition, %s: "
|
||
"} else if( " str " ) {" > \n
|
||
> _ \n)
|
||
"} else {" > \n
|
||
> _ \n
|
||
resume:
|
||
?\} > \n)
|
||
(sh "condition: "
|
||
'(setq input (sh-feature sh-test))
|
||
> "if " str "; then" \n
|
||
> _ \n
|
||
( "other condition, %s: "
|
||
> "elif " str "; then" > \n
|
||
> \n)
|
||
"else" > \n
|
||
> \n
|
||
resume:
|
||
"fi" > \n))
|
||
|
||
|
||
|
||
(define-skeleton sh-repeat
|
||
"Insert a repeat loop definition. See `sh-feature'."
|
||
(es nil
|
||
> "forever {" \n
|
||
> _ \n
|
||
?\} > \n)
|
||
(zsh "factor: "
|
||
> "repeat " str "; do" > \n
|
||
> \n
|
||
"done" > \n))
|
||
|
||
;;;(put 'sh-repeat 'menu-enable '(sh-feature sh-repeat))
|
||
|
||
|
||
|
||
(define-skeleton sh-select
|
||
"Insert a select statement. See `sh-feature'."
|
||
(ksh88 "Index variable: "
|
||
> "select " str " in " _ "; do" \n
|
||
> ?$ str \n
|
||
"done" > \n)
|
||
(bash sh-append ksh88))
|
||
;;;(put 'sh-select 'menu-enable '(sh-feature sh-select))
|
||
|
||
|
||
|
||
(define-skeleton sh-tmp-file
|
||
"Insert code to setup temporary file handling. See `sh-feature'."
|
||
(bash sh-append ksh88)
|
||
(csh (file-name-nondirectory (buffer-file-name))
|
||
"set tmp = `mktemp -t " str ".XXXXXX`" \n
|
||
"onintr exit" \n _
|
||
(and (goto-char (point-max))
|
||
(not (bolp))
|
||
?\n)
|
||
"exit:\n"
|
||
"rm $tmp* >&" null-device > \n)
|
||
(es (file-name-nondirectory (buffer-file-name))
|
||
> "local( signals = $signals sighup sigint;" \n
|
||
> "tmp = `{ mktemp -t " str ".XXXXXX } ) {" \n
|
||
> "catch @ e {" \n
|
||
> "rm $tmp^* >[2]" null-device \n
|
||
"throw $e" \n
|
||
"} {" > \n
|
||
_ \n
|
||
?\} > \n
|
||
?\} > \n)
|
||
(ksh88 sh-modify sh
|
||
7 "EXIT")
|
||
(rc (file-name-nondirectory (buffer-file-name))
|
||
> "tmp = `{ mktemp -t " str ".XXXXXX }" \n
|
||
"fn sigexit { rm $tmp^* >[2]" null-device " }" \n)
|
||
(sh (file-name-nondirectory (buffer-file-name))
|
||
> "TMP=`mktemp -t " str ".XXXXXX`" \n
|
||
"trap \"rm $TMP* 2>" null-device "\" " ?0 \n))
|
||
|
||
|
||
|
||
(define-skeleton sh-until
|
||
"Insert an until loop. See `sh-feature'."
|
||
(sh "condition: "
|
||
'(setq input (sh-feature sh-test))
|
||
> "until " str "; do" \n
|
||
> _ \n
|
||
"done" > \n))
|
||
;;;(put 'sh-until 'menu-enable '(sh-feature sh-until))
|
||
|
||
|
||
|
||
(define-skeleton sh-while
|
||
"Insert a while loop. See `sh-feature'."
|
||
(csh sh-modify sh
|
||
2 ""
|
||
3 "while( "
|
||
5 " )"
|
||
10 '<
|
||
11 "end")
|
||
(es sh-modify sh
|
||
3 "while { "
|
||
5 " } {"
|
||
10 ?\} )
|
||
(rc sh-modify sh
|
||
3 "while( "
|
||
5 " ) {"
|
||
10 ?\} )
|
||
(sh "condition: "
|
||
'(setq input (sh-feature sh-test))
|
||
> "while " str "; do" \n
|
||
> _ \n
|
||
"done" > \n))
|
||
|
||
|
||
|
||
(define-skeleton sh-while-getopts
|
||
"Insert a while getopts loop. See `sh-feature'.
|
||
Prompts for an options string which consists of letters for each recognized
|
||
option followed by a colon `:' if the option accepts an argument."
|
||
(bash sh-modify sh
|
||
18 "${0##*/}")
|
||
(csh nil
|
||
"while( 1 )" \n
|
||
> "switch( \"$1\" )" \n
|
||
'(setq input '("- x" . 2))
|
||
> >
|
||
( "option, %s: "
|
||
< "case " '(eval str)
|
||
'(if (string-match " +" str)
|
||
(setq v1 (substring str (match-end 0))
|
||
str (substring str 0 (match-beginning 0)))
|
||
(setq v1 nil))
|
||
str ?: \n
|
||
> "set " v1 & " = $2" | -4 & _ \n
|
||
(if v1 "shift") & \n
|
||
"breaksw" \n)
|
||
< "case --:" \n
|
||
> "shift" \n
|
||
< "default:" \n
|
||
> "break" \n
|
||
resume:
|
||
< < "endsw" \n
|
||
"shift" \n
|
||
< "end" \n)
|
||
(ksh88 sh-modify sh
|
||
16 "print"
|
||
18 "${0##*/}"
|
||
37 "OPTIND-1")
|
||
(posix sh-modify sh
|
||
18 "$(basename $0)")
|
||
(sh "optstring: "
|
||
> "while getopts :" str " OPT; do" \n
|
||
> "case $OPT in" \n
|
||
'(setq v1 (append (vconcat str) nil))
|
||
( (prog1 (if v1 (char-to-string (car v1)))
|
||
(if (eq (nth 1 v1) ?:)
|
||
(setq v1 (nthcdr 2 v1)
|
||
v2 "\"$OPTARG\"")
|
||
(setq v1 (cdr v1)
|
||
v2 nil)))
|
||
> str "|+" str sh-non-closing-paren \n
|
||
> _ v2 \n
|
||
> ";;" \n)
|
||
> "*" sh-non-closing-paren \n
|
||
> "echo" " \"usage: " "`basename $0`"
|
||
" [+-" '(setq v1 (point)) str
|
||
'(save-excursion
|
||
(while (search-backward ":" v1 t)
|
||
(replace-match " ARG] [+-" t t)))
|
||
(if (eq (preceding-char) ?-) -5)
|
||
(if (and (sequencep v1) (length v1)) "] " "} ")
|
||
"[--] ARGS...\"" \n
|
||
"exit 2" > \n
|
||
"esac" >
|
||
\n "done"
|
||
> \n
|
||
"shift " (sh-add "OPTIND" -1) \n
|
||
"OPTIND=1" \n))
|
||
|
||
|
||
|
||
(put 'sh-assignment 'delete-selection t)
|
||
(defun sh-assignment (arg)
|
||
"Remember preceding identifier for future completion and do self-insert."
|
||
(declare (obsolete nil "27.1"))
|
||
(interactive "p")
|
||
(self-insert-command arg)
|
||
(sh--assignment-collect))
|
||
|
||
(defun sh--assignment-collect ()
|
||
(sh-remember-variable
|
||
(when (eq ?= (char-before))
|
||
(save-excursion
|
||
(if (re-search-forward (sh-feature sh-assignment-regexp)
|
||
(prog1 (point)
|
||
(beginning-of-line 1))
|
||
t)
|
||
(match-string 1))))))
|
||
|
||
|
||
(defun sh--maybe-here-document ()
|
||
(when (and (looking-back "[^<]<<[ E-]" (line-beginning-position))
|
||
(save-excursion
|
||
(backward-char 2)
|
||
(not
|
||
(or (sh-quoted-p)
|
||
(sh--inside-noncommand-expression (point)))))
|
||
(not (nth 8 (syntax-ppss))))
|
||
(let ((tabs (if (string-match "\\`-" sh-here-document-word)
|
||
(make-string (/ (current-indentation) tab-width) ?\t)
|
||
""))
|
||
(delim (replace-regexp-in-string "['\"]" ""
|
||
sh-here-document-word)))
|
||
;; If we're at <<-, we don't want to delete the previous char.
|
||
(unless (= (preceding-char) ?-)
|
||
(delete-char -1))
|
||
(insert sh-here-document-word)
|
||
(or (eolp) (looking-at "[ \t]") (insert ?\s))
|
||
(end-of-line 1)
|
||
(while
|
||
(sh-quoted-p)
|
||
(end-of-line 2))
|
||
(insert ?\n tabs)
|
||
(save-excursion
|
||
(insert ?\n tabs (replace-regexp-in-string
|
||
"\\`-?[ \t]*" "" delim))))))
|
||
|
||
(define-minor-mode sh-electric-here-document-mode
|
||
"Make << insert a here document skeleton."
|
||
:lighter nil
|
||
(if sh-electric-here-document-mode
|
||
(add-hook 'post-self-insert-hook #'sh--maybe-here-document nil t)
|
||
(remove-hook 'post-self-insert-hook #'sh--maybe-here-document t)))
|
||
|
||
;; various other commands
|
||
|
||
(defun sh-beginning-of-command ()
|
||
;; FIXME: Redefine using SMIE.
|
||
"Move point to successive beginnings of commands."
|
||
(interactive)
|
||
(if (re-search-backward sh-beginning-of-command nil t)
|
||
(goto-char (match-beginning 2))))
|
||
|
||
(defun sh-end-of-command ()
|
||
;; FIXME: Redefine using SMIE.
|
||
"Move point to successive ends of commands."
|
||
(interactive)
|
||
(if (re-search-forward sh-end-of-command nil t)
|
||
(goto-char (match-end 1))))
|
||
|
||
;; Backslashification. Stolen from make-mode.el.
|
||
|
||
(defun sh-backslash-region (from to delete-flag)
|
||
"Insert, align, or delete end-of-line backslashes on the lines in the region.
|
||
With no argument, inserts backslashes and aligns existing backslashes.
|
||
With an argument, deletes the backslashes.
|
||
|
||
This function does not modify the last line of the region if the region ends
|
||
right at the start of the following line; it does not modify blank lines
|
||
at the start of the region. So you can put the region around an entire
|
||
shell command and conveniently use this command."
|
||
(interactive "r\nP")
|
||
(save-excursion
|
||
(goto-char from)
|
||
(let ((column sh-backslash-column)
|
||
(endmark (make-marker)))
|
||
(move-marker endmark to)
|
||
;; Compute the smallest column number past the ends of all the lines.
|
||
(if sh-backslash-align
|
||
(progn
|
||
(if (not delete-flag)
|
||
(while (< (point) to)
|
||
(end-of-line)
|
||
(if (= (preceding-char) ?\\)
|
||
(progn (forward-char -1)
|
||
(skip-chars-backward " \t")))
|
||
(setq column (max column (1+ (current-column))))
|
||
(forward-line 1)))
|
||
;; Adjust upward to a tab column, if that doesn't push
|
||
;; past the margin.
|
||
(if (> (% column tab-width) 0)
|
||
(let ((adjusted (* (/ (+ column tab-width -1) tab-width)
|
||
tab-width)))
|
||
(if (< adjusted (window-width))
|
||
(setq column adjusted))))))
|
||
;; Don't modify blank lines at start of region.
|
||
(goto-char from)
|
||
(while (and (< (point) endmark) (eolp))
|
||
(forward-line 1))
|
||
;; Add or remove backslashes on all the lines.
|
||
(while (and (< (point) endmark)
|
||
;; Don't backslashify the last line
|
||
;; if the region ends right at the start of the next line.
|
||
(save-excursion
|
||
(forward-line 1)
|
||
(< (point) endmark)))
|
||
(if (not delete-flag)
|
||
(sh-append-backslash column)
|
||
(sh-delete-backslash))
|
||
(forward-line 1))
|
||
(move-marker endmark nil))))
|
||
|
||
(defun sh-append-backslash (column)
|
||
(end-of-line)
|
||
;; Note that "\\\\" is needed to get one backslash.
|
||
(if (= (preceding-char) ?\\)
|
||
(progn (forward-char -1)
|
||
(delete-horizontal-space)
|
||
(indent-to column (if sh-backslash-align nil 1)))
|
||
(indent-to column (if sh-backslash-align nil 1))
|
||
(insert "\\")))
|
||
|
||
(defun sh-delete-backslash ()
|
||
(end-of-line)
|
||
(or (bolp)
|
||
(progn
|
||
(forward-char -1)
|
||
(if (looking-at "\\\\")
|
||
(delete-region (1+ (point))
|
||
(progn (skip-chars-backward " \t") (point)))))))
|
||
|
||
;;; Flymake backend
|
||
|
||
(defcustom sh-shellcheck-program "shellcheck"
|
||
"Name of the shellcheck executable."
|
||
:type 'string
|
||
:version "29.1")
|
||
|
||
(defcustom sh-shellcheck-arguments nil
|
||
"Additional arguments to the shellcheck program."
|
||
:type '(repeat string)
|
||
:version "29.1")
|
||
|
||
(defvar-local sh--shellcheck-process nil)
|
||
|
||
(defun sh-shellcheck-flymake (report-fn &rest _args)
|
||
"Flymake backend using the shellcheck program.
|
||
Takes a Flymake callback REPORT-FN as argument, as expected of a
|
||
member of `flymake-diagnostic-functions'."
|
||
(when (process-live-p sh--shellcheck-process)
|
||
(kill-process sh--shellcheck-process))
|
||
(let* ((source (current-buffer))
|
||
(dialect (named-let recur ((s sh-shell))
|
||
(pcase s
|
||
((or 'bash 'dash 'sh) (symbol-name s))
|
||
('ksh88 "ksh")
|
||
((guard s)
|
||
(recur (alist-get s sh-ancestor-alist))))))
|
||
(sentinel
|
||
(lambda (proc _event)
|
||
(when (memq (process-status proc) '(exit signal))
|
||
(unwind-protect
|
||
(if (with-current-buffer source
|
||
(not (eq proc sh--shellcheck-process)))
|
||
(flymake-log :warning "Canceling obsolete check %s" proc)
|
||
(with-current-buffer (process-buffer proc)
|
||
(goto-char (point-min))
|
||
(thread-last
|
||
(json-parse-buffer :object-type 'alist)
|
||
(alist-get 'comments)
|
||
(seq-filter
|
||
(lambda (item)
|
||
(let-alist item (string= .file "-"))))
|
||
(mapcar
|
||
(lambda (item)
|
||
(let-alist item
|
||
(flymake-make-diagnostic
|
||
source
|
||
(cons .line .column)
|
||
(unless (and (eq .line .endLine)
|
||
(eq .column .endColumn))
|
||
(cons .endLine .endColumn))
|
||
(pcase .level
|
||
("error" :error)
|
||
("warning" :warning)
|
||
(_ :note))
|
||
(format "SC%s: %s" .code .message)))))
|
||
(funcall report-fn))))
|
||
(kill-buffer (process-buffer proc)))))))
|
||
(unless dialect
|
||
(error "`sh-shellcheck-flymake' is not suitable for shell type `%s'"
|
||
sh-shell))
|
||
(setq sh--shellcheck-process
|
||
(make-process
|
||
:name "shellcheck" :noquery t :connection-type 'pipe
|
||
:buffer (generate-new-buffer " *flymake-shellcheck*")
|
||
:command `(,sh-shellcheck-program
|
||
"--format=json1"
|
||
"-s" ,dialect
|
||
,@sh-shellcheck-arguments
|
||
"-")
|
||
:sentinel sentinel))
|
||
(save-restriction
|
||
(widen)
|
||
(process-send-region sh--shellcheck-process (point-min) (point-max))
|
||
(process-send-eof sh--shellcheck-process))))
|
||
|
||
;;; Tree-sitter font-lock
|
||
|
||
(defvar sh-mode--treesit-operators
|
||
'("|" "|&" "||" "&&" ">" ">>" "<" "<<" "<<-" "<<<" "==" "!=" ";&" ";;&")
|
||
"A list of `sh-mode' operators to fontify.")
|
||
|
||
(defvar sh-mode--treesit-keywords
|
||
'("case" "do" "done" "elif" "else" "esac" "export" "fi" "for"
|
||
"function" "if" "in" "unset" "while" "then")
|
||
"Minimal list of keywords that belong to tree-sitter-bash's grammar.
|
||
|
||
Some reserved words are not recognize to keep the grammar
|
||
simpler. Those are identified with regex-based filtered queries.
|
||
|
||
\(See `sh-mode--treesit-other-keywords' and
|
||
`sh-mode--treesit-settings').")
|
||
|
||
(defun sh-mode--treesit-other-keywords ()
|
||
"Return a list `others' of key/reserved words.
|
||
These words are fontified with regex-based queries as they are
|
||
not part of tree-sitter-bash's grammar.
|
||
|
||
See `sh-mode--treesit-other-keywords' and
|
||
`sh-mode--treesit-settings')."
|
||
(let ((minimal sh-mode--treesit-keywords)
|
||
(all (append (sh-feature sh-leading-keywords)
|
||
(sh-feature sh-other-keywords)))
|
||
(others))
|
||
(dolist (keyword all others)
|
||
(if (not (member keyword minimal))
|
||
(setq others (cons keyword others))))))
|
||
|
||
(defvar sh-mode--treesit-declaration-commands
|
||
'("declare" "typeset" "export" "readonly" "local")
|
||
"Keywords in declaration commands.")
|
||
|
||
(defvar sh-mode--treesit-settings
|
||
(treesit-font-lock-rules
|
||
:feature 'comment
|
||
:language 'bash
|
||
'((comment) @font-lock-comment-face)
|
||
|
||
:feature 'function
|
||
:language 'bash
|
||
'((function_definition name: (word) @font-lock-function-name-face))
|
||
|
||
:feature 'string
|
||
:language 'bash
|
||
'([(string) (raw_string)] @font-lock-string-face)
|
||
|
||
:feature 'string-interpolation
|
||
:language 'bash
|
||
:override t
|
||
'((command_substitution) @sh-quoted-exec
|
||
(string (expansion (variable_name) @font-lock-variable-use-face)))
|
||
|
||
:feature 'heredoc
|
||
:language 'bash
|
||
'([(heredoc_start) (heredoc_body)] @sh-heredoc)
|
||
|
||
:feature 'variable
|
||
:language 'bash
|
||
'((variable_name) @font-lock-variable-name-face)
|
||
|
||
:feature 'keyword
|
||
:language 'bash
|
||
`(;; keywords
|
||
[ ,@sh-mode--treesit-keywords ] @font-lock-keyword-face
|
||
;; reserved words
|
||
(command_name
|
||
((word) @font-lock-keyword-face
|
||
(:match
|
||
,(rx-to-string
|
||
`(seq bol
|
||
(or ,@(sh-mode--treesit-other-keywords))
|
||
eol))
|
||
@font-lock-keyword-face))))
|
||
|
||
:feature 'command
|
||
:language 'bash
|
||
`(;; function/non-builtin command calls
|
||
(command_name (word) @font-lock-function-name-face)
|
||
;; builtin commands
|
||
(command_name
|
||
((word) @font-lock-builtin-face
|
||
(:match ,(let ((builtins
|
||
(sh-feature sh-builtins)))
|
||
(rx-to-string
|
||
`(seq bol
|
||
(or ,@builtins)
|
||
eol)))
|
||
@font-lock-builtin-face))))
|
||
|
||
:feature 'declaration-command
|
||
:language 'bash
|
||
`([,@sh-mode--treesit-declaration-commands] @font-lock-keyword-face)
|
||
|
||
:feature 'constant
|
||
:language 'bash
|
||
'((case_item value: (word) @font-lock-constant-face)
|
||
(file_descriptor) @font-lock-constant-face)
|
||
|
||
:feature 'operator
|
||
:language 'bash
|
||
`([,@sh-mode--treesit-operators] @font-lock-operator-face)
|
||
|
||
:feature 'builtin-variable
|
||
:language 'bash
|
||
`(((special_variable_name) @font-lock-builtin-face
|
||
(:match ,(let ((builtin-vars (sh-feature sh-variables)))
|
||
(rx-to-string
|
||
`(seq bol
|
||
(or ,@builtin-vars)
|
||
eol)))
|
||
@font-lock-builtin-face)))
|
||
|
||
:feature 'number
|
||
:language 'bash
|
||
`(((word) @font-lock-number-face
|
||
(:match "\\`[0-9]+\\'" @font-lock-number-face)))
|
||
|
||
:feature 'bracket
|
||
:language 'bash
|
||
'((["(" ")" "((" "))" "[" "]" "[[" "]]" "{" "}"]) @font-lock-bracket-face)
|
||
|
||
:feature 'delimiter
|
||
:language 'bash
|
||
'(([";" ";;"]) @font-lock-delimiter-face)
|
||
|
||
:feature 'misc-punctuation
|
||
:language 'bash
|
||
'((["$"]) @font-lock-misc-punctuation-face))
|
||
"Tree-sitter font-lock settings for `sh-mode'.")
|
||
|
||
(provide 'sh-script)
|
||
;;; sh-script.el ends here
|