参数传递策略——环境变量 vs. 命令行

106
我们开发人员编写的大多数应用程序需要在启动时进行外部参数化。 我们会传递文件路径、管道名称、TCP/IP地址等。到目前为止,我一直在使用命令行将这些传递给正在启动的应用程序。我必须在main中解析命令行并将参数传递到需要它们的位置,这当然是一个好的设计,但对于大量参数来说,难以维护。 最近,我决定使用环境变量机制。它们是全局的,可以从任何地方访问,这在架构角度上不太优雅,但减少了代码量

这是我对两种策略的第一印象(可能相当肤浅),但我想听听更有经验的开发人员的意见——使用环境变量和命令行参数传递参数给进程的优缺点是什么?我想考虑以下问题:

  1. 设计质量(灵活性/可维护性)
  2. 内存限制
  3. 解决方案的可移植性

备注:

Ad. 1. 这是我最感兴趣的主要方面。

Ad. 2. 这有点实用。我知道在Windows上有一些当前限制巨大(命令行和环境块都超过32kB)。不过,如果您需要传递大量参数,则只需使用文件即可。

Ad. 3. 我对Unix几乎一无所知,因此不确定两种策略在可用性方面是否与Windows中的可用性类似。如果可以,请详细说明。


你能提供更具体的信息吗?比如实际参数的数量?它们是否有分组,还是都是随机的?这段代码是用哪种编程语言写的?Java、C++等等... 我询问这么详细的原因是,虽然在任何语言中处理这个问题都可能会有困难,但可能有针对特定语言实现的解决方案,你可能不知道。 - James Drinkard
1
仅提及*nix操作系统,它们没有像“全局环境变量”这样的东西,每个环境变量都是在fork时从父进程传递给子进程的。因此,在至少对于这些操作系统而言,“全局”并不是环境变量优于命令行的优势。 - shr
嗨,@jamesDrinkard。我对一般方法很感兴趣。如果您想要将20个不同标记的字符串/整数/实数参数从Python脚本通过32位解释器传递给用C++编写的64位应用程序,您会使用哪种方法? - Janusz Lenar
嗨,@shr。感谢你的*nix笔记。正如Raymond在下面指出的那样,对于这个任务来说,这种全局性根本不是优点。 - Janusz Lenar
2
这可能与环境变量有关,并提倡使用它们:https://devcenter.heroku.com/articles/config-vars - eyeApps LLC
4个回答

96

1) 我建议尽可能避免使用环境变量。

环境变量的优点

  • 易于使用,因为它们可以从任何地方访问。 如果需要许多独立程序使用某一信息,则此方法更为方便。

环境变量的缺点

  • 难以正确使用,因为它们可以从任何地方看到(可以删除,可以设置)。 如果我安装一个依赖于环境变量的新程序,它会覆盖我的现有环境变量吗? 昨天胡闹时是否无意中弄坏了环境变量?

我的观点

  • 对于那些最有可能针对每个程序调用进行不同设置的参数(即计算n!的程序的n),请使用命令行参数。
  • 对于用户可能合理想要更改但不是很频繁的参数(例如窗口弹出时的显示大小)请使用配置文件。
  • 尽量少使用环境变量 - 最好只用于预计不会发生更改的参数(即Python解释器的位置)。
  • 你的观点它们是全局的并且可以从任何地方访问,这在架构角度上不太优雅,但限制了代码量让我想起了使用全局变量的理由 ;)

我亲身经历过度使用环境变量的伤疤

  • 我们在工作中需要使用的两个程序因环境冲突无法同时运行在同一台计算机上
  • 具有相同名称但存在不同错误的多个程序版本 - 由于程序位置从环境中提取并且(悄悄地,微妙地)错误,使整个研讨会陷入瘫痪数小时。

2) 限制

如果我超出命令行或环境可以处理的限制,我会立即进行重构。

我以前在一个命令行应用程序中使用过JSON,因为它需要很多参数。能够使用字典和列表、字符串和数字非常方便。该应用程序只需要几个命令行参数,其中之一是JSON文件的位置。

这种方法的优点

  • 不需要编写大量(痛苦的)代码与CLI库进行交互--获取许多常见库来强制执行复杂约束可能很棘手(通过“复杂”,我指的是比检查特定键或在一组键之间交替更复杂的约束)
  • 不必担心CLI库对参数顺序的要求--只需使用JSON对象!
  • 易于表示复杂数据(回答什么不能适合命令行参数?)例如列表
  • 易于从其他应用程序使用数据--两者都可以创建和以编程方式解析
  • 易于适应未来扩展

注意:我想区分这种方法与.config文件方法--这不是用于存储用户配置。也许我应该称其为“命令行参数文件”方法,因为我将其用于需要许多值的程序,而这些值不适合放在命令行上。


3)解决方案的可移植性:我不太了解Mac、PC和Linux之间在环境变量和命令行参数方面的差异,但我可以告诉您:

  • 所有三种操作系统都支持环境变量
  • 它们都支持命令行参数

是的,我知道--这并没有多大帮助。很抱歉。但关键点是,您可以期望合理的解决方案是可移植的,尽管您肯定要为自己的程序验证这一点(例如,命令行参数是否在任何平台上区分大小写?在所有平台上?我不知道)。


最后一个要点:

正如Tomasz所提到的,对于大多数应用程序来说,参数来自哪里并不重要。


1
谢谢,Matt。这是我一直在寻找的一种意见。你最重要的建议是使用环境变量来描述执行环境,它几乎不会改变,以及 cmd 文件来传递实际执行的简单/复杂参数。非常合理,谢谢。但请注意,您可以使用“本地”环境变量,这可能会弄乱子进程。这与命令行参数传递非常相似,除了 Raymond 在 Tomasz 的答案下指出的内容。 - Janusz Lenar
2
非常好的答案!关于环境变量可以从任何地方更改的缺点:还有一种可能性是从启动脚本(例如Bash或Batch脚本)本地设置应用程序的环境变量。在这种情况下,可以有一个系统范围的默认值,但如果必要,应用程序可以将默认值更改为自定义值。您对此有什么想法? - Lii
在考虑如何传递机密/凭据时,是否存在任何利弊? - iamyojimbo
2
我同意对于桌面和CLI应用程序。对于有许多部署的云系统,环境变量是一个很好的替代方案,例如在12因素指南中推荐的:https://12factor.net/config - Lars Grammel
3
我使用过的每个操作系统都可以为每个进程设置环境变量。Foo=bar app & Foo=baz app将运行两个相同程序的实例,每个实例都提供不同的Foo值。因此,只要您知道如何正确管理变量的作用域,那么您需要在工作中运行的这两个程序可以同时运行,而不会发生环境变量冲突。 - M-Pixel
1
同意 @M-Pixel -- 感谢您指出这一点,说实话,我的回答已经过时了。我写它的时候是考虑到桌面/CLI程序,但正如其他人所指出的,这个建议对于在不同环境(如容器)中运行没有任何意义。我会尝试想出一个好的方法来编辑这个答案,以明确我所考虑的上下文。再次感谢您指出这一点! - Matt Fenwick

10
你应该使用策略模式抽象读取参数。创建一个名为ConfigurationSource的抽象,其中包含readConfig(key) -> value方法(或返回一些Configuration对象/结构),具有以下实现:
  • CommandLineConfigurationSource
  • EnvironmentVariableConfigurationSource
  • WindowsFileConfigurationSource - 从C:/Document and settings...中加载配置文件
  • WindowsRegistryConfigurationSource
  • NetworkConfigrationSource
  • UnixFileConfigurationSource - 从/home/user/...中加载配置文件
  • DefaultConfigurationSource - 默认值
  • ...
你也可以使用责任链模式以不同的配置链接源,例如:如果没有提供命令行参数,则尝试环境变量,如果其他所有方法都失败,则返回默认值。

广告 1. 这种方法不仅允许您抽象读取配置,而且可以轻松更改底层机制,而不会影响客户端代码。此外,您可以同时使用多个来源,回退或从不同来源收集配置。

广告 2. 只需选择适合的实现即可。当然,某些配置条目可能不适合例如命令行参数。

广告 3. 如果某些实现不可移植,请使用两个实现,在给定系统不适用时默默忽略/跳过一个实现。


谢谢,这通常是个好主意。但它并不能帮助我们决定是使用环境还是命令行。对于你第二点中的“某些配置条目例如无法适应命令行参数”的解释会很有帮助。什么内容无法适应字符串?如果不适合,那么应该通过某种文件间接传递,对吧? - Janusz Lenar
1
我的观点是:不要强制用户使用命令行参数环境变量。要灵活(同时保持可维护的代码)。我认为配置文件是存储配置的最佳位置(它可以任意长,包含注释等),但有时使用命令行参数覆盖文件配置也很有用。什么不能适应命令行参数?如果您需要传递多个文件路径,它可能有效,但没有人喜欢过长的命令行。 - Tomasz Nurkiewicz
配置文件是最好的参数选项 - 这是有价值的意见,支持评论是使用它的一个很好的理由,谢谢。如果您在批处理脚本中启动应用程序时使用环境变量,可以使用remset使其具有非常可读的形式。如果您正在生成进程,则只需在spawnl之前设置所需的setenv即可。它方便、可读性强且灵活。为什么要使用.config而不是环境?这就是问题所在。 - Janusz Lenar
3
请注意环境变量是会被继承的。假设您的程序有两个参数ACTION和一个可选的NOTIFY。程序A设置ACTION=if owner=nobody set owner=bobNOTIFY=send,然后运行您的程序。您的程序更新一个项目,然后发现NOTIFY已经设置并运行sendsend程序会向Bob发送电子邮件,然后再次运行您的程序,设置ACTION=set last_send = today。它不想要任何通知,因此不设置NOTIFY。但它从程序A继承NOTIFY,所以您的程序将最后一次运行更新为今天,然后运行send。这将导致无限循环。 - Raymond Chen
1
谢谢,@Raymond。环境变量的作用范围太广了,确实是个好观点。 - Janusz Lenar
这必须是Java的规范答案。这就是为什么我不喜欢Java。 - j-a

7

我认为这个问题已经得到了很好的回答,但是我觉得它值得在2018年更新一下。我觉得环境变量的一个未被提及的好处是它们通常需要更少的样板代码来处理。这使得代码更加清晰易读。然而,一个主要的缺点是它们从运行在同一台机器上的不同应用程序中删除了一层隔离。我认为这就是Docker真正发挥作用的地方。我的最喜欢的设计模式是仅使用环境变量并在Docker容器内运行应用程序。这消除了隔离问题。


2
我基本上同意之前的答案,但有另一个重要方面:可用性
例如,在git中,您可以创建一个存储库,并将.git目录放在外部。要指定它,可以使用命令行参数--git-dir或环境变量GIT_DIR
当然,如果你将当前目录更改为另一个存储库或在脚本中继承环境变量,就会出现错误。但是,如果您需要在一个终端会话的分离存储库中键入多个git命令,则这非常方便:您不需要重复git-dir参数。
另一个例子是GIT_AUTHOR_NAME。似乎它甚至没有一个命令行伙伴(但是,git commit有一个--author参数)。GIT_AUTHOR_NAME覆盖了user.nameauthor.name配置设置。
总的来说,在UNIX上使用命令行或环境参数同样简单:可以使用命令行参数。
$ command --arg=myarg

或者在一行中使用环境变量:

$ ARG=myarg command

alias中,捕获命令行参数也很容易:

alias cfg='git --git-dir=$HOME/.cfg/ --work-tree=$HOME'  # for dotfiles
alias grep='grep --color=auto'

一般而言,大多数参数都是通过命令行传递的。我同意之前的答案,这种方式更加实用和直接,脚本中的环境变量就像程序中的全局变量一样。 GNU libc 表示:

argv 机制通常用于传递特定于被调用程序的命令行参数。另一方面,环境跟踪由许多程序共享、不经常更改且较少使用的信息。

除了关于环境变量危险性的说法外,它们还有很好的用例。GNU make 对环境变量有非常灵活的处理方式(因此与 shell 非常集成):

make 启动时看到的每个环境变量都会转换为具有相同名称和值的 make 变量。但是,在 makefile 中或使用命令参数进行显式赋值会覆盖环境变量。(-- 并且有一个选项来更改此行为)...

因此,通过在您的环境中设置变量CFLAGS,您可以使大多数makefile中的所有C编译都使用您喜欢的编译器开关。对于具有标准或常规含义的变量来说,这是安全的,因为您知道没有任何makefile会将它们用于其他事情。
最后,我要强调的是,对于一个程序来说,最重要的不是程序员,而是用户体验。也许您已经将其包含在设计方面了,但内部和外部设计是非常不同的实体。
关于编程方面,我想说几句。您没有写明使用哪种语言,但我们可以想象您的工具允许您进行最佳的参数解析。在Python中,我使用argparse,它非常灵活且功能丰富。要获取解析的参数,可以使用类似以下的命令:
args = parser.parse_args()
args可以进一步分解为已解析的参数(例如args.my_option),但我也可以将它们作为整体传递给我的函数。如果您的语言允许,这种解决方案绝对不会“难以维护大量参数”。实际上,如果您有许多参数,并且它们在参数解析期间未被使用,请将它们传递到容器中,以避免代码重复(这会导致不灵活性)。
最后的评论是,解析环境变量比命令行参数容易得多。环境变量只是一对VARIABLE=value。命令行参数可能更加复杂:它们可以是位置参数或关键字参数,也可以是子命令(例如git push)。它们可以捕获零个或多个值(请回想一下命令echo和像-vvv这样的标志)。有关更多示例,请参见argparse

还有一件事。你对内存的担忧有点令人不安。不要编写泛泛而谈的程序。库应该是灵活的,但是一个好的程序应该没有任何参数就可以使用。如果需要传递大量内容,这可能是数据,而不是参数。如何将数据读入程序是一个更为普遍的问题,没有适用于所有情况的单一解决方案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接