Updated:
Indentation support for shell-script modes for some shells
This is an "add-on" for sh-script.el to implementation indentation
for some shells. csh derivatives are not supported, but sh
rc derived shells are.
Eventually I'd like to see this merged into sh-script.el, but for
now it must be loaded separately, after which it is invoked
automatically from sh-script mode's hook.
Suggested Setup
One way would simply be to load shindent.el, by (assuming it is in
your load-path):
(require 'shindent)
Alternatively, you can arrange for shindent to be loaded whenever
sh-script.el is loaded, like this:
(eval-after-load "sh-script"
'(require 'shindent))
Customization:
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
shi-indent-for-XXX and shi-indent-after-XXX. For example.
shi-indent-after-if controls the indenting of a line following
and if statement, and shi-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 use the symbol '+ or '- which used the "default" value (or its
negative) which is the value in variable `shi-basic-offset'.
By changing this one variable you can increase or decrease how much
indentation there is.
Examples are sometimes handy; some possibilities for indenting
an sh if statement are:
if [ aaa ] ; then
bbb
else
ccc
fi
ddd
if [ aaa ] ; then
bbb
else
ccc
fi
ddd
If you like `then' on a line by itself, you could have (shown side by
side for brevity):
if [ aaa ] if [ aaa ] if [ aaa ] if [ aaa ]
then then then then
bbb bbb bbb bbb
else else else else
ccc ccc ccc ccc
fi fi fi fi
ddd ddd ddd ddd
There are 4 commands to help set these variables:
shi-show-indent
This shows what variable controls the indentation of the current
line and its value.
shi-set-indent
This allows you to set the value of the variable controlling the
current line's indentation. You can enter a number, or one of a
number of special symbols to denote the value of shi-basic-offset,
or its negative, or half it, or twice it, or.. If you've used
cc-mode this should be familiar.
shi-learn-line-indent
Simply make the line look the way you want it, then invoke this
function. It will set the value of the variable to the value
that makes the line indent like that. If the value is that of
shi-basic-offset it will set it to the symbol `+' and so on.
shi-learn-buffer-indent
This is the deluxe function! It "learns" the whole buffer (use
narrowing if you want it to process only part). It outputs to a
buffer *indent* any conflicts it finds, and then outputs to it all
the variables it has learned. This buffer is a sort of Occur mode
buffer, allowing you to easily find where something was set. It is
popped to automatically if there are any conflicts found or if
`shi-popup-occur-buffer' is non-nil. It will attempt to learn
`shi-indent-comment' if all comments follow the same pattern; if
they don't it sets it to nil. It can be made to set
`shi-basic-offset' depending on how variable
`shi-learn-basic-offset' is set. If it does not set it, it will
write into the *indent* buffer its guess or guesses if it finds
some.
Unfortunately, this command can take a long time to run
(e.g. if there are large case statements). Perhaps it does not
make sense to run it on large buffers: if lots of lines have
different indentation styles it will produce a lot of
diagnostics in the *indent* buffer; if there is a consistent
style then running `shi-learn-buffer-indent' on a small region
of the buffer should suffice. Maybe.
These commands are not bound by default, but can easily be done
so using a hook.
Hooks
When hook `shi-set-shell-hook' is called the shell type is known.
Example use:
(defun my-shi-set-shell-hook ()
(message (format "shell type: %s shi-basic-offset it %s"
sh-shell shi-basic-offset))
(local-set-key '[f9] 'shi-show-indent)
(local-set-key '[f10] 'shi-set-indent)
(local-set-key '[f11] 'shi-learn-line-indent)
(local-set-key '[f12] 'shi-learn-buffer-indent)
)
(add-hook 'shi-set-shell-hook 'my-shi-set-shell-hook)
`shi-hook' is, I think, obsolete now and may be removed.
Saving indentation values...
After you've learned the values in a buffer, how to you remember
them? Originally I had hoped that `shi-learn-buffer-indent'
would make this unnecessary; simply learn the values when you visit
the buffer.
You can do this automatically like this:
(add-hook 'shi-set-shell-hook 'shi-learn-buffer-indent)
However...
For one thing, `shi-learn-buffer-indent' is extremely slow,
especially on large-ish buffer. Also, if there are conflicts the
"last one wins" which may not produce the desired setting.
So...
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.
Note: This should be considered tentative at best...
It is a bit like cc-mode's styles, but the user interface is
different and the internal format is different. I am assuming that
while one may deal with only a few different styles of C, one may
be faced with a bigger variety of shell script settings. Perhaps
this is not true. Anyway, this is how to use it.
Use `shi-name-style' to give a name to the indentation settings of
the current buffer.
Use `shi-load-style' to load indentation settings for the current
buffer from a specific style.
Use `shi-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 `shi-learn-buffer-indent'. 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 `shi-make-vars-local' and 2
functions: `shi-make-vars-local' and
`shi-reset-indent-vars-to-global-values' .
If `shi-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 `shi-make-vars-local'.
Conversely, to update with the global values you can use the
command `shi-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 "$#".
2. Setting the text property makes the buffer modified, and cannot
be done in a read-only buffer. Since you may start visiting a
read-only buffer and then want to change something, we rescan
the buffer when going from read-only to non read-only. Since I
couldn't find a suitable hook, I constructed one (for
toggle-read-only) using `advice'.
BUGS:
- Here-documents are marked with text properties face and syntax
table. This serves 2 purposes: stopping indentation while inside
them, and moving over them when finding the previous line to
indent to. However, if font-lock mode is active when there is
any change inside the here-document font-lock clears that
property. This causes several problems: lines after the here-doc
will not be re-indentation properly, words in the here-doc region
may be fontified, and indentation may occur within the
here-document.
I'm not sure how to fix this, perhaps using the point-entered
property. Anyway, if you use font lock and change a
here-document, I recommend using M-x shi-rescan-buffer after the
changes are made. Similarly, when using higlight-changes-mode,
changes inside a here-document may confuse shindent, but again
using `shi-rescan-buffer' should fix them.
- Indenting many lines is slow. It currently does each line
independantly, rather than saving state information.
- `shi-learn-buffer-indent' is extremely slow.
Possible Future Plans:
- integrate with sh-script.el.
- have shi-get-indent-info optionally use and return state info to
make indent-region less slow.
Richard Sharman June 1999.
Newer versions may be availabale in
http://pobox.com/~rsharman/emacs/