Compare commits

..

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

8 changed files with 86 additions and 523 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -8,6 +8,22 @@ on:
branches: ["main"] branches: ["main"]
jobs: jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.23.5"
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
build-multiarch: build-multiarch:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -16,25 +32,22 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: "1.24.1" go-version: "1.23.5"
- name: Build binaries for multiple architectures - name: Build binaries for multiple architectures
run: | run: |
mkdir -p dist mkdir -p dist
# Linux # Linux
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/
# Windows
# GOOS=windows GOARCH=amd64 go build -o dist/gosh-windows-amd64.exe
# GOOS=windows GOARCH=arm64 go build -o dist/gosh-windows-arm64.exe
- name: Upload binaries as artifacts - name: Upload binaries as artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@ -54,46 +67,10 @@ jobs:
name: gosh-binaries name: gosh-binaries
path: dist/ path: dist/
- name: Generate Changelog
id: changelog
run: |
CURRENT_TAG=${GITHUB_REF#refs/tags/}
# Get all tags sorted by version (descending)
ALL_TAGS=$(git tag --sort=-version:refname)
PREVIOUS_TAG=""
found_current=0
# Find the tag immediately before the current one
for tag in $ALL_TAGS; do
if [ "$found_current" -eq 1 ]; then
PREVIOUS_TAG=$tag
break
fi
if [ "$tag" == "$CURRENT_TAG" ]; then
found_current=1
fi
done
# Fallback to initial commit if no previous tag
if [ -z "$PREVIOUS_TAG" ]; then
PREVIOUS_TAG=$(git rev-list --max-parents=0 HEAD)
fi
# Generate changelog
CHANGELOG=$(git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..$CURRENT_TAG)
if [ -z "$CHANGELOG" ]; then
CHANGELOG="No changes since previous release."
fi
# Output for GitHub Action
echo "CHANGELOG<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
files: dist/* files: dist/*
body: |
**Changelog**
${{ steps.changelog.outputs.CHANGELOG }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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

@ -5,7 +5,6 @@ A Shell made in Go, for fun
- History file wich you can browse for past commands - History file wich you can browse for past commands
- Some colors - Some colors
- Config file - Config file
- PTY support for interactive commands
I'm planning to add more features later (like syntax hilighting, etc...). I'm planning to add more features later (like syntax hilighting, etc...).
I'm also learning Go by doing this project. I'm also learning Go by doing this project.
You can expect breaking changes (or code), the problem has a workaround in the commit message. You can expect breaking changes (or code), the problem has a workaround in the commit message.
@ -14,45 +13,19 @@ If not, the issue will be fixed soon (in case of a typo for example)
## Installation ## Installation
You can grab the binaries for your system and architecture, or build it yourself You can grab the binaries for your system and architecture, or build it yourself
To build it, clone the repository, cd into it and run To build it, clone the repository, cd into it and run
`go build -o bin/GoSH` 'go build -o bin/GoSH'
To test the shell and see if it suits you. 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 !
## Usage ## Usage
To use the program, just invoke it with `GoSH` To use the program, just invoke it with 'GoSH'
~~If you see a message about a config file, create `~/.config/gosh/gosh_config.toml` and populate it with the defaults written inside this repo -> [here](/defaults.toml).~~ If you see a message about a config file, create '~/.config/gosh/gosh_config.toml' and populate it with the defaults written inside this repo.
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.
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 : You can use all "console colors", listed [https://gist.github.com/kamito/704813 | here]
- black
- red
- purple
- cyan
- white
- green
- yellow
- blue
You can change the history size with `set history_size <int>`
You can change the prompt with `set prompt <promp>`
Here is some exemple of prompts :
- `[{dir}] > `
- `["Hello World"] $ `
- `"Hello" `
You can use all "console colors", listed [here](https://gist.github.com/kamito/704813)
## Know Issues
Currently there is a number of known or unkwown issues.
We can list the fact that interactive programs, like SSH or VIM work partialy.
The config has to be manualy created and populated.
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.
## ToDo
- Tab completion (for cd)

View File

@ -1,3 +0,0 @@
color = 'blue'
history_size = 1000
prompt = '[{dir}] > '

2
go.mod
View File

@ -4,9 +4,7 @@ go 1.23.5
require ( require (
github.com/chzyer/readline v1.5.1 // indirect github.com/chzyer/readline v1.5.1 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect

4
go.sum
View File

@ -2,14 +2,10 @@ github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwys
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=

451
main.go
View File

@ -2,19 +2,12 @@ package main
import ( import (
"fmt" "fmt"
"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/google/shlex"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@ -23,55 +16,24 @@ 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 (
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() { 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(), // Utilise une fonction pour générer le prompt dynamiquement
HistoryFile: historyFile, HistoryFile: historyFile, // Permet de sauvegarder et charger l'historique
HistoryLimit: config.HistorySize, HistoryLimit: config.HistorySize,
AutoComplete: newCompleter(), AutoComplete: nil, // Peut être amélioré avec l'autocomplétion
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,135 +41,69 @@ 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 {
// Mettre à jour le prompt avec le répertoire courant
rl.SetPrompt(getPrompt()) rl.SetPrompt(getPrompt())
// Lecture de l'entrée utilisateur avec édition et historique
input, err := rl.Readline() input, err := rl.Readline()
if err != nil { if err != nil { // EOF ou Ctrl+D
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
} }
// Suppression des espaces inutiles
input = strings.TrimSpace(input) input = strings.TrimSpace(input)
if input == "" { if input == "" {
continue continue
} }
startTime := time.Now() // Exécute la commande
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/gosh_config.toml"
fmt.Println("Chemin du fichier de configuration:", configPath) // Log pour déboguer
if err := os.MkdirAll(configPath, 0755); err != nil { viper.SetConfigFile(configPath)
fmt.Fprintln(os.Stderr, "Erreur lors de la création du dossier de configuration:", err)
}
viper.AddConfigPath(configPath)
viper.SetConfigName("gosh_config")
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 {
// Si le fichier n'existe pas, le créer avec les valeurs par défaut
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); 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 {
// Autre erreur de lecture du fichier
fmt.Fprintln(os.Stderr, "Erreur de configuration:", err) fmt.Fprintln(os.Stderr, "Erreur de configuration:", err)
fmt.Println("Utilisation des valeurs par défaut.") fmt.Println("Utilisation des valeurs par défaut.")
} }
} }
// Charger la configuration dans la structure Config
if err := viper.Unmarshal(&config); err != nil { if err := viper.Unmarshal(&config); err != nil {
fmt.Fprintln(os.Stderr, "Erreur de chargement de la configuration:", err) fmt.Fprintln(os.Stderr, "Erreur de chargement de la configuration:", err)
fmt.Println("Utilisation des valeurs par défaut.") fmt.Println("Utilisation des valeurs par défaut.")
} }
// Validation des valeurs
if config.HistorySize <= 0 { if config.HistorySize <= 0 {
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
@ -217,230 +113,63 @@ func getPrompt() string {
wd = "?" wd = "?"
} }
// Remplacer le chemin du home par "~"
homeDir, _ := os.UserHomeDir() homeDir, _ := os.UserHomeDir()
if homeDir != "" && strings.HasPrefix(wd, homeDir) { if homeDir != "" && strings.HasPrefix(wd, homeDir) {
wd = "~" + strings.TrimPrefix(wd, homeDir) wd = "~" + strings.TrimPrefix(wd, homeDir)
} }
// Remplacer des variables dans le prompt // Utiliser le prompt défini dans la configuration
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 // Ajouter de la couleur si configuré
if colorCode, exists := colors[config.Color]; exists { if config.Color == "blue" {
prompt = colorCode + prompt + colors["reset"] blue := "\033[34m"
reset := "\033[0m"
prompt = blue + prompt + reset
}
if config.Color == "green" {
green := "\033[32m"
reset := "\033[0m"
prompt = green + prompt + reset
} }
return prompt return prompt
} }
// Fonction pour déterminer si une commande est interactive
func isInteractiveCommand(cmd string) bool {
interactiveCommands := map[string]bool{
"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 { func execInput(input string) error {
// Remplacer les alias input = strings.TrimSuffix(input, "\n")
expandedInput := replaceAliases(input) args := strings.Split(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)
}
if len(args) == 0 {
return nil
}
// Gérer les commandes intégrées
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 1.0.0")
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 // Exécuter la commande système
cmd := exec.Command(args[0], args[1:]...) 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)
}
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)
}()
io.Copy(os.Stdout, ptmx)
} else {
// Exécuter directement les commandes non interactives
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout
return cmd.Run()
// Démarrer la commande
if err := cmd.Start(); err != nil {
return fmt.Errorf("erreur lors du démarrage de la commande: %v", err)
}
}
// Attendre la fin du processus
if err := cmd.Wait(); err != nil {
// 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 <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
@ -449,9 +178,6 @@ func setConfig(key, value string) error {
case "prompt": case "prompt":
viper.Set("prompt", value) viper.Set("prompt", value)
case "color": case "color":
if _, exists := colors[value]; !exists {
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":
intValue, err := strconv.Atoi(value) intValue, err := strconv.Atoi(value)
@ -460,96 +186,19 @@ 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)
} }
// Sauvegarder la configuration dans le fichier
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)
} }
// Recharger la configuration
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
func getAvailableColors() []string {
keys := make([]string, 0, len(colors))
for k := range colors {
if k != "reset" {
keys = append(keys, k)
}
}
return keys
}