Files
pages/2019/04/24/shell.html
2026-01-17 09:36:00 +00:00

340 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Begin Jekyll SEO tag v2.8.0 -->
<title>(转)Shell 脚本编程陷阱 | Mayx的博客</title>
<meta name="generator" content="Jekyll v3.9.5" />
<meta property="og:title" content="(转)Shell 脚本编程陷阱" />
<meta name="author" content="Carpetsmoker" />
<meta property="og:locale" content="zh_CN" />
<meta name="description" content="随着代码量的增加,你的脚本会变得越来越难以维护,但你也不会想用别的语言重写一遍,因为你已经在这个 shell 版上花费了很多时间。" />
<meta property="og:description" content="随着代码量的增加,你的脚本会变得越来越难以维护,但你也不会想用别的语言重写一遍,因为你已经在这个 shell 版上花费了很多时间。" />
<meta property="og:site_name" content="Mayx的博客" />
<meta property="og:type" content="article" />
<meta property="article:published_time" content="2019-04-24T00:00:00+08:00" />
<meta name="twitter:card" content="summary" />
<meta property="twitter:title" content="(转)Shell 脚本编程陷阱" />
<meta name="google-site-verification" content="huTYdEesm8NaFymixMNqflyCp6Jfvd615j5Wq1i2PHc" />
<meta name="msvalidate.01" content="0ADFCE64B3557DC4DC5F2DC224C5FDDD" />
<meta name="yandex-verification" content="fc0e535abed800be" />
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting","author":{"@type":"Person","name":"Carpetsmoker"},"dateModified":"2019-04-24T00:00:00+08:00","datePublished":"2019-04-24T00:00:00+08:00","description":"随着代码量的增加,你的脚本会变得越来越难以维护,但你也不会想用别的语言重写一遍,因为你已经在这个 shell 版上花费了很多时间。","headline":"(转)Shell 脚本编程陷阱","mainEntityOfPage":{"@type":"WebPage","@id":"/2019/04/24/shell.html"},"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://avatars0.githubusercontent.com/u/17966333"},"name":"Carpetsmoker"},"url":"/2019/04/24/shell.html"}</script>
<!-- End Jekyll SEO tag -->
<link rel="canonical" href="https://mabbs.github.io/2019/04/24/shell.html" />
<link type="application/atom+xml" rel="alternate" href="/atom.xml" title="Mayx的博客" />
<link rel="alternate" type="application/rss+xml" title="Mayx的博客(RSS)" href="/rss.xml" />
<link rel="alternate" type="application/json" title="Mayx的博客(JSON Feed)" href="/feed.json" />
<link rel="stylesheet" href="/assets/css/style.css?v=1768642553" />
<!--[if !IE]> -->
<link rel="stylesheet" href="/Live2dHistoire/live2d/css/live2d.css" />
<!-- <![endif]-->
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Mayx的博客" />
<link rel="webmention" href="https://webmention.io/mabbs.github.io/webmention" />
<link rel="pingback" href="https://webmention.io/mabbs.github.io/xmlrpc" />
<link rel="preconnect" href="https://summary.mayx.eu.org" crossorigin="anonymous" />
<link rel="prefetch" href="https://www.blogsclub.org/badge/mabbs.github.io" as="image" />
<link rel="blogroll" type="text/xml" href="/blogroll.opml" />
<link rel="me" href="https://github.com/Mabbs" />
<script src="/assets/js/jquery.min.js"></script>
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-ajaxtransport-xdomainrequest/1.0.3/jquery.xdomainrequest.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
<script>
var lastUpdated = new Date("Sat, 17 Jan 2026 17:35:53 +0800");
var BlogAPI = "https://summary.mayx.eu.org";
</script>
<script src="/assets/js/main.js"></script>
<!--[if !IE]> -->
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async="async" src="https://www.googletagmanager.com/gtag/js?id=UA-137710294-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-137710294-1');
</script>
<script src="/assets/js/instant.page.js" type="module"></script>
<!-- <![endif]-->
</head>
<body>
<!--[if !IE]> --><noscript><marquee style="top: -15px; position: relative;"><small>发现当前浏览器没有启用JavaScript这不影响你的浏览但可能会有一些功能无法使用……</small></marquee></noscript><!-- <![endif]-->
<!--[if IE]><marquee style="top: -15px; position: relative;"><small>发现当前浏览器为Internet Explorer这不影响你的浏览但可能会有一些功能无法使用……</small></marquee><![endif]-->
<div class="wrapper">
<header class="h-card">
<h1><a class="u-url u-uid p-name" rel="me" href="/">Mayx的博客</a></h1>
<img src="https://avatars0.githubusercontent.com/u/17966333" fetchpriority="high" class="u-photo" alt="Logo" style="width: 90%; max-width: 300px; max-height: 300px;" />
<p class="p-note">Mayx's Home Page</p>
<form action="/search.html">
<input type="text" name="keyword" id="search-input-all" placeholder="Search blog posts.." />&#160;<input type="submit" value="搜索" />
</form>
<br />
<p class="view"><a class="u-url" href="/Mabbs/">About me</a></p>
<ul class="downloads">
<li style="width: 270px; border-right: none;"><a href="/MayxBlog.tgz">Download <strong>TGZ File</strong></a></li>
</ul>
</header>
<section class="h-entry">
<small><time class="date dt-published" datetime="2019-04-24T00:00:00+08:00">24 April 2019</time> - 字数统计2283 - 阅读大约需要8分钟 - Hits: <span id="/2019/04/24/shell.html" class="visitors">Loading...</span></small>
<h1 class="p-name">(转)Shell 脚本编程陷阱</h1>
<p class="view">by <a class="p-author h-card" href="//github.com/Carpetsmoker">Carpetsmoker</a></p>
<div id="outdate" style="display:none;">
<hr /><p>
这是一篇创建于 <span id="outime"></span> 天前的文章,其中的信息可能已经有所发展或是发生改变。
</p>
</div>
<script>
daysold = Math.floor((new Date().getTime() - new Date("Wed, 24 Apr 2019 00:00:00 +0800").getTime()) / (24 * 60 * 60 * 1000));
if (daysold > 90) {
document.getElementById("outdate").style.display = "block";
document.getElementById("outime").innerHTML = daysold;
}
</script>
<hr />
<b>AI摘要</b>
<p id="ai-output">这篇文章探讨了在编写和维护Shell脚本时遇到的陷阱尽管Shell代码简洁易懂但在大型脚本中难以维护且易于陷入“沉没成本谬误”。作者通过例子指出使用Python、Ruby等其他语言从一开始就编写会更有利于长期维护和添加新功能。文章列举了Shell编程的局限如处理特殊字符、正确的语法使用、平台兼容性、调试困难和错误处理等方面的问题。作者倡导在开发时考虑使用更现代的编程语言以避免此类问题。</p>
<hr />
<ul><li><a href="#附录问题汇总">附录:问题汇总</a></li><li><a href="#反馈">反馈</a></li></ul>
<hr />
<main class="post-content e-content" role="main"><p>随着代码量的增加,你的脚本会变得越来越难以维护,但你也不会想用别的语言重写一遍,因为你已经在这个 shell 版上花费了很多时间。 </p><p>
<!--more--></p>
<p>Shell 脚本很棒,你可以非常轻松地写出有用的东西来。甚至像是下面这个傻瓜式的命令:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 用含有 Go 的词汇起名字:</span>
<span class="nv">$ </span><span class="nb">grep</span> <span class="nt">-i</span> ^go /usr/share/dict/<span class="k">*</span> | <span class="nb">cut</span> <span class="nt">-d</span>: <span class="nt">-f2</span> | <span class="nb">sort</span> <span class="nt">-R</span> | <span class="nb">head</span> <span class="nt">-n1</span>
goldfish
</code></pre></div></div>
<p>如果用其他编程语言,就需要花费更多的脑力,用多行代码实现,比如用 Ruby 的话:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">puts</span><span class="p">(</span><span class="no">Dir</span><span class="p">[</span><span class="s1">'/usr/share/dict/*-english'</span><span class="p">].</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span>
<span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
<span class="p">.</span><span class="nf">readlines</span>
<span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">l</span><span class="o">|</span> <span class="n">l</span><span class="p">[</span><span class="mi">0</span><span class="o">..</span><span class="mi">1</span><span class="p">].</span><span class="nf">downcase</span> <span class="o">==</span> <span class="s1">'go'</span> <span class="p">}</span>
<span class="k">end</span><span class="p">.</span><span class="nf">flatten</span><span class="p">.</span><span class="nf">sample</span><span class="p">.</span><span class="nf">chomp</span><span class="p">)</span>
</code></pre></div></div>
<p>Ruby 版本的代码虽然不是那么长,也并不复杂。但是 shell 版是如此简单,我甚至不用实际测试就可以确保它是正确的。而 Ruby 版的我就没法确定它不会出错了,必须得测试一下。而且它要长一倍,看起来也更复杂。</p>
<p>这就是人们使用 Shell 脚本的原因,它简单却实用。下面是另一个例子:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://nl.wikipedia.org/wiki/Lijst_van_Nederlandse_gemeenten |
<span class="nb">grep</span> <span class="s1">'^&lt;li&gt;&lt;a href='</span> |
<span class="nb">sed</span> <span class="nt">-r</span> <span class="s1">'s|&lt;li&gt;&lt;a href="/wiki/.+" title=".+"&gt;(.+)&lt;/a&gt;.*&lt;/li&gt;|\1|'</span> |
<span class="nb">grep</span> <span class="nt">-Ev</span> <span class="s1">'(^Tabel van|^Lijst van|Nederland)'</span>
</code></pre></div></div>
<p>这个脚本可以从维基百科上获取荷兰基层政权的列表。几年前我写了这个临时的脚本,用来快速生成一个数据库,到现在它仍然可以正常运行,当时写它并没有花费我多少精力。但要用 Ruby 完成同样的功能则会麻烦得多。</p>
<p>现在来说说 shell 的缺点吧。随着代码量的增加,你的脚本会变得越来越难以维护,但你也不会想用别的语言重写一遍,因为你已经在这个 shell 版上花费了很多时间。 </p><p>
我把这种情况称为“Shell 脚本编程陷阱”,这是<a href="https://youarenotsosmart.com/2011/03/25/the-sunk-cost-fallacy/">沉没成本谬论</a>的一种特例LCTT 译注:“沉没成本谬论”是一个经济学概念,可以简单理解为,对已经投入的成本可能被浪费而念念不忘)。 </p><p>
实际上许多脚本会增长到超出预期的大小,你经常会花费过多的时间来“修复某个 bug”或者“添加一个小功能”。如此循环往复让人头大。 </p><p>
如果你从一开始就使用 Python、Ruby 或是其他类似的语言来写这个程序你可能会在写第一版的时候多花些时间但以后维护起来就容易很多bug 也肯定会少很多。 </p><p>
以我的 <a href="https://github.com/Carpetsmoker/packman.vim">packman.vim</a> 脚本为例。它起初只包含一个简单的用来遍历所有目录的 <code class="language-plaintext highlighter-rouge">for</code> 循环,外加一个 <code class="language-plaintext highlighter-rouge">git pull</code>,但在这之后就刹不住车了,它现在有 200 行左右的代码,这肯定不能算是最复杂的脚本,但假如我一上来就按计划用 Go 来编写它的话,那么增加一些像“打印状态”或者“从配置文件里克隆新的 git 库”这样的功能就会轻松很多;添加“并行克隆”的支持也几乎不算个事儿了,而在 shell 脚本里却很难实现(尽管不是不可能)。事后看来,我本可以节省时间,并且获得更好的结果。 </p><p>
出于类似的原因,我很后悔写出了许多这样的 shell 脚本,而我在 2018 年的新年誓言就是不要再犯类似的错误了。</p>
<h1 id="附录问题汇总">
<a href="#附录问题汇总"><svg class='octicon' viewBox='0 0 16 16' version='1.1' width='16' height='32' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg></a> 附录:问题汇总
</h1>
<p>需要指出的是shell 编程的确存在一些实际的限制。下面是一些例子:</p>
<ul>
<li>在处理一些包含“空格”或者其他“特殊”字符的文件名时,需要特别注意细节。绝大多数脚本都会犯错,即使是那些经验丰富的作者(比如我)编写的脚本,因为太容易写错了,<a href="https://dwheeler.com/essays/filenames-in-shell.html">只添加引号是不够的</a></li>
<li>有许多所谓“正确”和“错误”的做法。你应该用 <code class="language-plaintext highlighter-rouge">which</code> 还是 <code class="language-plaintext highlighter-rouge">command</code>?该用 <code class="language-plaintext highlighter-rouge">$@</code> 还是 <code class="language-plaintext highlighter-rouge">$*</code>,是不是得加引号?你是该用 <code class="language-plaintext highlighter-rouge">cmd $arg</code> 还是 <code class="language-plaintext highlighter-rouge">cmd "$arg"</code>?等等等等。</li>
<li>你没法在变量里存储空字节0x00shell 脚本处理二进制数据很麻烦。</li>
<li>虽然你可以非常快速地写出有用的东西,但实现更复杂的算法则要痛苦许多,即使用 ksh/zsh/bash 扩展也是如此。我上面那个解析 HTML 的脚本临时用用是可以的,但你真的不会想在生产环境中使用这种脚本。</li>
<li>很难写出跨平台的通用型 shell 脚本。<code class="language-plaintext highlighter-rouge">/bin/sh</code> 可能是 <code class="language-plaintext highlighter-rouge">dash</code> 或者 <code class="language-plaintext highlighter-rouge">bash</code>,不同的 shell 有不同的运行方式。外部工具如 <code class="language-plaintext highlighter-rouge">grep</code><code class="language-plaintext highlighter-rouge">sed</code> 等,不一定能支持同样的参数。你能确定你的脚本可以适用于 Linux、macOS 和 Windows 的所有版本吗(无论是过去、现在还是将来)?</li>
<li>调试 shell 脚本会很难,特别是你眼中的语法可能会很快变得记不清了,并不是所有人都熟悉 shell 编程的语境。</li>
<li>处理错误会很棘手(检查 <code class="language-plaintext highlighter-rouge">$?</code> 或是 <code class="language-plaintext highlighter-rouge">set -e</code>),排查一些超过“出了个小错”级别的复杂错误几乎是不可能的。</li>
<li>除非你使用了 <code class="language-plaintext highlighter-rouge">set -u</code>,变量未定义将不会报错,而这会导致一些“搞笑事件”,比如 <code class="language-plaintext highlighter-rouge">rm -r ~/$undefined</code> 会删除用户的整个家目录(<a href="https://github.com/ValveSoftware/steam-for-linux/issues/3671">瞅瞅 Github 上的这个悲剧</a>)。</li>
<li>所有东西都是字符串。一些 shell 引入了数组,能用,但是语法非常丑陋和费解。带分数的数字运算仍然难以应付,并且依赖像 <code class="language-plaintext highlighter-rouge">bc</code><code class="language-plaintext highlighter-rouge">dc</code> 这样的外部工具(<code class="language-plaintext highlighter-rouge">$(( .. ))</code> 这种方式只能对付一下整数)。</li>
</ul>
<h1 id="反馈">
<a href="#反馈"><svg class='octicon' viewBox='0 0 16 16' version='1.1' width='16' height='32' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg></a> 反馈
</h1>
<p>你可以发邮件到 <a href="mailto:martin@arp242.net">martin@arp242.net</a>,或者在 <a href="https://github.com/Carpetsmoker/arp242.net/issues/new">GitHub 上创建 issue</a> 来向我反馈,提问等。</p><hr />
<p>译者: <a href="https://linux.cn/lctt/jdh8383">jdh8383</a> </p><p>
翻译: <a href="https://linux.cn/article-10772-1.html">https://linux.cn/article-10772-1.html</a> </p><p>
源:<a href="https://arp242.net/weblog/shell-scripting-trap.html">The shell scripting trap</a></p></main>
<small style="display: block">tags: <a rel="category tag" class="p-category" href="/search.html?keyword=Shell"><em>Shell</em></a> - <a rel="category tag" class="p-category" href="/search.html?keyword=%E7%BC%96%E7%A8%8B"><em>编程</em></a> <span style="float: right;"><a href="https://gitlab.com/mayx/mayx.gitlab.io/tree/master/_posts/2019-04-24-shell.md">查看原始文件</a></span></small>
<h4 style="border-bottom: 1px solid #e5e5e5;margin: 2em 0 5px;">推荐文章</h4>
<p id="suggest-container">Loading...</p>
<script>
var suggest = $("#suggest-container");
$.get(BlogAPI + "/suggest?id=/2019/04/24/shell.html&update=" + lastUpdated.valueOf(), function (data) {
if (data.length) {
getSearchJSON(function (search) {
suggest.empty();
var searchMap = {};
for (var i = 0; i < search.length; i++) {
searchMap[search[i].url] = search[i];
}
var tooltip = $('<div class="content-tooltip"></div>').appendTo('body').hide();
for (var j = 0; j < data.length; j++) {
var item = searchMap[data[j].id];
if (item) {
var link = $('<a href="' + item.url + '">' + item.title + '</a>');
var contentPreview = item.content.substring(0, 100);
if (item.content.length > 100) {
contentPreview += "……";
}
link.hover(
function(e) {
tooltip.text($(this).data('content'))
.css({
top: e.pageY + 10,
left: e.pageX + 10
})
.show();
},
function() {
tooltip.hide();
}
).mousemove(function(e) {
tooltip.css({
top: e.pageY + 10,
left: e.pageX + 10
});
}).data('content', contentPreview);
suggest.append(link);
suggest.append(' - ' + item.date + '<br />');
}
}
});
} else {
suggest.html("暂无推荐文章……");
}
});
</script>
<br />
<div class="pagination">
<span class="prev">
<a href="/2019/04/13/iwara.html">
上一篇如何下载Iwara上的视频
</a>
</span>
<br />
<span class="next">
<a href="/2019/04/27/antiban.html">
下一篇Mayx的Anti-Ban计划
</a>
</span>
</div>
<!--[if !IE]> -->
<link rel="stylesheet" href="/assets/css/gitalk.css">
<script src="/assets/js/gitalk.min.js"></script>
<div id="gitalk-container"></div>
<script>
var gitalk = new Gitalk({
clientID: '36557aec4c3cb04f7ac6',
clientSecret: 'ac32993299751cb5a9ba81cf2b171cca65879cdb',
repo: 'mabbs.github.io',
owner: 'Mabbs',
admin: ['Mabbs'],
id: '/2019/04/24/shell', // Ensure uniqueness and length less than 50
distractionFreeMode: false, // Facebook-like distraction free mode
proxy: "https://cors-anywhere.mayx.eu.org/?https://github.com/login/oauth/access_token"
})
gitalk.render('gitalk-container')
</script>
<!-- <![endif]-->
</section>
<!--[if !IE]> -->
<div id="landlord" style="left:5px;bottom:0px;">
<div class="message" style="opacity:0"></div>
<canvas id="live2d" width="500" height="560" class="live2d"></canvas>
<div class="live_talk_input_body">
<form id="live_talk_input_form">
<div class="live_talk_input_name_body" >
<input type="checkbox" id="load_this" />
<input type="hidden" id="post_id" value="/2019/04/24/shell.html" />
<label for="load_this">
<span style="font-size: 11px; color: #fff;">&#160;想问这篇文章</span>
</label>
</div>
<div class="live_talk_input_text_body">
<input name="talk" type="text" class="live_talk_talk white_input" id="AIuserText" autocomplete="off" placeholder="要和我聊什么呀?" />
<button type="submit" class="live_talk_send_btn" id="talk_send">发送</button>
</div>
</form>
</div>
<input name="live_talk" id="live_talk" value="1" type="hidden" />
<div class="live_ico_box" style="display:none;">
<div class="live_ico_item type_info" id="showInfoBtn"></div>
<div class="live_ico_item type_talk" id="showTalkBtn"></div>
<div class="live_ico_item type_music" id="musicButton"></div>
<div class="live_ico_item type_youdu" id="youduButton"></div>
<div class="live_ico_item type_quit" id="hideButton"></div>
<input name="live_statu_val" id="live_statu_val" value="0" type="hidden" />
<audio src="" style="display:none;" id="live2d_bgm" data-bgm="0" preload="none"></audio>
<input id="duType" value="douqilai" type="hidden" />
</div>
</div>
<div id="open_live2d">召唤伊斯特瓦尔</div>
<!-- <![endif]-->
<footer>
<p>
<small>Made with ❤ by Mayx<br />Last updated at 2026-01-17 17:35:53<br /> 总字数614622 - 文章数178 - <a href="/atom.xml" >Atom</a> - <a href="/README.html" >About</a></small>
</p>
</footer>
</div>
<script src="/assets/js/scale.fix.js"></script>
<!--[if !IE]> -->
<script src="/assets/js/main_new.js"></script>
<script src="/Live2dHistoire/live2d/js/live2d.js"></script>
<script src="/Live2dHistoire/live2d/js/message.js"></script>
<!-- <![endif]-->
</body>
</html>