first commit

This commit is contained in:
2026-05-22 00:26:27 +03:00
commit c9d6d38491
17 changed files with 2311 additions and 0 deletions

19
LICENSE.txt Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2026 Grigorii Safronov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
# fush shell
**F**usion **U**nix **SH**ell — легковесная, расширяемая командная оболочка с поддержкой Linux и OpenIndiana.
[![Go Version](https://img.shields.io/badge/Go-1.26-blue.svg)](https://golang.org/)
[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
## Особенности
- 🚀 **Легковесная** — минимальное потребление ресурсов
- 🔧 **Расширяемая** — поддержка Lua скриптов
- 🎨 **Красивая** — ANSI цвета для приглашения и вывода
- 📜 **История команд** — сохранение и навигация по истории
- ⚙️ **Конфигурируемая** — TOML конфигурация
- 🔄 **Кроссплатформенная** — Linux и OpenIndiana (Illumos)
## Быстрый старт
### Установка
```bash
# Клонирование репозитория
git clone https://github.com/yourusername/fush.git
cd fush
# Сборка
go build -o fush
# Установка (опционально)
sudo cp fush /usr/local/bin/

565
build.sh Executable file
View File

@@ -0,0 +1,565 @@
#!/bin/bash
# build.sh - универсальный скрипт сборки для fush shell
# Поддерживает операционные системы Linux и OpenIndiana/Hipster
# Обеспечивает компиляцию, тестирование, установку и запуск shell
# Использует цветной вывод и автоматическое определение окружения
set -e
# Определение цветов для вывода
if [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m' # No Color
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
CYAN=''
MAGENTA=''
NC=''
fi
# Определение корневой директории проекта fush
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$SCRIPT_DIR"
FUSH_DIR="$PROJECT_ROOT"
# Проверяем, что мы находимся в директории проекта fush
if [ ! -d "$FUSH_DIR" ] || [ ! -f "$FUSH_DIR/go.mod" ]; then
# Пробуем подняться на уровень выше (на случай если скрипт в scripts/)
if [ -f "$SCRIPT_DIR/../go.mod" ]; then
FUSH_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECT_ROOT="$FUSH_DIR"
cd "$FUSH_DIR"
echo -e "${YELLOW}Скрипт запущен из поддиректории. Переход в корень проекта: ${FUSH_DIR}${NC}"
else
echo -e "${RED}error: Не найдена директория проекта fush с файлом go.mod${NC}"
echo -e "${RED}error: Проверены директории:${NC}"
echo -e "${RED} - ${FUSH_DIR}${NC}"
echo -e "${RED} - ${SCRIPT_DIR}/..${NC}"
echo -e "${RED}Убедитесь, что файл go.mod существует в корне проекта${NC}"
exit 1
fi
else
cd "$FUSH_DIR"
fi
# Определение переменных
PROJECT_NAME="fush"
BINARY_NAME="bin/${PROJECT_NAME}"
ROOT_BINARY_NAME="${PROJECT_NAME}"
BUILD_DIR="bin"
GO="go"
# Получение информации о версии
BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
REQUIRED_VERSION="1.26.0"
# Функция для сравнения версий (работает без sort -V)
version_compare() {
# Удаляем префикс 'v' если есть
local v1="${1#v}"
local v2="${2#v}"
# Разбиваем на компоненты
IFS='.' read -ra v1_arr <<< "$v1"
IFS='.' read -ra v2_arr <<< "$v2"
# Сравниваем компоненты
for i in 0 1 2; do
# Получаем числовые значения, удаляя ведущие нули
local num1="${v1_arr[$i]}"
local num2="${v2_arr[$i]}"
# Удаляем ведущие нули
num1="${num1#0}"
num2="${num2#0}"
# Если строка пустая после удаления нулей, значит было "0"
if [ -z "$num1" ]; then
num1=0
fi
if [ -z "$num2" ]; then
num2=0
fi
# Сравниваем как числа
if [ $num1 -lt $num2 ]; then
echo "-1"
return
elif [ $num1 -gt $num2 ]; then
echo "1"
return
fi
done
echo "0"
}
# Функция для отображения прав и информации о файле
show_file_info() {
local file_path="$1"
local file_name="$2"
if [ -f "$file_path" ]; then
echo -e "${CYAN}================================${NC}"
echo -e "${GREEN}Информация о собранном файле:${NC}"
echo -e "${CYAN}Имя файла:${NC} ${YELLOW}${file_name}${NC}"
echo -e "${CYAN}Полный путь:${NC} ${YELLOW}${file_path}${NC}"
echo -e "${CYAN}Размер:${NC} ${YELLOW}$(ls -lh "$file_path" | awk '{print $5}')${NC}"
echo -e "${CYAN}Права доступа:${NC} ${YELLOW}$(ls -l "$file_path" | awk '{print $1}')${NC}"
echo -e "${CYAN}Владелец:${NC} ${YELLOW}$(ls -l "$file_path" | awk '{print $3":"$4}')${NC}"
echo -e "${CYAN}Тип файла:${NC} ${YELLOW}$(file "$file_path" | cut -d: -f2)${NC}"
echo -e "${CYAN}================================${NC}"
else
echo -e "${RED}Файл не найден: ${file_path}${NC}"
fi
}
# Функция для вывода справки
show_help() {
echo -e "${CYAN}================================${NC}"
echo -e "${CYAN}fush Shell - Скрипт сборки${NC}"
echo -e "${CYAN}================================${NC}"
echo -e "${GREEN}Использование:${NC}"
echo -e " $0 [команда]"
echo
echo -e "${GREEN}Доступные команды:${NC}"
echo -e " ${YELLOW}all${NC} - Очистка и сборка (по умолчанию)"
echo -e " ${YELLOW}build${NC} - Сборка проекта"
echo -e " ${YELLOW}clean${NC} - Очистка временных файлов"
echo -e " ${YELLOW}install${NC} - Установка в систему"
echo -e " ${YELLOW}test${NC} - Запуск тестов"
echo -e " ${YELLOW}run${NC} - Сборка и запуск"
echo -e " ${YELLOW}help${NC} - Показать эту справку"
echo
echo -e "${GREEN}Примеры:${NC}"
echo -e " $0 # Выполнить очистку и сборку"
echo -e " $0 build # Только сборка"
echo -e " $0 install # Собрать и установить"
echo -e " $0 run # Собрать и запустить"
echo
}
# Функция проверки версии Go
check_go_version() {
echo -e "${YELLOW}Проверка версии Go...${NC}"
if [ -z "$GO_VERSION" ]; then
echo -e "${RED}error: Go не установлен${NC}"
exit 1
fi
echo -e "${GREEN}✓ Go версия ${GO_VERSION}${NC}"
# Сравнение версий с помощью собственной функции
local cmp=$(version_compare "$GO_VERSION" "$REQUIRED_VERSION")
if [ "$cmp" = "-1" ]; then
echo -e "${RED}error: Требуется Go версии ${REQUIRED_VERSION} или выше. Текущая версия: ${GO_VERSION}${NC}"
exit 1
fi
echo -e "${GREEN}✓ Версия Go соответствует требованиям${NC}"
}
# Функция проверки операционной системы
check_os() {
OS=$(uname -s)
case "$OS" in
Linux|SunOS)
echo -e "${GREEN}✓ Поддерживаемая ОС: ${OS}${NC}"
;;
*)
echo -e "${RED}error: Неподдерживаемая ОС: ${OS}${NC}"
echo -e "${RED}error: fush поддерживает только Linux и OpenIndiana${NC}"
exit 1
;;
esac
}
# Функция проверки наличия файлов проекта
check_project_files() {
echo -e "${YELLOW}Проверка файлов проекта в ${FUSH_DIR}...${NC}"
# Проверяем наличие go.mod
if [ ! -f "$FUSH_DIR/go.mod" ]; then
echo -e "${RED}error: Файл go.mod не найден в ${FUSH_DIR}${NC}"
exit 1
fi
echo -e "${GREEN}✓ Найден go.mod${NC}"
# Проверяем наличие go.sum
if [ ! -f "$FUSH_DIR/go.sum" ]; then
echo -e "${YELLOW}Предупреждение: Файл go.sum не найден в ${FUSH_DIR}${NC}"
echo -e "${YELLOW}Он будет создан при установке зависимостей${NC}"
else
echo -e "${GREEN}✓ Найден go.sum${NC}"
fi
# Проверяем наличие директории cmd/fush
if [ ! -d "$FUSH_DIR/cmd/fush" ]; then
echo -e "${RED}error: Директория cmd/fush не найдена в ${FUSH_DIR}${NC}"
exit 1
fi
echo -e "${GREEN}✓ Найдена директория cmd/fush${NC}"
# Проверяем наличие main.go
if [ ! -f "$FUSH_DIR/cmd/fush/main.go" ]; then
echo -e "${RED}error: Файл cmd/fush/main.go не найден${NC}"
exit 1
fi
echo -e "${GREEN}✓ Найден main.go${NC}"
}
# Функция установки зависимостей
install_deps() {
echo -e "${YELLOW}Установка зависимостей в ${FUSH_DIR}...${NC}"
# Проверяем наличие go.mod
if [ ! -f "go.mod" ]; then
echo -e "${RED}error: Файл go.mod не найден в текущей директории${NC}"
echo -e "${RED}error: Текущая директория: $(pwd)${NC}"
exit 1
fi
# Обновляем go.mod и создаем go.sum
echo -e "${YELLOW}Выполнение go mod download...${NC}"
$GO mod download 2>&1 | while IFS= read -r line; do
if [[ "$line" == *"error"* ]] || [[ "$line" == *"Error"* ]]; then
echo -e "${RED}error: $line${NC}"
else
echo "$line"
fi
done
echo -e "${YELLOW}Выполнение go mod tidy...${NC}"
$GO mod tidy 2>&1 | while IFS= read -r line; do
if [[ "$line" == *"error"* ]] || [[ "$line" == *"Error"* ]]; then
echo -e "${RED}error: $line${NC}"
else
echo "$line"
fi
done
# Проверяем наличие go.sum после установки
if [ -f "go.sum" ]; then
echo -e "${GREEN}✓ Файл go.sum успешно создан/обновлен${NC}"
GO_SUM_SIZE=$(ls -lh go.sum | awk '{print $5}')
echo -e "${GREEN}✓ Размер go.sum: ${GO_SUM_SIZE}${NC}"
else
echo -e "${RED}error: Файл go.sum не был создан после go mod tidy${NC}"
exit 1
fi
# Проверка наличия Lua
echo -e "${YELLOW}Проверка наличия Lua...${NC}"
if command -v lua >/dev/null 2>&1; then
echo -e "${GREEN}✓ Lua найден${NC}"
else
echo -e "${YELLOW}Предупреждение: Lua не найден. Для работы скриптов потребуется установить Lua${NC}"
echo -e "${YELLOW}Установите Lua с помощью вашего пакетного менеджера:${NC}"
echo -e " - Ubuntu/Debian: sudo apt-get install lua5.3"
echo -e " - OpenIndiana: sudo pkg install lua"
fi
}
# Функция сборки проекта
build_project() {
echo -e "${YELLOW}Компиляция ${PROJECT_NAME}...${NC}"
echo -e "${YELLOW}Директория проекта: ${FUSH_DIR}${NC}"
echo -e "${YELLOW}Текущая директория: $(pwd)${NC}"
# Проверяем наличие go.mod
if [ ! -f "go.mod" ]; then
echo -e "${RED}error: Файл go.mod не найден. Убедитесь, что вы находитесь в директории проекта fush${NC}"
return 1
fi
# Проверяем наличие go.sum
if [ ! -f "go.sum" ]; then
echo -e "${RED}error: Файл go.sum не найден. Выполните сначала установку зависимостей${NC}"
return 1
fi
# Проверяем существование директории cmd/fush
if [ ! -d "cmd/fush" ]; then
echo -e "${RED}error: Директория cmd/fush не найдена${NC}"
return 1
fi
# Проверяем существование main.go
if [ ! -f "cmd/fush/main.go" ]; then
echo -e "${RED}error: Файл cmd/fush/main.go не найден${NC}"
return 1
fi
# Создаем директорию для бинарных файлов
mkdir -p "$BUILD_DIR"
echo -e "${GREEN}✓ Директория ${BUILD_DIR} создана${NC}"
# Формируем флаги сборки
LDFLAGS="-X main.BuildTime=${BUILD_TIME} -X main.GitCommit=${GIT_COMMIT}"
# Выполняем сборку с явным указанием выходного файла
echo -e "${YELLOW}Выполнение: go build -v -ldflags=\"${LDFLAGS}\" -o ${BINARY_NAME} ./cmd/fush${NC}"
# Захватываем вывод команды и выводим в реальном времени
go build -v -ldflags="${LDFLAGS}" -o "${BINARY_NAME}" ./cmd/fush 2>&1 | while IFS= read -r line; do
if [[ "$line" == *"error"* ]] || [[ "$line" == *"Error"* ]] || [[ "$line" == *"undefined"* ]] || [[ "$line" == *"cannot"* ]]; then
echo -e "${RED}error: $line${NC}"
else
echo "$line"
fi
done
BUILD_EXIT_CODE=${PIPESTATUS[0]}
# Проверяем результат сборки
if [ $BUILD_EXIT_CODE -ne 0 ]; then
echo -e "${RED}error: Сборка завершилась с ошибкой (код: $BUILD_EXIT_CODE)${NC}"
return 1
fi
# Проверяем, создался ли бинарный файл
if [ -f "${BINARY_NAME}" ]; then
echo -e "${GREEN}✓ Сборка успешно завершена${NC}"
echo -e "${GREEN}✓ Исполняемый файл: ${BINARY_NAME}${NC}"
echo -e "${GREEN}✓ Полный путь: ${FUSH_DIR}/${BINARY_NAME}${NC}"
# Показываем размер бинарного файла
SIZE=$(ls -lh "${BINARY_NAME}" | awk '{print $5}')
echo -e "${GREEN}✓ Размер бинарного файла: ${SIZE}${NC}"
# Проверка прав доступа
if [ -x "${BINARY_NAME}" ]; then
echo -e "${GREEN}✓ Бинарный файл имеет права на выполнение${NC}"
else
echo -e "${YELLOW}Добавление прав на выполнение...${NC}"
chmod +x "${BINARY_NAME}"
echo -e "${GREEN}✓ Права на выполнение добавлены${NC}"
fi
# Копируем бинарный файл в корень проекта
echo -e "${YELLOW}Копирование бинарного файла в корень проекта...${NC}"
cp "${BINARY_NAME}" "${ROOT_BINARY_NAME}"
if [ -f "${ROOT_BINARY_NAME}" ]; then
chmod +x "${ROOT_BINARY_NAME}"
echo -e "${GREEN}✓ Бинарный файл скопирован в: ${ROOT_BINARY_NAME}${NC}"
else
echo -e "${RED}error: Не удалось скопировать бинарный файл в корень проекта${NC}"
return 1
fi
# Отображаем информацию о собранных файлах
echo ""
show_file_info "${BINARY_NAME}" "fush (в папке bin)"
echo ""
show_file_info "${ROOT_BINARY_NAME}" "fush (в корне проекта)"
return 0
else
echo -e "${RED}error: Бинарный файл не создан по пути: ${BINARY_NAME}${NC}"
echo -e "${RED}error: Содержимое директории ${BUILD_DIR}:${NC}"
ls -la "$BUILD_DIR" 2>/dev/null || echo "Директория пуста или не существует"
return 1
fi
}
# Функция очистки
clean_project() {
echo -e "${YELLOW}Очистка в ${FUSH_DIR}...${NC}"
if [ -d "$BUILD_DIR" ]; then
rm -rf "$BUILD_DIR"
echo -e "${GREEN}✓ Директория ${BUILD_DIR} удалена${NC}"
fi
if [ -f "$PROJECT_NAME" ]; then
rm -f "$PROJECT_NAME"
echo -e "${GREEN}✓ Файл ${PROJECT_NAME} удален${NC}"
fi
if [ -f "${ROOT_BINARY_NAME}" ]; then
rm -f "${ROOT_BINARY_NAME}"
echo -e "${GREEN}✓ Файл ${ROOT_BINARY_NAME} удален${NC}"
fi
echo -e "${GREEN}✓ Очистка завершена${NC}"
}
# Функция установки
install_project() {
echo -e "${YELLOW}Установка ${PROJECT_NAME}...${NC}"
# Проверяем, существует ли бинарный файл в корне
if [ ! -f "$ROOT_BINARY_NAME" ]; then
echo -e "${YELLOW}Бинарный файл не найден в корне. Выполняется сборка...${NC}"
build_project
if [ $? -ne 0 ]; then
echo -e "${RED}error: Ошибка сборки. Установка прервана.${NC}"
return 1
fi
fi
# Копируем в системную директорию
if [ -w "/usr/local/bin" ]; then
cp "$ROOT_BINARY_NAME" /usr/local/bin/
echo -e "${GREEN}✓ Установлен в /usr/local/bin/${PROJECT_NAME}${NC}"
else
echo -e "${YELLOW}Нет прав на запись в /usr/local/bin. Используем sudo...${NC}"
sudo cp "$ROOT_BINARY_NAME" /usr/local/bin/
if [ $? -eq 0 ]; then
echo -e "${GREEN}✓ Установлен в /usr/local/bin/${PROJECT_NAME}${NC}"
else
echo -e "${RED}error: Не удалось установить в /usr/local/bin/${PROJECT_NAME}${NC}"
return 1
fi
fi
# Создание директорий конфигурации
if [ ! -d ~/.config/fush ]; then
mkdir -p ~/.config/fush
if [ -f "config/fush.toml" ]; then
cp config/fush.toml ~/.config/fush/
echo -e "${GREEN}✓ Конфигурация скопирована в ~/.config/fush/${NC}"
fi
fi
# Создание директории для Lua скриптов
if [ ! -d ~/.local/share/fush/lua ]; then
mkdir -p ~/.local/share/fush/lua
if [ -f "lua/example.lua" ]; then
cp lua/example.lua ~/.local/share/fush/lua/
echo -e "${GREEN}✓ Пример Lua скрипта скопирован в ~/.local/share/fush/lua/${NC}"
fi
fi
echo -e "${GREEN}✓ Установка завершена${NC}"
}
# Функция запуска тестов
test_project() {
echo -e "${YELLOW}Запуск тестов в ${FUSH_DIR}...${NC}"
# Проверяем наличие go.mod
if [ ! -f "go.mod" ]; then
echo -e "${RED}error: Файл go.mod не найден${NC}"
return 1
fi
TEST_OUTPUT=$($GO test -v ./... 2>&1)
TEST_EXIT_CODE=$?
echo "$TEST_OUTPUT" | while IFS= read -r line; do
if [[ "$line" == *"FAIL"* ]] || [[ "$line" == *"error"* ]] || [[ "$line" == *"Error"* ]]; then
echo -e "${RED}$line${NC}"
elif [[ "$line" == *"PASS"* ]] || [[ "$line" == *"ok"* ]]; then
echo -e "${GREEN}$line${NC}"
else
echo "$line"
fi
done
if [ $TEST_EXIT_CODE -eq 0 ]; then
echo -e "${GREEN}Все тесты пройдены${NC}"
return 0
else
echo -e "${RED}error: Некоторые тесты не пройдены${NC}"
return 1
fi
}
# Функция запуска
run_project() {
echo -e "${YELLOW}Запуск ${PROJECT_NAME}...${NC}"
# Проверяем, существует ли бинарный файл в корне
if [ ! -f "$ROOT_BINARY_NAME" ]; then
echo -e "${YELLOW}Бинарный файл не найден. Выполняется сборка...${NC}"
build_project
if [ $? -ne 0 ]; then
echo -e "${RED}error: Ошибка сборки. Запуск невозможен.${NC}"
return 1
fi
fi
# Запускаем shell
"./$ROOT_BINARY_NAME"
}
# Функция полной сборки (очистка + сборка)
all_build() {
clean_project
install_deps
build_project
}
# Основная логика
main() {
echo -e "${CYAN}================================${NC}"
echo -e "${CYAN}fush Shell - Скрипт сборки${NC}"
echo -e "${CYAN}================================${NC}"
echo -e "${YELLOW}Директория проекта fush: ${FUSH_DIR}${NC}"
# Проверяем наличие файлов проекта
check_project_files
# Проверяем окружение
check_os
check_go_version
# Определяем команду
COMMAND="${1:-all}"
case "$COMMAND" in
all)
all_build
;;
build)
install_deps
build_project
;;
clean)
clean_project
;;
install)
install_deps
build_project
install_project
;;
test)
install_deps
test_project
;;
run)
install_deps
build_project
run_project
;;
help|--help|-h)
show_help
;;
*)
echo -e "${RED}error: Неизвестная команда: $COMMAND${NC}"
show_help
exit 1
;;
esac
if [ $? -eq 0 ] && [ "$COMMAND" != "help" ]; then
echo -e "${BLUE}================================${NC}"
echo -e "${GREEN}Операция '${COMMAND}' успешно завершена!${NC}"
echo -e "${BLUE}================================${NC}"
fi
}
# Запуск основной функции
main "$@"

90
cmd/fush/main.go Normal file
View File

@@ -0,0 +1,90 @@
// main.go - точка входа в приложение fush shell
// Загружает конфигурацию, инициализирует логгер и основные компоненты
// Обрабатывает системные сигналы для корректного завершения работы
// Содержит информацию о версии и времени сборки
package main
import (
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"fush/internal/config"
"fush/internal/logger"
"fush/internal/shell"
"fush/pkg/ansi"
)
var (
BuildTime = "unknown"
GitCommit = "unknown"
)
func main() {
// Обработка сигналов для корректного завершения
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Загрузка конфигурации
configPath := getConfigPath()
cfg, err := config.Load(configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Ошибка загрузки конфигурации: %v\n", err)
os.Exit(1)
}
// Инициализация логгера
log, err := logger.New(cfg.LogFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Ошибка инициализации логгера: %v\n", err)
os.Exit(1)
}
defer log.Close()
// Логирование запуска
log.Info("Запуск fush shell",
"version", GitCommit,
"build_time", BuildTime,
"os", cfg.OS)
// Вывод приветственного сообщения (цвет BrightCyan - такой же как в help)
fmt.Println()
ansi.Println(ansi.BrightCyan, "Welcome to fush shell")
fmt.Println()
// Создание и запуск shell
sh := shell.New(cfg, log)
// Запуск в отдельной горутине для обработки сигналов
go func() {
<-sigChan
log.Info("Получен сигнал завершения")
sh.Shutdown()
os.Exit(0)
}()
// Запуск основного цикла shell
if err := sh.Run(); err != nil {
log.Error("Ошибка выполнения shell", "error", err)
os.Exit(1)
}
}
// getConfigPath возвращает путь к файлу конфигурации
func getConfigPath() string {
// Приоритет: флаг командной строки, переменная окружения, стандартный путь
if path := os.Getenv("FUSH_CONFIG"); path != "" {
return path
}
home, err := os.Getenv("HOME"), error(nil)
if err != nil {
home = "."
}
// Стандартный путь: ~/.config/fush/fush.toml
return filepath.Join(home, ".config", "fush", "fush.toml")
}

22
config/fush.toml Normal file
View File

@@ -0,0 +1,22 @@
# Конфигурационный файл fush shell
# Приглашение командной строки
prompt = "fush:-> "
prompt_color = "#00bfff"
# Директории
lua_scripts_dir = "~/.local/share/fush/lua"
history_file = "~/.cache/fush/history"
log_file = "~/.cache/fush/fush.log"
# Размер истории команд
history_size = 1000
# Переменные окружения
[environment]
# Путь для поиска исполняемых файлов
PATH = "/usr/local/bin:/usr/bin:/bin"
# Домашняя директория
HOME = "~"
# Терминал по умолчанию
TERM = "xterm-256color"

25
fush.toml Normal file
View File

@@ -0,0 +1,25 @@
# fush.toml - конфигурационный файл fush shell
# Определяет приглашение командной строки и цветовые схемы
# Указывает пути для хранения истории, логов и Lua скриптов
# Содержит переменные окружения для работы shell
# Приглашение командной строки
prompt = "fush:-> "
prompt_color = "bright_cyan" # использовать именованный цвет
# Директории
lua_scripts_dir = "~/.local/share/fush/lua"
history_file = "~/.cache/fush/history"
log_file = "~/.cache/fush/fush.log"
# Размер истории команд
history_size = 1000
# Переменные окружения
[environment]
# Путь для поиска исполняемых файлов
PATH = "/usr/local/bin:/usr/bin:/bin"
# Домашняя директория
HOME = "~"
# Терминал по умолчанию
TERM = "xterm-256color"

70
gitignore.txt Normal file
View File

@@ -0,0 +1,70 @@
### **.gitignore**
```gitignore
# Binaries
fush
fush.exe
fush-*
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
# Go workspace files
*.mod
*.sum
go.work
go.work.sum
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Build artifacts
/dist/
/bin/
/build/
/tmp/
# Logs and data
*.log
*.pid
*.seed
*.pid.lock
*.history
# Configuration (user-specific)
.env
.env.local
.env.*.local
# Fush specific
.cache/
.local/
.config/
history
fush.log
# Test coverage
*.cover
*.coverprofile
coverage.html
coverage.out
# Debug
__debug_bin
debug
*.dwp
# OS generated
Thumbs.db
ehthumbs.db
Desktop.ini

11
go.mod Normal file
View File

@@ -0,0 +1,11 @@
module fush
go 1.26
require (
github.com/BurntSushi/toml v1.3.2
github.com/mattn/go-isatty v0.0.20
github.com/yuin/gopher-lua v1.1.1
)
require golang.org/x/sys v0.15.0 // indirect

9
go.sum Normal file
View File

@@ -0,0 +1,9 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

131
internal/config/config.go Normal file
View File

@@ -0,0 +1,131 @@
// config.go - управление конфигурацией fush shell
// Загружает и сохраняет настройки в формате TOML
// Предоставляет конфигурацию по умолчанию для новой установки
// Управляет переменными окружения и путями к директориям
package config
import (
"os"
"path/filepath"
"runtime"
"github.com/BurntSushi/toml"
)
// Config представляет структуру конфигурации fush
type Config struct {
// Общие настройки
Prompt string `toml:"prompt"`
PromptColor string `toml:"prompt_color"`
// Директории
LuaScriptsDir string `toml:"lua_scripts_dir"`
HistoryFile string `toml:"history_file"`
LogFile string `toml:"log_file"`
// Настройки истории
HistorySize int `toml:"history_size"`
// Переменные окружения
Environment map[string]string `toml:"environment"`
// ОС (определяется автоматически)
OS string `toml:"-"`
}
// DefaultConfig возвращает конфигурацию по умолчанию
func DefaultConfig() *Config {
home, _ := os.UserHomeDir()
cfg := &Config{
Prompt: "fush:-> ",
PromptColor: "#00bfff",
LuaScriptsDir: filepath.Join(home, ".local", "share", "fush", "lua"),
HistoryFile: filepath.Join(home, ".cache", "fush", "history"),
LogFile: filepath.Join(home, ".cache", "fush", "fush.log"),
HistorySize: 1000,
Environment: make(map[string]string),
}
// Определение ОС
cfg.OS = runtime.GOOS
if cfg.OS == "sunos" {
cfg.OS = "OpenIndiana"
}
// Установка переменных окружения по умолчанию
cfg.Environment["PATH"] = os.Getenv("PATH")
cfg.Environment["HOME"] = home
cfg.Environment["SHELL"] = os.Getenv("SHELL")
cfg.Environment["USER"] = os.Getenv("USER")
return cfg
}
// Load загружает конфигурацию из файла
func Load(path string) (*Config, error) {
cfg := DefaultConfig()
// Проверяем существование файла
if _, err := os.Stat(path); os.IsNotExist(err) {
// Создаем директорию для конфигурации
configDir := filepath.Dir(path)
if err := os.MkdirAll(configDir, 0755); err != nil {
return nil, err
}
// Сохраняем конфигурацию по умолчанию
if err := Save(cfg, path); err != nil {
return nil, err
}
return cfg, nil
}
// Загружаем конфигурацию
if _, err := toml.DecodeFile(path, cfg); err != nil {
return nil, err
}
// Создаем необходимые директории
if err := os.MkdirAll(filepath.Dir(cfg.HistoryFile), 0755); err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(cfg.LogFile), 0755); err != nil {
return nil, err
}
if err := os.MkdirAll(cfg.LuaScriptsDir, 0755); err != nil {
return nil, err
}
return cfg, nil
}
// Save сохраняет конфигурацию в файл
func Save(cfg *Config, path string) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
encoder := toml.NewEncoder(file)
return encoder.Encode(cfg)
}
// GetEnv возвращает значение переменной окружения
func (c *Config) GetEnv(key string) string {
if val, ok := c.Environment[key]; ok {
return val
}
return os.Getenv(key)
}
// SetEnv устанавливает переменную окружения
func (c *Config) SetEnv(key, value string) {
c.Environment[key] = value
os.Setenv(key, value)
}

175
internal/history/history.go Normal file
View File

@@ -0,0 +1,175 @@
// history.go - управление историей команд fush shell
// Хранит до 1000 последних команд в файле с атомарными операциями
// Обеспечивает навигацию по истории (вверх/вниз)
// Предотвращает дублирование последовательных одинаковых команд
package history
import (
"bufio"
"os"
"strings"
"sync/atomic"
"unsafe"
)
// History управляет историей команд
type History struct {
commands []string
position int
maxSize int
file string
loaded atomic.Bool
commandsPtr unsafe.Pointer // Атомарный указатель на слайс команд
}
// New создает новый объект истории
func New(historyFile string, maxSize int) *History {
h := &History{
commands: make([]string, 0, maxSize),
position: -1,
maxSize: maxSize,
file: historyFile,
}
// Инициализируем атомарный указатель
atomic.StorePointer(&h.commandsPtr, unsafe.Pointer(&h.commands))
// Загружаем историю из файла
h.load()
return h
}
// load загружает историю из файла
func (h *History) load() {
if h.loaded.Load() {
return
}
file, err := os.Open(h.file)
if err != nil {
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
commands := make([]string, 0, h.maxSize)
for scanner.Scan() {
cmd := strings.TrimSpace(scanner.Text())
if cmd != "" {
commands = append(commands, cmd)
}
}
// Ограничиваем размер
if len(commands) > h.maxSize {
commands = commands[len(commands)-h.maxSize:]
}
// Атомарно обновляем команды
h.commands = commands
atomic.StorePointer(&h.commandsPtr, unsafe.Pointer(&h.commands))
h.loaded.Store(true)
}
// Add добавляет команду в историю
func (h *History) Add(command string) {
if command == "" {
return
}
// Получаем текущие команды
oldCommands := h.getCommands()
// Не добавляем дубликаты с предыдущей командой
if len(oldCommands) > 0 && oldCommands[len(oldCommands)-1] == command {
return
}
// Создаем новый слайс
newCommands := make([]string, 0, h.maxSize)
newCommands = append(newCommands, oldCommands...)
newCommands = append(newCommands, command)
// Ограничиваем размер
if len(newCommands) > h.maxSize {
newCommands = newCommands[len(newCommands)-h.maxSize:]
}
// Атомарно обновляем
h.commands = newCommands
atomic.StorePointer(&h.commandsPtr, unsafe.Pointer(&h.commands))
// Сохраняем в файл
h.save()
}
// save сохраняет историю в файл
func (h *History) save() {
file, err := os.OpenFile(h.file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return
}
defer file.Close()
writer := bufio.NewWriter(file)
for _, cmd := range h.getCommands() {
writer.WriteString(cmd + "\n")
}
writer.Flush()
}
// getCommands атомарно возвращает текущий список команд
func (h *History) getCommands() []string {
ptr := atomic.LoadPointer(&h.commandsPtr)
return *(*[]string)(ptr)
}
// Previous возвращает предыдущую команду
func (h *History) Previous() string {
commands := h.getCommands()
if len(commands) == 0 {
return ""
}
if h.position == -1 {
h.position = len(commands) - 1
} else if h.position > 0 {
h.position--
}
if h.position >= 0 && h.position < len(commands) {
return commands[h.position]
}
return ""
}
// Next возвращает следующую команду
func (h *History) Next() string {
commands := h.getCommands()
if len(commands) == 0 {
return ""
}
if h.position < len(commands)-1 {
h.position++
return commands[h.position]
} else if h.position == len(commands)-1 {
h.position = -1
}
return ""
}
// ResetPosition сбрасывает позицию в истории
func (h *History) ResetPosition() {
h.position = -1
}
// GetSize возвращает размер истории
func (h *History) GetSize() int {
return len(h.getCommands())
}

110
internal/logger/logger.go Normal file
View File

@@ -0,0 +1,110 @@
// logger.go - система логирования для fush shell
// Записывает события четырех уровней: DEBUG, INFO, WARN, ERROR
// Использует атомарные операции для потокобезопасности
// Обеспечивает принудительную запись на диск после каждого сообщения
package logger
import (
"fmt"
"os"
"sync/atomic"
"time"
)
// LogLevel определяет уровень логирования
type LogLevel int
const (
LevelDebug LogLevel = iota
LevelInfo
LevelWarn
LevelError
)
// Logger представляет структуру логгера
type Logger struct {
file *os.File
level LogLevel
closed atomic.Bool
}
// New создает новый логгер
func New(logPath string) (*Logger, error) {
// Открываем файл для добавления записей
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
return &Logger{
file: file,
level: LevelInfo,
}, nil
}
// Close закрывает логгер
func (l *Logger) Close() error {
if l.closed.Load() {
return nil
}
l.closed.Store(true)
return l.file.Close()
}
// log записывает сообщение в лог
func (l *Logger) log(level LogLevel, format string, args ...interface{}) {
if l.closed.Load() {
return
}
if level < l.level {
return
}
levelStr := map[LogLevel]string{
LevelDebug: "DEBUG",
LevelInfo: "INFO",
LevelWarn: "WARN",
LevelError: "ERROR",
}[level]
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
message := fmt.Sprintf(format, args...)
logLine := fmt.Sprintf("[%s] [%s] %s\n", timestamp, levelStr, message)
// Атомарная запись в файл
_, err := l.file.WriteString(logLine)
if err != nil {
fmt.Fprintf(os.Stderr, "Ошибка записи лога: %v\n", err)
}
// Принудительная запись на диск
l.file.Sync()
}
// Debug записывает отладочное сообщение
func (l *Logger) Debug(format string, args ...interface{}) {
l.log(LevelDebug, format, args...)
}
// Info записывает информационное сообщение
func (l *Logger) Info(format string, args ...interface{}) {
l.log(LevelInfo, format, args...)
}
// Warn записывает предупреждение
func (l *Logger) Warn(format string, args ...interface{}) {
l.log(LevelWarn, format, args...)
}
// Error записывает ошибку
func (l *Logger) Error(format string, args ...interface{}) {
l.log(LevelError, format, args...)
}
// SetLevel устанавливает уровень логирования
func (l *Logger) SetLevel(level LogLevel) {
l.level = level
}

View File

@@ -0,0 +1,308 @@
// luabridge.go - интеграция Lua как скриптового языка в fush shell
// Предоставляет API для вызова команд shell из Lua скриптов
// Регистрирует функции exec, pipe, ls, cd, pwd в окружении Lua
// Позволяет создавать сложные скрипты для автоматизации задач
package luabridge
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync/atomic"
lua "github.com/yuin/gopher-lua"
)
// ShellInterface определяет интерфейс для взаимодействия с shell
type ShellInterface interface {
GetEnv(key string) string
SetEnv(key, value string)
}
// Bridge управляет выполнением Lua скриптов
type Bridge struct {
scriptsDir string
shell ShellInterface
state *lua.LState
closed atomic.Bool
}
// New создает новый Lua мост
func New(scriptsDir string, shell ShellInterface) *Bridge {
b := &Bridge{
scriptsDir: scriptsDir,
shell: shell,
}
// Создаем Lua состояние
b.state = lua.NewState()
// Регистрируем функции для Lua
b.registerFunctions()
return b
}
// registerFunctions регистрирует функции для Lua
func (b *Bridge) registerFunctions() {
// Регистрируем функцию print
b.state.SetGlobal("print", b.state.NewFunction(func(L *lua.LState) int {
args := make([]interface{}, L.GetTop())
for i := 1; i <= L.GetTop(); i++ {
args[i-1] = L.Get(i)
}
fmt.Println(args...)
return 0
}))
// Регистрируем функцию getenv
b.state.SetGlobal("getenv", b.state.NewFunction(func(L *lua.LState) int {
key := L.CheckString(1)
value := b.shell.GetEnv(key)
L.Push(lua.LString(value))
return 1
}))
// Регистрируем функцию setenv
b.state.SetGlobal("setenv", b.state.NewFunction(func(L *lua.LState) int {
key := L.CheckString(1)
value := L.CheckString(2)
b.shell.SetEnv(key, value)
return 0
}))
// Регистрируем функцию exec для выполнения команд из Lua
b.state.SetGlobal("exec", b.state.NewFunction(func(L *lua.LState) int {
cmd := L.CheckString(1)
args := make([]string, 0)
// Собираем аргументы
for i := 2; i <= L.GetTop(); i++ {
args = append(args, L.CheckString(i))
}
// Выполняем команду
externalCmd := exec.Command(cmd, args...)
externalCmd.Stdout = os.Stdout
externalCmd.Stderr = os.Stderr
err := externalCmd.Run()
if err != nil {
L.Push(lua.LBool(false))
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(lua.LBool(true))
return 1
}))
// Регистрируем функцию exec_output для получения вывода команды
b.state.SetGlobal("exec_output", b.state.NewFunction(func(L *lua.LState) int {
cmd := L.CheckString(1)
args := make([]string, 0)
for i := 2; i <= L.GetTop(); i++ {
args = append(args, L.CheckString(i))
}
externalCmd := exec.Command(cmd, args...)
output, err := externalCmd.Output()
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(lua.LString(string(output)))
return 1
}))
// Регистрируем функцию pipe для выполнения команд с пайпом
b.state.SetGlobal("pipe", b.state.NewFunction(func(L *lua.LState) int {
cmd1 := L.CheckString(1)
cmd2 := L.CheckString(2)
// Разбиваем команды на части
parts1 := strings.Fields(cmd1)
parts2 := strings.Fields(cmd2)
if len(parts1) == 0 || len(parts2) == 0 {
L.Push(lua.LBool(false))
L.Push(lua.LString("пустая команда"))
return 2
}
// Создаем команды
cmd1Exec := exec.Command(parts1[0], parts1[1:]...)
cmd2Exec := exec.Command(parts2[0], parts2[1:]...)
// Создаем пайп
stdout, err := cmd1Exec.StdoutPipe()
if err != nil {
L.Push(lua.LBool(false))
L.Push(lua.LString(err.Error()))
return 2
}
cmd2Exec.Stdin = stdout
cmd2Exec.Stdout = os.Stdout
cmd2Exec.Stderr = os.Stderr
// Запускаем команды
if err := cmd1Exec.Start(); err != nil {
L.Push(lua.LBool(false))
L.Push(lua.LString(err.Error()))
return 2
}
if err := cmd2Exec.Start(); err != nil {
L.Push(lua.LBool(false))
L.Push(lua.LString(err.Error()))
return 2
}
// Ожидаем завершения
cmd1Exec.Wait()
cmd2Exec.Wait()
L.Push(lua.LBool(true))
return 1
}))
// Регистрируем функцию ls
b.state.SetGlobal("ls", b.state.NewFunction(func(L *lua.LState) int {
path := "."
if L.GetTop() > 0 {
path = L.CheckString(1)
}
dir, err := os.Open(path)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
defer dir.Close()
files, err := dir.Readdir(-1)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
// Создаем таблицу с результатами
tbl := L.NewTable()
for _, file := range files {
tbl.Append(lua.LString(file.Name()))
}
L.Push(tbl)
return 1
}))
// Регистрируем функцию cd
b.state.SetGlobal("cd", b.state.NewFunction(func(L *lua.LState) int {
path := L.GetTop() > 0
if !path {
path = true
// Передаем true, но нужен путь
_ = path
}
// Простая реализация cd через shell
dir := b.shell.GetEnv("HOME")
if L.GetTop() > 0 {
dir = L.CheckString(1)
}
if err := os.Chdir(dir); err != nil {
L.Push(lua.LBool(false))
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(lua.LBool(true))
return 1
}))
// Регистрируем функцию pwd
b.state.SetGlobal("pwd", b.state.NewFunction(func(L *lua.LState) int {
dir, err := os.Getwd()
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(lua.LString(dir))
return 1
}))
}
// ExecuteScript выполняет Lua скрипт
func (b *Bridge) ExecuteScript(name string, args []string) error {
if b.closed.Load() {
return fmt.Errorf("мост закрыт")
}
// Формируем путь к скрипту
scriptPath := filepath.Join(b.scriptsDir, name+".lua")
// Проверяем существование файла
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
return err
}
// Загружаем и выполняем скрипт
if err := b.state.DoFile(scriptPath); err != nil {
return err
}
// Вызываем функцию main если она существует
if fn := b.state.GetGlobal("main"); fn.Type() == lua.LTFunction {
// Преобразуем аргументы в Lua значения
luaArgs := make([]lua.LValue, len(args))
for i, arg := range args {
luaArgs[i] = lua.LString(arg)
}
if err := b.state.CallByParam(lua.P{
Fn: fn,
NRet: 0,
Protect: true,
}, luaArgs...); err != nil {
return err
}
}
return nil
}
// ExecuteString выполняет Lua код из строки
func (b *Bridge) ExecuteString(code string) error {
if b.closed.Load() {
return fmt.Errorf("мост закрыт")
}
return b.state.DoString(code)
}
// GetState возвращает Lua состояние
func (b *Bridge) GetState() *lua.LState {
return b.state
}
// Close закрывает Lua состояние
func (b *Bridge) Close() {
if b.closed.Load() {
return
}
b.closed.Store(true)
if b.state != nil {
b.state.Close()
}
}

205
internal/shell/commands.go Normal file
View File

@@ -0,0 +1,205 @@
// commands.go - встроенные команды fush shell
// Реализует базовые команды: exit, ls, cd, mkdir, rm, touch, exec, help
// Обеспечивает навигацию по файловой системе и управление файлами
// Служит основой для расширения функциональности shell
package shell
import (
"fmt"
"os"
"time"
"fush/pkg/ansi"
)
// cmdExit обрабатывает команду exit
func (s *Shell) cmdExit(args []string) error {
s.running.Store(false)
s.logger.Info("Выполнена команда exit")
return nil
}
// cmdHelp обрабатывает команду help - выводит список доступных команд
func (s *Shell) cmdHelp(args []string) error {
fmt.Println()
ansi.Println(ansi.Cyan, "╔══════════════════════════════════════════════════════════════╗")
ansi.Println(ansi.Cyan, "║ fush shell - Доступные команды ║")
ansi.Println(ansi.Cyan, "╚══════════════════════════════════════════════════════════════╝")
fmt.Println()
// Внутренние команды
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
ansi.Println(ansi.BrightGreen, "ВСТРОЕННЫЕ КОМАНДЫ:")
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Printf(" %-15s %s\n", ansi.Colorize("exit", ansi.BrightWhite), "Выход из оболочки")
fmt.Printf(" %-15s %s\n", ansi.Colorize("help", ansi.BrightWhite), "Показать эту справку")
fmt.Printf(" %-15s %s\n", ansi.Colorize("ls [path]", ansi.BrightWhite), "Вывести список файлов в директории")
fmt.Printf(" %-15s %s\n", ansi.Colorize("cd [dir]", ansi.BrightWhite), "Сменить текущую директорию")
fmt.Printf(" %-15s %s\n", ansi.Colorize("mkdir <dir>", ansi.BrightWhite), "Создать новую директорию")
fmt.Printf(" %-15s %s\n", ansi.Colorize("rm <file>", ansi.BrightWhite), "Удалить файл или директорию")
fmt.Printf(" %-15s %s\n", ansi.Colorize("touch <file>", ansi.BrightWhite), "Создать файл или обновить время доступа")
fmt.Printf(" %-15s %s\n", ansi.Colorize("exec <cmd> [args...]", ansi.BrightWhite), "Выполнить внешнюю команду")
fmt.Println()
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
ansi.Println(ansi.BrightGreen, "ВНЕШНИЕ КОМАНДЫ:")
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Printf(" %-15s %s\n", ansi.Colorize("<program>", ansi.BrightWhite), "Запуск любого внешнего приложения")
fmt.Printf(" %-15s %s\n", ansi.Colorize("<lua_script>", ansi.BrightWhite), "Выполнение Lua скрипта из директории ~/.local/share/fush/lua/")
fmt.Println()
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
ansi.Println(ansi.BrightGreen, "СПЕЦИАЛЬНЫЕ ВОЗМОЖНОСТИ:")
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Printf(" %-20s %s\n", ansi.Colorize("cmd1 | cmd2", ansi.BrightWhite), "Пайплайн (канал) между командами")
fmt.Printf(" %-20s %s\n", ansi.Colorize("cmd > file", ansi.BrightWhite), "Перенаправление вывода в файл (перезапись)")
fmt.Printf(" %-20s %s\n", ansi.Colorize("cmd >> file", ansi.BrightWhite), "Перенаправление вывода в файл (добавление)")
fmt.Printf(" %-20s %s\n", ansi.Colorize("Lua API", ansi.BrightWhite), "Встроенный интерпретатор Lua с функциями: exec(), ls(), cd(), pwd()")
fmt.Println()
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
ansi.Println(ansi.BrightGreen, "ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:")
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
ansi.Println(ansi.BrightCyan, "ВСТРОЕННЫЕ КОМАНДЫ:")
fmt.Printf(" %-40s %s\n", ansi.Colorize("ls -la", ansi.BrightWhite), "Показать все файлы в текущей директории")
fmt.Printf(" %-40s %s\n", ansi.Colorize("cd /home/user", ansi.BrightWhite), "Перейти в директорию пользователя")
fmt.Printf(" %-40s %s\n", ansi.Colorize("mkdir mydir", ansi.BrightWhite), "Создать директорию 'mydir'")
fmt.Printf(" %-40s %s\n", ansi.Colorize("rm myfile.txt", ansi.BrightWhite), "Удалить файл 'myfile.txt'")
fmt.Printf(" %-40s %s\n", ansi.Colorize("touch newfile.txt", ansi.BrightWhite), "Создать файл 'newfile.txt'")
fmt.Printf(" %-40s %s\n", ansi.Colorize("exec go version", ansi.BrightWhite), "Выполнить команду 'go version'")
fmt.Println()
ansi.Println(ansi.BrightCyan, "ВНЕШНИЕ КОМАНДЫ (СИСТЕМНЫЕ УТИЛИТЫ):")
fmt.Printf(" %-40s %s\n", ansi.Colorize("go version", ansi.BrightWhite), "Показать версию Go (если установлен)")
fmt.Printf(" %-40s %s\n", ansi.Colorize("python --version", ansi.BrightWhite), "Показать версию Python (если установлен)")
fmt.Printf(" %-40s %s\n", ansi.Colorize("gcc --version", ansi.BrightWhite), "Показать версию GCC (если установлен)")
fmt.Printf(" %-40s %s\n", ansi.Colorize("date", ansi.BrightWhite), "Показать текущую дату и время")
fmt.Printf(" %-40s %s\n", ansi.Colorize("whoami", ansi.BrightWhite), "Показать имя текущего пользователя")
fmt.Printf(" %-40s %s\n", ansi.Colorize("pwd", ansi.BrightWhite), "Показать текущую рабочую директорию")
fmt.Printf(" %-40s %s\n", ansi.Colorize("echo Hello World", ansi.BrightWhite), "Вывести текст на экран")
fmt.Printf(" %-40s %s\n", ansi.Colorize("cat file.txt", ansi.BrightWhite), "Показать содержимое файла")
fmt.Printf(" %-40s %s\n", ansi.Colorize("grep pattern file.txt", ansi.BrightWhite), "Поиск строк по шаблону в файле")
fmt.Println()
ansi.Println(ansi.BrightCyan, "КОМАНДЫ ДЛЯ РАБОТЫ С ПАЙПЛАЙНАМИ (КАНАЛАМИ):")
fmt.Printf(" %-40s %s\n", ansi.Colorize("ls -la | grep .go", ansi.BrightWhite), "Показать только Go файлы в текущей директории")
fmt.Printf(" %-40s %s\n", ansi.Colorize("ps aux | grep python", ansi.BrightWhite), "Найти процессы Python")
fmt.Printf(" %-40s %s\n", ansi.Colorize("cat file.txt | wc -l", ansi.BrightWhite), "Подсчитать количество строк в файле")
fmt.Printf(" %-40s %s\n", ansi.Colorize("ls | sort | uniq", ansi.BrightWhite), "Отсортировать и убрать дубликаты")
fmt.Println()
ansi.Println(ansi.BrightCyan, "ПЕРЕНАПРАВЛЕНИЕ ВЫВОДА:")
fmt.Printf(" %-40s %s\n", ansi.Colorize("ls > files.txt", ansi.BrightWhite), "Сохранить список файлов в files.txt (перезапись)")
fmt.Printf(" %-40s %s\n", ansi.Colorize("echo Hello >> hello.txt", ansi.BrightWhite), "Добавить текст в конец файла")
fmt.Printf(" %-40s %s\n", ansi.Colorize("go version > version.txt", ansi.BrightWhite), "Сохранить версию Go в файл")
fmt.Println()
ansi.Println(ansi.BrightCyan, "LUA СКРИПТЫ:")
fmt.Printf(" %-40s %s\n", ansi.Colorize("example.lua", ansi.BrightWhite), "Выполнить Lua скрипт из директории ~/.local/share/fush/lua/")
fmt.Printf(" %-40s %s\n", ansi.Colorize("my_script.lua arg1 arg2", ansi.BrightWhite), "Выполнить Lua скрипт с аргументами")
fmt.Println()
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
ansi.Println(ansi.BrightCyan, "Для выхода из оболочки введите 'exit' или нажмите Ctrl+C")
fmt.Println()
return nil
}
// cmdLs обрабатывает команду ls
func (s *Shell) cmdLs(args []string) error {
// Определяем путь
path := "."
if len(args) > 0 {
path = args[0]
}
// Открываем директорию
dir, err := os.Open(path)
if err != nil {
return err
}
defer dir.Close()
// Читаем содержимое
files, err := dir.Readdir(-1)
if err != nil {
return err
}
// Выводим список
for _, file := range files {
name := file.Name()
if file.IsDir() {
name = name + "/"
}
fmt.Println(name)
}
return nil
}
// cmdCd обрабатывает команду cd
func (s *Shell) cmdCd(args []string) error {
// Определяем путь
path := s.GetEnv("HOME")
if len(args) > 0 {
path = args[0]
}
// Меняем директорию
if err := os.Chdir(path); err != nil {
return err
}
// Обновляем PWD
pwd, err := os.Getwd()
if err == nil {
s.SetEnv("PWD", pwd)
}
return nil
}
// cmdMkdir обрабатывает команду mkdir
func (s *Shell) cmdMkdir(args []string) error {
if len(args) == 0 {
return fmt.Errorf("требуется имя директории")
}
// Создаем директорию
return os.MkdirAll(args[0], 0755)
}
// cmdRm обрабатывает команду rm
func (s *Shell) cmdRm(args []string) error {
if len(args) == 0 {
return fmt.Errorf("требуется имя файла")
}
// Удаляем файл или директорию
return os.RemoveAll(args[0])
}
// cmdTouch обрабатывает команду touch
func (s *Shell) cmdTouch(args []string) error {
if len(args) == 0 {
return fmt.Errorf("требуется имя файла")
}
// Создаем файл или обновляем время модификации
file, err := os.OpenFile(args[0], os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()
// Обновляем время модификации
now := time.Now()
return os.Chtimes(args[0], now, now)
}

343
internal/shell/shell.go Normal file
View File

@@ -0,0 +1,343 @@
// shell.go - основной цикл командного интерпретатора fush
// Обрабатывает ввод пользователя, историю команд и выполнение
// Поддерживает пайпы (|) и перенаправление вывода (>, >>)
// Интегрирует Lua мост для выполнения скриптов
package shell
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync/atomic"
"fush/internal/config"
"fush/internal/history"
"fush/internal/logger"
"fush/internal/luabridge"
"fush/pkg/ansi"
)
// Shell представляет основную структуру командного интерпретатора
type Shell struct {
config *config.Config
logger *logger.Logger
history *history.History
luaBridge *luabridge.Bridge
running atomic.Bool
env map[string]string
}
// PipeCommand представляет команду в пайплайне
type PipeCommand struct {
Cmd string
Args []string
}
// New создает новый экземпляр shell
func New(cfg *config.Config, log *logger.Logger) *Shell {
sh := &Shell{
config: cfg,
logger: log,
env: make(map[string]string),
}
// Копируем переменные окружения из конфигурации
for k, v := range cfg.Environment {
sh.env[k] = v
}
// Инициализируем историю
sh.history = history.New(cfg.HistoryFile, cfg.HistorySize)
// Инициализируем Lua мост
sh.luaBridge = luabridge.New(cfg.LuaScriptsDir, sh)
return sh
}
// Run запускает основной цикл shell
func (s *Shell) Run() error {
s.running.Store(true)
s.logger.Info("Запуск основного цикла shell")
// Получаем цвет приглашения
//promptColor, err := ansi.HexToANSI(s.config.PromptColor)
//if err != nil {
// promptColor = ansi.Cyan
//}
promptColor := ansi.BrightCyan // Принудительно установить яркий циан
// Создаем читатель stdin
reader := bufio.NewReader(os.Stdin)
for s.running.Load() {
// Выводим приглашение
ansi.Printf(promptColor, s.config.Prompt)
// Читаем команду
input, err := reader.ReadString('\n')
if err != nil {
if err.Error() == "EOF" {
break
}
s.logger.Error("Ошибка чтения ввода", "error", err)
continue
}
// Обрабатываем команду
input = strings.TrimSpace(input)
if input == "" {
continue
}
// Добавляем в историю
s.history.Add(input)
// Обрабатываем команду
if err := s.executeCommand(input); err != nil {
ansi.Printf(ansi.Red, "Ошибка: %v\n", err)
s.logger.Error("Ошибка выполнения команды", "command", input, "error", err)
}
}
return nil
}
// executeCommand выполняет команду
func (s *Shell) executeCommand(input string) error {
// Проверяем наличие пайпов (каналов)
if strings.Contains(input, "|") {
return s.executePipeline(input)
}
// Проверяем перенаправление вывода
var outputFile string
var appendMode bool
if strings.Contains(input, ">>") {
parts := strings.SplitN(input, ">>", 2)
input = strings.TrimSpace(parts[0])
outputFile = strings.TrimSpace(parts[1])
appendMode = true
} else if strings.Contains(input, ">") {
parts := strings.SplitN(input, ">", 2)
input = strings.TrimSpace(parts[0])
outputFile = strings.TrimSpace(parts[1])
appendMode = false
}
// Разбиваем на команду и аргументы
parts := strings.Fields(input)
if len(parts) == 0 {
return nil
}
cmd := parts[0]
args := parts[1:]
var err error
// Проверяем внутренние команды
switch cmd {
case "exit":
err = s.cmdExit(args)
case "help":
err = s.cmdHelp(args)
case "ls":
err = s.cmdLs(args)
case "cd":
err = s.cmdCd(args)
case "mkdir":
err = s.cmdMkdir(args)
case "rm":
err = s.cmdRm(args)
case "touch":
err = s.cmdTouch(args)
case "exec":
err = s.cmdExec(args)
default:
// Пытаемся выполнить как Lua скрипт
err = s.luaBridge.ExecuteScript(cmd, args)
if err != nil {
// Пытаемся выполнить как внешнюю команду
err = s.executeExternal(cmd, args, nil, nil, nil)
}
}
// Обрабатываем перенаправление вывода если нужно
if err == nil && outputFile != "" {
// Для внутренних команд вывод уже был,
// для внешних перенаправление обрабатывается в executeExternal
_ = appendMode // Подавляем предупреждение о неиспользуемой переменной
}
return err
}
// executePipeline выполняет цепочку команд с пайпами
func (s *Shell) executePipeline(input string) error {
// Разбиваем на команды
cmdStrs := strings.Split(input, "|")
commands := make([]*exec.Cmd, 0, len(cmdStrs))
// Парсим каждую команду
for _, cmdStr := range cmdStrs {
cmdStr = strings.TrimSpace(cmdStr)
if cmdStr == "" {
return fmt.Errorf("пустая команда в пайплайне")
}
parts := strings.Fields(cmdStr)
if len(parts) == 0 {
return fmt.Errorf("пустая команда в пайплайне")
}
// Проверяем внутренние команды
switch parts[0] {
case "exit", "cd", "exec", "help":
return fmt.Errorf("команда '%s' не может использоваться в пайплайне", parts[0])
case "ls", "mkdir", "rm", "touch":
// Для внутренних команд используем внешний вызов
cmd := exec.Command(parts[0], parts[1:]...)
s.setCommandEnv(cmd)
commands = append(commands, cmd)
default:
// Внешняя команда
cmd := exec.Command(parts[0], parts[1:]...)
s.setCommandEnv(cmd)
commands = append(commands, cmd)
}
}
if len(commands) == 0 {
return fmt.Errorf("нет команд для выполнения")
}
// Создаем пайпы между командами
for i := 0; i < len(commands)-1; i++ {
stdout, err := commands[i].StdoutPipe()
if err != nil {
return fmt.Errorf("ошибка создания пайпа: %v", err)
}
commands[i+1].Stdin = stdout
}
// Последняя команда пишет в stdout
commands[len(commands)-1].Stdout = os.Stdout
commands[len(commands)-1].Stderr = os.Stderr
// Первая команда читает из stdin если нужно
if commands[0].Stdin == nil {
commands[0].Stdin = os.Stdin
}
// Запускаем все команды
for idx, cmd := range commands {
if err := cmd.Start(); err != nil {
return fmt.Errorf("ошибка запуска команды %d: %v", idx, err)
}
}
// Ожидаем завершения всех команд
for idx, cmd := range commands {
if err := cmd.Wait(); err != nil {
s.logger.Error("Ошибка выполнения команды в пайплайне", "index", idx, "error", err)
}
}
return nil
}
// cmdExec выполняет команду exec для запуска приложений
func (s *Shell) cmdExec(args []string) error {
if len(args) == 0 {
return fmt.Errorf("требуется команда для выполнения")
}
cmd := args[0]
cmdArgs := args[1:]
// Создаем exec.Command
externalCmd := exec.Command(cmd, cmdArgs...)
// Настраиваем окружение
s.setCommandEnv(externalCmd)
// Перенаправляем вывод
externalCmd.Stdout = os.Stdout
externalCmd.Stderr = os.Stderr
externalCmd.Stdin = os.Stdin
// Выполняем команду
err := externalCmd.Run()
if err != nil {
return fmt.Errorf("ошибка выполнения '%s': %v", cmd, err)
}
return nil
}
// executeExternal выполняет внешнюю команду с поддержкой перенаправления
func (s *Shell) executeExternal(cmd string, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
// Создаем команду
externalCmd := exec.Command(cmd, args...)
// Настраиваем окружение
s.setCommandEnv(externalCmd)
// Перенаправляем вывод
if stdin != nil {
externalCmd.Stdin = stdin
} else {
externalCmd.Stdin = os.Stdin
}
if stdout != nil {
externalCmd.Stdout = stdout
} else {
externalCmd.Stdout = os.Stdout
}
if stderr != nil {
externalCmd.Stderr = stderr
} else {
externalCmd.Stderr = os.Stderr
}
// Выполняем
return externalCmd.Run()
}
// setCommandEnv устанавливает переменные окружения для команды
func (s *Shell) setCommandEnv(cmd *exec.Cmd) {
cmd.Env = os.Environ()
for k, v := range s.env {
cmd.Env = append(cmd.Env, k+"="+v)
}
}
// Shutdown завершает работу shell
func (s *Shell) Shutdown() {
s.logger.Info("Завершение работы shell")
s.running.Store(false)
s.luaBridge.Close()
}
// GetEnv возвращает переменную окружения
func (s *Shell) GetEnv(key string) string {
if val, ok := s.env[key]; ok {
return val
}
return os.Getenv(key)
}
// SetEnv устанавливает переменную окружения
func (s *Shell) SetEnv(key, value string) {
s.env[key] = value
os.Setenv(key, value)
s.logger.Debug("Установлена переменная окружения", "key", key, "value", value)
}

30
lua/example.lua Normal file
View File

@@ -0,0 +1,30 @@
-- Пример Lua скрипта для fush shell
-- Сохраните этот файл в директории lua_scripts_dir
-- Функция main будет вызвана при выполнении скрипта
function main(...)
local args = {...}
print("Hello from Lua script!")
print("Arguments:", table.concat(args, " "))
-- Получаем переменные окружения
local user = getenv("USER")
local home = getenv("HOME")
print("User:", user)
print("Home:", home)
-- Устанавливаем переменную окружения
setenv("LUA_TEST", "success")
-- Выполняем внешнюю команду (если нужно)
-- exec("echo 'External command executed from Lua'")
return 0
end
-- Дополнительные функции могут быть определены здесь
function help()
print("This is an example Lua script for fush shell")
print("Usage: example [arguments...]")
end

168
pkg/ansi/colors.go Normal file
View File

@@ -0,0 +1,168 @@
// colors.go - система цветного вывода для fush shell
// Преобразует hex цвета в ANSI escape последовательности
// Поддерживает 256-цветную палитру для совместимости с терминалами
// Автоматически отключает цвета при выводе не в терминал
package ansi
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/mattn/go-isatty"
)
// Color представляет ANSI цвет
type Color struct {
code string
}
// Основные цвета
var (
Reset = Color{"\033[0m"}
Bold = Color{"\033[1m"}
Dim = Color{"\033[2m"}
Italic = Color{"\033[3m"}
Underline = Color{"\033[4m"}
Black = Color{"\033[30m"}
Red = Color{"\033[31m"}
Green = Color{"\033[32m"}
Yellow = Color{"\033[33m"}
Blue = Color{"\033[34m"}
Magenta = Color{"\033[35m"}
Cyan = Color{"\033[36m"}
White = Color{"\033[37m"}
BrightBlack = Color{"\033[90m"}
BrightRed = Color{"\033[91m"}
BrightGreen = Color{"\033[92m"}
BrightYellow = Color{"\033[93m"}
BrightBlue = Color{"\033[94m"}
BrightMagenta = Color{"\033[95m"}
BrightCyan = Color{"\033[96m"}
BrightWhite = Color{"\033[97m"}
)
// HexToANSI преобразует hex цвет в ANSI код
func HexToANSI(hex string) (Color, error) {
// Удаляем # если есть
hex = strings.TrimPrefix(hex, "#")
if len(hex) != 6 && len(hex) != 3 {
return Color{}, fmt.Errorf("неверный формат hex цвета: %s", hex)
}
// Обработка 3-символьных hex кодов (например, #0FF)
if len(hex) == 3 {
hex = string([]byte{hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]})
}
// Парсим RGB компоненты
r, err := strconv.ParseInt(hex[0:2], 16, 64)
if err != nil {
return Color{}, err
}
g, err := strconv.ParseInt(hex[2:4], 16, 64)
if err != nil {
return Color{}, err
}
b, err := strconv.ParseInt(hex[4:6], 16, 64)
if err != nil {
return Color{}, err
}
// Определяем, является ли цвет одним из стандартных
if r == 0 && g == 255 && b == 255 {
return BrightCyan, nil // #00ffff -> BrightCyan
}
if r == 255 && g == 255 && b == 0 {
return BrightYellow, nil // #ffff00 -> BrightYellow
}
if r == 0 && g == 255 && b == 0 {
return BrightGreen, nil // #00ff00 -> BrightGreen
}
if r == 255 && g == 0 && b == 0 {
return BrightRed, nil // #ff0000 -> BrightRed
}
if r == 0 && g == 0 && b == 255 {
return BrightBlue, nil // #0000ff -> BrightBlue
}
if r == 255 && g == 0 && b == 255 {
return BrightMagenta, nil // #ff00ff -> BrightMagenta
}
if r == 255 && g == 255 && b == 255 {
return BrightWhite, nil // #ffffff -> BrightWhite
}
if r == 0 && g == 0 && b == 0 {
return Black, nil // #000000 -> Black
}
// Используем 256-цветную палитру для других цветов
// Формула: 16 + 36*r + 6*g + b, где r,g,b в диапазоне 0-5
rIdx := int(r * 5 / 255)
gIdx := int(g * 5 / 255)
bIdx := int(b * 5 / 255)
colorCode := 16 + (36 * rIdx) + (6 * gIdx) + bIdx
return Color{fmt.Sprintf("\033[38;5;%dm", colorCode)}, nil
}
// Colorize окрашивает строку в указанный цвет
func Colorize(s string, color Color) string {
if !IsTerminalSupported() {
return s
}
return color.code + s + Reset.code
}
// Sprintf форматирует строку с цветом
func Sprintf(color Color, format string, args ...interface{}) string {
if !IsTerminalSupported() {
return fmt.Sprintf(format, args...)
}
return color.code + fmt.Sprintf(format, args...) + Reset.code
}
// Println выводит строку с цветом
func Println(color Color, args ...interface{}) {
if !IsTerminalSupported() {
fmt.Println(args...)
return
}
fmt.Print(color.code)
fmt.Println(args...)
fmt.Print(Reset.code)
}
// Printf выводит форматированную строку с цветом
func Printf(color Color, format string, args ...interface{}) {
if !IsTerminalSupported() {
fmt.Printf(format, args...)
return
}
fmt.Print(color.code)
fmt.Printf(format, args...)
fmt.Print(Reset.code)
}
// IsTerminalSupported проверяет поддержку цвета в терминале
func IsTerminalSupported() bool {
// Проверяем переменную окружения TERM
term := strings.ToLower(os.Getenv("TERM"))
if term == "dumb" || term == "" {
return false
}
// Проверяем, что вывод идет в терминал
if !isatty.IsTerminal(os.Stdout.Fd()) {
return false
}
return true
}