Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1.7. Эмулятор командной оболочки Linux

В этом разделе рассмотрим процесс разработки эмулятора командной оболочки Linux на языке программирования Python. В нашем эмуляторе будут поддерживаться как простые команды, так и команды для работы с виртуальной файловой системой.

1.7.1. Простые команды

Начнём с заготовки эмулятора с поддержкой команд echo, date, exit [8], работающей в режиме REPL (Read-Eval-Print Loop, цикл «чтение – вычисление – вывод»). Перед тем, как приступить к разработке эмулятора, изучим, как работают перечисленные команды в командной оболочке Linux:

~$ echo Hello, world!
Hello, world!
~$ date
Пн 13 янв 2025 02:22:54 MSK
~$ exit

Команда echo печатает в stdout введённый пользователем текст, date позволяет получить текущую дату и время, а exit завершает выполнение процесса командной оболочки.

Создадим файл emu.py и поместим в него код, выполняющий эмуляцию указанных команд:

import time

def repl():
    while True:
        match input('> ').split():
            case ('exit',):
                return
            case ('echo', *args):
                print(*args)
            case ('date',):
                print(time.asctime())

repl()

При выполнении программы emu.py запускается бесконечный цикл while, на каждой итерации которого демонстрируется приглашение к вводу >. Команда, введённая пользователем в stdin, при помощи метода split разделяется по пробельным символам на отдельные слова.

Для распознавания команд используется структурное сопоставление с образцом [26]. Первое слово, встречающееся во введённой пользователем команде, считается её именем, а все последующие слова – аргументами. Команда exit позволяет выйти из бесконечного цикла, команда echo печатает введённые пользователем аргументы, а команда date печатает текущее время.

Сеанс работы с нашим эмулятором сейчас выглядит так:

~$ python emu.py
> echo Hello, world!
Hello, world!
> date
Mon Jan 13 02:28:33 2025
> echo Bye, world!
Bye, world!
> exit

1.7.2. Виртуальная файловая система

Теперь добавим в эмулятор поддержку работы с виртуальной файловой системой. Сначала ограничимся отображением текущей директории в приглашении к вводу и поддержкой команд cd и pwd. Простой сеанс работы с файловой системой из командной оболочки Linux может иметь вид:

~$ mkdir repo
~$ cd repo
~/repo$ mkdir src
~/repo$ cd src
~/repo/src$ pwd
/home/user/repo/src
~/repo/src$ cd ..
~/repo$ cd ..
~$ cd unknown
bash: cd: unknown: No such file or directory
~$ rm -r repo

Команда mkdir создаёт пустую папку с указанным именем, а при помощи команды cd можно изменить текущую директорию на указанную пользователем. Директория, в которой находится пользователь, отображается в приглашении к вводу [8]. Например, команда cd repo меняет текущую директорию с ~ на ~/repo, и приглашение к вводу имеет вид ~/repo$ вместо ~$. Команда pwd печатает абсолютный путь к текущей директории. Команда cd .. позволяет перейти в родительскую директорию по отношению к текущей директории. При попытке перейти в несуществующую директорию unknown в консоль выводится сообщение об ошибке.

В файловой системе Linux папка содержит только имя для каждого находящегося внутри файла, а также численный указатель на расположение файла, связанный с его именем. Этот численный указатель также известен как индексный дескриптор (inode number) [3]. Структура данных inode, связанная с этим дескриптором, содержит метаданные файла – сведения о файле, за исключением содержащихся в файле данных.

Реализуем в нашем эмуляторе упрощённую модель файловой системы Linux. Создадим класс Node, содержащий сведения о типе файла, а также связанные с файлом данные. Реализуем также команды cd и pwd и подготовим образ виртуальной файловой системы по диаграмме, показанной на рис. 11.

Рисунок 11. Структура файловой системы, включающей папки repo и src

Обновлённая версия нашего эмулятора теперь имеет вид:

class Node:
    def __init__(self, file_type, data=''):
        self.file_type = file_type
        self.data = data

def cd(node, name):
    if name in node.data and node.data[name].file_type == 'dir':
        return node.data[name]
    print('No such file or directory')
    return node

def pwd(node):
    if par := node.data.get('..'):
        for name in par.data:
            if par.data[name] == node:
                return pwd(par) + name + '/'
    return '/'

def repl(node):
    while True:
        match input(f'{pwd(node)}> ').split():
            case ('exit',):
                return
            case ('cd', name):
                node = cd(node, name)
            case ('pwd',):
                print(pwd(node))
            # Реализация команд echo, date.

src = Node('dir', {})
repo = Node('dir', {'src': src})
root = Node('dir', {'repo': repo})

src.data['..'] = repo
repo.data['..'] = root
repl(root)

На вход функции repl передаётся подготовленный образ виртуальной файловой системы. Переменная node в функции repl представляет собой директорию, в которой находится пользователь. В цикл обработки ввода была добавлена поддержка команд cd и pwd, а реализация команд echo и date осталась без изменений.

Новая команда cd в нашем эмуляторе позволяет перемещаться по файловой системе. Если аргументом команды является слово .., текущая директория node меняется на родительскую директорию. Новая команда pwd позволяет получить путь к текущей директории, в качестве разделителя фрагментов пути используется символ /. Путь к текущей директории также печатается в приглашении к вводу, как в командной оболочке Linux.

Проверим работу обновлённого эмулятора:

~$ python emu.py
/> cd repo
/repo/> cd src
/repo/src/> echo It works!
It works!
/repo/src/> pwd
/repo/src/
/repo/src/> cd ..
/repo/> cd ..
/> cd unknown
No such file or directory
/> exit

Для работы с файловой системой полезно иметь возможность создавать как папки, так и файлы – для этого в командной оболочке Linux используются команды mkdir и touch. Кроме того, нужна возможность просмотра содержимого текущей директории – для этого подойдёт команда ls [8].

Добавим в emu.py поддержку команд mkdir, touch и ls :

def mkdir(node, names):
    for name in names:
        node.data[name] = Node('dir', {'..': node})

def touch(node, names):
    for name in names:
        node.data[name] = Node('file')

def ls(node):
    for name in sorted(node.data):
        if name != '..':
            print(name, end=' ')
    print()

def repl(node):
    while True:
        match input(f'{pwd(node)}> ').split():
            case ('exit',):
                return
            case ('mkdir', *names):
                mkdir(node, names)
            case ('touch', *names):
                touch(node, names)
            case ('ls',):
                ls(node)
            # Реализация команд cd, pwd, echo, date.

repl(Node('dir', {}))

Функция ls возвращает имена файлов и папок, находящихся внутри текущей директории node. Команды mkdir и touch в нашем эмуляторе создают пустые папки и файлы с указанными именами. Функция mkdir создаёт объект Node с типом файла dir и ссылкой .. на текущую директорию, а функция touch создаёт объект Node с типом файла file с пустым содержимым.

Проверим работу новых команд mkdir, touch и ls:

~$ python emu.py
/> mkdir repo
/> cd repo
/repo/> mkdir src
/repo/> cd src
/repo/src/> touch hello.js hello.py
/repo/src/> ls
hello.js hello.py
/repo/src/> cd ..
/repo/> touch readme.md
/repo/> ls
src readme.md
/repo/> exit

Несложно добавить в эмулятор поддержку команд cat для вывода содержимого указанных файлов в stdout и rm для удаления файлов. Подготовим образ виртуальной файловой системы, соответствующий диаграмме, показанной на рис. 12.

Обновим код нашего эмулятора:

def cat(node, names):
    for name in names:
        if node.data[name].file_type == 'file':
            print(node.data[name].data)

def rm(node, names):
    for name in names:
        del node.data[name]

def repl(node):
    while True:
        match input(f'{pwd(node)}> ').split():
            case ('exit',):
                return
            case ('cat', *names):
                cat(node, names)
            case ('rm', *names):
                rm(node, names)
            # Реализация команд ls, mkdir, touch, cd, pwd, echo, date.

src = Node('dir', {
    'hello.py': Node('file', 'print("Hello, world!")'),
    'hello.js': Node('file', 'console.log("Hello, world!")'),
})
repo = Node('dir', {'readme.md': Node('file', ''), 'src': src})
root = Node('dir', {'repo': repo})

src.data['..'] = repo
repo.data['..'] = root
repl(root)
Рисунок 12. Структура файловой системы, включающей папки и файлы

Проверим, работают ли новые команды:

~$ python emu.py
/> cd repo
/repo/> cd src
/repo/src/> ls
hello.js hello.py
/repo/src/> cat hello.py hello.js
print("Hello, world!")
console.log("Hello, world!")
/repo/src/> cat hello.js hello.py
console.log("Hello, world!")
print("Hello, world!")
/repo/src/> rm hello.js
/repo/src/> ls
hello.py
/repo/src/> exit

1.7.3. Упражнения

Задача 1. Добавьте в реализацию команд cd, mkdir, touch, cat, rm поддержку абсолютных путей.

Задача 2. Реализуйте функцию, которая по заданному пути в файловой системе Linux построит в памяти образ виртуальной файловой системы, совместимый с эмулятором.

Задача 3. Спроектируйте формат хранения данных виртуальной файловой системы и реализуйте инструменты для работы с этим форматом: сохранить файловую систему на диске и загрузить её в память с диска.

Задача 4. Реализуйте команду mount для подключения нескольких файловых систем.

Задача 5. Добавьте в эмулятор перенаправление ввода-вывода. Добавьте в эмулятор команду curl, совместимую с перенаправлением ввода-вывода.