[comp.unix.shell] cat, pipes, and filters

root@progress.COM (Root of all Evil) (05/31/91)

Hi,

   I've got a question regarding the way cat behaves in a pipeline.
(I know, his fur gets all oily %+})  Can I cat the contents of a
file | pipe the output to a filter (such as sed) | then cat the
filtered output back to the original file?  I've tried this with the
following commands:

   cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ > $FILE

   cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ | cat > $FILE

Both command produce identical results: $FILE is truncated to 0-length.

   However, the following command gives me the result I want:

   cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ | tee $FILE 1>/dev/null

So now my script works but I don't really understand why.  I've tested
this on SunOs 4.1 and Interactive SysV 2.2, no difference.  Is there
something simple about pipes or I/O redirection that I'm not grasping?
Or is this a feature of cat?  

   Any enlightenment would be appreciated.  Also, if you can think of
a better way to do the same thing (short of using perl), please let
me know.

                              Curious Rich

------------------------------------------------------------------------------
Rich Lenihan                 UUCP: mit-eddie!progress!rich
Progress Software Corp.      Internet: rich@progress.com
5 Oak Park                   >-   Insert funny stuff here  -<
Bedford, MA  01730           >-Draw amusing symbols, logo's-<
USA                          >-     or characters here     -<

jik@cats.ucsc.edu (Jonathan I Kamens) (06/01/91)

In article <1991May31.165446.1530@progress.com>, root@progress.COM (Root of all Evil) writes:
|>    cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ > $FILE
|> 
|>    cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ | cat > $FILE

In this case, you're using shell redirection to output to the file.  Shell
redirection takes place before any of the commands on the command line
actually get run.  Therefore, the shell opens $FILE and truncates it in
preparation for sending output to it *before* the "cat" at the beginning of
the pipeline opens it in order to read it.

|>    cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ | tee $FILE 1>/dev/null

In this case, cat gets started and opens the file in order to read it, and
*then* tee gets started, sees that it's supposed to send output to $FILE, and
opens the file and truncates it in preparation for sending output to it.  Once
cat has opened the file for read, it gets to read it, because the file that
tee is sending output to is (in effect) a "different file" from the kernel's
point of view.

|> So now my script works but I don't really understand why.  I've tested
|> this on SunOs 4.1 and Interactive SysV 2.2, no difference.  Is there
|> something simple about pipes or I/O redirection that I'm not grasping?
|> Or is this a feature of cat?  

Quoting from the man page cat(1):

NOTES
     Beware of `cat a b > a' and `cat a b > b', which destroy the
     input files before reading them.

tchrist@convex.COM (Tom Christiansen) (06/01/91)

From the keyboard of root@progress.COM (Root of all Evil):
:
:Hi,
:
:   I've got a question regarding the way cat behaves in a pipeline.
:(I know, his fur gets all oily %+})  Can I cat the contents of a
:file | pipe the output to a filter (such as sed) | then cat the
:filtered output back to the original file?  I've tried this with the
:following commands:
:
:   cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ > $FILE
:
:   cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ | cat > $FILE
:
:Both command produce identical results: $FILE is truncated to 0-length.
:
:   However, the following command gives me the result I want:
:
:   cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ | tee $FILE 1>/dev/null
:
:So now my script works but I don't really understand why.  I've tested
:this on SunOs 4.1 and Interactive SysV 2.2, no difference.  Is there
:something simple about pipes or I/O redirection that I'm not grasping?

yes.

:Or is this a feature of cat?  
:
:   Any enlightenment would be appreciated.  Also, if you can think of
:a better way to do the same thing (short of using perl), please let
:me know.

hm.... with that kind of proviso, i shouldn't answer.  :-)/2

you need to think about when things are getting opened.  the
shell opens your files before i calls the command.

so either use a temporary, cheat with tee, or else use

    perl -p -i -e "s/$ENTRY/$NEWENTRY/" $FILE

--tom
--
Tom Christiansen		tchrist@convex.com	convex!tchrist
		"So much mail, so little time." 

wallace@ynotme.enet.dec.com (Ray Wallace) (06/01/91)

In article <1991May31.165446.1530@progress.com>, root@progress.COM (Root of all Evil) writes...
>   I've got a question regarding the way cat behaves in a pipeline.
> 
>   cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ > $FILE
>Both command produce identical results: $FILE is truncated to 0-length.
One of the first things the shell does when parsing the line, is to handle I/O
redirection. So before any of the commands are executed the ">$FILE" part of
the line causes the shell to create an empty file which just happens to be
the file that you are trying to read (cat).

>   Any enlightenment would be appreciated.  Also, if you can think of
>a better way to do the same thing (short of using perl), please let
  cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ > ${FILE}.new ; mv ${FILE}.new $FILE
is a different way, not neccessarily a better way.

---
Ray Wallace		
		(INTERNET,UUCP) wallace@oldtmr.enet.dec.com
		(UUCP)		...!decwrl!oldtmr.enet!wallace
		(INTERNET)	wallace%oldtmr.enet@decwrl.dec.com
---

marc@mercutio.ultra.com (Marc Kwiatkowski {Host Software-AIX}) (06/01/91)

In article <1991May31.165446.1530@progress.com> root@progress.COM (Root of all Evil) writes:

	[stuff about inability to redirect standard in and standard
	 out to the same file omitted.]

This problem and a good solution are discussed thoroughly in 
"The Unix Programming Environment, B.W. Kernighan and R. Pike, 
Prentice Hall, ISBN 0-13-937699-2."  They provide a generalized 
bourne shell script that will work with any arbitrary command, and is 
fairly robust, called "overwrite"

-- 
        Marc P. Kwiatkowski			Ultra Network Technologies
        Internet: marc@ultra.com		101 Daggett Drive
        uucp: ...!ames!ultra!marc		San Jose, CA 95134 USA
        telephone: 408 922 0100 x249

torek@elf.ee.lbl.gov (Chris Torek) (06/01/91)

>In article <1991May31.165446.1530@progress.com> root@progress.COM
>(Root of all Evil) writes:
>>    cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ | tee $FILE 1>/dev/null

In article <16438@darkstar.ucsc.edu> jik@cats.ucsc.edu [ucsc?!]
(Jonathan I Kamens) writes:
>In this case, cat gets started and opens the file in order to read it, and
>*then* tee gets started, sees that it's supposed to send output to $FILE, and
>opens the file and truncates it in preparation for sending output to it.  Once
>cat has opened the file for read, it gets to read it, because the file that
>tee is sending output to is (in effect) a "different file" from the kernel's
>point of view.

Although the two processes hold different file table entries, they use
the same vnode and hence this is not a reliable way of doing things.
Depending on O/S vagaries, cat might read some, all, or none of $FILE
before tee truncates it; when tee truncates $FILE, anything cat has not
yet read will vanish forever.

As it happens, on most Unix boxes cat will run long enough to get the
first `block' (however big a block is, typically 8K under 4BSD-
derivative file systems) before sed will run, and sed will run long
enough to wait for cat before tee will run.  It is unwise to depend
on this.
-- 
In-Real-Life: Chris Torek, Lawrence Berkeley Lab CSE/EE (+1 415 486 5427)
Berkeley, CA		Domain:	torek@ee.lbl.gov

brnstnd@kramden.acf.nyu.edu (Dan Bernstein) (06/02/91)

In article <13798@dog.ee.lbl.gov> torek@elf.ee.lbl.gov (Chris Torek) writes:
  [ why cat foo | blah | tee foo involves race conditions ]

More to the point, if you want to have blah read foo and write its
output to a new copy of foo, do something like this:

  ( rm foo; blah > foo ) < foo

---Dan

bp@chorus.fr (Bruno Pillard) (06/03/91)

From article <1991May31.165446.1530@progress.com>, by root@progress.COM

>    However, the following command gives me the result I want:
> 
>    cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ | tee $FILE 1>/dev/null
> 
>    Any enlightenment would be appreciated. 

What about:

  ( /bin/rm $FILE ; sed  s/"$ENTRY"/"$NEWENTRY"/ > $FILE ) < $FILE

I understand that this may look harmful at first glance because of the
/bin/rm of your precious file but it  works perfectly for  me under sh
and (t)csh.

Is there any problem using that construction ?

   __                       
  /  )                        Bruno Pillard
 /--<  __  . . __   __        Chorus Systemes
/___/_/ (_(_/_/) )_(_)        6 Avenue Gustave Eiffel
                              78182  St-Quentin-en-Yvelines-Cedex  France
bp@chorus.fr  (Internet)      Tel: +33 (1) 30 64 82 13

mmk@d62iwa.mitre.org (Morris M. Keesan) (06/04/91)

In article <1991May31.165446.1530@progress.com> root@progress.COM (Root of all Evil) writes:

[various ways to edit a file in place using sed as part of a pipeline, e.g.]
> cat $FILE | sed s/"$ENTRY"/"$NEWENTRY"/ > $FILE

It's interesting that most of the responses have pointed out the problem
(creating the new $FILE before reading the old one), but all have accepted your
initial premise, which is that "sed" is the appropriate tool for editing a file
in place.  It seems to me that the much more obvious approach is to use "ed",
which is designed for editing files, as "sed" isn't.
My approach, tested under Ultrix 4.1:

#!/bin/sh
#this replaces all occurrences of $ENTRY with $NEWENTRY
ed $FILE <<KICKME
g/$ENTRY/s//$NEWENTRY/g
w
q
KICKME

gwc@root.co.uk (Geoff Clare) (06/04/91)

In <10918@chorus.fr> bp@chorus.fr (Bruno Pillard) writes:

>What about:

>  ( /bin/rm $FILE ; sed  s/"$ENTRY"/"$NEWENTRY"/ > $FILE ) < $FILE

>I understand that this may look harmful at first glance because of the
>/bin/rm of your precious file but it  works perfectly for  me under sh
>and (t)csh.

>Is there any problem using that construction ?

Yes!  If any errors occur you lose your data.

Slightly better would be:

  ( /bin/rm $FILE && sed  s/"$ENTRY"/"$NEWENTRY"/ > $FILE ) < $FILE

which would not truncate the file if the "rm" fails, but this still
loses your data if the disk is full.  It's much safer to:

  sed  s/"$ENTRY"/"$NEWENTRY"/ < $FILE > $TMPFILE && mv $TMPFILE $FILE
-- 
Geoff Clare <gwc@root.co.uk>  (Dumb American mailers: ...!uunet!root.co.uk!gwc)
UniSoft Limited, London, England.   Tel: +44 71 729 3773   Fax: +44 71 729 3273

wolfgang@wsrcc.com (Wolfgang S. Rupprecht) (06/06/91)

>More to the point, if you want to have blah read foo and write its
>output to a new copy of foo, do something like this:
>  ( rm foo; blah > foo ) < foo

Will this work if "foo" is an NFS file?

-wolfgang
-- 
Wolfgang Rupprecht    wolfgang@wsrcc.com (or) uunet!wsrcc!wolfgang
Snail Mail Address:   Box 6524, Alexandria, VA 22306-0524