关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

系统安装、升级讨论
版面规则
我们都知道新人的确很菜,也喜欢抱怨,并且带有浓厚的Windows习惯,但既然在这里询问,我们就应该有责任帮助他们解决问题,而不是直接泼冷水、简单的否定或发表对解决问题没有任何帮助的帖子。乐于分享,以人为本,这正是Ubuntu的精神所在。
回复
yuhao chen
帖子: 6
注册时间: 2014-03-05 9:01
系统: ubuntu12.04

关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#1

帖子 yuhao chen » 2014-04-19 15:55

起因:
在windows编了个exist.txt文本文件后,拷贝到Ubuntu下继续编辑,发现文本每行多了个 ^M (已经查阅知道原由),便有了去除 ^M 的想法。
1.使用的命令:
cat exist.txt | tr -d ^M > exist.txt
执行的结果是:
exist.txt 没返回错误信息,但exist.txt文件内容已经被清空。

2.使用的命令:
cat exist.txt | tr -d ^M > new.txt
执行的结果是:
exist.txt 没返回错误信息,^M 已经全部清除。

问题:
1和2的处理方式,产生的结果差异为何如此之大 :em20
头像
daf3707
论坛版主
帖子: 12739
注册时间: 2007-06-13 15:57
来自: 在他乡

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#2

帖子 daf3707 » 2014-04-19 16:00

windows与linux下的换行符是不一样的,
只需要用ldos2unix转换一下格式就OK了
cao627
帖子: 992
注册时间: 2007-12-05 10:57
系统: ubuntu14.04
来自: 金山

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#3

帖子 cao627 » 2014-04-19 17:39

cat exist.txt | tr -d ^M > exist.txt
是你这种方式犯了逻辑错误。
简单可这么理解:
你这里exist .txt文件就像一个水池,只有一个管道与外界相通, cat命令就好比取水池中的水(当然,单纯cat命令并不是将水池的水抽出来,使得水池空掉,这里是由于>命令本身有清空文件的作用,所以当 cat和>作用于一个文件时,可这么理解),然后经过tr处理 , 试图通过 >exit.txt, 将水灌回源处。
问题是你 > exist.txt 的时候,水池exist.txt 的管道还被cat占据着,水灌不进去,因此最终结果为水池空了
头像
leavfin
帖子: 599
注册时间: 2012-01-12 13:32

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#4

帖子 leavfin » 2014-04-19 18:03

cao627 写了:
cat exist.txt | tr -d ^M > exist.txt
是你这种方式犯了逻辑错误。
简单可这么理解:
你这里exist .txt文件就像一个水池,只有一个管道与外界相通, cat命令就好比取水池中的水,然后经过tr处理 , 试图通过 >exit.txt, 将水灌回源处。
问题是你 > exist.txt 的时候,水池exist.txt 的管道还被cat占据着,水灌不进去,因此最终结果为水池空了
以前也犯过这样的错误
输入输出为同一个目标,管道的流向循环了
《盗梦空间》里的相对的镜子
头像
astolia
论坛版主
帖子: 6703
注册时间: 2008-09-18 13:11

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#5

帖子 astolia » 2014-04-19 18:23

cao627 写了:
cat exist.txt | tr -d ^M > exist.txt
是你这种方式犯了逻辑错误。
简单可这么理解:
你这里exist .txt文件就像一个水池,只有一个管道与外界相通, cat命令就好比取水池中的水(当然,单纯cat命令并不是将水池的水抽出来,使得水池空掉,这里是由于>命令本身有清空文件的作用,所以当 cat和>作用于一个文件时,可这么理解),然后经过tr处理 , 试图通过 >exit.txt, 将水灌回源处。
问题是你 > exist.txt 的时候,水池exist.txt 的管道还被cat占据着,水灌不进去,因此最终结果为水池空了
这个和实际发生的事严重不符。

不存在什么灌不进去的问题,你可以用>>代替>试试

代码: 全选

$ cat a
aaa
$ cat a | tr z z >> a
$ cat a
aaa
aaa
实际情况是这样的:当你输入这个命令后,shell首先会进行解析,准备好整个命令中的管道、重定向等等东西。也就是说,重定向在cat具体执行之就已经准备完毕了。而不幸的是>重定向会将文件清空,所以cat 命令实际读到的exist.txt已经是个空文件了,tr也没啥事干,最终也没向exist.txt写入任何东西
头像
astolia
论坛版主
帖子: 6703
注册时间: 2008-09-18 13:11

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#6

帖子 astolia » 2014-04-19 18:45

可以写个简单的脚本做个实验:

代码: 全选

$ cat test.sh
#! /bin/bash -
echo -n "start" >&2
cat a >&2
echo "end" >&2

代码: 全选

$ cat a
aaa
$ ./test.sh | tr z z > b
startaaa
end
$ ./test.sh | tr z z > a
startend
cao627
帖子: 992
注册时间: 2007-12-05 10:57
系统: ubuntu14.04
来自: 金山

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#7

帖子 cao627 » 2014-04-19 19:21

重定向在cat具体执行之前就已经准备完毕了。而不幸的是>重定向会将文件清空,所以cat 命令实际读到的exist.txt已经是个空文件了
这个观点是对的
cat 命令经过管道 | 后不在抢占文件。
但cat file >file会出错
头像
astolia
论坛版主
帖子: 6703
注册时间: 2008-09-18 13:11

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#8

帖子 astolia » 2014-04-19 19:42

cao627 写了:
重定向在cat具体执行之前就已经准备完毕了。而不幸的是>重定向会将文件清空,所以cat 命令实际读到的exist.txt已经是个空文件了
这个观点是对的
cat 命令经过管道 | 后不在抢占文件。
但cat file >file会出错
不存在什么抢占文件的问题,这只是GNU cat自己的限制。不信你看看awk

代码: 全选

$ cat a	
aaa
$ awk 'BEGIN{print "bbb"}{print}END{print "ccc"}' a
bbb
aaa
ccc
$ awk 'BEGIN{print "bbb"}{print}END{print "ccc"}' a > a
$ cat a
bbb
ccc
再不信你看看GNU cat的源代码
http://git.savannah.gnu.org/cgit/coreut ... cat.c#n706

代码: 全选


      /* Compare the device and i-node numbers of this input file with
         the corresponding values of the (output file associated with)
         stdout, and skip this input file if they coincide.  Input
         files cannot be redirected to themselves.  */

      if (check_redirection
          && stat_buf.st_dev == out_dev && stat_buf.st_ino == out_ino
          && (input_desc != STDIN_FILENO))
        {
          error (0, 0, _("%s: input file is output file"), infile);
          ok = false;
          goto contin;
        }
之所以GNU cat要加这个限制,原因可能是看到了原来BSD cat的缺陷。因为BSD cat没有做这种安全检查,用 >>作重定向时,陷入无限循环,直到文件占满整个硬盘

现在仍然可以用awk或sed之类的工具重现这个问题。

代码: 全选

awk '{print}' a >> a
sed -n 'p' a >> a
如果a比较大(一般超过4KB就行了),awk/sed的缓冲区装不下,最终结果就会不断膨胀下去占满整个硬盘
cao627
帖子: 992
注册时间: 2007-12-05 10:57
系统: ubuntu14.04
来自: 金山

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#9

帖子 cao627 » 2014-04-19 20:03

了解。
我想当然了,将cat和> 等同与硬件层面读和写了。 :em01
殊不知从cat和>的读和写到磁盘磁头的读和写,中间隔着厚厚的操作系统。
yuhao chen
帖子: 6
注册时间: 2014-03-05 9:01
系统: ubuntu12.04

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#10

帖子 yuhao chen » 2014-04-19 21:44

多谢各位大大的热心帮助(真的没有想到回的这么快 :em11 )!!!
个人比较支持shell解析,优先实行重定向>对文件的清空这种说法。做了个实验:(又有问题了。。。 :em06
yuhao@yuhao-Latitude-3440:~$ touch a b
yuhao@yuhao-Latitude-3440:~$ cat > a
abc
yuhao@yuhao-Latitude-3440:~$ cat a | tr -d b > a
yuhao@yuhao-Latitude-3440:~$ cat a
ac

yuhao@yuhao-Latitude-3440:~$ cat a | tr -d c > a
yuhao@yuhao-Latitude-3440:~$ cat a

yuhao@yuhao-Latitude-3440:~$ cat > b
abc
yuhao@yuhao-Latitude-3440:~$ cat b | tr -d b > b
yuhao@yuhao-Latitude-3440:~$ cat b

yuhao@yuhao-Latitude-3440:~$ cat --version
cat (GNU coreutils) 8.13
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Torbjörn Granlund and Richard M. Stallman.
红色部分和以上说法冲突,.....BUG ,BUG :em02
头像
astolia
论坛版主
帖子: 6703
注册时间: 2008-09-18 13:11

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#11

帖子 astolia » 2014-04-19 22:51

yuhao chen 写了: 个人比较支持shell解析,优先实行重定向>对文件的清空这种说法。
没什么好比较的。自己man bash,查看REDIRECTION那段第一句话就说清楚了。我多年前学linux系统编程的时候也花一个晚上写过一个简单的shell玩儿,如果不在执行具体命令前预先建立管道和打开重定向文件,根本没办法保证数据的传递
yuhao chen 写了:做了个实验:(又有问题了。。。 :em06
yuhao@yuhao-Latitude-3440:~$ touch a b
yuhao@yuhao-Latitude-3440:~$ cat > a
abc
yuhao@yuhao-Latitude-3440:~$ cat a | tr -d b > a
yuhao@yuhao-Latitude-3440:~$ cat a
ac

yuhao@yuhao-Latitude-3440:~$ cat a | tr -d c > a
yuhao@yuhao-Latitude-3440:~$ cat a

yuhao@yuhao-Latitude-3440:~$ cat > b
abc
yuhao@yuhao-Latitude-3440:~$ cat b | tr -d b > b
yuhao@yuhao-Latitude-3440:~$ cat b

yuhao@yuhao-Latitude-3440:~$ cat --version
cat (GNU coreutils) 8.13
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Torbjörn Granlund and Richard M. Stallman.
红色部分和以上说法冲突,.....BUG ,BUG :em02
3年前的cat……你在用ubuntu 12.04?这可能是当年bash的某个bug。现在新版本应该都修正了
头像
royclark
帖子: 301
注册时间: 2011-05-15 1:01
系统: Debian GNU/Linux sid

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#12

帖子 royclark » 2014-04-20 1:10

我觉得出现 a 未被清空的情况不算 bug,而且应该与版本无关。
对 cat a | tr -d b > a 运行一万次结果如下,少数情况下 cat 读到的 a 是没被清空的。

代码: 全选

for x in {0..10000} ; do echo abc > a ; cat a | tr -d b > a ; cat a ; done 
ac
ac
ac
ac
ac
ac
ac
ac
ac
ac
ac
ac
ac
用 strace 跟踪系统调用:

代码: 全选

$ echo abc > a 
$ strace -f -o test.log bash -c 'cat a | tr -d b > a' 
$ cat test.log | grep -e clone -e pipe -e dup2 -e close -e open -e exec 
15506 是父进程 PID,15507、15508 是两个子进程 PID。尽管用 grep 进行了过滤,仍有一些无关信息,下面略去了这些信息。
15506 pipe([3, 4]) = 0
# 父进程用 pipe() 打开管道,相应文件描述符是 3 和 4,其中 3 是读端,4 是写端 。
15506 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD
, child_tidptr=0xb762d728) = 15507
15506 close(4) = 0
15506 close(4) = -1 EBADF (Bad file descriptor)
# 父进程用 clone() 产生一个子进程,然后关闭管道写端 4 。
15506 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD
, child_tidptr=0xb762d728) = 15508
15506 close(3) = 0
# 父进程用 clone() 产生一个子进程,然后关闭管道读端 3。
# 这时子进程 15508 是只有管道的读端的,因为写端在 clone() 之前就关闭了。

15507 close(3 <unfinished ...>
15507 <... close resumed> ) = 0
15507 dup2(4, 1 <unfinished ...>
# 子进程 15507 将管道写端 4 用 dup2() 赋给标准输出。
15507 <... dup2 resumed> ) = 1
15507 close(4 <unfinished ...>
15507 <... close resumed> ) = 0
15508 dup2(3, 0 <unfinished ...>
# 子进程 15508 将管道读端 3 用 dup2() 赋给标准输入。
15508 <... dup2 resumed> ) = 0
15508 close(3 <unfinished ...>
15508 <... close resumed> ) = 0
15508 open("a", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 3
15508 dup2(3, 1) = 1
# 子进程 15508 打开文件“a”,并赋给标准输出。因为有 O_TRUNC,文件“a”被清空。即对应于重定向“ > a”。
15508 close(3) = 0
15508 execve("/usr/bin/tr", ["tr", "-d", "b"], [/* 39 vars */]) = 0
# 子进程 15508 用 execve() 执行 "tr -d b "。
15507 execve("/bin/cat", ["cat", "a"], [/* 39 vars */]) = 0
# 子进程 15507 执行 "cat a"。
15507 open("a", O_RDONLY|O_LARGEFILE) = 3
15507 close(3) = 0
# 子进程 15507 打开文件“a”,相当于 cat 读取 a。

是 cat 先读了 a,还是重定向先清空了 a 取决于哪一个先被执行。而从 clone() 开始两个子进程是完全独立的,重定向和 exec() 又都是在 clone() 后才进行的,因而执行先后不是绝对的,受进程调度等影响,两种结果都可能出现。

系统 Debian GNU/Linux Wheezy i386

代码: 全选

$ bash --version 
GNU bash, version 4.2.37(1)-release (i486-pc-linux-gnu)
...
$ cat --version 
cat (GNU coreutils) 8.13
...
更正:系统 amd64 -> i386
yuhao chen
帖子: 6
注册时间: 2014-03-05 9:01
系统: ubuntu12.04

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#13

帖子 yuhao chen » 2014-04-20 10:18

:em20 小小菜鸟一只,刚刚学liunx,先mark 了,以后再来深究 :em09 。再次谢谢各位大大热心帮助 :em01
头像
astolia
论坛版主
帖子: 6703
注册时间: 2008-09-18 13:11

Re: 关于cat , | , tr , > 指令混合执行,结果让我挺郁闷的。。。

#14

帖子 astolia » 2014-04-20 11:03

royclark 写了: 是 cat 先读了 a,还是重定向先清空了 a 取决于哪一个先被执行。而从 clone() 开始两个子进程是完全独立的,重定向和 exec() 又都是在 clone() 后才进行的,因而执行先后不是绝对的,受进程调度等影响,两种结果都可能出现。
确实,fork/clone之后的进程执行顺序是由内核来调度的,如果shell的具体实现是用子进程来处理重定向到文件的话,就会导致这个问题。我当年写的shell是由父进程处理的,有点先入为主了
回复