Compare commits

..

9 Commits
v2.1.2 ... main

Author SHA1 Message Date
Alexandre1a
169929f577
Added a Todo 2025-04-14 15:19:33 +02:00
Alexandre1a
f25176ea2b
Added some features 2025-03-30 22:40:27 +02:00
Alexandre1a
53c5cba02a
Added the ability to create the config folder 2025-03-30 22:02:39 +02:00
Alexandre1a
b5fb8a86e9
Delete the key 2025-03-30 17:16:06 +02:00
Alexandre1a
bd17ee4cba
Changed mac to darwin 2025-03-30 13:26:13 +02:00
Alexandre1a
b9c24fe445
Added a file for my package manager 2025-03-30 12:09:13 +02:00
Alexandre1a
1aa84809a6
Update README.md
Fixed indentation
2025-03-19 15:37:50 +01:00
Alexandre1a
af73e3e4de
Update README.md
Fixed a typo
2025-03-19 15:36:31 +01:00
Alexandre1a
ed30bd50d0
Update main.go
Changed some defaults
2025-03-19 15:33:16 +01:00
5 changed files with 388 additions and 57 deletions

BIN
.DS_Store vendored Normal file

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-mac-amd64 GOOS=darwin GOARCH=amd64 go build -o dist/gosh-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -o dist/gosh-mac-arm64 GOOS=darwin GOARCH=arm64 go build -o dist/gosh-darwin-arm64
ls -lh dist/ ls -lh dist/

27
.goblin.yaml Normal file
View File

@ -0,0 +1,27 @@
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 intall` `go install`
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,7 +29,16 @@ 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 : black red purple cyan white green yellow blue All the avalable colors :
- 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 :
@ -44,3 +53,6 @@ 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)

356
main.go
View File

@ -5,8 +5,12 @@ 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"
@ -19,12 +23,19 @@ 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"`
} }
var config Config // Constantes pour la version et le nom du shell
const (
VERSION = "2.3.0"
SHELL_NAME = "GoShell"
)
var (
config Config
// Map des couleurs ANSI // Map des couleurs ANSI
var colors = map[string]string{ colors = map[string]string{
"black": "\033[30m", "black": "\033[30m",
"red": "\033[31m", "red": "\033[31m",
"green": "\033[32m", "green": "\033[32m",
@ -34,22 +45,33 @@ var colors = map[string]string{
"cyan": "\033[36m", "cyan": "\033[36m",
"white": "\033[37m", "white": "\033[37m",
"reset": "\033[0m", "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 := homeDir + "/.gosh_history" historyFile := filepath.Join(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: nil, AutoComplete: newCompleter(),
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)
@ -57,11 +79,19 @@ 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
} }
@ -70,34 +100,93 @@ func main() {
continue continue
} }
startTime := time.Now()
if err := execInput(input); err != nil { 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 // Charger la configuration depuis un fichier
func loadConfig() { func loadConfig() {
homeDir, _ := os.UserHomeDir() homeDir, _ := os.UserHomeDir()
configPath := homeDir + "/.config/gosh/" configPath := filepath.Join(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", "blue") viper.SetDefault("color", "green")
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...")
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) 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:", configPath) fmt.Println("Fichier de configuration créé avec succès:", configFilePath)
} }
} else { } else {
fmt.Fprintln(os.Stderr, "Erreur de configuration:", err) fmt.Fprintln(os.Stderr, "Erreur de configuration:", err)
@ -114,6 +203,11 @@ 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
@ -128,7 +222,12 @@ 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 {
@ -143,21 +242,55 @@ 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 {
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) 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 {
@ -166,36 +299,91 @@ 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])
case "exit": // Expansion du tilde en chemin complet du répertoire utilisateur
os.Exit(0) if args[1] == "~" || strings.HasPrefix(args[1], "~/") {
case "version": homeDir, err := os.UserHomeDir()
fmt.Println("GoShell Version 2.1.2") if err != nil {
return nil return fmt.Errorf("impossible de trouver le home: %v", err)
case "set":
if len(args) < 3 {
return fmt.Errorf("Usage: set <key> <value>")
} }
return setConfig(args[1], strings.Join(args[2:], " ")) 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 <key> <value>")
}
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)
@ -207,20 +395,54 @@ 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 avant d'attendre sa fin // Démarrer la commande
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 {
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 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 {
@ -228,7 +450,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":
@ -238,26 +460,96 @@ 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
} }