ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Shell编程实例——标准输入

Shell编程实例——标准输入 标准输入1、从文件获取输入2、将数据与脚本存放在一起3、避免here-document中的怪异行为4、缩进here-document5、获取用户输入6、获取yes或no7、选择选项列表8、提示输入密码1、从文件获取输入你希望 shell 命令从文件中读取数据。用代表输入重定向的符号 从文件中读取数据wcmy.file就像 可以将输出发送至文件 则可以从文件中获取输入。之所以选择这种形状的操作符号原因在于它们可以从视觉上提示重定向的方向。你看出来了吗​注意“箭头”​很多 shell 命令可以接受一个或多个文件名作为参数但如果没有给出文件名命令就会从标准输入读取。使用这种命令时可以采用 command filename 或者command filename这两种形式的结果没什么区别。在这个例子中wc 是这样换作 cat 或其他命令也是如此。这看起来也许并不起眼如果你之前接触过 DOS 命令行一定也不会陌生但这是 shell 脚本编程的一项重要特性DOS 命令行也借鉴了​对 shell 的功能性和简单性必不可少。2、将数据与脚本存放在一起你需要获得脚本输入但又不想用单独的文件。使用 here-document从命令行而非文件重定向输入文本。如果放在 shell 脚本中则脚本文件可以同时包含数据与代码。以下是名为 ext 的 shell 脚本示例$catext## 下面是here-document#grep$1EOF mike x.123 joe x.234 sue x.555 pete x.818 sara x.822 bill x.919 EOF$这个脚本可以用于简单的电话号码搜索$ ext bill bill x.919 $或者$ ext555sue x.555 $grep 命令查找第一个参数是否在指定文件中出现如果没有指定文件那么它会在标准输入中查找。grep 的典型用法如下所示grepsomestring file.txt或者grepmyvar *.c在脚本 ext 中通过将待搜索的字符串称为 shell 脚本的参数$1​我们实现了 grep 的参数化。通常我们认为grep 就是在不同文件中搜索固定的字符串但这里我们打算在同一批数据中搜索不同的内容。可以将电话号码保存在文件中比如phonenumbers.txt然后在调用 grep 命令时指定文件名grep$1phonenumbers.txt但这样就需要两个文件脚本文件和数据文件​随之而来的问题是把这两个文件放哪里如何确保两者集中在一起因此我们不再提供一个或多个用于搜索的文件名而是设置 here-document告诉 shell 将标准输入重定向临时到此处。 语法表示我们想创建一个临时输入源EOF 是一个任意的字符串你想用什么都行​用作临时输入的终止符。它并不属于输入的一部分只是作为标记告诉输入在哪里结束。正常的 shell 脚本如果有的话会在该标记之后继续。你可以给 grep 命令加入 -i 选项以示搜索时不区分大小写。这样便可以使用 grep -i $1EOF同时搜索“Bill”或“bill”​。3、避免here-document中的怪异行为here-document 在使用时可能会出现一些怪异的行为。你想用上一节介绍的方法来保存一份简单的捐赠人列表因此创建了一个名为 donors 的文件如下所示$catdonors## 简单地查找慷慨的捐赠人#grep$1EOF # 捐赠人及其捐赠额 pete$100joe$200sam $ 25 bill $ 9 EOF$但是运行时出现了奇怪的输出$ ./donors bill pete bill00 bill $9$ ./donors pete pete pete00 $通过转义结尾标记中的任意或所有字符关闭 here-document 内部的 shell 特性grep$1EOF pete $100 joe $200 sam $ 25 bill $ 9 EOF尽管其中存在非常微妙的区别但也可以将 EOF 替换成 \EOF 或 ‘EOF’甚至是 E\OF都没问题。尽管这并不是最优雅的语法但足以告诉 bash 你希望区别处理 here-document 中的内容。正常情况下除非使用了转义语法​bash 手册页中是这样说的​“……here-document 的每一行都要执行参数扩展、命令替换以及算术扩展”​。因此最初的 donors 脚本中所发生的事情是捐赠额被当作 shell 变量了。例如$100 被视为 shell 变量 $1随后跟着两个 0。这就是为什么我们在搜索“pete”时得到的是 pete00搜索“bill”时得到的是 bill00。如果我们转义了 EOF 的部分或全部字符那么 bash 就知道不用执行扩展这样就符合我们的预期行为了。$ ./donors pete pete$100当然了你可能确实想要 bash 对 here-document 内部执行扩展操作这种做法在某些情况下自有用处不过眼前这个例子并非如此。我们发现除非你真的要扩展数据否则坚持转义终止符就像 ‘EOF’ 或 \EOF是一种不错的做法这样能够避免产生出乎意料的结果。在结尾处的 EOF 标记中出现的拖尾空白字符哪怕只是一个空格会导致无法将其识别为结束标记。bash 会吞掉脚本的剩余部分将其也视为输入并继续查找 EOF。所以一定要确保 EOF 之后没有额外的空白字符尤其是空格或制表符​。4、缩进here-documenthere-document 固然不错但是它会弄乱 shell 脚本的格式。为了提高可读性你希望能对其作缩进处理。使用 -然后就可以在每行的开头用制表符仅限制表符缩进 shell 脚本中的 here-document 部分$catmyscript.sh...grep$1-EOFlots of data can go here its indented with tabs to match the scripts indenting but the leading tabs are discarded whenreadEOFls... $ 之后的连字符-足以告诉 bash 忽略前导制表符。但这仅适用于制表符其他空白字符是无法忽略的。注意对于 EOF 或者选用的其他标记这一点尤为重要。如果出现了空格就无法将 EOF 识别为结束标记here-document 的数据部分就会一直延续到文件结尾吞掉剩余的脚本​。出于安全起见你可以始终左对齐 EOF或其他标记​。就像结尾处的 EOF 标记中出现的任何拖尾空白字符都会导致其无法被识别为结束标记一样参见 3.3 节中的警告部分​使用除制表符外的前导字符也会造成同样的后果。如果你的脚本使用空格或混合空格和制表符来进行缩进可别在 here-document 中这么做。要么使用制表符要么什么都不用。另外小心有些文本编辑器会自动将制表符替换成空格。5、获取用户输入你需要获取用户输入。使用 read 语句read或者read-panswer me this ANSWER或者read-t3-panswer quickly: ANSWER又或者readPRE MID POST不带参数的 read 语句会读取用户输入并将其保存在 shell变量 REPLY 中这是 read 的最简形式。如果希望 bash 在读取用户输入前先输出提示信息可以 使用 -p 选项。-p 之后的单词就是提示信息如果想提供多个单词可以将其引用起来。记住要在提示信息结尾处加上标点符号或空格因为光标会停在那里等待输入。-t 选项可以设置超时值。指定秒数达到后不管用户是否输入read 语句都会返回。我们的示例同时用到了 -t 和 -p 选项但你也可以单独使用 -t 选项。从 bash 4 开始你甚至可以将超时值指定为小数如 .25 或 3.5。如果读取超时则退出状态码$?将大于 128。如果你在 read 语句中提供了多个变量名那么 read 会将输入解析为多个单词依次赋给这些变量。如果用户输入的单词数较少多出的变量就会被设为空null​。如果用户输入的单词数多于变量数则多出的单词会全部赋给最后那个变量。6、获取yes或no你需要从用户处获得一个简单的 yes 或 no 作为输入同时希望尽可能地对用户友好尤其是不用区分大小写如果用户直接按回车键还能提供默认值。如果操作比较简单可以使用独立函数。# 实例文件func_choose# 由用户选择并根据答案执行相应代码# 调用方式choose default (y or n) prompt yes action no action# 例如choose y \# Do you want to play a game? \# /usr/games/GlobalThermonuclearWar \# printf %b See you later Professor Falkin.\n 2# 返回无functionchoose{localdefault$1localprompt$2localchoice_yes$3localchoice_no$4localanswerread-p$promptanswer[-z$answer]answer$defaultcase$answerin[yY1])eval$choice_yes# error check;;[nN0])eval$choice_no# error check;;*)printf%bUnexpected answer $answer!2;;esac}# 函数choose结束如果操作比较复杂可以使的函数并在主代码中处理函数返回值。# 实例文件func_choice.1# 由用户选择并返回标准答案。默认值的处理方式以及接下来怎么做# 取决于主代码中choice函数之后的if/then分支# 调用方式choice prompt# 例如choice Do you want to play a game?# 返回全局变量CHOICEfunctionchoice{CHOICElocalprompt$*localanswerread-p$promptanswercase$answerin[yY1])CHOICEy;;[nN0])CHOICEn;;*)CHOICE$answer;;esac}# 函数choice结束如果我们返回“0”表示 no返回“1”表示yes表达式 if choice … ; then 的用法则值得注意。我们将此作为练习留给你来实践。调用了 choice 函数提示并核实软件包的日期。假设已经设置 $THISPACKAGE代码显示该日期并要求用户核实。如果用户输入 y、Y或者直接按下回车键就接受所显示的日期。如果用户输入新的日期代码则再次循环继续核实日期。# 实例文件func_choice.2CHOICEuntil[$CHOICEy];doprintf%bThis packages date is$THISPACKAGE\n2choiceIs that correct? [Y/,New date]: if[-z$CHOICE];thenCHOICEyelif[$CHOICE!y];thenprintf%bOverriding$THISPACKAGEwith$CHOICE\nTHISPACKAGE$CHOICEfidone# 在此构建软件包接下来看看处理 yes 或 no 的其他方法。仔细阅读提示信息注意默认操作。在两种情况下用户都可以直接按回车键脚本将采用程序员安排好的默认处理方式# 如果用户输入n不区分大小写之外的任何内容则会看到错误日志choiceDo you want to look at the error logfile? [Y/n]: if[$CHOICE!n];thenlesserror.logfi# 如果用户输入y不区分大小写之外的任何内容则看不到消息日志choiceDo you want to look at the message logfile? [y/N]: if[$CHOICEy];thenlessmessage.logfi最后下例中的函数会要求用户输入有可能不存在​。# 实例文件func_choice.3choiceEnter your favorite color, if you have one: if[-n$CHOICE];thenprintf%bYou chose:$CHOICE\nelseprintf%bYou do not have a favorite color.\nfi如果用户可能做出的选择及相应处理代码非常直观那么第一个独立函数更易于使用但可能不够灵活。第二个函数在主代码中做了更多处理因而灵活性更好。注意我们将用户提示发送到了 STDERR这样一来就算重定向出现在 STDOUT 的主脚本输出也不会弄乱提示信息。7、选择选项列表你需要为用户提供选项列表但不希望用户输入不必要的内容。使用 bash 内建的 select 来生成菜单随后用户可以输入选项数字进行选择​。# 实例文件select_dirdirectorylistFinished$(foriin/*;do[-d$i]echo$i;done)PS3Directory to process? # 设置有帮助的选择提示until[$directoryFinished];doprintf%b\a\n\nSelect a directory to process:\n2selectdirectoryin$directorylist;do# 用户输入的数字被保存在$REPLY中# 但是select返回的是用户选中的选项值if[$directoryFinished];thenechoFinished processing directories.breakelif[-n$directory];thenechoYou chose number$REPLY, processing$directory...# 在此进行相关处理breakelseechoInvalid selection!fi# 结束选项处理done# 结束目录选择处理done# 如果用户选中Finished选项则结束循环select 语句能够轻松地在 STDERR 上为用户生成编号列表以便用户从中做出选择。虽然按下 Ctrl-D 可以结束select空输入会再次输出菜单但别忘了提供“退出”或“结束”选项。用户所输入的选项编号保存在 $REPLY 中选项值保存在select 语句指定的变量中。8、提示输入密码你需要提示用户输入密码但不希望在屏幕上回显出密码内容。用 read 命令读取用户输入但要加上一个特殊选项来关闭回显read-s-ppassword: PASSWDprintf%b\n-s 选项告诉 read 命令不要回显输入的字符s 代表silent​-p 选项指明下一个参数是提示信息会在读取用户输入之前显示。从用户那里读取到的输入行保存在变量 $PASSWD 中。在 read 之后我们用 printf 输出了一个换行符。这里的printf 不能少因为 read -s 会关闭字符回显。如果禁止了回显功能当用户按下回车键时就不会回显换行符后续输出就会和提示信息出现在同一行。输出换行符会将光标带到下一行这也正是我们想要的效果。将代码全都写在同一行也许更方便这样可以避免干扰代码逻辑另外还能避免将其复制到别处时出现遗漏​read-s-ppassword: PASSWD;printf%b\n如果要将密码读入环境变量那就要小心了因为密码在内存中是以明文形式存放的有可能通过核心转储或/proc/core如果你所用的操作系统提供了 /proc/访问到。在多进程环境中也是如此其他进程也有可能读取到密码。可能的话最好使用 SSH 证书。无论任何情况明智的做法是假定系统中的 root 和其他可能的用户都能接触到密码并对其进行相应的处理。有些陈旧的脚本也许会用 stty -echo 来禁止输入密码时的屏幕回显。这么做的问题在于如果脚本意外终止那么回显仍会处于关闭状态。有经验的用户知道输入 stty sane 将其恢复但这并非人人都懂。如果你的确需要使用这种方法就设置好陷阱以便在脚本终止时重新启用回显。
返回列表