GithubHelp home page GithubHelp logo

minishell's Introduction

███╗░░░███╗ ██╗ ███╗░░██╗ ██╗ ░██████╗ ██╗░░██╗ ███████╗ ██╗░░░░░ ██╗░░░░░
████╗░████║ ██║ ████╗░██║ ██║ ██╔════╝ ██║░░██║ ██╔════╝ ██║░░░░░ ██║░░░░░
██╔████╔██║ ██║ ██╔██╗██║ ██║ ╚█████╗░ ███████║ █████╗░░ ██║░░░░░ ██║░░░░░
██║╚██╔╝██║ ██║ ██║╚████║ ██║ ░╚═══██╗ ██╔══██║ ██╔══╝░░ ██║░░░░░ ██║░░░░░
██║░╚═╝░██║ ██║ ██║░╚███║ ██║ ██████╔╝ ██║░░██║ ███████╗ ███████╗ ███████╗
╚═╝░░░░░╚═╝ ╚═╝ ╚═╝░░╚══╝ ╚═╝ ╚═════╝░ ╚═╝░░╚═╝ ╚══════╝ ╚══════╝ ╚══════╝

Компоненты оболочки Linux

Оболочка-это сложная часть программного обеспечения, содержащая множество различных частей.

Основной частью любой оболочки Linux является интерпретатор командной строки, или CLI. Эта часть служит двум целям: она читает и анализирует пользовательские команды, а затем выполняет проанализированные команды. Вы можете думать о самом CLI как о двух частях: синтаксическом анализаторе (или front-end) и исполнителе (или back-end).

Анализатор сканирует входные данные и разбивает их на токены. Токен состоит из одного или нескольких символов (букв, цифр, символов) и представляет собой единую единицу ввода. Например, токен может быть именем переменной, ключевым словом, числом или арифметическим оператором.

Синтаксический анализатор берет эти токены, группирует их вместе и создает специальную структуру, которую мы называем абстрактным синтаксическим деревом, или AST.

Вы можете думать о AST как о высокоуровневом представлении командной строки, которую вы дали оболочке. Синтаксический анализатор принимает AST и передает его исполнителю, который считывает AST и выполняет проанализированную команду.

Другой частью оболочки является пользовательский интерфейс, который обычно работает, когда оболочка находится в интерактивном режиме, например, когда вы вводите команды в командной строке оболочки. Здесь оболочка работает в цикле, который мы знаем как цикл Read-Eval-Print, или REPL. Как следует из названия цикла, оболочка считывает входные данные, анализирует и выполняет их, затем выполняет цикл для чтения следующей команды и так далее, пока вы не введете команду, такую как выход, завершение работы или перезагрузка.

Большинство оболочек реализуют структуру, известную как таблица символов, которая используется оболочкой для хранения информации о переменных, а также их значений и атрибутов. Мы реализуем таблицу символов в Части II этого руководства.

Оболочки Linux также имеют средство истории, которое позволяет пользователю получить доступ к самым последним введенным командам, а затем редактировать и повторно выполнять команды без особого набора текста. Оболочка также может содержать встроенные утилиты, которые представляют собой специальный набор команд, реализуемых как часть самой программы оболочки. Встроенные утилиты включают обычно используемые команды, такие как cd, fg и bg. Мы будем реализовывать многие из встроенных утилит по мере продвижения этого урока.

Теперь, когда мы знаем основные компоненты типичной оболочки Linux, давайте начнем строить нашу собственную оболочку.

Наш Первый Shell

Наша первая версия оболочки не будет делать ничего необычного; она просто напечатает строку запроса, прочитает строку ввода, а затем эхом вернет ввод обратно на экран. В последующих частях этого урока мы добавим возможность анализировать и выполнять команды, циклы, условные выражения и многое другое.

Первое, что мы сделаем, это напишем наш базовый цикл REPL. Создайте файл с именем main.c (используя touch main.c), затем откройте его с помощью вашего любимого текстового редактора. Введите следующий код в свой main.c:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include "shell.h"

int		main(int argc, char **argv)
{
	char *cmd;

	while (1)
	{
		print_prompt1();
		cmd = read_cmd(cmd);
		if (!cmd)
		{
			exit(EXIT_SUCCESS);
		}
		if (cmd[0] == '\0' || strcmp(cmd, "\n") == 0)
		{
			free(cmd);
			continue;
		}
		if (strcmp(cmd, "exit") == 0)
		{
			free(cmd);
			break ;
		}
		write(1, cmd, ft_strlen(cmd));
		write(1, "\n", 1);
		free(cmd);
	}
	exit(EXIT_SUCCESS);
}

Наша функция main() довольно проста, так как ей нужно только реализовать цикл REPL. Сначала мы печатаем приглашение оболочки, а затем читаем команду (пока давайте определим команду как строку ввода, заканчивающуюся на \n). Если есть ошибка чтения команды, мы выходим из оболочки. Если команда пуста (то есть пользователь нажал ENTER, ничего не записывая, мы пропускаем этот ввод и продолжаем цикл. Если команда exit, мы выходим из оболочки. В противном случае мы эхом возвращаем команду, освобождаем память, которую мы использовали для хранения команды, и продолжаем цикл.

Довольно просто, не так ли?

Наша функция main() вызывает две пользовательские функции print_prompt1() и read_cmd(). Первая функция выводит строку приглашения, а вторая считывает следующую строку ввода. Давайте более подробно рассмотрим эти две функции.

Строки Запроса На Печать

Мы сказали, что оболочка выводит строку запроса перед чтением каждой команды. На самом деле существует пять различных типов строки приглашения: PS0 , PS1, PS2, PS3 и PS4. Нулевая строка, PS0, используется только bash, поэтому мы не будем рассматривать ее здесь. Остальные четыре строки печатаются в определенное время, когда оболочка хочет передать определенные сообщения пользователю. В этом разделе мы поговорим о PS1 и PS2. Остальное придет позже, когда мы обсудим более продвинутые темы оболочки.

Теперь создайте исходный файл prompt.c и введите следующий код:

#include <stdio.h>
#include "shell.h"

void print_prompt1(void)
{
   fprintf(stderr, "$ ");
}

void print_prompt2(void)
{
   fprintf(stderr, "> ");
} 

Первая функция выводит первую строку приглашения, или PS1, которую вы обычно видите, когда оболочка ожидает ввода команды. Вторая функция печатает вторую строку приглашения, или PS2, которая печатается оболочкой при вводе многострочной команды (Подробнее об этом ниже).

Далее, Давайте прочитаем некоторые пользовательские данные.

Чтение Пользовательского Ввода

Добавим наш get_next_line в файл get_next_line.c

#include "shell.h"

void	ft_putchar_fd(char c, int fd)
{
	write(fd, &c, 1);
}

char	*ft_strdup(const char *s)
{
	char	*str;
	char	*str2;
	int		i;
	int		j;

	i = 0;
	j = 0;
	str2 = (char *)s;
	while (str2[i] != '\0')
		i++;
	str = (char*)malloc(sizeof(*str2) * (i + 1));
	if (!str)
		return (NULL);
	while (j < i)
	{
		str[j] = str2[j];
		j++;
	}
	str[j] = '\0';
	return (str);
}

size_t	ft_strlen(const char *s)
{
	size_t len;

	len = 0;
	while (s[len])
	{
		len++;
	}
	return (len);
}

char	*ft_strjoin(char const *s1, char const *s2)
{
	size_t	y;
	size_t	i;
	char	*str;

	i = 0;
	y = 0;
	if (!s1 || !s2)
		return (NULL);
	if (!(str = malloc(sizeof(char) * (ft_strlen(s1) + ft_strlen(s2) + 1))))
		return (NULL);
	while (s1[i])
	{
		str[i] = s1[i];
		i++;
	}
	while (s2[y])
		str[i++] = s2[y++];
	str[i] = '\0';
	return (str);
}

int		get_next_line(int fd, char **line)
{
	char	buf[2];
	int		sr;
	char	*tmp;

	sr = 0;
	buf[1] = '\0';
	if (!line)
		return (-1);
	if (!(*line = malloc(1)))
		return (-1);
	**line = 0;
	while ((sr = read(fd, buf, 1)) > 0)
	{
		if (*buf != '\n' && *buf != EOF && *buf != '\0')
		{
			tmp = *line;
			*line = ft_strjoin(*line, buf);
			free(tmp);
		}
		else
			break ;
	}
	return (sr);
}

Откройте файл main.c и введите следующий код в конце, сразу после функции main() :

char	*read_cmd(char *buf)
{
	int		buflen;
	char	*tmp;

	get_next_line(0, &buf);
	buflen = ft_strlen(buf);
	while (buflen > 1 && buf[buflen - 1] == '\\')
	{
		buf[buflen - 1] = '\n';
		buf[buflen] = '\0';
		print_prompt2();
		tmp = buf;
		get_next_line(0, &buf);
		buf = ft_strjoin(tmp, buf);
		buflen = ft_strlen(buf);
	}
	return (buf);
}

Здесь мы считываем входные данные из 0 гнлом и храним их в буфере. Здесь мы не должны сталкиваться с какими-либо проблемами памяти. Если все идет хорошо, мы копируем фрагмент ввода, который только что прочитали от пользователя, в наш буфер и соответствующим образом корректируем наши указатели.

Последний блок кода интересен. Чтобы понять, зачем нам нужен этот блок кода, рассмотрим следующий пример. Допустим, вы хотите ввести очень-очень длинную строку ввода:

echo "This is a very long line of input, one that needs to span two, three, or perhaps even more lines of input, so that we can feed it to the shell"

Это глупый пример, но он прекрасно демонстрирует то, о чем мы говорим. Чтобы ввести такую длинную команду, мы можем записать все это в одной строке (как мы сделали здесь), что является громоздким и уродливым процессом. Или мы можем нарезать леску на более мелкие кусочки и скормить эти кусочки Shell, по одному кусочку за раз:

echo "This is a very long line of input, \
	one that needs to span two, three, \
	or perhaps even more lines of input, \
	so that we can feed it to the shell"

После ввода первой строки и чтобы оболочка знала, что мы не закончили ввод, мы заканчиваем каждую строку символом обратной косой черты \, за которым следует новая строка (я также сделал отступы в строках, чтобы сделать их более читабельными). Мы называем это экранированием символа новой строки. Когда оболочка видит экранированную новую строку, она знает, что ей нужно отбросить два символа и продолжить чтение ввода.

Теперь давайте вернемся к нашей функции read_cmd (). Мы обсуждали последний блок кода, который гласит:

while (buflen > 1 && buf[buflen - 1] == '\\')
	{
		buf[buflen - 1] = '\n';
		buf[buflen] = '\0';
		print_prompt2();
		tmp = buf;
		get_next_line(0, &buf);
		buf = ft_strjoin(tmp, buf);
		buflen = ft_strlen(buf);
	}

Здесь мы проверяем, заканчивается ли входной сигнал, который мы получили в буфере символом обратной косой черты \. Если последний \n не экранирован, то входная строка завершена, и мы возвращаем ее в функцию main (). В противном случае мы удаляем символ (\), распечатываем PS2 и продолжаем чтение ввода.

Компиляция Shell

С приведенным выше кодом наша нишевая оболочка почти готова к компиляции. Мы просто добавим заголовочный файл с нашими прототипами функций, прежде чем приступим к компиляции оболочки. Этот шаг необязателен, но он значительно улучшает читаемость нашего кода и предотвращает появление нескольких предупреждений компилятора. Создайте исходный файл shell.h и введите следующий код:

#ifndef SHELL_H
#define SHELL_H

void	print_prompt1(void);
void	print_prompt2(void);
char	*read_cmd(char *buf);
int		get_next_line(int fd, char **line);
char	*ft_strjoin(char const *s1, char const *s2);
size_t	ft_strlen(const char *s);
char	*ft_strdup(const char *s);
void	ft_putchar_fd(char c, int fd);

#endif 

Что такое простая команда?

Простая команда состоит из списка слов, разделенных пробелами (пробел, табуляция, новая строка). Первое слово-это имя команды, и оно обязательно (в противном случае оболочка не будет иметь команды для разбора и выполнения!). Второе и последующие слова являются необязательными. Если они присутствуют, они формируют аргументы, которые мы хотим, чтобы оболочка передала выполняемой команде.

Например, следующая команда: ls-l состоит из двух слов: ls (имя команды) и-l (первый и единственный аргумент). Аналогично команда: gcc -o shell main.c prompt.c (которую мы использовали в части I для компиляции нашей оболочки) состоит из 5 слов: имени команды и списка из 4 аргументов.

Теперь, чтобы иметь возможность выполнять простые команды, наша оболочка должна выполнить следующие шаги:

Сканируйте ввод, по одному символу за раз, чтобы найти следующий маркер. Мы называем этот процесс лексическим сканированием, и часть оболочки, которая выполняет эту задачу, известна как лексический сканер, или просто сканер.

Извлеките входные токены. Мы называем эти входные разбора.

Проанализируйте маркеры и создайте абстрактное синтаксическое дерево, или AST. Часть оболочки, ответственная за это, называется синтаксическим анализатором.

Выполните АСТ. Это работа исполнителя.

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

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

Сканирование входных данных

Чтобы получить следующий маркер, мы должны иметь возможность сканировать наши входные данные, по одному символу за раз, чтобы мы могли идентифицировать символы, которые могут быть частью маркера, и те, которые являются символами-разделителями. Символ-разделитель-это символ, который обозначает конец маркера (и, возможно, начало другого маркера). Как правило, разделителями являются символы пробела (пробел, табуляция, новая строка), но также могут быть и другие символы, такие как ; и &.

В общем, сканирование входных данных означает, что мы должны быть в состоянии:

Извлечь следующий символ из входных данных.

Вернуть последний символ, который мы прочитали, обратно на вход.

Lookahead (или peek), чтобы проверить следующий символ, фактически не извлекая его.

Пропустить пробелы.

Мы определим функции для выполнения всех этих задач в течение минуты. Но сначала давайте поговорим об абстрагировании входных данных. Помните функцию read_cmd (), которую мы определили в части I этого урока? Это была функция, которую мы использовали, чтобы прочитать ввод пользователя и вернуть его в виде строки malloc'D. Мы могли бы передать эту строку непосредственно на наш сканер, но это сделало бы процесс сканирования немного громоздким. В частности, сканеру было бы очень трудно запомнить последний символ, который он дал нам, чтобы он мог пройти мимо этого символа и дать нам следующий символ.

Чтобы облегчить работу сканера, мы абстрагируем наши входные данные, передавая входную строку как часть структуры struct source_s, которую мы определим в исходном файле source.h. Создайте этот файл в исходном каталоге, затем откройте его в своем любимом текстовом редакторе и добавьте следующий код:

Проанализируйте маркеры и создайте абстрактное синтаксическое дерево, или AST. Часть оболочки, ответственная за это, называется синтаксическим анализатором.

#ifndef SOURCE_H
# define SOURCE_H

# define EOF			(-1)
# define ERRCHAR		( 0)
# define INIT_SRC_POS	(-2)

typedef struct	s_source
{
	char		*buffer;
	long		bufsize;
	long		curpos;
}				t_source;

char			next_char(struct source_s *src);
void			unget_char(struct source_s *src);
char			peek_char(struct source_s *src);
void			skip_white_spaces(struct source_s *src);

#endif

Сосредоточившись на определении структуры, вы можете увидеть, что наша структура source_s содержит указатель на входную строку, в дополнение к двум длинным полям, которые содержат информацию о длине строки и нашей текущей позиции в строке (откуда мы получим следующий символ).

Теперь создайте еще один файл с именем source.c, в который вы должны добавить следующий код:

#include <errno.h>
#include "shell.h"
#include "source.h"

void	unget_char(t_source *src)
{
	if (src->curpos < 0)
		return ;
	src->curpos--;
}

char	next_char(t_source *src)
{
	char	c1;

	if (!src || !src->buffer)
	{
		errno = ENODATA;
		return (ERRCHAR);
	}
	c1 = 0;
	if (src->curpos == INIT_SRC_POS)
		src->curpos = -1;
	else
		c1 = src->buffer[src->curpos];
	if (++src->curpos >= src->bufsize)
	{
		src->curpos = src->bufsize;
		return (EOF);
	}
	return (src->buffer[src->curpos]);
}

char	peek_char(t_source *src)
{
	long	pos;

	if (!src || !src->buffer)
	{
		errno = ENODATA;
		return (ERRCHAR);
	}
	pos = src->curpos;
	if (pos == INIT_SRC_POS)
		pos++;
	pos++;
	if (pos >= src->bufsize)
		return (EOF);
	return (src->buffer[pos]);
}

void	skip_white_spaces(t_source *src)
{
	char	c;

	if (!src || !src->buffer)
		return ;
	while (((c = peek_char(src)) != EOF) && (c == ' ' || c == '\t'))
		next_char(src);
}

Функция unget_char() возвращает (или разгружает) последний символ, который мы извлекли из входных данных, обратно к источнику входных данных. Он делает это, просто манипулируя указателями исходной структуры. Вы увидите преимущества этой функции позже в этой серии.

Функция next_char() возвращает следующий символ ввода и обновляет указатель источника, так что следующий вызов функции next_char() возвращает следующий входной символ. Когда мы достигаем последнего символа во входных данных, функция возвращает специальный символ EOF, который мы определили как -1 в source.h выше.

Функция peek_char() аналогична функции next_char() в том, что она возвращает следующий символ ввода. Единственное отличие состоит в том, что функция peek_char() не обновляет исходный указатель, так что следующий вызов функции next_char() возвращает тот же входной символ, который мы только что просмотрели. Вы увидите преимущество подглядывания ввода позже в этой серии.

Наконец, функция skip_white_spaces() пропускает все символы пробела. Это поможет нам, когда мы закончим чтение маркера, и мы хотим пропустить пробелы разделителей, прежде чем читать следующий маркер.

Токенизация Входных Данных

Теперь, когда у нас есть функции нашего сканера, мы будем использовать эти функции для извлечения входных маркеров. Мы начнем с определения новой структуры, которую будем использовать для представления наших токенов. Создайте файл scanner.h в исходном каталоге, откройте его и добавьте следующий код:

#ifndef SCANNER_H
#define SCANNER_Hl

struct token_s
{
	struct source_s *src;       /* source of input */
	int    text_len;            /* length of token text */
	char   *text;               /* token text */
};

/* the special EOF token, which indicates the end of input */
extern struct token_s eof_token;
struct token_s *tokenize(struct source_s *src);
void free_token(struct token_s *tok);

#endif 

Сосредоточившись на определении структуры, наша структура token_s содержит указатель на структуру source_s, которая содержит ваши входные данные. Структура также содержит указатель на текст токена и поле, которое сообщает нам длину этого текста (так что нам не нужно повторно вызывать strlen() для текста токена).

Далее мы напишем функцию tokenize (), которая будет извлекать следующий токен из входных данных. Мы также напишем некоторые вспомогательные функции, которые помогут нам работать с входными токенами.

В исходном каталоге, создайте файл под названием scaner.c и введите следующий код:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include "shell.h"
#include "scanner.h"
#include "source.h"

char *tok_buf = NULL;
int   tok_bufsize  = 0;
int   tok_bufindex = -1;

/* special token to indicate end of input */
struct token_s eof_token = 
{
	.text_len = 0,
};

void add_to_buf(char c)
{
	tok_buf[tok_bufindex++] = c;
	if(tok_bufindex >= tok_bufsize)
	{
		char *tmp = realloc(tok_buf, tok_bufsize*2);
		if(!tmp)
		{
			errno = ENOMEM;
			return;
		}
		tok_buf = tmp;
		tok_bufsize *= 2;
	}
}

struct token_s *create_token(char *str)
{
	struct token_s *tok = malloc(sizeof(struct token_s));

	if(!tok)
	{
		return NULL;
	}
	memset(tok, 0, sizeof(struct token_s));
	tok->text_len = strlen(str);

	char *nstr = malloc(tok->text_len+1);

	if(!nstr)
	{
		free(tok);
		return NULL;
	}

	strcpy(nstr, str);
	tok->text = nstr;

	return tok;
}

void free_token(struct token_s *tok)
{
	if(tok->text)
	{
		free(tok->text);
	}
	free(tok);
}

struct token_s *tokenize(struct source_s *src)
{
	int  endloop = 0;
	if(!src || !src->buffer || !src->bufsize)
	{
		errno = ENODATA;
		return &eof_token;
	}

	if(!tok_buf)
	{
		tok_bufsize = 1024;
		tok_buf = malloc(tok_bufsize);
		if(!tok_buf)
		{
			errno = ENOMEM;
			return &eof_token;
		}
	}
	tok_bufindex     = 0;
	tok_buf[0]       = '\0';
	char nc = next_char(src);
	if(nc == ERRCHAR || nc == EOF)
	{
		return &eof_token;
	}
	do
	{
		switch(nc)
		{
			case ' ':
			
			case '\t':
				if(tok_bufindex > 0)
				{
					endloop = 1;
				}
				break;
	    
			case '\n':
				if(tok_bufindex > 0)
				{
					unget_char(src);
				}
				else
				{
					add_to_buf(nc);
				}
				endloop = 1;
				break;
	    
			default:
				add_to_buf(nc);
				break;
		}
		if(endloop)
		{
			break;
		}
	} while((nc = next_char(src)) != EOF);
	if(tok_bufindex == 0)
	{
		return &eof_token;
	}

	if(tok_bufindex >= tok_bufsize)
	{
		tok_bufindex--;
	}
	tok_buf[tok_bufindex] = '\0';
	struct token_s *tok = create_token(tok_buf);
	if(!tok)
	{
		fprintf(stderr, "error: failed to alloc buffer: %s\n",
			strerror(errno));
		return &eof_token;
	} 
	tok->src = src;
	return tok;
}

Глобальные переменные, которые мы определили в этом файле, служат следующим целям:

tok_buf - это указатель на буфер, в котором мы будем хранить текущий токен.

tok_bufsize - это количество байтов, которые мы выделяем в буфер.

tok_bufindex - это текущий индекс буфера (т. е. он говорит нам, где добавить следующий входной символ в буфер).

eof_token - это специальный токен, который мы будем использовать для сигнализации конца файла/ввода (EOF).

Теперь давайте посмотрим на функции, которые мы определили в этом файле. Функция add_to_buf() добавляет один символ в буфер токенов. Если буфер заполнен, функция позаботится о его расширении.

Функция create_token() принимает строку и преобразует ее в структуру struct token_s. Он заботится о выделении памяти для структуры и текста токена и заполняет поля-члены структуры.

Функция free_token() освобождает память, используемую структурой токена, а также память, используемую для хранения текста токена.

Функция tokenize() - это сердце и душа нашего лексического сканера, и код этой функции довольно прямолинеен. Он начинается с выделения памяти для нашего буфера токенов (если это еще не сделано), а затем инициализирует наш буфер токенов и исходные указатели. Затем он вызывает функцию next_char() для извлечения следующего входного символа. Когда мы достигаем конца ввода, tokenize() возвращает специальный eof_token, который отмечает конец ввода.

Затем функция выполняет циклическое считывание входных символов по одному за раз. Если он встречает символ пробела, он проверяет буфер токенов, чтобы увидеть, пуст он или нет. Если буфер не пуст, мы разделяем текущий токен и выходим из цикла. В противном случае мы пропускаем пробелы и переходим к началу следующего маркера.

После того как мы получили наш токен, tokenize() вызывает create_token(), передавая ему текст токена (который мы сохранили в буфере). Текст токена преобразуется Мы, конечно, добились большого прогресса в этой части, но наша оболочка все еще не готова анализировать и выполнять команды. Поэтому сейчас мы не будем компилировать оболочку. Наша следующая компиляция будет в конце части III, после того, как мы реализуем наш синтаксический анализатор и наш исполнитель.

Разбор Простых Команд

В предыдущей части этого урока мы реализовали наш лексический сканер. Теперь давайте обратимся к синтаксическому анализатору.

Напомним, что синтаксический анализатор-это часть интерпретатора командной строки, которая вызывает лексический сканер для извлечения маркеров, а затем строит абстрактное синтаксическое дерево, или AST, из этих маркеров. АСТ-это то, что мы передадим исполнителю, чтобы он был, ну, казнен.

Наш парсер будет содержать только одну функцию-parse_simple_command(). В следующих частях этого урока мы добавим дополнительные функции, позволяющие нашей оболочке анализировать циклы и условные выражения.

Итак, давайте начнем кодировать наш парсер. Вы можете начать с создания файла parser.h в исходном каталоге, в который вы добавите следующий код:

#ifndef PARSER_H
#define PARSER_H

#include "scanner.h"    /* struct token_s */
#include "source.h"     /* struct source_s */

struct node_s *parse_simple_command(struct token_s *tok);

#endif

Ничего особенного, просто объявляем нашу единственную функцию парсера.

Затем создайте parser.c и добавьте к нему следующий код:

#include <unistd.h>
#include "shell.h"
#include "parser.h"
#include "scanner.h"
#include "node.h"
#include "source.h"

struct node_s *parse_simple_command(struct token_s *tok)
{
	if(!tok)
	{
		return NULL;
	}
	
	struct node_s *cmd = new_node(NODE_COMMAND);
	if(!cmd)
	{
		free_token(tok);
		return NULL;
	}
	
	struct source_s *src = tok->src;
	
	do
	{
		if(tok->text[0] == '\n')
		{
			free_token(tok);
			break;
		}
		struct node_s *word = new_node(NODE_VAR);
		if(!word)
		{
			free_node_tree(cmd);
			free_token(tok);
			return NULL;
		}
		set_node_val_str(word, tok->text);
		add_child_node(cmd, word);
		free_token(tok);
	} while((tok = tokenize(src)) != &eof_token);
	return cmd;
}

Довольно просто, правда? Чтобы разобрать простую команду, нам нужно только вызвать tokenize() для извлечения входных токенов, один за другим, пока мы не получим токен новой строки (который мы тестируем в строке, которая гласит: if(tok->text[0] == '\n')), или мы достигнем конца нашего ввода (мы знаем, что это произошло, когда мы получили токен eof_token. См. условное выражение цикла в нижней части предыдущего списка). Мы используем входные маркеры для создания AST, который представляет собой древовидную структуру, содержащую информацию о компонентах команды. Деталей должно быть достаточно, чтобы исполнитель мог правильно выполнить команду.

Каждый узел в AST команды должен содержать информацию о входном токене, который он представляет (например, текст исходного токена). Узел также должен содержать указатели на его дочерние узлы (если узел является корневым узлом), а также на его родственные узлы (если узел является дочерним узлом). Поэтому нам нужно будет определить еще одну структуру, struct node_s, которую мы будем использовать для представления узлов в нашем AST.

Идти вперед и создать новый файл, node.h и добавьте в него следующий код:

#ifndef NODE_H
#define NODE_H

enum node_type_e
{
	NODE_COMMAND,           /* simple command */
	NODE_VAR,               /* variable name (or simply, a word) */
};

enum val_type_e
{
	VAL_SINT = 1,       /* signed int */
	VAL_UINT,           /* unsigned int */
	VAL_SLLONG,         /* signed long long */
	VAL_ULLONG,         /* unsigned long long */
	VAL_FLOAT,          /* floating point */
	VAL_LDOUBLE,        /* long double */
	VAL_CHR,            /* char */
	VAL_STR,            /* str (char pointer) */
};

union symval_u
{
	long               sint;
	unsigned long      uint;
	long long          sllong;
	unsigned long long ullong;
	double             sfloat;
	long double        ldouble;
	char               chr;
	char              *str;
};

struct node_s
{
	enum   node_type_e type;    /* type of this node */
	enum   val_type_e val_type; /* type of this node's val field */
	union  symval_u val;        /* value of this node */
	int    children;            /* number of child nodes */
	struct node_s *first_child; /* first child node */
	struct node_s *next_sibling, *prev_sibling;
						/*
						* if this is a child node, keep
						* pointers to prev/next siblings
						*/
};

struct  node_s *new_node(enum node_type_e type);
void    add_child_node(struct node_s *parent, struct node_s *child);
void    free_node_tree(struct node_s *node);
void    set_node_val_str(struct node_s *node, char *val);

#endif

Перечисление node_type_e определяет типы наших узлов AST. На данный момент нам нужны только два типа. Первый тип представляет корневой узел AST простой команды, в то время как второй тип представляет дочерние узлы простой команды (которые содержат имя команды и аргументы). В следующих частях этого руководства мы добавим в это перечисление дополнительные типы узлов.

Перечисление val_type_e представляет типы значений, которые мы можем хранить в данной структуре узлов. Для простых команд мы будем использовать только строки (тип перечисления VAL_STR). Позже в этой серии мы будем использовать другие типы при обработке других типов сложных команд.

Объединение symval_u представляет значение, которое мы можем хранить в данной узловой структуре. Каждый узел может иметь только один тип значения, например символьную строку или числовое значение. Мы получаем доступ к значению узла, ссылаясь на соответствующий член объединения (sint для длинных целых чисел со знаком, str для строк и т. д.).

Структура struct node_s представляет собой узел AST. Он содержит поля, которые сообщают нам о типе узла, типе значения узла, а также само значение. Если это корневой узел, то поле children содержит количество дочерних узлов, а first_child указывает на первый дочерний узел (в противном случае оно будет равно NULL). Если это дочерний узел, мы можем обойти узлы AST, следуя указателям next_sibling и prev_sibling.

Если мы хотим получить значение узла, нам нужно проверить поле val_type и, в соответствии с тем, что мы там находим, получить доступ к соответствующему члену поля val. Для простых команд все узлы будут иметь следующие атрибуты:

type => NODE_COMMAND (корневой узел) или NODE_VAR (имя команды и список аргументов)

val_type => VAL_STR

val.str => указатель на строковое значение

Теперь давайте напишем некоторые функции, которые помогут нам работать со структурами узлов.

Создайте файл с именем node.c и добавьте следующий код:

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include "shell.h"
#include "node.h"
#include "parser.h"

struct node_s *new_node(enum node_type_e type)
{
	struct node_s *node = malloc(sizeof(struct node_s));

	if(!node)
	{
		return NULL;
	}
	
	memset(node, 0, sizeof(struct node_s));
	node->type = type;
	
	return node;
}

void add_child_node(struct node_s *parent, struct node_s *child)
{
	if(!parent || !child)
	{
		return;
	}
	if(!parent->first_child)
	{
		parent->first_child = child;
	}
	else
	{
		struct node_s *sibling = parent->first_child;
	
		while(sibling->next_sibling)
		{
			sibling = sibling->next_sibling;
		}
	
		sibling->next_sibling = child;
		child->prev_sibling = sibling;
	}
	parent->children++;
}

void set_node_val_str(struct node_s *node, char *val)
{
	node->val_type = VAL_STR;
	if(!val)
	{
		node->val.str = NULL;
	}
	else
	{
		char *val2 = malloc(strlen(val)+1);
	
		if(!val2)
		{
			node->val.str = NULL;
		}
		else
		{
			strcpy(val2, val);
			node->val.str = val2;
		}
	}
}

void free_node_tree(struct node_s *node)
{
	if(!node)
	{
		return;
	}
	struct node_s *child = node->first_child;
	
	while(child)
	{
		struct node_s *next = child->next_sibling;
		free_node_tree(child);
		child = next;
	}
	
	if(node->val_type == VAL_STR)
	{
		if(node->val.str)
		{
			free(node->val.str);
		}
	}
	free(node);
} 

Функция new_node() создает новый узел и задает его поле типа.

Функция add_child_node() расширяет AST простой команды, добавляя новый дочерний узел и увеличивая поле дочерних узлов корневого узла. Если корневой узел не имеет дочерних элементов, то новый дочерний элемент назначается полю first_child корневого узла. В противном случае ребенок добавляется в конец списка детей.

Функция set_node_val_str() устанавливает значение узла в заданную строку. Он копирует строку во вновь выделенное пространство памяти, а затем устанавливает поля val_type и val.str соответственно. В будущем мы определим аналогичные функции, позволяющие устанавливать значения узлов для различных типов данных, таких как целые числа и плавающие точки.

Функция free_node_tree() освобождает память, используемую узловой структурой. Если у узла есть дочерние элементы, функция вызывается рекурсивно, чтобы освободить каждый из них.

Это все для парсера. Теперь давайте напишем нашу команду executor.

Выполнение Простых Команд

Подобно нашему синтаксическому анализатору, исполнитель будет содержать только одну функцию do_simple_command(). В следующих частях этого урока мы добавим дополнительные функции, которые позволят нам выполнять все виды команд, такие как циклы и условные выражения.

Создайте файл с именем executor.h и добавьте следующий код:

#ifndef BACKEND_H
#define BACKEND_H

#include "node.h"

char *search_path(char *file);
int do_exec_cmd(int argc, char **argv);
int do_simple_command(struct node_s *node);

#endif

Просто некоторые прототипы функций. Теперь создайте executor.c и определите следующие функции:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "shell.h"
#include "node.h"
#include "executor.h"

char *search_path(char *file)
{
	char *PATH = getenv("PATH");
	char *p    = PATH;
	char *p2;
	
	while(p && *p)
	{
		p2 = p;
		while(*p2 && *p2 != ':')
		{
			p2++;
		}
		
		int  plen = p2-p;
		if(!plen)
		{
			plen = 1;
		}
		
		int  alen = strlen(file);
		char path[plen+1+alen+1];
		
		strncpy(path, p, p2-p);
		path[p2-p] = '\0';
		
		if(p2[-1] != '/')
		{
			strcat(path, "/");
		}
		strcat(path, file);
		
		struct stat st;
		if(stat(path, &st) == 0)
		{
			if(!S_ISREG(st.st_mode))
			{
			errno = ENOENT;
			p = p2;
			if(*p2 == ':')
			{
				p++;
			}
			continue;
			}
			p = malloc(strlen(path)+1);
			if(!p)
			{
			return NULL;
			}
			
		strcpy(p, path);
			return p;
		}
		else    /* file not found */
		{
			p = p2;
			if(*p2 == ':')
			{
			p++;
			}
		}
	}
	errno = ENOENT;
	return NULL;
}
int do_exec_cmd(int argc, char **argv)
{
	if(strchr(argv[0], '/'))
	{
		execv(argv[0], argv);
	}
	else
	{
		char *path = search_path(argv[0]);
		if(!path)
		{
			return 0;
		}
		execv(path, argv);
		free(path);
	}
	return 0;
}

static inline void free_argv(int argc, char **argv)
{
	if(!argc)
	{
		return;
	}
	while(argc--)
	{
		free(argv[argc]);
	}
}

int do_simple_command(struct node_s *node)
{
	if(!node)
	{
		return 0;
	}
	struct node_s *child = node->first_child;
	if(!child)
	{
		return 0;
	}
	
	int argc = 0;
	long max_args = 255;
	char *argv[max_args+1];/* keep 1 for the terminating NULL arg */
	char *str;
	
	while(child)
	{
		str = child->val.str;
		argv[argc] = malloc(strlen(str)+1);
		
		if(!argv[argc])
		{
			free_argv(argc, argv);
			return 0;
		}
		
		strcpy(argv[argc], str);
		if(++argc >= max_args)
		{
			break;
		}
		child = child->next_sibling;
	}
	argv[argc] = NULL;
	pid_t child_pid = 0;
	if((child_pid = fork()) == 0)
	{
		do_exec_cmd(argc, argv);
		fprintf(stderr, "error: failed to execute command: %s\n", 
			strerror(errno));
		if(errno == ENOEXEC)
		{
			exit(126);
		}
		else if(errno == ENOENT)
		{
			exit(127);
		}
		else
		{
			exit(EXIT_FAILURE);
		}
	}
	else if(child_pid < 0)
	{
		fprintf(stderr, "error: failed to fork command: %s\n", 
			strerror(errno));
		return 0;
	}
	int status = 0;
	waitpid(child_pid, &status, 0);
	free_argv(argc, argv);
	
	return 1;
}

Функция search_path() принимает имя команды, а затем ищет каталоги, перечисленные в переменной $PATH, чтобы попытаться найти исполняемый файл команды. Переменная $PATH содержит разделенный запятыми список каталогов, таких как /bin:/usr/bin. Для каждого каталога мы создаем путь, добавляя имя команды к имени каталога, а затем вызываем stat (), чтобы проверить, существует ли файл с заданным путем (для простоты мы не проверяем, действительно ли файл является исполняемым или у нас достаточно разрешений для его выполнения). Если файл существует, мы предполагаем, что он содержит команду, которую мы хотим выполнить, и возвращаем полный путь к этой команде. Если мы не находим файл в первом каталоге $PATH, мы ищем второй, третий и остальные каталоги $PATH, пока не найдем наш исполняемый файл. Если нам не удается найти команду путем поиска всех каталогов в $PATH, мы возвращаем NULL (обычно это означает, что пользователь ввел недопустимое имя команды).

Функция do_exec_cmd() выполняет команду, вызывая execv (), чтобы заменить текущий образ процесса новым исполняемым файлом команды. Если имя команды содержит какие-либо символы косой черты, мы рассматриваем его как путь и непосредственно вызываем execv(). В противном случае мы попытаемся найти команду, вызвав функцию search_path(), которая должна вернуть полный путь, который мы передадим execv().

Функция free_argv() освобождает память, которую мы использовали для хранения списка аргументов последней выполненной команды.

Функция do_simple_command() является основной функцией в нашем исполнителе. Он принимает AST команды и преобразует его в список аргументов (помните, что нулевой аргумент, или argv[0], содержит имя команды, которую мы хотим выполнить).

Затем функция разветвляет новый дочерний процесс. В дочернем процессе мы выполняем команду, вызывая do_exec_cmd(). Если команда выполнена успешно, этот вызов не должен возвращаться. Если он возвращается, это означает, что произошла ошибка (например, команда не была найдена, файл не был исполняемым, недостаточно памяти,...). В этом случае мы печатаем соответствующее сообщение об ошибке и выходим с ненулевым статусом выхода.

В Родительском процессе мы вызываем waitpid(), чтобы дождаться завершения выполнения нашего дочернего процесса. Затем мы освобождаем память, которую использовали для хранения списка аргументов, и возвращаемся.

Теперь, чтобы включить новый код в нашу существующую оболочку, вам нужно сначала обновить функцию main (), удалив строку, которая читает printf("%s\n", cmd); и заменить ее следующими строками:

struct source_s src;
src.buffer   = cmd;
src.bufsize  = strlen(cmd);
src.curpos   = INIT_SRC_POS;
parse_and_execute(&src);

Теперь, прежде чем закрыть файл main.c, перейдите в начало файла и добавьте следующие строки после последней директивы #include:

#include "source.h"
#include "parser.h"
#include "backend.h"

Затем перейдите в конец файла (после определения функции read_cmd ()) и добавьте следующую функцию:

int parse_and_execute(struct source_s *src)
{
	skip_white_spaces(src);
	struct token_s *tok = tokenize(src);
	if(tok == &eof_token)
	{
		return 0;
	}
	while(tok && tok != &eof_token)
	{
		struct node_s *cmd = parse_simple_command(tok);
		if(!cmd)
		{
			break;
		}
		do_simple_command(cmd);
		free_node_tree(cmd);
		tok = tokenize(src);
	}
	return 1;
}

Эта функция уводит часть Eval-Print нашего цикла Read-Eval-Print-Loop (REPL) от функции main (). Он начинает с пропуска любых ведущих символов пробела, затем анализирует и выполняет простые команды, по одной команде за раз, пока входные данные не будут израсходованы, прежде чем он вернет управление циклу REPL в функции main ().

Наконец, не забудьте добавить следующий прототип include и функции в файл shell.h, прямо перед закрывающей директивой #endif:

#include "source.h"
int  parse_and_execute(struct source_s *src);

И это должно быть так! Теперь давайте скомпилируем нашу оболочку.

Введение в Часть IV

В этой части мы собираемся добавить таблицы символов в нашу оболочку. Таблица символов-это структура данных, используемая компиляторами и интерпретаторами для хранения переменных в виде записей в таблице. Каждая запись состоит из ключа (имя переменной) и связанного с ним значения (значение переменной). Ключи обычно уникальны, то есть мы не можем иметь две записи, которые имеют один и тот же ключ (то есть не можем иметь две переменные, которые имеют одно и то же имя переменной).

Обычно оболочка Linux заполняет свою таблицу символов при запуске. После заполнения таблицы символов компилятор или интерпретатор может легко выполнить поиск переменной в таблице, чтобы получить значение этой переменной. Мы также можем выполнять проверку типов, применять правила области видимости (например, делать переменную видимой только для функции, в которой она была объявлена) и экспортировать переменные оболочки во внешние команды.

Чтобы заполнить таблицу символов, оболочка считывает список переменных среды, который передается оболочке из родительского процесса (обычно это процесс, вошедший в систему пользователя, или дочерний процесс процесса входа в систему). Оболочка добавляет каждую переменную (и ее значение) в таблицу символов. После этого мы можем редактировать, удалять или экспортировать переменные оболочки по своему усмотрению, используя соответствующие встроенные утилиты (о которых мы поговорим позже в этой серии).

Зачем нам нужна таблица символов?

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

Всякий раз, когда вы просите оболочку повторить, экспортировать или отменить значение переменной оболочки, вы фактически просите оболочку получить доступ и/или изменить ее таблицу символов. Все оболочки имеют некоторую реализацию таблицы символов, хотя некоторые оболочки могут иметь для нее разные имена. Например, предположим, что вы вызвали следующую команду:

echo $PATH

Что должно дать вам результат, подобный этому:

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Вы, вероятно, знаете, что команда echo не имела ничего общего с выводом, который вы видите на экране, за исключением того факта, что echo распечатал путь. Именно оболочка на самом деле поняла, что $PATH означает имя переменной оболочки. Это также оболочка, которая заменила слово $PATH фактическим значением пути, которое затем было передано echo. Команда echo просто повторила аргумент, переданный оболочкой, который является исполняемым путем, который вы видите напечатанным на экране.

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

Реализация таблицы символов

Существуют различные способы реализации таблицы символов, наиболее распространенными из которых являются связанные списки, хэш-таблицы и бинарные деревья поиска. У каждого метода есть свои плюсы и минусы, и у нас нет ни времени, ни места, чтобы подробно обсудить каждый из них. Для наших целей мы будем использовать связанные списки, ко��орые проще всего реализовать и которые хорошо подходят с точки зрения скорости доступа и использования памяти. (Примечание: Если вы хотите использовать оболочку для чего-то другого, кроме обучения, вам следует рассмотреть возможность изменения реализации таблицы символов на ту, которая использует хэш-таблицы или двоичные деревья. Пример реализации хэш-таблицы можно найти (https://github.com/moisam/Layla-Shell/tree/master/src/symtab).

А теперь давайте взломаем этот код. В исходном каталоге создайте подкаталог symtab (вызовите mkdir symtab из эмулятора терминала). Перейдите в этот каталог (cd symtab) и создайте файл с именем symtab.h. добавьте следующий код в только что созданный заголовочный файл:

#ifndef SYMTAB_H
#define SYMTAB_H

#include "../node.h"

#define MAX_SYMTAB	256

/* the type of a symbol table entry's value */
enum symbol_type_e
{
	SYM_STR ,
	SYM_FUNC,
};

/* the symbol table entry structure */
struct symtab_entry_s
{
	char     *name;
	enum      symbol_type_e val_type;
	char     *val;
	unsigned  int flags;
	struct    symtab_entry_s *next;
	struct    node_s *func_body;
};

/* the symbol table structure */
struct symtab_s
{
	int    level;
	struct symtab_entry_s *first, *last;
};
/* values for the flags field of struct symtab_entry_s */                       
#define FLAG_EXPORT (1 << 0) /* export entry to forked commands */

/* the symbol table stack structure */
struct symtab_stack_s
{
	int    symtab_count;
	struct symtab_s *symtab_list[MAX_SYMTAB];
	struct symtab_s *global_symtab, *local_symtab;
};
struct symtab_s       *new_symtab(int level);
struct symtab_s       *symtab_stack_push(void);
struct symtab_s       *symtab_stack_pop(void);
int rem_from_symtab(struct symtab_entry_s *entry, struct symtab_s *symtab);
struct symtab_entry_s *add_to_symtab(char *symbol);
struct symtab_entry_s *do_lookup(char *str, struct symtab_s *symtable);
struct symtab_entry_s *get_symtab_entry(char *str);
struct symtab_s       *get_local_symtab(void);
struct symtab_s       *get_global_symtab(void);
struct symtab_stack_s *get_symtab_stack(void);
void init_symtab(void);
void dump_local_symtab(void);
void free_symtab(struct symtab_s *symtab);
void symtab_entry_setval(struct symtab_entry_s *entry, char *val); 
#endif

Перечисление symbol_type_e определяет типы записей нашей таблицы символов. Мы будем использовать тип SYM_STR для представления переменных оболочки и SYM_FUNC для представления функций (мы будем иметь дело с функциями оболочки позже в этой серии).

Структура struct symtab_entry_s представляет наши записи таблицы символов. Структура содержит следующие поля: name => имя переменной оболочки (или функции), представленной этой записью.

val_type => SYM_STR для переменных оболочки, SYM_FUNC для функций оболочки.

val => строковое значение (только для переменных оболочки).

flags => указывает различные свойства, которые мы будем присваивать переменным и функциям, например флаги export и readonly (мы рассмотрим их позже в этой серии).

next => указатель на следующую запись таблицы символов (поскольку мы реализуем таблицу как односвязный список).

func_body => для функций оболочки - абстрактное синтаксическое дерево, или AST, тела функции (мы говорили об AST в части I этого учебника).

Структура struct symtab_s представляет собой единую таблицу символов. Для начала мы воспользуемся одной таблицей символов, в которой определим все переменные оболочки. Позже, когда мы обсудим функции оболочки и начнем работать с файлами сценариев, нам нужно будет определить больше таблиц символов. Нулевая таблица символов будет глобальной таблицей, в которой мы определим наши глобальные переменные (те, которые доступны оболочке, а также все функции и скрипты, выполняемые ею). Таблицы символов номер один и выше-это локальные таблицы, в которых мы будем определять наши локальные переменные (те, которые доступны только для функции оболочки или скрипта, в котором они были объявлены). Каскадируя таблицы символов таким образом, мы эффективно реализуем переменную область видимости.

Наша структура struct symtab_s содержит следующие поля:

уровень => 0 для глобальной таблицы символов, 1 и выше для локальных таблиц символов.

во-первых, last => указатели на первую и последнюю записи в связанном списке таблицы соответственно.

Теперь, чтобы иметь возможность каскадировать таблицы символов, как мы обсуждали выше, нам нужно определить и реализовать стек таблиц символов. Стек-это структура данных Last-In-First-Out, или LIFO, в которой последний добавленный (или вытесненный) элемент является первым удаленным (или выскочившим) элементом. Структура struct symtab_stack_s представляет наш стек таблиц символов. Структура содержит следующие поля: symtab_count => количество таблиц символов, находящихся в данный момент в стеке.

symtab_list => массив указателей на таблицы символов стека. Нулевой элемент указывает на глобальную таблицу символов, а элемент symtab_count-1-на последнюю (или локальную) таблицу символов. Стек может содержать до записей MAX_SYMTAB, которые мы определили в начале заголовочного файла равными 256.

global_symtab, local_symtab => указатели на глобальные и локальные таблицы символов соответственно (для удобства доступа).

Мы реализуем стек позже в этом уроке. Сейчас мы начнем с написания функций, которые нам понадобятся для работы с таблицами символов.

Функции Таблицы Символов

Создайте файл symtab.c (в подкаталоге symtab) и начните с добавления следующего кода:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "../shell.h"
#include "../node.h"
#include "../parser.h"
#include "symtab.h"

struct symtab_stack_s symtab_stack;
int    symtab_level;

void init_symtab(void)
{
	symtab_stack.symtab_count = 1;
	symtab_level = 0;
	struct symtab_s *global_symtab = malloc(sizeof(struct symtab_s));
	if(!global_symtab)
	{
		fprintf(stderr, "fatal error: no memory for global symbol table\n");
		exit(EXIT_FAILURE);
	}
	memset(global_symtab, 0, sizeof(struct symtab_s));
	symtab_stack.global_symtab  = global_symtab;
	symtab_stack.local_symtab   = global_symtab;
	symtab_stack.symtab_list[0] = global_symtab;
	global_symtab->level        = 0;
}

Во-первых, у нас есть две глобальные переменные:

symtab_stack => указатель на наш стек таблиц символов (нам нужен только один стек на оболочку).

symtab_level => наш текущий уровень в стеке (0, если мы работаем с глобальной таблицей символов, в противном случае ненулевой).

Функция init_symtab() инициализирует наш стек таблиц символов, затем выделяет память для нашей глобальной таблицы символов и инициализирует ее.

Затем добавьте следующую функцию:

struct symtab_s *new_symtab(int level)
{
	struct symtab_s *symtab = malloc(sizeof(struct symtab_s));
	if(!symtab)
	{
		fprintf(stderr, "fatal error: no memory for new symbol table\n");
		exit(EXIT_FAILURE);
	}
	memset(symtab, 0, sizeof(struct symtab_s));
	symtab->level = level;
	return symtab;
}

Мы будем вызывать функцию new_symtab() всякий раз, когда мы хотим создать новую таблицу символов (например, когда мы собираемся выполнить функцию оболочки).

Затем добавьте следующую функцию:

void free_symtab(struct symtab_s *symtab)
{
	if(symtab == NULL)
	{
		return;
	}
	struct symtab_entry_s *entry = symtab->first;
	while(entry)
	{
		if(entry->name)
		{
			free(entry->name);
		}
		if(entry->val)
		{
			free(entry->val);
		}
		if(entry->func_body)
		{
			free_node_tree(entry->func_body);
		}
		struct symtab_entry_s *next = entry->next;
		free(entry);
		entry = next;
	}
	free(symtab);
}

Мы вызовем функцию free_symtab (), когда закончим работу с таблицей символов, и мы хотим освободить память, используемую таблицей символов и ее записями.

Далее мы определим функцию отладки:

void dump_local_symtab(void)
{
	struct symtab_s *symtab = symtab_stack.local_symtab;
	int i = 0;
	int indent = symtab->level * 4;

	fprintf(stderr, "%*sSymbol table [Level %d]:\r\n", indent, " ", symtab->level);
	fprintf(stderr, "%*s===========================\r\n", indent, " ");
	fprintf(stderr, "%*s  No               Symbol                    Val\r\n", indent, " ");
	fprintf(stderr, "%*s------ -------------------------------- ------------\r\n", indent, " ");
	struct symtab_entry_s *entry = symtab->first;
	while(entry)
	{
		fprintf(stderr, "%*s[%04d] %-32s '%s'\r\n", indent, " ",
				i++, entry->name, entry->val);
		entry = entry->next;
	}
	fprintf(stderr, "%*s------ -------------------------------- ------------\r\n", indent, " ");
}

Эта функция выводит содержимое локальной таблицы символов. Когда наша оболочка запускается, локальные и глобальные таблицы символов будут ссылаться на одну и ту же таблицу. Только когда оболочка собирается запустить функцию оболочки или файл сценария, у нас есть локальная таблица, которая отличается от глобальной таблицы. (позже в этом уроке мы напишем встроенную утилиту, которая вызовет dump_local_symtab (), чтобы помочь нам визуализировать содержимое глобальной таблицы символов нашей оболочки).

Теперь давайте определим некоторые функции, которые помогут нам работать с записями таблицы символов. В том же файле (symtab.c) добавьте следующую функцию:

struct symtab_entry_s *add_to_symtab(char *symbol)
{
	if(!symbol || symbol[0] == '\0')
	{
		return NULL;
	}
	struct symtab_s *st = symtab_stack.local_symtab;
	struct symtab_entry_s *entry = NULL;

	if((entry = do_lookup(symbol, st)))
	{
		return entry;
	}
	entry = malloc(sizeof(struct symtab_entry_s));
	if(!entry)
	{
		fprintf(stderr, "fatal error: no memory for new symbol table entry\n");
		exit(EXIT_FAILURE);
	}
	memset(entry, 0, sizeof(struct symtab_entry_s));
	entry->name = malloc(strlen(symbol)+1);
	if(!entry->name)
	{
		fprintf(stderr, "fatal error: no memory for new symbol table entry\n");
		exit(EXIT_FAILURE);
	}
	strcpy(entry->name, symbol);
	if(!st->first)
	{
		st->first      = entry;
		st->last       = entry;
	}
	else
	{
		st->last->next = entry;
		st->last       = entry;
	}
	return entry;
}

Эта функция добавляет новую запись в локальную таблицу символов. Помните, в начале этого урока я сказал, что каждая запись должна иметь уникальный ключ, который является именем, которое мы даем переменной оболочки или функции. Чтобы обеспечить эту уникальность, мы сначала проверяем, существует ли запись с заданным именем, вызывая do_lookup() (который мы определим через минуту). Если запись с заданным именем существует, мы просто возвращаем существующую запись, не добавляя новую. В противном случае мы добавим запись, установим ее имя и настроим указатели таблицы символов. Наконец, мы возвращаем только что добавленную запись.

Следующая функция выполняет противоположную работу, т. е. удаляет запись таблицы символов, ключ которой соответствует заданному имени:

int rem_from_symtab(struct symtab_entry_s *entry, struct symtab_s *symtab)
{
	int res = 0;
	if(entry->val)
	{
		free(entry->val);
	}
	if(entry->func_body)
	{
		free_node_tree(entry->func_body);
	}
	free(entry->name);
	if(symtab->first == entry)
	{
		symtab->first = symtab->first->next;
		if(symtab->last == entry)
		{
			symtab->last = NULL;
		}
		res = 1;
	}
	else
	{
		struct symtab_entry_s *e = symtab->first;
		struct symtab_entry_s *p = NULL;
		while(e && e != entry)
		{
			p = e;
			e = e->next;
		}
		if(e == entry)
		{
			p->next = entry->next;
			res = 1;
		}
	}
	free(entry);
	return res;
}

Эта функция освобождает память, используемую записью, и настраивает указатели связанного списка, чтобы удалить запись из таблицы символов.

Чтобы выполнить поиск (то есть поиск переменной с заданным именем), нам нужно определить следующую функцию в том же файле:

struct symtab_entry_s *do_lookup(char *str, struct symtab_s *symtable)
{
	if(!str || !symtable)
	{
		return NULL;
	}
	struct symtab_entry_s *entry = symtable->first;
	while(entry)
	{
		if(strcmp(entry->name, str) == 0)
		{
			return entry;
		}
		entry = entry->next;
	}
	return NULL;
}

Эта функция выполняет поиск в данной таблице символов, начиная с первой записи. Если ключ записи совпадает с именем переменной, которую мы ищем, функция возвращает запись. В противном случае функция следует указателям связанного списка, чтобы посмотреть на каждую запись, в свою очередь, пока мы не найдем запись, ключ которой соответствует нашему желаемому имени. Если совпадение не найдено, мы возвращаем NULL.

Затем добавьте следующую функцию:

struct symtab_entry_s *get_symtab_entry(char *str)
{
	int i = symtab_stack.symtab_count-1;
	do
	{
		struct symtab_s *symtab = symtab_stack.symtab_list[i];
		struct symtab_entry_s *entry = do_lookup(str, symtab);
		if(entry)
		{
			return entry;
		}
	} while(--i >= 0);
	return NULL;
}

Эта функция ищет запись таблицы символов, ключ которой соответствует заданному имени. На первый взгляд это может показаться излишним, поскольку мы уже определили функцию do_lookup() для поиска в локальной таблице символов. Разница здесь в том, что get_symtab_entry() ищет весь стек, начиная с локальной таблицы символов. На данный момент это различие не имеет значения, так как наши локальные и глобальные таблицы символов относятся к одной и той же таблице. Только когда мы говорим о функциях оболочки и файлах сценариев, вы поймете, что делает эта функция (так что держитесь крепко!).

Наконец, добавьте следующую функцию:

void symtab_entry_setval(struct symtab_entry_s *entry, char *val)
{
	if(entry->val)
	{
		free(entry->val);
	}
	if(!val)
	{
		entry->val = NULL;
	}
	else
	{
		char *val2 = malloc(strlen(val)+1);
		if(val2)
		{
			strcpy(val2, val);
		}
		else
		{
			fprintf(stderr, "error: no memory for symbol table entry's value\n");
		}
		entry->val = val2;
	}
}

Эта функция освобождает память, используемую для хранения значения старой записи (если таковое существует). Затем он создает копию нового значения и сохраняет его в записи таблицы символов.

Вот и все для функций таблицы символов. Теперь давайте напишем несколько функций, которые помогут нам работать со стеком таблиц символов.

Функции Стека Таблиц Символов

Добавьте следующий код в тот же исходный файл symtab.c:

void symtab_stack_add(struct symtab_s *symtab)
{
	symtab_stack.symtab_list[symtab_stack.symtab_count++] = symtab;
	symtab_stack.local_symtab = symtab;
}

struct symtab_s *symtab_stack_push(void)
{
	struct symtab_s *st = new_symtab(++symtab_level);
	symtab_stack_add(st);
	return st;
}

struct symtab_s *symtab_stack_pop(void)
{
	if(symtab_stack.symtab_count == 0)
	{
		return NULL;
	}
	struct symtab_s *st = symtab_stack.symtab_list[symtab_stack.symtab_count-1];
	symtab_stack.symtab_list[--symtab_stack.symtab_count] = NULL;
	symtab_level--;
	if(symtab_stack.symtab_count == 0)
	{
		symtab_stack.local_symtab  = NULL;
		symtab_stack.global_symtab = NULL;
	}
	else
	{
		symtab_stack.local_symtab = symtab_stack.symtab_list[symtab_stack.symtab_count-1];
	}
	return st;
}

struct symtab_s *get_local_symtab(void)
{
	return symtab_stack.local_symtab;
}

struct symtab_s *get_global_symtab(void)
{
	return symtab_stack.global_symtab;
}

struct symtab_stack_s *get_symtab_stack(void)
{
	return &symtab_stack;
}

Вот краткое описание вышеперечисленных функций:

symtab_stack_add() добавляет данную таблицу символов в стек и назначает вновь добавленную таблицу в качестве локальной таблицы символов.

symtab_stack_push() создает новую пустую таблицу символов и помещает ее поверх стека.

symtab_stack_pop() удаляет (или всплывает) таблицу символов поверх стека, регулируя указатели стека по мере необходимости.

get_local_symtab() и get_global_symtab() возвращают указатели на локальную и глобальную таблицы символов соответственно.

get_symtab_stack() возвращает указатель на стек таблицы символов.

Инициализация Нашего Стека Таблиц Символов

Помните, в начале этого урока я говорил вам, что оболочка должна инициализировать свою глобальную таблицу символов и добавить в нее переменные из списка окружения? Итак, в этой части мы инициализируем стек таблиц символов и глобальную таблицу символов. В следующей части этой серии мы прочитаем список переменных окружения и добавим их в таблицу символов.

Создайте исходный файл с именем initsh.c в исходном каталоге и добавьте следующий код:

#include <string.h>
#include "shell.h"
#include "symtab/symtab.h"

extern char **environ;

void initsh(void)
{
	init_symtab();
	struct symtab_entry_s *entry;
	char **p2 = environ;
	while(*p2)
	{
		char *eq = strchr(*p2, '=');
		if(eq)
		{
			int len = eq-(*p2);
			char name[len+1];
			strncpy(name, *p2, len);
			name[len] = '\0';
			entry = add_to_symtab(name);
			if(entry)
			{
				symtab_entry_setval(entry, eq+1);
				entry->flags |= FLAG_EXPORT;
			}
		}
		else
		{
			entry = add_to_symtab(*p2);
		}
		p2++;
	}
	entry = add_to_symtab("PS1");
	symtab_entry_setval(entry, "$ ");
	entry = add_to_symtab("PS2");
	symtab_entry_setval(entry, "> ");
}

Эта функция инициализирует стек таблиц символов (включая глобальную таблицу символов) и сканирует список окружений, добавляя в таблицу каждую переменную окружения (и ее значение). Наконец, функция добавляет две переменные, которые мы будем использовать для хранения строк приглашения, PS1 и PS2 (мы говорили о строках приглашения в части I). Не забудьте добавить прототип функции в свою shell.h заголовочный файл:

void initsh(void);

Затем нам нужно вызвать эту функцию из нашей функции main (), прежде чем мы войдем в цикл REPL. Для этого добавьте следующую строку в main(), прямо перед телом цикла:

initsh();

Последнее, что нам нужно сделать, это обновить наши функции печати строк приглашения, чтобы они использовали переменные PS1 и PS2, которые мы только что добавили в глобальную таблицу символов в initsh(). Мы также напишем нашу первую встроенную утилиту dump.

Теперь давайте применим эти изменения к нашему коду.

Обновление функций оперативной печати

Открыть свой запрос.файл c. Удалите строку в теле функции print_prompt1() и замените ее следующим кодом:

void print_prompt1(void)
{   
	struct symtab_entry_s *entry = get_symtab_entry("PS1");
	if(entry && entry->val)
	{
		fprintf(stderr, "%s", entry->val);
	}
	else
	{
		fprintf(stderr, "$ ");
	}
}

Новый код проверяет, есть ли запись таблицы символов с именем PS1. Если есть, мы используем значение этой записи для печати первой строки приглашения. В противном случае мы используем встроенное значение по умолчанию, которое равно $ .

Изменения кода в функции print_prompt2() аналогичны, поэтому я не буду показывать его здесь, но вы можете проверить его по ссылке РЕПО GitHub, которую я предоставил в верхней части этой страницы.

Не забудьте добавить следующую директиву include в верхней части файла, сразу после строки #include "shell.h" :

#include "symtab/symtab.h"

Определение нашей первой встроенной утилиты

Встроенная утилита (a.k.a. builtin или внутренняя команда) - это команда, код которой компилируется как часть самого исполняемого файла оболочки, то есть оболочке не нужно выполнять внешнюю программу, а также не нужно разветвлять новый процесс для выполнения команды. Многие команды, которые мы используем ежедневно, такие как cd, echo, export и readonly, на самом деле встроены в утилиты. Вы можете прочитать больше о встроенных утилитах shell в этой стандартной ссылке POSIX.

В ходе этого урока мы будем добавлять различные встроенные утилиты для расширения нашей оболочки. Мы начнем с определения структуры, которая поможет нам хранить информацию о наших различных встроенных утилитах. Открой свою раковину.h заголовочный файл и добавьте следующий код в конце, прямо перед директивой #endif:

/* shell builtin utilities */
int dump(int argc, char **argv);
/* struct for builtin utilities */
struct builtin_s
{
	char *name;    /* utility name */
	int (*func)(int argc, char **argv); /* function to call to execute the utility */
};
/* the list of builtin utilities */
extern struct builtin_s builtins[];
/* and their count */
extern int builtins_count;

Прототип функции объявляет нашу первую встроенную утилиту dump. Строгая структура building_s определяет наши встроенные утилиты и имеет следующие поля:

name => встроенное имя утилиты, которое мы будем использовать для вызова утилиты.

func => function указатель на функцию, реализующую встроенную утилиту в нашей оболочке.

Мы будем использовать массив builtins[] для хранения информации о наших встроенных утилитах. Массив содержит встроенное количество элементов in_count.

Теперь создайте новый подкаталог и назовите его встроенные модули. Именно здесь мы определим все наши встроенные утилиты. Затем создайте файл builtins.c и добавьте в него следующий код:

#include "../shell.h"

struct builtin_s builtins[] =
{   
	{ "dump"    , dump       },
};

int builtins_count = sizeof(builtins)/sizeof(struct builtin_s);

Затем мы добавим встроенную утилиту с именем dump, которая будет использоваться для сброса или печати содержимого таблицы символов, чтобы мы знали, что происходит за кулисами (я в основном написал эту утилиту, чтобы наше обсуждение не звучало слишком абстрактно и теоретически).

В подкаталоге built ins создайте файл dump.c и добавьте в него следующий код:

#include "../shell.h"
#include "../symtab/symtab.h"

int dump(int argc, char **argv)
{
	dump_local_symtab();
	return 0;
}

Просто, правда? Эта функция реализует нашу встроенную утилиту дампа, которая печатает содержимое локальной таблицы символов.

Обновление исполнителя

Далее нам нужно рассказать нашему исполнителю о нашей новой встроенной утилите, чтобы при вводе дампа команды оболочка выполняла нашу функцию dump (), а не искала внешнюю команду с именем dump. Для этого нам нужно исправить нашу функцию do_simple_command ().

В исходном каталоге откройте исходный файл executor.c и перейдите к определению функции do_simple_command (). Теперь найдите две линии:

argv[argc] = NULL;
pid_t child_pid = 0;

Вставьте новую строку между этими двумя строками и введите следующий код:

int i = 0;
for( ; i < builtins_count; i++)
{
    if(strcmp(argv[0], builtins[i].name) == 0)
    {
        builtins[i].func(argc, argv);
        free_argv(argc, argv);
        return 1;
    }
}

Теперь наша оболочка проверит, является ли имя команды, которую она собирается выполнить, именем встроенной утилиты, и если да, то она вызывает функцию, реализующую встроенную утилиту, и возвращает ее.

И это все! Теперь давайте скомпилируем и протестируем нашу оболочку.

Введение в Часть V

Как мы видели в предыдущих частях, простая команда состоит из одного или нескольких аргументов, также известных как слова. Первое слово содержит имя команды, которую мы хотим выполнить, в то время как остальные слова содержат аргументы команды.

Прежде чем оболочка выполнит команду, она выполняет расширение слова для командных слов. Расширение слова-это процесс, посредством которого оболочка берет командное слово, проверяет его, чтобы увидеть, содержит ли оно имена переменных, пути, команды и арифметические выражения, и заменяет каждое имя/команду/выражение своим значением. Результирующее слово, которое обычно (но не всегда) длиннее исходного, может быть разбито на одно или несколько подсловов (или полей) в процессе, известном как расщепление полей.

В этой части мы реализуем 7 расширений слов, определенных POSIX, а именно: расширение Тильды, расширение параметра, арифметическое расширение, подстановка команд, разделение полей, расширение пути и удаление кавычек. Существуют и другие расширения слов, такие как расширение скобок и подстановка процессов, которые не определяются POSIX и которые мы здесь не будем обсуждать. После завершения этого урока было бы хорошим упражнением, если бы вы расширили оболочку, реализовав расширения слов, отличные от POSIX.

Примечание о коде этого урока

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

После того, как мы закончим обсуждение отдельных функций расширения слов, мы напишем основную функцию, которая свяжет все вместе, а затем мы обновим код нашей существующей оболочки, чтобы заставить нашу оболочку использовать функции расширения слов, которые мы только что реализовали. Чтобы облегчить чтение кода, я определил все наши функции расширения word в исходном файле wordexp.c, который вы найдете по этой ссылке (функции сопоставления шаблонов определены в файле pattern.c, который вы можете прочитать по этой ссылке).

А теперь давайте начнем.

Процесс Расширения Слова

Когда оболочка выполняет расширение слова, она проверяет каждое слово в командной строке, чтобы увидеть, содержит ли оно возможные расширения слова. Расширения могут появляться в любом месте слова: в начале, в середине или в конце слова. Расширение может также включать в себя все слово.

Расширению слов предшествует знак$. Символы, следующие за знаком$, указывают тип расширения, выполняемого оболочкой. Эти символы интерпретируются оболочкой следующим образом:

Одна или несколько цифр, которые указывают на переменное расширение позиционного параметра (мы обсудим их в следующем уроке этого урока).

Один из них @, *, #, ?, -, $, !, или 0, которые указывают на переменное расширение специального параметра (мы обсудим их в следующем уроке этого урока).

Один или несколько буквенно-цифровых символов и/или символов подчеркивания, начинающихся с буквы или символа подчеркивания, который указывает имя переменной оболочки.

Имя переменной, заключенное в фигурные скобки { и }.

Арифметическое разложение, окруженное ( и ).

Подстановка команд, окруженная (( и )).

Оболочка сначала выполняет расширение Тильды, расширение параметров, подстановку команд и арифметическое расширение, за которым следует разделение полей и расширение пути. Наконец, оболочка удаляет все символы кавычек из развернутого слова(ов), которые были частью исходного слова.

Работа со словами

Когда оболочка выполняет расширение слова, процесс может привести к нулю, одному или нескольким словам. Мы будем использовать специальную структуру для представления этих слов, которые мы определим в нашем заголовочном файле shell.h:

struct word_s
{
	char  *data;
	int    len;
	struct word_s *next;
};

Структура содержит следующие поля:

data => строка, представляющая это слово.

len => длина поля данных.

next => указатель на следующее слово или NULL, если это последнее слово (мы будем использовать связанный список для представления наших развернутых слов).

Конечно, нам понадобятся некоторые функции для выделения и освобождения наших структур struct word_s. Для этого мы будем использовать следующие функции:

struct word_s *make_word(char *str);
void free_all_words(struct word_s *first);

Первая функция выделяет память для структуры, создает копию строки слова и возвращает вновь выделенное слово. Вторая функция освобождает память, используемую списком структур слов. Вы можете прочитать код функций в нашем исходном файле wordexp.c.

Определение Некоторых Вспомогательных Функций

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

В следующем списке показаны прототипы функций для наших вспомогательных функций, которые мы определим в исходном файле wordexp.c:

char *wordlist_to_str(struct word_s *word);
void delete_char_at(char *str, size_t index);
int is_name(char *str);
size_t find_closing_quote(char *data);
size_t find_closing_brace(char *data);
char *substitute_str(char *s1, char *s2, size_t start, size_t end);
int substitute_word(char **pstart, char **p, size_t len, char *(func)(char *), int add_quotes);

Вот разбивка того, что делают эти функции:

wordlist_to_str() => преобразует связанный список развернутых слов в одну строку.

delete_char_at() => удаляет символ с заданным индексом из строки.

is_name() => проверяет, представляет ли строка допустимое имя переменной (см. раздел процесс расширения слов выше).

find_closing_quote() => когда расширение слова содержит открывающий символ кавычки ( " , ' или `), нам нужно найти соответствующий закрывающий символ кавычки, который заключает строку в кавычки (подробнее о цитировании ниже). Эта функция возвращает нулевой индекс закрывающего символа кавычки в слове.

find_closing_brace() => аналогично приведенному выше, за исключением того, что он находит соответствующую закрывающую скобку. То есть, если открывающая скобка имеет значение {, (или [, эта функция возвращает нулевой индекс соответствующего символа}, ) или ] соответственно. Поиск пар кавычек важен для обработки расширения параметров, арифметического расширения и замены команд.

substitute_str() => заменяет подстроку s1, начиная с символа в начале позиции и заканчивая символом в конце позиции (обе позиции основаны на нуле), строкой s2. Это полезно, когда расширение слова является частью более длинного слова, такого как ${PATH}/ls, и в этом случае нам нужно только развернуть ${PATH}, а затем присоединить /ls к концу расширенной строки.

substitute_word() => вспомогательная функция, вызывающая другие функции расширения слов, которые мы определим в следующих разделах.

Кроме того, мы определим некоторые функции, которые помогут нам работать со строками. Мы определим все эти функции в исходном файле strings.c:

char   *strchr_any(char *string, char *chars);
char   *quote_val(char *val, int add_quotes);
int     check_buffer_bounds(int *count, int *len, char ***buf);
void    free_buffer(int len, char **buf);

Вот что делают эти функции:

strchr_any() => аналогично strchr(), за исключением того, что он ищет в данной строке любой из заданных символов.

quote_val() => делает обратное удаление кавычек, то есть преобразует строку в строку в кавычках (это может показаться тривиальным на первый взгляд, но цитирование может стать сложным, когда нам придется экранировать кавычки и символы обратной косой черты, например).

Функции check_buffer_bounds() и free_buffer() позволят нашему бэкенд-исполнителю поддерживать переменное число аргументов команды вместо жесткого ограничения, установленного в Части II, которое составляло 255. Теперь давайте напишем функции для обработки каждого типа расширения слова.

Расширение Тильды

Во время расширения Тильды оболочка заменяет символ тильды (за которым следует необязательное имя пользователя) путем к домашнему каталогу пользователя. Например, ~ и ~ / -это Тильда, расширенная до домашнего каталога текущего пользователя, а ~john-это Тильда, расширенная до домашнего каталога пользователя Джона, и так далее. Символ тильды, в дополнение ко всем последующим символам вплоть до первой некотируемой прямой косой черты, известен как префикс Тильды (мы будем иметь дело с цитированием позже в этом сеансе).

Чтобы выполнить расширение Тильды, мы определим функцию tilde_expand (), которая имеет следующий прототип:

char *tilde_expand(char *s);

Функция принимает один аргумент: префикс Тильды, который мы хотим развернуть. Если расширение выполняется успешно, функция возвращает строку malloc'D, представляющую префикс, расширенный Тильдой. В противном случае он возвращает NULL. Вот краткое описание того, что делает функция для расширения префикса Тильды:

Если префикс равен ~, то получите значение переменной оболочки $HOME. Если $HOME определен и не равен NULL, верните его значение. В противном случае получите текущий идентификатор пользователя (UID), вызвав getuid(), и передайте UID в getpwuid (), чтобы получить запись базы данных паролей, соответствующую текущему пользователю. Поле pw_dir записи базы данных паролей содержит путь к домашнему каталогу, который возвращает функция.

Если префикс содержит другие символы (кроме ведущего ~), мы принимаем эти буквы за имя пользователя, чей домашний каталог мы хотим получить. Мы вызываем getpwnam(), передавая ему имя пользователя, и возвращаем значение поля pw_dir.

Если мы не можем получить домашний каталог (например, если имя пользователя неверно), мы возвращаем NULL. В противном случае мы возвращаем копию malloc'D пути к домашнему каталогу.

Найдите минутку, чтобы прочитать код функции tilde_expand() в нашем репозитории GitHub.

Расширение Параметров

При расширении параметров оболочка заменяет имя переменной оболочки значением переменной (отсюда и другое название-расширение переменной). Расширение параметров - это то, что позволяет оболочке выполнять такие команды, как echo $PATH. В этом примере оболочка выполняет расширение параметров переменной $PATH, заменяя ее фактическим исполняемым путем (что-то вроде /bin:/sbin:/usr/bin:/usr/sbin). Команда echo видит не слово $PATH, а его расширенное значение (которое, конечно, может быть нулевым).

Чтобы сообщить оболочке, что мы хотим расширить переменную оболочки, мы предваряем имя переменной знаком$. То есть, чтобы развернуть переменные PATH, USER и SHELL, нам нужно передать слова $PATH, $USER и $SHELL в оболочку соответственно (в качестве альтернативы мы можем передать эти расширения переменных в оболочку shell как ${PATH}, ${USER} и ${SHELL}). Имена переменных оболочки могут содержать любую комбинацию букв, цифр и символов подчеркивания. Имена могут содержать заглавные или строчные буквы, хотя, по соглашению, заглавные имена зарезервированы для стандартных переменных оболочки, таких как те, которые определены POSIX.

Мы можем управлять тем, как оболочка выполняет расширение параметров, используя модификатор расширения параметров, который сообщает оболочке, какую часть значения мы хотим расширить, а также что делать, если нет переменной оболочки с заданным именем. В таблице ниже приведены модификаторы расширения параметров (те, которые определены POSIX, отмечены словом POSIX в столбце описание). Большинство оболочек поддерживают другие модификаторы (найденные в нижней половине таблицы), которые мы здесь не будем обсуждать. Обратитесь к странице справочника оболочки для получения дополнительной информации на не-POSIX модификаторов.

Чтобы выполнить расширение параметра (или переменной), мы определим функцию var_expand (), которая имеет следующий прототип:

char *var_expand(char *orig_var_name);

Функция принимает один аргумент: параметр (т. е. имя переменной), который мы хотим развернуть. Если расширение выполняется успешно, функция возвращает строку malloc'D, содержащую развернутое значение. В противном случае он возвращает NULL. Вот краткое описание того, что делает функция для расширения имени переменной, чтобы получить ее значение:

Если имя переменной окружено фигурными скобками (например, ${PATH}), удалите их, так как они не являются частью самого имени переменной.

Если имя начинается с #, нам нужно получить длину имени переменной (расширение ${#parameter}, которое видно в 5-й строке таблицы выше).

Если имя переменной содержит двоеточие, мы будем использовать его для отделения имени от (необязательного) слова или шаблона. Слово или шаблон используется, как указано в таблице выше.

Получите запись таблицы символов с заданным именем переменной (мы реализовали стек таблиц символов в части IV). Получите значение записи таблицы символов.

Если значение пустое или нулевое, используйте альтернативное слово, указанное в расширении, если таковое имеется.

Если значение не пустое, используйте его в качестве развернутого результата. Чтобы оболочка могла выполнять сопоставление шаблонов (расширения ${parameter#word} и ${parameter%word}), нам понадобятся две вспомогательные функции: match_suffix() и match_prefix(). Мы не будем обсуждать эти функции здесь, но вы можете прочитать их код по этой ссылке (https://github.com/moisam/lets-build-a-linux-shell/blob/master/part5/pattern.c).

Если модификатором расширения является ${parameter:=word}, нам нужно установить значение записи таблицы символов в значение, которое мы только что расширили.

Если расширение начинается с #, получите длину развернутого значения и используйте его в качестве конечного результата.

Возвращает копию расширенного значения malloc'D или его длину (в строковом формате), если это необходимо.

Найдите минутку, чтобы прочитать код функции var_expand() в нашем репозитории GitHub. подстановка команды

При подстановке команд оболочка разветвляет процесс для выполнения команды, а затем заменяет расширение подстановки команд выводом команды. Например, в следующем цикле:

for i in $(ls); do echo $i; done

оболочка разветвляет процесс, в котором выполняется команда ls. Результатом выполнения этой команды является список файлов в текущем каталоге. Оболочка принимает эти выходные данные, разбивает их на список слов, а затем передает эти слова по одному в цикл. На каждой итерации цикла переменной $i присваивается имя следующего файла из списка. Это имя передается команде echo, которая выводит имя в отдельной строке (практически было бы лучше выполнить ls напрямую, но этот пример просто показывает, как можно использовать подстановку команд в оболочке).

Подстановка команд может быть записана как $(command) или command. Для выполнения подстановки команд мы определим функцию command_substitute (), которая имеет следующий прототип:

char *command_substitute(char *orig_cmd);

Функция принимает один аргумент: команду, которую мы хотим выполнить. Если расширение выполняется успешно, функция возвращает строку malloc'D, представляющую выходные данные команды. Если расширение завершается неудачно или команда ничего не выводит, функция возвращает значение NULL. Вот краткое описание того, что делает функция для расширения подстановки команд:

В зависимости от используемого формата мы начинаем с удаления $() или обратных кавычек `. Это оставляет нас с командой, которую мы должны выполнить.

Вызовите popen (), чтобы создать канал. Мы передаем команду для выполнения в popen () и получаем указатель на файловый поток, из которого будем считывать выходные данные команды.

Вызовите fread (), чтобы прочитать выходные данные команды из канала. Храните считанную строку в буфере.

Удалите все конечные символы новой строки.

Закройте канал и верните буфер с выводом команды.

Найдите минутку, чтобы прочитать код функции command_substitute() в нашем репозитории GitHub.

Вычисление Арифметических Выражений

Используя арифметическое расширение, мы можем позволить оболочке выполнять различные арифметические операции и использовать результат при выполнении других команд. В то время как POSIX требует, чтобы оболочка поддерживала только знаковую длинную целочисленную арифметику, многие оболочки (например, ksh и zsh) поддерживают арифметику с плавающей запятой. Кроме того, оболочка не обязана поддерживать какие-либо математические функции, хотя большинство оболочек это делают. Для нашей простой оболочки мы будем поддерживать только знаковую длинную целочисленную арифметику без поддержки математических функций. Арифметическое разложение записывается как $((выражение)). Для выполнения расширения мы определим функцию arithm_expand (), которая имеет следующий прототип:

char *arithm_expand(char *expr);

Функция arithm_expand() получает строку, содержащую арифметическое выражение, выполняет необходимые вычисления и возвращает результат в виде строки malloc'D (или NULL в случае ошибки, например недопустимой арифметической операции). Эта функция и связанные с ней вспомогательные функции сложны и длительны, но вот основные моменты:

Арифметическое выражение преобразуется в обратную польскую нотацию (RPN), которую легче анализировать и вычислять. RPN состоит из ряда арифметических операций, где оператор следует (т. е. идет после) своих операндов. Например, RPN x - y равен x y -, а RPN 3 + 4 × (2 − 1) равен 3 4 2 1 − × +.

Во время преобразования арифметические операторы помещаются в стек операторов, из которого мы будем извлекать каждый оператор и выполнять его операцию позже. Аналогично, операнды добавляются в свой собственный стек. Операторы выскакивают из стека, один за другим, и оператор проверяется. Один или два операнда выскакивают из стека, в зависимости от типа оператора. Правила, которые управляют этим процессом, являются правилами алгоритма маневрового двора, о котором вы можете прочитать здесь.

Результат преобразуется в строку, которая возвращается вызывающему объекту.

Найдите минутку, чтобы прочитать код функции arithm_expand() в нашем репозитории GitHub.

Split Поля

Во время разделения полей оболочка принимает результат(ы) расширения параметров, замены команд и арифметического расширения и разбивает их на одну или несколько частей, которые мы называем полями (отсюда и название, разделение полей). Этот процесс зависит от значения переменной оболочки $IFS. IFS-это исторический термин, обозначающий внутренние (или входные) разделители полей, и он берет свое начало с тех времен, когда оболочки Unix не имели встроенного типа массива. В качестве обходного пути ранние оболочки Unix должны были найти другой способ представления многочленных массивов. Оболочка соединит элементы массива в одну строку, разделенную пробелами. Когда оболочке требуется извлечь элементы массива, она разбивает строку на одно или несколько полей. Переменная $IFS сообщает оболочке, где именно нужно разбить эту строку. Оболочка интерпретирует символы $IFS следующим образом (это описание взято из POSIX):

Если значение $IFS является пробелом, табуляцией и новой строкой или если переменная не задана, любая последовательность символов пробела, табуляции или новой строки в начале или конце ввода игнорируется, и любая последовательность этих символов внутри ввода ограничивает поле.

Если значение $IFS равно null, то разделение полей не выполняется.

В противном случае последовательно применяются следующие правила: (а) пробелы $IFS игнорируются в начале и конце входных данных. (b) каждое вхождение во входные данные символа $IFS, который не является пробелом $IFS, наряду с любым смежным пробелом $IFS, должно ограничивать поле, как описано ранее. (c) поле должно ограничиваться пробелом ненулевой длины $IFS.

Для выполнения расширения мы определим функцию field_split (), которая имеет следующий прототип:

struct word_s *field_split(char *str);

Найдите минутку, чтобы прочитать код функции field_split() в нашем репозитории GitHub.

Расширение Имени Пути

Во время расширения пути (также известного как глобирование имени файла) оболочка сопоставляет одно или несколько имен файлов с заданным шаблоном. Шаблон может содержать обычные символы (которые сопоставляются сами с собой), в дополнение к специальным символам*, ? и [], которые также известны как глобирующие символы. Звездочка * соответствует любой длине символов (включая нулевые символы), а ? соответствует одному символу, и скобки вводят регулярное выражение (RE) скобочное выражение. Результатом расширения является список файлов, имена которых соответствуют шаблону.

Для выполнения расширения мы определим функцию pathnames_expand (), которая имеет следующий прототип:

struct word_s *pathnames_expand(struct word_s *words);

Эта функция принимает один аргумент: указатель на первое слово в связанном списке слов, которые мы хотим развернуть по пути. Для каждого слова функция выполняет следующие действия: Проверьте, содержит ли слово какие-либо глобирующие символы ( * ,? и []), вызвав вспомогательную функцию has_glob_chars(), которую мы определим в шаблоне исходного файла.c. если слово содержит глобирующие символы, мы рассматриваем его как шаблон, который нам нужно сопоставить; в противном случае мы переходим к следующему слову (если таковое имеется).

Получите список файлов, имена которых соответствуют шаблону, исключая специальные имена . и .. (которые указывают на текущий каталог и родительский каталог соответственно). Мы делегируем сопоставление шаблонов другой вспомогательной функции get_filename_matches(), которую определим в том же исходном файле pattern.c.

Добавьте соответствующие имена файлов в окончательный список.

Переходите к следующему слову и циклу.

Возвращает список имен файлов, которые соответствуют всем заданным словам.

Найдите минутку, чтобы прочитать код функции pathnames_expand() в нашем репозитории GitHub.

Удаление Цитаты

Последний шаг в процессе расширения слова - удаление кавычек. Кавычки используются для удаления специального значения определенных символов в оболочку. Оболочка особым образом обрабатывает некоторые символы, такие как обратная косая черта и кавычки. Чтобы подавить это поведение, нам нужно процитировать эти символы, чтобы заставить оболочку рассматривать их как обычные символы (вы можете прочитать больше о цитировании в этой ссылке (https://www.gnu.org/software/bash/manual/html_node/Quoting.html)).

Мы можем цитировать символы одним из трех способов: с помощью обратной косой черты, одинарных кавычек или двойных кавычек. Символ обратной косой черты используется для сохранения буквального значения (то есть для экранирования) символа, следующего за обратной косой чертой. Это похоже на то, как мы экранируем символы в языке Си.

Одинарные кавычки сохраняют буквальное значение всех символов внутри кавычек, то есть оболочка не пытается расширить слово в строках с одинарными кавычками.

Двойные кавычки аналогичны одинарным кавычкам, за исключением того, что оболочка распознает особое значение обратной кавычки, обратной косой черты и знака$. То есть оболочка может выполнять расширение слова внутри строк в двойных кавычках.

Чтобы выполнить удаление кавычек, мы определим функцию remove_quotes(), которая имеет следующий прототип:

void remove_quotes(struct word_s *wordlist);

Найдите минутку, чтобы прочитать код функции remove_quotes() в нашем репозитории GitHub.

Складывая Все Это Вместе

Теперь, когда у нас есть функции расширения слов, пришло время связать все это вместе. В этом разделе мы напишем нашу основную функцию расширения слова, которую мы будем вызывать для выполнения расширения слова. Эта функция, в свою очередь, вызовет другие функции для выполнения различных шагов расширения слова.

Наша основная функция-word_expand(), которую мы определим в исходном файле wordexp.c:

struct word_s *word_expand(char *orig_word);

Вот что делает функция для выполнения расширения слова на слово, которое мы передаем в качестве его единственного аргумента:

Создайте копию исходного слова. Мы выполним наши расширения слов на этой копии, так что если что-то пойдет не так, мы будем иметь наше оригинальное слово нетронутым.

Сканируйте слово, символ за символом, ища специальные символы ~, ", ', `, =, , и $.

Если один из вышеперечисленных символов найден, вызовите функцию substitute_word(), которая, в свою очередь, вызовет соответствующую функцию расширения слова.

Пропустите все символы, которые не имеют особого значения.

Закончив с расширением слова, выполните разбиение полей, вызвав функцию field_split().

Выполните расширение имени пути, вызвав функцию pathnames_expand().

Выполните удаление кавычек, вызвав remove_quotes().

Возвращает список развернутых слов.

Найдите минутку, чтобы прочитать код функции word_expand() в нашем репозитории GitHub.

Обновление сканера

В Части II этого урока мы написали нашу функцию tokenize (), которую мы использовали для получения входных токенов. До сих пор наша функция tokenize() не знает, как обрабатывать строки в кавычках и экранированные символы. Чтобы добавить эту функциональность, нам нужно обновить наш код. Откройте файл scanner.c и добавьте следующий код в функцию tokenize() сразу после открывающей скобки оператора switch:

case  '"':
case '\'':
case  '`':
	add_to_buf(nc);
	i = find_closing_quote(src->buffer+src->curpos);
	if(!i)
	{
		src->curpos = src->bufsize;
		fprintf(stderr,
			"error: missing closing quote '%c'\n", nc);
		return &eof_token;
	}
	while(i--)
	{
		add_to_buf(next_char(src));
	}
	break;
case '\\':
	nc2 = next_char(src);
	if(nc2 == '\n')
	{
		break;
	}
	add_to_buf(nc);
	if(nc2 > 0)
	{
		add_to_buf(nc2);
	}
	break;
	
case '$':
	add_to_buf(nc);
	nc = peek_char(src);
	if(nc == '{' || nc == '(')
	{
		i = find_closing_brace(src->buffer+
						src->curpos+1);
		if(!i)
		{
			src->curpos = src->bufsize;
			fprintf(stderr,
				"error: missing closing brace '%c'\n",
				nc);
			return &eof_token;
		}
		while(i--)
		{
			add_to_buf(next_char(src));
		}
	}
	else if(isalnum(nc) || nc == '*' || nc == '@' ||
				nc == '#' || nc == '!' || nc == '?' ||
				nc == '$')
	{
		add_to_buf(next_char(src));
	}
	break;

Теперь наш лексический сканер умеет распознавать и пропускать строки в кавычках, экранированные символы и другие конструкции расширения слов.

Обновление исполнителя

Наконец, нам нужно обновить наш бэкенд-исполнитель, чтобы он мог:

перед выполнением этой команды выполните расширение слов по аргументам команды.

поддержка более 255 аргументов на команду (мы установили этот предел в Части II).

Откройте файл executor.c, перейдите к началу функции do_simple_command() и найдите следующие строки (я сократил цикл до ... чтобы сохранить пространство):

int argc = 0;
long max_args = 255;
char *argv[max_args+1];
char *str;
while(child)
{
    ...
}
argv[argc] = NULL;

и замените их следующим кодом:

int argc = 0;
int targc = 0;
char **argv = NULL;
char *str;
while(child)
{
    str = child->val.str;
    struct word_s *w = word_expand(str);
    
    if(!w)
    {
        child = child->next_sibling;
        continue;
    }
    struct word_s *w2 = w;
    while(w2)
    {
        if(check_buffer_bounds(&argc, &targc, &argv))
        {
            str = malloc(strlen(w2->data)+1);
            if(str)
            {
                strcpy(str, w2->data);
                argv[argc++] = str;
            }
        }
        w2 = w2->next;
    }
    
    free_all_words(w);
    
    child = child->next_sibling;
}
if(check_buffer_bounds(&argc, &targc, &argv))
{
    argv[argc] = NULL;
}

С помощью этого кода исполнитель вызывает функцию word_expand() для каждого аргумента команды и добавляет развернутое слово(ы) в список аргументов, который мы в конечном итоге передадим команде. Список может расти столько, сколько необходимо, благодаря нашей функции check_buffer_bounds (), которая выделяет память в буфер по мере необходимости.

Все, что остается теперь, это освободить наш список аргументов, как только мы закончим выполнение команды, прежде чем мы вернемся к вызывающему объекту. Мы делаем это вызовом функции:

free_buffer(argc, argv);

в трех разных местах: после выполнения встроенной утилиты if fork() возвращает статус ошибки (что означает, что мы не можем выполнить внешнюю команду) и после того, как waitpid() вернулся.

Компиляция оболочки

Давайте скомпилируем нашу оболочку. Откройте свой любимый эмулятор терминала, перейдите в исходный каталог и убедитесь, что там есть 19 файлов и 2 подкаталога

minishell's People

Contributors

elegija avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.