Yes shells, and bash
in particular, are careful to read the file one line at a time, so it works the same as when you use it interactively.
You'll notice that when the file is not seekable (like a pipe), bash
even reads one byte at a time to be sure not to read past the \n
character. When the file is seekable, it optimises by reading full blocks at a time, but seek back to after the \n
.
That means you can do things like:
bash << \EOF
read var
var's content
echo "$var"
EOF
Or write scripts that update themselves. Which you wouldn't be able to do if it didn't give you that guarantee.
Now, it's rare that you want to do things like that and, as you found out, that feature tends to get in the way more often than it is useful.
To avoid it, you could try and make sure you don't modify the file in-place (for instance, modify a copy, and move the copy in place (like sed -i
or perl -pi
and some editors do for instance)).
Or you could write your script like:
{
sleep 20
echo test
}; exit
(note that it's important that the exit
be on the same line as }
; though you could also put it inside the braces just before the closing one).
or:
main() {
sleep 20
echo test
}
main "$@"; exit
The shell will need to read the script up until the exit
before starting to do anything. That ensures the shell will not read from the script again.
That means the whole script will be stored in memory though.
That can also affect the parsing of the script.
For instance, in bash
:
export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'
Would output that U+00E9 encoded in UTF-8. However, if you change it to:
{
export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'
}
The \ue9
will be expanded in the charset that was in effect at the time that command was parsed which in this case is before the export
command is executed.
Also note that if the source
aka .
command is used, with some shells, you'll have the same kind of problem for the sourced files.
That's not the case of bash
though whose source
command reads the file fully before interpreting it. If writing for bash
specifically, you could actually make use of that, by adding at the start of the script:
if [[ ! $already_sourced ]]; then
already_sourced=1
source "$0"; exit
fi
(I wouldn't rely on that though as you could imagine future versions of bash
could change that behaviour which can be currently seen as a limitation (bash and AT&T ksh are the only POSIX-like shells that behave like that as far as can tell) and the already_sourced
trick is a bit brittle as it assumes that variable is not in the environment, not to mention that it affect the content of the BASH_SOURCE variable)
\n
would do the trick? Maybe a subshell()
will do? I'm not very experienced with it, please help! – VasyaNovikov Dec 21 '16 at 07:59sleep 20 ;\n echo test ;\n sleep 20
and I start editing it, it may misbehave. For example, bash could read the first 10 bytes of the script, understand thesleep
command and go to sleep. After it resumes, there would be different contents in the file starting at 10 bytes. – VasyaNovikov Dec 21 '16 at 08:01