first commit
This commit is contained in:
19
LICENSE.txt
Normal file
19
LICENSE.txt
Normal 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
30
README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# fush shell
|
||||||
|
|
||||||
|
**F**usion **U**nix **SH**ell — легковесная, расширяемая командная оболочка с поддержкой Linux и OpenIndiana.
|
||||||
|
|
||||||
|
[](https://golang.org/)
|
||||||
|
[](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
565
build.sh
Executable 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
90
cmd/fush/main.go
Normal 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
22
config/fush.toml
Normal 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
25
fush.toml
Normal 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
70
gitignore.txt
Normal 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
11
go.mod
Normal 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
9
go.sum
Normal 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
131
internal/config/config.go
Normal 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
175
internal/history/history.go
Normal 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
110
internal/logger/logger.go
Normal 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
|
||||||
|
}
|
||||||
308
internal/luabridge/luabridge.go
Normal file
308
internal/luabridge/luabridge.go
Normal 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
205
internal/shell/commands.go
Normal 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
343
internal/shell/shell.go
Normal 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
30
lua/example.lua
Normal 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
168
pkg/ansi/colors.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user