侧边栏壁纸
博主头像
分享你我博主等级

行动起来,活在当下

  • 累计撰写 117 篇文章
  • 累计创建 13 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

js解析sh文件

管理员
2025-03-09 / 0 评论 / 0 点赞 / 18 阅读 / 11525 字
/**
 * 管理 .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);
})();
```


0

评论区