/**
* 管理 .sh 文件或字符串内容中的环境变量
*/
class ShEnvManager {
/**
* 构造函数
* @param {Object} options - 配置选项
* @param {string} options.input - .sh 文件路径或内容字符串
* @param {boolean} [options.isFile=false] - 输入是否为文件路径
*/
constructor(options = {}) {
if (!options.input || typeof options.input !== 'string') {
throw new Error('Input must be a non-empty string');
}
this.input = options.input;
this.isFile = options.isFile || false;
}
/**
* 解析内容并提取指定环境变量的值
* @param {string|string[]} keys - 要提取的环境变量名称,单个键或键数组
* @returns {Promise<Object|string|null>} - 单个键返回字符串,多个键返回对象,未找到返回 null
*/
async extractEnv(keys) {
try {
// 获取内容
const content = this.isFile ? await this._readFile(this.input) : this.input;
const lines = content.split('\n');
// 处理单个或多个键
const keyArray = Array.isArray(keys) ? keys : [keys];
const results = {};
let i = 0;
while (i < lines.length) {
let line = lines[i].trim();
// 跳过空行或注释
if (line === '' || line.startsWith('#')) {
i++;
continue;
}
// 匹配 export 语句,仅要求开头有引号
const exportMatch = line.match(/^export\s+([A-Za-z0-9_]+)=(['"])(.*)/);
if (exportMatch && keyArray.includes(exportMatch[1])) {
const key = exportMatch[1];
let value = exportMatch[3] || '';
const quoteType = exportMatch[2];
// 处理多行值
if (!line.endsWith(quoteType)) {
let j = i + 1;
let multiLineValue = [value];
while (j < lines.length && !lines[j].trim().endsWith(quoteType)) {
multiLineValue.push(lines[j].trim());
j++;
}
if (j < lines.length && lines[j].trim().endsWith(quoteType)) {
// 去除最后一行的结束引号
const lastLine = lines[j].trim().replace(new RegExp(`${quoteType}$`), '');
if (lastLine) multiLineValue.push(lastLine);
i = j;
}
value = multiLineValue.join('\n');
} else {
// 单行值,去除结束引号
value = value.replace(new RegExp(`${quoteType}$`), '');
}
results[key] = value;
}
i++;
}
// 记录未找到的键
keyArray.forEach((key) => {
if (!results[key]) {
console.log(`Environment variable ${key} not found in input`);
results[key] = null;
}
});
return Array.isArray(keys) ? results : results[keys] || null;
} catch (error) {
console.error(`Error processing input:`, error);
throw error;
}
}
/**
* 更新环境变量并返回更新后的内容
* @param {Object} updates - 要更新的键值对
* @param {Object} [options] - 可选配置
* @param {string} [options.quoteType] - 强制写入时的引号类型(' 或 ")
* @returns {Promise<string>} - 返回更新后的内容
*/
async updateEnv(updates, options = {}) {
try {
const content = this.isFile ? await this._readFile(this.input) : this.input;
const lines = content.split('\n');
// 获取现有键的引号类型
const quoteTypes = await this._getQuoteTypes(content);
let updatedLines = [];
let i = 0;
while (i < lines.length) {
let line = lines[i];
// 跳过空行或注释
if (line.trim() === '' || line.trim().startsWith('#')) {
updatedLines.push(line);
i++;
continue;
}
// 匹配 export 语句
const exportMatch = line.match(/^export\s+([A-Za-z0-9_]+)=(['"])(.*)/);
if (exportMatch) {
const key = exportMatch[1];
const originalQuoteType = exportMatch[2];
let value = exportMatch[3] || '';
let fullLine = line;
// 处理多行值
if (!line.trim().endsWith(originalQuoteType)) {
let j = i + 1;
while (j < lines.length && !lines[j].trim().endsWith(originalQuoteType)) {
value += '\n' + lines[j];
fullLine += '\n' + lines[j];
j++;
}
if (j < lines.length) {
value += '\n' + lines[j];
fullLine += '\n' + lines[j];
i = j;
}
}
// 更新值
if (updates.hasOwnProperty(key)) {
const newValueLines = String(updates[key]).split('\n');
const formattedValue = newValueLines.join('\n');
const quoteType = options.quoteType || quoteTypes[key] || '"';
updatedLines.push(`export ${key}=${quoteType}${formattedValue}${quoteType}`);
} else {
updatedLines.push(fullLine);
}
} else {
updatedLines.push(line);
}
i++;
}
return updatedLines.join('\n');
} catch (error) {
console.error(`Error processing input:`, error);
throw error;
}
}
/**
* 添加新的环境变量并返回更新后的内容
* @param {Object} newVars - 要添加的键值对
* @param {Object} [options] - 可选配置
* @param {string} [options.quoteType] - 写入时的引号类型(' 或 ")
* @returns {Promise<string>} - 返回更新后的内容
*/
async addEnv(newVars, options = {}) {
try {
const content = this.isFile ? await this._readFile(this.input) : this.input;
const quoteType = options.quoteType || '"';
// 追加新变量
const newLines = Object.entries(newVars).map(([key, value]) => {
const formattedValue = String(value).split('\n').join('\n');
return `export ${key}=${quoteType}${formattedValue}${quoteType}`;
});
return content + '\n' + newLines.join('\n') + '\n';
} catch (error) {
console.error(`Error adding variables:`, error);
throw error;
}
}
/**
* 获取现有环境变量的引号类型
* @param {string} content - .sh 内容
* @returns {Promise<Object>} - 键到引号类型的映射
* @private
*/
async _getQuoteTypes(content) {
const lines = content.split('\n');
const quoteTypes = {};
let i = 0;
while (i < lines.length) {
let line = lines[i].trim();
if (line === '' || line.startsWith('#')) {
i++;
continue;
}
const exportMatch = line.match(/^export\s+([A-Za-z0-9_]+)=(['"])(.*)/);
if (exportMatch) {
const key = exportMatch[1];
const quoteType = exportMatch[2];
quoteTypes[key] = quoteType || '"';
if (!line.endsWith(quoteType)) {
let j = i + 1;
while (j < lines.length && !lines[j].trim().endsWith(quoteType)) {
j++;
}
i = j;
}
}
i++;
}
return quoteTypes;
}
/**
* 读取文件内容
* @param {string} filePath - 文件路径
* @returns {Promise<string>} - 文件内容
* @private
*/
async _readFile(filePath) {
return await fs.readFile(filePath, 'utf8');
}
/**
* 更新输入内容
* @param {string} input - 新的 .sh 文件路径或内容字符串
* @param {boolean} [isFile=false] - 输入是否为文件路径
*/
setInput(input, isFile = false) {
if (!input || typeof input !== 'string') {
throw new Error('Input must be a non-empty string');
}
this.input = input;
this.isFile = isFile;
}
}
示例:
```
## 在运行 ql repo 命令时,是否自动删除失效的脚本与定时任务
AutoDelCron="true"
## 在运行 ql repo 命令时,是否自动增加新的本地定时任务
AutoAddCron="true"
## 拉取脚本时默认的定时规则,当匹配不到定时规则时使用,例如: 0 9 * * *
DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js mjs py pyc so ts sh"
## 代理地址,支持HTTP/SOCK5,例如 http://127.0.0.1:7890
ProxyUrl=""
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
CpuWarn=80
MemoryWarn=80
DiskWarn=90
## 设置定时任务执行的超时时间,例如1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="3h"
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
RandomDelay=""
## 需要随机延迟运行任务的文件后缀,直接写后缀名即可,多个后缀用空格分开,例如: js py ts
## 默认仅给javascript任务加随机延迟,其它任务按定时规则准点运行。全部任务随机延迟赋值为空
RandomDelayFileExtensions=""
## 每小时的第几分钟准点运行任务,当在这些时间运行任务时将忽略 RandomDelay 配置,不会被随机延迟
## 默认是第0分钟和第30分钟,例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空
RandomDelayIgnoredMinutes=""
## 如果你自己会写shell脚本,并且希望在每次容器启动时,额外运行你的 shell 脚本,请赋值为 "true"
EnableExtraShell=""
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl=""
## 通知环境变量
## 1. Server酱
## https://sct.ftqq.com
## 下方填写 SCHKEY 值或 SendKey 值
export PUSH_KEY=""
## 2. BARK
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
export BARK_PUSH=""
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
export BARK_ICON="https://qn.whyour.cn/logo.png"
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong"
export BARK_GROUP="QingLong"
## bark 推送时效性
export BARK_LEVEL="active"
## bark 推送是否存档
export BARK_ARCHIVE=""
## bark 推送跳转 URL
export BARK_URL=""
```
用法:
```
/**
* 示例用法
*/
(async () => {
const envManager = new ShEnvManager({ input: path.join(__dirname, 'config.sh'), isFile: true }); // 默认使用单引号
// 示例 1:提取单个环境变量
console.log('=== Extracting JDHEALTH_SHARECODES ===');
const jdHealthValue = await envManager.extractEnv('JDHEALTH_SHARECODES');
console.log('Extracted JDHEALTH_SHARECODES:', jdHealthValue);
// 示例 2:提取多个环境变量
console.log('\n=== Extracting multiple values ===');
const values = await envManager.extractEnv(['JDHEALTH_SHARECODES', 'kg_ck']);
console.log('Extracted multiple values:', values);
// 示例 3:更新环境变量
console.log('\n=== Updating values ===');
const updatedContent = await envManager.updateEnv({
JDHEALTH_SHARECODES: 'T0225KkcR0wR81zSJR79nf4CfQCjVfnoaW5kRrbA_new',
kg_ck: `dfid=new_dfid&signature=new_signature
signature=new_signature2`
});
// 示例 4:添加新变量
console.log('\n=== Adding new variable ===');
const addedContent = await envManager.addEnv({
NEW_VAR: 'new_value'
});
// 示例 5:更新输入内容(可选)
console.log('\n=== Testing with new input ===');
envManager.setInput(`
export NEW_KEY='new_value'
`, false);
const newValue = await envManager.extractEnv('NEW_KEY');
console.log('Extracted NEW_KEY:', newValue);
})();
```
版权归属:
管理员
许可协议:
本文使用《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》协议授权
评论区