From f25176ea2bf74ba473594e2c65d5c77cd88a5e96 Mon Sep 17 00:00:00 2001 From: Alexandre1a Date: Sun, 30 Mar 2025 22:40:27 +0200 Subject: [PATCH] Added some features --- main.go | 390 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 338 insertions(+), 52 deletions(-) diff --git a/main.go b/main.go index 84c537b..14506a8 100644 --- a/main.go +++ b/main.go @@ -5,8 +5,12 @@ import ( "io" "os" "os/exec" + "os/signal" + "path/filepath" "strconv" "strings" + "syscall" + "time" "github.com/chzyer/readline" "github.com/creack/pty" @@ -16,40 +20,58 @@ import ( // Structure pour stocker la configuration type Config struct { - Prompt string `mapstructure:"prompt"` - Color string `mapstructure:"color"` - HistorySize int `mapstructure:"history_size"` + Prompt string `mapstructure:"prompt"` + Color string `mapstructure:"color"` + HistorySize int `mapstructure:"history_size"` + Aliases map[string]string `mapstructure:"aliases"` } -var config Config +// Constantes pour la version et le nom du shell +const ( + VERSION = "2.2.0" + SHELL_NAME = "GoShell" +) -// Map des couleurs ANSI -var colors = map[string]string{ - "black": "\033[30m", - "red": "\033[31m", - "green": "\033[32m", - "yellow": "\033[33m", - "blue": "\033[34m", - "purple": "\033[35m", - "cyan": "\033[36m", - "white": "\033[37m", - "reset": "\033[0m", -} +var ( + config Config + // Map des couleurs ANSI + colors = map[string]string{ + "black": "\033[30m", + "red": "\033[31m", + "green": "\033[32m", + "yellow": "\033[33m", + "blue": "\033[34m", + "purple": "\033[35m", + "cyan": "\033[36m", + "white": "\033[37m", + "reset": "\033[0m", + // Ajout de couleurs supplémentaires + "bold": "\033[1m", + "underline": "\033[4m", + } +) func main() { + // Gestion des signaux + setupSignalHandling() + // Chargement de la configuration loadConfig() // Chargement de l'historique au démarrage homeDir, _ := os.UserHomeDir() - historyFile := homeDir + "/.gosh_history" + historyFile := filepath.Join(homeDir, ".gosh_history") // Configuration du shell interactif rl, err := readline.NewEx(&readline.Config{ - Prompt: getPrompt(), - HistoryFile: historyFile, - HistoryLimit: config.HistorySize, - AutoComplete: nil, + Prompt: getPrompt(), + HistoryFile: historyFile, + HistoryLimit: config.HistorySize, + AutoComplete: newCompleter(), + InterruptPrompt: "^C", + EOFPrompt: "exit", + HistorySearchFold: true, + FuncFilterInputRune: nil, }) if err != nil { fmt.Fprintln(os.Stderr, "Erreur readline:", err) @@ -57,11 +79,19 @@ func main() { } defer rl.Close() + fmt.Printf("Bienvenue dans %s version %s\nTapez 'help' pour afficher l'aide.\n\n", SHELL_NAME, VERSION) + for { rl.SetPrompt(getPrompt()) input, err := rl.Readline() if err != nil { + if err == readline.ErrInterrupt { + continue // Ignorer Ctrl+C + } else if err == io.EOF { + break // Ctrl+D pour quitter + } + fmt.Fprintln(os.Stderr, "Erreur de lecture:", err) break } @@ -70,18 +100,66 @@ func main() { continue } + startTime := time.Now() if err := execInput(input); err != nil { - fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(os.Stderr, "Erreur:", err) + } + duration := time.Since(startTime) + + // Afficher le temps d'exécution pour les commandes qui prennent plus d'une seconde + if duration > time.Second { + fmt.Printf("Temps d'exécution: %s\n", duration.Round(time.Millisecond)) } } } +// Mise en place de la gestion des signaux +func setupSignalHandling() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + fmt.Println("\nAu revoir!") + os.Exit(0) + }() +} + +// Implémentation simple de l'auto-complétion +func newCompleter() *readline.PrefixCompleter { + // Liste des commandes internes + internalCommands := []readline.PrefixCompleterInterface{ + readline.PcItem("cd"), + readline.PcItem("exit"), + readline.PcItem("help"), + readline.PcItem("version"), + readline.PcItem("set", + readline.PcItem("prompt"), + readline.PcItem("color", + readline.PcItem("black"), + readline.PcItem("red"), + readline.PcItem("green"), + readline.PcItem("yellow"), + readline.PcItem("blue"), + readline.PcItem("purple"), + readline.PcItem("cyan"), + readline.PcItem("white"), + readline.PcItem("bold"), + readline.PcItem("underline"), + ), + readline.PcItem("history_size"), + ), + readline.PcItem("alias"), + readline.PcItem("unalias"), + readline.PcItem("aliases"), + } + + return readline.NewPrefixCompleter(internalCommands...) +} + // Charger la configuration depuis un fichier func loadConfig() { homeDir, _ := os.UserHomeDir() - - configPath := homeDir + "/.config/gosh/" - fmt.Println("Chemin du fichier de configuration:", configPath) + configPath := filepath.Join(homeDir, ".config", "gosh") if err := os.MkdirAll(configPath, 0755); err != nil { fmt.Fprintln(os.Stderr, "Erreur lors de la création du dossier de configuration:", err) @@ -95,15 +173,20 @@ func loadConfig() { viper.SetDefault("prompt", "[{dir}] $ ") viper.SetDefault("color", "green") viper.SetDefault("history_size", 1000) + viper.SetDefault("aliases", map[string]string{ + "ll": "ls -la", + "la": "ls -a", + }) // Lire le fichier de configuration if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { fmt.Println("Création du fichier de configuration avec les valeurs par défaut...") - if err := viper.WriteConfigAs(configPath + "gosh_config"); err != nil { + configFilePath := filepath.Join(configPath, "gosh_config.toml") + if err := viper.WriteConfigAs(configFilePath); err != nil { fmt.Fprintln(os.Stderr, "Erreur lors de la création du fichier de configuration:", err) } else { - fmt.Println("Fichier de configuration créé avec succès:", configPath) + fmt.Println("Fichier de configuration créé avec succès:", configFilePath) } } else { fmt.Fprintln(os.Stderr, "Erreur de configuration:", err) @@ -120,6 +203,11 @@ func loadConfig() { fmt.Fprintln(os.Stderr, "Taille de l'historique invalide. Utilisation de la valeur par défaut (1000).") config.HistorySize = 1000 } + + // Initialiser la map des alias si elle est nil + if config.Aliases == nil { + config.Aliases = make(map[string]string) + } } // Fonction pour générer le prompt avec le répertoire courant @@ -134,7 +222,12 @@ func getPrompt() string { wd = "~" + strings.TrimPrefix(wd, homeDir) } + // Remplacer des variables dans le prompt prompt := strings.Replace(config.Prompt, "{dir}", wd, -1) + prompt = strings.Replace(prompt, "{time}", time.Now().Format("15:04:05"), -1) + prompt = strings.Replace(prompt, "{date}", time.Now().Format("2006-01-02"), -1) + prompt = strings.Replace(prompt, "{shell}", SHELL_NAME, -1) + prompt = strings.Replace(prompt, "{version}", VERSION, -1) // Appliquer la couleur si elle existe dans la map if colorCode, exists := colors[config.Color]; exists { @@ -147,23 +240,57 @@ func getPrompt() string { // Fonction pour déterminer si une commande est interactive func isInteractiveCommand(cmd string) bool { interactiveCommands := map[string]bool{ - "vim": true, - "nano": true, - "ssh": true, - "top": true, - "htop": true, - "less": true, - "more": true, + "vim": true, + "nano": true, + "emacs": true, + "ssh": true, + "top": true, + "htop": true, + "less": true, + "more": true, + "man": true, + "vi": true, + "pico": true, } return interactiveCommands[cmd] } +// Remplacer les alias par leurs commandes correspondantes +func replaceAliases(input string) string { + args, err := shlex.Split(input) + if err != nil || len(args) == 0 { + return input + } + + // Si le premier mot est un alias, le remplacer + if aliasCmd, exists := config.Aliases[args[0]]; exists { + // Si l'alias contient des arguments, les combiner avec ceux de la commande + aliasArgs, err := shlex.Split(aliasCmd) + if err != nil { + return input + } + + if len(args) > 1 { + // Joindre les arguments de l'alias avec ceux de la commande + return strings.Join(append(aliasArgs, args[1:]...), " ") + } + return aliasCmd + } + + return input +} + func execInput(input string) error { - input = strings.TrimSuffix(input, "\n") + // Remplacer les alias + expandedInput := replaceAliases(input) + if expandedInput != input { + fmt.Printf("Alias expanded: %s\n", expandedInput) + input = expandedInput + } args, err := shlex.Split(input) if err != nil { - return fmt.Errorf("Erreur lors de la division des arguments: %v", err) + return fmt.Errorf("erreur lors de la division des arguments: %v", err) } if len(args) == 0 { @@ -172,36 +299,91 @@ func execInput(input string) error { switch args[0] { case "cd": + // Gestion du changement de répertoire if len(args) < 2 || args[1] == "" { homeDir, err := os.UserHomeDir() if err != nil { - return fmt.Errorf("Impossible de trouver le home") + return fmt.Errorf("impossible de trouver le home") } return os.Chdir(homeDir) } - return os.Chdir(args[1]) - case "exit": - os.Exit(0) - case "version": - fmt.Println("GoShell Version 2.1.2") + + // Expansion du tilde en chemin complet du répertoire utilisateur + if args[1] == "~" || strings.HasPrefix(args[1], "~/") { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("impossible de trouver le home: %v", err) + } + args[1] = strings.Replace(args[1], "~", homeDir, 1) + } + + if err := os.Chdir(args[1]); err != nil { + return fmt.Errorf("cd: %v", err) + } return nil + + case "exit": + fmt.Println("Au revoir!") + os.Exit(0) + + case "version": + fmt.Printf("%s Version %s\n", SHELL_NAME, VERSION) + return nil + + case "help": + printHelp() + return nil + case "set": + if len(args) < 2 { + // Afficher la configuration actuelle + fmt.Printf("Configuration actuelle:\n") + fmt.Printf(" prompt = %s\n", config.Prompt) + fmt.Printf(" color = %s\n", config.Color) + fmt.Printf(" history_size = %d\n", config.HistorySize) + return nil + } if len(args) < 3 { - return fmt.Errorf("Usage: set ") + return fmt.Errorf("usage: set ") } return setConfig(args[1], strings.Join(args[2:], " ")) + + case "alias": + if len(args) == 1 { + // Afficher tous les alias + return listAliases() + } + if len(args) < 3 { + return fmt.Errorf("usage: alias ") + } + return addAlias(args[1], strings.Join(args[2:], " ")) + + case "unalias": + if len(args) != 2 { + return fmt.Errorf("usage: unalias ") + } + return removeAlias(args[1]) + + case "aliases": + return listAliases() } + // Exécution des commandes externes cmd := exec.Command(args[0], args[1:]...) if isInteractiveCommand(args[0]) { // Utiliser un PTY pour les commandes interactives ptmx, err := pty.Start(cmd) if err != nil { - return fmt.Errorf("Erreur lors du démarrage du PTY: %v", err) + return fmt.Errorf("erreur lors du démarrage du PTY: %v", err) } defer ptmx.Close() + // Gérer le redimensionnement du terminal + go func() { + // TODO: Implémenter la gestion du redimensionnement + }() + // Rediriger stdin et stdout go func() { io.Copy(ptmx, os.Stdin) @@ -213,20 +395,54 @@ func execInput(input string) error { cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin - // Démarrer la commande avant d'attendre sa fin + // Démarrer la commande if err := cmd.Start(); err != nil { - return fmt.Errorf("Erreur lors du démarrage de la commande: %v", err) + return fmt.Errorf("erreur lors du démarrage de la commande: %v", err) } } // Attendre la fin du processus if err := cmd.Wait(); err != nil { - return fmt.Errorf("Erreur lors de l'exécution de la commande: %v", err) + // Vérifier si l'erreur est due à un code de sortie non nul + if exitErr, ok := err.(*exec.ExitError); ok { + return fmt.Errorf("la commande a échoué avec le code: %d", exitErr.ExitCode()) + } + return fmt.Errorf("erreur lors de l'exécution de la commande: %v", err) } return nil } +// Afficher l'aide du shell +func printHelp() { + fmt.Printf("%s v%s - Un shell léger écrit en Go\n\n", SHELL_NAME, VERSION) + fmt.Println("Commandes internes:") + fmt.Println(" cd [dir] - Changer de répertoire") + fmt.Println(" exit - Quitter le shell") + fmt.Println(" version - Afficher la version du shell") + fmt.Println(" help - Afficher cette aide") + fmt.Println(" set [key] [value] - Afficher ou modifier la configuration") + fmt.Println(" alias - Créer un alias pour une commande") + fmt.Println(" unalias - Supprimer un alias") + fmt.Println(" aliases - Lister tous les alias") + fmt.Println() + fmt.Println("Variables de prompt:") + fmt.Println(" {dir} - Répertoire courant") + fmt.Println(" {time} - Heure actuelle") + fmt.Println(" {date} - Date actuelle") + fmt.Println(" {shell} - Nom du shell") + fmt.Println(" {version} - Version du shell") + fmt.Println() + fmt.Println("Couleurs disponibles:") + fmt.Print(" ") + for color := range colors { + if color != "reset" { + fmt.Printf("%s ", color) + } + } + fmt.Println() +} + // Fonction pour modifier la configuration à la volée func setConfig(key, value string) error { switch key { @@ -234,7 +450,7 @@ func setConfig(key, value string) error { viper.Set("prompt", value) case "color": if _, exists := colors[value]; !exists { - return fmt.Errorf("Couleur inconnue: %s. Couleurs disponibles: %v", value, getAvailableColors()) + return fmt.Errorf("couleur inconnue: %s. Couleurs disponibles: %v", value, getAvailableColors()) } viper.Set("color", value) case "history_size": @@ -244,26 +460,96 @@ func setConfig(key, value string) error { } viper.Set("history_size", intValue) default: - return fmt.Errorf("Clé de configuration inconnue: %s", key) + return fmt.Errorf("clé de configuration inconnue: %s", key) } if err := viper.WriteConfig(); err != nil { - return fmt.Errorf("Erreur lors de la sauvegarde de la configuration: %v", err) + return fmt.Errorf("erreur lors de la sauvegarde de la configuration: %v", err) } if err := viper.Unmarshal(&config); err != nil { - return fmt.Errorf("Erreur lors du rechargement de la configuration: %v", err) + return fmt.Errorf("erreur lors du rechargement de la configuration: %v", err) } fmt.Printf("Configuration mise à jour: %s = %s\n", key, value) return nil } +// Ajouter un alias +func addAlias(name, command string) error { + if name == "" || command == "" { + return fmt.Errorf("le nom et la commande ne peuvent pas être vides") + } + + // Vérifier que le nom n'est pas un mot clé réservé + reservedCommands := map[string]bool{ + "cd": true, + "exit": true, + "version": true, + "help": true, + "set": true, + "alias": true, + "unalias": true, + "aliases": true, + } + + if reservedCommands[name] { + return fmt.Errorf("impossible de créer un alias avec un nom réservé: %s", name) + } + + // Ajouter ou mettre à jour l'alias + if config.Aliases == nil { + config.Aliases = make(map[string]string) + } + config.Aliases[name] = command + viper.Set("aliases", config.Aliases) + + if err := viper.WriteConfig(); err != nil { + return fmt.Errorf("erreur lors de la sauvegarde des alias: %v", err) + } + + fmt.Printf("Alias ajouté: %s = %s\n", name, command) + return nil +} + +// Supprimer un alias +func removeAlias(name string) error { + if config.Aliases == nil || config.Aliases[name] == "" { + return fmt.Errorf("alias non trouvé: %s", name) + } + + delete(config.Aliases, name) + viper.Set("aliases", config.Aliases) + + if err := viper.WriteConfig(); err != nil { + return fmt.Errorf("erreur lors de la sauvegarde des alias: %v", err) + } + + fmt.Printf("Alias supprimé: %s\n", name) + return nil +} + +// Lister tous les alias +func listAliases() error { + if config.Aliases == nil || len(config.Aliases) == 0 { + fmt.Println("Aucun alias défini.") + return nil + } + + fmt.Println("Aliases définis:") + for name, command := range config.Aliases { + fmt.Printf(" %s = %s\n", name, command) + } + return nil +} + // Retourne la liste des couleurs disponibles func getAvailableColors() []string { keys := make([]string, 0, len(colors)) for k := range colors { - keys = append(keys, k) + if k != "reset" { + keys = append(keys, k) + } } return keys }