// 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] ", "Создать новую директорию"}, {"rm [-rf] ", "Удалить файл или директорию"}, {"touch ", "Создать файл или обновить время доступа"}, {"cat ", "Вывести содержимое файла"}, {"echo [text...]", "Вывести текст на экран"}, {"exec [args...]", "Выполнить внешнюю команду"}, {"mycurl [опции] ", "Скачать файл (аналог 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 [опции] \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{ `]*http-equiv=["']refresh["'][^>]*content=["']\d+;\s*url=([^"']+)["']`, `]*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)) } }