Skip to content

Instantly share code, notes, and snippets.

@hmason
Created August 21, 2012 22:01
Show Gist options
  • Select an option

  • Save hmason/3419831 to your computer and use it in GitHub Desktop.

Select an option

Save hmason/3419831 to your computer and use it in GitHub Desktop.
Did you know that bash will reload a script *while it is executing*?!
#!/bin/bash
function addnext {
NUM=$1
sleep 1
echo HI $NUM
NUM=$(expr $NUM + 1)
echo addnext $NUM >> $0
}
addnext 1
@unwiredben

Copy link
Copy Markdown

is it really reloading? Maybe it's just that it's reading the script file in a non-exclusive mode a line at a time with no look-ahead caching, and the constant adding of a line by addnext means you never hit EOF and terminate the script.

@ploxiln

ploxiln commented Aug 21, 2012

Copy link
Copy Markdown

yeah it's really reloading. in another less-contained example, you could just replace the entire file, and it picks up at the line number it left off on

@ploxiln

ploxiln commented Aug 21, 2012

Copy link
Copy Markdown

actually, I thought we had seen it work when replacing the entire file, but I can't seem to come up with a contrived example that works...

@ploxiln

ploxiln commented Aug 21, 2012

Copy link
Copy Markdown

ok try this:

#!/bin/bash
echo >testA.sh
echo >testB.sh
for I in $(seq 8) ; do
    echo "sleep 1; echo A $I" >>testA.sh
    echo "sleep 1; echo B $I" >>testB.sh
done

bash testA.sh &
sleep 4
cp testB.sh testA.sh
sleep 5

It doesn't work if the "cp" is "mv" instead, so you're right. If the filename gets a new inode, bash isn't affected (that's what "mv" does). But "cp", a very common way to replace a script, just opens the existing file, truncates it, and rewrites it, and bash's file read pointer stays somewhere in the middle.

@phooky

phooky commented Aug 21, 2012

Copy link
Copy Markdown

Works in dash, too (once the 'function' bashism is removed). Neat!

@phooky

phooky commented Aug 21, 2012

Copy link
Copy Markdown

Oh, I could go so deep down this rabbit hole:

!/bin/sh

addnext() {
NUM=$1
sleep 1
echo pew
NUM=$(expr $NUM + 1)
cat $0 | sed 's/\ p\ew/\ p\ewp\ew/' >tmp.sh
echo addnext $NUM >> tmp.sh
cat tmp.sh >$0
}

addnext 1

@rianhunter

Copy link
Copy Markdown

nothing is surprising about this. bash doesn't need to preprocess its source (there's no bash vm) so why would it first buffer the entire file (which could be very large)?

even if it did buffer the entire file, it would pick up new changes as the file was appended to as it was buffering.

@RichardBronosky

Copy link
Copy Markdown

Not to split hairs here, but because the file lacks an EOL, this does not work until you edit the file and add a \n to it.

curl -LO https://gist.githubusercontent.com/hmason/3419831/raw/test.sh; chmod +x ./test.sh; ./test.sh

In other words...

$ curl -LO https://gist.githubusercontent.com/hmason/3419831/raw/test.sh; chmod +x test.sh; ./test.sh
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   122  100   122    0     0    346      0 --:--:-- --:--:-- --:--:--   346
HI 1

$ cat test.sh
#!/bin/bash

function addnext {
	NUM=$1
	sleep 1
	echo HI $NUM
	NUM=$(expr $NUM + 1)
	echo addnext $NUM >> $0
}

addnext 1addnext 2

$ curl -LO https://gist.githubusercontent.com/hmason/3419831/raw/test.sh; chmod +x test.sh; echo >> test.sh; ./test.sh
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   122  100   122    0     0    321      0 --:--:-- --:--:-- --:--:--   321
HI 1
HI 2
HI 3
HI 4
HI 5
HI 6
HI 7
HI 8
HI 9
HI 10
...

@ngrigoriev

ngrigoriev commented May 16, 2018

Copy link
Copy Markdown

A simple strace shows that it is not reloading anything.

script:

#!/bin/bash

num=0

while [[ $num -lt 20 ]] ; do
        echo "waiting...$num"
        sleep 1s
        num=$(( $num + 1 ))
done


echo "I am done!"
open("./run.sh", O_RDONLY)              = 3
ioctl(3, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffda02d2010) = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
read(3, "#!/bin/bash\n\nnum=0\n\nwhile [[ $nu"..., 80) = 80
lseek(3, 0, SEEK_SET)                   = 0
getrlimit(RLIMIT_NOFILE, {rlim_cur=1024, rlim_max=4*1024}) = 0
fcntl(255, F_GETFD)                     = -1 EBADF (Bad file descriptor)
dup2(3, 255)                            = 255
close(3)                                = 0
fcntl(255, F_SETFD, FD_CLOEXEC)         = 0
fcntl(255, F_GETFL)                     = 0x8000 (flags O_RDONLY|O_LARGEFILE)
fstat(255, {st_mode=S_IFREG|0775, st_size=128, ...}) = 0
lseek(255, 0, SEEK_CUR)                 = 0
...
read(255, "#!/bin/bash\n\nnum=0\n\nwhile [[ $nu"..., 128) = 128
...
lseek(255, -20, SEEK_CUR)               = 108
...
read(255, "\n\necho \"I am done!\"\n", 128) = 20
read(255, "", 128)                      = 0
exit_group(0)                           = ?

I did not check bash source code but I would imagine it would never read the entire script in memory. It should read it by chunks as it executes it. I am not sure why it does that lseek() but probably just to go to the beginning of the last line and continue reading from there. Just a guess. So, the behaviour of the script "changing" one the fly would depend on how the filesystem react to the concurrent reading and writing.

I may be wrong, this is just my guess based on what I would expect and what I see in strace on Linux....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment