<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="https://www.gentoo-zh.org/extern.php?action=feed&amp;tid=536&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / shell中while循环的陷阱]]></title>
		<link>https://www.gentoo-zh.org/viewtopic.php?id=536</link>
		<description><![CDATA[shell中while循环的陷阱 最近发表的帖子。]]></description>
		<lastBuildDate>Sun, 13 Nov 2022 10:06:33 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[shell中while循环的陷阱]]></title>
			<link>https://www.gentoo-zh.org/viewtopic.php?pid=573#p573</link>
			<description><![CDATA[<p>在写while循环的时候，发现了一个问题，在while循环内部对变量赋值、定义变量、数组定义等等环境，在循环外面失效。<br />一个简单的测试脚本如下：<br />#!/bin/bash<br />echo &quot;abc xyz&quot; | while read line<br />do<br />&#160; &#160; new_var=$line<br />done<br />echo new_var is null: $new_var?<br />执行结果证明，$new_var的结果是空值。</p><p>问题出在管道上。先看看下面的内容。</p><p>while循环的写法有好几种，它的语法结构为：<br />&#160; &#160; while test_cmd_list; do cmd_list; done<br />但更经常地，while循环更多地用于读取标准输入的内容来实现循环。有以下几种写法：</p><p>写法一：使用管道传递内容，这是用的最多、但却最烂的写法<br />&#160; &#160; echo &quot;abc xyz&quot; | while read line&#160; &#160;<br />&#160; &#160; do <br />&#160; &#160; &#160; &#160; ...<br />&#160; &#160; done</p><p>写法二：<br />&#160; &#160; while read line<br />&#160; &#160; do<br />&#160; &#160; &#160; &#160; ...<br />&#160; &#160; done &lt;&lt;&lt; &quot;abc xyz&quot;</p><p>写法三：从文件中读取内容</p><p>&#160; &#160; while read line<br />&#160; &#160; do<br />&#160; &#160; &#160; &#160; ...<br />&#160; &#160; done &lt;/path/filename</p><p>方法四：采用进程替换<br />&#160; &#160; while read var<br />&#160; &#160; do<br />&#160; &#160; &#160; &#160; ...<br />&#160; &#160; done &lt; &lt;(cmd_list)&#160; &#160; &#160; &#160; &#160; &#160;<br />方法五：改变标准输入<br />&#160; &#160; exec &lt;filename<br />&#160; &#160; while read var<br />&#160; &#160; do<br />&#160; &#160; &#160; &#160; ...<br />&#160; &#160; done&#160; &#160; &#160; &#160; </p><p>尽管写法有多种，但它们并不等价。</p><p>陷阱一：</p><p>方法一中使用的是管道符号，这使得while语句在子shell中执行，这意味着while语句内部设置的变量、数组、函数等在循环外部都不再生效。这正是文章开头所说的陷阱。更简单的：echo haha | a=5，在命令执行结束后，变量a的值也不再是5。其余4种写法，while语句都不在子shell中执行，因此都不会出现文章开头所说的问题。</p><p>例如，使用写法二的here string代替写法一：</p><p>#!/bin/bash<br />while read line<br />do<br />&#160; &#160; new_var=$line<br />done &lt;&lt;&lt; &quot;abc xyz&quot;<br />echo new_var is null: $new_var?</p><p>或者使用写法四的进程替换：</p><p>#!/bin/bash<br />while read line<br />do<br />&#160; &#160; new_var=$line<br />done &lt; &lt;(echo &quot;abc xyz&quot;)<br />echo new_var is null: $new_var?</p><p>陷阱二：</p><p>关于这几种while循环的写法，还有一点要注意：写法一和写法四传递数据的源都是一个单独的进程，它们传递的数据一被while循环读取，所有数据就丢弃了，而以实体文件作为重定向传递的数据，while读取了之后并不会丢弃。更标准一些的说法是，当标准输入是非实体文件时(如管道传递的、独立进程产生的)只供一次读取；当标准输入是直接重定向实体文件时，可供多次读取，但只要某一次读取了该文件的全部内容就无法再提供读取。</p><p>举个例子，老师让我们听写10个单词，而我记忆力比较烂，他念完10个单词时我可能只写出了3个，剩余的7个因为记不住就没法再写出来。但如果我有小抄，我就可以慢悠悠的一个一个写，写了一个还可以等一段时间再写第二个，但当我写完10个之后，小抄这种东西就应该销毁掉。</p><p>回到IO重定向上，无论什么数据资源，只要被读取完毕或者主动丢弃，那么该资源就不可再得。①对于独立进程传递的数据(管道左侧进程产生的数据、进程替换产生的数据)，它们都是&quot;虚拟&quot;数据，要不被一次读取完毕，要不读一部分剩余的丢弃，这是真正的一次性资源。②而实体文件重定向传递的数据，只要不是一次性被全部读取，它就是可再得资源，直到该文件数据全部读取结束，这是&quot;伪&quot;一次性资源。其实①是进程间通信时数据传递的现象，只不过这个问题容易被人忽略。</p><p>大多数时候，独立进程传递的数据和文件直接传递的数据并没有什么区别，但有些命令可以标记当前读取到哪个位置，使得下次该命令的读取动作可以从标记位置处恢复并继续读取，特别是这些命令用在循环中时。据我到目前的总结，这样的命令有&quot;head -n N&quot;和&quot;grep -m&quot;，经测试，tail并没有位置标记的功能。</p><p>说了这么多，现在终于开始验证。下面的循环中，本该head每次读取2行，但实际执行结果中总共就只读取了一次2行。</p><p>[root@xuexi ~]# i=0<br />[root@xuexi ~]# cat /etc/fstab | while head -n 2 ; [[ &quot;$i&quot; -le 3 ]];do echo $i;let ++i;done&#160; &#160; &#160;</p><p>使用进程替换的结果是一样的。</p><p>[root@xuexi ~]# i=0<br />[root@xuexi ~]# while head -n 2; [[ &quot;$i&quot; -le 3 ]];do echo $i;let ++i;done &lt; &lt;(cat /etc/fstab)</p><p>但如果是直接将实体文件进行重定向传递给head，则结果和上面的不一样。</p><p>[root@xuexi ~]# i=0;while head -n 2 ; [[ &quot;$i&quot; -le 3 ]];do echo $i;let ++i;done &lt; /etc/fstab</p><p># /etc/fstab<br /># Created by anaconda on Thu May 11 04:17:44 2017</p><p># Accessible filesystems, by reference, are maintained under &#039;/dev/disk&#039;<br /># See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info</p><p>UUID=b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8 /&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;xfs&#160; &#160; &#160;defaults&#160; &#160; &#160; &#160; 0 0<br />UUID=367d6a77-033b-4037-bbcb-416705ead095 /boot&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;xfs&#160; &#160; &#160;defaults&#160; &#160; &#160; &#160; 0 0</p><p>可以看到结果中每次读取两行并echo一次&quot;$i&quot;，而且每次读取的两行是不同的，后一次读取的两行是从前一次读取结束的地方开始的，这是因为head有&quot;读取到指定行数后做上位置标记&quot;的功能。</p><p>要确定命令、工具是否具有做位置标记的能力，只需像下面例子一样做个简单的测试。以head和sed为例，即使sed的&quot;q&quot;命令能让sed匹配到内容就退出，但却不做位置标记，而且数据资源使用一次就丢弃。</p><p>[root@xuexi ~]# (head -n 2;head -n 2) &lt;/etc/fstab </p><p>#<br /># /etc/fstab<br /># Created by anaconda on Thu May 11 04:17:44 2017</p><p>[root@xuexi ~]# (sed -n /default/&#039;{p;q}&#039; ;sed -n /default/&#039;{p;q}&#039;) &lt;/etc/fstab&#160; &#160; &#160;<br />UUID=b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8 /&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;xfs&#160; &#160; &#160;defaults&#160; &#160; &#160; &#160; 0 0</p><p>其实在实际应用过程中，这根本就不是个问题，因为搜索和处理文本数据的工具虽然不少，但绝大多数都是用一次文本就&quot;丢&quot;一次，几乎不可能因此而产生问题。之所以说这么多废话，主要是想说上面的5种while写法中，使用最广泛的写法一虽然最简单、方便，但其实是最烂的一种。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sun, 13 Nov 2022 10:06:33 +0000</pubDate>
			<guid>https://www.gentoo-zh.org/viewtopic.php?pid=573#p573</guid>
		</item>
	</channel>
</rss>
