Compare commits

..

No commits in common. "main" and "v2.1.2" have entirely different histories.
main ... v2.1.2

5 changed files with 54 additions and 385 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -25,8 +25,8 @@ jobs:
GOOS=linux GOARCH=amd64 go build -o dist/gosh-linux-amd64 GOOS=linux GOARCH=amd64 go build -o dist/gosh-linux-amd64
GOOS=linux GOARCH=arm64 go build -o dist/gosh-linux-arm64 GOOS=linux GOARCH=arm64 go build -o dist/gosh-linux-arm64
# macOS # macOS
GOOS=darwin GOARCH=amd64 go build -o dist/gosh-darwin-amd64 GOOS=darwin GOARCH=amd64 go build -o dist/gosh-mac-amd64
GOOS=darwin GOARCH=arm64 go build -o dist/gosh-darwin-arm64 GOOS=darwin GOARCH=arm64 go build -o dist/gosh-mac-arm64
ls -lh dist/ ls -lh dist/

View File

@ -1,27 +0,0 @@
name: gosh
version: 0.1.0
language: go
go_version: "1.20+"
build:
commands:
- go build -o gosh .
install:
files:
- src: gosh
dest: $BIN_DIR/gosh
permissions:
- path: $BIN_DIR/gosh
mode: 0755
dependencies:
build:
- git
- golang >= 1.20
runtime: []
tests:
- command: ./gosh --version
output: "gosh 0.1.0"

View File

@ -19,7 +19,7 @@ To test the shell and see if it suits you.
If everything works, then move the binary to a place inside your path. If everything works, then move the binary to a place inside your path.
To directly install it with the Go toolchain, just use To directly install it with the Go toolchain, just use
`go install` `go intall`
This will build and place the binary inside your `$HOME/go/bin` folder. This will build and place the binary inside your `$HOME/go/bin` folder.
Add this folder to your path and you are good to go ! Add this folder to your path and you are good to go !
@ -29,16 +29,7 @@ To use the program, just invoke it with `GoSH`
To change config parameter on the fly, use the `set` builtin. To change config parameter on the fly, use the `set` builtin.
Currently, `set` has a limited amount of configuration options and need to have a valid config file to write to. Currently, `set` has a limited amount of configuration options and need to have a valid config file to write to.
To change the color of the prompt use `set color <color>` To change the color of the prompt use `set color <color>`
All the avalable colors : All the avalable colors : black red purple cyan white green yellow blue
- black
- red
- purple
- cyan
- white
- green
- yellow
- blue
You can change the history size with `set history_size <int>` You can change the history size with `set history_size <int>`
You can change the prompt with `set prompt <promp>` You can change the prompt with `set prompt <promp>`
Here is some exemple of prompts : Here is some exemple of prompts :
@ -53,6 +44,3 @@ We can list the fact that interactive programs, like SSH or VIM work partialy.
The config has to be manualy created and populated. The config has to be manualy created and populated.
Also pipes aren't supported yet, so no `ls | grep "thing"` Also pipes aren't supported yet, so no `ls | grep "thing"`
PTY currently don't support signals like 'Ctrl+C' so don't use vim, nano nor nvim for exemple. PTY currently don't support signals like 'Ctrl+C' so don't use vim, nano nor nvim for exemple.
## ToDo
- Tab completion (for cd)

392
main.go
View File

@ -5,12 +5,8 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time"
"github.com/chzyer/readline" "github.com/chzyer/readline"
"github.com/creack/pty" "github.com/creack/pty"
@ -20,58 +16,40 @@ import (
// Structure pour stocker la configuration // Structure pour stocker la configuration
type Config struct { type Config struct {
Prompt string `mapstructure:"prompt"` Prompt string `mapstructure:"prompt"`
Color string `mapstructure:"color"` Color string `mapstructure:"color"`
HistorySize int `mapstructure:"history_size"` HistorySize int `mapstructure:"history_size"`
Aliases map[string]string `mapstructure:"aliases"`
} }
// Constantes pour la version et le nom du shell var config Config
const (
VERSION = "2.3.0"
SHELL_NAME = "GoShell"
)
var ( // Map des couleurs ANSI
config Config var colors = map[string]string{
// Map des couleurs ANSI "black": "\033[30m",
colors = map[string]string{ "red": "\033[31m",
"black": "\033[30m", "green": "\033[32m",
"red": "\033[31m", "yellow": "\033[33m",
"green": "\033[32m", "blue": "\033[34m",
"yellow": "\033[33m", "purple": "\033[35m",
"blue": "\033[34m", "cyan": "\033[36m",
"purple": "\033[35m", "white": "\033[37m",
"cyan": "\033[36m", "reset": "\033[0m",
"white": "\033[37m", }
"reset": "\033[0m",
// Ajout de couleurs supplémentaires
"bold": "\033[1m",
"underline": "\033[4m",
}
)
func main() { func main() {
// Gestion des signaux
setupSignalHandling()
// Chargement de la configuration // Chargement de la configuration
loadConfig() loadConfig()
// Chargement de l'historique au démarrage // Chargement de l'historique au démarrage
homeDir, _ := os.UserHomeDir() homeDir, _ := os.UserHomeDir()
historyFile := filepath.Join(homeDir, ".gosh_history") historyFile := homeDir + "/.gosh_history"
// Configuration du shell interactif // Configuration du shell interactif
rl, err := readline.NewEx(&readline.Config{ rl, err := readline.NewEx(&readline.Config{
Prompt: getPrompt(), Prompt: getPrompt(),
HistoryFile: historyFile, HistoryFile: historyFile,
HistoryLimit: config.HistorySize, HistoryLimit: config.HistorySize,
AutoComplete: newCompleter(), AutoComplete: nil,
InterruptPrompt: "^C",
EOFPrompt: "exit",
HistorySearchFold: true,
FuncFilterInputRune: nil,
}) })
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "Erreur readline:", err) fmt.Fprintln(os.Stderr, "Erreur readline:", err)
@ -79,19 +57,11 @@ func main() {
} }
defer rl.Close() defer rl.Close()
fmt.Printf("Bienvenue dans %s version %s\nTapez 'help' pour afficher l'aide.\n\n", SHELL_NAME, VERSION)
for { for {
rl.SetPrompt(getPrompt()) rl.SetPrompt(getPrompt())
input, err := rl.Readline() input, err := rl.Readline()
if err != nil { 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 break
} }
@ -100,93 +70,34 @@ func main() {
continue continue
} }
startTime := time.Now()
if err := execInput(input); err != nil { if err := execInput(input); err != nil {
fmt.Fprintln(os.Stderr, "Erreur:", err) fmt.Fprintln(os.Stderr, 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 // Charger la configuration depuis un fichier
func loadConfig() { func loadConfig() {
homeDir, _ := os.UserHomeDir() homeDir, _ := os.UserHomeDir()
configPath := filepath.Join(homeDir, ".config", "gosh") configPath := homeDir + "/.config/gosh/"
fmt.Println("Chemin du fichier de configuration:", configPath)
if err := os.MkdirAll(configPath, 0755); err != nil {
fmt.Fprintln(os.Stderr, "Erreur lors de la création du dossier de configuration:", err)
}
viper.AddConfigPath(configPath) viper.AddConfigPath(configPath)
viper.SetConfigName("gosh_config") viper.SetConfigName("gosh_config")
viper.SetConfigType("toml") viper.SetConfigType("toml")
// Valeurs par défaut // Valeurs par défaut
viper.SetDefault("prompt", "[{dir}] $ ") viper.SetDefault("prompt", "[{dir}] > ")
viper.SetDefault("color", "green") viper.SetDefault("color", "blue")
viper.SetDefault("history_size", 1000) viper.SetDefault("history_size", 1000)
viper.SetDefault("aliases", map[string]string{
"ll": "ls -la",
"la": "ls -a",
})
// Lire le fichier de configuration // Lire le fichier de configuration
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok { if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Println("Création du fichier de configuration avec les valeurs par défaut...") fmt.Println("Création du fichier de configuration avec les valeurs par défaut...")
configFilePath := filepath.Join(configPath, "gosh_config.toml") if err := viper.WriteConfigAs(configPath + "gosh_config"); err != nil {
if err := viper.WriteConfigAs(configFilePath); err != nil {
fmt.Fprintln(os.Stderr, "Erreur lors de la création du fichier de configuration:", err) fmt.Fprintln(os.Stderr, "Erreur lors de la création du fichier de configuration:", err)
} else { } else {
fmt.Println("Fichier de configuration créé avec succès:", configFilePath) fmt.Println("Fichier de configuration créé avec succès:", configPath)
} }
} else { } else {
fmt.Fprintln(os.Stderr, "Erreur de configuration:", err) fmt.Fprintln(os.Stderr, "Erreur de configuration:", err)
@ -203,11 +114,6 @@ func loadConfig() {
fmt.Fprintln(os.Stderr, "Taille de l'historique invalide. Utilisation de la valeur par défaut (1000).") fmt.Fprintln(os.Stderr, "Taille de l'historique invalide. Utilisation de la valeur par défaut (1000).")
config.HistorySize = 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 // Fonction pour générer le prompt avec le répertoire courant
@ -222,12 +128,7 @@ func getPrompt() string {
wd = "~" + strings.TrimPrefix(wd, homeDir) wd = "~" + strings.TrimPrefix(wd, homeDir)
} }
// Remplacer des variables dans le prompt
prompt := strings.Replace(config.Prompt, "{dir}", wd, -1) 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 // Appliquer la couleur si elle existe dans la map
if colorCode, exists := colors[config.Color]; exists { if colorCode, exists := colors[config.Color]; exists {
@ -240,57 +141,23 @@ func getPrompt() string {
// Fonction pour déterminer si une commande est interactive // Fonction pour déterminer si une commande est interactive
func isInteractiveCommand(cmd string) bool { func isInteractiveCommand(cmd string) bool {
interactiveCommands := map[string]bool{ interactiveCommands := map[string]bool{
"vim": true, "vim": true,
"nano": true, "nano": true,
"emacs": true, "ssh": true,
"ssh": true, "top": true,
"top": true, "htop": true,
"htop": true, "less": true,
"less": true, "more": true,
"more": true,
"man": true,
"vi": true,
"pico": true,
} }
return interactiveCommands[cmd] 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 { func execInput(input string) error {
// Remplacer les alias input = strings.TrimSuffix(input, "\n")
expandedInput := replaceAliases(input)
if expandedInput != input {
fmt.Printf("Alias expanded: %s\n", expandedInput)
input = expandedInput
}
args, err := shlex.Split(input) args, err := shlex.Split(input)
if err != nil { 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 { if len(args) == 0 {
@ -299,91 +166,36 @@ func execInput(input string) error {
switch args[0] { switch args[0] {
case "cd": case "cd":
// Gestion du changement de répertoire
if len(args) < 2 || args[1] == "" { if len(args) < 2 || args[1] == "" {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { 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(homeDir)
} }
return os.Chdir(args[1])
// 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": case "exit":
fmt.Println("Au revoir!")
os.Exit(0) os.Exit(0)
case "version": case "version":
fmt.Printf("%s Version %s\n", SHELL_NAME, VERSION) fmt.Println("GoShell Version 2.1.2")
return nil return nil
case "help":
printHelp()
return nil
case "set": 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 { if len(args) < 3 {
return fmt.Errorf("usage: set <key> <value>") return fmt.Errorf("Usage: set <key> <value>")
} }
return setConfig(args[1], strings.Join(args[2:], " ")) 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 <name> <command>")
}
return addAlias(args[1], strings.Join(args[2:], " "))
case "unalias":
if len(args) != 2 {
return fmt.Errorf("usage: unalias <name>")
}
return removeAlias(args[1])
case "aliases":
return listAliases()
} }
// Exécution des commandes externes
cmd := exec.Command(args[0], args[1:]...) cmd := exec.Command(args[0], args[1:]...)
if isInteractiveCommand(args[0]) { if isInteractiveCommand(args[0]) {
// Utiliser un PTY pour les commandes interactives // Utiliser un PTY pour les commandes interactives
ptmx, err := pty.Start(cmd) ptmx, err := pty.Start(cmd)
if err != nil { 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() defer ptmx.Close()
// Gérer le redimensionnement du terminal
go func() {
// TODO: Implémenter la gestion du redimensionnement
}()
// Rediriger stdin et stdout // Rediriger stdin et stdout
go func() { go func() {
io.Copy(ptmx, os.Stdin) io.Copy(ptmx, os.Stdin)
@ -395,54 +207,20 @@ func execInput(input string) error {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
// Démarrer la commande // Démarrer la commande avant d'attendre sa fin
if err := cmd.Start(); err != nil { 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 // Attendre la fin du processus
if err := cmd.Wait(); err != nil { if err := cmd.Wait(); err != nil {
// Vérifier si l'erreur est due à un code de sortie non nul return fmt.Errorf("Erreur lors de l'exécution de la commande: %v", err)
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 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 <nom> <cmd> - Créer un alias pour une commande")
fmt.Println(" unalias <nom> - 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 // Fonction pour modifier la configuration à la volée
func setConfig(key, value string) error { func setConfig(key, value string) error {
switch key { switch key {
@ -450,7 +228,7 @@ func setConfig(key, value string) error {
viper.Set("prompt", value) viper.Set("prompt", value)
case "color": case "color":
if _, exists := colors[value]; !exists { 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) viper.Set("color", value)
case "history_size": case "history_size":
@ -460,96 +238,26 @@ func setConfig(key, value string) error {
} }
viper.Set("history_size", intValue) viper.Set("history_size", intValue)
default: 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 { 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 { 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) fmt.Printf("Configuration mise à jour: %s = %s\n", key, value)
return nil 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 // Retourne la liste des couleurs disponibles
func getAvailableColors() []string { func getAvailableColors() []string {
keys := make([]string, 0, len(colors)) keys := make([]string, 0, len(colors))
for k := range colors { for k := range colors {
if k != "reset" { keys = append(keys, k)
keys = append(keys, k)
}
} }
return keys return keys
} }