Seek Seek Revolution
The script was modified on disk while running, but it wasn't re-read. Only if I run it a second time do I get a surprise.
>
Have I misunderstood something?
That unix shell scripts read linewise is part of the trick. How the file is edited is also important; some programs use a rename(2) to move the update in place, which creates a new inode (possibly one with a different security context, which might be bad). This atomicity is usually highly desirable, but not here. Also the -i flag to sed is not portable as it was copied in various ways from perl(1) by various folks at various times. Thus the scripts shown below may need porting to other operating systems that pass for unix these days.
$ uname;uname -r
OpenBSD
7.7
$ cp orig selfedit
$ cat selfedit
#!/bin/sh
echo start
sed -i 's/middling/surprise/' selfedit
echo middling
echo end
$ ls -i selfedit
9506196 selfedit
$ sh selfedit
start
middling
end
$ ls -i selfedit
9506168 selfedit
"selfedit" now has a new inode number, which means the old inode went away, or will eventually go away once any programs holding onto it exit or close the file descriptor. This is how one can restore "removed but still held open files", by getting to the still-open file somehow (under /proc on a Linux system, or maybe by attaching a debugger).
If we write a program that holds a file open, we should be able to see the change, or a lack of change should the editing program instead create a new inode.
#!/usr/bin/perl
# sirholdsalot - hold a file open until USR1 signal
my $file = shift // die "Usage: $0 file-to-hold-open\n";
open my $fh, '<', $file or die "open '$file': $!\n";
my $show_file = 0;
$SIG{USR1} = sub { $show_file = 1 };
do { sleep 3 } until $show_file;
print while readline $fh;
Thus run,
$ cp orig selfedit
$ perl sirholdsalot selfedit &
[1] 75608
$ sh selfedit
start
middling
end
$ pgrep -lf perl
75608 perl sirholdsalot selfedit
20020 /usr/bin/perl /usr/local/libexec/smtpd/cuxselgre
$ kill -USR1 75608
$ #!/bin/sh
echo start
sed -i 's/middling/surprise/' selfedit
echo middling
echo end
[1] + Done perl sirholdsalot selfedit
$ cat selfedit
#!/bin/sh
echo start
sed -i 's/surprise/surprise/' selfedit
echo surprise
echo end
the sirholdsalot script shows the original "middling" after the file has been updated by `sh selfedit`.
The next step is to edit the original file without a new inode like `sed -i` does, or may do. This can be tricky, as a naive "< file sed ... > file"† will clobber the file you're trying to read from, hence sponge(1) of moreutils having been written. Again, a lot of modern programs will use a rename(2) from a temporary file so that there's never a half-edited file seen by some other process. Assuming moreutils is installed,
$ cp orig selfedit
$ perl sirholdsalot selfedit &
[1] 86123
$ ls -i selfedit
9506217 selfedit
$ < selfedit sed 's/middling/surprise/' | sponge selfedit
$ ls -i selfedit
9506217 selfedit
$ kill -USR1 86123
$ #!/bin/sh
echo start
sed -i 's/surprise/surprise/' selfedit
echo surprise
echo end
[1] + Done perl sirholdsalot selfedit
the inode is not changed by the edit, and sirholdsalot now shows the edit to the file. What happens if we modify script without changing the inode?
$ cat orig2
#!/bin/sh
echo start
< selfedit2 sed 's/[m]iddling/surprise/' | sponge selfedit2
echo middling
echo end
$ cp orig2 selfedit2
$ sh selfedit2
start
middling
end
Nope, still not there. What if we change how the script is run?
$ cp orig2 selfedit2
$ sh < selfedit2
start
surprise
end
Another important point is that file descriptor objects can be shared between processes, and thus what process A does to that object (e.g. with lseek(2)) will influence process B.
It may be helpful to use ktrace or strace or similar to see what system calls are going on whilst these various programs are running. This can be verbose, though one can usually drill down to see what is going on for the specific filenames or file descriptors involved.
—
† "< foo program arg > bar" is mostly the same as "program arg < foo > bar" or "program > bar arg < foo", unless program is a while loop, in which case zsh does what you want while other shells will most likely yell at you.