mirrored from git://git.sv.gnu.org/emacs.git
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
sh-script.el
3395 lines (3015 loc) · 120 KB
/
sh-script.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; 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))