背景
相信大部分开发或运维同学,都有接触和使用过 .env 文件,不少的框架和工具都支持从 .env 文件中加载环境变量,比如 PHP 语言的 Laravel 框架或者是 Docker 的系列工具。
但是 .env 文件的读取,基本都依赖特定的语言库或者是特定的工具,如果希望在任何一个 Linux 或者类 Linux 环境中,加载 .env 定义的环境变量,并给后续的程序使用,又要怎么做呢?
dotenv 简介
在尝试开发这个功能的时候,我有想过利用 dotenv 作为关键词去搜索,看看前辈们是如何解决的。
但很不幸地发现,并没有找到非常官方的介绍,也就没有官方标准,而是每个语言库对其各自理解和实现。
好在各自实现的功能相差无几,基本都能正常理解以下的定义方式,也就没有太多转换的困难。
KEY_1=VAL_1
KEY_2='VAL_2'
KEY_3="VAL_3"
KEY_4=any value here
KEY_5=any value here # comment
Shell 的困境
对于各种语言库,它们大部分可以利用高级语言去编程,配合各种高效的库,可以很方便地加载 .env 文件的内容并处理。但对于纯粹的 Shell 来说,就没有那么容易了。
使用 Shell 加载环境变量,一般考虑直接使用 source .env 或者 export KEY_1=VAL_1 的方式。但无论是哪种,都不得不考虑一些特殊情况。
如何校验 KEY 的合法性
首先第一个问题就是 KEY 的合法性,只能允许英文字母和数字以及下划线等字符,且不允许以数字开头。对于不合法的 KEY,在加载环境变量前需要进行过滤,否则可能导致脚本异常。
如何处理引号
用户在使用引号时,可能会出现五花八门的情况:
- 不使用任何引号
- 不使用单引号/双引号包裹变量值,且变量值内有引号
- 使用单引号/双引号包裹变量值,且变量值内没有引号
- 使用单引号/双引号包裹变量值,且变量值内出现引号
要如何正确的理解用户意图,或者引导用户按我们的处理原理去使用引号,是非常困难的事情。
如何处理空格及特殊符号
如果变量值没有被引号包裹,但包含了空格或者其它特殊符号,在使用 source 和 export 时,也会出现如 syntax error 的错误,导致后续的其它变量无法正常设置。
如何处理多行文本
对于变量值是多行文本的情况要如何处理,也是一个比较棘手的问题。比如用户可能是这样定义的变量:
KEY_A=any
multiple
line
value
又或者用户在 .env 文件中是这样写的:
KEY_B=any\nmultiple\nline\nvalue
要怎么样做,才算是理解了用户的真正意图?
别人怎么做的
在互联网上一番搜寻之后,得到的答案无外乎 source、export、eval 几种方式,但无论哪种都没有完全解决上述的几种问题。
于是,我去对比了 GitHub Actions 和 GitLabCI 的做法:
- 从 GitLabCI 的 文档 来看,它不支持多行文本,但变量值是否被引号包裹它都能处理
- 但实际看它的代码,发现和文档描述的差别太大了
- 从代码和实测效果来看,它不能处理换行,也不能处理有引号的情况(引号也被当作是变量值的一部分)
- 从 GitHub Actions 的 文档 来看,它通过另外的格式支持多行文本,但变量值的的引号也视为变量值的一部分
- InitializeFiles
- ProcessCommand:
- EnvFileKeyValuePairs
鉴于 GitLabCI 的不靠谱行为(文档和实际行为不一致),而 GitHub Actions 给了一个相对完美的解决方案,而且 GitHub Actions 作为业界标杆,按照它的方式来实现,也是比较可以接受的。
用 Shell 实现
模仿的目标确定了,接下来就是要用 Shell 来实现 GitHub Actions 的逻辑,同时要尽量兼容 sh、bash、zsh 的不同语法。废话不多说,直接上代码:
ENV_FILE=".env"
if [ -f "$ENV_FILE" ]; then
set -a
ENV_LINE=""
ENV_DELI=""
ENV_MATCH="no"
ENV_KEY=""
ENV_VAL=""
while read -r ENV_LINE
do
if [ "$ENV_DELI" != "" ]; then
if [ "$ENV_LINE" = "$ENV_DELI" ]; then
if [ "ENV_VAL" != "" ]; then
ENV_VAL=${ENV_VAL%??}
fi
ENV_DELI=""
ENV_MATCH="yes"
else
ENV_VAL="${ENV_VAL}${ENV_LINE}\n"
fi
else
if [ "$(echo $ENV_LINE | grep -E '=|<<')" != "" ]; then
if [ $(echo ${ENV_LINE%%<<*} | wc -c) -lt $(echo ${ENV_LINE%%=*} | wc -c) ]; then
ENV_KEY="${ENV_LINE%%<<*}"
ENV_VAL=""
ENV_DELI="${ENV_LINE#*<<}"
ENV_MATCH="no"
else
ENV_KEY="${ENV_LINE%%=*}"
ENV_VAL="${ENV_LINE#*=}"
ENV_DELI=""
ENV_MATCH="yes"
fi
fi
fi
if [ "$ENV_MATCH" = "yes" ]; then
if [ "$(echo $ENV_KEY | grep -E '^[a-zA-Z_][a-zA-Z0-9_]*')" != "" ]; then
eval "$ENV_KEY=\$ENV_VAL"
fi
ENV_KEY=""
ENV_VAL=""
ENV_MATCH="no"
fi
done < $ENV_FILE
unset ENV_LINE
unset ENV_DELI
unset ENV_MATCH
unset ENV_KEY
unset ENV_VAL
set +a
fi
总结
以上就是本次折腾的心历路程,如果对正在冲浪的你有所帮助,那便是极好的。