Node.js 模拟 Linux 环境

🧩 项目介绍

该项目使用 Node.js 实现了一个模拟的 Linux 终端环境 ,支持多种常见的 Linux 命令(如 ls, cd, cat, mkdir, rm 等),所有文件操作都在内存中进行,并持久化到本地文件系统中。适合用于学习 Shell 命令实现原理、文件系统结构或作为教学演示工具。


📦 依赖安装

确保你已安装 Node.js(建议版本 14+),然后运行以下命令安装依赖:

bash 复制代码
npm install

🚀 启动项目

在项目根目录下运行:

bash 复制代码
node index.js

你会看到命令提示符:

bash 复制代码
simu-shell:~$ _

此时你可以输入 Linux 命令进行操作。


📚 支持命令列表

以下是你可以在模拟终端中使用的命令及其基本用法说明:

命令 用法示例 功能说明
help help 显示所有可用命令
exit exit 退出模拟终端
clear clear 清空终端屏幕
history history 查看历史命令
pwd pwd 显示当前路径
ls ls 列出当前目录下的文件
ll ll 显示当前目录下的详细文件信息(带类型、大小、修改时间等)
cd cd /home/user 切换目录
mkdir mkdir newdir 创建目录
rmdir rmdir emptydir 删除空目录
rm rm file.txt rm -r dir 删除文件或目录(递归)
touch touch newfile.txt 创建空文件
echo echo "Hello" > file.txt 将字符串写入文件
cat cat file.txt 查看文件内容
cp cp src.txt dest.txt 复制文件或目录
mv mv oldname.txt newname.txt 移动或重命名文件/目录
head head file.txt head -n 5 file.txt 查看文件前几行
tail tail file.txt tail -n 5 file.txt 查看文件最后几行
grep grep "hello" file.txt 在文件中查找字符串
find find file.txt 查找文件
stat stat file.txt 显示文件或目录的详细信息
vim vim file.txt 编辑文件
yum yum install https://downloads.mysql.com/archives/get/p/23/file/mysql-5.7.26-winx64.zip 下载文件
zip zip -r archive.zip test.txt 压缩文件
unzip unzip archive.zip -d unzip_target 解压文件
rz rz a.txt 上传文件
sz sz a.txt 下载文件

💡 示例操作流程

你可以在模拟终端中依次执行以下命令来测试功能:

bash 复制代码
help
ls
mkdir test
cd test
touch file.txt
echo "Hello World" > file.txt
cat file.txt
cp file.txt copy.txt
ls
mv copy.txt renamed.txt
cat renamed.txt
rm renamed.txt
ls
cd ..
rm -r test

🧪 测试脚本

项目中提供了 test.md 文件,里面包含了完整的测试命令集,建议你在终端中逐步运行测试命令以验证所有功能是否正常。


🧱 数据持久化机制

项目使用了内存中的虚拟文件系统(VFS),并通过以下方式持久化:

  • 文件内容保存在 storage/files/ 目录下,使用 UUID 命名;
  • 文件系统结构保存在 vfs_data.json 中,每次操作后会自动保存。

📁 项目结构说明

复制代码
nodejs模拟Linux环境/
├── index.js                  # 主程序入口
├── shell.js                  # 命令处理核心逻辑
├── commands/                 # 各命令的实现
├── vfs.js                    # 虚拟文件系统核心
├── storage.js                # 文件系统结构的持久化
├── fileStorage.js            # 文件内容的持久化
├── utils.js                  # 工具函数(如引号解析)
├── test.md                   # 测试命令列表
├── README.md                 # 本文件
└── package.json              # 项目依赖配置

✅ 项目特点

  • 🧠 使用纯 Node.js 实现,无需依赖外部库(除 uuid);
  • 💾 支持数据持久化,重启后可恢复文件系统状态;
  • 📚 支持大多数常见 Linux 命令;
  • 🛠️ 结构清晰,便于扩展新命令或修改现有逻辑;
  • 🧪 提供完整测试用例,方便验证功能。

📎 扩展建议

你可以根据需要扩展以下功能:

  • 添加新的命令(如 chmod, chmod, grep -r);
  • 支持管道(|)和重定向(>>, <);
  • 支持用户权限管理;
  • 添加命令自动补全;
  • 添加图形化界面(Electron);
  • 支持多用户系统。

源码下载

Node.js 模拟 Linux 环境

核心代码

tool/index.js

javascript 复制代码
// index.js
const readline = require('readline');
const { processCommand } = require('./shell');
const { loadHistory, saveHistory } = require('./historyStorage');

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
    prompt: 'simu-shell:~$ ',
});

// 启动时清屏
process.stdout.write('\x1B[2J\x1B[0f');

rl.history = loadHistory();
rl.historySize = 100;

rl.prompt();

rl.on('line', (line) => {
    const trimmed = line.trim();
    processCommand(trimmed, rl, () => {
        rl.prompt();
    });
}).on('close', () => {
    saveHistory(rl.history);
    console.log('\n退出模拟终端');
    process.exit(0);
});

tool/vfs.js

javascript 复制代码
// vfs.js

const path = require('path');

const fs = require('./storage').load({
    '/': {
        type: 'dir',
        children: {
            home: {
                type: 'dir',
                children: {
                    user: {
                        type: 'dir',
                        children: {
                            'file.txt': { type: 'file', content: 'Hello World' },
                            'notes.md': { type: 'file', content: '# My Notes' }
                        }
                    }
                }
            },
            bin: {
                type: 'dir',
                children: {}
            }
        }
    }
});

const storage = require('./storage');

// 每次修改后自动保存
function persist() {
    storage.save(fs);
}

// 提供一个统一的写入接口
function updateFilesystem(mutateFn) {
    mutateFn(fs);
    persist();
}

function readdir(path, callback) {
    const parts = path.split('/').filter(p => p !== '');
    let current = fs['/'];

    for (let part of parts) {
        if (current && current.type === 'dir' && current.children[part]) {
            current = current.children[part];
        } else {
            return callback(`找不到目录: ${path}`);
        }
    }

    callback(null, Object.keys(current.children));
}

function chdir(path, currentDir, callback) {
    const resolvedPath = resolvePath(path, currentDir);
    const parts = resolvedPath.split('/').filter(p => p !== '');
    let current = fs['/'];

    for (let part of parts) {
        if (current && current.type === 'dir' && current.children[part]) {
            current = current.children[part];
        } else {
            return callback(`找不到目录: ${resolvedPath}`);
        }
    }

    callback(null, resolvedPath);
}

function resolvePath(path, currentDir) {
    if (path.startsWith('/')) return path;
    if (currentDir === "/") return normalizePath(`/${path}`);
    return normalizePath(`${currentDir}/${path}`);
}

function normalizePath(inputPath) {
    // 使用 path.normalize 解析 .. 等相对路径
    let normalized = path.normalize(inputPath)
        .replace(/^(\.\.\/|\/)?/, '')  // 移除开头的 ./ ../ /
        .replace(/\\/g, '/');          // 统一为正斜杠

    if (normalized.startsWith("/")) {
        return normalized;
    }
    return '/' + normalized;
}

function getNodeByPath(path) {
    const parts = path.split('/').filter(p => p !== '');
    let current = fs['/'];
    for (let part of parts) {
        if (current && current.type === 'dir' && current.children[part]) {
            current = current.children[part];
        } else {
            return null;
        }
    }
    return current;
}

function getDirStats(node) {
    let totalSize = 0;
    let latestTime = new Date(node.mtime);

    function traverse(current) {
        if (current.type === 'file') {
            totalSize += current.size;
            const mtime = new Date(current.mtime);
            if (mtime > latestTime) latestTime = mtime;
        } else if (current.type === 'dir') {
            for (let child of Object.values(current.children)) {
                traverse(child);
            }
        }
    }

    traverse(node);
    return {
        size: totalSize,
        mtime: latestTime.toISOString()
    };
}

module.exports = {
    fs,
    readdir,
    chdir,
    resolvePath,
    normalizePath,
    updateFilesystem,
    getNodeByPath,
    getDirStats
};

tool/shell.js

javascript 复制代码
// shell.js
const vfs = require('./vfs');
const fs = vfs.fs;

const commands = {
    cat: require('./commands/cat'),
    cd: require('./commands/cd'),
    clear: require('./commands/clear'),
    cp: require('./commands/cp'),
    echo: require('./commands/echo'),
    exit: require('./commands/exit'),
    find: require('./commands/find'),
    grep: require('./commands/grep'),
    head: require('./commands/head'),
    help: require('./commands/help'),
    history: require('./commands/history'),
    ll: require('./commands/ll'),
    ls: require('./commands/ls'),
    mkdir: require('./commands/mkdir'),
    mv: require('./commands/mv'),
    pwd: require('./commands/pwd'),
    rm: require('./commands/rm'),
    rmdir: require('./commands/rmdir'),
    stat: require('./commands/stat'),
    tail: require('./commands/tail'),
    touch: require('./commands/touch'),
    vim: require('./commands/vim'),
    yum: require('./commands/yum'),
    zip: require('./commands/zip'),
    unzip: require('./commands/unzip'),
    rz: require('./commands/rz'),
    sz: require('./commands/sz'),
};

let currentDir = '/home/user';

function processCommand(input, rl, promptCall) {
    const args = input.trim().split(/\s+/);
    const cmd = args[0];

    if (!commands[cmd]) {
        console.log(`命令未找到: ${cmd}`);
        promptCall();
        return;
    } else if (cmd === 'history') {
        commands[cmd].execute([], currentDir, rl);
        promptCall();
        return;
    } else if (cmd === 'vim') {
        commands[cmd].execute(args, currentDir, rl);
        return;
    } else if (cmd === 'yum') {
        commands[cmd].execute(args, currentDir, rl);
        return;
    } else if (cmd === 'exit') {
        commands[cmd].execute(rl);
        return;
    }

    commands[cmd].execute(args, currentDir, (newDir) => {
        if (newDir) currentDir = newDir;
    });
    promptCall();
}

module.exports = { processCommand };

演示截图

相关推荐
誰能久伴不乏1 小时前
Linux系统调用概述与实现:深入浅出的解析
linux·运维·服务器
程序员学习随笔1 小时前
Linux进程深度解析(2):fork/exec写时拷贝性能优化与exit资源回收机制(进程创建和销毁)
linux·运维·服务器
mmoyula1 小时前
【RK3568 PWM 子系统(SG90)驱动开发详解】
android·linux·驱动开发
-SGlow-1 小时前
MySQL相关概念和易错知识点(2)(表结构的操作、数据类型、约束)
linux·运维·服务器·数据库·mysql
代码改变世界ctw2 小时前
Linux内核设计与实现 - 第14章 块I/O层
linux·运维·服务器
van叶~4 小时前
Linux网络-------1.socket编程基础---(TCP-socket)
linux·网络·tcp/ip
风吹落叶花飘荡4 小时前
Ubuntu系统 系统盘和数据盘扩容具体操作
linux·运维·ubuntu
zoulingzhi_yjs5 小时前
haproxy配置详解
linux·云原生
大神的风范5 小时前
从0开始学linux韦东山教程Linux驱动入门实验班(5)
linux