Files
fush/internal/shell/commands.go

805 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// commands.go - встроенные команды fush shell (стиль busybox)
// Реализует базовые команды: exit, ls, cd, mkdir, rm, touch, pwd, cat, echo, mycurl
// Все команды работают независимо от ОС, используя только стандартную библиотеку Go
package shell
import (
"bufio"
"crypto/tls"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"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 (обновленная с mycurl)
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, "ВСТРОЕННЫЕ КОМАНДЫ (BUSYBOX-STYLE):")
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
commands := []struct {
name string
desc string
}{
{"exit", "Выход из оболочки"},
{"help", "Показать эту справку"},
{"ls [path]", "Вывести список файлов в директории"},
{"cd [dir]", "Сменить текущую директорию"},
{"pwd", "Показать текущую директорию"},
{"mkdir [-p] <dir>", "Создать новую директорию"},
{"rm [-rf] <file>", "Удалить файл или директорию"},
{"touch <file>", "Создать файл или обновить время доступа"},
{"cat <file>", "Вывести содержимое файла"},
{"echo [text...]", "Вывести текст на экран"},
{"exec <cmd> [args...]", "Выполнить внешнюю команду"},
{"mycurl [опции] <URL>", "Скачать файл (аналог curl)"},
}
for _, cmd := range commands {
fmt.Printf(" %-20s %s\n", ansi.Colorize(cmd.name, ansi.BrightWhite), cmd.desc)
}
fmt.Println()
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
ansi.Println(ansi.BrightGreen, "ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:")
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Printf(" %-30s %s\n", ansi.Colorize("ls -la", ansi.BrightWhite), "Показать все файлы")
fmt.Printf(" %-30s %s\n", ansi.Colorize("cd /tmp", ansi.BrightWhite), "Перейти в /tmp")
fmt.Printf(" %-30s %s\n", ansi.Colorize("pwd", ansi.BrightWhite), "Показать текущий путь")
fmt.Printf(" %-30s %s\n", ansi.Colorize("mkdir -p a/b/c", ansi.BrightWhite), "Создать вложенные директории")
fmt.Printf(" %-30s %s\n", ansi.Colorize("rm -rf olddir", ansi.BrightWhite), "Удалить директорию рекурсивно")
fmt.Printf(" %-30s %s\n", ansi.Colorize("cat file.txt", ansi.BrightWhite), "Показать содержимое файла")
fmt.Printf(" %-30s %s\n", ansi.Colorize("echo Hello World", ansi.BrightWhite), "Вывести текст")
fmt.Printf(" %-30s %s\n", ansi.Colorize("exec go version", ansi.BrightWhite), "Выполнить внешнюю команду")
fmt.Printf(" %-30s %s\n", ansi.Colorize("mycurl -o file.zip 'https://example.com/file.zip'", ansi.BrightWhite), "Скачать файл (URL в кавычках)")
fmt.Printf(" %-30s %s\n", ansi.Colorize("mycurl -O 'https://example.com/image.jpg'", ansi.BrightWhite), "Скачать с сохранением имени")
fmt.Printf(" %-30s %s\n", ansi.Colorize("curl -s 'https://example.com/file.dat'", ansi.BrightWhite), "Использовать как curl")
fmt.Printf(" %-30s %s\n", ansi.Colorize("ls | grep .go", ansi.BrightWhite), "Пайплайн")
fmt.Printf(" %-30s %s\n", ansi.Colorize("ls > files.txt", ansi.BrightWhite), "Перенаправление вывода")
fmt.Println()
ansi.Println(ansi.Yellow, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
return nil
}
// cmdLs обрабатывает команду ls (стиль busybox)
func (s *Shell) cmdLs(args []string) ([]byte, error) {
path := "."
showAll := false
longFormat := false
// Парсим аргументы
for _, arg := range args {
if arg == "-a" || arg == "--all" {
showAll = true
} else if arg == "-l" {
longFormat = true
} else if !strings.HasPrefix(arg, "-") {
path = arg
}
}
dir, err := os.Open(path)
if err != nil {
return nil, err
}
defer dir.Close()
entries, err := dir.Readdir(-1)
if err != nil {
return nil, err
}
var output strings.Builder
for _, entry := range entries {
name := entry.Name()
// Пропускаем скрытые файлы если не указан -a
if !showAll && strings.HasPrefix(name, ".") {
continue
}
if longFormat {
// Формат: права ссылки владелец группа размер дата имя
perms := entry.Mode().String()
nlink := 1 // В Go сложно получить количество жестких ссылок
uid := fmt.Sprintf("%d", entry.Sys() != nil) // Упрощённо
gid := "users"
size := entry.Size()
modTime := entry.ModTime().Format("Jan _2 15:04")
if entry.IsDir() {
name = name + "/"
} else if entry.Mode()&os.ModeSymlink != 0 {
// Для симлинков пытаемся прочитать цель
if target, err := os.Readlink(filepath.Join(path, entry.Name())); err == nil {
name = name + " -> " + target
}
}
fmt.Fprintf(&output, "%s %3d %-8s %-8s %8d %s %s\n",
perms, nlink, uid, gid, size, modTime, name)
} else {
if entry.IsDir() {
name = name + "/"
}
fmt.Fprint(&output, name, "\n")
}
}
return []byte(output.String()), nil
}
// cmdCd обрабатывает команду cd
func (s *Shell) cmdCd(args []string) error {
path := s.GetEnv("HOME")
if len(args) > 0 {
path = args[0]
}
if path == "~" {
path = s.GetEnv("HOME")
}
if err := os.Chdir(path); err != nil {
return err
}
pwd, err := os.Getwd()
if err == nil {
s.SetEnv("PWD", pwd)
}
return nil
}
// cmdPwd обрабатывает команду pwd
func (s *Shell) cmdPwd(args []string) ([]byte, error) {
dir, err := os.Getwd()
if err != nil {
return nil, err
}
return []byte(dir + "\n"), nil
}
// cmdMkdir обрабатывает команду mkdir (с поддержкой -p)
func (s *Shell) cmdMkdir(args []string) error {
if len(args) == 0 {
return fmt.Errorf("требуется имя директории")
}
createParents := false
var dirs []string
for _, arg := range args {
if arg == "-p" || arg == "--parents" {
createParents = true
} else if !strings.HasPrefix(arg, "-") {
dirs = append(dirs, arg)
}
}
for _, dir := range dirs {
var err error
if createParents {
err = os.MkdirAll(dir, 0755)
} else {
err = os.Mkdir(dir, 0755)
}
if err != nil {
return err
}
}
return nil
}
// cmdRm обрабатывает команду rm (с поддержкой -r и -f)
func (s *Shell) cmdRm(args []string) error {
if len(args) == 0 {
return fmt.Errorf("требуется имя файла")
}
recursive := false
force := false
var targets []string
for _, arg := range args {
switch arg {
case "-r", "-R", "--recursive":
recursive = true
case "-f", "--force":
force = true
default:
if !strings.HasPrefix(arg, "-") {
targets = append(targets, arg)
}
}
}
for _, target := range targets {
info, err := os.Stat(target)
if err != nil {
if !force {
return err
}
continue
}
if info.IsDir() && !recursive {
return fmt.Errorf("'%s' является директорией, используйте -r для удаления", target)
}
if err := os.RemoveAll(target); err != nil && !force {
return err
}
}
return nil
}
// cmdTouch обрабатывает команду touch
func (s *Shell) cmdTouch(args []string) error {
if len(args) == 0 {
return fmt.Errorf("требуется имя файла")
}
now := time.Now()
for _, filename := range args {
if strings.HasPrefix(filename, "-") {
continue
}
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
file.Close()
if err := os.Chtimes(filename, now, now); err != nil {
return err
}
}
return nil
}
// cmdCat обрабатывает команду cat (конкатенация файлов)
func (s *Shell) cmdCat(args []string) ([]byte, error) {
if len(args) == 0 {
// Читаем из stdin
info, err := os.Stdin.Stat()
if err != nil {
return nil, err
}
if info.Mode()&os.ModeCharDevice != 0 {
return nil, fmt.Errorf("ожидается файл или stdin")
}
reader := bufio.NewReader(os.Stdin)
var output strings.Builder
for {
line, err := reader.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
output.WriteString(line)
}
return []byte(output.String()), nil
}
var output strings.Builder
for _, filename := range args {
if strings.HasPrefix(filename, "-") {
continue
}
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("ошибка чтения '%s': %v", filename, err)
}
output.Write(data)
if len(data) > 0 && data[len(data)-1] != '\n' {
output.WriteByte('\n')
}
}
return []byte(output.String()), nil
}
// cmdEcho обрабатывает команду echo
func (s *Shell) cmdEcho(args []string) ([]byte, error) {
newline := true
startIdx := 0
if len(args) > 0 && args[0] == "-n" {
newline = false
startIdx = 1
}
var output strings.Builder
for i := startIdx; i < len(args); i++ {
if i > startIdx {
output.WriteByte(' ')
}
output.WriteString(args[i])
}
if newline {
output.WriteByte('\n')
}
return []byte(output.String()), nil
}
// resolveURL преобразует относительный URL в абсолютный
func resolveURL(baseURL, relURL string) (string, error) {
base, err := url.Parse(baseURL)
if err != nil {
return "", err
}
rel, err := url.Parse(relURL)
if err != nil {
return "", err
}
resolved := base.ResolveReference(rel)
return resolved.String(), nil
}
// cmdMycurl обрабатывает команду mycurl для скачивания файлов
func (s *Shell) cmdMycurl(args []string) error {
if len(args) == 0 {
return fmt.Errorf("использование: mycurl [опции] <URL>\n" +
"Опции:\n" +
" -o <файл> выходной файл (по умолчанию из URL)\n" +
" -O использовать имя файла из URL\n" +
" -L следовать редиректам (включено по умолчанию)\n" +
" -s тихий режим (без прогресс-бара)\n" +
" -v подробный вывод\n" +
" -A <строка> User-Agent строка\n" +
" -e <строка> Referer строка\n" +
" -k игнорировать проверку SSL сертификатов\n" +
" -h, --help показать справку\n\n" +
"Важно: URL должен быть заключён в кавычки, если содержит спецсимволы (&, ?, =)")
}
// Парсим аргументы
outputFile := ""
silent := false
verbose := false
urlStr := ""
userAgent := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
referer := "https://futriix.ru:8083/"
insecure := false
maxRedirects := 10
for i := 0; i < len(args); i++ {
arg := args[i]
switch arg {
case "-o":
if i+1 < len(args) {
outputFile = args[i+1]
i++
} else {
return fmt.Errorf("-o требует имя файла")
}
case "-O":
outputFile = "" // Будет определено из URL позже
case "-L":
// Поддержка редиректов включена по умолчанию, игнорируем
case "-s":
silent = true
case "-v":
verbose = true
case "-k":
insecure = true
case "-A":
if i+1 < len(args) {
userAgent = args[i+1]
i++
} else {
return fmt.Errorf("-A требует строку User-Agent")
}
case "-e":
if i+1 < len(args) {
referer = args[i+1]
i++
} else {
return fmt.Errorf("-e требует строку Referer")
}
case "-h", "--help":
return s.cmdMycurlHelp(args)
default:
if !strings.HasPrefix(arg, "-") && urlStr == "" {
urlStr = arg
}
}
}
if urlStr == "" {
return fmt.Errorf("URL не указан")
}
// Удаляем кавычки, если они есть (одинарные или двойные)
urlStr = strings.Trim(urlStr, "'\"")
// Декодируем URL если нужно (обработка экранированных символов)
decodedURL, err := url.QueryUnescape(urlStr)
if err == nil && decodedURL != urlStr {
if verbose {
fmt.Printf("Декодированный URL: %s\n", decodedURL)
}
urlStr = decodedURL
}
// Проверяем валидность URL
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
return fmt.Errorf("неверный URL (должен начинаться с http:// или https://): %s", urlStr)
}
// Определяем имя выходного файла
if outputFile == "" {
outputFile = getFilenameFromURL(urlStr)
if outputFile == "" {
outputFile = "downloaded_file"
}
}
if !silent {
fmt.Printf("Загрузка: %s\n", urlStr)
fmt.Printf("Сохраняется в: %s\n", outputFile)
}
// Создаем Cookie Jar для сохранения сессии
jar, err := cookiejar.New(nil)
if err != nil {
return fmt.Errorf("ошибка создания cookie jar: %v", err)
}
// Создаем HTTP клиент с поддержкой кук и редиректов
client := &http.Client{
Timeout: 60 * time.Second,
Jar: jar,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if verbose {
fmt.Printf("-> HTTP редирект на: %s\n", req.URL.String())
}
if len(via) >= maxRedirects {
return fmt.Errorf("слишком много HTTP редиректов")
}
return nil
},
}
// Если нужно игнорировать SSL сертификаты
if insecure {
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client.Transport = transport
}
// Функция для выполнения запроса с обработкой редиректов
currentURL := urlStr
for redirectCount := 0; redirectCount < maxRedirects; redirectCount++ {
// Создаем запрос
req, err := http.NewRequest("GET", currentURL, nil)
if err != nil {
return fmt.Errorf("ошибка создания запроса: %v", err)
}
// Устанавливаем заголовки
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Referer", referer)
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
req.Header.Set("Accept-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Pragma", "no-cache")
if verbose {
fmt.Printf("\nЗаголовки запроса:\n")
for key, values := range req.Header {
fmt.Printf(" %s: %s\n", key, strings.Join(values, ", "))
}
fmt.Printf("\nВыполнение запроса...\n")
}
// Выполняем запрос
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("ошибка загрузки: %v", err)
}
if verbose {
fmt.Printf("\nСтатус ответа: %s\n", resp.Status)
fmt.Printf("Заголовки ответа:\n")
for key, values := range resp.Header {
fmt.Printf(" %s: %s\n", key, strings.Join(values, ", "))
}
fmt.Println()
}
// Проверяем статус ответа
if resp.StatusCode == http.StatusOK {
// Успешный ответ - скачиваем файл
defer resp.Body.Close()
// Показываем размер файла если известен
if !silent && resp.ContentLength > 0 {
sizeMB := float64(resp.ContentLength) / (1024 * 1024)
fmt.Printf("Размер файла: %.2f MB\n", sizeMB)
}
// Создаем выходной файл
file, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("ошибка создания файла: %v", err)
}
defer file.Close()
// Скачиваем с прогресс-баром или без него
var written int64
if silent {
written, err = io.Copy(file, resp.Body)
} else {
written, err = s.downloadWithProgress(file, resp.Body, resp.ContentLength)
}
if err != nil {
return fmt.Errorf("ошибка сохранения файла: %v", err)
}
if !silent {
fmt.Printf("\n✅ Загрузка завершена! Сохранено %d байт (%s)\n",
written, formatBytes(written))
}
s.logger.Info("Файл загружен",
"url", urlStr,
"output", outputFile,
"size", written)
return nil
} else if resp.StatusCode == 500 || resp.StatusCode == 200 {
// Читаем тело для поиска meta refresh
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return fmt.Errorf("ошибка чтения тела ответа: %v", err)
}
bodyStr := string(body)
// Ищем meta refresh редирект (разные форматы)
patterns := []string{
`<meta[^>]*http-equiv=["']refresh["'][^>]*content=["']\d+;\s*url=([^"']+)["']`,
`<meta[^>]*content=["']\d+;\s*url=([^"']+)["'][^>]*http-equiv=["']refresh["']`,
`URL\s*=\s*['"]?([^'"\s>]+)`,
}
redirectURL := ""
for _, pattern := range patterns {
re := regexp.MustCompile(pattern)
matches := re.FindStringSubmatch(bodyStr)
if len(matches) > 1 {
redirectURL = matches[1]
break
}
}
if redirectURL != "" {
if verbose {
fmt.Printf("-> HTML meta refresh редирект на: %s\n", redirectURL)
}
// Преобразуем относительный URL в абсолютный
absURL, err := resolveURL(currentURL, redirectURL)
if err != nil {
return fmt.Errorf("ошибка преобразования URL редиректа: %v", err)
}
if verbose {
fmt.Printf("-> Абсолютный URL: %s\n", absURL)
}
currentURL = absURL
continue // Делаем следующий запрос
} else if resp.StatusCode == 500 {
return fmt.Errorf("HTTP ошибка: %s\nТело ответа: %s", resp.Status,
func(s string) string {
if len(s) > 500 {
return s[:500] + "..."
}
return s
}(bodyStr))
}
} else {
resp.Body.Close()
return fmt.Errorf("неожиданный HTTP статус: %s", resp.Status)
}
}
return fmt.Errorf("превышено максимальное количество редиректов (%d)", maxRedirects)
}
// cmdMycurlHelp показывает справку по команде mycurl
func (s *Shell) cmdMycurlHelp(args []string) error {
fmt.Println()
fmt.Println("mycurl - встроенная утилита для скачивания файлов")
fmt.Println()
fmt.Println("Использование:")
fmt.Println(" mycurl [опции] URL")
fmt.Println()
fmt.Println("Опции:")
fmt.Println(" -o <файл> Сохранить в указанный файл")
fmt.Println(" -O Использовать имя файла из URL")
fmt.Println(" -L Следовать редиректам (включено по умолчанию)")
fmt.Println(" -s Тихий режим (без прогресс-бара)")
fmt.Println(" -v Подробный вывод (заголовки запроса/ответа)")
fmt.Println(" -A <строка> User-Agent строка")
fmt.Println(" -e <строка> Referer строка")
fmt.Println(" -k Игнорировать проверку SSL сертификатов")
fmt.Println(" -h, --help Показать эту справку")
fmt.Println()
fmt.Println("Примеры:")
fmt.Println(" mycurl 'https://example.com/file.zip'")
fmt.Println(" mycurl -o myfile.zip 'https://example.com/file.zip?param=value'")
fmt.Println(" mycurl -O 'https://example.com/file.zip'")
fmt.Println(" mycurl -s 'https://example.com/largefile.iso'")
fmt.Println(" mycurl -v 'https://example.com/debug.zip'")
fmt.Println(" mycurl -A 'CustomAgent/1.0' -e 'https://google.com' 'https://example.com/file.zip'")
fmt.Println(" curl -o file.zip 'https://example.com/file.zip' # алиас")
fmt.Println()
fmt.Println("Примечания:")
fmt.Println(" - Кавычки (одинарные или двойные) ОБЯЗАТЕЛЬНЫ, если URL содержит &, ?, =")
fmt.Println(" - Кавычки автоматически удаляются при обработке")
fmt.Println(" - Поддерживаются cookies и сессии")
fmt.Println(" - Поддерживаются HTML meta refresh редиректы")
fmt.Println(" - Утилита эмулирует поведение curl с заголовками User-Agent и Referer")
fmt.Println(" - Поддерживает автоматическое следование редиректам")
fmt.Println(" - Работает на Linux и OpenIndiana/Hipster")
fmt.Println()
return nil
}
// downloadWithProgress скачивает с отображением прогресса
func (s *Shell) downloadWithProgress(writer io.Writer, reader io.Reader, total int64) (int64, error) {
progress := &progressWriter{
total: total,
writer: writer,
started: time.Now(),
}
return io.Copy(progress, reader)
}
// progressWriter реализует запись с прогресс-баром
type progressWriter struct {
total int64
written int64
writer io.Writer
started time.Time
}
func (pw *progressWriter) Write(p []byte) (int, error) {
n, err := pw.writer.Write(p)
pw.written += int64(n)
// Обновляем прогресс
if pw.total > 0 {
percent := float64(pw.written) / float64(pw.total) * 100
elapsed := time.Since(pw.started)
speed := float64(pw.written) / elapsed.Seconds()
// Скорость в MB/s
speedMBps := speed / (1024 * 1024)
// Оставшееся время
var eta string
if speed > 0 {
remaining := float64(pw.total-pw.written) / speed
if remaining < 60 {
eta = fmt.Sprintf("%.0fs", remaining)
} else if remaining < 3600 {
eta = fmt.Sprintf("%.0fm%.0fs", remaining/60, float64(int(remaining)%60))
} else {
eta = fmt.Sprintf("%.0fh%.0fm", remaining/3600, (int(remaining)%3600)/60)
}
} else {
eta = "?"
}
fmt.Printf("\rПрогресс: %.1f%% [%s/%s] @ %.2f MB/s ETA: %s",
percent,
formatBytes(pw.written),
formatBytes(pw.total),
speedMBps,
eta)
} else {
fmt.Printf("\rЗагружено: %s", formatBytes(pw.written))
}
return n, err
}
// getFilenameFromURL извлекает имя файла из URL
func getFilenameFromURL(urlStr string) string {
// Убираем параметры запроса
if idx := strings.Index(urlStr, "?"); idx != -1 {
urlStr = urlStr[:idx]
}
// Извлекаем последний сегмент пути
parts := strings.Split(urlStr, "/")
if len(parts) > 0 {
filename := parts[len(parts)-1]
if filename != "" && strings.Contains(filename, ".") {
return filename
}
}
return ""
}
// formatBytes форматирует байты в человекочитаемый вид
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
switch exp {
case 0:
return fmt.Sprintf("%.1f KB", float64(bytes)/float64(div))
case 1:
return fmt.Sprintf("%.1f MB", float64(bytes)/float64(div))
case 2:
return fmt.Sprintf("%.1f GB", float64(bytes)/float64(div))
default:
return fmt.Sprintf("%.1f TB", float64(bytes)/float64(div))
}
}