尽管有很多类似的问题,但我找不到直接回答我的问题的问题。我了解操作系统内存管理、进程、进程之间的通信等基础知识。但是,对我来说,可以通过管道接收输入的命令的实现与只能接受命令参数的命令的实现之间的根本区别并不明显。
为什么命令会通过一种方法接受输入而不是另一种方法,以及如何轻松判断命令接受哪种输入方法?
ls
举一个简单的例子,为什么我可以通过管道输出to的文件名cat
进行显示,但不能对 执行相同的操作rm
?是否存在哲学原因、深层技术原因或实施中的任意选择导致了这种行为差异?
ls | cat
delete_me_1.txt
delete_me_2.txt
ls | rm
rm: missing operand
换句话说,如果我正在编写 Linux 命令,我需要考虑哪些技术因素来选择是否通过管道、命令参数或两者接受命令输入?
答案1
然而,对我来说,可以通过管道接收输入的命令的实现与只能接受命令参数的命令的实现之间的根本区别并不明显。
好吧,是否使用其中之一(或两者?),这是程序开发人员决定的事情;它与操作系统无关,只是操作系统提供了传递参数、读取输入文件描述符以及呈现标准输入的方法。
所以,这个问题比 Linux 早了 20 多年,这是标准输入和命令行参数概念的最短年龄(UNIX 的诞生,我确信 Thompson、Ritchie、Kernigan 和 McIlroy 没有提出来)出乎意料地)。
换句话说,如果我正在编写 Linux 命令,我需要考虑哪些技术因素来选择是否通过管道、命令参数或两者接受命令输入?
可用性和适合您的目的!
如果你处理一些数据,可能会更多有用将其作为标准输入输入,特别是如果您可以在生成输入的程序退出之前就已经生成输出。由于您只能在程序开始时传递命令行参数,因此如果您正在做一些需要使用连续生产者流式传输数据的事情,那么想知道该怎么做是没有意义的。
如果您要更改某些内容的行为或基于可变数量的参数计算某些内容,那么命令行参数可能比将这些参数作为输入传递的结构化方式更容易使用。
所以,这实际上归结为你如何想您要使用的程序。这不是一个技术问题 - 这是一个问题目的。
事实上:现在是 2022 年。对于任何广泛使用的现代语言,都有一些库可以将结构化输入数据(例如 JSON)解析为可以在程序中轻松使用的字典/映射/键值数据结构。解析命令行参数也是如此!通常,命令行参数解析器还可以解析配置文件(并且您可以使用标准输入作为配置文件,例如 CLI11),因此从技术上讲,您甚至不必选择可以执行任一操作的程序。
答案2
为什么命令会通过一种方法接受输入而不是另一种方法,
最终,因为程序就是这样制定的。
更实际的是,程序倾向于以最有意义的方式接受输入:如果输入是一组文件名或修改程序行为的选项,则通常在命令行上给出。通常这些是文本并且很短。如果输入是一团数据,则通过 stdin 读取它。要处理的数据可能是二进制数据,并且长度很大。
例如,两者cat
都rm
采用文件名列表作为命令行参数(两者都可以采用选项)。sed
也在命令行上接受文件名,但它也接受选项,其中一些可以指定它应用于这些文件中的数据的实际指令。它们都对指定的文件进行操作:cat
打印其内容、rm
删除它们、sed
按照指定的方式处理内容。
只是如果没有给出文件名,默认情况下两者sed
都会读取标准输入。cat
如何轻松判断命令接受哪种输入法?
你阅读手册。或者根据合理的情况进行猜测。
ls
举一个简单的例子,为什么我可以通过管道输出to的文件名cat
进行显示,但不能对 执行相同的操作rm
?
因为cat
默认情况下从标准输入获取数据流,rm
只获取文件名列表。请注意,如果您这样做ls | cat
,那就是不是 cat
作用于列出的文件名,但作用于列出本身。即使ls
打印foo.txt
、ls |cat
输出foo.txt
,但是cat foo.txt
会输出内容的foo.txt
。ls * |cat
不等于cat *
.
现在,通过不带选项的普通管道cat
有点无用,因为它只是按原样传递数据。使用 可能更有意义cat -n
。
是的,输出ls
本质上是数据流,就像任何文本文件一样。对于人眼来说,它可能看起来像一个文件名列表,但实际上很难适当地就这样使用它,以至于人们写过类似的文章https://mywiki.wooledge.org/ParsingLs来反对它。这些文章在很多场合都被链接到这里,这是一种很常见的误解。 (是的,当然有类似的工具xargs
(尝试)将数据流转换为命令行参数列表,但是,好吧......只需记住换行符在文件名中是有效的。)
是否存在哲学原因、深层技术原因或实施中的任意选择导致了这种行为差异?
这听起来像是情人眼里出西施,但FWIW,我只看到了实际的技术原因(见上文)。
(*其他情况:sed
也可以从文件中处理指令,该文件通过命令行选项 ( -f
) 命名。如果脚本很长,这很有意义,就像将 C 程序存储在文件中一样。有些程序读取配置文件包含与命令行上相同的选项,这是一些人做出的选择,也很有意义,因为这样用户只需学习一组魔术词,并且文件更适合永久存储。)
答案3
如果您正在设计一台用于切割木材的机器,您会认为输入的数量是木材,并且切割厚度等参数将在任务持续时间内设置。
许多数据任务都与这个类比有关。它适用于各种安排和架构:链接(管道)、并行化、分布。该范例非常灵活且强大。
wood > board 15cm | style moulding | transport | varnishing | transport > deposit
所以问题“什么”和“以什么方式”首先得到答复。如果一个任务可以处理任意大的数据量,那么它必然是一个输入,因此其他信息是设置或参数。
设计良好的程序是过滤器,也就是说,它们只是通过管道进行协作。您会注意到非常常见的程序sed, grep, awk, gzip, cat, md5sum, base64, bash, ssh
等被设计为读取标准输入。或者,它们也接受文件名作为参数,它们是数据源而不是数据本身。文件名是元数据,而内容是数据。
md5sum < /etc/crontab # don't care metadata
md5sum /etc/crontab # same result with metadata
rm
是模棱两可的,如果它接受一个条目,它可能是要删除的路径,但将它们保留为参数似乎更方便。
无论如何,有很多方法可以从输入流切换到参数。将输入转换为参数时,必须考虑解释器的限制和其他解析陷阱(引号、分隔符、替换)
ls | xargs -d'\n' rm # apply rm according to arg length limits
# NOT broken by spaces in filenames
# broken by newlines in filenames
ls | parallel -m rm # apply rm whith many parallel processes
# NOT broken by spaces in filenames
# broken by newlines in filenames
ls | while read;do rm "$REPLY";done # apply rm one by one
# NOT broken by spaces in filenames
# broken by newlines in filenames
ls | split -l1 --filter='read && rm "$REPLY"'
# why make it simple when you can make it complicated
rm $(ls) # apply rm all at once
# broken by arg length limit
# broken by space in filenames
rm * # apply rm all at once
# broken by arg length limit
# NOT broken by newline in filenames
(结论:如果你想出现问题,请在文件名中添加换行符)
有时,当您处理业务数据时,您倾向于在参数中放入一些表征数据的信息,例如要处理的数据类型或其他元数据。我认为区分元数据和其他参数是个好主意。只要机器能够读取并适应元数据,元数据就会自然地与数据一起发生。
例子:
hard wood > board 15cm | style moulding | transport | varnishing | transport > deposit
在这种情况下,如果切割机和成型机能够调整其功率和刀片,而不是由操作员检查调整,您就会获益。
有许多程序示例将一些元数据和数据放在一起构建单个数据流:
tar -cf- /etc | tar -tf-
最后,在很大程度上分布式的业务工作流程中,您应该明确区分仅涉及特定任务的信息(即本地参数)和遵循输入、数据和元数据的信息,特别是因为需要可追溯性。
还有一件事要知道:参数是命令公开可见的ps
,因此设计带有秘密参数的程序是一个非常糟糕的主意。而是使用间接输入法。