;;; sh-script.el --- shell-script editing commands for Emacs -*- lexical-binding:t -*-
-;; Copyright (C) 1993-1997, 1999, 2001-2015 Free Software Foundation, Inc.
+;; Copyright (C) 1993-1997, 1999, 2001-2016 Free Software Foundation,
+;; Inc.
;; Author: Daniel Pfeiffer <occitan@esperanto.org>
;; Version: 2.0f
(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")
. ((nil
;; function FOO
;; function FOO()
- "^\\s-*function\\s-+\\\([[:alpha:]_][[:alnum:]_]*\\)\\s-*\\(?:()\\)?"
+ "^\\s-*function\\s-+\\([[:alpha:]_][[:alnum:]_]*\\)\\s-*\\(?:()\\)?"
1)
;; FOO()
(nil
(concat "\\(?:"
;; function FOO
;; function FOO()
- "^\\s-*function\\s-+\\\([[:alpha:]_][[:alnum:]_]*\\)\\s-*\\(?:()\\)?"
+ "^\\s-*function\\s-+\\([[:alpha:]_][[:alnum:]_]*\\)\\s-*\\(?:()\\)?"
"\\)\\|\\(?:"
;; FOO()
"^\\s-*\\([[:alpha:]_][[:alnum:]_]*\\)\\s-*()"
"\\(?:\\(?:.*[^\\\n]\\)?\\(?:\\\\\\\\\\)*\\\\\n\\)*.*")
(defconst sh-here-doc-open-re
- (concat "<<-?\\s-*\\\\?\\(\\(?:['\"][^'\"]+['\"]\\|\\sw\\|[-/~._]\\)+\\)"
+ (concat "[^<]<<-?\\s-*\\\\?\\(\\(?:['\"][^'\"]+['\"]\\|\\sw\\|[-/~._]\\)+\\)"
sh-escaped-line-re "\\(\n\\)")))
(defun sh--inside-noncommand-expression (pos)
(pcase (char-after)
(?\' (pcase state
(`double-quote nil)
- (_ (forward-char 1) (skip-chars-forward "^'" limit))))
+ (_ (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)))
:type 'hook
:group 'sh-script)
-(defcustom sh-mode-hook nil
+(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-learn-basic-offset nil
(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 '-'."
+punctuation characters like `-'."
(concat word "\\([^-[:alnum:]_]\\|$\\)"))
(defconst sh-re-done (sh-mkword-regexpr "done"))
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-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
\\[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.
+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
(setq-local syntax-propertize-function #'sh-syntax-propertize-function)
(add-hook 'syntax-propertize-extend-region-functions
#'syntax-propertize-multiline 'append 'local)
- (sh-electric-here-document-mode 1)
(setq-local skeleton-pair-alist '((?` _ ?`)))
(setq-local skeleton-pair-filter-function 'sh-quoted-p)
(setq-local skeleton-further-elements
(setq-local skeleton-filter-function 'sh-feature)
(setq-local skeleton-newline-indent-rigidly t)
(setq-local defun-prompt-regexp
- (concat "^\\(function[ \t]\\|[[:alnum:]]+[ \t]+()[ \t]+\\)"))
+ (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)
((string-match "[.]sh\\>" buffer-file-name) "sh")
((string-match "[.]bash\\>" buffer-file-name) "bash")
((string-match "[.]ksh\\>" buffer-file-name) "ksh")
- ((string-match "[.]csh\\>" buffer-file-name) "csh")
+ ((string-match "[.]t?csh\\(rc\\)?\\>" buffer-file-name) "csh")
((equal (file-name-nondirectory buffer-file-name) ".profile") "sh")
(t sh-shell-file))
nil nil)
;; Pretend the here-document is a "newline representing a
;; semi-colon", since the here-doc otherwise covers the newline(s).
";")
- (let ((semi (sh-smie--newline-semi-p)))
- (forward-line 1)
- (if (or semi (eobp)) ";"
- (sh-smie-sh-forward-token))))
+ (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))
(t tok)))))))
(defcustom sh-indent-after-continuation t
- "If non-nil, try to make sure text is indented after a line continuation."
- :version "24.3"
- :type 'boolean
+ "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 nil :tag "Never")
+ (const t :tag "Only if needed to make it deeper")
+ (const always :tag "Always"))
:group 'sh-indentation)
(defun sh-smie--continuation-start-indent ()
(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-indentation))))
+ (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-indentation) max))
+ nil)))))
+ indent))))))
+
(defun sh-smie-sh-rules (kind token)
(pcase (cons kind token)
(`(:elem . basic) sh-indentation)
(`(:after . "case-)") (- (sh-var-value 'sh-indent-for-case-alt)
(sh-var-value 'sh-indent-for-case-label)))
- ((and `(:before . ,_)
- ;; After a line-continuation, make sure the rest is indented.
- (guard sh-indent-after-continuation)
- (guard (save-excursion
- (ignore-errors
- (skip-chars-backward " \t")
- (sh-smie--looking-back-at-continuation-p))))
- (let initial (sh-smie--continuation-start-indent))
- (guard (let* ((sh-indent-after-continuation nil)
- (indent (smie-indent-calculate)))
- (and (numberp indent) (numberp initial)
- (<= indent initial)))))
- `(column . ,(+ initial sh-indentation)))
(`(:before . ,(or `"(" `"{" `"[" "while" "if" "for" "case"))
(if (not (smie-rule-prev-p "&&" "||" "|"))
(when (smie-rule-hanging-p)
;; 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 `"(" `"{" `"[")) (sh-var-value 'sh-indent-after-open))
+ (`(: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.
))
Point should be before the newline."
(save-excursion
(let ((tok (funcall smie-backward-token-function)))
- (if (or (when (equal tok "not") (forward-word 1) t)
+ (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)))))
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."
+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 "Shell \(default %s\): "
+ (format "Shell (default %s): "
sh-shell-file)
;; This used to use interpreter-mode-alist, but that is
;; no longer appropriate now that uses regexps.
(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")
"Return indent-info for this line.
This is a list. nil means the line is to be left as is.
Otherwise it contains one or more of the following sublists:
-\(t NUMBER\) NUMBER is the base location in the buffer that indentation is
+\(t NUMBER) NUMBER is the base location in the buffer that indentation is
relative to. If present, this is always the first of the
sublists. The indentation of the line in question is
derived from the indentation of this point, possibly
modified by subsequent sublists.
-\(+ VAR\)
-\(- VAR\) Get the value of variable VAR and add to or subtract from
+\(+ VAR)
+\(- VAR) Get the value of variable VAR and add to or subtract from
the indentation calculated so far.
-\(= VAR\) Get the value of variable VAR and *replace* the
+\(= VAR) Get the value of variable VAR and *replace* the
indentation with its value. This only occurs for
special variables such as `sh-indent-comment'.
STRING This is ignored for the purposes of calculating
(setq prev (point))
))
;; backward-sexp failed
- (if (zerop (skip-chars-backward " \t()[\]{};`'"))
+ (if (zerop (skip-chars-backward " \t()[]{};`'"))
(forward-char -1))
(if (bolp)
(let ((back (sh-prev-line nil)))